MOBILE-灰签名回廊

ISCC2026 WriteUp 提交模板

MOBILE-灰签名回廊

凌晨两点,内网审计系统捕获到一台“已下线终端”的异常心跳。
它像一座被遗弃的保险库,所有入口都还在,却没有任何一把完整钥匙。
你能拿到的只有零散线索:一段来自内容接口的回声、一条只在握手后出现的广播、一扇需要特定深链唤醒的隐门,以及一层看似多余却总在最后收口的校验。
这台终端最狡猾的地方,不在于把答案藏得深,而在于它让“正确路径”看起来像“错误捷径”。
有人试图跳步,有人试图硬猜,有人只盯着终值;
最后都卡在同一个问题上:
你看到的是结果,还是结果背后的因果。

解题思路

1.追踪字符串
通过搜索ISCC定位到,package com.example.gnd.security;

image.png

image.png

数组和0x11异或后就是ISCC{前缀

image.png

这里是一段Frida检测

image.png

这里是对flag做最终hash验证。

image.png

四段flag验证。

image.png

fragment获取

image.png

所以flag,java层检验流程,把flag的body部分切成 7 + 5 + 5 + 7 四段,最后验证hash。

2.确认fragement
这个是通过分析AndroidManifest.xml文件,发现里面有三个隐藏的组件

image.png

image.png

SecretContentProvider — fragment2

// content://com.example.gnd.secret.provider/keys/fragment
int[] {100, 71, 111, 100} XOR 23 = {'s', 'P', 'x', 's'} → "sPxs"
f00.t("sPxs")       // 自定义Base64编码
→ SharedPreferences("key_cache")["fragment2"]

无需认证,直接 query 即可拿到 fragment2。

image.png

image.png

token == "ISCC2026" SP receiver_auth 写入 authenticated=true + 时间戳
需 5 分钟内认证过 且 part == 3 {11,16,12,110} → QKT7

image.png

byte[] {73,83,67,67,50,48,50,100,117} = "ISCC202du"
取 bArr[7]='d', bArr[8]='u' → "du" → f00.t("du")
→ SharedPreferences("key_cache")["fragment4"]

3.接着回java层的四段flag校验,开始分析native检测。

image.png

符号没混淆,直接分析

image.png

第一步分,开始是个小反调试,然后是个根据获取到的flag_length走不同分支,我们已经确认了这里length一定是7,看7的逻辑即可。

image.png

image.png

其实就是:

// 逐字节: output[i] = (input[i] + 1) ^ 7
for (int i = 0; i < 7; i++) {
    dest[i] = (dest[i] + 1) ^ 7;
}

image.png

输出 7 字节回到 Java 后与 a(certDigest) 对比,验证 flag 第1段。先用 apksigner 取签名:

133415c3f8aad739be8d0df905481208aa7448da7254a474028eeeddfc2849af

image.png

然后就是这段比较逆回去,就能直接得到:

part1 = JdoAXrf

image.png

逆向求解 part2(已知 key = fragment2 解码 = "sPxs"),
还原出:

part2 = _Y7Gk

image.png

这是第三段校验:
求解后:part3 = _MLKz

3.收尾

image.png

Java_com_example_gnd_security_SecureValidator_nativeComputeChainToken校验函数
会把:

  1. 第一段校验后的返回字节
  2. fragment2 = sPxs
  3. fragment3 = QKT7
    串起来算出一个 chain token

Java_com_example_gnd_security_SecureValidator_nativeVerifyPart4函数:
它用前面几段算出来的 chain token + 隐藏组件给的 fragment4,拼出一个 14 字节 RC4 key,然后只校验 part4 的前 4 个字节,最后 3 个字节完全不看。

image.png

然后把最后两个字节替换成:

fragment4[0:2] ^ "du"

同时把 chainToken 低 32 位拆成 4 个字节,再分别异或 "S" "e" "c" "u":

key_prefix = low32(chainToken) ^ "Secu"

所以最终 KSA 用的 key 实际是:

(chain_low32 ^ "Secu") || "sPxsQKT7" || (fragment4 ^ "du")

总长度正好 14 字节。

image.png

  1. 0x5e206 ~ 0x5e33d 初始化并打乱 256 字节状态表 这段就是标准 RC4 KSA:

    • 先构造 S = [0,1,2,...,255]
    • 然后做:
      j = (j + S[i] + key[i % 14]) & 0xff; swap(S[i], S[j]);

    反编译里这个很绕的表达式:*(&v34 + v13 + -14 * (v13 / 0xE))本质就是 key[i % 14]。

  2. 0x5e33f ~ 0x5e413 跑 4 轮 PRGA
    这里只生成 4 个 keystream 字节,并分别异或 part4[0..3]:
    b0 = part4[0] ^ ks0 b1 = part4[1] ^ ks1 b2 = part4[2] ^ ks2 b3 = part4[3] ^ ks3
    注意:part4[4]、part4[5]、part4[6] 根本没被读。

  3. 0x5e41b ~ 0x5e447 做最终比较
    4 个字节不会直接逐字节比,而是压成一个掩码后比较:
    (((((b0 << 6) ^ (b1 << 3) ^ b2) << 12) ^ (b3 << 9)) & 0x3FF0000) == 0x2DC0000

可以把把第四段前缀压到 _UqW...
最后通过已知hash爆破最后三位,定位到pfF

image.png

ISCC{JdoAXrf_Y7Gk_MLKz_UqWpfF}

Exp

from __future__ import annotations

import base64
import hashlib
import itertools
import string
from dataclasses import dataclass

CUSTOM_B64_ALPHABET = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/"
STANDARD_B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
TARGET_FLAG_SHA256 = "3a52a4f84f9100272987f903da1c54ff9161813f122bac64b143ab9bb8d6a2ac"

PART1_VECTOR = bytes([76, 98, 119, 69, 94, 116, 96])
PART2_MAGIC = bytes.fromhex("18094f34")
PART3_MAGIC = bytes.fromhex("9fa989b4")

PART4_PREFIX = "_UqW"
BRUTEFORCE_ALPHABET = "_" + string.ascii_letters + string.digits


@dataclass(frozen=True)
class HiddenFragments:
    fragment2: bytes
    fragment3: bytes
    fragment4: bytes


def decode_custom_b64(token: str) -> bytes:
    table = str.maketrans(CUSTOM_B64_ALPHABET, STANDARD_B64_ALPHABET)
    normalized = token.translate(table)
    normalized += "=" * (-len(normalized) % 4)
    return base64.b64decode(normalized)


def collect_hidden_fragments() -> HiddenFragments:
    fragment2 = bytes(value ^ 0x17 for value in (100, 71, 111, 100))
    fragment3 = bytes((index ^ value) ^ 0x5A for index, value in enumerate((11, 16, 12, 110)))
    fragment4 = b"du"
    return HiddenFragments(fragment2, fragment3, fragment4)


def recover_part1() -> str:
    recovered = bytes(((value ^ 0x07) - 1) & 0xFF for value in PART1_VECTOR)
    return recovered.decode("latin1")


def recover_part2(fragment2: bytes) -> str:
    recovered = bytes(
        [
            0x2C ^ fragment2[0],
            PART2_MAGIC[1] ^ fragment2[1],
            PART2_MAGIC[2] ^ fragment2[2],
            PART2_MAGIC[3] ^ fragment2[3],
            PART2_MAGIC[0] ^ fragment2[0],
        ]
    )
    return recovered.decode("latin1")


def recover_part3(fragment3: bytes) -> str:
    recovered = bytes(
        [
            ((0x97 - fragment3[0]) & 0xFF) ^ 0x19,
            ((PART3_MAGIC[0] - fragment3[1]) & 0xFF) ^ 0x19,
            ((PART3_MAGIC[1] - fragment3[2]) & 0xFF) ^ 0x19,
            ((PART3_MAGIC[2] - fragment3[3]) & 0xFF) ^ 0x19,
            ((PART3_MAGIC[3] - fragment3[0]) & 0xFF) ^ 0x19,
        ]
    )
    return recovered.decode("latin1")


def recover_part4(body_prefix: str) -> str:
    for tail in itertools.product(BRUTEFORCE_ALPHABET, repeat=3):
        candidate = PART4_PREFIX + "".join(tail)
        flag = f"ISCC{{{body_prefix}{candidate}}}"
        if hashlib.sha256(flag.encode()).hexdigest() == TARGET_FLAG_SHA256:
            return candidate
    raise RuntimeError("failed to recover part4")


def validate_fragments(fragments: HiddenFragments) -> None:
    assert decode_custom_b64("x8Y5xd") == fragments.fragment2
    assert decode_custom_b64("FFgFMd") == fragments.fragment3
    assert decode_custom_b64("ASF") == fragments.fragment4


def solve() -> str:
    fragments = collect_hidden_fragments()
    validate_fragments(fragments)

    part1 = recover_part1()
    part2 = recover_part2(fragments.fragment2)
    part3 = recover_part3(fragments.fragment3)

    body_prefix = part1 + part2 + part3
    part4 = recover_part4(body_prefix)

    return f"ISCC{{{body_prefix}{part4}}}"


def main() -> None:
    flag = solve()
    print(flag)
    print(hashlib.sha256(flag.encode()).hexdigest())


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