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->syscal
l,而且如果我们可以在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 * |