题目信息

https://github.com/ctfs/write-ups-2015/blob/master/plaidctf-2015/pwnable/tp/problem_414d338fffb41107a3cf70bd0a7feffe.elf?raw=true

测试运行

直接运行什么也不会显示,大概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

stage1的exploit