【CTF对抗- NCTF2023 逆向题目ezVM 题解】此文章归类为:CTF对抗。
在NCTF2023的题目中出现了一道VM类型的题目,针对该题本人采用Frida对程序进行插桩,利用侧信道攻击的方法爆破出flag。
首先分析该程序,是一个64bit的程序,并且加了UPX壳使用 -d 自动脱壳即可。
然后将程序放入IDA中逆向分析
比较传统的读取操作码以及操作数,根据不同操作码模拟不同的虚拟指令。
在0xFB处程序利用putchar输出回显信息
运行程序后,程序也会提示flag的格式以及长度
传统思路的话肯定是利用IDApython或者手撕出所有模拟的代码,然后进行逆向分析求解。当然这种方法适合逆向功底比较深厚的选手!
但是由于这种自设计的虚拟机模拟的局限性以及作者对选手的关爱,加密算法一般都是单字符加密的。
单字符加密的话由于密文空间很小(一般都是从printable的表中枚举),将可能的字符经过正向的加密,然后与密文进行比较来判断是否为正确的字符。所以针对这个题目可以采取爆破的方法,不断枚举flag的每一位字符,然后通过运行结果来判断加密后的单字符是否正确。
咱们应该可以理解当flag字符串正确的位数越多的时候,程序在运行时经过Opcode分发那一块的汇编指令的次数也越多,因此可以在Opcode分发的位置进行插桩,从而将程序判断的结果通过插桩的次数来展现出来,通过这种侧信道的方式来将程序的比较结果展现出来。
这里本人采用了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
|
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
更多【CTF对抗- NCTF2023 逆向题目ezVM 题解】相关视频教程:www.yxfzedu.com