首页
社区
课程
招聘
[原创]CTF 逆向:基础 VM 的三大解法剖析
发表于: 2025-3-3 20:54 10136

[原创]CTF 逆向:基础 VM 的三大解法剖析

2025-3-3 20:54
10136

前几天,一位学弟发了道很基础的vm题目给我,我自己想用多种思路来解,中间碰壁多次,于是想着写一篇博客分享下碰壁过程以及关于vm题型我自己的一些解法

程序大多会有自己一套循环的执行程序流
大致长相为

其中opcode一般情况是由操作数和操作组成的一大串数据(通常由成百上千个byte组成)
说白了就是其中的加密过程很多,我们需要模拟执行流

关键定位:虚拟指令分发器(opcode)
加密逻辑:找到他的循环加密
模拟重建:模拟执行流进行爆破或者解密

将整个执行流程保存下来,倒着用enc去回到flag

模拟加密过程,使用Z3约束求解来得到flag

因为单字节或者双字节的缘故,所有可能也不过那么几样,我们通过一些手段可以实现爆破程序得到flag

2024年强网杯就有一道vm,对我来说很难
找不到opcode和中间的执行流
这种题我目前的想法也只会无脑的爆破程序,不出也不会了,硬看经常大脑卡死
(大佬轻松手撕)

解题前放两位大佬关于vm的博客,我也是跟着学的~
upsw1ng佬关于vm的博客
swdd佬关于利用frida去插桩爆破的博客

因为前面两种方法都要去提取opcode,所以放一起了
我们以isctf2022年的一道很基础的vm来进行讲解
(题目已经上传了)
图片描述
很纯粹的vm,不带任何杂质
一眼看见了程序加密逻辑
图片描述
同时可以看见i是我们的opcode,i是由v8赋值而来,我们再看一眼v8
图片描述
双击进去
图片描述
基本上是opcode无疑
这里我们可以选择第一个变量,shift+8提取opcode
一共24个变量,一个变量16 byte
图片描述
这里多提取一点以防万一
当然这里也可以使用idapython来提取,我就不过多赘述了
贴个脚本

提取了opcode我们再去复现执行流
这里有个坑了,我贴上最后的代码片段大家可以对比下
图片描述

可以看见很多地方不一样,至于原理我就不是很懂了,有佬知道可以解答下吗~
具体解密流程建议大家去看汇编代码
图片描述
现在opcode和执行流程有了,就是我们的求解方法了
这里衍生出了两张,一种是直接手撕逆回去,因为+-^都是可以逆的

先贴exp

这里因为异或优先级的原因,所以我选择把opcode存起来,然后从后往前进行解密
图片描述
中间打印的那些,只是为了我当时方便去调试

这里有两个异曲同工的解法
首先推荐的是第一种

这里有个巨大的坑,卡了我十个小时,因为约束求解多项式的原因,优先级问题,我们在用flag去进行encode的时候,必须要创建一个副本,不然会因为符号变量原因什么的,导致中间那个&0xff和^的优先级会混乱
像下面这样
图片描述
所以一定要encoded_flag = encode(flag, opcode)
flag副本去solver

当然你也可以一个一个去打印出来,这样方便你去调试,这道题的wp就是这样做的,我也不水字数了,大家可以自己去看看

这里不会的师傅一定去看看上面给大家推荐的swdd的博客,我就直接贴脚本了

frida代码

报告大帅!解题完毕!
图片描述

while(i<len(opcode)):
    if(opcode[i]==0x00):
        i+=1
    elif(opcode[i]==0x01):
        i+=2
    elif(opcode[i]==0x02):
        i+=3
while(i<len(opcode)):
    if(opcode[i]==0x00):
        i+=1
    elif(opcode[i]==0x01):
        i+=2
    elif(opcode[i]==0x02):
        i+=3
import idaapi
import idc
start_address = 0x7A31D0
data_length = 16 * 25  # 略微多提取,一定要提取正确,不然后面很痛苦
# 将字节对象转换为十六进制字符串
with open("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt", "a") as f:
    for i in range(0,data_length):
        f.write(hex(idc.get_wide_byte(start_address+i)))
        f.write("\n")
import idaapi
import idc
start_address = 0x7A31D0
data_length = 16 * 25  # 略微多提取,一定要提取正确,不然后面很痛苦
# 将字节对象转换为十六进制字符串
with open("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt", "a") as f:
    for i in range(0,data_length):
        f.write(hex(idc.get_wide_byte(start_address+i)))
        f.write("\n")
def encode(flag, opcode):
    encoded_flag = flag.copy() 
    i = 1
    while i < len(opcode):
        op_type = opcode[i - 1]
        target = opcode[i]
        value = opcode[i + 1]
        if op_type == 1:
            writefile(f"flag[{target}] = flag[{target}] + {hex(value)};")
            i += 3
        elif op_type == 4:
            writefile(f"flag[{target}] = flag[{target}] - {hex(value)};")
            i += 3
        elif op_type == 5:
            writefile(f"flag[{target}] = flag[{target}] ^ {hex(value)};")
            i += 3
        else:
            break 
    return encoded_flag
def encode(flag, opcode):
    encoded_flag = flag.copy() 
    i = 1
    while i < len(opcode):
        op_type = opcode[i - 1]
        target = opcode[i]
        value = opcode[i + 1]
        if op_type == 1:
            writefile(f"flag[{target}] = flag[{target}] + {hex(value)};")
            i += 3
        elif op_type == 4:
            writefile(f"flag[{target}] = flag[{target}] - {hex(value)};")
            i += 3
        elif op_type == 5:
            writefile(f"flag[{target}] = flag[{target}] ^ {hex(value)};")
            i += 3
        else:
            break 
    return encoded_flag
import z3
def getfile(file_path):
    opcode = []
    with open(file_path, 'r') as file:
        for line in file:
            opcode.append(int(line.strip(), 16))
    return opcode
def enopcode(opcode):
    read_opcode=[]
    i = 1
    while i<len(opcode):
        while True:
            while opcode[i - 1] == 1:
                read_opcode.append(opcode[i])
                read_opcode.append(1)
                read_opcode.append(opcode[i+1])
                i += 3
            if opcode[i - 1] != 4:
                break
            read_opcode.append(opcode[i])
            read_opcode.append(2)
            read_opcode.append(opcode[i+1])
            i += 3
        if opcode[i - 1] == 5:
            read_opcode.append(opcode[i])
            read_opcode.append(3)
            read_opcode.append(opcode[i+1])
            i += 3
        else:
            break
    return read_opcode
def deopcode(value,opcode,number):
    for i in range(len(opcode)-3,-1,-3):
        if opcode[i]==number:
            if opcode[i+1]==1:
                value-=opcode[i+2]
                print(f'flag[{number}]-={hex(opcode[i+2])}')
            elif opcode[i+1]==2:
                value+=opcode[i+2]
                print(f'flag[{number}]+={hex(opcode[i+2])}')
            elif opcode[i+1]==3:
                value^=opcode[i+2]
                print(f'flag[{number}]^={hex(opcode[i+2])}')
    return value&0xff
if __name__ == '__main__':
    oriopcode = getfile("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt")
    flag_enc = [
        0x65, 0xE2, 0x57, 0x60, 0xCE, 0x1E, 0xE1, 0x5C, 0x4B, 0x4B,
        0x23, 0x6D, 0x8C, 0xC2, 0xBC, 0x58, 0x84, 0x92, 0x7E, 0x8C,
        0x43, 0xDB, 0x15, 0x71, 0x97, 0x4A, 0xE3, 0xC4, 0x1F, 0x7C,
        0xC2, 0xFD
    ]
    opcode=enopcode(oriopcode)
    flag=[]
    for i in range(0,len(flag_enc)):
        flag.append(deopcode(flag_enc[i],opcode,i))
    print(''.join(chr(flag[i]) for i in range(len(flag))))
import z3
def getfile(file_path):
    opcode = []
    with open(file_path, 'r') as file:
        for line in file:
            opcode.append(int(line.strip(), 16))
    return opcode
def enopcode(opcode):
    read_opcode=[]
    i = 1
    while i<len(opcode):
        while True:
            while opcode[i - 1] == 1:
                read_opcode.append(opcode[i])
                read_opcode.append(1)
                read_opcode.append(opcode[i+1])
                i += 3
            if opcode[i - 1] != 4:
                break
            read_opcode.append(opcode[i])
            read_opcode.append(2)
            read_opcode.append(opcode[i+1])
            i += 3
        if opcode[i - 1] == 5:
            read_opcode.append(opcode[i])
            read_opcode.append(3)
            read_opcode.append(opcode[i+1])
            i += 3
        else:
            break
    return read_opcode
def deopcode(value,opcode,number):
    for i in range(len(opcode)-3,-1,-3):
        if opcode[i]==number:
            if opcode[i+1]==1:
                value-=opcode[i+2]
                print(f'flag[{number}]-={hex(opcode[i+2])}')
            elif opcode[i+1]==2:
                value+=opcode[i+2]
                print(f'flag[{number}]+={hex(opcode[i+2])}')
            elif opcode[i+1]==3:
                value^=opcode[i+2]
                print(f'flag[{number}]^={hex(opcode[i+2])}')
    return value&0xff
if __name__ == '__main__':
    oriopcode = getfile("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt")
    flag_enc = [
        0x65, 0xE2, 0x57, 0x60, 0xCE, 0x1E, 0xE1, 0x5C, 0x4B, 0x4B,
        0x23, 0x6D, 0x8C, 0xC2, 0xBC, 0x58, 0x84, 0x92, 0x7E, 0x8C,
        0x43, 0xDB, 0x15, 0x71, 0x97, 0x4A, 0xE3, 0xC4, 0x1F, 0x7C,
        0xC2, 0xFD
    ]
    opcode=enopcode(oriopcode)
    flag=[]
    for i in range(0,len(flag_enc)):
        flag.append(deopcode(flag_enc[i],opcode,i))
    print(''.join(chr(flag[i]) for i in range(len(flag))))
import z3
 
def getfile(file_path):
    opcode = []
    with open(file_path, 'r') as file:
        for line in file:
            opcode.append(int(line.strip(), 16))
    return opcode
 
def encode(flag, opcode):
    encoded_flag = flag.copy() 
    i = 1
    while i < len(opcode):
        op_type = opcode[i - 1]
        target = opcode[i]
        value = opcode[i + 1]
         
        if op_type == 1:
            # 加法操作后截断为 8 位
            encoded_flag[target] = (encoded_flag[target] + value) & 0xFF
            print(f"flag[{target}] += {hex(value)}")
            i += 3
        elif op_type == 4:
            # 减法操作后截断为 8 位
            encoded_flag[target] = (encoded_flag[target] - value) & 0xFF
            print(f"flag[{target}] -= {hex(value)}")
            i += 3
        elif op_type == 5:
            # 异或操作后截断为 8 位
            encoded_flag[target] = (encoded_flag[target] ^ value) & 0xFF
            print(f"flag[{target}] ^= {hex(value)}")
            i += 3
        else:
            break  # 未知操作类型退出循环
    return encoded_flag
 
if __name__ == '__main__':
    opcode = getfile("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt")
    flag_size = 32
    flag = [z3.BitVec(f'flag[{i}]', 8) for i in range(flag_size)]  # 使用 8 位 BitVec
    flag_enc = [
        0x65, 0xE2, 0x57, 0x60, 0xCE, 0x1E, 0xE1, 0x5C, 0x4B, 0x4B,
        0x23, 0x6D, 0x8C, 0xC2, 0xBC, 0x58, 0x84, 0x92, 0x7E, 0x8C,
        0x43, 0xDB, 0x15, 0x71, 0x97, 0x4A, 0xE3, 0xC4, 0x1F, 0x7C,
        0xC2, 0xFD
    ]
     
    solver = z3.Solver()
    encoded_flag = encode(flag, opcode)
     
    # 添加约束:每个字节最终等于 flag_enc
    for j in range(len(flag_enc)):
        solver.add(encoded_flag[j] == flag_enc[j])
    for i in solver.assertions():
        print(i)
    if solver.check() == z3.sat:
        model = solver.model()
        result_flag = [model.evaluate(flag[i]).as_long() for i in range(flag_size)]
        print(result_flag)
    else:
        print("No solution found.")
# a=[98, 48, 98, 51, 101, 97, 97, 57, 55, 56, 51, 102, 54, 49, 57, 101, 49, 49, 98, 48, 97, 52, 48, 54, 52, 48, 50, 53, 48, 49, 56, 97]
# for i in a:
#     print(chr(i),end='')
import z3
 
def getfile(file_path):
    opcode = []
    with open(file_path, 'r') as file:
        for line in file:
            opcode.append(int(line.strip(), 16))
    return opcode
 
def encode(flag, opcode):
    encoded_flag = flag.copy() 
    i = 1
    while i < len(opcode):
        op_type = opcode[i - 1]
        target = opcode[i]
        value = opcode[i + 1]
         
        if op_type == 1:
            # 加法操作后截断为 8 位
            encoded_flag[target] = (encoded_flag[target] + value) & 0xFF
            print(f"flag[{target}] += {hex(value)}")
            i += 3
        elif op_type == 4:
            # 减法操作后截断为 8 位
            encoded_flag[target] = (encoded_flag[target] - value) & 0xFF
            print(f"flag[{target}] -= {hex(value)}")
            i += 3

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-3-3 22:41 被namename123编辑 ,原因: 有个没有加载出来的图片
上传的附件:
收藏
免费 5
支持
分享
最新回复 (7)
雪    币: 349
活跃值: (10633)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
2
shift+8 还纳闷啥快快捷键.一看 尼玛不就是 数组 *号键 
2025-3-4 17:44
0
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
TkBinary shift+8 还纳闷啥快快捷键.一看 尼玛不就是 数组 *号键[em_056]
笑死,我看了半天也在下这啥快捷键。。。
2025-3-5 09:32
0
雪    币: 349
活跃值: (10633)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
4
逆向小玖 笑死,我看了半天也在下这啥快捷键。。。
2025-3-5 09:39
0
雪    币: 239
活跃值: (195)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
TkBinary shift+8 还纳闷啥快快捷键.一看 尼玛不就是 数组 *号键[em_056]
我是菜狗~只知道这样按可以
2025-3-5 12:34
0
雪    币: 824
活跃值: (1139)
能力值: ( LV5,RANK:73 )
在线值:
发帖
回帖
粉丝
6

感谢楼主分享,这里也补充一种方法
大部分的VM题目的opcode均模拟汇编指令,或者可以用一条或几条x86指令等价表示。所以可以识别出每一条opcode对应的x86指令,将其转化为对应的x86汇编,用编译出bin文件,最后IDA反编译bin文件得到伪代码。在一些复杂算法的VM上这种方法的还原效果会比较好
如2021年L3HCTF的double-joy: L3HCTF double-joy引出的解VM类题型新思路

最后于 2025-3-11 12:10 被Qfrost编辑 ,原因:
2025-3-11 12:09
1
雪    币: 239
活跃值: (195)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
Qfrost 感谢楼主分享,这里也补充一种方法大部分的VM题目的opcode均模拟汇编指令,或者可以用一条或几条x86指令等价表示。所以可以识别出每一条opcode对应的x86指令,将其转化为对应的x86汇编,用编 ...
佬!
2025-3-11 20:27
0
雪    币: 580
活跃值: (491)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
Qfrost 感谢楼主分享,这里也补充一种方法大部分的VM题目的opcode均模拟汇编指令,或者可以用一条或几条x86指令等价表示。所以可以识别出每一条opcode对应的x86指令,将其转化为对应的x86汇编,用编 ...
膜拜!!!感谢分享
2025-3-15 11:21
0
游客
登录 | 注册 方可回帖
返回