前言
昨天刚刚结束的湾区杯
全场只有5解的pwn题,差点拿到3血(其实差了不少: / )
失去了AI的辅助,全身心投入逆向分析,看起来也不是件坏事
附件分析
附件可以到看雪上下载: 下载链接
本题的数据传输采取了类似protobuf
的序列化/反序列化数据包传输指令
所以我们在进行堆利用前要将自定义的vm操作
梳理清楚
数据包逆向
在主逻辑前,先是进行了对时间种子的设置
经典的伪随机数
随后是个循环读入
注意看这里读入的循环判断,是当前读入的字节(char)为负数
字符串与数字的转化
读入后进行了dump_num操作,按照一定格式把str转化为__int64
1 | __int64 __fastcall dump_num(_QWORD *size, __int64 ptr, unsigned __int64 n9_1, unsigned __int64 *len) |
可以看到在循环中,将字符转化为数字,并使用”|
“运算和”<<
“运算将数字不断拼接
只有当前读入的字符对应的ascii
为负数,才会继续读入
最终读取到对应正数的字符停止循环,将字符串对应的数字和有效字符串长度记录在size
和n9_1
变量中
下图代表了每个字节在此vm
中的意义
而且负数只会取有效数部分而忽略正负标志位(flag
)所以我们想要读入一个数字也要按照它设定的规则进行数字的传递,如下
1 | def m(n): |
必须为保证想要读入的每一位对应的字符的正负标志位为1,我们可以为每位(除了最高位)都”|
“上一个0x80(char
类型的正负标志位),最高一字节保持正常(ascii
对应为正数)
预期情况下会申请一个我们刚刚传入大小的堆块,并要求我们输入申请大小的数据
组成opcode
的结构体
随后返回main
函数调用dump_opcode
函数
1 | __int64 __fastcall dump_opcode(__int64 ptr, unsigned __int64 size, opchunk *opcode) |
首先从上个函数读入的内容中开始使用dump_num取出一个数字
根据最低三比特与第四比特的bool标志进入不同的分支
分别opcode的不同字段设置内容/设置不同内容对应的标志位
从这个函数中我们也能分析出对应的opcode结构体
1 | struct __fixed opchunk // sizeof=0x38 |
其中
opcode
用于指定进行什么堆操作(add,edit,free…)Idx
用于设置操作堆块的编号size
用于设置操作堆块的大小content_ptr
用于设置想要add/edit
的内容content_len
用于指定content
的长度每个成员都对应一个
int
类型的flag
位,用于标记此变量是否存在
根据每次从ptr中取出的num不同,可以一次性设置多个成员变量
Idx的获取算法
随后根据数据包中idx的数值,使用get_idx转化真实的idx
1 | __int64 __fastcall get_idx(char long2) |
可以看到idx的获取,是数据包中的Idx成员与随机数进行xor,然后高低位反转,作为最后真正的idx
据此我们可以逆向分析得到如何输入Idx
1 | def idx(Id): |
堆块管理系统分析
随后进入到了堆块管理系统
根据数据包中opcode成员,进入不同的处理函数
create
可以看到,进行create操作,需要传入的结构体中存在size
,content
与 content_lenth
成员存在
1 | int __fastcall create(size_t size, const void *src, size_t n0x100000_2) |
简单进行判断后,将size
,ptr
,Id
与flag低位
存入全局变量list
中
然后将content使用memcpy复制进去,并对结尾进行置零操作
由于对size与content_lenth进行了完备的检查,此处不存在溢出
我们需要发送一下数据包进行create操作的请求
1 | def add(size,content): |
edit
通过log说明,需要Idx
,size
和content(lenth)
1 | int __fastcall edit(unsigned int idx, size_t Size, const void *src, size_t ture_len) |
通过Idx在bss上寻址,找到对应堆块,将内容复制并置零
同样对长度有完备的检查,不存在溢出
以下结构进行edit
请求
1 | def edit(Id,content): |
delet/show
1 | int __fastcall delet(unsigned int n0x10) |
正常的delet操作,不存在uaf漏洞
1 | def free(Id): |
1 | int __fastcall show(unsigned int n0x10) |
正常show
1 | def show(Id): |
uaf
1 | int __fastcall uaf(unsigned int n0x10) |
出题人给了一次uaf机会,且在uaf后调用了一次可以无限栈溢出的函数overflow
以下结构请求uaf操作
1 | def uaf(Id): |
攻击思路
由于create和edit操作中都有置零操作,所以不存在泄露libc/heap信息的机会
想要泄露信息只能通过uaf后的show,那么我们就无法利用uaf中的overflow函数
之后无论是劫持IO结构体还是劫持栈上的返回值都是可以getshell/orw的
信息泄露
1 | add(0x10,b'a')#0 |
通过简单堆风水进行heap与libc的泄露
tcachebins_poison攻击
1 | free(4) |
污染next指针为_IO_list_all,将其劫持到堆上的可控地址中
伪造IO结构体,并触发其中的函数表
1 | data=heap+0x340 |
经典的house of apple2,调用system(“ sh”)getshell
完整EXP
1 | from pwn import * |
后记
本道题解数少,最重要的原因是结构体
与算法
的逆向复杂+耗费时间,且由于ctf+awdp并发进行,pwn手只能专注于一方面的攻击
无论是在ctf还是iot的学习中,我总感觉自己的逆向能力在AI的帮助下不断退化,可能这也是不少pwn手目前的真实写照
在去AI
的情况下打出这道题,真的让我很开心 : )