School

三进制战争

第一段就不说了,第二段两个字符是12位3进制,按两个字符爆破就行。

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
Java.perform(function () {
let res = "";
Java.scheduleOnMainThread(function () {
try{
const MainActivity = Java.use("com.example.mobile02.MainActivity");
const instance = MainActivity.$new();
const target = "011010000102021112011120012212010120";
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?/`~';
let key = instance.stringFromJN1();
for(let k =0;k<3;k++){ //两个字节爆破一次,一起6字节
for (let i = 0; i < charset.length; i++) {
for (let j = 0; j < charset.length; j++) {
const candidate = charset[i] + charset[j];
try {
let result = instance.stringFromJNl(key,res +candidate); //前面的字节也要带上(res)
result = result.slice(0,k*12 +12);
// console.log(candidate + " => " + result);
if (result === target.slice(0,k*12 +12)) {
res+=candidate; //得到爆破结果加到res中
i = charset.length; //结束循环
console.log("[✔] Found match: " + candidate + " => " + result);
break;
}
} catch (err) {
console.error("Error with input:", candidate, err);
}
}
}
}
console.log("res = " + res);
}
catch(e){
console.log(e);
}

});

const MainActivity = Java.use("com.example.mobile02.MainActivity");
MainActivity["stringFromJNI"].implementation = function (str1) {
console.log(`MainActivity.stringFromJNI is called: str1=${str1}`);
let result = this["stringFromJNI"](str1);
console.log(`MainActivity.stringFromJNI result=${result}`);
console.log("flag: " + "ISCC{"+ result+ res+ "}");
return result;
};

MainActivity["CHECK1"].implementation = function (str1) {
console.log(`MainActivity.CHECK1 is called: str1=${str1}`);
let inp = "ISCC{111111111111111}"
let result = this["CHECK1"](inp);
// console.log(`MainActivity.CHECK1 result=${result}`);
return result;
};

// MainActivity["stringFromJNl"].implementation = function (key, input) {
// console.log(`MainActivity.stringFromJNl is called: key=${key}, input=${input.slice(3, 9)}`);
// let data = "8edf4116e5bbacd84bbe78bd8bdf99f7";
// let result = this["stringFromJNl"](data, input.slice(3, 9));
// console.log(`MainActivity.stringFromJNl result=${result}`);
// return result;
// };
// MainActivity["CHECK2"].implementation = function (str1) {
// console.log(`MainActivity.CHECK2 is called: str1=${str1}`);
// let result = this["CHECK2"](str1);
// console.log(`MainActivity.CHECK2 result=${result}`);
// return result;
// };
// // MainActivity["stringFromJN1"].implementation = function () {
// console.log(`MainActivity.stringFromJN1 is called`);
// let result = this["stringFromJN1"]();
// console.log(`MainActivity.stringFromJN1 result=${result}`);
// return result;
// };

});

/*
[✔] Found match: A1 => 011010000102
[✔] Found match: b@ => 011010000102021112011120
[✔] Found match: 3c => 011010000102021112011120012212010120
res = A1b@3c
MainActivity.CHECK1 is called: str1=1
MainActivity.stringFromJNI is called: str1=7kL@22
MainActivity.stringFromJNI result=%3G'bc'rw
flag: ISCC{%3G'bc'rwA1b@3c}
*/

Region

re

faze

格式化字符串把flag解密出来与ISCC头拼接,直接下断点拿到flag

image-20250510104837691

SecretGrid

第一关就是一个数独的约束。

第二关是一个数独游戏,除了满足普通数独的要求外,在结果中还要包含至少五个特定的句子,单词。用z3约束求解。

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
from z3 import *

#将句子字符串转换为句子列表,每个句子是一个单词列表,每个单词是一个字符列表
def init_sentences(sentences):
sentences = sentences.split('\n')
sentences = [s.split(" ") for s in sentences if s]
sentences = [[list(map(ord,s)) for s in sen] for sen in sentences]
return sentences

s = Solver()

word = ['a','e','i','l','p','r','s','t','u']
word_int =[ord(i) for i in word] #将字母转换为整数列表

# 创建9宫格变量
var = [[Int(f"var_{i}_{j}") for j in range(9)] for i in range(9)]

# 约束 var[r][c] 必须是字母中的一个值
for r in range(9):
for c in range(9):
s.add(Or([var[r][c] == val for val in word_int]))

# 行列不相等
for i in range(9):
s.add(Distinct([var[i][j] for j in range(9)]))
s.add(Distinct([var[j][i] for j in range(9)]))

# 3*3内不相等
for i in range(3):
for j in range(3):
three = [var[i*3+k][j*3+l] for k in range(3) for l in range(3)]
s.add(Distinct(three))

# 条件句子
sentences ="""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
"""

#将句子字符串转换为句子列表,每个句子是一个单词列表,每个单词是一个字符列表
sentences = init_sentences(sentences)

# 一个单词在特定位往旁边8个方向延伸的方向向量
vector = [[-1,-1],[-1,0],[-1,1], [0,-1],[0,1], [1,-1],[1,0],[1,1]]

# 每个句子在9宫格中满足的路径
sentences_paths = []

for line_index,line in enumerate(sentences):
# 一个句子中的每个单词在9宫格中满足的路径
line_paths =[]
for word in line:
#一个单词满足的路径
word_paths =[]
word_len = len(word)
# 遍历9宫格中每个格子,以每个格子为开始位置,往8个方向延伸,看是否满足单词
for i in range(9):
for j in range(9):
# 一个单词在一个格子上八个方向满足的路径
for start,end in vector:
one_path = []
# 边界检查,延伸的长度不能超出9宫格
if i+start*word_len >= 0 and i+start*word_len <= 9 and j+end*word_len >= 0 and j+end*word_len <= 9:
# 其中的一条路径
for k in range(word_len):
one_path.append(var[i+start*k][j+end*k] == word[k])
word_paths.append(And(one_path)) #每个字母组成一个单词,每个字母必须全部出现

line_paths.append(Or(word_paths)) #每个单词至少出现一次
sentences_paths.append(And(line_paths)) #每个句子的单词必须全部出现

s.add(Or(sentences_paths)) # 添加每个句子的条件

# 判断句子是否出现
ifsentence = [Bool(f"if_sentence_{i}") for i in range(len(sentences_paths))]
for i in range(len(sentences_paths)): s.add(Implies(ifsentence[i],sentences_paths[i]))

#如果句子出现就为1,否则为0,要满足至少5个句子出现
s.add(Sum([If(sen, 1, 0) for sen in ifsentence]) >= 5)

#输出结果
res = ""
if s.check() == sat:
m = s.model()
for i in range(9):
for j in range(9):
res += chr(m[var[i][j]].as_long())
else:
print("No solution found")

print(res)


#lrapiuetsieptlsurautsraeliptpiseralueaulptisrslrauitperueislpatpiterasulaslutprei

输入结果后给出了flag:ISCC{s_ale_ru_upatu_prrlaullre_},但是是错的,提示”True decode is in true_decode 郟搒?”。

回看输出函数,在decode函数中发现还有一层加密,里面有一堆摩托罗拉字节码和密文,用脚本修一下末尾的校验和。把内容放到input.srec中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data = """S123050C9421FFD093E1002C7C3F0B78907F000C909F000839200000913F001E4800012C5B
S123052C813F001E552907FE2F890000409E0058813F001E815F000C7D2A4A1489290000DB
S123054C7D2A07743D20100281090018813F001E7D284A1489290000392900025529063E60
S123056C7D2907747D494A787D280774813F001E815F00087D2A4A14550A063E99490000AD
S123058C480000BC815F001E3D205555612955567D0A48967D49FE707D2940501D2900030A
S12305AC7D2950502F890000409E0058813F001E815F000C7D2A4A14892900007D2A0774AB
S12305CC3D20100281090018813F001E7D284A1489290000392900055529063E7D290774E2
S12305EC7D494A787D280774813F001E815F00087D2A4A14550A063E994900004800004094
S123060C813F001E815F000C7D2A4A14890900003D20100281490018813F001E7D2A4A14CA
S123062C89490000813F001E80FF00087D274A147D0A5278554A063E99490000813F001E82
S123064C39290001913F001E813F001E2F89001E409DFED0813F00083929001F39400000E7
S11B066C9949000060000000397F003083EBFFFC7D615B784E800020BF
"""

def checksum_fix(hex_str):
a =[i for i in bytes.fromhex(hex_str)]
checkvalue = hex(0xff - (sum(a) & 0xFF))[2:].upper()
if len(checkvalue) < 2:
checkvalue = "0"*(2-len(checkvalue)) + checkvalue
return hex_str + checkvalue

for line in data.splitlines():
print("S1"+checksum_fix(line[2:-2:]))

再用Srecord转成二进制文件。

1
srec_cat.exe input.srec -o output.bin -binary

用ida以powerPC处理器打开output.bin文件。在字节码开始的位置P,F5即可看到伪代码。

image-20250514005639613

逻辑恢复,猜测密钥就是刚刚的假flag(刚好30字节),解密即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def xor(data:list, key:list) ->list:
res =[]
for i in range(30):
if i & 1:
if i % 3:
res.append(data[i] ^ key[i])
else:
res.append(data[i] ^ (key[i]+5))
else:
res.append(data[i] ^ (key[i]+2))
return res

data =[8, 53, 46, 124, 16, 56, 88, 27, 59, 89, 59, 86, 27, 50, 82, 68, 50, 30, 37, 124, 43, 74, 34, 91, 61, 70, 93, 84, 44, 43, 28]
key = [ord(i) for i in "ISCC{s_ale_ru_upatu_prrlaullre_}"]

res = xor(data, key)

flag = "ISCC{" + "".join([chr(i) for i in res]) +"}"
print(flag)
# ISCC{Cfk4mK9zU3Z$lm%1QjR#Y=V7^33%XN}

有趣的⼩游戏

一个类似吃豆人的游戏,1000分即可通关。逻辑如下。

main函数

image-20250515000653583

is_val函数

image-20250515000938469

这里每一步操作和到达C点都对密文进行一次解密。

在dec里面动态加载了附带的两个二进制文件。动调可以发现是一个xxtea解密函数。里面可以直接拿到密钥。

相比于标准xxtea解密算法,本题魔改了第二处解密操作的密钥获取索引为(key[e] ^ z)。

image-20250514225610721

主要就是根据路径长度对密文进行xxtea次解密,虽然有dec里有两个xxtea解密函数,但是他们的解密逻辑是相同,只要解密到特定的次数就可以得到flag明文。这里分数最少都是1000,移动的步数最少也是一千步,即至少解密1000次。直接写脚本爆破解密次数即可。

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

void dec_btea(uint32_t* v, int n, uint32_t const key[4])
{
uint32_t y, z, sum, delta;
unsigned p, rounds, e;
delta = 0x9e3779b9;
rounds = 6 + 52 / n;
sum = rounds * delta;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--)
{
z = v[p - 1];
y = v[p] -= (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)));
}
z = v[n - 1];
y = v[0] -= (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[e] ^ z)));
sum -= delta;
} while (--rounds);
}


int main()
{
uint32_t v[30] = { 0xDF6C144A, 0xED60654A, 0x3E43C8E7, 0x5B05A933, 0x5E0946EF, 0xB873A0C4, 0x5C50EA5A, 0x19F34CBB,
0xAAA28A10, 0x3787B44A, 0x71AC6EB3, 0x5B29F29C, 0xC3AD78DD, 0xB1DE477B, 0x106286A2, 0x4AAFCE01,
0x76C8CDFD, 0x9A643D12, 0x7A5DBDB7, 0x5FB17810, 0xF34E4921, 0x0FC56F93, 0x694991C9, 0x23A99B89,
0xFD8BEB88, 0x2178FCD8, 0x19339C3B, 0x740208FA, 0x4973D277, 0x3BB34AD9};

uint32_t const k[4] = { 0x12345678, 0x9ABCDEF0, 0xFEDCBA98, 0x76543210 };
for (int i = 0; i < 10000; i++) {
dec_btea(v, 30, k);
if (v[1] < 0xff && v[1]>0) break;
}

for (int i = 0; i < 30; i++) {
if (1) {
printf("%c", v[i]);
}

}
}
//ISCC{<9O"|[0&s5;,*@7$RI}GOcO4}

greeting

通过字符串找到真正的main函数,分析下来核心加密逻辑就只有下面这个,后面就是flag校验,直接写逆向脚本解密即可。

image-20250515004226986

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>

int ROR(uint8_t *data, int n) {
*data = (*data >> n) | (*data << (8 - n));
return 1;
}

int main() {
uint8_t mm[] = { 0x13,0x10,0x7C,0xF0,0x52,0x36,0x5C,0x45,0xCA,0xF0,0x9,0xB2,0xC8,0xE1,0xE4,0x14 };
for (int i = 0; i < 16; i++) {
ROR(&mm[i], i%5);
mm[i] ^= i + 90;
}
for (int i = 0; i < 16; i++) {
printf("%c", mm[i]);
}
}
//ISCC{iN0;lm<T[&}

mobile

mobide1

第一个页面直接输入flag会闪退,发现是因为传入blowfish的密钥为空,需要去寻找密钥,题目还附带了一个enflag.db的数据库,但是被加密了。

题目中有第二个页面,里面创建了数据库,并往数据库里写入了信息,输入名字和等级就可查询到写入的base64字符串,用base64解密foLaG?,提示{flag0.o?keyo.0}ccsl。因为我们只能获取前三个,推测前三个就是数据库密钥,用三层base64即可解密,拼接在一起就是数据库key。

image-20250510150800736

image-20250510151746557

在Windows下,用sqlchiper解密数据库

1
2
3
4
sqlcipher-shell64.exe enflag.db
PRAGMA key = 'CAELUS';
ATTACH DATABASE 'plaintext.db' AS plaintext KEY '';
SELECT sqlcipher_export('plaintext');

解密后可以看到blowfish的key

image-20250510152642805

用frida脚本把key写回去就可以拿到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java.perform(function () {
var t = 0

let loop = setInterval(function () {
let b = Java.use("com.example.mobile01.b");
let DESHelper = Java.use("com.example.mobile01.DESHelper");
b["c"].implementation = function () {
console.log(`b.c is called`);
return "WxYzAbCdEfGhIjKl"; //写回密钥
};
t+=1
if (t > 100) clearInterval(loop)

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}`);
console.log(`FLAG=ISCC{${result}}`);
return result;
};
}, 100)
})
//FLAG=ISCC{pAK51YtjzLPtTlO2WF16SLnw63oJpCHn}

mobile2Detective

flag先转为16进制,再用b.a进行混淆,然后用a.c按四位16进制转为字符。最后传入native的stringFromJNI()中。

native的stringFromJNI()里面只有一个异或加密,密钥是“Sherlock”。

image-20250518183326603

按照函数逻辑逆推回去就行

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

key = [0x53, 0x68, 0x65, 0x72, 0x6C, 0x6F, 0x63, 0x6B]
enc = "1027444F3F5742506A3B24535F2C224A"


bytes_dec = [int(enc[i:i+2], 16) for i in range(0, len(enc), 2)]

#XOR
recovered = [(b ^ key[i % len(key)]) for i, b in enumerate(bytes_dec)]
raw_str = bytes(recovered).decode()


# 每个字符4位转十六进制
hex_fragment = ''.join(f'{ord(c):04x}' for c in raw_str)
if hex_fragment.startswith("00"):
hex_fragment = hex_fragment[2:]

# 去除每 2 位后的插入 '00'
if len(hex_fragment) % 4 == 2:
hex_fragment += "00"
res = ''.join(hex_fragment[i:i+2] for i in range(0, len(hex_fragment), 4))

# b.a()2
res1 = []
idx = 0
while idx < len(res):
if idx + 3 < len(res) and res[idx+2:idx+4] == "21":
res1.append(res[idx+1])
res1.append(res[idx])
idx += 4
else:
res1.extend(res[idx:idx+2])
idx += 2
res2 = ''.join(res1)

# b.a()1
midpoint = len(res2) // 2
one_half = res2[:midpoint]
two_half = res2[midpoint:]

sb = ""
for idx in range(len(one_half)):
char = one_half[idx]
if char == '3' and (idx == 1 or (idx - 1) % 3 == 0):
sb += '0'
else:
sb += char

sb2 = ""
for idx in range(len(two_half)):
char = two_half[idx]
if char == '3' and (idx == 0 or idx % 3 == 0):
sb2 += '0'
else:
sb2 += char


interleaved = []
i = j = 0
for k in range(len(res2)):
if k % 2 == 0:
interleaved.append(sb2[i])
i += 1
else:
interleaved.append(sb[j])
j += 1
final_hex = ''.join(interleaved)

# 十六进制转字符
output = []
i = 0
while i < len(final_hex):
if final_hex[i] == '0' and i + 2 < len(final_hex):
output.append(chr(int(final_hex[i+1:i+3], 16)))
i += 3
else:
output.append(chr(int(final_hex[i:i+4], 16)))
i += 4
plaintext = ''.join(output)


print(f"ISCC{{{plaintext}}}")
#ISCC{I_AMSH1K}

mobile3HolyGrail

对耶稣和十二门徒进行排序,顺序在上一题mobile(Detective)的so文件(libSequence-Clues.so)里,顺序如下

image-20250516213009973

1
Jesus,  St. Pete,  St. John,  Judah,  Saint Matthew,  Old Jacob,  St. Thomas,  Saint Simon,  St. Philip,  Bartholomew,  Jacob,  St. Andrew,  Saint Tartary

根据res/layout/activity_main.xml文件下对应的名称来获取最终的字符串数组

1
["checkBox8","checkBox6","checkBox7","checkBox5","checkBox12","checkBox3","checkBox10","checkBox13", "checkBox11","checkBox","checkBox9","checkBox4","checkBox14"]

发现密文由CipherDataHandler.getCipherText(this.userSequence)方法获得。主动调用的到密文”[!PL`Jd0ZWYXbYQ”。

image-20250518154519671

先用密钥对flag进行了维吉尼亚加密,然后闯入native中的processWithNative(String str);方法。先按十六进制的字符转为数组,然后进行base13编码。最后在b.a()中按2位十六进制转为字符串。

image-20250518160728184

注意到processWithNative(String str);是一个单字节加密,由于每个字符加密后的长度有可能不同,于是按单字节hook获取字符串表,并直接按照字典格式输出。

image-20250518175905689

hook获取维吉尼亚的静态密钥TheDaVinciCode。

image-20250518171218021

总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
Java.perform(function () {
let CipherDataHandler = Java.use("com.example.holygrail.CipherDataHandler");
var dec = CipherDataHandler.$new();
var ArrayList = Java.use("java.util.ArrayList");
var list = ArrayList.$new();
var box = ["checkBox8","checkBox6","checkBox7","checkBox5","checkBox12","checkBox3","checkBox10","checkBox13", "checkBox11","checkBox","checkBox9","checkBox4","checkBox14"]
box.forEach(function (item) {
list.add(item);
});
var res = dec.getCipherText(list);
console.log(res);

let a = Java.use("com.example.holygrail.a");
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;
};

var table ="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
var aa = a.$new();

var dict = {};

for (var i = 0; i < table.length; i++) {
var res3 = aa.processWithNative(table[i]);
dict[table[i]] = res3;

}
for (var key in dict) {
if (dict.hasOwnProperty(key)) {
console.log( "'" + dict[key]+"'" +": " + "'" + 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
108
109
110
dict = {'3921': '0',
'3A21': '1',
'3B21': '2',
'3C21': '3',
'4021': '4',
'4121': '5',
'4221': '6',
'4321': '7',
'4421': '8',
'4521': '9',
'46': 'a',
'47': 'b',
'48': 'c',
'49': 'd',
'4A': 'e',
'4B': 'f',
'4C': 'g',
'50': 'h',
'51': 'i',
'52': 'j',
'53': 'k',
'54': 'l',
'55': 'm',
'56': 'n',
'57': 'o',
'58': 'p',
'59': 'q',
'5A': 'r',
'5B': 's',
'5C': 't',
'60': 'u',
'61': 'v',
'62': 'w',
'63': 'x',
'64': 'y',
'65': 'z',
'5021': 'A',
'5121': 'B',
'5221': 'C',
'5321': 'D',
'5421': 'E',
'5521': 'F',
'5621': 'G',
'5721': 'H',
'5821': 'I',
'5921': 'J',
'5A21': 'K',
'5B21': 'L',
'5C21': 'M',
'30': 'N',
'31': 'O',
'32': 'P',
'33': 'Q',
'34': 'R',
'35': 'S',
'36': 'T',
'37': 'U',
'38': 'V',
'39': 'W',
'3A': 'X',
'3B': 'Y',
'3C': 'Z',
'2721': '!',
'2921': '#',
'2A21': '$',
'2B21': '%',
'2C21': '&',
'3021': '\'',
'3121': '(',
'3221': ')',
'3321': '*',
'3421': '+',
'3521': ',',
'3621': '-',
'3721': '.',
'3821': '/',
'4621': ':',
'4721': ';',
'4821': '<',
'4921': '=',
'4A21': '>',
'4B21': '?',
'4C21': '@',
'40': '[',
'41': '\\',
'42': ']',
'43': '^',
'44': '_',
'45': '`',
'66': '{',
'67': '|',
'68': '}',
'69': '~',}

enc = b"[!PL`Jd0ZWYXbYQ".hex().upper()

print(enc)
res = ""
i =0
while i < len(enc):
if enc[i+2:i+4] == '21':
res += dict[enc[i:i+4]]
i+=4
else:
res += dict[enc[i:i+2]]
i+=2

print(res)

#LhgueyNroqpwqi

用密钥进行维吉尼亚解密

image-20250518180053555

1
ISCC{SacredFeminine}

Final

re1CrackMe

nop一下花指令

image-20250517000100846

加密逻辑如下

image-20250517004757117

mm是密文,直接用赛博厨师解密。

image-20250517004833405

re2uglyCpp

大概逻辑就是二叉树后序遍历转int类型异或加密。

image-20250517021951528

std::function<void ()(std::shared_ptr,std::string &)>::operator()( &GxZuWxsXXlsb[abi:cxx11], head2_ro_xorkey,key_and_after);函数比较难看,动调从汇编跟进去就可以看到后序遍历的函数了。(其实是中序遍历,有反调试)。

get_xorkey()就是用原密钥生成一次密钥,enc函数里还会有个get_key()但是最后就只有一个异或,直接动调拿密钥就行了。

image-20250517022326478

下条件断点获得异或密钥

image-20250517015301066

1
2
3
import ida_dbg
edx = ida_dbg.get_reg_val("edx")
print(",",edx,end="")

可以构造一棵相同的线索二叉树,即可拿到字符在中序遍历后的表。动调的时候输入一串字符“abcdefghijklmnopqrstuvwxyzABCDEFGHIJ”,即可得到字符映射表

image-20250517034454942

main函数上面有个函数有ptrace反调试会把中序遍历函数改为后序遍历,绕过一下。

image-20250517034646875

密文在cmp()函数里面,脚本如下

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
import string
def decxor(data:list,key:list) -> list:
res =[]
for i,k in zip(data,key):
res.append(i^k)
return res

def to_str(data:list) -> str:
res =b''
for i in data:
a = i.to_bytes(4,byteorder="little")
res+=a
return res.decode()

def recover(data:str,chartable:list,after_chartable:list) -> str:
res =[]
index = [i for i in range(36)]
for i in range(36):
index[i] = after_chartable.index(chartable[i])
res.append(data[index[i]])
return "".join(res)

enc = [133064547, 2490223663, 2979342693, 4108347074, 1177400706, 206648502, 2095174241, 3887509941, 147376445]
key =[1054224987, 3607740183, 3824320024, 2693203857, 21419769, 2086748415, 280400902, 2497569014, 1085455212]

a = decxor(enc,key)

b = to_str(a)

chartable = string.ascii_lowercase+string.ascii_uppercase
chartable = chartable[0:36:]
print("table:",chartable)

after_chartable = "FpGhHqIdJrisbtjuevkwaxlyfzmAcBnCgDoE"

flag = recover(b,chartable,after_chartable)
print(b)
print('result:',flag)
#result: ISCC{gQ9ggk0WkzUsWRaTcGtpFlQsrH888d}

mobile1GGAD

首先要输入密钥,在validateKey()方法下调用了native函数validateKey(),里面要求密钥的sha256是”e60bc9dff5c6c5b4b63b8257ae4d55dfe1d8a622ecb531a2a9898c8fe5c1cd15”。

在apk的res文件夹里有个压缩包,里面有很多后缀为png的txt文件,按大小排序后在png614.png里可找到一个脚本,观察加密脚本发现是lsb隐写,用zsteg尝试res中大小在100KB以上的图片即可找到flag。

80269F1C2B0308145796A5FC05E3782B

找到一串字符”ExpectoPatronum”,计算sha256和上面的值是相同的,这就是密钥。

继续分析加密流程,flag先传入native中的JNI1(),是一个标准RC4加密。

image-20250517130052266

加密结果转为二进制,然后在JNI2中转为16进制,并对输入的二进制进行了取反(异或0xff)。

image-20250517133814837

结果再传入b.a()中,按照下标的奇偶位置拆分成两部分,第一部分和一个二进制字符串比较,第二部分与一个维吉尼亚解密出来的字符比较。

先frida主动调用拿到维吉尼亚解密后的字符串E2C6E85E481439。

1
2
3
4
5
Java.perform(function () {
let c = Java.use("com.example.ggad.c").$new();
var res = c.decrypt("I2Z6T85I481439","ExpectoPatronum");
console.log(`c.a result=${res}`);
})

image-20250517144857554

按照逻辑写恢复脚本。

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
import binascii
import Crypto.Cipher.ARC4 as RC4

def bin_to_hex(binary):
res = list(int(binary[i:i+8],2) for i in range(0,len(binary),8))
return "".join(chr(i) for i in res)

def NOT(a:bytes)->bytes:
res =b""
for i in a:
res += (i ^ 0xff).to_bytes(1)
return res

binary1 = "0100001100110100010001000011001000110010001101110100001100110001001101010011000001000010001100100100001100110100"
a = bin_to_hex(binary1)

b = "E2C6E85E481439"

res =[0] * len(a)*2

# 拼接
for i in range(0,len(a)*2):
if i % 2 == 0:
res[i] = a[i//2]
else:
res[i] = b[i//2]

dec_data = "".join(res)
dec_data = binascii.unhexlify(dec_data)

# 取反
dec_data = NOT(dec_data)
#RC4解密
key = b"ExpectoPatronum"
cipher = RC4.new(key)
flag = cipher.decrypt(dec_data)

print("ISCC{" + flag.decode() + "}")

mobile2叽米是梦的开场白

需要先加载一个dex文件,在mobile4.so的decryptSegment函数里。题目调用dex里的checkFlag方法对前半段flag进行了check。

image-20250516220518898

导出dex

image-20250516220706632

发现是一个3DES加密

image-20250516221324228

去Sunday.so里的getkey()寻找到密钥“74KLVll8hUBIt3joOiKfYLCj”。

image-20250516221448754

解3DES即可拿到前半段。

程序从asset中加载了一个名为enreal的lib文件,然后把后半段flag和lib文件一起传入了example.mobile04.a中的一个native函数checkFlag2,到Monday.so中分析checkFlag2函数。

在checkFlag2中,并没有对我们输入的flag进行操作 ,而是对传入的lib文件进行了一些异或操作。而且函数在末尾固定返回0,也就是如果这个函数用于校验flag,那么校验永远也不会成功。推测这个实际是lib文件的解密函数。这里ida伪代码有点问题,看汇编可以恢复解密逻辑。

image-20250516230119636

写个脚本解密一下asset中的enreal(X64)。

1
2
3
4
5
6
7
8
f = open("enreal", "rb").read()
input = list(f)
for i in range(len(input)):
input[i] = (((input[i] << 2) | (input[i] >> 6)) & 0xff) ^ 0x44
input[i] = ((input[i] >> 3) | (input[i] << 5)) & 0xff

outputf = open("decode_enreal", "wb")
outputf.write(bytes(input))

解密后在里面找到一个real_check(),和前一段差不多,这里也是一个3DES加密。密文:0x298B602DA18468FC;密钥:“9uzGw2TJszopH0NAecGL0sUS”。脚本解密即可得到后半段flag。

image-20250516232519907

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
from Crypto.Cipher import DES3
from Crypto.Util.Padding import unpad

key = b"74KLVll8hUBIt3joOiKfYLCj"

ciphertext = "".join(chr(i) for i in [69, 49, 52, 68, 53, 53, 53, 66, 50, 65, 57, 55, 56, 50, 52, 53])
ciphertext = bytes.fromhex(ciphertext)
cipher = DES3.new(key, DES3.MODE_ECB)

flag =""
try:
plaintext = unpad(cipher.decrypt(ciphertext), DES3.block_size)
flag = plaintext.decode()
except ValueError as e:
print(e)

ciphertext2 = bytes.fromhex("298B602DA18468FC")
ciphertext2 = ciphertext2[::-1]

key2 = b"9uzGw2TJszopH0NAecGL0sUS"

cipher2 = DES3.new(key2, DES3.MODE_ECB)
plaintext2 = cipher2.decrypt(ciphertext2)
flag += plaintext2.decode()

print("ISCC{" + flag +"}")
# ISCC{Zccr1lzQTGEr0d}