L3Hctf部分wp

TemporalParadox

写这一题用了快半天的时间www,反正得先把函数都理解透了才行

先找到主函数入口,有一个花指令,nop掉之后反编译

这里直接跳转,中间插入的就是花指令

image-20250716234017426

image-20250716224607175

注意,以下函数运行在所给的时间节点下v61 > 1751990400 && v61 <= 1752052051,主要看sub_140001963函数

image-20250716231843615

具体分析看注释

image-20250716231900454

大概就是通过计算两个表达式是否相等来决定字符串的拼接,然后计算字符串的md5的值看是否与所给的密文相等

Salt: tlkyeueq7fej8vtzitt26yl24kswrgm5 固定值,通过以下代码得到

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

dword_14000B060 = [
0x000000CC, 0x000000B4, 0xFFFFFF94, 0xFFFFFF86, 0xFFFFFF9A, 0xFFFFFF8A, 0xFFFFFF9A, 0xFFFFFF8E,
0xFFE7AC2D, 0x000000A2, 0xFFFFFF9A, 0x000000AE, 0xFFB70487, 0x000000D2, 0x000000CC, 0x000000DE,
0xFFFFFF96, 0x000000CC, 0x000000CC, 0xFFFFE65F, 0xFFF7E40F, 0xFFFFFF86, 0x000000B4, 0xFFFFE65F,
0xFFFF1957, 0xFFFFFF94, 0xFFFFFF8C, 0xFFFFFF88, 0x000000C6, 0xFFFFFF98, 0xFFFFFF92, 0xFFFD4C05
]

salt_chars = []

for v9 in dword_14000B060:
if v9 >= 0x80000000: # 转换负数
v9 -= 0x100000000
if v9 >= 0:
v10 = v9 // 3 + 48
else:
if v9 >= -728:
v10 = ~v9
else:
v10 = int(math.log(-v9) / 1.09861228866811 - 6.0 + 48.0)
salt_chars.append(chr(v10))

salt = ''.join(salt_chars)
print("Salt:", salt)

r,a,b,x,y都与随机数生成有关,cipher的生成来自于函数sub_14000184D,同时也与两个置换盒有关

第 1~3 轮每轮用 sub_1400016A0的片段 xor 进状态,再做 s盒与p盒变换。

第 4 轮取第 4 片段 XOR,再做 仅 S盒;最后取第 5 片段再 xor,返回 cipher。

image-20250716233103122

这里的判断条件也与随机生成数有关,最终可以通过爆破来实现

image-20250716233149894

整个过程就可以分为字符串为salt=xxx&t=xxx&r=xxx&a=xxx&b=xxx&x=xxx&y=xxx和salt=xxx&t=xxx&r=xxx&cipher=xxx两种进行爆破(最终结果是满足salt=xxx&t=xxx&r=xxx&a=xxx&b=xxx&x=xxx&y=xxx这个字符串爆破成功)

这里贴出脚本,由于要用到openssl这个库,我直接在虚拟机上运行代码了,安装这个库也比在windows端方便多

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <openssl/md5.h>
#include <openssl/sha.h>

#define SALT "tlkyeueq7fej8vtzitt26yl24kswrgm5"
#define TARGET_MD5 "8a2fc1e9e2830c37f8a7f51572a640aa"

static const uint8_t SBOX[16] = {
0x0E, 0x04, 0x0D, 0x01,
0x02, 0x0F, 0x0B, 0x08,
0x03, 0x0A, 0x06, 0x0C,
0x05, 0x09, 0x00, 0x07
};

static const uint8_t PERM[16] = {
1, 5, 9, 13,
2, 6, 10, 14,
3, 7, 11, 15,
4, 8, 12, 16
};

static inline uint32_t truncate_to_u32(uint64_t v) {
return (uint32_t)(v & 0xFFFFFFFF);
}

static int32_t to_signed32(uint32_t v) {
if (v & 0x80000000)
return (int32_t)(v - 0x100000000);
return (int32_t)v;
}

// 伪随机数生成器
void prng_generate(uint32_t *state, uint32_t *output) {
uint32_t val = *state;
uint32_t v1 = (((val << 13) ^ val) >> 17) ^ ((val << 13) ^ val);
uint32_t new_val = (32 * v1) ^ v1;
*state = new_val;
*output = new_val & 0x7FFFFFFF;
}

uint32_t apply_sbox(uint32_t input) {
uint32_t s = input;
for (int i = 0; i < 4; i++) {
uint8_t index = (s >> 12) & 0xF;
s = ((s << 4) & 0xFFFFFFFF) | SBOX[index];
}
return s;
}

uint32_t apply_pbox(uint32_t input) {
uint32_t s = input;
uint32_t result = 0;
for (int i = 0; i < 16; i++) {
int src_bit = PERM[i] - 1;
if ((s >> src_bit) & 1) {
result |= (1 << i);
}
}
return result;
}

// 一轮加密变换
uint32_t perform_round(uint32_t state) {
state = apply_sbox(state);
state = apply_pbox(state);
return state;
}

// 生成某轮轮密钥
uint16_t generate_round_key(uint32_t key, int round) {
uint32_t shifted = (key << (4 * (round - 1))) & 0xFFFFFFFF;
return (shifted >> 16) & 0xFFFF;
}

// 加密主函数,生成cipher
uint16_t encrypt_token(uint32_t timestamp, uint32_t round_key) {
uint32_t state = timestamp;

for (int round = 1; round <= 3; round++) {
uint16_t rk = generate_round_key(round_key, round);
state ^= rk;
state = perform_round(state);
}

uint16_t rk4 = generate_round_key(round_key, 4);
state ^= rk4;
state = apply_sbox(state);

uint16_t rk5 = generate_round_key(round_key, 5);
uint16_t final_state = (uint16_t)(state ^ rk5);
return final_state;
}

// 计算字符串的MD5并转hex
void compute_md5_hex(const char* input, char output[33]) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)input, strlen(input), digest);
for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
sprintf(output + i * 2, "%02x", digest[i]);
}
output[32] = 0;
}

// 计算字符串的SHA1并转hex
void compute_sha1_hex(const char* input, char output[41]) {
unsigned char digest[SHA_DIGEST_LENGTH];
SHA1((const unsigned char*)input, strlen(input), digest);
for (int i = 0; i < SHA_DIGEST_LENGTH; i++) {
sprintf(output + i * 2, "%02x", digest[i]);
}
output[40] = 0;
}

int main() {
char query[512];
char md5_str[33];
char sha1_str[41];

for (uint32_t t = 1751990400; t <= 1752052051; t++) {
uint32_t state = t;
uint32_t output;
prng_generate(&state, &output);
uint32_t cnt = output;

uint32_t a = 0, b = 0, x = 0, y = 0;
uint32_t i = 0;

while (i < cnt) {
prng_generate(&state, &a);
prng_generate(&state, &b);
prng_generate(&state, &x);
prng_generate(&state, &y);
prng_generate(&state, &cnt);
i++;
}

prng_generate(&state, &output);
uint32_t r = output;

int32_t sa = to_signed32(a);
int32_t sb = to_signed32(b);
int32_t sx = to_signed32(x);
int32_t sy = to_signed32(y);

double val1 = pow((double)(sa | sx), 2.0);
double val2 = pow((double)(sb | sy), 2.0);

if (fabs(0x61 * val1 - 0xb * val2) < 1e-9) {
uint16_t cipher = encrypt_token(t, r);
snprintf(query, sizeof(query), "salt=%s&t=%u&r=%u&cipher=%u",
SALT, t, r, cipher);
} else {
snprintf(query, sizeof(query), "salt=%s&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u",
SALT, t, r, a, b, x, y);
}

compute_md5_hex(query, md5_str);
if (strcmp(md5_str, TARGET_MD5) == 0) {
compute_sha1_hex(query, sha1_str);
printf("[+] Found!\nQuery: %s\nMD5: %s\nSHA1: %s\n",
query, md5_str, sha1_str);
break;
}
}
return 0;
}

image-20250716233822571

终焉之门

直接看看不到什么有用的代码,就直接动调,随便翻翻就看到了这个多层base64,解密一下hhhhh

后来知道这一段是用来循环异或加密得到主要逻辑的

image-20250717165521134

aVersion430Core动调的时候双击进去看看,主加密内容放在了.data段

image-20250718000630226

这段代码实现了一个运行在 GPU 上的简单虚拟机,通过执行opcodes中的指令序列,对栈数据进行运算

这里是核心校验

栈中的前 16 个数,必须等于 cipher[i] - 20

image-20250718215943468


还可以通过另一个方法得到主要的逻辑

我先搜索字符串,找到那么一串base64编码,然后交叉引用找到函数

image-20250719213918550

这个加密函数调用aVersion430Core的内容与base64编码进行循环异或

image-20250719214002648

写出代码可以直接跑出内容

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
 #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;
break;
}

case 15:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = int(a == b);
break;
}

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;
}
l

进程已结束,退出代码为 0


从这三个地址里得到opcodes,co_consts和cipher的内容

image-20250718225015357

这里贴出主函数的内容以及注释部分

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
__int64 __fastcall sub_7FF656701CF0(double a1)
{
int v1; // ebx
__m128i v2; // xmm6
unsigned int v3; // eax
unsigned int v4; // r13d
unsigned int v5; // eax
int v6; // eax
__int64 v7; // rdi
bool v8; // dl
bool v9; // al
int v10; // eax
char *v12; // r15
unsigned int v13; // ebx
int v14; // eax
unsigned int v15; // r9d
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]

v1 = 0;
sub_7FF6566FE370();
sub_7FF656693480(8256);
sub_7FF65668F730(0x500u, 0x320u, "Password Checker");// 创建窗口
sub_7FF656691100(&v26, 0i64, aVersion330Defi);
v2 = _mm_loadu_si128(&v26);
v3 = sub_7FF65667E700(aVersion430Core, 37305i64);// 创建计算着色器
v20 = sub_7FF65667EEE0(v3); // 创建着色器程序
v21 = sub_7FF65667EFF0(0x2A0u, &unk_7FF6567030E0, 0x88EAu);// opcodes
v4 = sub_7FF65667EFF0(0x80u, &dword_7FF656703060, 0x88EAu);// co_consts
v22 = sub_7FF65667EFF0(0x40u, &unk_7FF656703020, 0x88EAu);// cipher
v23 = sub_7FF65667EFF0(0x400u, &unk_7FF65675F040, 0x88EAu);// stack
v5 = sub_7FF65667EFF0(4u, &dword_7FF656703000, 0x88EAu);// verdict
v33[0] = 0i64;
v24 = v5;
*Str = 0i64;
v29 = 0i64;
v30 = 0i64;
v31 = 0i64;
memset(v32, 0, sizeof(v32));
*(v33 + 5) = 0i64;
sub_7FF6566931A0(60);
while ( !sub_7FF65668CAC0() )
{
v6 = sub_7FF656695A40(); // 输入
if ( v6 > 0 && v1 <= 99 )
{
v7 = v1 + 1;
do
{
Str[v7 - 1] = v6;
v1 = v7;
v6 = sub_7FF656695A40();
v8 = v7++ <= 99;
}
while ( v8 && v6 > 0 );
}
v9 = sub_7FF6566958E0(259);
if ( v1 > 0 && v9 )
Str[--v1] = 0;
if ( sub_7FF6566958E0(257) && strlen(Str) == 40 && !strncmp(Str, "L3HCTF{", 7ui64) && HIBYTE(v32[0]) == 125 )
{ // 提取花括号内的32个字符
v25 = v1;
v12 = &Str[7];
v13 = 0;
do // 将十六进制字符串转换为数值
{
v17 = *v12;
v18 = v12[1];
if ( v17 > 96 )
v14 = v17 - 87; // 字符转数字 (0-9, a-f, A-F)
else
v14 = v17 - 48;
v19 = 16 * v14;
v15 = v13;
v16 = v18 - 48;
if ( v18 >= 97 )
v16 = v18 - 87;
v12 += 2;
v13 += 4;
v27 = v16 + v19; // 组合成一个字节
// 32 个字符被转换为 16 个整数,每个整数由 2 个十六进制字符组成
sub_7FF65667F0B0(v4, &v27, 4u, v15); // 将转换后的字节写入co_consts缓冲区
}
while ( v32 + 7 != v12 );
v1 = v25;
sub_7FF65667C100(v20); // 执行计算着色器
sub_7FF65667F180(v21, 0i64); // opcodes
sub_7FF65667F180(v4, 2i64); // co_consts
sub_7FF65667F180(v22, 3i64); // cipher
sub_7FF65667F180(v23, 4i64);
sub_7FF65667F180(v24, 5i64);
sub_7FF65667EFE0(1i64, 1i64, 1i64); // 执行着色器
sub_7FF65667F140(v24, &dword_7FF656703000, 4i64, 0i64);
sub_7FF65667C110();
}
sub_7FF65668FC90(a1);
v26 = v2;
sub_7FF656690650(&v26);
a1 = sub_7FF65668E170();
v26 = v2;
*&a1 = a1;
v27 = LODWORD(a1);
v10 = sub_7FF656691440(&v26);
v26 = v2;
sub_7FF656691460(v26.m128i_i64, v10, &v27, 0);
sub_7FF6566AB9D0(0, 0, 1280i64, 0x320u, 0xFFFFFFFF);
sub_7FF656690690();
sub_7FF6566BDA20(Str, 0x64u, 0xC8u, 40, -16777216);
if ( dword_7FF656703000 == 1 )
sub_7FF6566BDA20("success", 0x64u, 0x12Cu, 40, -13863680);
else
sub_7FF6566BDA20("wrong password", 0x64u, 0x12Cu, 40, -13162010);
sub_7FF6566BDA20("Type password and press [Enter] to check!", 0x64u, 0x64u, 20, -8224126);
sub_7FF6566BDA20("Press [Backspace] to delete characters.", 0x64u, 0x82u, 20, -8224126);
sub_7FF656695CE0();
}
sub_7FF65668FAA0();
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
STACK_SIZE = 256
MAX_OPCODES = 1000

def main():
# 指令流
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
]

# 常量池(输入)
co_consts = [
0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF,
0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00
]

# cipher(目标值)
cipher = [
0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE,
0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146, 0x97
]

# 初始化栈
stack_data = [0] * STACK_SIZE
sp = 0

# 执行指令
i = 0
while i < len(opcodes):
opcode = opcodes[i]
arg = opcodes[i + 1]

sp0 = sp # 保存执行前的栈指针位置

if opcode == 2:
# PUSH co_consts[arg]
v = co_consts[arg] if arg < len(co_consts) else 0
stack_data[sp] = v
print(f"[IP={i // 2}]\tstack_data[{sp}] = co_consts[{arg}] = 0x{v:X}")
sp += 1

elif opcode == 7:
# ADD
b = stack_data[sp - 1]
a = stack_data[sp - 2]
sp -= 2
stack_data[sp] = a + b
print(f"[IP={i // 2}]\tstack_data[{sp}] = a + b = stack_data[{sp0 - 2}] + stack_data[{sp0 - 1}] = 0x{a + b:X}")
sp += 1

elif opcode == 8:
# SUB a - b
b = stack_data[sp - 1]
a = stack_data[sp - 2]
sp -= 2
stack_data[sp] = a - b
print(f"[IP={i // 2}]\tstack_data[{sp}] = a - b = stack_data[{sp0 - 2}] - stack_data[{sp0 - 1}] = 0x{a - b:X}")
sp += 1

elif opcode == 14:
# XOR
b = stack_data[sp - 1]
a = stack_data[sp - 2]
sp -= 2
stack_data[sp] = a ^ b
print(f"[IP={i // 2}]\tstack_data[{sp}] = a ^ b = stack_data[{sp0 - 2}] ^ stack_data[{sp0 - 1}] = 0x{a ^ b:X}")
sp += 1

elif opcode == 15:
# EQ
b = stack_data[sp - 1]
a = stack_data[sp - 2]
sp -= 2
result = 1 if a == b else 0
stack_data[sp] = result
print(f"[IP={i // 2}]\tstack_data[{sp}] = (a == b) = stack_data[{sp0 - 2}] == stack_data[{sp0 - 1}] = 0x{result:X}")
sp += 1

elif opcode == 16:
# VERIFY
print(f"[IP={i // 2}]\t=== VERIFY cipher check ===")
for j in range(16):
expected = cipher[j] - 20
actual = stack_data[j]
print(f" stack[{j:2d}]=0x{actual:X} vs cipher[{j:2d}]-20=0x{expected:X}")
print("✅ Verification complete.")
break # 假设验证后程序结束

elif opcode == 18:
# JZ
c = stack_data[sp - 1]
sp -= 1
print(f"[IP={i // 2}]\tJZ if top==0 jump to {arg} (top=0x{c:X})")
if c == 0:
i = arg * 2 - 2 # 跳转到指定指令位置
else:
print(f"[IP={i // 2}]\tUNKNOWN OPCODE {opcode}, abort")
return -1

i += 2 # 下一条指令

return 0

if __name__ == "__main__":
main()

过程:

每一步操作都是基于栈的,利用这三种运算异或(opcode=14)加法(opcode=7)减法(opcode=8)

stack [2] 的生成为例(对应目标 t2)

用户输入即co_consts的内容为x0-x15

步骤:

  • [IP=6] 加载 x2 到栈 stack[2] = 0xFA = x2;

  • [IP=7] 加载 x1 到栈 stack[3] = 0xC8 = x1;

  • [IP=8] 异或 stack[2] = stack[2] ^ stack[3] = x2 ^ x1;

  • [IP=9] 加载 0到栈 stack[3] = 0x0;

  • [IP=10] 减法 stack[2] = stack[2] - stack[3] = stack[2] - 0 = x2 ^ x1 。

    需要满足x2 ^ x1 = t2

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
[IP=0]	stack_data[0] = co_consts[0] = 0xB0
[IP=1] stack_data[1] = co_consts[1] = 0xC8
[IP=2] stack_data[2] = co_consts[0] = 0xB0
[IP=3] stack_data[1] = a ^ b = stack_data[1] ^ stack_data[2] = 0x78
[IP=4] stack_data[2] = co_consts[16] = 0x0
[IP=5] stack_data[1] = a - b = stack_data[1] - stack_data[2] = 0x78
[IP=6] stack_data[2] = co_consts[2] = 0xFA
[IP=7] stack_data[3] = co_consts[1] = 0xC8
[IP=8] stack_data[2] = a ^ b = stack_data[2] ^ stack_data[3] = 0x32
[IP=9] stack_data[3] = co_consts[17] = 0x0
[IP=10] stack_data[2] = a - b = stack_data[2] - stack_data[3] = 0x32
[IP=11] stack_data[3] = co_consts[3] = 0x86
[IP=12] stack_data[4] = co_consts[2] = 0xFA
[IP=13] stack_data[3] = a ^ b = stack_data[3] ^ stack_data[4] = 0x7C
[IP=14] stack_data[4] = co_consts[18] = 0x0
[IP=15] stack_data[3] = a + b = stack_data[3] + stack_data[4] = 0x7C
[IP=16] stack_data[4] = co_consts[4] = 0x6E
[IP=17] stack_data[5] = co_consts[3] = 0x86
[IP=18] stack_data[4] = a ^ b = stack_data[4] ^ stack_data[5] = 0xE8
[IP=19] stack_data[5] = co_consts[19] = 0x0
[IP=20] stack_data[4] = a + b = stack_data[4] + stack_data[5] = 0xE8
[IP=21] stack_data[5] = co_consts[5] = 0x8F
[IP=22] stack_data[6] = co_consts[4] = 0x6E
[IP=23] stack_data[5] = a ^ b = stack_data[5] ^ stack_data[6] = 0xE1
[IP=24] stack_data[6] = co_consts[20] = 0x0
[IP=25] stack_data[5] = a - b = stack_data[5] - stack_data[6] = 0xE1
[IP=26] stack_data[6] = co_consts[6] = 0xAF
[IP=27] stack_data[7] = co_consts[5] = 0x8F
[IP=28] stack_data[6] = a ^ b = stack_data[6] ^ stack_data[7] = 0x20
[IP=29] stack_data[7] = co_consts[21] = 0x0
[IP=30] stack_data[6] = a + b = stack_data[6] + stack_data[7] = 0x20
[IP=31] stack_data[7] = co_consts[7] = 0xBF
[IP=32] stack_data[8] = co_consts[6] = 0xAF
[IP=33] stack_data[7] = a ^ b = stack_data[7] ^ stack_data[8] = 0x10
[IP=34] stack_data[8] = co_consts[22] = 0x0
[IP=35] stack_data[7] = a + b = stack_data[7] + stack_data[8] = 0x10
[IP=36] stack_data[8] = co_consts[8] = 0xC9
[IP=37] stack_data[9] = co_consts[7] = 0xBF
[IP=38] stack_data[8] = a ^ b = stack_data[8] ^ stack_data[9] = 0x76
[IP=39] stack_data[9] = co_consts[23] = 0x0
[IP=40] stack_data[8] = a + b = stack_data[8] + stack_data[9] = 0x76
[IP=41] stack_data[9] = co_consts[9] = 0x64
[IP=42] stack_data[10] = co_consts[8] = 0xC9
[IP=43] stack_data[9] = a ^ b = stack_data[9] ^ stack_data[10] = 0xAD
[IP=44] stack_data[10] = co_consts[24] = 0x0
[IP=45] stack_data[9] = a + b = stack_data[9] + stack_data[10] = 0xAD
[IP=46] stack_data[10] = co_consts[10] = 0xD7
[IP=47] stack_data[11] = co_consts[9] = 0x64
[IP=48] stack_data[10] = a ^ b = stack_data[10] ^ stack_data[11] = 0xB3
[IP=49] stack_data[11] = co_consts[25] = 0x0
[IP=50] stack_data[10] = a + b = stack_data[10] + stack_data[11] = 0xB3
[IP=51] stack_data[11] = co_consts[11] = 0xC3
[IP=52] stack_data[12] = co_consts[10] = 0xD7
[IP=53] stack_data[11] = a ^ b = stack_data[11] ^ stack_data[12] = 0x14
[IP=54] stack_data[12] = co_consts[26] = 0x0
[IP=55] stack_data[11] = a + b = stack_data[11] + stack_data[12] = 0x14
[IP=56] stack_data[12] = co_consts[12] = 0xE3
[IP=57] stack_data[13] = co_consts[11] = 0xC3
[IP=58] stack_data[12] = a ^ b = stack_data[12] ^ stack_data[13] = 0x20
[IP=59] stack_data[13] = co_consts[27] = 0x0
[IP=60] stack_data[12] = a - b = stack_data[12] - stack_data[13] = 0x20
[IP=61] stack_data[13] = co_consts[13] = 0xEF
[IP=62] stack_data[14] = co_consts[12] = 0xE3
[IP=63] stack_data[13] = a ^ b = stack_data[13] ^ stack_data[14] = 0xC
[IP=64] stack_data[14] = co_consts[28] = 0x0
[IP=65] stack_data[13] = a - b = stack_data[13] - stack_data[14] = 0xC
[IP=66] stack_data[14] = co_consts[14] = 0x87
[IP=67] stack_data[15] = co_consts[13] = 0xEF
[IP=68] stack_data[14] = a ^ b = stack_data[14] ^ stack_data[15] = 0x68
[IP=69] stack_data[15] = co_consts[29] = 0x0
[IP=70] stack_data[14] = a + b = stack_data[14] + stack_data[15] = 0x68
[IP=71] stack_data[15] = co_consts[15] = 0x0
[IP=72] stack_data[16] = co_consts[14] = 0x87
[IP=73] stack_data[15] = a ^ b = stack_data[15] ^ stack_data[16] = 0x87
[IP=74] stack_data[16] = co_consts[30] = 0x0
[IP=75] stack_data[15] = a - b = stack_data[15] - stack_data[16] = 0x87
[IP=76] === VERIFY cipher check ===
stack[ 0]=0xB0 vs cipher[ 0]-20=0xDF
stack[ 1]=0x78 vs cipher[ 1]-20=0x6E
stack[ 2]=0x32 vs cipher[ 2]-20=0x-E
stack[ 3]=0x7C vs cipher[ 3]-20=0x1E9
stack[ 4]=0xE8 vs cipher[ 4]-20=0x13C
stack[ 5]=0xE1 vs cipher[ 5]-20=0x24
stack[ 6]=0x20 vs cipher[ 6]-20=0x9E
stack[ 7]=0x10 vs cipher[ 7]-20=0xCA
stack[ 8]=0x76 vs cipher[ 8]-20=0x146
stack[ 9]=0xAD vs cipher[ 9]-20=0x183
stack[10]=0xB3 vs cipher[10]-20=0x88
stack[11]=0x14 vs cipher[11]-20=0x1C3
stack[12]=0x20 vs cipher[12]-20=0x5A
stack[13]=0xC vs cipher[13]-20=0x14
stack[14]=0x68 vs cipher[14]-20=0x132
stack[15]=0x87 vs cipher[15]-20=0x83

最终解密代码

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, 0xFD, 0x50, 0x38, 0xB2, 0xDE, 0x5A, 0x97, 0x9C, 0xD7, 0x6E, 0x28, 0x46, 0x97]
target = [c - 20 for c in cipher]
co_consts_fixed = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87]

x = [0] * 16

x[0] = target[0]

x1_xor_x0 = target[1] + co_consts_fixed[0]
x[1] = x1_xor_x0 ^ x[0]
x1_xor_x0 = co_consts_fixed[0] - target[1]
x[1] = x1_xor_x0 ^ x[0]

x = [
0xDF, 0x9D, 0x4B, 0xA4, 0x12, 0x58, 0x57, 0x4C,
0xCB, 0x71, 0x55, 0xB9, 0xD0, 0x1F, 0x5C, 0x58
]

# 转换为32位十六进制(每个x[i]→2位,补0)
hex_str = ''.join(f"{num:02x}" for num in x)

# 最终密码
password = f"L3HCTF{{{hex_str}}}"
print("正确解密密码:")
print(password)

ez_android

这题也是花了一天多的时间才解决QAQ,真是不容易啊,还是得好好理解,多动动脑筋嘞~

直接在com目录下找到mainactivity,发现TauriActivity,不太清楚Tauri 框架是啥

在swdd的指导下先学习一下理论知识


TauriActivity

TauriActivity 是 Tauri 框架为 Android 平台提供的桥接 Activity 类,Tauri 是一个跨平台应用开发框架(主要用 Rust 编写),允许使用 Web 技术(HTML/JS/CSS) 构建前端,同时通过 Rust 后端实现逻辑。

TauriActivity 的主要功能:

  1. 提供一个 WebView 容器,加载应用前端(HTML/JS)。
  2. 通过 JNI 调用 Rust 编译的共享库(libtauri.so)。
  3. 提供文件选择、权限处理、JS ↔ Rust 通信接口。

Tauri 框架的静态资源提取方法:

先直接搜索关键词:index.html,交叉引用发现这里的包含文件名和文件位置的表

image-20250718175941196

以下代码把文件内容dump出来

1
2
3
4
5
6
7
8
9
10
11
12
import ida_bytes

addr = 0x00000000000C9498
size = 0xEB

dump = ida_bytes.get_bytes(addr, size)

file_path = r"C:\Users\38489\Desktop\index_html.br"
with open(file_path, "wb") as f:
f.write(dump)

print(f"[+] 提取完成,文件已保存到: {file_path} (大小 {len(dump)} bytes)")

由于我的idapython一直没法安装成功brotli,就先dump出来文件之后在本地解压缩

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

compressed_file_path = r"C:\Users\38489\Desktop\index_html.br"
output_file_path = r"C:\Users\38489\Desktop\dumpp"

with open(compressed_file_path, "rb") as f:
content = f.read()

print(f"Compressed file size: {len(content)} bytes")

def try_decompress(data):
try:
return brotli.decompress(data)
except brotli.error:
return None

decompressed = None
for i in range(len(content), 0, -1):
decompressed = try_decompress(content[:i])
if decompressed is not None:
break

if decompressed is not None:
with open(output_file_path, "wb") as f:
f.write(decompressed)
print(f"Decompressed content written to {output_file_path}")
else:
print("Failed to decompress the content.")

然后就可以读取html文件内容啦


这题的解题wp:

解包apk文件之后看看有啥内容

打开assets想要找到.js文件或者.html文件,但是只有prof文件,说明前端资源被打包或者压缩了,结合tauri框架,前端文件可能被打包进rust后端可执行文件里了

image-20250717161547585

ida反编译so文件

依照上面写的静态资源提取方法,解压缩index.html内容

image-20250718173951503

index内容

image-20250718182510695

加载核心 JS,index-BsFf5qny.js 是打包后的 入口 JS 文件。负责启动 Vue 应用、挂载到 #app,以及通过 Tauri 的 API 调用 Rust 后端。

1
<script type="module" crossorigin src="/assets/index-BsFf5qny.js"></script>

和上述过程一样,继续搜索关键词,找到这个js文件,dump出来后解压

image-20250718174211972

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 ida_bytes
import idc
import os

OUT_DIR = r"C:\Users\38489\Desktop"
RES_NAME = "index-BsFf5qny.js.br"

data_ea = idc.get_name_ea_simple("unk_C356F")
if data_ea == idc.BADADDR:
raise RuntimeError("找不到符号 unk_C356F,请确认名字一致。")

comp_size = 0x5D50

print(f"[+] Dumping resource from 0x{data_ea:X}, size 0x{comp_size:X} ({comp_size} bytes)")

blob = ida_bytes.get_bytes(data_ea, comp_size)
if blob is None:
raise RuntimeError("读取资源失败(可能地址或长度不对)。")

os.makedirs(OUT_DIR, exist_ok=True)
out_path = os.path.join(OUT_DIR, RES_NAME)
with open(out_path, "wb") as f:
f.write(blob)

print(f"[+] 写出压缩文件: {out_path} (size={len(blob)} bytes)")

1
2
3
4
5
6
7
8
9
10
11
12
13
import brotli

in_path = r"C:\Users\38489\Desktop\index-BsFf5qny.js.br"
out_path = r"C:\Users\38489\Desktop\index-BsFf5qny.js"

data = open(in_path, "rb").read()
try:
dec = brotli.decompress(data)
open(out_path, "wb").write(dec)
print("[+] 成功解压 ->", out_path, "长度", len(dec))
except brotli.error as e:
print("[-] 解压失败:", e)

解压出来之后,找到关键点:js与后端rust交互,后端接口是greet,

image-20250718183114865

直接在ida里搜索greet,找到函数,就是主加密函数

脚本

写脚本的时候还是要注意一下密文的提取,最后是v8的第19位与v19的第三位数据往后的八字节内容,当时脚本在密文这没好好分析,结果一直没出来,还是得注意一下

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

TABLE = bytes([
0x64,0x47,0x68,0x70,0x63,0x32,0x6C,0x7A,
0x59,0x57,0x74,0x6C,0x65,0x51,
0x57,0x72,0x6F,0x6E,0x67,0x20,0x61,0x6E,0x73,0x77,0x65,0x72
])

target_values = [
0x0A409663A025150C,
0x1FE106294065165C,
0xFC020A4C0E2C7290,
0x2A324F
]

full_data = b''.join(struct.pack('<Q', v) for v in target_values)
TARGET = full_data[:27]

def rol8(x, n): return ((x << (n&7)) | (x >> (8-(n&7)))) & 0xFF
def ror8(x, n): return ((x >> (n&7)) | (x << (8-(n&7)))) & 0xFF

def forward_byte(i, b_in):
idx_mix = i if i < 14 else i - 14
tblA = TABLE[idx_mix]
idx_dyn = (((2*i) | 1) - 14 * ((147 * ((2*i) | 1)) >> 11)) & 0xFF
tblB = TABLE[idx_dyn % len(TABLE)]
rot_src = TABLE[(i + 3) % 14]
xsrc = TABLE[(i + 4) % 14]
rot = rot_src & 7
v11 = (tblB + (b_in ^ tblA)) & 0xFF
return xsrc ^ rol8(v11, rot)

def reverse_byte(i, b_out):
idx_mix = i if i < 14 else i - 14
tblA = TABLE[idx_mix]
idx_dyn = (((2*i) | 1) - 14 * ((147 * ((2*i) | 1)) >> 11)) & 0xFF
tblB = TABLE[idx_dyn % len(TABLE)]
rot_src = TABLE[(i + 3) % 14]
xsrc = TABLE[(i + 4) % 14]
rot = rot_src & 7
r = b_out ^ xsrc
v11 = ror8(r, rot)
tmp = (v11 - tblB) & 0xFF
return tmp ^ tblA

recovered = bytes(reverse_byte(i, TARGET[i]) for i in range(len(TARGET)))
print("Recovered 27-byte input:", recovered)
print("Hex:", recovered.hex())

recheck = bytes(forward_byte(i, recovered[i]) for i in range(len(TARGET)))
print("Forward recompute matches TARGET?:", recheck == TARGET)

完结撒花~

另附,学习这位师傅的tauri框架静态资源提取的方法https://blog.yllhwa.com/2023/05/09/Tauri%20%E6%A1%86%E6%9E%B6%E7%9A%84%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%8F%90%E5%8F%96%E6%96%B9%E6%B3%95%E6%8E%A2%E7%A9%B6/


L3Hctf部分wp
https://j1nxem-o.github.io/2025/08/02/L3Hctf部分wp/
作者
J1NXEM
发布于
2025年8月2日
更新于
2025年8月23日
许可协议