MOBILE-深潮协定

ISCC2026 WriteUp 提交模板

MOBILE-深潮协定

解题思路

1.初步分析
MainActivity函数

image.png

继续看

image.png

image.png

放到so层了。
先看CryptoEngine.transform(context, str.substring(5, str.length() - 1)

image.png

image.png

Telemetry.load() 会把 assets/telemetry.bin 读出来,然后按换行切成 4 段:

0b04110916060d
1337c0de42a5197f
Qh2s7Ca9VxN4fKpB8J1mL+zoF0ewAyOu3DrRYSgPItU6McdkXl/WTnGEZ5ivbHjq
T8GHtJzYDRX7siXzWKLJCGlrpi1sVwnvMhZCPj3jwDWYU4Y=

对应关系如下:

  • 第 1 行:shifts,十六进制转字节,长度 7
  • 第 2 行:mask,十六进制转字节,长度 8
  • 第 3 行:自定义 Base64 字母表,长度 64
  • 第 4 行:目标串

Cyberengine做的就是:

  1. 对可打印 ASCII 做一个 mod 95 的循环位移
  2. 再和 mask 做循环异或
  3. 再加上前一个输出字节 prev
  4. 再额外加上 3 * i

2.分析so文件

image.png

这段是在检查切分后的字符串数量是否达到 4。也就是说,native 期望:

  • 至少有 4 个字段
  • 后面会从其中取出两项做进一步处理

image.png

可以看到:

  • 初始状态是 0x5D
  • 每轮都在更新 8-bit 状态
  • 异或密钥不是固定值,而是 state + 7*i

image.png

  1. 用两个字符串参数调用一个解码函数
  2. 把解码结果拿到 r15
  3. 先比较长度
  4. 再做逐字节比较

结合前面的取值位置,可以确认这两个字符串就是:

  • 第 3 行:自定义 Base64 字母表
  • 第 4 行:目标串

所以 native 的职责是:

  • 先把 Java 输出改写一遍
  • 再把 telemetry.bin 里的目标串按自定义 Base64 解码
  • 最后比较两边是否一致

4.还原思路

到这里,整个校验链就已经完整了:

flag body
  -> CryptoEngine.transform()
  -> native keystream xor
  -> compare(decoded_target)

所以我们需要:

  1. 先把目标串按“自定义字母表 Base64”解码
  2. 再逆掉 native 的滚动异或
  3. 最后逆掉 Java 的状态变换

image.png

ISCC{abyssal_vows_bind_the_last_protocol}

Exp

from __future__ import annotations

import base64
import zipfile
from pathlib import Path


APK_PATH = Path(__file__).with_name("app-release.apk")
STANDARD_B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


def load_telemetry(apk_path: Path) -> tuple[bytes, bytes, str, str]:
    with zipfile.ZipFile(apk_path) as zf:
        shifts_hex, mask_hex, alphabet, target = zf.read("assets/telemetry.bin").decode().splitlines()
    return bytes.fromhex(shifts_hex), bytes.fromhex(mask_hex), alphabet, target


def decode_custom_base64(payload: str, alphabet: str) -> bytearray:
    translated = payload.translate(str.maketrans(alphabet, STANDARD_B64))
    return bytearray(base64.b64decode(translated))


def undo_native_keystream(data: bytearray) -> bytearray:
    state = 0x5D
    for index in range(len(data)):
        feedback = ((state >> 2) ^ (state >> 3) ^ (state >> 5) ^ state) & 1
        state = ((state >> 1) | (feedback << 7)) & 0xFF
        data[index] ^= (state + 7 * index) & 0xFF
    return data


def undo_java_transform(transformed: bytes, shifts: bytes, mask: bytes) -> str:
    previous = 0
    recovered = bytearray()
    for index, current in enumerate(transformed):
        mixed = (current - previous - 3 * index) & 0xFF
        rotated = mixed ^ mask[index % len(mask)]
        if not 32 <= rotated <= 126:
            raise ValueError(f"unexpected non-printable intermediate byte at index {index}: 0x{rotated:02x}")
        plain = ((rotated - 32 - shifts[index % len(shifts)]) % 95) + 32
        recovered.append(plain)
        previous = current
    return recovered.decode("ascii")


def main() -> None:
    shifts, mask, alphabet, target = load_telemetry(APK_PATH)
    decoded_target = decode_custom_base64(target, alphabet)
    java_output = undo_native_keystream(decoded_target)
    body = undo_java_transform(java_output, shifts, mask)
    print(f"[+] APK: {APK_PATH}")
    print(f"[+] shifts length: {len(shifts)}")
    print(f"[+] mask length: {len(mask)}")
    print(f"[+] decoded target bytes: {len(decoded_target)}")
    print(f"[+] flag body: {body}")
    print(f"[+] flag: ISCC{{{body}}}")


if __name__ == "__main__":
    main()
posted @ 2026-05-19 16:29  MillionMind  阅读(14)  评论(0)    收藏  举报