REVERSE- QuorumGlyph

ISCC2026 WriteUp 提交模板

REVERSE- QuorumGlyph

解题思路

1.从main函数开始

image.png

image.png

输入长度必须是 42
2. 前缀必须是 ISCC{QRM_
3. 第 14 位必须是 _
4. 最后一位必须是 }
5. 所有字符都必须落在可打印 ASCII 范围

image.png

继续往后跟,会发现前两段大校验在进入核心逻辑前都做了同一件事:

s_1[13] = 0;
*(_DWORD *)&s_1[9] = 0;

也就是把 s[9..13] 这 5 个字符整段清零。

image.png

image.png

image.png

所以这里能得到一个非常重要的结论:

  • witness-1 和 witness-2 从设计上就不打算告诉我们中间 5 位
  • 它们只负责把 flag 外壳钉死
  • 中间 5 位一定是由最后那一段单独恢复

在 main 的后半段,有一段 37 轮的 u32 变换,最后会检查两类结果:

  1. 16 个 u32 经过混洗后的整块内容,是否等于 .bss 中的一块 64 字节目标
  2. 四个聚合指标和一个 64 位哈希,是否分别等于
0x110
0xEE
0xFC
0x616E8714
0x4A9B46E51D6EEEB3

但如果只做静态查看,会发现 .bss 那块目标在文件里不是明文,而是启动构造器 sub_401080 运行后才写进去。

所以顺着数据流走,先把构造器抄出来更稳。构造器做完后,关键运行时目标会变成:

qword_405068 = 0x4A9B46E51D6EEEB3
qword_405080 = 0xE04CF5339F776142
xmmword_405070 = [0x110, 0xEE, 0xFC, 0x616E8714]
unk_4050E0    = 64-byte runtime block

接下来把 main 里第二段的 64 字节输入构造过程和 37 轮 u32 变换翻成 Python,拿“中间 5 位清零”的外壳去跑,就能稳定命中这些运行时常量。

也就是说,前两段 witness 共同锁死的其实是这一层:

ISCC{QRM_....._3_w1tness_no_single_path!!}

最后一段逻辑单独读取 s[9:14] 这 5 个字符,并要求它们都来自下面这个 32 字符表:

ABCDEFGHJKLMNPQRSTUVWXYZ23456789

程序会把这 5 个字符当作 base32 数字,逐个打包成一个 24-bit 整数 x:

n0xFFFFFF = (idx(ch) | (32 * n0xFFFFFF))

随后程序并不是直接把 x 送进哈希,而是先做一步模 2^24 的可逆变换:

y = ((x - 0xB3EF33) * inv(0xD1E995)) mod 2^24

这里 inv(0xD1E995) 是 0xD1E995 在 2^32 下的逆元低 24 位。
从代码里的牛顿迭代可以算出:

inv(0xD1E995) mod 2^24 = 0x9B19BD

然后程序检查:

sub_401CA0(y) == 0xE04CF5339F776142

这一段的搜索空间只有 2^24,完全可以离线复现。
我这次复现时直接把 sub_401CA0 精确翻成 Python,最后唯一命中的值是:

y = 0x0C0DE5

再代回去恢复 x:

x = ((0x0C0DE5 * 0xD1E995) + 0xB3EF33) mod 2^24
  = 0x52727C

把 0x52727C 按程序同样的 base32 打包顺序还原回 5 个字符,得到:

FE6V6

于是完整 flag 就出来了:

ISCC{QRM_FE6V6_3_w1tness_no_single_path!!}

image.png

Exp

from __future__ import annotations

import sys
from pathlib import Path


ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
MASK24 = (1 << 24) - 1


STATIC_WITNESS2 = bytes(
    [
        0x25,
        0x4F,
        0x69,
        0x5E,
        0x54,
        0xD4,
        0xA3,
        0x30,
        0x6C,
        0x91,
        0x0E,
        0x60,
        0xDA,
        0x36,
        0x87,
        0x76,
        0x6A,
        0x9C,
        0x3B,
        0xBB,
        0x28,
        0x9F,
        0xB9,
        0x6D,
        0x11,
        0xF4,
        0x92,
        0x4C,
        0x8F,
        0xF4,
        0x9A,
        0x66,
        0x03,
        0x26,
        0x30,
        0xC4,
        0x56,
        0x1E,
        0xD5,
        0x4C,
        0xA5,
        0xFD,
        0x01,
        0x73,
        0x46,
        0x20,
        0x01,
        0x7E,
        0x28,
        0x99,
        0xC2,
        0x64,
        0xA5,
        0xE5,
        0x2D,
        0x02,
        0xAB,
        0x63,
        0x28,
        0x3F,
        0xEE,
        0x66,
        0x8D,
        0xC1,
    ]
)

WITNESS2_TARGET_WORDS = [0x110, 0xEE, 0xFC, 0x616E8714]
WITNESS2_TARGET_HASH = 0x4A9B46E51D6EEEB3
WITNESS3_TARGET = 0xE04CF5339F776142


def u32(x: int) -> int:
    return x & 0xFFFFFFFF


def u64(x: int) -> int:
    return x & 0xFFFFFFFFFFFFFFFF


def rol32(x: int, r: int) -> int:
    x &= 0xFFFFFFFF
    r &= 31
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF


def mix64(x: int) -> int:
    x &= 0xFFFFFFFFFFFFFFFF
    y = (0xBF58476D1CE4E5B9 * (x ^ (x >> 30))) & 0xFFFFFFFFFFFFFFFF
    z = (0x94D049BB133111EB * ((y >> 27) ^ y)) & 0xFFFFFFFFFFFFFFFF
    return ((z >> 31) ^ z) & 0xFFFFFFFFFFFFFFFF


def rol64(x: int, r: int) -> int:
    x &= 0xFFFFFFFFFFFFFFFF
    r &= 63
    return ((x << r) | (x >> ((64 - r) & 63))) & 0xFFFFFFFFFFFFFFFF


def hash_witness3(seed24: int) -> int:
    seed24 &= 0xFFFFFFFF
    v2 = seed24 ^ 0xB198FA1458A7836C
    v3 = 0
    v4 = ((2654435761 * seed24) ^ 0xC5C9AFD93FF67D70) & 0xFFFFFFFFFFFFFFFF
    table = [0] * 1024
    for i in range(1024):
        tmp = (v2 + i + 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
        v2 = mix64(tmp)
        v7 = v3 ^ v2
        v3 = (v3 + 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
        v4 = mix64(v7 ^ v4)
        table[i] = v2 ^ rol64(v4, i)

    v9 = 0
    v10 = 0
    n1536 = 0
    while n1536 != 1536:
        v12 = table[((n1536 & 0xFFFF) ^ ((v4 ^ v2) & 0xFFFF)) & 0x3FF]
        v13 = table[((v12 & 0xFFFF) ^ ((v10 ^ (v2 >> 17)) & 0xFFFF)) & 0x3FF]
        v2 = mix64((v12 + v2 + v9) & 0xFFFFFFFFFFFFFFFF)
        rot = (v2 >> 59) & 63
        v4 ^= rol64((v13 + v2) & 0xFFFFFFFFFFFFFFFF, rot)
        v15 = (v12 + n1536 + v13) & 0xFFFFFFFFFFFFFFFF
        v16 = ((v4 & 0xFFFF) ^ (((n1536 & 0xFFFF) << 7) ^ v12) & 0xFFFF) & 0x3FF
        v17 = mix64(v15)
        n1536 += 1
        v10 = (v10 + 131) & 0xFFFFFFFFFFFFFFFF
        v9 = (v9 - 0x2917014799A6026D) & 0xFFFFFFFFFFFFFFFF
        table[v16] = (v2 ^ v4 ^ v17) & 0xFFFFFFFFFFFFFFFF

    v18 = table[((v2 & 0xFFFF) + (v4 & 0xFFFF)) & 0x3FF] ^ v2 ^ v4 ^ 0x51474C5950484153
    return mix64(v18)


def build_runtime_witness2_buf() -> bytes:
    out = bytearray(64)
    state = 0
    for off in range(0, 64, 4):
        prev = state
        state = u64(state - 0x61C8864680B583EB)
        word = int.from_bytes(STATIC_WITNESS2[off : off + 4], "little")
        word ^= mix64(prev ^ 0xA781EC1645A68B63) & 0xFFFFFFFF
        out[off : off + 4] = word.to_bytes(4, "little")
    return bytes(out)


def witness2_shell(flag_bytes: bytes) -> bytes:
    shell = bytearray(flag_bytes)
    shell[9:14] = b"\x00" * 5
    return bytes(shell)


def derive_witness2_input(shell: bytes) -> list[int]:
    buf = bytearray(64)
    buf[:42] = shell

    v27 = 0x51554F52554D474C
    acc = 0
    for b in shell:
        v27 = mix64(v27 ^ (acc + b))
        acc += 40503

    for i in range(8):
        buf[42 + i] = (v27 >> (8 * i)) & 0xFF

    v38 = v27 ^ 0x123456789ABCDEF0
    acc2 = 0
    idx = 0
    for i in range(14):
        v41 = acc2 + shell[idx % 0x2A]
        acc2 += 65537
        v38 = mix64(v38 + v41)
        buf[50 + i] = v38 & 0xFF
        idx += 7

    return [int.from_bytes(buf[i : i + 4], "little") for i in range(0, 64, 4)]


def run_witness2(shell: bytes) -> tuple[bool, list[int], tuple[int, int, int, int], int]:
    words = derive_witness2_input(shell)
    target_words = [
        int.from_bytes(build_runtime_witness2_buf()[i : i + 4], "little")
        for i in range(0, 64, 4)
    ]

    n37 = 0
    n183 = 1838395311
    v50 = 1
    n25 = 25
    for _ in range(37):
        n25_1 = n37
        n37 = u32(n37 + 1)
        n183_1 = n183
        n37_1 = n37
        for i in range(16):
            v78 = n183_1 ^ words[i]
            n183_1 = u32(n183_1 + 2135587861)
            words[i] = rol32(v78, n37_1)
            n37_1 += 3

        v80 = words[0]
        v81 = n25_1 + 5
        for i in range(1, 16):
            v80 = words[i] ^ rol32(v80, v81)
            words[i] = v80
            v81 += 5

        v83 = words[15]
        v84 = v50 + 20
        for i in range(14, -1, -1):
            v83 = words[i] ^ rol32(v83, v84)
            words[i] = v83
            v84 -= 1

        cur = n25_1
        n5_7 = v50
        while cur != n25:
            a = cur & 0xF
            b = n5_7 & 0xF
            if a != b:
                words[a], words[b] = words[b], words[a]
            cur += 5
            n5_7 = u32(n5_7 + 7)

        n25 = cur + 1
        v50 = u32(v50 + 3)
        n183 = u32(n183 - 1640531527)

    v128 = words[0].bit_count()
    v130 = (words[0] ^ rol32(words[0], 1)).bit_count()
    v129 = 0
    v127 = 0
    v95 = words[0]
    v102 = words[0]
    v94 = 0
    v104 = 0
    n5 = 5
    for i in range(16):
        v111 = rol32(v95, n5)
        n5_4 = n5 + 3
        v113 = v94 ^ v102
        v127 ^= v111
        v94 += 40503
        v104 = mix64(v104 ^ v113)
        if i == 15:
            break
        nxt = words[i + 1]
        v128 += nxt.bit_count()
        v130 += (nxt ^ rol32(nxt, 1)).bit_count()
        v129 += (words[i] ^ nxt).bit_count()
        v95 = nxt
        v102 = nxt
        n5 = n5_4

    ok = (
        words == target_words
        and (v128, v130, v129, v127) == tuple(WITNESS2_TARGET_WORDS)
        and v104 == WITNESS2_TARGET_HASH
    )
    return ok, words, (v128, v130, v129, v127), v104


def solve_middle() -> tuple[str, int, int]:
    # This value is the unique hit obtained during reproduction by brute-forcing
    # the 24-bit witness-3 domain and checking sub_401CA0 against 0xE04CF5339F776142.
    y = 0x0C0DE5
    packed = ((y * 0xD1E995) + 0xB3EF33) & MASK24
    tmp = packed
    chars = []
    for _ in range(5):
        chars.append(ALPHABET[tmp & 0x1F])
        tmp >>= 5
    middle = "".join(reversed(chars))
    if hash_witness3(y) != WITNESS3_TARGET:
        raise RuntimeError("witness-3 verification failed")
    return middle, packed, y


def validate_full_flag(flag: str) -> bool:
    data = flag.encode()
    if len(data) != 42:
        return False
    if not data.startswith(b"ISCC{QRM_"):
        return False
    if data[14] != ord("_") or data[41] != ord("}"):
        return False
    if any(not (33 <= b <= 126) for b in data):
        return False
    shell = witness2_shell(data)
    ok2, _, _, _ = run_witness2(shell)
    if not ok2:
        return False

    packed = 0
    for ch in flag[9:14]:
        idx = ALPHABET.find(ch)
        if idx < 0:
            return False
        packed = (packed << 5) | idx
    if packed > MASK24:
        return False

    inv = 1
    for _ in range(5):
        inv = (inv * (2 - 0xD1E995 * inv)) & 0xFFFFFFFF
    y = ((packed - 0xB3EF33) * (inv & MASK24)) & MASK24
    return hash_witness3(y) == WITNESS3_TARGET


def main() -> int:
    _ = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("./QuorumGlyph")

    runtime = build_runtime_witness2_buf()
    shell = "ISCC{QRM_....._3_w1tness_no_single_path!!}"
    middle, packed, y = solve_middle()
    flag = shell.replace(".....", middle)

    ok2, _, witness2_metrics, witness2_hash = run_witness2(witness2_shell(flag.encode()))
    accepted = validate_full_flag(flag)

    print(f"[*] witness-2 hash: 0x{witness2_hash:016x}")
    print(f"[*] witness-3 target: 0x{WITNESS3_TARGET:016x}")
    print(f"[*] witness-2 metrics: {[hex(x) for x in witness2_metrics]}")
    print(f"[*] shell: {shell}")
    print(f"[*] gap solved: {middle}")
    print(f"[*] packed x = 0x{packed:06x}")
    print(f"[*] packed y = 0x{y:06x}")
    print(f"[*] runtime witness-2 bytes prefix: {runtime[:16].hex()}")
    print(f"[*] witness-2 match: {ok2}")
    print(f"[*] checker says: {'Yes???' if accepted else 'No???'}")
    print(flag)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

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