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

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

2023-12-26 10:17
11421

前言

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

题目分析

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

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

传统思路

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

侧信道攻击

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

Frida注入

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);

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

图片描述

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

1
frida -l h00k.js -n ezVM.exe

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

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

构建自动化测试脚本

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# -*- 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()

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

运行效果

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import subprocess
 
# 创建进程并执行命令
 
flag = 'flag{O1SC_VM_1s_h4rd_to_r3v3rs3_#a78abffaa }'
 
for i in range(32,128):
    process = subprocess.Popen("ezVm.exe",
                           stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE,
                           universal_newlines=True)
    input_data = flag.replace(" ",chr(i))
    process.stdin.write(input_data)
    #process.stdin.flush()  # 刷新输入缓冲区
    print(input_data)
 
    # 读取进程的输出
    output, error = process.communicate()
 
    # 打印输出结果
    if ("Invalid" not in output.strip()):
        print('Output:', output.strip())
 
    # 打印错误信息(如果有)
    if error:
        print('Error:', error.strip())
    process.terminate()

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


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

最后于 2023-12-26 16:23 被Just_Cracker编辑 ,原因: 增加内容
上传的附件:
收藏
点赞12
打赏
分享
最新回复 (12)
雪    币: 999
活跃值: (1518)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
WMBa0 2023-12-26 15:23
2
1
老铁666
雪    币: 19773
活跃值: (29385)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-12-26 15:37
3
1
感谢分享
雪    币: 18869
活跃值: (60323)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2023-12-26 15:48
4
0
感谢分享,示例文件上传论坛一份,方便大家下载练习
雪    币: 179
活跃值: (771)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
0
是个好思路,而且比较通用,就是时间久了点,不过话又说回来,通过逆向分析来搞2、3小时能不能搞定还是未知数。
雪    币: 229
活跃值: (238)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
马飞飞 2023-12-29 23:33
6
0
 
雪    币: 215
活跃值: (636)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
DATAL 2023-12-30 14:34
7
0
实际测验了一下,20分钟就跑完了,好快
雪    币: 215
活跃值: (636)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
DATAL 2023-12-30 14:39
8
0
DATAL 实际测验了一下,20分钟就跑完了,好快
而且爆破最后一位的时候,可以发现正确的flag次数是111635,其余是111636,可以直接锁定“#”是最后一位
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_onywfwdo 2024-1-6 13:26
9
0
这个0x113fhook的地址是怎么找的,我试过很多其他的地址都是失效的
雪    币: 1261
活跃值: (551)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Shangwendada 2024-1-9 03:10
10
1
这里我发现爆破速度很慢的原因是我们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_!!!!!!!!!!!!!!!!!!!}
雪    币: 620
活跃值: (774)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
Just_Cracker 2024-1-14 00:00
11
0
Shangwendada 这里我发现爆破速度很慢的原因是我们hook上程序之后程序退出会变慢,如果我们调用exit函数在我们获取发送过来的number之后结束程序,可以得到更快的爆破速度,类似于这样。 Interceptor ...
感谢师傅的改进建议!学习到了!
雪    币: 620
活跃值: (774)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
Just_Cracker 2024-1-14 00:01
12
0
mb_onywfwdo 这个0x113fhook的地址是怎么找的,我试过很多其他的地址都是失效的
这个我也尝试其它地方都失效了 只有这个成功了
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
吃饭去2 2024-1-21 19:47
13
0
我换成其他可执行文件手动命令行测试有send回来,但是python自动测试没办法有时候没办法send回来
游客
登录 | 注册 方可回帖
返回