题目信息
测试运行
直接运行什么也不会显示,大概10秒后killed
strace
$ env -i strace -fi ./tp
- 省略前面的加载library部分
- 有一个alarm(10)的时间限制(造成killed的原因)
- clone,pipe2,prctl, syscall_317(seccomp)之类的,看起来像sandbox的system call。也有一个可疑的SIGSTOP信号
checksec
防御全开
vmmap
- 没有自身的elf
- 一个非常大的mmap
开启ASLR运行
$ gdb -q ./tp -ex 'set disable-randomization off'
这次正常了,也就是说,如果关掉ASLR就不能正常运行
Stage1
- 暂时不需要考虑绕过sandbox
- sandbox部分在Stage2
- 这部分主要是pwn
- 目标是读取/home/tp/flag1
解析 - 整体构成
- 父进程(sandbox)
- 子A(sandbox)
- 子B(main,note管理类)
子B主要5个功能,内部实现是红黑树,node持有指向note的指针,note持有指向data的指针
解析 - 主操作部分
do_command
- 右侧:恢复点
- 读取command
- 根据command进行分支
- 常规的note管理类服务
command0_new
- 接受要创建的note的size
- 但不对size进行检查
- 在没有对size检查的情况下生成新note
- 搜索已经free的note,如果存在,会再次使用它
build_node_and_insert_note
- 初始化与node相连的note
- 搜索能够构建tree的node插入点
- 初始化要插入tree的node
- 将node插入tree
- 在没有检查size的情况下,分配连接到note的buffer
- new(buffer)如果失败,产生std::bad_alloc异常,调用handler
恢复点
- note使用的buffer(data)如果存在,则释放它
- note自身也释放
- 但是data和note还是连接在一起的
- 这就产生了Use After Free
command1_read
- 接收note的id,以及要读取的size
- 搜索指定id的note
- 读取note的data
command2_write
- 接收note的id,以及要写入的size
- 搜索指定id的note
- 写入note的data
command3_free
- 接收要变更为不可利用的note的id
- 搜索指定id的note
- clear used 比特位
command4_delete
- 接收要废弃的note的id
- 搜索指定id的note
- 释放node和note
攻略(stage1)
环境
创建tp用户,创建测试flag:
不需要alerm(10),因为有PIE,不能用peda的deactive,直接patch为alerm(0)
之后socat启动服务,因为有ASLR,不能使用gdbserver(禁用ASLR时,二进制的.text段会消失)。所以这次需要使用gdb attach:
终端1启动服务,终端3使用gdb attach,终端2为攻击者发送数据:
pgrep -w <filename>
可以筛选LWP(包括线程的进程)列表,之后使用tail -1
选择最后一个生成的线程,也就是只选择子B。- 但是,因为有PIE,每次attach,text的基地址都会改变
根据attach后stack上的各种值,可以计算出PIE的base:
- 通过vmmap检查PIE的base,这次是0x7fdc7bdcc000
- 根据stack上的值计算出偏移:0x7fdc7bdce34f - 0x7fdc7bdcc000= 0x234f (diff)
*$rsp-0x234f == PIE的base,set $base = *(void**)$rsp-0x234f
,即使有PIE,偏移量是不会改变的,因此我们可以通过每次的rsp计算出PIE的base
生成command文件
将禁用SIGSYS信号的stop,print,读取heap信息,计算base等命令写到文件里,再次gdb attach:
将程序各个功能封装成函数:
生成dump函数
确认UAF之前,准备好dump node和note信息的函数比较好
- 这样会非常效率
- 便于理解heap内部情况
可以看到生成了一个存在note,buffer为NULL的异常node
Pwn - 生成共有状态
利用UAF可以达到任意内存读写
- 但首先需要生成共用内存的状态
- 即存在两个指针A和B,指向同一块内存区域
1. _new(size=1)
创建了一个size为1的note,之后tree是这样的
2. _new(size=1)
- 创建一个后面用于覆盖的元素
- 作为id为0的node的父node,插入到tree中
3. _new(size=-1)
- alloc_note内部,首先生成note
- 接下来准备生成data
- 但是因为size是负数,data生成失败,malloc内部生成了thread_arena
- 刚才生成的note被free后,来自node的引用仍然存在
- fastbins与这个note相连接,arena切换,这个note已经不能被使用
4. _new(size=-1)
- 同样,先在alloc_note内部,首先生成note
- 之后,data生成失败,如果已经存在thread_arena,malloc就不会再创建新的arena
- 因为data生成失败,data被free,来自node的引用仍然存在
- bins和note相连接
5. _free(id=1)
- tree开头的节点,id=1的note设置为used=0
- 如果存在used=0的note,它的size与指定的size不一致,下次alloc_note时,先free(data),然后重新分配一次
6. _new(size=0x28)
- 因为used=0的note的id=1,它将被再次使用
- 但是size不一致,会先将id=1的datafree掉
- 之后的malloc,会从thread_arena生成data
- 因为bins中存在sz=0x28的chunk,会直接再次使用,作为id=1的data
- 这时候,id=1的data和id=3的data是共有状态
- 但是,id=3的note,实际上id信息已经破坏,因此需要修正
Pwn - heap leak
生成了共有状态
- 接下来,对id=1的data的变更操作,也会对id=3的note的data操作
- 可以做任意读写
- 如果可以指定GOT地址进行读取,就不需要考虑libc相关地址
- 也就是说,无视ASLR
- 但是因为这里同时有PIE+ASLR,我们不知道GOT(.text)的地址
- 可以利用Partial Overwrite,来leak内存
- 首先需要泄漏heap的地址(thread_arena的mapped区域)
7. _read(id=1, data=pQ(0x1337))
- 更新id=1的data
- 这会导致id=3的note更新,变成id=0x1337
- 这时,id=0x1337变成used=0
8. _new(size=0x100)
- 因为找到的used=0的note的id=0x1337,通过alloc_note对这个note再次使用
- id=0x1337,生成data
- size=0x100,不在bins里的小的size就可以
- 这时,id=0x1337的note上,生成了指向data的指针
- 这个指向data的指针,指向的是从thread_arena的mapped区域申请的chunk
9. _read(id=1, data=pQ(0x1337) + “\x70\x08”)
- 使用id=1,来覆盖指向id=0x1337的note的data的指针,只能写两个字节
- 指向0x1337的data的指针,指向的是thread_arena内部的bin上的一个chunk
- 可以看到,指向data的指针,后两个字节已经被重写
- 这次是指向mapped+0x870作为data
- 里面是mapped+0x858
10. _write(id=0x1337, size=8)
- 通过写出id=0x1337,可以泄漏mapped+0x858之类的值
- 泄漏的值减去0x858就能够知道heap的base地址
Pwn - libc leak
- 能够泄漏heap的base地址
- 附近(mapped+0x888)存在和libc相关的地址
- 从thread_arena到main_arena的next指针
- 这个也可以被泄漏
11. _read(id=1, data=pQ(0x1337) + “\x88\x08”)
- 使用id=1,来覆盖指向id=0x1337的note的data的指针,只能写两个字节
- 指向0x1337的data的指针,指向的是thread_arena的next
- next中是main_arena的地址
12. _write(id=0x1337, size=8)
- 通过写出id=0x1337,可以泄漏main_arena的地址
- 泄漏的值减去0x3be760就能够知道libc的base地址
Pwn - stack leak
- 能够泄漏libc的base地址
- 结合偏移量就能够计算出libc中各种函数地址
- 但计算出system函数并不能结束
- 因为有sandbox,system,execve之类的不能使用
- 但是heap操作必要的,mprotect/mmap这类是被允许的
- 最快的方法是,通过mprotect使heap权限为RWX,然后在heap中加载执行open()->read()->write()这样的shellcode
- 因此,策略是覆盖stack返回地址,执行mprotect()->read()这样的ROP
13. _read(id=1, data=pQ(0x1337)+pQ(libc_base+0x3c14a0))
- libc中存在指向stack的
__environ,__libc_stack_end
变量 - 这里是将id=0x1337的note的data变更为
__environ
,也就是libc_base + 0x3c14a0
14. _write(id=0x1337, size=8)
- 通过写出id=0x1337,可以泄漏stack上envp[]的地址
- 泄漏的值减去0x200就是储存read()函数返回地址的地方
Pwn - stack伪造
15. _read(id=1, data=pQ(0x1337)+pQ(ret)+pQ(len(rop)))
- 让id=0x1337的note指向ret
16. _read(id=0x1337, data=rop)
- 之后在ret附近写入ROP