首页
社区
课程
招聘
[原创] NCTF2023 逆向题目ezVM 题解
发表于: 2023-12-26 10:17 15160

[原创] NCTF2023 逆向题目ezVM 题解

2023-12-26 10:17
15160

在NCTF2023的题目中出现了一道VM类型的题目,针对该题本人采用Frida对程序进行插桩,利用侧信道攻击的方法爆破出flag。

首先分析该程序,是一个64bit的程序,并且加了UPX壳使用 -d 自动脱壳即可。
然后将程序放入IDA中逆向分析
图片描述

图片描述
比较传统的读取操作码以及操作数,根据不同操作码模拟不同的虚拟指令。
图片描述
在0xFB处程序利用putchar输出回显信息
图片描述
运行程序后,程序也会提示flag的格式以及长度

传统思路的话肯定是利用IDApython或者手撕出所有模拟的代码,然后进行逆向分析求解。当然这种方法适合逆向功底比较深厚的选手!

但是由于这种自设计的虚拟机模拟的局限性以及作者对选手的关爱,加密算法一般都是单字符加密的。
单字符加密的话由于密文空间很小(一般都是从printable的表中枚举),将可能的字符经过正向的加密,然后与密文进行比较来判断是否为正确的字符。所以针对这个题目可以采取爆破的方法,不断枚举flag的每一位字符,然后通过运行结果来判断加密后的单字符是否正确。
图片描述
咱们应该可以理解当flag字符串正确的位数越多的时候,程序在运行时经过Opcode分发那一块的汇编指令的次数也越多,因此可以在Opcode分发的位置进行插桩,从而将程序判断的结果通过插桩的次数来展现出来,通过这种侧信道的方式来将程序的比较结果展现出来。

这里本人采用了Frida这样一款工具,对程序进行一个模拟的插桩。
注入的Frida脚本如下

其中hook的两个位置分别为opcode分发和putchar的位置
图片描述

图片描述

测试一下 可以采用如下的命令向进程中注入脚本

图片描述
当输入的flag不符合标准flag形式的时候(图中测试字符串长度为43),可以都看到返回结果为341
图片描述
如果符合格式的话可以看到返回结果已经很大

dang 图片描述
当第一位是正确字符的时候可以看到返回值更大了

通过刚才的思路可以知道该方法理论是可行的,但是一个个手动尝试时间复杂度也是很难得,所以需要写自动化脚本来代替手工操作。
首先要利用python实现进程的创建(利用subprocess库)
然后使用相关的Frida API实现注入frida脚本

然后js脚本中利用send函数向主控的python发送数据
图片描述

图片描述
可以看到成功爆破出索引为6位置字符为1 (插桩数增大)
按照这个思路 跑大概两三个小时? 最后可以得到42位正确的flag
缺失最后一位 再写脚本爆破一下该字符就可以到的最后flag

执行后得到最后的flag
图片描述

var number = 0
function main()
{
    var base =  Module.findBaseAddress("ezVM.exe")
    //获取目标进程的基地址
    //console.log("inject success!!!")
    //console.log("base:",base)
    if(base){
        Interceptor.attach(base.add(0x1044), {
     
                onEnter: function(args) {
      
                   //console.log("number",number)
                    number+=1
                    //进行插桩 每当程序运行到这里 number+=1
                     
                }
 
            });
 
            Interceptor.attach(base.add(0x0113f), {
                onEnter: function(args) {
                     
                    console.log("end!",number)
                    //send(number)
                    //当程序执行结束后把结果发送个消息处理函数
                }
 
            });
    }
}
setImmediate(main);
var number = 0
function main()
{
    var base =  Module.findBaseAddress("ezVM.exe")
    //获取目标进程的基地址
    //console.log("inject success!!!")
    //console.log("base:",base)
    if(base){
        Interceptor.attach(base.add(0x1044), {
     
                onEnter: function(args) {
      
                   //console.log("number",number)
                    number+=1
                    //进行插桩 每当程序运行到这里 number+=1
                     
                }
 
            });
 
            Interceptor.attach(base.add(0x0113f), {
                onEnter: function(args) {
                     
                    console.log("end!",number)
                    //send(number)
                    //当程序执行结束后把结果发送个消息处理函数
                }
 
            });
    }
}
setImmediate(main);
frida -l h00k.js -n ezVM.exe
frida -l h00k.js -n ezVM.exe
# -*- coding: UTF-8 -*-
import subprocess
import win32api
import win32con
def start_suspended_process(proc_name):
    creation_flags = 0x14
    process = subprocess.Popen(proc_name, creationflags=creation_flags)
    print("子进程已启动并挂起")
    return process.pid
import ctypes
def resume_process(pid):
    try:
        kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
        kernel32.DebugActiveProcess(pid)
        print(f"进程 {pid} 已恢复.")
    except OSError as e:
        print(f"恢复进程时发生错误: {str(e)}")
 
printable = "`!\"#$%&'()*+,-./:;<=>?@[\]^_{|}~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#以`开头是因为flag中极大概率不会出现该字符 所以该字符作为一个检验的标准
 
import frida, sys
number = 102741
number =103833
new_number = 0
def is_right():
    global new_number,number
    if new_number > number:
        number = new_number
        return True
    else:
        return False
         
def on_message(message, data):
    global new_number
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
        new_number = message['payload']
        # val = int(message['payload'], 16)
        # script.post({'type': 'input', 'payload': str(val * 2)})
    elif message['type'] == "error":
        print(message["description"])
        print(message["stack"])
        print(message["fileName"],"line:",message["lineNumber"],"colum:",message["columnNumber"])
    else:
        print(message)
pass
jscode = open("h00k.js","rb").read().decode()
import subprocess
# 44 -6 = 38  5--42
flag = "flag{O"
 
for index in range(len(flag),44):
    for i in printable:
        process = subprocess.Popen("ezVm.exe",
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                universal_newlines=True)
        tmp_flag = (flag+i).ljust(43,"A")+"}"
        print(tmp_flag)
        print("try index:",index ,"chr :",i)
         
         
        session = frida.attach("ezVM.exe")
        # 在目标进程里创建脚本
        script = session.create_script(jscode)
        # 注册消息回调
        script.on('message', on_message)
        #print('[*] Start attach')
        # 加载创建好的javascript脚本
        script.load()
 
        process.stdin.write(tmp_flag)
 
        output, error = process.communicate()
        if(i == '`'):
            number = new_number
         
        elif(is_right() == True):
            flag  +=i
            print(flag)
            break
        process.terminate()
         
 
    #打印输出结果
    # print('Output:', output.strip())
    # 打印错误信息(如果有)
    # if error:
    #     print('Error:', error.strip())
 
    #sys.stdin.read()
# -*- coding: UTF-8 -*-
import subprocess
import win32api
import win32con
def start_suspended_process(proc_name):
    creation_flags = 0x14
    process = subprocess.Popen(proc_name, creationflags=creation_flags)
    print("子进程已启动并挂起")
    return process.pid
import ctypes
def resume_process(pid):
    try:
        kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
        kernel32.DebugActiveProcess(pid)
        print(f"进程 {pid} 已恢复.")
    except OSError as e:
        print(f"恢复进程时发生错误: {str(e)}")
 
printable = "`!\"#$%&'()*+,-./:;<=>?@[\]^_{|}~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#以`开头是因为flag中极大概率不会出现该字符 所以该字符作为一个检验的标准
 
import frida, sys
number = 102741
number =103833
new_number = 0
def is_right():
    global new_number,number
    if new_number > number:
        number = new_number
        return True
    else:
        return False
         
def on_message(message, data):
    global new_number
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
        new_number = message['payload']

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2023-12-26 16:23 被Just_Cracker编辑 ,原因: 增加内容
上传的附件:
收藏
免费 12
支持
分享
最新回复 (12)
雪    币: 922
活跃值: (1813)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2
老铁666
2023-12-26 15:23
1
雪    币: 3059
活跃值: (30876)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2023-12-26 15:37
1
雪    币: 26245
活跃值: (63297)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
4
感谢分享,示例文件上传论坛一份,方便大家下载练习
2023-12-26 15:48
0
雪    币: 244
活跃值: (935)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
是个好思路,而且比较通用,就是时间久了点,不过话又说回来,通过逆向分析来搞2、3小时能不能搞定还是未知数。
2023-12-29 23:03
0
雪    币: 229
活跃值: (238)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
 
2023-12-29 23:33
0
雪    币: 215
活跃值: (636)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
实际测验了一下,20分钟就跑完了,好快
2023-12-30 14:34
0
雪    币: 215
活跃值: (636)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
DATAL 实际测验了一下,20分钟就跑完了,好快
而且爆破最后一位的时候,可以发现正确的flag次数是111635,其余是111636,可以直接锁定“#”是最后一位
2023-12-30 14:39
0
雪    币: 200
活跃值: (100)
能力值: ( LV3,RANK:21 )
在线值:
发帖
回帖
粉丝
9
这个0x113fhook的地址是怎么找的,我试过很多其他的地址都是失效的
2024-1-6 13:26
0
雪    币: 1460
活跃值: (1931)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
10
这里我发现爆破速度很慢的原因是我们hook上程序之后程序退出会变慢,如果我们调用exit函数在我们获取发送过来的number之后结束程序,可以得到更快的爆破速度,类似于这样。
Interceptor.attach(base.add(0x113f), {
                onEnter: function(args) {
                    
                    //console.log("end!",number)
                    send(number)
                    //当程序执行结束后把结果发送个消息处理函数
                    var a = 0;
                    for(var i = 0 ; i < 9999 ; i ++ ){
                                                       a+=1;
                    }
                    var f = new NativeFunction(base.add(0x21D8),'void',['int']);
                    f(0)
                }
 
            });
大概十几二十秒一位
本位耗时:23.772489309310913s,正确字符为:s
flag{O1SC_VM_1s!!!!!!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:17.439038038253784s,正确字符为:_
flag{O1SC_VM_1s_!!!!!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:20.81083393096924s,正确字符为:h
flag{O1SC_VM_1s_h!!!!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:5.902461528778076s,正确字符为:4
flag{O1SC_VM_1s_h4!!!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:23.32218360900879s,正确字符为:r
flag{O1SC_VM_1s_h4r!!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:19.15107274055481s,正确字符为:d
flag{O1SC_VM_1s_h4rd!!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:17.603569269180298s,正确字符为:_
flag{O1SC_VM_1s_h4rd_!!!!!!!!!!!!!!!!!!!!!!}
本位耗时:23.526416301727295s,正确字符为:t
flag{O1SC_VM_1s_h4rd_t!!!!!!!!!!!!!!!!!!!!!}
本位耗时:22.265416622161865s,正确字符为:o
flag{O1SC_VM_1s_h4rd_to!!!!!!!!!!!!!!!!!!!!}
本位耗时:18.028404712677002s,正确字符为:_
flag{O1SC_VM_1s_h4rd_to_!!!!!!!!!!!!!!!!!!!}
2024-1-9 03:10
1
雪    币: 620
活跃值: (934)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
11
Shangwendada 这里我发现爆破速度很慢的原因是我们hook上程序之后程序退出会变慢,如果我们调用exit函数在我们获取发送过来的number之后结束程序,可以得到更快的爆破速度,类似于这样。 Interceptor ...
感谢师傅的改进建议!学习到了!
2024-1-14 00:00
0
雪    币: 620
活跃值: (934)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
12
mb_onywfwdo 这个0x113fhook的地址是怎么找的,我试过很多其他的地址都是失效的
这个我也尝试其它地方都失效了 只有这个成功了
2024-1-14 00:01
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
我换成其他可执行文件手动命令行测试有send回来,但是python自动测试没办法有时候没办法send回来
2024-1-21 19:47
0
游客
登录 | 注册 方可回帖
返回
//