MOBILE-蔡文姬的胡笳琴音波谜阵
ISCC2026 WriteUp 提交模板
MOBILE-蔡文姬的胡笳琴音波谜阵
解题思路
1.使用jadx加载看MainActivity

发现加载了System.loadLibrary("mobile04");
继续看,前面的check其实是个反调试和逻辑没关系

我们接着看init是个交互

这里是个判断:

注意看这一行:if (e1.callNative(file.getAbsolutePath(), e1.v(strTrim), strTrim.substring(5, strTrim.length() - 1))) {
很明显了,第三行是ISCC{XXX}中间部分。
String strTrim = editText.getText().toString().trim();而edit实现在init中,所以我们还得分析下init函数
for (int i = 0; i < 8; i++) {
ImageButton imageButton = (ImageButton) findViewById(iArr[i]);
if (imageButton != null) {
this.mm.attach(imageButton, this);
}
}
这是会生成8个按钮:
int[] iArr = {R.id.v_mem_83E1A, R.id.v_0xAF47B, R.id.v_res_22E8A1, R.id.v_ptr_7BD2, R.id.v_temp_9F002B, R.id.v_0x331F, R.id.v_0x5CB7, R.id.v_0x11C90E};
每个按钮点击后都追加个code


就是:
hashMap.put(Integer.valueOf(R.id.v_0x331F), new SkinData("FLOW", "329.6"));
hashMap.put(Integer.valueOf(R.id.v_0xAF47B), new SkinData("CWJ", "261.6"));
hashMap.put(Integer.valueOf(R.id.v_res_22E8A1), new SkinData("ROSE", "392.0"));
hashMap.put(Integer.valueOf(R.id.v_0x11C90E), new SkinData("XMAS", "293.7"));
hashMap.put(Integer.valueOf(R.id.v_0x5CB7), new SkinData("STAR", "349.2"));
hashMap.put(Integer.valueOf(R.id.v_ptr_7BD2), new SkinData("GOAL", "440.0"));
hashMap.put(Integer.valueOf(R.id.v_temp_9F002B), new SkinData("MAGI", "523.3"));
hashMap.put(Integer.valueOf(R.id.v_mem_83E1A), new SkinData("DANCE", "493.9"));
按频率从低到高排列:
261.6 -> CWJ
293.7 -> XMAS
329.6 -> FLOW
349.2 -> STAR
392.0 -> ROSE
440.0 -> GOAL
493.9 -> DANCE
523.3 -> MAGI
所以正确点击序列为:
CWJXMASFLOWSTARROSEGOALDANCEMAGI
即
image2 -> image8 -> image6 -> image7 -> image3 -> image4 -> image1 -> image5
2.分析校验

E1.v() 根据点击序列生成 native 第二个参数 cw

if (inputSequence == null || inputSequence.length() < 25 || inputSequence.length() > 64) { return "cw:err_len:00000000"; }
长度做了要求
iArr = Z1.d(K1.g(this.ctx));:

这里是so层的实现,获取了个key
我们继续看K1.g:
InputStream openRawResource = context.getResources().openRawResource(R.raw.guanggao_plus);
int available = openRawResource.available();
return ("com.example.mobile04".hashCode() ^ available) ^ MAGIC_MASK;
追踪后发现:
package = "com.example.mobile04"
guanggao_plus.mp4 size = 77055908
MAGIC_MASK = -1025350075
native 的 Z1.d(int) 本质是把输入整数复制成 4 个 word,再与常量异或。静态提取出的 16 字节常量为:
f1 0d 1a e2 43 83 19 e7 2a c9 97 b2 43 8a 2a 80

继续看C1.t
public static byte[] t(byte[] bArr) {
byte[] bArr2 = new byte[bArr.length];
for (int i = 0; i < bArr.length; i++) {
byte b = bArr[i];
bArr2[i] = (byte) (((b & 85) << 1) | ((b & 170) >> 1));
}
return bArr2;
}
也就是交换每个字节中相邻 bit:
bit0 <-> bit1
bit2 <-> bit3
bit4 <-> bit5
bit6 <-> bit7
接着又做了

追踪有C1.D = -1640531527,即 0x9e3779b9。
最终格式为:
return String.format("cw:%s:%08x", hex8, crc32);
对正确序列计算得到:
cw:05d6f1cf55a9d2fc:7d25d211
3.分析so文件
JNI_OnLoad` 动态注册 Z1.l 和 Z1.d
动态符号看到:
JNI_OnLoad
_Z14_n_derive_implP7_JNIEnvP7_jclassi
_Z7_n_implP7_JNIEnvP8_jobjectP8_jstringS4_S4_
其实就是:
Z1.d -> _n_derive_impl
Z1.l -> _n_impl
在 native 中追踪提取到三个关键全局常量:
_K1 = "bwkwJ"
_K2 = "94p%%JmBDal6Q0&x"
_TGT = "5PFcZRidVzA4Cp3wfiugoDWyo1WARao5XL2KxBf4Q34x"
所以流程大概就是:
crc32(cw + db前16字节)
-> 解密 assets/appview_cache.db
-> 取两个 seed
-> 用 _K2 初始化 RC6 类算法
-> 加密 body
-> 与 base58_decode(_TGT) ^ _K1 比较
把 _TGT base58 解码后异或 _K1,得到目标密文;再用正确点击序列解出的 seed 做 RC6 逆运算,得到 body:
JiaQinBoP%94p^^JmBDal6Q0*xdTuT-=
所以最终flag就是:ISCC{JiaQinBoP%94p^^JmBDal6Q0*xdTuT-=}
Exp
from pathlib import Path
import binascii
import struct
WORD_MASK = 0xFFFFFFFF
CASE_DIR = Path(__file__).resolve().parent / "app-release"
CACHE_FILE = CASE_DIR / "apktool-out" / "assets" / "appview_cache.db"
AD_VIDEO_FILE = CASE_DIR / "apktool-out" / "res" / "raw" / "guanggao_plus.mp4"
APP_ID = "com.example.mobile04"
RESOURCE_MIXER = (-1025350075) & WORD_MASK
MELODY_ORDER = "CWJXMASFLOWSTARROSEGOALDANCEMAGI"
DERIVE_MASK_BYTES = bytes.fromhex("f10d1ae2438319e72ac997b2438a2a80")
TARGET_XOR_KEY = b"bwkwJ"
ROUND_KEY_MATERIAL = b"94p%%JmBDal6Q0&x"
ENCODED_TARGET = "5PFcZRidVzA4Cp3wfiugoDWyo1WARao5XL2KxBf4Q34x"
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def keep_word(value):
return value & WORD_MASK
def rotate_right(value, amount):
amount &= 31
if amount == 0:
return keep_word(value)
return keep_word((value >> amount) | (value << (32 - amount)))
def rotate_left(value, amount):
return rotate_right(value, -amount)
def android_hash(text):
digest = 0
for char in text:
digest = keep_word(digest * 31 + ord(char))
return digest
def rebuild_native_words(seed):
masks = struct.unpack("<4I", DERIVE_MASK_BYTES)
return [keep_word(seed ^ item) for item in masks]
def swap_adjacent_bits(blob):
return bytes((((byte & 0x55) << 1) | ((byte & 0xAA) >> 1)) for byte in blob)
def fnv1a_64(blob):
digest = 0xCBF29CE484222325
for byte in blob:
digest = ((digest ^ byte) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
return digest
def tea_pack(left, right, words):
total = 0
delta = 0x9E3779B9
for _ in range(32):
total = keep_word(total + delta)
left = keep_word(left + ((((right << 4) & WORD_MASK) + words[0]) ^ keep_word(right + total) ^ ((right >> 5) + words[1])))
right = keep_word(right + ((((left << 4) & WORD_MASK) + words[2]) ^ keep_word(left + total) ^ ((left >> 5) + words[3])))
return left, right
def build_cw_token(melody):
video_size = AD_VIDEO_FILE.stat().st_size
seed = keep_word(android_hash(APP_ID) ^ video_size ^ RESOURCE_MIXER)
tea_words = rebuild_native_words(seed)
digest = fnv1a_64(swap_adjacent_bits(melody.encode()))
payload = struct.pack("<2I", *tea_pack(digest & WORD_MASK, digest >> 32, tea_words))
checksum = binascii.crc32(payload) & WORD_MASK
return "cw:%s:%08x" % (payload.hex(), checksum)
def unpack_cache_seed_pair(cw_token):
raw_cache = CACHE_FILE.read_bytes()
cache_crc = binascii.crc32(cw_token.encode() + raw_cache[:16]) & WORD_MASK
stream = bytearray(raw_cache[16:])
for offset, value in enumerate(stream):
stream[offset] = value ^ (((cache_crc >> ((offset & 3) * 8)) + offset) & 0xFF)
first_seed = struct.unpack("<I", stream[0x80:0x84])[0]
second_seed = struct.unpack("<I", stream[0xB4:0xB8])[0]
return cache_crc, first_seed, second_seed
def expand_round_table(first_seed, second_seed):
stride = keep_word((first_seed ^ rotate_right(second_seed, 7)) + 0x84F32551) | 1
state = keep_word((first_seed ^ rotate_right(second_seed, 19)) ^ 0x5366ECE2)
table = [keep_word(state + index * stride) for index in range(44)]
user_words = list(struct.unpack("<4I", ROUND_KEY_MATERIAL))
mixed_a = mixed_b = table_index = user_index = 0
for _ in range(132):
mixed_a = table[table_index] = rotate_left(keep_word(table[table_index] + mixed_a + mixed_b), 3)
mixed_b = user_words[user_index] = rotate_left(
keep_word(user_words[user_index] + mixed_a + mixed_b),
keep_word(mixed_a + mixed_b) & 31,
)
table_index = (table_index + 1) % 44
user_index = (user_index + 1) % 4
return table
def decrypt_target_block(cipher_block, table):
word_a, word_b, word_c, word_d = struct.unpack("<4I", cipher_block)
word_c = keep_word(word_c - table[43])
word_a = keep_word(word_a - table[42])
for round_no in range(20, 0, -1):
word_a, word_b, word_c, word_d = word_d, word_a, word_b, word_c
mix_d = rotate_left(keep_word(word_d * keep_word(2 * word_d + 1)), 5)
mix_b = rotate_left(keep_word(word_b * keep_word(2 * word_b + 1)), 5)
word_c = keep_word(rotate_right(keep_word(word_c - table[2 * round_no + 1]), mix_b & 31) ^ mix_d)
word_a = keep_word(rotate_right(keep_word(word_a - table[2 * round_no]), mix_d & 31) ^ mix_b)
word_d = keep_word(word_d - table[1])
word_b = keep_word(word_b - table[0])
return struct.pack("<4I", word_a, word_b, word_c, word_d)
def base58_to_bytes(text):
little_endian_digits = [0]
for char in text:
carry = BASE58_ALPHABET.index(char)
for index in range(len(little_endian_digits)):
value = little_endian_digits[index] * 58 + carry
little_endian_digits[index] = value & 0xFF
carry = value >> 8
while carry:
little_endian_digits.append(carry & 0xFF)
carry >>= 8
for char in text:
if char == "1":
little_endian_digits.append(0)
else:
break
return bytes(reversed(little_endian_digits))
def solve():
cw_token = build_cw_token(MELODY_ORDER)
cache_crc, first_seed, second_seed = unpack_cache_seed_pair(cw_token)
round_table = expand_round_table(first_seed, second_seed)
decoded_target = base58_to_bytes(ENCODED_TARGET)
expected_cipher = bytes(
value ^ TARGET_XOR_KEY[index % len(TARGET_XOR_KEY)]
for index, value in enumerate(decoded_target[:32])
)
flag_body = decrypt_target_block(expected_cipher[:16], round_table)
flag_body += decrypt_target_block(expected_cipher[16:], round_table)
print("[melody]", MELODY_ORDER)
print("[cw]", cw_token)
print("[cache-crc] %08x" % cache_crc)
print("[seeds] %08x %08x" % (first_seed, second_seed))
print("[flag] ISCC{%s}" % flag_body.decode())
if __name__ == "__main__":
solve()

浙公网安备 33010602011771号