ogeek ctf 2019 win pwn babyheap 详解
TOC
- 1. babyheap
- 2. 漏洞点
- 3. leak heap header
- 4. Windows heap unlink
- 5. 泄露地址信息
- 6. 查询peb和teb,泄露StackBase
- 7. 寻找main_ret_addr
- 8. ROP拿shell
- 9. 完整脚本
一道很经典的 win pwn ,根据出题人的意思,该题是受WCTF
的LazyFragmentationHeap
启发而得来的。
源程序下载:babyheap.zip 。
在这里先感谢出题人m4x
和WCTF
的一位大佬Angelboy
的指点。
babyheap
源码:oGeekCTF2019_babyheap_src.zip。
漏洞点
程序流比较简单,直接就是polish
存在堆溢出。
void polish() |
leak heap header
Windows 10 使用的是Nt heap
,对于使用中的堆块和free的堆块头部都会用_HEAP->Encoding
进行异或加密,用来防止堆溢出,所以我们要先leak出free的堆块头部加密后的内容,否则我们堆溢出时会被check。
sh.recvuntil('gift : 0x') |
这里特别要注意的是,使用中的heap 头部和 free 的heap 头部并不相同,所以一定不能leak错了。
Windows heap unlink
这个以前从来没有见过,和Linux的unlink差别挺大的,原理可以用下面的代码简单描述一下:
|
其作用就是让ptr[2]
指针指向自己,这个和Linux有点像。
destroy(4) |
然后再用后门功能使得unlink
后的指针可以进行编辑。
sh.sendlineafter('choice?\r\n', '1337') |
完成这些操作后,我们就能利用index_2
来操作index_3
指针的指向,实现任意地址读写。
泄露地址信息
这个和Linux 差不多,只不过Linux 是 got 表,而 Windows 是 iat 表。至于iat具体在哪个dll动态库里面,这个可以用IDA或者PE工具来查看。
其查询结果如下所示:
.idata:00403000 ; Imports from KERNEL32.dll |
我们会在后面需要ntdll
的地址,而ntdll
并不在babyheap
的导入表中,所以我们需要从KERNEL32
中进行泄露。
# leak dll base addr |
查询peb和teb,泄露StackBase
当我么拥有了任意读写能力,该怎么控制程序流呢?
由于 Windows 的 Nt heap 似乎并没有 hook 之类的,所以我们只能利用传统的栈溢出来控制程序流,但是我们该如何获知栈地址呢,根据Angelboy
师傅的提示,TEB中会储存栈基地址。
如下所示:
0:000> !teb |
对于 Windows 的程序来说,每个进程都有一个PEB
,每个线程都有一个TEB
,而且他们的相对偏移一般是固定的。那么我们只要知道PEB
的地址,就可以计算出TEB
的地址,从而泄露StackBase
。
但是PEB
的地址又该怎么查询呢,在ntdll!PebLdr
附近,有一个值可以泄露出PEB
的地址,其调试结果如下:
0:000> r $peb |
从上面可以看到ntdll!PebLdr
向上偏移52
字节的地方存储着PEB
地址的信息,而且这个地址信息和PEB
地址的偏移总是0x21c
,所以我们可以利用该地址信息来计算出PEB
的地址。
ntdll_PedLdr_addr = ntdll_addr + 0x120c40 |
又因为PEB
和TEB
的地址的偏移是固定的,我们可以计算出babyheap
线程的TEB
的地址然后泄露出该线程的栈基地址。
其偏移结果如下:
0:000> r $peb |
查看之前,要先把线程调成babyheap
的,通过查看计算出他们的偏移是0x3000
。
对应的脚本如下:
# leak StackBase |
寻找main_ret_addr
我们虽然知道了StackBase
,但是由于受到ASLR
影响,main函数的返回地址对于StackBase
来说并不是固定偏移的,这点和Linux
是一样的,那么我们该怎么查找main_ret_addr
的返回地址呢?
由于程序的地址信息我们都已经泄露出来了,所以我们根据偏移是可以计算出main_ret_addr
这个地址里储存的内容的,而且我们原本就有任意地址读的能力,那么我们可以直接读取栈,直到找到main_ret_addr
这个地址里储存的内容,这样我们便可以确定其就是main_ret_addr
。
这里提一下我犯得一个错误,开始时我尝试将整个栈一次性全部读取下来,但是不仅花的时间长,而且还总是crash,最后我想了一个办法,由于
main_ret_addr
地址是低二位对齐的,所我们只要读取地址低二位为0的地址就可以了,而且一定要从后往前读。
在寻找之前,我们要先把g_inuse
全部设置为1,以加快查找速度。
polish(2, 4, p32(g_inuse_addr + 3) + '\n') |
由于栈比较大,所以整体读取需要的时间还是比较长的,需要耐心等待,如果超时可以重新试一遍,因为main_ret_addr
本身就是不固定的,所以读取时间或长或短。
ROP拿shell
读到main_ret_addr
之后就是正常的ROP了。
polish(2, 0x10, p32(main_ret_addr) + 'cmd.exe\0\n') |
完整脚本
#!/usr/bin/python2 |
运行实例:
ex@Ex:~/ogeek2019/pwn/babyheap$ python my_exp.py |