NCTF2023 逆向题目ezVM 题解
2023-12-29 18:5:9 Author: 看雪学苑(查看原文) 阅读量:8 收藏

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

首先分析该程序,是一个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

*本文为看雪论坛优秀文章,由 Just_Cracker 原创,转载请注明来自看雪社区

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458532875&idx=1&sn=e854d592ee03a58587d3b8171353f059&chksm=b0e5e7a505a990a1ddea1c3116621f2d94b1d0a80639bf9115dc4f890162a1738b5eff635c2e&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh