Mobile-密码验证
ISCC2026 WriteUp 提交模板
Mobile-密码验证
在混乱交织的关系中寻找正确的方向
解题思路
1.jadx打开apk,搜索ISCC定位逻辑


简化下就是:
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(...)

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

现在需要去看so层了,IDA打开so文件,直接搜函数名定位。
分析内容如下:






现在我们梳理一下:
- 入口先把 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

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()

浙公网安备 33010602011771号