NSSCTF 2025 4th Reverse WP

checkit

java 层

附件是 apk, java 层没什么东西
MainActivity 如下,关键在于调用了本地方法 checkInput

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
package com.test.ezre;

import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
private EditText inputEditText;
private TextView resultTextView;

public native String checkInput(String str);

static {
System.loadLibrary("check");
}

@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.inputEditText = (EditText) findViewById(R.id.editTextInput);
this.resultTextView = (TextView) findViewById(R.id.textViewResult);
}

public void onCheckClick(View view) {
String input = this.inputEditText.getText().toString().trim();
String result = checkInput(input);
this.resultTextView.setText(result);
}
}

native 层

导出 libcheck.so 文件
在 Java_com_test_ezre_MainActivity_checkInput 函数里面找到关键处 exec(code, *v4)

点进这个函数,再跳转一次后,发现是一个 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
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
171
172
173
174
175
176
177
178
179
180
181
182
__int64 __fastcall exec(__int64 a1, unsigned __int8 *a2)
{
__int64 result; // x0
int v3; // w8
__int64 v4; // x10
int v5; // w9
int v6; // [xsp+1Ch] [xbp-D4h]

result = __memset_chk(byte_4CE8, 0LL, 200LL, 200LL);
*((_DWORD *)a2 + 4) = 0;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v6 = *(_DWORD *)(a1 + 4LL * *((int *)a2 + 2));
if ( v6 != 0x22 )
break;
*a2 += a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x23 )
break;
*a2 -= a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x24 )
break;
*a2 *= a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x25 )
break;
if ( !a2[3] )
return __strlcpy_chk(byte_4CE8, "Error: Division by zero", 200LL, 200LL);
*a2 /= a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x26 )
break;
*a2 ^= a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x27 )
break;
if ( *((int *)a2 + 3) > 0x1F3 )
return __strlcpy_chk(byte_4CE8, "Error: Stack overflow", 200LL, 200LL);
v3 = *a2;
v4 = (int)(*((_DWORD *)a2 + 3))++;
stack[v4] = v3;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x28 )
break;
if ( *((int *)a2 + 3) < 1 )
return __strlcpy_chk(byte_4CE8, "Error: Stack underflow", 200LL, 200LL);
v5 = *((_DWORD *)a2 + 3) - 1;
*((_DWORD *)a2 + 3) = v5;
*a2 = stack[v5];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x29 )
break;
if ( a2[1] > 0x32u )
{
*a2 = 0;
}
else
{
result = ((__int64 (__fastcall *)(void *))__emutls_get_address)(&unk_44D8);
*a2 = *(_BYTE *)(result + a2[1]);
}
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x2A )
break;
*a2 = a2[3];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x2B )
break;
a2[1] = *a2;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x2C )
break;
a2[2] = *a2;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x2D )
break;
a2[3] = *a2;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x2E )
break;
*a2 = *(_DWORD *)(a1 + 4LL * *((int *)a2 + 2) + 4);
*((_DWORD *)a2 + 2) += 2;
}
if ( v6 != 0x2F )
break;
*a2 = a2[1];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x30 )
break;
if ( a2[1] > 0x31u )
{
a2[4] = 1;
return result;
}
*a2 = cmp_data[a2[1]];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x31 )
break;
++*a2;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 0x32 )
break;
if ( --a2[2] )
*((_DWORD *)a2 + 2) -= *(_DWORD *)(a1 + 4LL * *((int *)a2 + 2) + 4);
else
*((_DWORD *)a2 + 2) += 2;
}
if ( v6 != 0x33 )
break;
++*((_DWORD *)a2 + 4);
if ( a2[1] >= 0x32u )
{
a2[4] = 1;
return result;
}
if ( !a2[4] && *a2 != a2[3] )
a2[4] = 1;
--a2[1];
++*((_DWORD *)a2 + 2);
}
if ( v6 != 52 )
break;
++*((_DWORD *)a2 + 2);
}
if ( v6 != 255 )
return sub_17B8(byte_4CE8, 200LL, 200LL, "Unknown opcode: %d", v6);
if ( a2[4] || *((_DWORD *)a2 + 4) != 50 )
return __strlcpy_chk(byte_4CE8, "Maybe something wrong??(存在不匹配元素)", 200LL, 200LL);
return __strlcpy_chk(byte_4CE8, "oh!You are right!", 200LL, 200LL);
}

操作码就是刚刚的 code

在 vm 还中发现一个 cmp_data

分析这个 vm 可以知道它使用了四个寄存器来存值,第一个用于临时存储和算术运算,第二个用于索引输入字符串和 cmp_data 数组,第三个用于循环计数,第四个用于临时存储,把它们记为 A、B、C、D,还用了一个栈来存储数据
同构出 vm python 脚本来 trace 加密过程,跟之前写的 vm 同构脚本相比这次在 ai 的帮助下换了一种写法,写了一个单独的 trace 函数来打印每一步的状态,感觉比原来的要便捷很多

同构 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
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
opcode = [
0x34, 0x2E, 0, 0x2B, 0x2E, 0x19, 0x2C, 0x29, 0x2D,
0x2E, 0x4E, 0x22, 0x27, 0x2E, 0x53, 0x2D, 0x28, 0x23,
0x2D, 0x2E, 0x53, 0x26, 0x27, 0x2F, 0x31, 0x2B, 0x2E,
0x43, 0x2D, 0x29, 0x26, 0x2D, 0x2E, 0x54, 0x22, 0x27,
0x2E, 0x46, 0x2D, 0x28, 0x23, 0x27, 0x2F, 0x31, 0x2B,
0x32, 0x26, 0x2E, 0x31, 0x2B, 0x2E, 0x32, 0x2C, 0x30,
0x2D, 0x28, 0x33, 0x32, 4, 0xFF
]

cmp_data = [
0x1A, 0x1E, 0x1D, 0x0E, 0x1C, 0x13, 0x25, 0x0E, 0x78,
0x3B, 0x31, 0x3F, 0x68, 0x45, 0x23, 0x3D, 0x0F, 0x45,
0x37, 0x3A, 0x3A, 0x70, 0x07, 0x81, 0x1A, 0x2A, 0x3D,
0x7E, 0x7D, 0x3C, 0x09, 0x82, 0x39, 0x2A, 0x0E, 0x7E,
0x09, 0x32, 0x19, 0x81, 0x0C, 0x2A, 0x68, 0x45, 0x09,
0x43, 0x3B, 0x70, 0x4F, 0x4C,
]

input_str = "12345678901234567890123456789"

# 寄存器
A = B = C = D = 0
stack = []
IP = 0
step = 0
error = 0

def trace(msg):
global step, A, B, C, D, IP, stack
print(f"Step {step:2d}: IP={IP:2d} | A={A:02X} B={B:02X} C={C:02X} D={D:02X} | {msg}")
print(f" Stack: {[f'{x:02X}' for x in stack]}")

while IP < len(opcode):
op = opcode[IP]
step += 1

if op == 0x22: # A += D
A = (A + D) & 0xFF
trace("A += D")
IP += 1

elif op == 0x23: # A -= D
A = (A - D) & 0xFF
trace("A -= D")
IP += 1

elif op == 0x26: # A ^= D
A ^= D
trace("A ^= D")
IP += 1

elif op == 0x27: # push A
stack.append(A)
trace("push A")
IP += 1

elif op == 0x28: # pop A
if stack:
A = stack.pop()
trace("pop A")
else:
trace("ERROR: stack underflow")
error = 1
break
IP += 1

elif op == 0x29: # A = input[B]
if B < len(input_str):
A = ord(input_str[B])
trace(f"A = input[{B}] = '{input_str[B]}' ({A:02X})")
else:
trace("ERROR: input index OOB")
error = 1
break
IP += 1

elif op == 0x2B: # B = A
B = A
trace("B = A")
IP += 1

elif op == 0x2C: # C = A
C = A
trace("C = A")
IP += 1

elif op == 0x2D: # D = A
D = A
trace("D = A")
IP += 1

elif op == 0x2E: # load immediate
if IP + 1 < len(opcode):
A = opcode[IP+1]
trace(f"load immediate {A:02X}")
IP += 2
else:
trace("ERROR: no immediate")
error = 1
break

elif op == 0x2F: # A = B
A = B
trace("A = B")
IP += 1

elif op == 0x30: # A = cmp_data[B]
if B < len(cmp_data):
A = cmp_data[B]
trace(f"A = cmp_data[{B}] = {A:02X}")
else:
trace("ERROR: cmp_data index OOB")
error = 1
break
IP += 1

elif op == 0x31: # A++
A = (A+1) & 0xFF
trace("A++")
IP += 1

elif op == 0x32: # C-- and jump if not zero
C = (C-1) & 0xFF
if C != 0:
if IP+1 < len(opcode):
offset = opcode[IP+1]
IP = (IP + offset) & 0xFFFF
trace(f"C--={C}, jump to {IP}")
else:
trace("ERROR: no jump offset")
error = 1
break
else:
trace(f"C--={C}, no jump")
IP += 2

elif op == 0x33: # compare A and D
if A != D:
trace(f"ERROR: compare fail A={A:02X} D={D:02X}")
error = 1
break
else:
trace(f"compare OK A={A:02X} == D={D:02X}")
IP += 1

elif op == 0x34:
trace("init/unknown")
IP += 1

elif op == 0xFF:
trace("end")
break

else:
trace(f"ERROR: unknown opcode {op:02X}")
error = 1
break

运行结果是

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
Step  1: IP= 0 | A=00 B=00 C=00 D=00 | init/unknown
Stack: []
Step 2: IP= 1 | A=00 B=00 C=00 D=00 | load immediate 00
Stack: []
Step 3: IP= 3 | A=00 B=00 C=00 D=00 | B = A
Stack: []
Step 4: IP= 4 | A=19 B=00 C=00 D=00 | load immediate 19
Stack: []
Step 5: IP= 6 | A=19 B=00 C=19 D=00 | C = A
Stack: []
Step 6: IP= 7 | A=31 B=00 C=19 D=00 | A = input[0] = '1' (31)
Stack: []
Step 7: IP= 8 | A=31 B=00 C=19 D=31 | D = A
Stack: []
Step 8: IP= 9 | A=4E B=00 C=19 D=31 | load immediate 4E
Stack: []
Step 9: IP=11 | A=7F B=00 C=19 D=31 | A += D
Stack: []
Step 10: IP=12 | A=7F B=00 C=19 D=31 | push A
Stack: ['7F']
Step 11: IP=13 | A=53 B=00 C=19 D=31 | load immediate 53
Stack: ['7F']
Step 12: IP=15 | A=53 B=00 C=19 D=53 | D = A
Stack: ['7F']
Step 13: IP=16 | A=7F B=00 C=19 D=53 | pop A
Stack: []
Step 14: IP=17 | A=2C B=00 C=19 D=53 | A -= D
Stack: []
Step 15: IP=18 | A=2C B=00 C=19 D=2C | D = A
Stack: []
Step 16: IP=19 | A=53 B=00 C=19 D=2C | load immediate 53
Stack: []
Step 17: IP=21 | A=7F B=00 C=19 D=2C | A ^= D
Stack: []
Step 18: IP=22 | A=7F B=00 C=19 D=2C | push A
Stack: ['7F']
Step 19: IP=23 | A=00 B=00 C=19 D=2C | A = B
Stack: ['7F']
Step 20: IP=24 | A=01 B=00 C=19 D=2C | A++
Stack: ['7F']
Step 21: IP=25 | A=01 B=01 C=19 D=2C | B = A
Stack: ['7F']
Step 22: IP=26 | A=43 B=01 C=19 D=2C | load immediate 43
Stack: ['7F']
Step 23: IP=28 | A=43 B=01 C=19 D=43 | D = A
Stack: ['7F']
Step 24: IP=29 | A=32 B=01 C=19 D=43 | A = input[1] = '2' (32)
Stack: ['7F']
Step 25: IP=30 | A=71 B=01 C=19 D=43 | A ^= D
Stack: ['7F']
Step 26: IP=31 | A=71 B=01 C=19 D=71 | D = A
Stack: ['7F']
Step 27: IP=32 | A=54 B=01 C=19 D=71 | load immediate 54
Stack: ['7F']
Step 28: IP=34 | A=C5 B=01 C=19 D=71 | A += D
Stack: ['7F']
Step 29: IP=35 | A=C5 B=01 C=19 D=71 | push A
Stack: ['7F', 'C5']
Step 30: IP=36 | A=46 B=01 C=19 D=71 | load immediate 46
Stack: ['7F', 'C5']
Step 31: IP=38 | A=46 B=01 C=19 D=46 | D = A
Stack: ['7F', 'C5']
Step 32: IP=39 | A=C5 B=01 C=19 D=46 | pop A
Stack: ['7F']
Step 33: IP=40 | A=7F B=01 C=19 D=46 | A -= D
Stack: ['7F']
Step 34: IP=41 | A=7F B=01 C=19 D=46 | push A
Stack: ['7F', '7F']
Step 35: IP=42 | A=01 B=01 C=19 D=46 | A = B
Stack: ['7F', '7F']
Step 36: IP=43 | A=02 B=01 C=19 D=46 | A++
Stack: ['7F', '7F']
Step 37: IP=44 | A=02 B=02 C=19 D=46 | B = A
Stack: ['7F', '7F']
Step 38: IP=83 | A=02 B=02 C=18 D=46 | C--=24, jump to 83
Stack: ['7F', '7F']

里面加载了几个立即数,打印出来可以发现是 flag 的开头

1
2
3
4
data = [0x19,0x4E,0x53,0x53,0x43,0x54,0x46]
print([chr(x) for x in data])

# ['\x19', 'N', 'S', 'S', 'C', 'T', 'F']

分析 解密

同时还可以看出对奇数索引和偶数索引的处理方式不同
input[0] (偶数索引)

1
2
3
4
5
6
7
8
A = input[0]
D = input[0]
A = 0x4E
A = 0x4E + input[0]
A = 0x4E + input[0] - 0x53
= input - 0x05
D = input - 0x05
A = 0x53 ^ (input - 0x05)

input[1] (奇数索引)

1
2
3
4
5
6
A = input[1]
A = input[1] ^ 0x43
D = input[1] ^ 0x43
A = 0x54 + (input[1] ^ 0x43)
A = 0x54 + (input[1] ^ 0x43)- 0x46
= 0x0E + (input[1] ^ 0x43)

总结出来加密逻辑大概是

1
2
3
4
if i % 2 ==0:
enc[i] = (input[i] - 0x05) ^ 0x53
else:
enc[i] = (input[i] ^ 0x43) + 0x0E

那么解密就应该是

1
2
3
4
if i % 2 == 0:
dec[i] = (enc[i] ^ 0x53) + 0x05
else:
dec[i] = (enc[i] - 0x0E) ^ 0x43

从 C 的值可以看出,一共有 0x19(25) 轮加密,一次加密两个字符,而刚好 cmp_data 的长度为50
解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enc = [
0x1A, 0x1E, 0x1D, 0x0E, 0x1C, 0x13, 0x25, 0x0E, 0x78,
0x3B, 0x31, 0x3F, 0x68, 0x45, 0x23, 0x3D, 0x0F, 0x45,
0x37, 0x3A, 0x3A, 0x70, 0x07, 0x81, 0x1A, 0x2A, 0x3D,
0x7E, 0x7D, 0x3C, 0x09, 0x82, 0x39, 0x2A, 0x0E, 0x7E,
0x09, 0x32, 0x19, 0x81, 0x0C, 0x2A, 0x68, 0x45, 0x09,
0x43, 0x3B, 0x70, 0x4F, 0x4C
]
# print(len(cmp_data))
dec = []
for i in range(50):
if i % 2 == 0:
dec.append((enc[i] ^ 0x53) + 0x05)
else:
dec.append((enc[i] - 0x0E) ^ 0x43)

flag = ''.join(chr(c) for c in dec)
print(flag)

flag 为 NSSCTF{C0ngr@tulation!Y0N_s33m_7o_b3_gO0d_@t_vm!!}

CrackMe&F***Me

开始做的时候给我看得一头雾水,找不到方向,梳理做完之后还是有收获的 (讨厌这个出题人.jpg

魔改 PyInstaller

附件(改名为 crack.exe了) 用 DIE 查看是 PyInstaller 打包的,起初直接用工具 pyinstxtractor-2025.02 解包失败了
python pyinstxtractor.py crack.exe
报错如下

Missing cookie 说明 exe 中找不到 PyInstaller 的标识
然后就尝试用 010 editor 来修复文件,找了两能够被正常解包的 python 程序来进行对比,最后发现这样改成这样的话是能够被识别然后解包的(解包时用与打包相同的 python 版本,此处是用的 3.8)

再次解包,仍然有报错

成功识别 PyInstaller 版本,但在解压 exe 内部的模块是发生错误

解决办法仍是把这个程序和正常打包的 python 程序在 ida 中进行对比,观察哪个函数进行了魔改,从头对比,观察到在 sub_140001450 处函数的地址不一样了,说明上个函数(sub_1400011F0)就是被魔改的函数

魔改的 sub_1400011F0

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
171
172
173
174
175
__int64 __fastcall sub_1400011F0(FILE *Stream_1, __int64 a2, FILE *Stream_2, char *a4)
{
void *Buffer_2; // rbp
unsigned int v8; // eax
unsigned int v9; // r12d
__m128i *Buffer; // r15
int *v12; // rax
int *v13; // rax
int v14; // edi
unsigned __int64 n0x4000_1; // r13
__m128 si128; // xmm6
size_t n0x4000; // rbx
unsigned __int64 n0x4000_4; // rdx
const __m128i *v19; // rax
__m128 v20; // xmm0
__m128i *v21; // rax
size_t v22; // rcx
int n2; // eax
int n2_1; // edi
size_t ElementCount; // rbx
int v26; // [rsp+20h] [rbp-B8h]
__m128i *p_Buffer; // [rsp+30h] [rbp-A8h] BYREF
int n0x4000_2; // [rsp+38h] [rbp-A0h]
void *Buffer_3; // [rsp+40h] [rbp-98h]
unsigned int n0x4000_3; // [rsp+48h] [rbp-90h]
__int64 v31; // [rsp+60h] [rbp-78h]
__int64 v32; // [rsp+68h] [rbp-70h]
__int64 v33; // [rsp+70h] [rbp-68h]

v31 = 0;
v32 = 0;
v33 = 0;
n0x4000_2 = 0;
Buffer_2 = 0;
p_Buffer = 0;
v8 = sub_14000C390(&p_Buffer, "1.3.1", 88);
v9 = v8;
if ( v8 )
{
sub_140003390("Failed to extract %s: inflateInit() failed with return code %d!\n", (const char *)(a2 + 18), v8);
return 0xFFFFFFFFLL;
}
Buffer = (__m128i *)j__malloc_base(0x4000u);
if ( Buffer )
{
Buffer_2 = j__malloc_base(0x4000u);
if ( !Buffer_2 )
{
v13 = errno();
sub_140003560(
"malloc",
(unsigned int)*v13,
"Failed to extract %s: failed to allocate temporary output buffer!\n",
(const char *)(a2 + 18));
goto LABEL_42;
}
v9 = -1;
v14 = 1;
n0x4000_1 = *(unsigned int *)(a2 + 8);
si128 = (__m128)_mm_load_si128((const __m128i *)&xmmword_14002EA50);
v26 = 1;
LABEL_8:
n0x4000 = n0x4000_1;
if ( n0x4000_1 > 0x4000 )
n0x4000 = 0x4000;
if ( fread(Buffer, 1u, n0x4000, Stream_1) != n0x4000 || ferror(Stream_1) )
goto LABEL_42;
n0x4000_1 -= n0x4000;
if ( !v14 )
{
LABEL_26:
n0x4000_2 = n0x4000;
p_Buffer = Buffer;
while ( 1 )
{
Buffer_3 = Buffer_2;
n0x4000_3 = 0x4000;
n2 = sub_14000A7D0(&p_Buffer, 0);
n2_1 = n2;
v9 = -1;
if ( (unsigned int)(n2 + 4) <= 2 )
break;
if ( n2 == 2 )
{
n2_1 = -3;
break;
}
ElementCount = 0x4000LL - n0x4000_3;
if ( Stream_2 )
{
if ( fwrite(Buffer_2, 1u, ElementCount, Stream_2) != ElementCount || ferror(Stream_2) )
{
n2_1 = -1;
break;
}
}
else if ( a4 )
{
memcpy(a4, Buffer_2, ElementCount);
a4 += ElementCount;
}
if ( n0x4000_3 )
{
if ( n2_1 == 1 )
{
v9 = 0;
goto LABEL_42;
}
if ( n0x4000_1 )
{
v14 = v26;
goto LABEL_8;
}
break;
}
}
sub_140003390("Failed to extract %s: decompression resulted in return code %d!\n", (const char *)(a2 + 18), n2_1);
goto LABEL_42;
}
if ( n0x4000 >= 4
&& Buffer->m128i_i8[0] == 0x73 // 's'
&& Buffer->m128i_i8[1] == 119 // 'w'
&& Buffer->m128i_i8[2] == 100 // 'd'
&& Buffer->m128i_i8[3] == 100 // 'd'
)
{
n0x4000 -= 4LL;
memcpy(Buffer, (char *)Buffer->m128i_i64 + 4, n0x4000);
n0x4000_4 = 0;
if ( n0x4000 )
{
if ( n0x4000 < 0x40 )
goto LABEL_23;
v19 = Buffer + 2;
do
{
v20 = (__m128)_mm_loadu_si128(v19 - 2);
v19 += 4;
n0x4000_4 += 64LL;
v19[-6] = (const __m128i)_mm_xor_ps(si128, v20);
v19[-5] = (const __m128i)_mm_xor_ps((__m128)_mm_loadu_si128(v19 - 5), si128);
v19[-4] = (const __m128i)_mm_xor_ps((__m128)_mm_loadu_si128(v19 - 4), si128);
v19[-3] = (const __m128i)_mm_xor_ps(si128, (__m128)_mm_loadu_si128(v19 - 3));
}
while ( n0x4000_4 < (n0x4000 & 0xFFFFFFFFFFFFFFC0uLL) );
}
if ( n0x4000_4 < n0x4000 )
{
LABEL_23:
v21 = &Buffer[n0x4000_4 / 0x10];
v22 = n0x4000 - n0x4000_4;
do
{
v21->m128i_i8[0] ^= 0xAAu;
v21 = (__m128i *)((char *)v21 + 1);
--v22;
}
while ( v22 );
}
}
v26 = 0;
goto LABEL_26;
}
v12 = errno();
sub_140003560(
"malloc",
(unsigned int)*v12,
"Failed to extract %s: failed to allocate temporary input buffer!\n",
(const char *)(a2 + 18));
LABEL_42:
sub_14000C070(&p_Buffer);
free(Buffer);
free(Buffer_2);
return v9;
}

与正常的相比,这个的主要改动在于:
检测首 4 字节如果是 “swdd“ 则将 buffer 向前移 4 字节(丢弃 “swdd” )
数据块前 64 字节分组:每组 64 字节 = 16B × 4,用 16B 的常量向量si128 异或,剩余不足 64 字节的部分每字节 ^0xAA

研究 pyinstxtractor.py 源码,修改,添加上述功能

添加处理标头 swdd 和进行 xor 的函数

1
2
3
4
5
6
def _maybe_xor_decode(self, data):
if data.startswith(b"swdd"):
print("[+] Detected XOR-wrapped block, decoding...")
data = data[4:] # 去掉头 "swdd"
data = bytes([b ^ 0xAA for b in data])
return data

并且在 extractFiles 函数中新增了一行
data = self._maybe_xor_decode(data)

使用修改后的 pyinstxtractor.py(完整代码贴在文末了) 重新解包,这次全部成功了

pyc 反编译 解密

在提取出的 crack.exe_extracted 文件夹中找到 CreackMe&&FxxMe.pyc,在线反编译一下,得到

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
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.8

import binascii

def arc_cipher(data = None, key = None):
'''自实现 RC4 加密/解密'''
S = list(range(256))
j = 0
key_length = len(key)
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i] = S[j]
S[j] = S[i]
i = 0
j = 0
out = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i] = S[j]
S[j] = S[i]
K = S[(S[i] + S[j]) % 256]
out.append(byte ^ K)
return bytes(out)

target_hex = 'd29b81e136efc517c2967b863f584baf4b82f710f8869f5a56185cb22a9a25fc'
target_bytes = bytes.fromhex(target_hex)
key = b'NSSCTF'
user_input = input('Enter input: ').encode()
encrypted = arc_cipher(user_input, key)
if encrypted == target_bytes:
print(f'''Your are right, flag is NSSCTF{{{user_input.decode()}}}''')
else:
print('Wrong input')

开始看到中间两行的 S[i] = S[j]S[j] = S[i]以为是魔改,还去整了好一会儿爆破脚本…
后面直接当成标准 RC4 来处理,得到 flag 了

flag 为 NSSCTF{4984aa7eeb8c7fa0709832e364e03989}

pyinstxtractor_modified.py

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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
from __future__ import print_function
import os
import struct
import marshal
import zlib
import sys
from uuid import uuid4 as uniquename


class CTOCEntry:
def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
self.position = position
self.cmprsdDataSize = cmprsdDataSize
self.uncmprsdDataSize = uncmprsdDataSize
self.cmprsFlag = cmprsFlag
self.typeCmprsData = typeCmprsData
self.name = name


class PyInstArchive:
PYINST20_COOKIE_SIZE = 24 # For pyinstaller 2.0
PYINST21_COOKIE_SIZE = 24 + 64 # For pyinstaller 2.1+
MAGIC = b'MEI\014\013\012\013\016' # Magic number which identifies pyinstaller

def __init__(self, path):
self.filePath = path
self.pycMagic = b'\0' * 4
self.barePycList = [] # List of pyc's whose headers have to be fixed

def open(self):
try:
self.fPtr = open(self.filePath, 'rb')
self.fileSize = os.stat(self.filePath).st_size
except:
print('[!] Error: Could not open {0}'.format(self.filePath))
return False
return True

def close(self):
try:
self.fPtr.close()
except:
pass

def checkFile(self):
print('[+] Processing {0}'.format(self.filePath))

searchChunkSize = 8192
endPos = self.fileSize
self.cookiePos = -1

if endPos < len(self.MAGIC):
print('[!] Error : File is too short or truncated')
return False

while True:
startPos = endPos - searchChunkSize if endPos >= searchChunkSize else 0
chunkSize = endPos - startPos

if chunkSize < len(self.MAGIC):
break

self.fPtr.seek(startPos, os.SEEK_SET)
data = self.fPtr.read(chunkSize)

offs = data.rfind(self.MAGIC)

if offs != -1:
self.cookiePos = startPos + offs
break

endPos = startPos + len(self.MAGIC) - 1

if startPos == 0:
break

if self.cookiePos == -1:
print('[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive')
return False

self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)

if b'python' in self.fPtr.read(64).lower():
print('[+] Pyinstaller version: 2.1+')
self.pyinstVer = 21 # pyinstaller 2.1+
else:
self.pyinstVer = 20 # pyinstaller 2.0
print('[+] Pyinstaller version: 2.0')

return True

def getCArchiveInfo(self):
try:
if self.pyinstVer == 20:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
(magic, lengthofPackage, toc, tocLen, pyver) = \
struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))

elif self.pyinstVer == 21:
self.fPtr.seek(self.cookiePos, os.SEEK_SET)
(magic, lengthofPackage, toc, tocLen, pyver, pylibname) = \
struct.unpack('!8sIIii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))

except:
print('[!] Error : The file is not a pyinstaller archive')
return False

self.pymaj, self.pymin = (pyver//100, pyver%100) if pyver >= 100 else (pyver//10, pyver%10)
print('[+] Python version: {0}.{1}'.format(self.pymaj, self.pymin))

tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE)

self.overlaySize = lengthofPackage + tailBytes
self.overlayPos = self.fileSize - self.overlaySize
self.tableOfContentsPos = self.overlayPos + toc
self.tableOfContentsSize = tocLen

print('[+] Length of package: {0} bytes'.format(lengthofPackage))
return True

def parseTOC(self):
self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)

self.tocList = []
parsedLen = 0

while parsedLen < self.tableOfContentsSize:
(entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
nameLen = struct.calcsize('!iIIIBc')

(entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
struct.unpack( \
'!IIIBc{0}s'.format(entrySize - nameLen), \
self.fPtr.read(entrySize - 4))

try:
name = name.decode("utf-8").rstrip("\0")
except UnicodeDecodeError:
newName = str(uniquename())
print('[!] Warning: File name {0} contains invalid bytes. Using random name {1}'.format(name, newName))
name = newName

if name.startswith("/"):
name = name.lstrip("/")

if len(name) == 0:
name = str(uniquename())
print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))

self.tocList.append(CTOCEntry(
self.overlayPos + entryPos,
cmprsdDataSize,
uncmprsdDataSize,
cmprsFlag,
typeCmprsData,
name
))

parsedLen += entrySize
print('[+] Found {0} files in CArchive'.format(len(self.tocList)))

def _writeRawData(self, filepath, data):
nm = filepath.replace('\\', os.path.sep).replace('/', os.path.sep).replace('..', '__')
nmDir = os.path.dirname(nm)
if nmDir != '' and not os.path.exists(nmDir):
os.makedirs(nmDir)
with open(nm, 'wb') as f:
f.write(data)

# 新增:处理 XOR 包裹数据
def _maybe_xor_decode(self, data):
if data.startswith(b"swdd"):
print("[+] Detected XOR-wrapped block, decoding...")
data = data[4:] # 去掉头 "swdd"
data = bytes([b ^ 0xAA for b in data])
return data

def extractFiles(self):
print('[+] Beginning extraction...please standby')
extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')

if not os.path.exists(extractionDir):
os.mkdir(extractionDir)

os.chdir(extractionDir)

for entry in self.tocList:
self.fPtr.seek(entry.position, os.SEEK_SET)
data = self.fPtr.read(entry.cmprsdDataSize)

if entry.cmprsFlag == 1:
try:
data = self._maybe_xor_decode(data) # <<== 新增
data = zlib.decompress(data)
except zlib.error:
print('[!] Error : Failed to decompress {0}'.format(entry.name))
continue
assert len(data) == entry.uncmprsdDataSize

if entry.typeCmprsData == b'd' or entry.typeCmprsData == b'o':
continue

basePath = os.path.dirname(entry.name)
if basePath != '':
if not os.path.exists(basePath):
os.makedirs(basePath)

if entry.typeCmprsData == b's':
print('[+] Possible entry point: {0}.pyc'.format(entry.name))
if self.pycMagic == b'\0' * 4:
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)

elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm':
if data[2:4] == b'\r\n':
if self.pycMagic == b'\0' * 4:
self.pycMagic = data[0:4]
self._writeRawData(entry.name + '.pyc', data)
else:
if self.pycMagic == b'\0' * 4:
self.barePycList.append(entry.name + '.pyc')
self._writePyc(entry.name + '.pyc', data)

else:
self._writeRawData(entry.name, data)

if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
self._extractPyz(entry.name)

self._fixBarePycs()

def _fixBarePycs(self):
for pycFile in self.barePycList:
with open(pycFile, 'r+b') as pycFile:
pycFile.write(self.pycMagic)

def _writePyc(self, filename, data):
with open(filename, 'wb') as pycFile:
pycFile.write(self.pycMagic)
if self.pymaj >= 3 and self.pymin >= 7:
pycFile.write(b'\0' * 4)
pycFile.write(b'\0' * 8)
else:
pycFile.write(b'\0' * 4)
if self.pymaj >= 3 and self.pymin >= 3:
pycFile.write(b'\0' * 4)
pycFile.write(data)

def _extractPyz(self, name):
dirName = name + '_extracted'
if not os.path.exists(dirName):
os.mkdir(dirName)

with open(name, 'rb') as f:
pyzMagic = f.read(4)
assert pyzMagic == b'PYZ\0'
pyzPycMagic = f.read(4)

if self.pycMagic == b'\0' * 4:
self.pycMagic = pyzPycMagic
elif self.pycMagic != pyzPycMagic:
self.pycMagic = pyzPycMagic
print('[!] Warning: pyc magic of files inside PYZ archive are different')

if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
print('[!] Warning: Python version mismatch, skipping pyz extraction')
return

(tocPosition, ) = struct.unpack('!i', f.read(4))
f.seek(tocPosition, os.SEEK_SET)

try:
toc = marshal.load(f)
except:
print('[!] Unmarshalling FAILED. Cannot extract {0}.'.format(name))
return

print('[+] Found {0} files in PYZ archive'.format(len(toc)))

if type(toc) == list:
toc = dict(toc)

for key in toc.keys():
(ispkg, pos, length) = toc[key]
f.seek(pos, os.SEEK_SET)
fileName = key
try:
fileName = fileName.decode('utf-8')
except:
pass
fileName = fileName.replace('..', '__').replace('.', os.path.sep)

if ispkg == 1:
filePath = os.path.join(dirName, fileName, '__init__.pyc')
else:
filePath = os.path.join(dirName, fileName + '.pyc')

fileDir = os.path.dirname(filePath)
if not os.path.exists(fileDir):
os.makedirs(fileDir)

try:
data = f.read(length)
data = zlib.decompress(data)
except:
print('[!] Error: Failed to decompress {0}, probably encrypted.'.format(filePath))
open(filePath + '.encrypted', 'wb').write(data)
else:
self._writePyc(filePath, data)


def main():
if len(sys.argv) < 2:
print('[+] Usage: pyinstxtractor.py <filename>')
else:
arch = PyInstArchive(sys.argv[1])
if arch.open():
if arch.checkFile():
if arch.getCArchiveInfo():
arch.parseTOC()
arch.extractFiles()
arch.close()
print('[+] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
print('')
print('You can now use a python decompiler on the pyc files within the extracted directory')
return
arch.close()

if __name__ == '__main__':
main()

NSSCTF 2025 4th Reverse WP
http://example.com/2025/08/25/NSSCTF2025/
作者
Eleven
发布于
2025年8月25日
许可协议