L3HCTF 2025

  1. 1. L3HCTF
    1. 1.1. 1.TemporaParadox
    2. 1.2. 2.终焉之门
    3. 1.3. 3.easyvm

L3HCTF

1.TemporaParadox

拿到题目就发现代码写得很乱,又是难看的C++

先找到主函数,有一个花指令

nop掉反编译,然后开始审计代码

代码的关键就是时间戳的条件判断和里面的sub_140001963函数,意味着只有在(v62 > 1751990400 && v62 <= 1752052051)中的某个特定时间下运行这个代码,才能得到正确的md5字符串

然后具体分析sub_140001963函数(标在注释上了)

考点貌似是加盐md5,然后通过判断左右两个表达式是否相等决定了两种不一样的字符串拼接方式

具体的盐值为tlkyeueq6fej8vtzitt25yl24kswrgm5,通过以下代码获取:

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
import math

# 解析出的32个整数值
values = [
204, 180, -108, -122, -102, -118, -102, -114,
-1586131, 162, -102, 174, -4823929, 210, 204, 222,
-106, 204, 204, -6561, -528881, -122, 180, -6561,
-63145, -108, -116, -120, 198, -104, -110, -178171
]

salt = ""
for v9 in values:
if v9 >= 0:
v10 = v9 // 3 + 48
else:
if v9 >= -728:
# v10 = ~v9 (按位取反)
v10 = (~v9) & 0xFF # 确保在0-255范围内
else:
v10 = int(math.log(-v9) / 1.09861228866811 - 6.0 + 48.0)

# 确保v10在有效ASCII范围内
v10 = max(32, min(126, v10))
salt += chr(v10)

print("Salt value:", salt)

字符串拼接要用到的a, b, x, y, r都通过随机数获取,而cipher值的计算经过了较为复杂的变换,与两个置换盒有关

前三轮分别取了第1~第3的片段,先异或,后分别与S盒和P盒进行两次不同的变换

第四轮提取了第4个片段的数据,异或后进行仅S盒的变换

最后一轮取最后一个片段,仅进行了异或操作

最后返回的值就是cipher

因为有伪随机数种子,那么只需要写代码在规定时间戳范围内进行爆破就能得到最后的md5字符串了

EXP:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import math
import time
from hashlib import md5, sha1
from tqdm import tqdm

def gen(dword):
v1 = ((((dword << 13) & 0xffffffff) ^ dword) >> 17) ^ ((dword << 13) & 0xffffffff) ^ dword
dword = (((32 * v1) & 0xffffffff) ^ v1) & 0xffffffff
return dword, dword & 0x7FFFFFFF

# S-Box 和 P-Box 定义
S_BOX = [0x0000000E, 0x00000004, 0x0000000D, 0x00000001, 0x00000002, 0x0000000F, 0x0000000B, 0x00000008, 0x00000003, 0x0000000A, 0x00000006, 0x0000000C, 0x00000005, 0x00000009, 0x00000000, 0x00000007]

P_BOX = [0x00000001, 0x00000005, 0x00000009, 0x0000000D, 0x00000002, 0x00000006, 0x0000000A, 0x0000000E, 0x00000003, 0x00000007, 0x0000000B, 0x0000000F, 0x00000004, 0x00000008, 0x0000000C, 0x00000010]

def to_u32(n):
"""将一个数转换为32位无符号整数"""
​ ​return n & 0xFFFFFFFF

def to_s32(n):
"""将一个数转换为32位有符号整数"""
​ ​n = n & 0xFFFFFFFF
if n & 0x80000000:
return n - 0x100000000
return n

def s_box_transform(state, s_box_table):
"""S-盒替换"""
​ ​s = to_u32(state)
for _ in range(4):
index = (s >> 12) & 0xF
sbox_val = s_box_table[index]
s = sbox_val | (s << 4)
return to_u32(s)

def p_box_transform(state, p_box_table):
"""P-盒置换"""
​ ​s = to_u32(state)
new_state = 0
for i in range(16):
source_bit_pos = p_box_table[i] - 1
if (s >> source_bit_pos) & 1:
new_state |= (1 << i)
return new_state

def round_function(state, s_box_table, p_box_table):
"""轮函数"""
​ ​state = s_box_transform(state, s_box_table)
state = p_box_transform(state, p_box_table)
return state

def generate_round_key(key, round_num):
"""生成轮密钥"""
​ ​key_u32 = to_u32(key)
shift_amount = 4 * (round_num - 1)
shifted_key = key_u32 << shift_amount
return to_u32(shifted_key) >> 16

def encrypt_token(timestamp, r_key, s_box_table, p_box_table):
"""加密函数"""
​ ​state = to_u32(timestamp)

for i in range(1, 4):
round_key = generate_round_key(r_key, i)
state ^= round_key
state = round_function(state, s_box_table, p_box_table)

round_key_4 = generate_round_key(r_key, 4)
state ^= round_key_4
state = s_box_transform(state, s_box_table)

round_key_5 = generate_round_key(r_key, 5)
final_state = state ^ round_key_5

return to_u32(final_state)

# 修改后的主函数
def main():
target_md5 = "8a2fc1e9e2830c37f8a7f51572a640aa"
salt = "tlkyeueq7fej8vtzitt26yl24kswrgm5"
found = False

# 从 1751990400 到 1752052051 进行遍历
for t in tqdm(range(1751990400, 1752052052), desc="Processing", ncols=100):
dword = t
dword, ret = gen(dword)
cnt = ret
i = 0
while i < cnt:
dword, ret = gen(dword)
a = ret
dword, ret = gen(dword)
b = ret
dword, ret = gen(dword)
x = ret
dword, ret = gen(dword)
y = ret
dword, ret = gen(dword)
cnt = ret
i += 1
dword, ret = gen(dword)
r = ret

# 使用 pow 来计算 val1 和 val2
val1 = math.pow(float(to_s32(a) | to_s32(x)), 2.0)
val2 = math.pow(float(to_s32(b) | to_s32(y)), 2.0)

# 判断是否满足条件
if math.isclose(0x61 * val1, 0xb * val2):
cipher = encrypt_token(t, r, S_BOX, P_BOX)
query = f"salt={salt}&t={t}&r={r}&cipher={cipher}"
else:
query = f"salt={salt}&t={t}&r={r}&a={a}&b={b}&x={x}&y={y}"

# 计算 MD5
query_md5 = md5(query.encode()).hexdigest()
if query_md5 == target_md5:
print("[+] Found!")
print(f"Query: {query}")
# 输出 SHA1 值
print(f"SHA1: {sha1(query.encode()).hexdigest()}")
found = True
break

if not found:
print("No matching MD5 found.")

# 运行主函数
if __name__ == "__main__":
main()

2.终焉之门

直接打开看不到什么有用的东西,所以直接启动动调看看

发现动调之后这两个位置的字符变了

跟进发现把主要逻辑藏在这儿了

然后把主要逻辑理出来,丢给ai后才知道这是OpenGL Shading Language代码语言

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
#version 330
#define S(a,b,t) smoothstep(a,b,t)
uniform float time;
out vec4 fragColor;
mat2 Rot(float a) {
float s = sin(a);
float c = cos(a);
return mat2(c, -s, s, c);
}
vec2 hash(vec2 p) {
p = vec2(dot(p, vec2(2127.1, 81.17)), dot(p, vec2(1269.5, 283.37)));
return fract(sin(p) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(dot(-1.0 + 2.0 * hash(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),
dot(-1.0 + 2.0 * hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
mix(dot(-1.0 + 2.0 * hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
dot(-1.0 + 2.0 * hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), u.y) * 0.5 + 0.5;
}

void main() {
vec2 uSize = vec2(1280.0, 800.0);
vec2 uv = gl_FragCoord.xy / uSize;
float ratio = uSize.x / uSize.y;
vec2 tuv = uv - 0.5;
float degree = noise(vec2(time * 0.1, tuv.x * tuv.y));
tuv.y *= 1.0 / ratio;',0Ah
tuv *= Rot(radians((degree - 0.5) * 720.0 + 180.0));
tuv.y *= ratio;
float frequency = 3.5;
float amplitude = 10.0;
float speed = time * 1.5;
tuv.x += sin(tuv.y * frequency + speed) / amplitude;
tuv.y += sin(tuv.x * frequency * 1.5 + speed) / (amplitude * 0.5);
vec3 color1 = vec3(0.8, 0.4, 0.9);
vec3 color2 = vec3(0.4, 0.7, 1.0);
vec3 color3 = vec3(1.0, 0.6, 0.4);
vec3 color4 = vec3(0.6, 1.0, 0.6);
vec3 layer1 = mix(color1, color2, S(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
vec3 layer2 = mix(color3, color4, S(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
vec3 finalColor = mix(layer1, layer2, S(0.5, -0.3, tuv.y));
fragColor = vec4(finalColor, 1.0);
}

第一段是一个片段着色器,就是用来生成程序运行时会变换的彩色动态背景的代码

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
#version 430 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer OpCodes { int opcodes[]; };
layout(std430, binding = 2) buffer CoConsts { int co_consts[]; };
layout(std430, binding = 3) buffer Cipher { int cipher[16]; };
layout(std430, binding = 4) buffer Stack { int stack_data[256]; };
layout(std430, binding = 5) buffer Out { int verdict; };
const int MaxInstructionCount = 1000;

void main()
{
if (gl_GlobalInvocationID.x > 0) return;
uint ip = 0u;
int sp = 0;
verdict = -233;
while (ip < uint(MaxInstructionCount))
{
int opcode = opcodes[int(ip)];
int arg = opcodes[int(ip)+1];
switch (opcode)
{
case 2:
stack_data[sp++] = co_consts[arg];
break;
case 7:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a + b;
break;
}
case 8:
{
int a = stack_data[--sp];
int b = stack_data[--sp];
stack_data[sp++] = a - b;
break;
}
case 14:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a ^ b;
beak;
}
case 16:
{
bool ok = true;
for (int i = 0; i < 16; i++)
{
if (stack_data[i] != (cipher[i] - 20))
{
ok = false;
break;
}
}
verdict = ok ? 1 : -1;
return;
}
case 18:
{
int c = stack_data[--sp];
if (c == 0) ip = uint(arg);
break;
}
default:
verdict = 500;
return;
}
ip+=2;
}
verdict = 501;
}

第二段实现了一个简单的虚拟机,通过执行opcodes对应的操作,对栈数据加以运算

其中的比对校验是栈数据的前16位等于(cipher[i] - 20)

然后回去分析主函数,以下附上主函数及注释

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
__int64 sub_7FF60DF61CF0()
{
int v0; // ebx
__m128i v1; // xmm6
unsigned int v2; // eax
unsigned int v3; // r13d
unsigned int v4; // eax
int v5; // eax
__int64 v6; // rdi
bool v7; // dl
char v8; // al
double v9; // xmm0_8
unsigned int v10; // eax
char *v12; // r15
unsigned int v13; // ebx
int v14; // eax
__int64 v15; // r9
int v16; // edx
int v17; // eax
int v18; // ecx
int v19; // eax
unsigned int v20; // [rsp+38h] [rbp-100h]
unsigned int v21; // [rsp+3Ch] [rbp-FCh]
unsigned int v22; // [rsp+40h] [rbp-F8h]
unsigned int v23; // [rsp+44h] [rbp-F4h]
unsigned int v24; // [rsp+48h] [rbp-F0h]
int v25; // [rsp+4Ch] [rbp-ECh]
__m128i v26; // [rsp+50h] [rbp-E8h] BYREF
int v27; // [rsp+6Ch] [rbp-CCh] BYREF
char Str[8]; // [rsp+70h] [rbp-C8h] BYREF
__int64 v29; // [rsp+78h] [rbp-C0h]
__int64 v30; // [rsp+80h] [rbp-B8h]
__int64 v31; // [rsp+88h] [rbp-B0h]
__int64 v32[7]; // [rsp+90h] [rbp-A8h] BYREF
__int64 v33[3]; // [rsp+C8h] [rbp-70h]

v0 = 0;
sub_7FF60DF5E370();
sub_7FF60DEF3480(8256i64);
sub_7FF60DEEF730(1280i64, 800i64, "Password Checker"); // 创建窗口
sub_7FF60DEF1100(&v26, 0i64, aVersion330Defi); // 创建片段着色器
v1 = _mm_loadu_si128(&v26);
v2 = sub_7FF60DEDE700(aVersion430Core, 37305i64); // 创建计算着色器
v20 = sub_7FF60DEDEEE0(v2); // 创建着色器程序
v21 = sub_7FF60DEDEFF0(672i64, &opcodes, 0x88EAi64);
v3 = sub_7FF60DEDEFF0(128i64, &co_consts, 0x88EAi64);
//这里的co_consts前16位都是空的,给了后16位
v22 = sub_7FF60DEDEFF0(64i64, &cipher, 0x88EAi64);
v23 = sub_7FF60DEDEFF0(1024i64, &stack, 0x88EAi64);
v4 = sub_7FF60DEDEFF0(4i64, &out, 0x88EAi64);
//这里的五个初始化数据可以从虚拟机开头的layout对应过来
v33[0] = 0i64;
v24 = v4;
*Str = 0i64;
v29 = 0i64;
v30 = 0i64;
v31 = 0i64;
memset(v32, 0, sizeof(v32));
*(v33 + 5) = 0i64;
sub_7FF60DEF31A0(60i64);
while ( !sub_7FF60DEECAC0() ) // 主循环,检测窗口是否关闭
{
v5 = sub_7FF60DEF5A40(); // 获取输入
if ( v5 > 0 && v0 <= 99 )
{
v6 = v0 + 1;
do
{
Str[v6 - 1] = v5;
v0 = v6;
v5 = sub_7FF60DEF5A40();
v7 = v6++ <= 99;
}
while ( v7 && v5 > 0 );
}
v8 = sub_7FF60DEF58E0(259i64); // 检查Backspace键
if ( v0 > 0 && v8 )
Str[--v0] = 0;
if ( sub_7FF60DEF58E0(257i64) && strlen(Str) == 40 && !strncmp(Str, "L3HCTF{", 7ui64) && HIBYTE(v32[0]) == '}' )
// 检查Enter键 && 密码长度为40 && 以L3HCTF{开头,以}结尾
{ // 提取花括号内的内容(32个字符)
v25 = v0;
v12 = &Str[7]; // 跳过"L3HCTF{"
v13 = 0;
do // 将16进制字符串转换为字节
{
v17 = *v12;
v18 = v12[1];
if ( v17 > 96 ) // 'a'-'f' -> 10-15
v14 = v17 - 87;
else
v14 = v17 - 48; // '0'-'9' -> 0-9
v19 = 16 * v14; // 高位字符*16
v15 = v13;
v16 = v18 - 48;
if ( v18 >= 97 )
v16 = v18 - 87;
v12 += 2;
v13 += 4;
v27 = v16 + v19; // 组成一个字节
// 将32个字符转换为16个整数,每个整数由2个十六进制字符组成
sub_7FF60DEDF0B0(v3, &v27, 4i64, v15); // 将字节写入缓冲区
}
while ( v32 + 7 != v12 );
v0 = v25;
sub_7FF60DEDC100(v20);
sub_7FF60DEDF180(v21, 0i64); // opcodes
sub_7FF60DEDF180(v3, 2i64); // co_consts
sub_7FF60DEDF180(v22, 3i64); // cipher
sub_7FF60DEDF180(v23, 4i64); // stack
sub_7FF60DEDF180(v24, 5i64);
sub_7FF60DEDEFE0(1i64, 1i64, 1i64); // 执行计算着色器
sub_7FF60DEDF140(v24, &output, 4i64, 0i64); // 读取结果
sub_7FF60DEDC110();
}
sub_7FF60DEEFC90();
v26 = v1;
sub_7FF60DEF0650(&v26);
v9 = sub_7FF60DEEE170();
v26 = v1;
*&v9 = v9;
v27 = LODWORD(v9);
v10 = sub_7FF60DEF1440(&v26, "time");
v26 = v1;
sub_7FF60DEF1460(&v26, v10, &v27, 0i64);
sub_7FF60DF0B9D0(0, 0, 1280, 800, -1);
sub_7FF60DEF0690();
sub_7FF60DF1DA20(Str, 100, 200, 40, -16777216);
if ( output == 1 )
sub_7FF60DF1DA20("success", 100, 300, 40, -13863680);
else
sub_7FF60DF1DA20("wrong password", 100, 300, 40, -13162010);
sub_7FF60DF1DA20("Type password and press [Enter] to check!", 100, 100, 20, -8224126);
sub_7FF60DF1DA20("Press [Backspace] to delete characters.", 100, 130, 20, -8224126);
sub_7FF60DEF5CE0();
}
sub_7FF60DEEFAA0();
return 0i64;
}

关键信息都已可见,接下来就是要写个虚拟机的解释器,获取加密逻辑了

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <stdint.h>

int main()
{
int opcodes[] = {
2,0,2,1,2,0,14,0,2,16,8,0,2,2,2,1,14,0,2,17,8,0,2,3,2,2,14,0,2,18,7,0,
2,4,2,3,14,0,2,19,7,0,2,5,2,4,14,0,2,20,8,0,2,6,2,5,14,0,2,21,7,0,
2,7,2,6,14,0,2,22,7,0,2,8,2,7,14,0,2,23,7,0,2,9,2,8,14,0,2,24,7,0,
2,10,2,9,14,0,2,25,7,0,2,11,2,10,14,0,2,26,7,0,2,12,2,11,14,0,2,27,8,0,
2,13,2,12,14,0,2,28,8,0,2,14,2,13,14,0,2,29,7,0,2,15,2,14,14,0,2,30,8,0,
16,0,2,16,2,17,15,0,18,84,2,31,1,0,3,1
};

//常量池前16位补成0
int co_consts[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF,
0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00
};

int cipher[] = {
0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE,
0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146, 0x97
};

int stack_data[256] = {};
int sp = 0;

for (int i = 0; i < 168; i += 2)
{
int opcode = opcodes[i];
int arg = opcodes[i + 1];
int sp0 = sp;

switch (opcode)
{
case 2:
{
int v = (arg >= 16) ? co_consts[arg] : 0; // 安全检查
stack_data[sp++] = v;
printf("[IP=%d]\tstack_data[%d] = co_consts[%d] = %#x\n",
i / 2, sp - 1, arg, v);
break;
}
case 7:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a + b;
printf("[IP=%d]\tstack_data[%d] = a + b = stack_data[%d] + stack_data[%d] = %#x\n",
i / 2, sp - 1, sp0 - 2, sp0 - 1, a + b);
break;
}
case 8:
{
int a = stack_data[--sp];
int b = stack_data[--sp];
stack_data[sp++] = a - b;
printf("[IP=%d]\tstack_data[%d] = a - b = stack_data[%d] - stack_data[%d] = %#x\n",
i / 2, sp - 1, sp0 - 1, sp0 - 2, a - b);
break;
}
case 14:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a ^ b;
printf("[IP=%d]\tstack_data[%d] = a ^ b = stack_data[%d] ^ stack_data[%d] = %#x\n",
i / 2, sp - 1, sp0 - 2, sp0 - 1, a ^ b);
break;
}
case 15:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = int(a == b);
printf("[IP=%d]\tstack_data[%d] = (a == b) = stack_data[%d] == stack_data[%d] = %#x\n",
i / 2, sp - 1, sp0 - 2, sp0 - 1, (a == b));
break;
}
case 16:
{
printf("[IP=%d]\t=== VERIFY cipher check ===\n", i / 2);
for (int j = 0; j < 16; j++)
{
printf(" stack[%2d]=0x%X vs cipher[%2d]-20=0x%X\n",
j, stack_data[j], j, cipher[j] - 20);
}
break;
}
case 18:
{
int c = stack_data[--sp];
printf("[IP=%d]\tJZ if top==0 jump to %d (top=%d)\n",
i / 2, arg, c);
if (c == 0)
{
i = arg * 2 - 2; // 跳转
}
break;
}
default:
{
printf("[IP=%d]\tUNKNOWN OPCODE %d, abort\n", i / 2, opcode);
return -1;
}
}
}

return 0;
}

得到VM的运行逻辑

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
94
95
96
97
[IP=0]  stack_data[0] = co_consts[0] = 0
[IP=1] stack_data[1] = co_consts[1] = 0
[IP=2] stack_data[2] = co_consts[0] = 0
[IP=3] stack_data[1] = a ^ b = stack_data[1] ^ stack_data[2] = 0
[IP=4] stack_data[2] = co_consts[16] = 0xb0
[IP=5] stack_data[1] = a - b = stack_data[2] - stack_data[1] = 0xb0
[IP=6] stack_data[2] = co_consts[2] = 0
[IP=7] stack_data[3] = co_consts[1] = 0
[IP=8] stack_data[2] = a ^ b = stack_data[2] ^ stack_data[3] = 0
[IP=9] stack_data[3] = co_consts[17] = 0xc8
[IP=10] stack_data[2] = a - b = stack_data[3] - stack_data[2] = 0xc8
[IP=11] stack_data[3] = co_consts[3] = 0
[IP=12] stack_data[4] = co_consts[2] = 0
[IP=13] stack_data[3] = a ^ b = stack_data[3] ^ stack_data[4] = 0
[IP=14] stack_data[4] = co_consts[18] = 0xfa
[IP=15] stack_data[3] = a + b = stack_data[3] + stack_data[4] = 0xfa
[IP=16] stack_data[4] = co_consts[4] = 0
[IP=17] stack_data[5] = co_consts[3] = 0
[IP=18] stack_data[4] = a ^ b = stack_data[4] ^ stack_data[5] = 0
[IP=19] stack_data[5] = co_consts[19] = 0x86
[IP=20] stack_data[4] = a + b = stack_data[4] + stack_data[5] = 0x86
[IP=21] stack_data[5] = co_consts[5] = 0
[IP=22] stack_data[6] = co_consts[4] = 0
[IP=23] stack_data[5] = a ^ b = stack_data[5] ^ stack_data[6] = 0
[IP=24] stack_data[6] = co_consts[20] = 0x6e
[IP=25] stack_data[5] = a - b = stack_data[6] - stack_data[5] = 0x6e
[IP=26] stack_data[6] = co_consts[6] = 0
[IP=27] stack_data[7] = co_consts[5] = 0
[IP=28] stack_data[6] = a ^ b = stack_data[6] ^ stack_data[7] = 0
[IP=29] stack_data[7] = co_consts[21] = 0x8f
[IP=30] stack_data[6] = a + b = stack_data[6] + stack_data[7] = 0x8f
[IP=31] stack_data[7] = co_consts[7] = 0
[IP=32] stack_data[8] = co_consts[6] = 0
[IP=33] stack_data[7] = a ^ b = stack_data[7] ^ stack_data[8] = 0
[IP=34] stack_data[8] = co_consts[22] = 0xaf
[IP=35] stack_data[7] = a + b = stack_data[7] + stack_data[8] = 0xaf
[IP=36] stack_data[8] = co_consts[8] = 0
[IP=37] stack_data[9] = co_consts[7] = 0
[IP=38] stack_data[8] = a ^ b = stack_data[8] ^ stack_data[9] = 0
[IP=39] stack_data[9] = co_consts[23] = 0xbf
[IP=40] stack_data[8] = a + b = stack_data[8] + stack_data[9] = 0xbf
[IP=41] stack_data[9] = co_consts[9] = 0
[IP=42] stack_data[10] = co_consts[8] = 0
[IP=43] stack_data[9] = a ^ b = stack_data[9] ^ stack_data[10] = 0
[IP=44] stack_data[10] = co_consts[24] = 0xc9
[IP=45] stack_data[9] = a + b = stack_data[9] + stack_data[10] = 0xc9
[IP=46] stack_data[10] = co_consts[10] = 0
[IP=47] stack_data[11] = co_consts[9] = 0
[IP=48] stack_data[10] = a ^ b = stack_data[10] ^ stack_data[11] = 0
[IP=49] stack_data[11] = co_consts[25] = 0x64
[IP=50] stack_data[10] = a + b = stack_data[10] + stack_data[11] = 0x64
[IP=51] stack_data[11] = co_consts[11] = 0
[IP=52] stack_data[12] = co_consts[10] = 0
[IP=53] stack_data[11] = a ^ b = stack_data[11] ^ stack_data[12] = 0
[IP=54] stack_data[12] = co_consts[26] = 0xd7
[IP=55] stack_data[11] = a + b = stack_data[11] + stack_data[12] = 0xd7
[IP=56] stack_data[12] = co_consts[12] = 0
[IP=57] stack_data[13] = co_consts[11] = 0
[IP=58] stack_data[12] = a ^ b = stack_data[12] ^ stack_data[13] = 0
[IP=59] stack_data[13] = co_consts[27] = 0xc3
[IP=60] stack_data[12] = a - b = stack_data[13] - stack_data[12] = 0xc3
[IP=61] stack_data[13] = co_consts[13] = 0
[IP=62] stack_data[14] = co_consts[12] = 0
[IP=63] stack_data[13] = a ^ b = stack_data[13] ^ stack_data[14] = 0
[IP=64] stack_data[14] = co_consts[28] = 0xe3
[IP=65] stack_data[13] = a - b = stack_data[14] - stack_data[13] = 0xe3
[IP=66] stack_data[14] = co_consts[14] = 0
[IP=67] stack_data[15] = co_consts[13] = 0
[IP=68] stack_data[14] = a ^ b = stack_data[14] ^ stack_data[15] = 0
[IP=69] stack_data[15] = co_consts[29] = 0xef
[IP=70] stack_data[14] = a + b = stack_data[14] + stack_data[15] = 0xef
[IP=71] stack_data[15] = co_consts[15] = 0
[IP=72] stack_data[16] = co_consts[14] = 0
[IP=73] stack_data[15] = a ^ b = stack_data[15] ^ stack_data[16] = 0
[IP=74] stack_data[16] = co_consts[30] = 0x87
[IP=75] stack_data[15] = a - b = stack_data[16] - stack_data[15] = 0x87
[IP=76] === VERIFY cipher check ===
stack[ 0]=0x0 vs cipher[ 0]-20=0xDF
stack[ 1]=0xB0 vs cipher[ 1]-20=0x6E
stack[ 2]=0xC8 vs cipher[ 2]-20=0xFFFFFFF2
stack[ 3]=0xFA vs cipher[ 3]-20=0x1E9
stack[ 4]=0x86 vs cipher[ 4]-20=0x13C
stack[ 5]=0x6E vs cipher[ 5]-20=0x24
stack[ 6]=0x8F vs cipher[ 6]-20=0x9E
stack[ 7]=0xAF vs cipher[ 7]-20=0xCA
stack[ 8]=0xBF vs cipher[ 8]-20=0x146
stack[ 9]=0xC9 vs cipher[ 9]-20=0x183
stack[10]=0x64 vs cipher[10]-20=0x88
stack[11]=0xD7 vs cipher[11]-20=0x1C3
stack[12]=0xC3 vs cipher[12]-20=0x5A
stack[13]=0xE3 vs cipher[13]-20=0x14
stack[14]=0xEF vs cipher[14]-20=0x132
stack[15]=0x87 vs cipher[15]-20=0x83
[IP=77] stack_data[16] = co_consts[16] = 0xb0
[IP=78] stack_data[17] = co_consts[17] = 0xc8
[IP=79] stack_data[16] = (a == b) = stack_data[16] == stack_data[17] = 0
[IP=80] JZ if top==0 jump to 84 (top=0)

每一步操作都是对栈数据进行处理,但只利用了加、减和异或这三种简单的运算

对每个字节的处理模式为:

1
2
3
4
5
6
PUSH co_consts[i]        // 压入当前字节
PUSH co_consts[i+1] // 压入下一个字节(或0)
XOR // 两个字节异或
PUSH 0 // 压入0(arg=16,17,18...)
加减运算 // 与0进行运算,实际保持值不变
结果存入stack[i]位置

具体加密过程:(以字节0为例)

1
2
3
4
5
6
7
[IP=0]  stack_data[0] = co_consts[0] = 0    // 1. 压入字节0
[IP=1] stack_data[1] = co_consts[1] = 0 // 2. 压入字节1
[IP=2] stack_data[2] = co_consts[0] = 0 // 1. 再次压入字节0(为了异或)
[IP=3] stack_data[1] = a ^ b = stack_data[1] ^ stack_data[2] = 0 // 3. 异或运算
[IP=4] stack_data[2] = co_consts[16] = 0xb0 // 4. 压入0
[IP=5] stack_data[1] = a - b = stack_data[2] - stack_data[1] = 0xb0
// 5. 减法运算(运行时实际值不变)

最后就是写EXP:

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
cipher = [0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE, 0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146, 0x97]
co_consts = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00]
opcodes = [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1]

targets = [c - 20 for c in cipher]
inputs = [0] * 16

inputs[0] = targets[0]

for i in range(1, 16):
init_input = inputs[i - 1]
target = targets[i]
const = co_consts[i - 1]
opcode = opcodes[i - 1]

if opcode == 0: # ADD
temp = (target - const) & 0xff
elif opcode == 1: # SUB
temp = (const - target) & 0xff

inputs[i] = temp ^ init_input

flag = f"L3HCTF{{{bytes(inputs).hex()}}}"
print(flag)
#L3HCTF{df9d4ba41258574ccb7155b9d01f5c58}

3.easyvm

找到主函数,是用C++编写的,代码比较难看

分析代码重命名一下,重点就在VM和check函数上,check函数里存有密文

看到题目的第一想法是具体分析VM每个指令的功能,但是进入代码一看发现写的特别乱,而且有很多switch函数和case的嵌套,不知道从哪下手了

然后去参考了一下SU的wp,知道了这道VM题要采用另一种解题方式:通过idapython脚本模拟trace获取加密逻辑

分析VM具体代码,发现里面有几个对数据进行处理的关键操作码

分别是:

  • 0x18 异或
  • 0x17 右移
  • 0x16 左移
  • 0x14 取模
  • 0x13 除法
  • 0x12 乘法
  • 0x11 减法
  • 0x10 加法

但是由于取模和乘除运算不太可能逆向恢复,所以可以在其它五个运算代码处下断点获取具体加密流程

写出五个运算对应的idapython脚本(参考星盟的讲解视频)

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 idc
op1 = idc.get_reg_value('EAX')
rbp = idc.get_reg_value('RBP')
tmp = rbp + 0xC0 - 0x74
op2 = idc.get_wide_dword(tmp)
print(f'xor {hex(op1)} , {hex(op2)} = {(hex((op1 ^ op2) & 0xFFFFFFFF))}')

import idc
op1 = idc.get_reg_value('EDX')
op2 = idc.get_reg_value('cl')
print(f'shr {hex(op1)} , {hex(op2)} = {hex((op1 >> op2) & 0xFFFFFFFF)}')

import idc
op1 = idc.get_reg_value('EDX')
op2 = idc.get_reg_value('cl')
print(f'shl {hex(op1)} , {hex(op2)} = {hex((op1 << op2) & 0xFFFFFFFF)}')

import idc
op1 = idc.get_reg_value('EAX')
rbp = idc.get_reg_value('RBP')
tmp = rbp + 0xC0 - 0xA4
op2 = idc.get_wide_dword(tmp)
print(f'sub {hex(op1)} , {hex(op2)} = {hex((op1 - op2) & 0xFFFFFFFF)}')

import idc
op1 = idc.get_reg_value('EAX')
op2 = idc.get_reg_value('EDX')
print(f'add {hex(op1)} , {hex(op2)} = {hex((op1 + op2) & 0xFFFFFFFF)}')

调试之后得到加密逻辑,取一段进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shl 0x32323232 , 0x3 = 0x91919190            //v1 << 3
add 0xa56babcd , 0x91919190 = 0x36fd3d5d //(v1 << 3) + key[0]
add 0x0 , 0x32323232 = 0x32323232 //v1 + sum
add 0x0 , 0x32323232 = 0x32323232 //(v1 + sum) + key[1]
xor 0x32323232 , 0x36fd3d5d = 0x4cf0f6f //((v1 << 3) + key[0]) ^ (v1 + sum + key[1])
shr 0x32323232 , 0x4 = 0x3232323 //v1 >> 4
add 0xffffffff , 0x3232323 = 0x3232322 //(v1 >> 4) + key[2]
xor 0x4cf0f6f , 0x3232322 = 0x7ec2c4d
//((v1 >> 4) + key[2]) ^ (((v1 << 3) + key[0]) ^ (v1 + sum + key[1]))
add 0x31313131 , 0x7ec2c4d = 0x391d5d7e
//v0 += ((v1 >> 4) + key[2]) ^ (((v1 << 3) + key[0]) ^ (v1 + sum + key[1]))
add 0x11223344 , 0x0 = 0x11223344 //sum += delta
shl 0x391d5d7e , 0x2 = 0xe47575f8 //v0 << 2
add 0xffffffff , 0xe47575f8 = 0xe47575f7 //(v0 << 2) + key[2]
add 0x11223344 , 0x391d5d7e = 0x4a3f90c2 //v0 + sum
add 0xabcdef01 , 0x4a3f90c2 = 0xf60d7fc3 //(v0 + sum) + key[3]
xor 0xf60d7fc3 , 0xe47575f7 = 0x12780a34 //((v0 << 2) + key[2]) ^ ((v0 + sum) + key[3])
shr 0x391d5d7e , 0x5 = 0x1c8eaeb //v0 >> 5
add 0xa56babcd , 0x1c8eaeb = 0xa73496b8 //(v0 >> 5) + key[0]
xor 0x12780a34 , 0xa73496b8 = 0xb54c9c8c
//((v0 >> 5) + key[0]) ^ (((v0 << 2) + key[2]) ^ ((v0 + sum) + key[3]))
add 0x32323232 , 0xb54c9c8c = 0xe77ecebe
//v1 += ((v0 >> 5) + key[0]) ^ (((v0 << 2) + key[2]) ^ ((v0 + sum) + key[3]))
sub 0x40 , 0x1 = 0x3f //循环次数-1

找到密钥为0xa56babcd,0x0,0xffffffff,0xabcdef01​,delta为0x11223344

然后我们看看下一组块加密进行了什么操作(即下一段以sub 0x40 , 0x1 = 0x3f为结尾的代码)

可以看到,下一组加密开始时的sum的值使用了上一组加密最后sum的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shl 0x32323232 , 0x3 = 0x91919190
add 0xa56babcd , 0x91919190 = 0x36fd3d5d
add 0x488cd100 , 0x32323232 = 0x7abf0332
add 0x0 , 0x7abf0332 = 0x7abf0332
xor 0x7abf0332 , 0x36fd3d5d = 0x4c423e6f
shr 0x32323232 , 0x4 = 0x3232323
add 0xffffffff , 0x3232323 = 0x3232322
xor 0x4c423e6f , 0x3232322 = 0x4f611d4d
add 0x31313131 , 0x4f611d4d = 0x80924e7e
add 0x11223344 , 0x488cd100 = 0x59af0444
shl 0x80924e7e , 0x2 = 0x24939f8
add 0xffffffff , 0x24939f8 = 0x24939f7
add 0x59af0444 , 0x80924e7e = 0xda4152c2
add 0xabcdef01 , 0xda4152c2 = 0x860f41c3
xor 0x860f41c3 , 0x24939f7 = 0x84467834
shr 0x80924e7e , 0x5 = 0x4049273
add 0xa56babcd , 0x4049273 = 0xa9703e40
xor 0x84467834 , 0xa9703e40 = 0x2d364674
add 0x32323232 , 0x2d364674 = 0x5f6878a6
sub 0x40 , 0x1 = 0x3f

分析出加密逻辑后就可以开始写解密代码了

EXP:

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
#include <stdint.h>
#include <stdio.h>
#include <string.h>

void decrypt_xtea(uint32_t v[2], uint32_t const key[4], uint32_t i)
{
uint32_t num_rounds = 0x40;
uint32_t v0 = v[0], v1 = v[1], delta = 0x11223344;
i += 1;
uint32_t sum = delta * num_rounds * i;

for (i = 0; i < num_rounds; i++)
{
v1 -= ((v0 >> 5) + key[0]) ^ (((v0 << 2) + key[2]) ^ ((v0 + sum) + key[3]));
sum -= delta;
v0 -= ((v1 >> 4) + key[2]) ^ (((v1 << 3) + key[0]) ^ (v1 + sum + key[1]));
}
v[0] = v0;
v[1] = v1;
}

int main()
{
uint32_t enc[] = {0x877A62A6,0x6A55F1F3,0xAE194847,0xB1E643E7,0xA94FE881,0x9BC8A28A,0xC4CFAA9F,0xF1A00CA1};
uint32_t key[] = {0xa56babcd,0x0,0xffffffff,0xabcdef01};

for (size_t i = 0; i < 4; i++)
decrypt_xtea(&enc[i * 2], key, i);
printf("%s\n", enc);

return 0;
}
//L3HCTF{9c50d10ba864bedfb37d7efa4e110bf2}