题目分析
首先分析该程序,是一个64bit的程序,并且加了UPX壳使用 -d 自动脱壳即可。
然后将程序放入IDA中逆向分析。
比较传统的读取操作码以及操作数,根据不同操作码模拟不同的虚拟指令。
在0xFB处程序利用putchar输出回显信息。
运行程序后,程序也会提示flag的格式以及长度。
传统思路
传统思路的话肯定是利用IDApython或者手撕出所有模拟的代码,然后进行逆向分析求解。当然这种方法适合逆向功底比较深厚的选手!
侧信道攻击
但是由于这种自设计的虚拟机模拟的局限性以及作者对选手的关爱,加密算法一般都是单字符加密的。
单字符加密的话由于密文空间很小(一般都是从printable的表中枚举),将可能的字符经过正向的加密,然后与密文进行比较来判断是否为正确的字符。所以针对这个题目可以采取爆破的方法,不断枚举flag的每一位字符,然后通过运行结果来判断加密后的单字符是否正确。
咱们应该可以理解当flag字符串正确的位数越多的时候,程序在运行时经过Opcode分发那一块的汇编指令的次数也越多,因此可以在Opcode分发的位置进行插桩,从而将程序判断的结果通过插桩的次数来展现出来,通过这种侧信道的方式来将程序的比较结果展现出来。
Frida注入
这里本人采用了Frida这样一款工具,对程序进行一个模拟的插桩。
注入的Frida脚本如下:
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的位置。
测试一下 可以采用如下的命令向进程中注入脚本:
frida -l h00k.js -n ezVM.exe
当输入的flag不符合标准flag形式的时候(图中测试字符串长度为43),可以都看到返回结果为341。
如果符合格式的话可以看到返回结果已经很大。
dang
当第一位是正确字符的时候可以看到返回值更大了。
构建自动化测试脚本
通过刚才的思路可以知道该方法理论是可行的,但是一个个手动尝试时间复杂度也是很难得,所以需要写自动化脚本来代替手工操作。
首先要利用python实现进程的创建(利用subprocess库),
然后使用相关的Frida API实现注入frida脚本。
# -*- 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。
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。
看雪ID:Just_Cracker
https://bbs.kanxue.com/user-home-946278.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):NCTF2023 逆向题目ezVM 题解