Mobile-密码验证

ISCC2026 WriteUp 提交模板

Mobile-密码验证

在混乱交织的关系中寻找正确的方向

解题思路

1.jadx打开apk,搜索ISCC定位逻辑

image.png
image.png

简化下就是:

if (flag.length() == 36 && startsWith("ISCC{") && endsWith("}")) {
    verifying = true;
    resultText = "验证中…";
    resultColor = gray;
    launch coroutine -> 走真正校验逻辑
} else {
    resultText = "格式错误";
    resultColor = red;
    Toast.makeText(..., "格式错误", 0).show();
}

追踪:new MainActivity$onCreate$1$1$1$1$1$1$2$1$1(...)

image.png|608

很明显要看boolean ok = NativeBridge.verifyFlag(assets, this.$flag);

image.png

现在需要去看so层了,IDA打开so文件,直接搜函数名定位。

分析内容如下:

image.png

image.png

image.png

image.png

image.png

image.png

现在我们梳理一下:

  • 入口先把 jstring 取成 C++ std::string,做格式检查:长度 36、前缀 ISCC{、后缀 }。
  • sub_29B30 从 APK 里读出 4 个 asset:cipher1.bin、cipher2.bin、cipher3.bin、puzzle.bin。
  • sub_29E30 把 .rodata 里的三组 16 字节常量做变换:(x - 0x13) ^ 0x5A。
  • sub_2A510 是 16 轮 TEA-like,加密 part1 的 8 字节后和 cipher1.bin 比对。
  • sub_2A610 是 GF(2^8) 上的 4x4 矩阵乘法,用来校验 part2 的 12 字节和 cipher2.bin。
  • sub_2A8F0 是 64-bit LCG,拿 p2 前 8 字节做 seed,生成 10 字节 keystream 去验 part3 和 cipher3.bin。
  • sub_2AC30 做 part1 + part2 的 xor/sum,最后两字节约束 part3 尾部。
  • sub_2A5D0 本质是通用等值比较封装。

所以还原步骤为:
part1: cipher1.bin 用 16 轮 TEA-like 解密,再 xor rolling key
part2: cipher2.bin 按 GF(2^8) 4x4 矩阵求逆
part3: cipher3.bin 用 LCG 生成 keystream 后 xor

image.png

ISCC{o<y:$Mcjh;t\9G.pt3*FPq]T]+Uz0#}

Exp

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

import argparse
import struct
import sys
import zipfile
from dataclasses import dataclass
from pathlib import Path


ALPHABET = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}_-!@#$%^&*()[]<>?.,:;+/\\" 
SBOX = bytes.fromhex(
    "637c777bf26b6fc53001672bfed7ab76"
    "ca82c97dfa5947f0add4a2af9ca472c0"
    "b7fd9326363ff7cc34a5e5f171d83115"
    "04c723c31896059a071280e2eb27b275"
    "09832c1a1b6e5aa0523bd6b329e32f84"
    "53d100ed20fcb15b6acbbe394a4c58cf"
    "d0efaafb434d338545f9027f503c9fa8"
    "51a3408f929d38f5bcb6da2110fff3d2"
    "cd0c13ec5f974417c4a77e3d645d1973"
    "60814fdc222a908846eeb814de5e0bdb"
    "e0323a0a4906245cc2d3ac629195e479"
    "e7c8376d8dd54ea96c56f4ea657aae08"
    "ba78252e1ca6b4c6e8dd741f4bbd8b8a"
    "703eb5664803f60e613557b986c11d9e"
    "e1f8981169d98e949b1e87e9ce5528df"
    "8ca1890dbfe6426841992d0fb054bb16"
)

ROUND_DELTA = 0x9E3779B9
LCG_MULT = 0x5851F42D4C957F2D
LCG_INC = 0x14057B7EF767814F
MASK32 = 0xFFFFFFFF
MASK64 = (1 << 64) - 1
ROW_PERMUTATION = (2, 3, 0, 1)
PREFERRED_LIBRARIES = (
    "lib/x86_64/libnativecrypto.so",
    "lib/x86/libnativecrypto.so",
    "lib/arm64-v8a/libnativecrypto.so",
)


@dataclass(frozen=True)
class ApkPayload:
    cipher1: bytes
    cipher2: bytes
    cipher3: bytes
    puzzle: bytes
    native_blob: bytes
    native_path: str


@dataclass(frozen=True)
class DerivedState:
    stage_a: bytes
    stage_b: bytes
    stage_c: bytes


def gf_product(left: int, right: int) -> int:
    result = 0
    x = left & 0xFF
    y = right & 0xFF
    while y:
        if y & 1:
            result ^= x
        carry = x & 0x80
        x = (x << 1) & 0xFF
        if carry:
            x ^= 0x1B
        y >>= 1
    return result


def gf_inverse(value: int) -> int:
    for candidate in range(1, 256):
        if gf_product(value, candidate) == 1:
            return candidate
    raise ValueError("matrix is not invertible")


def invert_matrix_4x4(matrix: bytes) -> list[list[int]]:
    rows: list[list[int]] = []
    for row_index in range(4):
        identity = [0, 0, 0, 0]
        identity[row_index] = 1
        rows.append(list(matrix[row_index * 4 : (row_index + 1) * 4]) + identity)

    for pivot_col in range(4):
        pivot_row = next((idx for idx in range(pivot_col, 4) if rows[idx][pivot_col]), None)
        if pivot_row is None:
            raise ValueError("matrix is singular")
        rows[pivot_col], rows[pivot_row] = rows[pivot_row], rows[pivot_col]

        inv = gf_inverse(rows[pivot_col][pivot_col])
        rows[pivot_col] = [gf_product(cell, inv) for cell in rows[pivot_col]]
        for row_index in range(4):
            if row_index == pivot_col or not rows[row_index][pivot_col]:
                continue
            factor = rows[row_index][pivot_col]
            rows[row_index] = [
                rows[row_index][offset] ^ gf_product(factor, rows[pivot_col][offset])
                for offset in range(8)
            ]

    return [row[4:] for row in rows]


def decode_native_byte(raw: int) -> int:
    return ((raw - 0x13) & 0xFF) ^ 0x5A


def xor_bytes(left: bytes, right: bytes) -> bytes:
    return bytes(a ^ b for a, b in zip(left, right))


def iter_chunks(blob: bytes, width: int):
    for start in range(0, len(blob), width):
        yield blob[start : start + width]


def gf_dot(coeffs, values) -> int:
    acc = 0
    for coeff, value in zip(coeffs, values):
        acc ^= gf_product(coeff, value)
    return acc


def tea_encrypt_block(block: bytes, key_words) -> bytes:
    left, right = struct.unpack(">2I", block)
    total = 0
    for _ in range(16):
        total = (total + ROUND_DELTA) & MASK32
        left = (left + ((((right << 4) & MASK32) + key_words[0]) ^ ((right + total) & MASK32) ^ ((right >> 5) + key_words[1]))) & MASK32
        right = (right + ((((left << 4) & MASK32) + key_words[2]) ^ ((left + total) & MASK32) ^ ((left >> 5) + key_words[3]))) & MASK32
    return struct.pack(">2I", left, right)


def tea_decrypt_block(block: bytes, key_words) -> bytes:
    left, right = struct.unpack(">2I", block)
    total = (ROUND_DELTA * 16) & MASK32
    for _ in range(16):
        right = (right - ((((left << 4) & MASK32) + key_words[2]) ^ ((left + total) & MASK32) ^ ((left >> 5) + key_words[3]))) & MASK32
        left = (left - ((((right << 4) & MASK32) + key_words[0]) ^ ((right + total) & MASK32) ^ ((right >> 5) + key_words[1]))) & MASK32
        total = (total - ROUND_DELTA) & MASK32
    return struct.pack(">2I", left, right)


def load_payload(apk_path: Path) -> ApkPayload:
    with zipfile.ZipFile(apk_path, "r") as archive:
        native_candidates = [name for name in archive.namelist() if name.endswith("/libnativecrypto.so")]
        if not native_candidates:
            raise FileNotFoundError("libnativecrypto.so not found in APK")

        native_path = next((name for name in PREFERRED_LIBRARIES if name in native_candidates), native_candidates[0])
        return ApkPayload(
            cipher1=archive.read("assets/cipher1.bin"),
            cipher2=archive.read("assets/cipher2.bin"),
            cipher3=archive.read("assets/cipher3.bin"),
            puzzle=archive.read("assets/puzzle.bin"),
            native_blob=archive.read(native_path),
            native_path=native_path,
        )


def locate_native_constants(native_blob: bytes) -> tuple[bytes, bytes, bytes]:
    marker = native_blob.find(ALPHABET)
    if marker < 48:
        raise ValueError("failed to locate charset/key constants in native library")

    decoded = bytes(decode_native_byte(value) for value in native_blob[marker - 48 : marker])
    return decoded[:16], decoded[16:32], decoded[32:48]


def build_state(key1: bytes, key2: bytes, key3: bytes) -> DerivedState:
    mixed = xor_bytes(key1, key3)
    stage_a = mixed[3:] + mixed[:3]

    expanded = [0] * 16
    for row in range(4):
        matrix_row = key2[row * 4 : (row + 1) * 4]
        for column, coeff in enumerate(matrix_row):
            vector = stage_a[column * 4 : (column + 1) * 4]
            for lane, value in enumerate(vector):
                expanded[row * 4 + lane] ^= gf_product(coeff, value)
    stage_b = bytes(
        cell
        for row in ROW_PERMUTATION
        for cell in expanded[row * 4 : (row + 1) * 4]
    )

    stage_c = bytearray(SBOX[x ^ y ^ z] for x, y, z in zip(stage_a, stage_b, key3))
    for index in range(1, len(stage_c)):
        stage_c[index] ^= stage_c[index - 1]

    return DerivedState(stage_a=stage_a, stage_b=stage_b, stage_c=bytes(stage_c))


def recover_part1(cipher1: bytes, state: DerivedState) -> bytes:
    key_words = struct.unpack(">4I", state.stage_a)
    rolling = bytes(state.stage_c[(index + 1) % 16] for index in range(8))
    return xor_bytes(tea_decrypt_block(cipher1, key_words), rolling)


def recover_part2(cipher2: bytes, state: DerivedState) -> bytes:
    inverse = invert_matrix_4x4(state.stage_b)
    plain = bytearray()
    for block in iter_chunks(cipher2, 4):
        for row in inverse:
            plain.append(gf_dot(row, block))
    return bytes(plain)


def recover_part3(cipher3: bytes, state: DerivedState) -> bytes:
    current = int.from_bytes(state.stage_c[:8], "big")
    stream = bytearray()
    for _ in range(len(cipher3)):
        current = (LCG_MULT * current + LCG_INC) & MASK64
        stream.append((current >> 24) & 0xFF)
    return xor_bytes(cipher3, bytes(stream))


def validate_solution(payload: ApkPayload, state: DerivedState, part1: bytes, part2: bytes, part3: bytes) -> None:
    if (len(part1), len(part2), len(part3)) != (8, 12, 10):
        raise ValueError("unexpected part length")

    left_matrix = payload.puzzle[:16]
    expected_tail = payload.puzzle[16:]
    for row_index, row in enumerate(iter_chunks(left_matrix, 4)):
        if gf_dot(row, part1[:4]) != expected_tail[row_index]:
            raise ValueError("puzzle check failed")

    key_words = struct.unpack(">4I", state.stage_a)
    rolling = bytes(state.stage_c[(index + 1) % 16] for index in range(8))
    if tea_encrypt_block(xor_bytes(part1, rolling), key_words) != payload.cipher1:
        raise ValueError("cipher1 check failed")

    transformed = bytearray()
    for block in iter_chunks(part2, 4):
        for row_index in range(4):
            coeffs = state.stage_b[row_index * 4 : (row_index + 1) * 4]
            transformed.append(gf_dot(coeffs, block))
    if bytes(transformed) != payload.cipher2:
        raise ValueError("cipher2 check failed")

    parity = 0
    for byte in part1:
        parity ^= byte
    parity = (((parity << 3) & 0xFF) | (parity >> 5)) & 0xFF
    if part2[0] != ALPHABET[parity % len(ALPHABET)]:
        raise ValueError("part2 checksum check failed")

    xor_total = 0
    sum_total = 0
    for byte in part1 + part2:
        xor_total ^= byte
        sum_total = (sum_total + byte) & 0xFF
    trailer = bytes((ALPHABET[xor_total % len(ALPHABET)], ALPHABET[sum_total % len(ALPHABET)]))
    if part3[-2:] != trailer:
        raise ValueError("part3 checksum check failed")


def solve(apk_path: Path) -> bytes:
    payload = load_payload(apk_path)
    key1, key2, key3 = locate_native_constants(payload.native_blob)
    state = build_state(key1, key2, key3)

    part1 = recover_part1(payload.cipher1, state)
    part2 = recover_part2(payload.cipher2, state)
    part3 = recover_part3(payload.cipher3, state)

    validate_solution(payload, state, part1, part2, part3)
    return b"ISCC{" + part1 + part2 + part3 + b"}"


def main() -> None:
    parser = argparse.ArgumentParser(description="Recover the flag from attachment-23.apk.")
    parser.add_argument("apk", nargs="?", default="attachment-23.apk", help="APK path to solve")
    options = parser.parse_args()
    sys.stdout.write(solve(Path(options.apk)).decode("ascii") + "\n")


if __name__ == "__main__":
    main()

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