校赛

没写WP只记录了一题

睡蕉小猴

第一部分

base64

frida hook 绕过拿到 key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function main(){
Java.perform(function(){
var MainActivity = Java.use("com.example.mobile03.MainActivity")
var showAd = MainActivity.showAd
showAd.implementation = function(){
}
var MainActivity = Java.use("com.example.mobile03.MainActivity")
var Jformat = MainActivity.Jformat
Jformat.implementation = function(str1, str2){
console.log(str1, str2)
var result = this.Jformat(str1, str2)
console.log(result)
return result
}
})
}
setImmediate(main)

用这个 key 去解密 res目录下的一个txt文件,snow 隐写获得公钥

https://darkside.com.au/snow/snwdos32.zip

接下来要调用 swan 里的 getPvKey 方法,并将snow解出的当作16进制传入

Unidbg调用,构造输入来获取xor_key

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
package com.example.mobile03;

import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class chal extends AbstractJni implements Closeable {
private static final String SO_FILE_PATH = "unidbg-android/src/test/java/com/example/mobile03/libswan.so";
private static final String PROCESS_NAME = "com.example.mobile03";
private static final String SWAN_CLASS_NAME = "com/example/mobile03/swan";
private static final String PIG_CLASS_NAME = "pig";

private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass swanClass;
private final DvmClass pigClass;
private final boolean isLoggingEnabled;

public chal(boolean logging) throws FileNotFoundException {
this.isLoggingEnabled = logging;

emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName(PROCESS_NAME)
.addBackendFactory(new Unicorn2Factory(true)) // 启用指令跟踪
.build();

Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // API Level 23

vm = emulator.createDalvikVM();
vm.setVerbose(isLoggingEnabled);
vm.setJni(this);

DalvikModule dalvikModule = vm.loadLibrary(new File(SO_FILE_PATH), false);
module = dalvikModule.getModule();

Debugger debugger = emulator.attach();

dalvikModule.callJNI_OnLoad(emulator);

// 解析需要的 DVM 类
swanClass = vm.resolveClass(SWAN_CLASS_NAME);
pigClass = vm.resolveClass(PIG_CLASS_NAME);
}

/**
* 执行加密操作。
* @return 成功则返回 true,否则可以根据实际情况调整返回值或抛出异常。
*/
public String performEncryption() {
// 示例输入字符串,实际应用中可能来自参数或配置文件
String inputText = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

// 调用目标 JNI 方法
StringObject result = swanClass.callStaticJniMethodObject(emulator, "getPvKey()Ljava/lang/String;", inputText);
return result.getValue();
}

@Override
public void close() throws IOException {
try {
if (emulator != null) {
emulator.close();
}
if (isLoggingEnabled) {
System.out.println("Emulator resources released.");
}
} catch (Exception e) {
// 包装成 IOException 或自定义异常,以便调用者可以按需处理
throw new IOException("Error closing emulator resources", e);
}
}

public static void main(String[] args) {
// 使用 try-with-resources 确保资源被正确关闭
try (chal chalInstance = new chal(true)) {
String encryptedResult = chalInstance.performEncryption();
System.out.println("Encryption Result: " + encryptedResult);
} catch (FileNotFoundException e) {
System.err.println("SO file not found: " + e.getMessage());
e.printStackTrace();
} catch (IOException e) {
System.err.println("An I/O error occurred: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("An unexpected error occurred: " + e.getMessage());
e.printStackTrace();
}
}
}

构造

xor_key去解公钥获取私钥

求私钥

密文

密文base

用私钥解RSA

区域赛

Reverse

greeting

有混淆和反调试,但是不用管,直接盯帧,看关键的加密

密文在开头

1
2
3
4
5
6
7
8
9
10
def rotate_right(n, shift, bits=8):
shift %= bits
return ((n >> shift) | (n << (bits - shift))) & 0xFF

encrypted = [0x13, 0x10, 0x7C, 0xF0, 0x52, 0x37, 0x64, 0x14, 0x59, 0x90, 0x13, 0xA6, 0x6D, 0xEA, 0xA1, 0x14]

flag = [rotate_right(encrypted[i], i % 5) ^ (i + 90) for i in range(len(encrypted))]

print(bytes(flag).decode())
#ISCC{hRdIjw6=:r}

有趣的小游戏

调试来分析整个过程,先简单看while循环前逻辑如下

进入sub_41D820

回到循环,跟进到sub_41D620看看

一些移动的操作

我们需要跟进一下吃到金币的操作,sub_41D580下断跟进

sub_40165D对密文a1+56进行了操作,a1+80应该是密钥,跟进看看

根据不同值读取不同文件,执行到v4时生成函数看看

xxtea加密,那file1应该就是解密

直接解就行了

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
import struct
from Crypto.Util.number import *

def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

def xxtea_decrypt(v, k):
delta = 0x9e3779b9
n = len(v)
rounds = 6 + 52 // n

x = (delta * rounds) & 0xFFFFFFFF
y = v[0]

for i in range(rounds):
e = (x >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[p]

z = v[n - 1]
v[0] = (v[0] - shift(z, y, x, k, 0, e)) & 0xFFFFFFFF
y = v[0]
x = (x - delta) & 0xFFFFFFFF

return v

k = [0x12345678, 0x9ABCDEF0, 0xFEDCBA98, 0x76543210]
v = [0x3741FBE5, 0x34108032, 0xD030FAE1, 0x174B9A65, 0xEAF532BB, 0xDF3A3FED,
0xD24A6ADB, 0x1E6DB366, 0x78E32718, 0x425FCF2F, 0xECBC6F2, 0xFC92FE60,
0xB7D08EFA, 0xAC73AC57, 0x40708ED, 0xD6C8B6FB, 0x64F349AA, 0x610FD31,
0x85CFCC2, 0xAA9F9DF2, 0xF10EF068, 0x4FA8A14C, 0xC2DFED05, 0x105F53CC,
0x16C9A063, 0x7C44AE11, 0xCD76BF2C, 0xBF6DF247, 0x270C4CCD, 0x82DCB371]

for i in range(10000):
v = xxtea_decrypt(v, k)
result_bytes = b""
for val in v:
result_bytes += long_to_bytes(val, 4)
try:
print(result_bytes.decode())
break
except UnicodeDecodeError:
if i % 1000 == 999:
print(f"已尝试 {i+1} 次...")
continue

SecretGrid

先要解一个数独

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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
import numpy as np
import time
from itertools import combinations
import multiprocessing as mp

# 全局变量标识是否使用GPU
USE_GPU = False

try:
import cupy as cp
from numba import cuda, jit
# 测试是否真正可用
test_array = cp.zeros(10)
test_array[0] = 1
if test_array[0] == 1:
USE_GPU = True
print("GPU加速可用")
except Exception as e:
print(f"GPU加速不可用: {e}")
print("将使用CPU多线程模式")

class SudokuSolver:
def __init__(self, num_strings):
self.grid = np.zeros((9, 9), dtype=np.int32)
self.num_strings = num_strings
self.solutions = []

# 位掩码优化
self.row_masks = np.zeros(9, dtype=np.int32)
self.col_masks = np.zeros(9, dtype=np.int32)
self.box_masks = np.zeros(9, dtype=np.int32)

# 性能统计
self.max_sol = 0
self.tried_placements = 0
self.start_time = time.time()

def solve(self):
"""主入口函数"""
self._init_masks()
return self._backtrack(0)

def dump(self):
"""打印当前网格状态"""
print(f"网格状态 ({len(self.num_strings)}个数串)")
for row in self.grid:
print(row)

def _backtrack(self, str_index):
"""统一回溯框架:处理数串放置和数独填充"""
# 所有数串已放置完成,开始填充剩余格
if str_index == len(self.num_strings):
if self._solve_remaining():
self.solutions.append(self.grid.copy())
return True
return False

if str_index > self.max_sol:
self.max_sol = str_index
elapsed = time.time() - self.start_time
print(f'IDX{str_index}: {self.num_strings[str_index]} [尝试: {self.tried_placements}, 用时: {elapsed:.2f}秒]')

# 当前数串
current_str = self.num_strings[str_index]
current_str_len = len(current_str)

# 生成所有可能的坐标
all_coors = self._generate_all_possible_coors(current_str_len)

# 对每个坐标尝试放置
for coors in all_coors:
self.tried_placements += 1
coors_s = list(zip(coors, [int(x) for x in current_str]))
if self._can_place_str(coors_s):
self._place_str(coors_s)
if self._backtrack(str_index + 1):
return True
self._remove_str(coors_s)

return False

def _generate_all_possible_coors(self, str_len):
"""生成所有可能的坐标方案"""
result = []

# 所有可能的方向
directions = [(0,1), (0,-1), (1,0), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]

# 遍历所有起始位置和方向
for r in range(9):
for c in range(9):
for dr, dc in directions:
coors = []
valid = True

# 检查该方向是否能放置当前数串
for i in range(str_len):
new_r, new_c = r + i*dr, c + i*dc
if new_r < 0 or new_r >= 9 or new_c < 0 or new_c >= 9:
valid = False
break
coors.append((new_r, new_c))

if valid:
result.append(coors)

return result

def _solve_remaining(self):
"""填充剩余空格"""
empty = self._find_empty()
if not empty:
return True

row, col = empty
for num in self._get_possible_numbers(row, col):
if self._is_valid(row, col, num):
self._set_number(row, col, num)
if self._solve_remaining():
return True
self._clear_number(row, col)
return False

def _can_place_str(self, coors):
"""检查是否可以放置数串"""
for cr, val in list(coors): # 创建副本避免修改原列表
r, c = cr
if r >= 9 or c >= 9:
return False
gval = self.grid[r][c]
if gval != 0:
if gval != val:
return False
coors.remove((cr, val))

# 复制掩码进行尝试
tmp_row_mask = self.row_masks.copy()
tmp_col_mask = self.col_masks.copy()
tmp_box_mask = self.box_masks.copy()

for cr, val in coors:
if not self._try_mask(tmp_row_mask, tmp_col_mask, tmp_box_mask, cr, val):
return False
return True

def _try_mask(self, rm, cm, bm, coor, val):
"""尝试更新掩码"""
mask = 1 << (val - 1)
r, c = coor
if (rm[r] & mask != 0 or
cm[c] & mask != 0 or
bm[(r//3)*3 + (c//3)] & mask != 0):
return False
rm[r] |= mask
cm[c] |= mask
bm[(r//3)*3 + (c//3)] |= mask
return True

def _place_str(self, coors):
"""放置数串"""
for cr, val in coors:
r, c = cr
self._set_number(r, c, val)

def _remove_str(self, coors):
"""移除数串"""
for cr, _ in coors:
r, c = cr
self._clear_number(r, c)

def _init_masks(self):
"""初始化位掩码"""
for r in range(9):
for c in range(9):
num = self.grid[r][c]
if num != 0:
self._update_masks(r, c, num, set_mask=True)

def _update_masks(self, row, col, num, set_mask):
"""更新掩码"""
mask = 1 << (num - 1)
if set_mask:
self.row_masks[row] |= mask
self.col_masks[col] |= mask
self.box_masks[(row//3)*3 + (col//3)] |= mask
else:
self.row_masks[row] &= ~mask
self.col_masks[col] &= ~mask
self.box_masks[(row//3)*3 + (col//3)] &= ~mask

def _find_empty(self):
"""寻找空位置"""
for r in range(9):
for c in range(9):
if self.grid[r][c] == 0:
return (r, c)
return None

def _get_possible_numbers(self, row, col):
"""获取可能的数字"""
used = self.row_masks[row] | self.col_masks[col] | self.box_masks[(row//3)*3 + (col//3)]
return [i for i in range(1, 10) if not (used & (1 << (i-1)))]

def _is_valid(self, row, col, num):
"""验证数字是否有效"""
mask = 1 << (num - 1)
return not (self.row_masks[row] & mask or
self.col_masks[col] & mask or
self.box_masks[(row//3)*3 + (col//3)] & mask)

def _set_number(self, row, col, num):
"""设置数字"""
self.grid[row][col] = num
self._update_masks(row, col, num, set_mask=True)

def _clear_number(self, row, col):
"""清除数字"""
num = self.grid[row][col]
if num != 0: # 避免清除空格
self._update_masks(row, col, num, set_mask=False)
self.grid[row][col] = 0


# GPU加速版求解器
if USE_GPU:
class SudokuSolverGPU(SudokuSolver):
def __init__(self, num_strings):
super().__init__(num_strings)

def _generate_all_possible_coors(self, str_len):
"""GPU加速版坐标生成"""
# 在这里使用更简单的CUDA实现,避免复杂数据结构
return self._generate_all_possible_coors_gpu_simple(str_len)

def _generate_all_possible_coors_gpu_simple(self, str_len):
"""简化的GPU坐标生成,完全避免在CUDA内核中使用复杂数据结构"""
# 预先分配结果数组
all_results = []

# 只在GPU上并行计算坐标的有效性,不构建完整的坐标列表
# 这样避免了在CUDA内核中构建列表
directions = [(0,1), (0,-1), (1,0), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]

for dr, dc in directions:
# 每个方向分别处理
d_valid = cp.zeros((9, 9), dtype='bool')

# 在主机上准备数据
h_valid = np.zeros((9, 9), dtype=bool)
for r in range(9):
for c in range(9):
valid = True
for i in range(str_len):
new_r, new_c = r + i*dr, c + i*dc
if new_r < 0 or new_r >= 9 or new_c < 0 or new_c >= 9:
valid = False
break
h_valid[r, c] = valid

# 复制到GPU
d_valid = cp.array(h_valid)

# 从GPU获取结果并生成坐标列表
valid_positions = cp.where(d_valid == True)
for r, c in zip(valid_positions[0].get(), valid_positions[1].get()):
coors = []
for i in range(str_len):
coors.append((r + i*dr, c + i*dc))
all_results.append(coors)

return all_results

# 字符映射函数
def c2n(ch):
"""字符到数字的映射"""
map_c = ['a', 'e', 'u', 'i', 'r', 'p', 'l', 's', 't']
return map_c.index(ch) + 1

def word2ns(chs):
"""将单词转换为数字串"""
map_c = ['a', 'e', 'u', 'i', 'r', 'p', 'l', 's', 't']
data = []
for i in chs:
for j in range(len(map_c)):
if i == map_c[j]:
data.append(str(j + 1))
break
return ''.join(data)

def solve_sudoku_with_words(words):
"""用单词列表求解数独"""
# 预处理:转换字词为数字串
print("正在转换单词为数字串...")
num_strings = [word2ns(i) for i in words]
print(f"数字串: {num_strings}")

print("开始求解数独...")
start_time = time.time()

# 根据是否可用选择GPU或CPU版本
if USE_GPU:
solver = SudokuSolverGPU(num_strings)
else:
solver = SudokuSolver(num_strings)

if solver.solve():
end_time = time.time()
print(f"求解成功!耗时 {end_time - start_time:.2f} 秒")
print("完整数独解:")
for row in solver.solutions[0]:
print(row)
return solver.solutions[0]
else:
end_time = time.time()
print(f"无可行解。耗时 {end_time - start_time:.2f} 秒")
return None

def decode(s):
"""解码数独解为字符"""
map_c = ['a', 'e', 'u', 'i', 'r', 'p', 'l', 's', 't']
payload = ''
for i in s:
for j in i:
if j > 0: # 避免处理零值
payload += map_c[j-1]
return payload

def strstr(words: list):
"""移除被其他单词包含的单词"""
i = 0
while i < len(words):
word = words[i]
contained = False
for j in range(len(words)):
if i != j and word in words[j]:
contained = True
break
if contained:
words.pop(i)
else:
i += 1

def process_combination(elements, idx, total):
"""处理单个组合"""
print(f"\n正在尝试第 {idx+1}/{total} 种组合...")

words = set()
for ent in elements:
for word in ent.split():
words.add(word)

words = list(words)
words.sort(key=len)
strstr(words)
words = words[::-1]
print(f"处理后的单词列表: {words}")

result = solve_sudoku_with_words(words)
if result is not None:
solution_text = decode(result)
print(f"找到解决方案: {solution_text}")
return solution_text
else:
print("此组合无解")
return None

def process_batch(batch, start_idx, total):
"""处理一批组合"""
results = []
for i, elements in enumerate(batch):
idx = start_idx + i
result = process_combination(elements, idx, total)
if result:
results.append((idx, result))
return results

if __name__ == "__main__":
calist = [
'past is pleasure',
'please user it',
'rap less piter',
'its pure latter',
'is leet',
'rit platstep',
'all use peatrle',
'pali atar usar',
'sets a pure sereat',
'tales sell appets',
]

# 生成所有组合
print("生成所有可能的单词组合...")
all_combinations = list(combinations(calist, 5))
all_combinations.reverse() # 从后往前尝试
total = len(all_combinations)
print(f"总共有 {total} 种组合需要尝试")

# 是否使用多进程
use_multiprocessing = not USE_GPU and mp.cpu_count() > 1

if use_multiprocessing:
# 多进程CPU并行版本
print(f"使用多进程并行尝试 (CPU核心数: {mp.cpu_count()})")
cpu_count = mp.cpu_count()
batch_size = (total + cpu_count - 1) // cpu_count

# 将组合分成多个批次
batches = [all_combinations[i:i+batch_size] for i in range(0, total, batch_size)]

# 运行多进程
with mp.Pool(processes=cpu_count) as pool:
batch_args = [(batch, i*batch_size, total) for i, batch in enumerate(batches)]
all_results = pool.starmap(process_batch, batch_args)

# 汇总结果
solutions = []
for batch_results in all_results:
solutions.extend(batch_results)

if solutions:
for idx, solution in solutions:
print(f"组合 {idx+1} 的解决方案: {solution}")
else:
print("所有组合均无解")
else:
# 串行版本
for idx, elements in enumerate(all_combinations):
result = process_combination(elements, idx, total)
if result:
print(f"组合 {idx+1} 的解决方案: {result}")
break

跑了两个多小时,写Z3更快,但是当时没想到暴力竟然要这么久

转置也是可以的

lrapiuetsieptlsurautsraeliptpiseralueaulptisrslrauitperueislpatpiterasulaslutprei或者

ilterupaseuapslirtrsptialuepalitresutrsupealiueialsrtplteruispasiulapteraprsetuil

得到

以为这就是flag,但不对。

需要 decode,和这个decode函数对脑电波

问 AI看出这一大串是 S-record

把这串dump下来,用srec_cat转化一下

得到一个二进制文件

问一下AI

IDA选择 PowerPC

这里按C开始分析,分析完按P生成函数

一个异或过程

key猜测就是 s-record 上面的v4

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

# 十六进制初始化数组文本
text = '''
v4[0] = 0x2E;
v4[1] = 0x30;
v4[2] = 0x1B;
v4[3] = 0x32;
v4[4] = 0x50;
v4[5] = 0x3E;
v4[6] = 0x55;
v4[7] = 0x50;
v4[8] = 0x46;
v4[9] = 0x3A;
v4[10] = 0x24;
v4[11] = 0x1E;
v4[12] = 0x30;
v4[13] = 0x2F;
v4[14] = 0x45;
v4[15] = 0x2A;
v4[16] = 0x5A;
v4[17] = 0x41;
v4[18] = 0xE;
v4[19] = 0x16;
v4[20] = 0x10;
v4[21] = 0x46;
v4[22] = 0x26;
v4[23] = 0x44;
v4[24] = 0x15;
v4[25] = 0x19;
v4[26] = 0;
v4[27] = 0x3C;
v4[28] = 0x49;
v4[29] = 0x1C;
v4[30] = 0x1C;
'''

values = []
pattern = re.compile(r'=\s*(0x[0-9A-F]+|\d+);', re.IGNORECASE)

for line in text.strip().splitlines():
match = pattern.search(line)
if match:
value_text = match.group(1)
if value_text.startswith('0x'):
num = int(value_text[2:], 16)
else:
num = int(value_text, 10)
values.append(num)

key = bytes(values)
print(f"key: {key}")

encrypted_flag = b'ISCC{s_ale_ru_upatu_prrlaullre_}'

decrypted = [0] * 0x20
for idx in range(0x1f):
key_byte = key[idx]
enc_byte = encrypted_flag[idx]

if idx % 2 == 0:
decrypted[idx] = (key_byte ^ (enc_byte + 2))
elif idx % 3 == 0:
decrypted[idx] = (key_byte ^ (enc_byte + 5))
else:
decrypted[idx] = (key_byte ^ enc_byte)

decrypted[idx] &= 0xff

decrypted[0x1f] = 0
decrypted = decrypted[:0x1f]

try:
flag = 'ISCC{' + bytes(decrypted).decode()
print(f"flag: \n{flag}")
assert(flag.isprintable())
except:
print(f'解码失败: {decrypted}')

ISCC{ec^z-M41(PElGp2_95yIb1R(vlnM=y}

faze

比较的位置下个断点

运行后在内存中查看到flag

Mobile

HolyGrail

阅读 Java 层代码

根据选择的checkbox 顺序生成序列数据,然后再去维吉尼亚加密,根据getEncryptionKey知道key在/res/raw/key.txt

由于序列生成是本地so层的,看了一眼很难分析,但是他本地生成完序列数据会返回getCipherText所以只需要hook一下就行。

所以我们现在需要排序checkbox

首先hook 获取一下每个checkbox 对应的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
let CipherDataHandler = Java.use("com.example.holygrail.CipherDataHandler");
CipherDataHandler["getCipherText"].implementation = function (list) {
console.log(`CipherDataHandler.getCipherText is called: list=${list}`);
let result = this["getCipherText"](list);
console.log(`CipherDataHandler.getCipherText result=${result}`);
var size = list.size();
for (var i = 0; i < size; i++) {
var Name = list.get(i);
console.log(`Name ${i}: ${Name.toString()}`);
}
return result;
};

});

然后根据说明要耶稣和他的门徒的顺序,问一下AI

对应一下就是

1
2
3
4
5
6
7
8
9
10
11
12
13
"checkBox8",
"checkBox6",
"checkBox7",
"checkBox5",
"checkBox12",
"checkBox3",
"checkBox10",
"checkBox13",
"checkBox11",
"checkBox",
"checkBox9",
"checkBox4",
"checkBox14"

然后用这个正确的顺序去注入,调用维吉尼亚加密得到密文

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
function hook() {
Java.perform(function () {
let a = Java.use("com.example.holygrail.a");
var targetClass = Java.use("com.example.holygrail.CipherDataHandler");
var args = Java.array("java.lang.String", [
"checkBox8",
"checkBox6",
"checkBox7",
"checkBox5",
"checkBox12",
"checkBox3",
"checkBox10",
"checkBox13",
"checkBox11",
"checkBox",
"checkBox9",
"checkBox4",
"checkBox14"
]);
console.log(targetClass.generateCipherText(args));
a["vigenereEncrypt"].implementation = function (str, str2) {
console.log(`a.vigenereEncrypt is called: str=${str}, str2=${str2}`);
let result = this["vigenereEncrypt"](str, str2);
console.log(`a.vigenereEncrypt result=${result}`);
return result;
};
});
}
setImmediate(hook);

接下来就是密文是如何生成的了,看so的processString是一个是一个逐字节处理的过程,所以我们只需要知道对应的加密结果即可

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
function hook() {
Java.perform(function() {
var aClass = Java.use("com.example.holygrail.a");
var bClass = Java.use("com.example.holygrail.b");

var original_processWithNative = aClass.processWithNative;
var original_b_a = bClass.a;

aClass.processWithNative.implementation = function(str) {
console.log("[+] processWithNative被调用,参数:" + str);
var result = original_processWithNative.call(this, str);
console.log("[+] processWithNative原始返回值:" + result);
return result;
};

bClass.a.implementation = function(str) {
console.log("[+] b.a被调用,参数:" + str);
var originalResult = original_b_a.call(this, str);
console.log("[+] b.a原始返回值:" + originalResult);
return originalResult;
};

aClass.validateFlag.implementation = function(context, str) {
console.log("[+] validateFlag被调用,flag内容:" + str);

var b_result = this.b(context, str);
console.log("[+] b(context, str)结果:" + b_result);

var result = aClass.validateFlag.call(this, context, str);
console.log("[+] validateFlag完整结果:" + result);

return result;
};
});
}

setImmediate(hook);

解维吉尼亚

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
printable=r"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
table="39213A213B213C21402141214221432144214521464748494A4B4C505152535455565758595A5B5C60616263646550215121522153215421552156215721582159215A215B215C21303132333435363738393A3B3C272129212A212B212C2130213121322133213421352136213721382146214721482149214A214B214C2140414243444566676869"
data=[]
while table!="":
if table[2:4]=="21":
data.append(table[:4].lower())
table=table[4:]
else:
data.append(table[:2].lower())
table=table[2:]
def vigenere_decrypt(ciphertext, key):
decrypted = []
key = key.lower()
key_length = len(key)
key_index = 0

for char in ciphertext:
if char.isalpha():
# 确定字符偏移
offset = ord('a') if char.islower() else ord('A')
k = ord(key[key_index % key_length]) - ord('a')

# 解密公式
decrypted_char = chr((ord(char) - offset - k) % 26 + offset)
decrypted.append(decrypted_char)

key_index += 1
else:
# 保留非字母字符
decrypted.append(char)

return ''.join(decrypted)

KEY = "TheDaVinciCode"

def decrypt(enc):
encs = []
while enc != "":
if enc[2:4] == "21":
encs.append(enc[:4])
enc = enc[4:]
else:
encs.append(enc[:2])
enc = enc[2:]

cipher_text = ''.join(printable[data.index(chunk)] for chunk in encs)

print(f'Ciphertext: {cipher_text}')
plaintext = vigenere_decrypt(cipher_text, KEY)
print(f'ISCC{{{plaintext}}}')


decrypt(b"W!bdA!iV!JIi6ZQQJU".hex())
#Ciphertext: Hwy5~Ged~Triiem
#ISCC{Opu5~Dei~Legacy}

Detective

密文

最后调用 native,查看是一个异或,密钥是Sherlock

两个加密过程,还原回去即可

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
def decrypt(cipher: str) -> str:
# 步骤1: 将密文转换为十六进制
hex_str = ''.join(f'{ord(ch):04x}' for ch in cipher)
hex_str = hex_str[2:] if hex_str.startswith('00') else hex_str

# 步骤2: 处理填充
if len(hex_str) % 4 == 2:
hex_str += '00'
hex_str = ''.join(hex_str[i:i+2] for i in range(0, len(hex_str), 4))

# 步骤3: 处理对混淆
result = []
i = 0
while i < len(hex_str):
if i + 3 < len(hex_str) and hex_str[i+2:i+4] == '21':
result.extend([hex_str[i+1], hex_str[i]])
i += 4
else:
result.append(hex_str[i:i+2])
i += 2
hex_str = ''.join(result)

# 步骤4: 拆分并修复零值
half = len(hex_str) // 2
part1, part2 = hex_str[:half], hex_str[half:]

part2 = ''.join('0' if ch == '3' and (i == 0 or i % 3 == 0) else ch
for i, ch in enumerate(part2))
part1 = ''.join('0' if ch == '3' and (i == 1 or (i - 1) % 3 == 0) else ch
for i, ch in enumerate(part1))

merged = []
for i in range(half):
merged.append(part2[i])
if i < half:
merged.append(part1[i])
hex_str = ''.join(merged)

# 步骤5: 十六进制转换为明文
chars = []
i = 0
while i < len(hex_str):
if hex_str[i] == '0' and i + 2 < len(hex_str):
chars.append(chr(int(hex_str[i+1:i+3], 16)))
i += 3
else:
chars.append(chr(int(hex_str[i:i+4], 16)))
i += 4
return ''.join(chars)

if __name__ == "__main__":
xor_key = b'Sherlock'
res = "1027444F3F5742506A3B24535F2C224A"
res_bytes = bytes.fromhex(res)

# 异或解密
flag = [b ^ xor_key[i % len(xor_key)] for i, b in enumerate(res_bytes)]
cipher_text = bytes(flag).decode()

print("密文:", cipher_text)
print("明文:", decrypt(cipher_text))

ISCC{I_AMSH1K}

邦布出击

sqlcipher解密数据库

打开解密后的数据库,拿到key

frida hook 一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook() {
Java.perform(function () {
let b = Java.use("com.example.mobile01.b");
b["c"].implementation = function () {

return "JkLmNoPqRsTuVwXy";
};

let DESHelper = Java.use("com.example.mobile01.DESHelper");
DESHelper["encrypt"].implementation = function (str, str2, str3) {
console.log(`DESHelper.encrypt is called: str=${str}, str2=${str2}, str3=${str3}`);
let result = this["encrypt"](str, str2, str3);
console.log(`DESHelper.encrypt result=${result}`);
return result;
};
});
}

setImmediate(hook);

ISCC{11C4BHKpDaiKRdJg8LjHcMo8I682n0Pe}

决赛

Reverse

CrackMe

加密函数如下sub_140001000sub_140001080sub_1400012A0里都有花指令

nop掉就能分析了

第一处

得到sub_140001000即将偶数位置字符异或,传入的参数在主加密函数里可以看到是0x41

第二处

得到sub_140001080,一个凯撒,偏移是3

第三处和第四处

得到一个标准的rc4

密文

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

void rc4(unsigned char* key, int key_Len, unsigned char* data, int data_Len)
{
int i = 0, j = 0, t = 0;
unsigned char s[256] = { 0 };
unsigned char tmp = 0;
for (i = 0; i < 256; i++) {
s[i] = i;
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + key[i % key_Len]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
int q = 0;
i = j = 0;
for (q = 0; q < data_Len; q++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;

tmp = s[i];
s[i] = s[j];
s[j] = tmp;
t = (s[i] + s[j]) % 256;
data[q] ^= s[t];
}
}
int main()
{
unsigned char flag[42] = {
0x1C, 0xB8, 0x2E, 0x47, 0xDD, 0x72, 0x1C, 0xA2, 0xDE, 0x13, 0x32, 0x46, 0xC8, 0xF0, 0x59, 0x81, 0xB0, 0xE6, 0xE4, 0xEE, 0x03, 0x9A, 0x58, 0x28, 0x4E, 0x6B, 0x9F, 0xE8, 0x80, 0x24, 0x82, 0x3F, 0xD6, 0x15, 0x68, 0x17, 0xEE, 0x91, 0xD7, 0xFE, 0x35, 0x74
};
const char* decryptKey = "SecretKey";
rc4((unsigned char*)decryptKey, strlen(decryptKey), flag, sizeof(flag));

for (int i = 0; i < sizeof(flag); i++) {
unsigned char c = flag[i];

if (c >= 'a' && c <= 'z') {
flag[i] = 'a' + ((c - 'a' + 23) % 26);
}
else if (c >= 'A' && c <= 'Z') {
flag[i] = 'A' + ((c - 'A' + 23) % 26);
}
}

for (int i = 0; i < sizeof(flag); i += 2) {
printf("%c", flag[i] ^ 0x41);
}

return 0;
}

uglyCpp

调试,这里判断了长度

进入ZNK25mJ6Xq4ExTMs4qaNhgFkHaofHSMUlRKSt6vectorIjSaIjEEE_clES3_

ZZNK25mJ6Xq4ExTMs4qaNhgFkHaofHSMUlRKSt6vectorIjSaIjEEE_clES3_ENKUlRmS5_RjS6_RS1_E1_clES5_S5_S6_S6_S7_执行完后生成了字符映射v13

进入ZNK28gxoPJ4FNZcYkWUGp7wE96Z9Pzuw8MUlRKSt6vectorIjSaIjEES3_mE_clES3_S3_m

这里拿到v8异或的key

回到主函数ZNK12S4V3u5wVUXnyMUlRSt6vectorIjSaIjEEE_clES2_里有密文

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
def decrypt_flag():
key = [0x3ED6325B, 0xD709BF17, 0xE3F27E18, 0xA0870791, 0x146D6F9,
0x7C6140FF, 0x10B69406, 0x94DDE0F6, 0x40B2BB6C]

result = [0x52B15E33, 0x947DD374, 0xD3BC3465, 0xE9EE7EC2, 0x640CAE82,
0x105929B6, 0x28FDF74F, 0xE491A2B5, 0x22F7CD14]
xor_values = [k ^ r for k, r in zip(key, result)]

decoded_parts = []
for value in xor_values:
byte_string = bytes.fromhex(hex(value)[2:].zfill(8))
reversed_str = byte_string.decode('latin-1')[::-1]
decoded_parts.append(reversed_str)

decoded_flag = ''.join(decoded_parts)
print(f"中间解码结果: {decoded_flag}")

source = "1234567890abcdefghijklmnopqrstuvwxyz"
target = "vwfxyg8zhi94jk0lma52nobpqc6rsdtue731"

char_map = {target[i]: i for i in range(len(target))}

final_result = ''.join(decoded_flag[char_map[c]] for c in source if c in char_map)

return final_result

if __name__ == "__main__":
result = decrypt_flag()
print(f"最终解码结果: {result}")

Mobile

GGAD

打开是一段视频,需要先将启动Activity修改了

用MT管理器,提取安装包后查看AndroidManifest.xml将其修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<activity
android:name="com.example.ggad.VideoActivity"
android:exported="true"
android:screenOrientation="0"/>
<activity
android:name="com.example.ggad.MainActivity"
android:exported="true">
<intent-filter>
<action
android:name="android.intent.action.MAIN"/>
<category
android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

保存即可,然后重新安装

key验证在native层

cmd5查到是ExpectoPatronum

这里又生成了一个密文的一部分

可以hook拿到也可以直接扣源码跑

1
2
3
4
5
6
7
8
9
10
11
function hook() {
Java.perform(function () {
var TargetClass = Java.use('com.example.ggad.c');
TargetClass.a.overload().implementation = function () {
var result = this.a();
console.log('[Hook] c.a() 返回值:', result);
return result;
};
});
}
setImmediate(hook);
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
class C0501c {

public static String vigenereDecrypt(String str, String str2) {
StringBuilder sb = new StringBuilder();
int i = 0;
for (int i2 = 0; i2 < str.length(); i2++) {
char charAt = str.charAt(i2);
if (Character.isLetter(charAt)) {
sb.append((char) (((((Character.toUpperCase(charAt) - 'A') - (Character.toUpperCase(str2.charAt(i % str2.length())) - 'A')) + 26) % 26) + 65));
i++;
} else {
sb.append(charAt);
}
}
return sb.toString();
}

public static String decrypt(String str, String str2) {
return vigenereDecrypt(str, str2);
}

/* renamed from: a */
public static String m54a() {
return decrypt("476J7X721PJH29", "ExpectoPatronum");
}
}

public class Text2 {
public static void main(String args[]) {
String str = C0501c.m54a();
System.out.println(str);
//System.out.println(C0501c.rc4Decrypt(str, 'ExpectoPatronum'));
}
}

得到这个之后加上这段解就行

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
# 给定的二进制字符串
binary_input = '0100010000110001010001000011000000110100001101110100010100110001001100000011010001000101001100100011100100110100'
hex_input = '476F7A721AFF29'

# 将二进制字符串转换为ASCII字符
ascii_chars = ''.join(chr(int(binary_input[i:i+8], 2)) for i in range(0, len(binary_input), 8))

interleaved = ''.join(ascii_chars[i] + hex_input[i] for i in range(len(hex_input)))

# 将每两位解析为十六进制并转换为字节
byte_values = [int(interleaved[i:i+2], 16) for i in range(0, len(interleaved), 2)]

# 将字节转换为二进制字符串
binary_representation = ''.join(format(byte, '08b') for byte in byte_values)

# 翻转二进制位 (0变1, 1变0)
inverted_binary = ''.join('0' if bit == '1' else '1' for bit in binary_representation)

# 将翻转后的二进制转换为字节数组
cipher_bytes = [int(inverted_binary[i:i+8], 2) for i in range(0, len(inverted_binary), 8)]
final_cipher = bytes(cipher_bytes)

def rc4_ksa(key):
"""密钥调度算法 (KSA)

得到初始置换后的S表
"""
# 种子密钥key若为字符串,则转成字节串
if isinstance(key, str):
key = key.encode()
S = list(range(256)) # 初始化S表
# 利用K表,对S表进行置换
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i] # 置换
return S


def rc4_prga(S, text):
"""伪随机生成算法 (PRGA)

利用S产生伪随机字节流,
将伪随机字节流与明文或密文进行异或,完成加密或解密操作
"""
# 待处理文本text若为字符串,则转成字节串
if isinstance(text, str):
text = text.encode()
i = j = 0
result = []
count=0
for byte in text:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i] # 置换
t = (S[i] + S[j]) % 256
k = S[t] # 得到密钥字k
# 将明文或密文与k进行异或,得到处理结果
result.append(byte ^ k)
return bytes(result)

S = rc4_ksa('ExpectoPatronum')
res = rc4_prga(S, final_cipher)
flag = "ISCC{" + res.decode() +"}"
print(flag)

叽米是梦的开场白

分段对flag进行check

mobile04里导出了一个dex

采用frida-dexdump

得到3个dex,有个2kb的打开


得到flag第一段密文,密钥在Sunday.so

得到flag第一段

第二段一开始以为是EvilService里的

看起来来就不大对

但是看到还有一个a方法的check,他对一个加密的本地库进行check

这里loadEncryptedLib()方法从应用的assets目录中加载了一个名为enreal的加密库文件

这个库传给Monday.so里checkflag2

有一些调试检测,但不重要,看这部分加密逻辑即可

于是去assets目录下解密enreal

1
2
3
4
5
6
7
8
with open("enreal", "rb") as f:
data = bytearray(f.read())
for i in range(len(data)):
data[i] = ((data[i] << 2) | (data[i] >> 6)) & 0xff
data[i] ^= 0x44
data[i] = ((data[i] >> 3) | (data[i] << 5)) & 0xff
with open("decode_enreal", "wb") as f:
f.write(data)

找到realcheck

依然是Triple DES,密钥是9uzGw2TJszopH0NAecGL0sUS注意密文是小端序即可298B602DA18468FC

两段拼接

ISCC{Zccr1lzQTGEr0d}