SetContent ORW libc-2.27
参考资料:https://www.cnblogs.com/pwnfeifei/p/15819825.html
setcontext+orw 大致可以把2.27,2.29做为两个分界点。我们先来讨论 2.27 及以下的 setcontext + orw 的写法。
在学习过 SROP 之后我们知道 pwntools 自带了一款可以控制寄存器值的工具
1 | frame = SigreturnFrame() |
frame 的本质就是依靠 SetContext 来实现的,我们可以从 IDA 中查看他的具体信息
我们可以清楚地看到 SetContent 是通过 rdi 寄存器附近的地址来设置各个寄存器的值。在 glibc-2.27 当中,我们通常从 SetContent + 53 开始使用,也就是图中 0x52180 + 53 = 0x521B5 的位置。
[CISCN 2021 初赛]silverwolf(libc-2.27 SetContent+ORW)
程序分析:
这道题可以和 lonelywolf 放在一起去分析,两者程序不同的地方在于 silverwolf 在初始化的时候开启了沙箱
1 | __int64 sub_C70() |
整理一下思路:
这道题比上一题多了沙箱机制的保护,也就是说我们需要去打一个 ORW。既然是堆上的 ORW 就需要去泄露 libc_base 和 heap_base。泄露 heap_base 是为了将栈迁移到堆上,并在堆上构造 ROP 链;libc_base 是为了寻找相应的 ROPgadgets。
EXP 思路:
封装函数:
1 | def add(size): |
在我们封装完函数看一下堆情况,发现部分 Tcachebins 已经被填满了
泄露 heap 基地址
1 | for i in range(7): |
1 | for i in range(2): |
对 index0 的堆进行 Double Free,同时清空其 fd 和 bk 指针(可以不要),让他指向自己,防止影响程序运行。
1 | show() |
通过 double free 泄露当前堆地址。
在 Linux 中,系统分配内存是以 页 (Page) 为单位的,一个标准页的大小通常是 4KB,即十六进制的 0x1000
堆内存(Heap)在初始化时,操作系统会分配一大块连续的内存空间。这块空间的起始地址一定是按页对齐的。也就是说,堆的真正开头(Heap Base)地址的最后三位十六进制数一定是 000。
- 泄露的地址:0x0000555f50014fa0 (这是堆中的某个具体位置)
- 堆的基地址:0x0000555f50014000 (这是整个堆的起点)
泄露 libc 基地址
1 | edit(p64(heap_base + 0x10)) |
- edit(p64(heap_base+0x10)): 修改 index 0(当前位于 tcache 头部)的 fd 指针为 heap_base+0x10。这个地址是 tcache_perthread_struct 结构体在堆上的位置,它掌管着整个 tcache
- add(0x78); add(0x78): 第一次分配拿到原来的 chunk,第二次分配就将 tcache_perthread_struct 当作正常的 chunk 分配出来了,此时 index 0 指向了 tcache 控制块。
1 | edit('\x00'*0x23 + '\x07') |
- edit(‘\x00’*0x23 + ‘\x07’): 篡改 tcache 控制块。将偏移 0x23 的位置改为 \x07。在 tcache_perthread_struct 中,这部分记录了各个大小的 tcache bin 的 chunk 数量。改为 7 意味着欺骗 glibc,让它以为特定大小的 tcache 已经满了。事实上,这个数改大一点也没有关系,比如 \x0ff。
- <–改成 0xff 其实我自己也不确定可不可以,lonelywolf 是可以的,本质就是欺骗 Tcachebin 数量–>
- free(); show(): 再次释放当前 chunk,因为 tcache 被伪造为已满,所以它会被放入 Unsorted Bin 中。放入 Unsorted Bin 的 chunk,其 fd 会被修改为 main_arena + 96,之后通过 show() 泄露该地址。
准备 ROP gadgets 地址和函数地址
1 | free_hook = libc_base + libc.sym['__free_hook'] |
- 由于启用了 seccomp 限制了 system(“/bin/sh”),必须使用 ORW (Open, Read, Write) 手段读文件。
- 这里计算了大量 ROP 需要用到的汇编代码片段(Gadgets)的真实内存地址。
- setcontext = … + 53: 这是 glibc 2.27 经典技巧。setcontext 函数内部包含一段可以直接根据寄存器(由 rdi 指向的内存)恢复所有程序上下文的汇编。加上偏移 53 可以避开开头的 sigprocmask 调用,直接执行 mov rsp, [rdi+0xa0] 等指令,实现栈迁移。
有一说一,gadgets 还是挺不好找的,syscall 找不到对应的,在网上看到了另一种寻找偏移方法
1 | ROPgadget --binary /home/kali/Desktop/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so | grep "rax" |
篡改 Tcache Entries 数组实现多次任意地址分配
1 | payload = b'\x02'*0x40 |
- 此时 index 0 依然指向 tcache_perthread_struct。
- b’\x02’*0x40: 把所有大小的 tcache bin 的计数器都设置为 2。
- 接下来的内容覆盖了 tcache_perthread_struct 中的 entries 数组,即不同大小的 free chunk 的链表头指针
- 通过这样构造,后续申请特定大小的内存时,会直接返回我们伪造的任意地址。例如,申请大小为 0x20 的 chunk 时,malloc 会直接把 __free_hook 的地址交给我们。
部署 ORW 链与触发 setcontext
1 | # 构造 ORW ROP 链 |
- orw = …: 用汇编指令构造了相当于 open(“./flag”, 0) -> read(fd, buf, 0x20) -> write(1, buf, 0x20) 的 ROP 执行流。
- add 与 edit 配合: 这几步将 ROP 链、目标文件路径(./flag)、以及触发环境精确放置到了前面规划好的堆地址中,并将 __free_hook 劫持为了 setcontext。
- free(): 触发点!调用 free(chunk) 时,因为 __free_hook 被覆盖,实际上执行的是 setcontext(chunk_addr)。此时 rdi 指向正在释放的 chunk,setcontext 会将程序栈指针 (rsp) 劫持到我们放置在 heap_base + 0x3000 的 ROP 链处,并开始依次执行 Open、Read、Write 读取并打印出 flag。
- io.interactive(): 交互模式,将控制权交还给用户,准备在屏幕上接收打印出来的 flag。
总 EXP:
1 | #!/usr/bin/env python |
更新: 2026-04-20 15:58:44
原文: https://www.yuque.com/idcm/wnemg9/wimy9lqmwh06ybs9