MOBILE-消失的喵星密使
ISCC2026 WriteUp 提交模板
MOBILE-消失的喵星密使
解题思路
1.看一下程序

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

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

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

这些字节异或 0x42 后得到 ISCC{,并且要求输入以 } 结尾,所以 flag 格式确定为 ISCC{flag_body}。
2.根据协议文件和 MD5 恢复点击序列
回忆一下初始化:

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



因为由 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 字节是MEOWMEOW后面的 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}

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

浙公网安备 33010602011771号