tctf2019 Elements & zerotask & babyaegis
- https://github.com/ray-cp/ctf-pwn/tree/master/0ctf2019
- http://blog.leanote.com/post/xp0int/[Pwn]-zerotask-Cpt.shao
这次比赛只做出了一道re。。赛后复现了两道pwn,记录一下。
Elements
这道题目的是输入一串字符满足一定的条件,成功输出flag即为成功。
这个题目一开始做的时候就知道输入的字符串会经过两个数组的转换,第一个数组转换是大写字母转换为小写字母(gdb调试可以看到其数组具体操作:下面是数组截取的一部分)。
1 | 0x7f601c0be7a0 <_nl_C_LC_CTYPE_tolower+704>: 0x0000003100000030 0x0000003300000032 |
第二个数组我们分析了半天。。。其实就是将每段字符转为16进制,例如’aabb’ -> 0xaabb。。。然而因为不够敏感,我们又对那个数组进行了详细的分析,分类,再慢慢推敲才看出来是怎么一回事。。下面是整个数组,做个记录,以后别犯傻:
1 | 0x7f934e7215e0 <_nl_C_LC_CTYPE_class+256>: 0x0002000200020002 0x0002000200020002 0x00 |
我是真的做了分析。。不然也不会做这么久,是真的菜,不信你看我的傻逼分析操作:
1 | 0x0002 28 |
好了回归正题,我们一起来分析一下这个题目,这个题目的逻辑很简单,是只有一个main函数的程序,首先输入的字符其实是44位就行,而且要求前面5位一定为 “flag{” 最后1位一定为 “}” ,然后经过数组__ctype_tolower_loc
会把字符串中大写字母转换为小写字母,程序中操作如下:
1 | # 大写字母转换为小写字母 |
然后是strtok()函数,利用 ” - “ 字符来进行分割,简单分析可以看出flag形式应该为flag{xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxx}
这种形式。然后就到了对这个__ctype_b_loc
数组的分析。
可以查询相关这个数组的资料,可以发现:
1 | enum |
程序的操作如下:
1 | if ( *v8 ) |
好了,知道这个数组是将每段字符转为16进制的了,进入下一步,进行了好几个SSE2,但经过调试,好像并没有什么变化(不知道干啥的),所以略过。其中后面strtok()函数取后面几段赋值给v23 & v24 & v25
1 | v17 = (__m128i)_mm_sub_pd( |
然后最重要的一部分来了
1 | if ( v9 > 2 || !v18 ) |
这个东西其实就是几个方程,如下:

然后其中最重要的可能是一步化简?就是4a^2b^2 - (a^2 + b^2 - c^2)^2 = (a+b+c)(a+b-c)(c-a+b)(c+a-b)
然后可以得到下列两个方程,再用sympy求解即可得到结果:

1 | from sympy import * |
1 | from pwn import * |
zerotask
1 | +0 data_point # 指向一块堆上的内存,表示待加密或待解密的数据 |
程序功能简介:
- 添加任务功能,程序首先分配一块0x70大小的内存用来存放task结构体,接着让用户输入key、iv以及data等信息,并完成加解密上下文的初始化。这里的输入数据功能比较特别,如果输入的内容没有达到要求的长度,程序会一直等待输入
- 删除任务功能,根据用户输入task的id来确定删除哪一个任务,遍历任务链表来查找并free掉相关内存
- 运行任务功能(go),该功能会启动一个线程来执行,根据结构体中的内容来进行加解密操作,一共有三次执行该操作的机会
程序漏洞分析
程序的漏洞在于:启动线程来执行任务时,在最开始处sleep了2秒,接着进行正常的加解密操作。因为是启动的线程,导致主线程的运行仍然不受影响,这样通过适当的利用可以控制task结构体的内容,进而劫持程序的运行。在读取结构体信息(指针)后创建线程,并在线程函数里进行的sleep(2),因此可以利用这个特点,产生一个条件竞争漏洞,在sleep(2)的时候,加解密之前就把该已经读取的指针里面的内容改写掉,因为线程与主进程是共享内存的,所以才产生了这样一个条件竞争漏洞,然而恰好其中的UAF可以配合此漏洞进行利用。
1 | int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, |
调试方法:把go()注释掉来调试。
ADD
每add一次就会有四段新的堆块产生:
1 | 0x55f3dc14a730 0x0 0x80 Used None None |
0x80:
1 | gdb-peda$ x /30xg 0x5586bd935270 |
对应于结构体:
1 | +0 data_point # 指向一块堆上的内存,表示待加密或待解密的数据 |
0xb0: task结构体里面存放着EVP_CIPHER_CTX_ptr
这样一个指针(也就是上面结构体中+0x58的位置),该指针指向的结构体如下:大小(0xb0)
1 | gdb-peda$ x /30xg 0x5586bd9352f0 |
对应的结构体为:
1 | struct evp_cipher_ctx_st { |
结构体的第一个字段指向的仍然是一个结构体:
1 | struct evp_cipher_st { |
在内存中大概长这个样子:
1 | gdb-peda$ x /30xg 0x00007f1c95e9f620 |
还有一个EVP_CIPHER_CTX
对象创建的堆块 (0x110大小)和根据DATA_SIZE分配的堆块(大小<0x1000)。
观察上面结构体中有多个指向函数的虚表指针,所以利用的思路就是自己伪造上述的两个结构体,将evp_cipher_st
结构体中的指针指向one_gadget
即可getshell
。伪造起来也很容易,我们可以完全控制的就是加解密数据的内容,而且知道了堆地址,只需要修改指针指向我们伪造的这两个结构体即可。
泄露地址解释
这里泄露地址的方法主要是利用UAF,在go()之后,把堆块释放掉,也就是把data变为fd
,这样一来如果可以正常把数据加密那得到的加密结果我们再对密文进行解密就可以得到明文(堆地址),其中需要注意的是要把确认EVP_CIPHER_CTX
结构体是一个有效的结构体才能进行有效的加密,所以这里需要再add数据来对空的0xb0大小的结构体进行填充,确保没有使用控指针的情况。而泄露libc地址同理,只是把unsortedbin来free掉而已。
调试过程
调试准备:
- 关闭ASLR
echo 0 > /proc/sys/kernel/randomize_va_space
- 两个断点
bb 0x141D
ADD &bb 0x1521
delete
然后我们来看一下操作:
1 | add_task(0, 1, 'test', '1234', 8, 'AAAA') |
删除完之后,得到的tcache_entry链表如下:
1 | (0x20) tcache_entry[0](5): 0x5586bd935e40 --> 0x5586bd935be0 --> 0x5586bd935980 --> 0x5586bd935720 --> 0x5586bd9354c0 |
查看id=2的task的结构:
1 | gdb-peda$ x /30xg 0x5586bd935730 |
然后查看数据段内容,可以看到他的内容已经变成了一个堆的地址,如果在delete之前go一下,这样就可以在go之后,加解密之前把,data的数据偷换为指针内容:
1 | gdb-peda$ x 0x00005586bd9354e0 |
1 | gdb-peda$ x /30xg 0x00005586bd9357c0 |
但是这里需要注意的是后面两个add的作用就是为了0x5586bd9357c0
的堆块内容填回去(free之后会把EVP_CIPHER_CTX
的堆块free掉并清空),第一个add就把两个0xb的空闲块分配走了,第二个add就把倒数第三个也就是id=2的时候指向的大小为0xb的EVP_CIPHER_CTX
堆块分配出去,这样加解密的时候取结构体才不会读到空指针。
1 | add_task(5, 1, 'test', '1234', 0xa0, 'FFFF') # get twices 0xb0 |
此时的tcache_entry链表:
1 | (0x20) tcache_entry[0](4): 0x5586bd935be0 --> 0x5586bd935980 --> 0x5586bd935720 --> 0x5586bd9354c0 |
可以看到0x5586bd9357c0
堆块已经被分配出去了
同理泄露libc
1 | add_task(7, 1, 'test', '1234', 0x1000, 'HHHH') |
pause()之后断点断下来之后,找到数据存放的地址
1 | gdb-peda$ parseheap |
1 | gdb-peda$ x /30xg 0x55dd9adc2f70 |
可以这样寻找原始的task结构体:
1 | gdb-peda$ find 0x55dd9adc2f80 heap |
id = 7 的 task
1 | gdb-peda$ x /30xg 0x55dd9adc2730 |
id = 8 的 task
1 | gdb-peda$ x /30xg 0x558a704b94d0 |
伪造之后的结构体,刚好把id=7的那个结构体覆盖掉(而id=8的task变成id=9的task,这时分配0x70大小堆块,刚好符合堆块大小是0x80这样就会把id=7的task结构体对应堆块分配给id=9的数据块了,这样就能覆写id
=7的task结构体):
1 | gdb-peda$ x /30xg 0x55f585578730 |
所以这样就能成功泄露libc地址了。
利用过程
利用过程如下:
1 | add_task(10, 1, 'test', '1234', 8, 'JJJJ') |
上面的id=12也就是覆盖id=10的task结构体为特定内容,伪造EVP_CIPHER_CTX
指针为heap + 0x2350
,其中指向内容为one_gadget,然后go中sleep(完之后),调用到evp_cipher_st
结构体里面的虚表函数的时候就直接getshell了
咱们来看下调试过程:
详细脚本以及部分解释:
然后最后就是对两个结构体的伪造,利用前面free掉的堆块进行UAF利用。
1 | from pwn import * |
tcache学习
babyaegis
- AddressSanitizer: A Fast Address Sanity Checker:https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf
- https://github.com/google/sanitizers/wiki/AddressSanitizer
- https://github.com/Microsoft/compiler-rt/blob/master/lib/asan/asan_allocator.cc

1 | ShadowAddr = (Addr >> 3) + Offset; |
- 所以说只要让k等于0,那我们就相当于绕过了asan,这样就能征程利用漏洞了,然后选项666可以做到
- k=0的地方就是指针或者说是不可控制的位置,一旦这个地方被控制了,也就意味着那个地方为非0。
- 非0至少可以理解为不可写的地方。
漏洞分析
这个题目主要难度在于加了asan检查,程序在update那里存在offbyone,但是普通的堆块内容跟结构体是相差很远的,所以按理来说并没有什么用,但是问题是当你分配数据大小为0x10的时候会发现数据段跟结构体是相邻的,这样一来就可以修改结构体了,然后secret(0x0c047fff8004)
主要是为了绕过asan对0x602000000020
这个地址的检查,以便后面可以修改覆盖这个地方的值。然后最重要的就是改成什么了,这个主要看asan源码可以看到关于这个结构体的信息:
1 | // The memory chunk allocated from the underlying allocator looks like this: |
根据所查的资料,进行一系列的update修改,后面再delete的时候才算成功把堆块释放,否则会因为asan的特性free会不释放堆块,这样后面再add的时候就不能覆盖到原来的堆块上了。
1 | update_note(0, "c" * 0x10 + "\x02\x02", str(0xff0fffffff000041)) |
因为前面update时候刚好让数据段覆盖到了结构体,再经过delete之后再add时的结构体与数据刚好错位,也就是add时候的数据段也就是前面的结构体,这样就能UAF,就可以泄露数据和任意地址写了;最后我们泄露出栈地址,写返回地址就可以getshell了,当然其中还有一些小细节问题就需要多调试。
详细脚本解释(带调试信息)
1 | from pwn import * |
脚本
1 | from pwn import * |