MOBILE-消失的喵星密使

ISCC2026 WriteUp 提交模板

MOBILE-消失的喵星密使

解题思路

1.看一下程序

image.png

来一个有emoji点选的密码界面

image.png

上面的注释里写了详细逻辑
我们继续往下看

image.png

这个函数提取flag中间部分,并检验。提取逻辑在下面这个函数里面:

image.png

这些字节异或 0x42 后得到 ISCC{,并且要求输入以 } 结尾,所以 flag 格式确定为 ISCC{flag_body}。

2.根据协议文件和 MD5 恢复点击序列
回忆一下初始化:

image.png

6x6 表格的行列字符集是:

D V F G X A

同时,assets/sys/OBSERVER_PROTOCOL 看起来是十六进制文本。解码后能看到协议约束:

P1 = (1,1)
P2 = (2,4)
P6 = (6,1)
Row_Total ≡ 21
Col_Total ≡ 20

image.png

image.png

image.png

因为由 6 个格子组成,每个格子贡献一对行列字符,所以可以固定 P1、P2、P6,枚举中间 3 个点,再用行列和与 MD5 过滤。得到:

q = DDVGFAGVXAAD

3.复现native校验

Java 层 _A.check() 调用 native:

public native boolean _v(String str, String str2, Bitmap bitmap);

参数分别是 flag_body、q 和 assets/hint.png。分析 libmobile03.so 可以看到:

  • _f_ext 从 hint.png 的 LSB 中提取数据,并要求前 4 字节是 MEOW
  • MEOW 后面的 256 字节是自定义 S-box
  • _f1 使用 _q 和 native 中的 36 字符表生成 32 字节 key
  • _f_enc 是使用该 S-box 的 AES-256 变体
  • 内置目标串为 o8kfT+HTQprrOJ0XZSR0WKoVkhfBq/xUhTIQa1D2O+Q=

实际图片数据藏在蓝色通道 LSB 中。Base64 目标串解码后还要逐字节异或 0x66,得到真正密文。逆向 AES-256 变体后得到 flag body。

完整解题脚本如下,直接从 APK 读取 assets/hint.png,恢复 _q,提取 S-box,派生 key,并输出 flag:

运行结果:

q = DDVGFAGVXAAD
key = 331f39644b3b418987f6b9b5d7fb15403727656563d2a5d1fbd7f15c13430941
flag = ISCC{k1t7y_pa7rol_adE6r}

image.png

Exp

#!/usr/bin/env python3
from __future__ import annotations

import hashlib
import itertools
import sys
import zipfile
from base64 import b64decode
from dataclasses import dataclass
from pathlib import Path

from PIL import Image


@dataclass(frozen=True)
class ChallengeSpec:
    apk_name: str = "app-release.apk"
    row_col_alphabet: str = "DVFGXA"
    q_md5: str = "ef37302826a6f0fb5fc3372ae060ba58"
    lookup_table: bytes = b"g5zkvit4yjnqohamucds628lwr703ebfp19x"
    cipher_b64: str = "o8kfT+HTQprrOJ0XZSR0WKoVkhfBq/xUhTIQa1D2O+Q="
    rcon: tuple[int, ...] = (1, 2, 4, 8, 16, 32, 64, 128, 0x1B, 0x36)
    inverse_shift: tuple[int, ...] = (0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11)
    hidden_magic: bytes = b"MEOW"


SPEC = ChallengeSpec()


def apk_path_from_argv() -> Path:
    if len(sys.argv) > 1:
        return Path(sys.argv[1])
    return Path(__file__).with_name(SPEC.apk_name)


def read_hint_image(apk_path: Path) -> Image.Image:
    with zipfile.ZipFile(apk_path, "r") as archive:
        with archive.open("assets/hint.png") as fp:
            return Image.open(fp).convert("RGB")


def recover_sbox_from_image(img: Image.Image) -> list[int]:
    payload = bytearray(0x106)
    bit_cursor = 0

    for y in range(img.height):
        for x in range(img.width):
            blue = img.getpixel((x, y))[2]
            if blue & 1:
                payload[bit_cursor // 8] |= 1 << (7 - (bit_cursor % 8))
            bit_cursor += 1
            if bit_cursor >= 0x830:
                break
        if bit_cursor >= 0x830:
            break

    if bytes(payload[:4]) != SPEC.hidden_magic:
        raise RuntimeError(f"unexpected embedded magic: {bytes(payload[:4])!r}")

    sbox = list(payload[4:260])
    if len(set(sbox)) != 256:
        raise RuntimeError("embedded S-box is not a permutation")
    return sbox


def pair_from_coordinates(row: int, col: int) -> str:
    alphabet = SPEC.row_col_alphabet
    return alphabet[row - 1] + alphabet[col - 1]


def coordinates_from_pair(pair: str) -> tuple[int, int]:
    alphabet = SPEC.row_col_alphabet
    return alphabet.index(pair[0]) + 1, alphabet.index(pair[1]) + 1


def recover_click_sequence() -> str:
    first = pair_from_coordinates(1, 1)
    second = pair_from_coordinates(2, 4)
    sixth = pair_from_coordinates(6, 1)
    candidates = [a + b for a in SPEC.row_col_alphabet for b in SPEC.row_col_alphabet]

    for middle in itertools.product(candidates, repeat=3):
        sequence = [first, second, *middle, sixth]
        rows, cols = zip(*(coordinates_from_pair(token) for token in sequence))

        if sum(rows) != 21 or sum(cols) != 20:
            continue

        q = "".join(sequence)
        if hashlib.md5(q.encode()).hexdigest() == SPEC.q_md5:
            return q

    raise RuntimeError("failed to recover _q")


def reorder_click_sequence(q: str) -> str:
    # Decompiled _f1 groups indices by offset modulo 4 in this order.
    order = (1, 0, 2, 3)
    return "".join(q[i] for offset in order for i in range(offset, len(q), 4))


def derive_aes_key(q: str, sbox: list[int]) -> bytes:
    reordered = reorder_click_sequence(q)
    axis_index = {ch: i for i, ch in enumerate("ADFGVX")}
    out = bytearray()

    for i in range(32):
        left = reordered[(2 * i) % len(reordered)]
        right = reordered[(2 * i + 1) % len(reordered)]
        table_index = axis_index[left] * 6 + axis_index[right]
        out.append(sbox[SPEC.lookup_table[table_index]] ^ ((i * 0x13) & 0xFF))

    return bytes(out)


def xtime(value: int) -> int:
    return ((value << 1) & 0xFF) ^ (0x1B if value & 0x80 else 0)


def gf_mul(a: int, b: int) -> int:
    result = 0
    while b:
        if b & 1:
            result ^= a
        a = xtime(a)
        b >>= 1
    return result & 0xFF


def invert_sbox(sbox: list[int]) -> list[int]:
    inverse = [0] * 256
    for i, value in enumerate(sbox):
        inverse[value] = i
    return inverse


def expand_round_keys(key: bytes, sbox: list[int]) -> bytes:
    round_keys = bytearray(240)
    round_keys[:32] = key

    for word_index in range(8, 60):
        temp = list(round_keys[(word_index - 1) * 4 : word_index * 4])

        if word_index % 8 == 0:
            temp = [
                sbox[temp[1]] ^ SPEC.rcon[word_index // 8 - 1],
                sbox[temp[2]],
                sbox[temp[3]],
                sbox[temp[0]],
            ]
        elif word_index % 8 == 4:
            temp = [sbox[x] for x in temp]

        for j in range(4):
            round_keys[word_index * 4 + j] = round_keys[(word_index - 8) * 4 + j] ^ temp[j]

    return bytes(round_keys)


def add_round_key(state: bytearray, round_keys: bytes, round_index: int) -> bytearray:
    start = 16 * round_index
    return bytearray(x ^ y for x, y in zip(state, round_keys[start : start + 16]))


def inverse_sub_shift(state: bytearray, inverse_sbox: list[int]) -> bytearray:
    out = bytearray(16)
    for out_idx, src_idx in enumerate(SPEC.inverse_shift):
        out[src_idx] = inverse_sbox[state[out_idx]]
    return out


def inverse_mix_columns(state: bytearray) -> bytearray:
    out = bytearray(16)
    for col in range(4):
        a = state[4 * col : 4 * col + 4]
        out[4 * col + 0] = gf_mul(a[0], 14) ^ gf_mul(a[1], 11) ^ gf_mul(a[2], 13) ^ gf_mul(a[3], 9)
        out[4 * col + 1] = gf_mul(a[0], 9) ^ gf_mul(a[1], 14) ^ gf_mul(a[2], 11) ^ gf_mul(a[3], 13)
        out[4 * col + 2] = gf_mul(a[0], 13) ^ gf_mul(a[1], 9) ^ gf_mul(a[2], 14) ^ gf_mul(a[3], 11)
        out[4 * col + 3] = gf_mul(a[0], 11) ^ gf_mul(a[1], 13) ^ gf_mul(a[2], 9) ^ gf_mul(a[3], 14)
    return out


def decrypt_block(block: bytes, round_keys: bytes, inverse_sbox: list[int]) -> bytes:
    state = bytearray(block)

    for round_index in range(14, 0, -1):
        state = add_round_key(state, round_keys, round_index)
        if round_index != 14:
            state = inverse_mix_columns(state)
        state = inverse_sub_shift(state, inverse_sbox)

    return bytes(add_round_key(state, round_keys, 0))


def decode_flag_body(round_keys: bytes, inverse_sbox: list[int]) -> str:
    ciphertext = bytes(x ^ 0x66 for x in b64decode(SPEC.cipher_b64))
    plaintext = b"".join(
        decrypt_block(ciphertext[offset : offset + 16], round_keys, inverse_sbox)
        for offset in range(0, len(ciphertext), 16)
    )
    return plaintext.rstrip(b"\x00").decode()


def decode_flag_prefix() -> str:
    return "".join(chr(x ^ 0x42) for x in (11, 17, 1, 1, 57))


def main() -> None:
    apk_path = apk_path_from_argv()
    sbox = recover_sbox_from_image(read_hint_image(apk_path))
    q = recover_click_sequence()
    key = derive_aes_key(q, sbox)
    round_keys = expand_round_keys(key, sbox)
    flag_body = decode_flag_body(round_keys, invert_sbox(sbox))
    flag = f"{decode_flag_prefix()}{flag_body}}}"

    print(f"q = {q}")
    print(f"key = {key.hex()}")
    print(f"flag = {flag}")


if __name__ == "__main__":
    main()

posted @ 2026-05-19 16:28  MillionMind  阅读(18)  评论(0)    收藏  举报