2025 ACTF only_read的三种解法
| 0 | 前言
回顾2025actf,常规glibc_ctf只有一道only_read,虽然我最后成功解出来了(偷跑),但是在看了Unauth师傅的ret2dlresolve和星盟师傅的magic_gadget利用后越来越感觉自己的手法丑陋无比,所以今天在这里将三种解法都展现出来
同时十分感谢星盟师傅教会了我如何在没有csu片段的情况下利用magic_gadget重定向函数的got表
题目链接:攻防世界
| 1 | 题目分析
1 | [*] '/home/zer00ne/Desktop/ACTF/3/pwn' |
got表为Partial RELRO,说明我们可以将其重定向,没有canary和PIE也似乎指明了这是一道栈利用

用IDA打开后可以看到main函数十分简洁,我们有着很长的溢出长度(0x778字节),但是可以利用的函数只有read函数
而且elf文件中没有init函数,说明我们无法使用ret2csu进行任意参数的填充
| 2 | 解题思路
微量偏移+SROP
常规的开局
这是我的打法,集合和许多巧合和偶然性才打通的

通过gdb我们可以发现,在read函数开头的附近有个syscall,如果read的got表中被我们修改为syscall,我们就可以做更多事情
修改的方法也很简单,就是使用通过栈迁移,将main函数中read的二参修改到read的got表上,并将低8字节覆盖为0x5f(此所谓微量偏移),此时read.got->syscall,而且如果我们可以在read.got的高地址处提前布置好ROP链,我们就可以继续控制执行流
同时这也是这个手法的难点:我们布置的ROP并不是布置完就执行,而是错位的(布置好第三条链子后才开始执行第二条链子)
1 | from pwn import * |
开始布置多条ROP链
首先我们将需要使用的地址和gadget先列出来
1 | payload=b'\x00'*0x80+p64(tar+0x80-0x8)+p64(read) |
第一步肯定是栈迁移,为什么要把rbp迁移到这个位置呢?
因为迁移后调用read时,read的二参刚刚好got+0x80,也就是为我们对got 进行微量偏移后的执行流控制做准备
我们此时把ROP链发送过去
1 | payload=p64(bss+0x100-0x88)+p64(leave) #here |
我们将rbp移动到bss-0x100的位置,调用read继续布置ROP链,留意这里的read+4的布置,后面会回来
1 | s=SigreturnFrame() |
提前将SROP的Frame和/bin/sh写好,并放在特定的位置(这个位置都是一步一步调试出来的,一次性肯定写不出来)
然后再次使用栈迁移,同时调用read时,此时read的rsi为got,并在这条ROP上将leave布置好,准备将后续的执行流控制
死局?
回到此刻的RIP,看到调用read改变got后,rsi被钉在了got地址上,此时我们是无法进行SROP的

我们需要将rsi迁移到其他地方,而rsi是根据rbp决定的,可是此时我们已经无法调用read来随意栈迁移了
更糟糕的是,因为rax被用来间接传参,我们甚至没法使用read_syscall,似乎这里我们进入了死局
最后的希望!!!
我们观察此时的rbp,它被我们设置为
1 |
|
此时如果我们进行一步leave;ret,就可以将rsp移动到0x404878+8的位置
如果我们可以将SigreturnFrame提前写到这个位置,然后:
- 通过
pop rbp;ret将rbp设置为0xf+0x80,这样间接引用rax传参时,rax就会变成0xf - 将
read.plt接在末尾,此时便会执行Sigreturn,而我们已经设置好Frame
我们就可以执行execve("/bin/sh",0,0),成功getshell!!!!
最终EXP
1 | from pwn import * |
本身用来限制我们read_syscall而间接引用的rax最终反而被我们反向利用,成为了Sigreturn的关键
不得不说是一种幸运啊 :)
Magic_gadget的高阶用法
在星盟的博客上闲逛时偶然发现了这个wp,还看到了magic_gadget的身影
星盟的师傅还是太超标了: 星盟wp
前置知识
- magic_gadget

一段原本不存在于elf中,通过错位找到的gadget,可以目标地址中的数据进行加减,在我先前的博客中对此有过介绍
- gadget
1 | 0x323b3: |
在libc库中有着一段和csu非常非常相似的一段gadget,可以控制多个寄存器,只要能调用这个,我们就可以将got写上ogg
然后简简单单getshell
- start
如果我们先将rsp迁移到bss上,然后调用start函数,此时会有两个libc地址被留在bss上
先将需要用的gadget写好
1 | from pwn import * |
常规开局
1 | payload=b'a'*0x80+p64(bss+0xf00-0x18)+p64(read) |
这里我们将栈迁移到一个比较高的地址,因为不需要偏移got所以这次的ROP都是写完就用,算是难度降了一个level
至于为什么要把栈迁移到一个比较高的地址,后面会解释
开始利用
1 | payload=p64(bss+0xf00)+p64(0x401050)+b'a'*0x70+p64(bss+0xf00-0x18-0x80)+p64(leave) #start |
将返回地址改为leave;ret这样就可以执行此时输入的内容并将rsp拉到bss上,即start
此时我们便将两个libc地址印在bss上
参数的准备

分别是start的返回地址和__libc_start_main+139
返回地址我们需要覆盖所以不能用,而__libc_start_main+139和目标gadget->0x323b3的距离是
0x2a28b->0x323b3 is 0x8128 bytes (0x1025 words)
此时的rbx是0x404e80,事实上rbx会被设置为栈迁移后的RSP&0x20的位置(重点)
二者间的距离大约等于两个rbx//0x10(所以为什么要把RSP设置为一个较高的bss地址)
那剩下的微量差距该怎么办呢?—>当然是通过覆盖最后8字节将bss上的地址准确重定向到目标gadget
那//0x10该怎么处理呢?—>在使用magic gadget进行重定向时,将rbp与__libc_start_main+139错开一个地址
这样rbp中的值就是__libc_start_main+139的0x10倍
1 | payload=b'a'*0x80+p64(0x404e07+0x3d)+p64(magic)*2+p64(rbp)+p64(target-0x100)+p64(read) |
在调用magic gadget后
gadget的调用
继续填入调用调整rbp和read,在目标gadget的低地址处填满ret并将目标gadget使用p8(0xb3)覆盖最后8位
1 | one_gadget=0xef52b |
通过通过调试确定对[rax,rsp,rbx,rbp,r12,r13,r14,r15]的填装,最后再次调用magic gadget对got重定向到one gadget
同时我们对 rax进行清空操作(one gadget的约束)
最后将执行流引导到read上,完成getshell
最终EXP
1 | from pwn import * |
ret2dlresolve
田师傅的手笔,还说这是道比较极限的ret2dlresolve,几乎把所有bss都用来伪造各种结构
解析待补充(主要是主播也不会)
EXP
1 | from pwn import * |