MOBILE-蔡文姬的胡笳琴音波谜阵

ISCC2026 WriteUp 提交模板

MOBILE-蔡文姬的胡笳琴音波谜阵

解题思路

1.使用jadx加载看MainActivity

image.png

发现加载了System.loadLibrary("mobile04");

继续看,前面的check其实是个反调试和逻辑没关系

image.png

我们接着看init是个交互

image.png

这里是个判断:

image.png

注意看这一行: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

image.png

image.png

就是:

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.分析校验

image.png

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

image.png

if (inputSequence == null || inputSequence.length() < 25 || inputSequence.length() > 64) { return "cw:err_len:00000000"; }

长度做了要求

iArr = Z1.d(K1.g(this.ctx));

image.png

这里是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

image.png

继续看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

接着又做了

image.png

追踪有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()
posted @ 2026-05-19 16:32  MillionMind  阅读(12)  评论(0)    收藏  举报