base_family:三层编码套娃与 Bitcoin Base58

题目:base_family.exe(17 KB,x64,MinGW 编译,无壳) 题面提示:“你知道茴字有多少种写法吗”

这个提示很直白——考点就是编码家族(base family),而且不止一种。

快速定性

PE 头 Machine = 0x8664                    → x64
字符串区发现 "GCC ... Mingw-w64 ... 15.1.0" → MinGW 编译的 C
用 DIE 查看                                 → 无壳

跳过加壳分析,直接进逻辑。

第一层:自定义字母表 Base64

抠字符串时抓到两个关键数据:

0x140005078  0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-
             → 64 字符自定义字母表
0x140005100  L5D3L4OjIdj8OMXXNtamTLzXSZDVT79fOsipP5zYULzYDZHz
             → 48 字符密文,正好是 36 字节的 base64 长度

注意字母表是 0-9 开头、_- 收尾,和标准 Base64(A-Za-z0-9+/)完全不同。所以 base64.b64decode 会直接报错,只能手写解码器:

ALPHA = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-"

def b64_custom_decode(s):
    bits = nbits = 0
    out = bytearray()
    for ch in s:
        bits = (bits << 6) | ALPHA.index(ch)
        nbits += 6
        if nbits >= 8:
            nbits -= 8
            out.append((bits >> nbits) & 0xFF)
    return bytes(out)

手写 6-bit 累积是处理自定义字母表的通用套路:不要试图去 translate 成标准表再喂给库,直接按 6 位一组自己拼字节,永远不会错。

一个嘲讽你的假 flag

解出来是:

TSCTF-J{Haha_y0u_ar3_trick3d_by_b64}

假的。题目在这里埋了个嘲讽——如果你只解一层就收手,就上钩了。

定位 main 时的坑:函数指针间接调用

反汇编里看到这种形式:

mov  rax, qword ptr [rip + 0x2af0]   ; .data:0x140004000 = 0x140001674
call rax

必须去数据段确认被调用的到底是谁,否则会跟丢逻辑。这是写 C 代码时用函数指针表的典型编译产物,逆向时要多跳一步。

第二层:藏在 .rdata 里的 XOR blob

继续挖,在 .rdata 里发现一段 44 字节的不明数据:

文件偏移 0x28C0 起,44 字节
注意:.rdata 段 VA 0x5000 → 文件偏移 0x2800
所以 va 0x50c0 → file 0x28c0

RVA ↔ 文件偏移的换算是 PE 逆向的基本功,这里必须自己算一遍,直接按 VA 去文件里找会找错位置。

数据是 XOR 0x26 加密的:

data = exe_bytes[0x28C0 : 0x28C0 + 0x2C]
blob = bytes(b ^ 0x26 for b in data)
# → 6gAtKazuEXraUFwyjrMbJbDSw4xUqwBa3T8uLi41uoQG

得到一串 44 字符的字符串。看到这个字符集——没有 0、O、I、l——基本可以断定是 Bitcoin Base58。

第三层:Bitcoin Base58

Base58 的字母表:

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

去掉了容易混淆的 0(数字零)、O(大写 o)、I(大写 i)、l(小写 L)。

反汇编特征:不用取模指令,而是编译成乘以魔数 0x28f5c28f5c28f5c3 后移位(编译器把”除以 58”优化成乘法)。看到这个魔数就知道是 Base58。

解码:

B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

def b58_decode(s):
    n = 0
    for ch in s:
        n = n * 58 + B58.index(ch)
    # 前导 '1' 代表前导零字节
    pad = 0
    for ch in s:
        if ch == '1':
            pad += 1
        else:
            break
    body = n.to_bytes((n.bit_length() + 7) // 8, 'big') if n else b''
    return b'\x00' * pad + body

前导 1 输出零字节这一条最容易漏——它是 Base58 的固定规则,不是巧合。

结果

→ TSCTF-J{b4se58_1s_7he_r34l_de4l}

真 flag 说得很明白:Base58 才是真的。

复盘

这题的价值在于把”编码家族”这个考点讲透了:

层编码识别特征
1自定义 Base6464 字符表,但字符集不是 A-Za-z0-9+/
2XOR 0x26藏在 .rdata,需要 RVA 换算
3Bitcoin Base58无 0OIl、除以 58 编译成魔数乘法

三层是同一个主题的不同变体,跟题面”茴字有多少种写法”完全呼应。

踩坑

问题原因解决
b64decode 报错字母表是自定义的手写 6-bit 累积解码
找不到 blob按 VA 直接读文件RVA → 文件偏移换算(.rdata VA 0x5000 = 偏移 0x2800)
跟丢主逻辑.data 里是函数指针多跳一步查 .data 存的实际地址
Base58 解出少字节漏了前导 1 的零字节显式统计前导 1 补 \x00