NCTF-2026-Reverse详解
字数 4168
更新时间 2026-04-12 12:16:55

NCTF-2026 Reverse 题目详解教学文档

概述

本文档对 NCTF-2026 逆向工程类题目进行详细解析,包含多道具有代表性的题目分析,涵盖Android逆向、Godot游戏保护、Electron应用、虚拟机分析和二进制协议漏洞等多个技术方向。

题目一:Hook My Secret

1. 题目信息

  • 类型:Android Reverse / Crypto
  • 样本:app-release.apk
  • 包名:com.nctf.hookmysecret
  • 活动链:PatternActivity → Stage2Activity → Stage3Activity → SuccessActivity

2. 三段校验链分析

Stage1:手势密码校验

  • 位置com.nctf.hookmysecret.ui.PatternActivity
  • 校验逻辑
    1. 手势点序列拼接为逗号分隔字符串(如"0,1,2,4,8")
    2. 计算SHA-256哈希值
    3. 与常量比较:4a6bc34076c8eef0f9eac59ad30d99bb4f56ecea4b0bfab92540fb655ac680f3
  • 破解方法:枚举3×3网格(0-8)的所有排列组合
  • 结果:Pattern = 0,1,2,4,8

Stage2:JNI Native算法

  • 位置libhookmysecret.so中的Java_com_nctf_hookmysecret_nativebridge_NativeBridge_encryptStage2
  • 算法逻辑
    初始状态: d = 0x51
    对第i个输入字节x:
      t = rol8(((13*i + 0x42) ^ d ^ x), 3)
      out = (7*d + i + t) & 0xff
      d = (x + d + (out ^ i)) & 0xff
    
  • 目标数组[250, 113, 87, 185, 6, 125, 167, 156, 4, 0, 229, 239, 119, 155, 187, 95]
  • 逆算法
    t = (out - (7*d + i)) & 0xff
    y = ror8(t, 3)
    x = y ^ d ^ (13*i + 0x42)
    
  • 结果:Stage2 Key = k7Xm2Pq9Wv4N8bRt

Stage3:AES/CBC解密

  • 数据来源
    • Key:Stage2的结果
    • IV:SQLite数据库中的stage3_iv(Base64解码为VerifyVector1234
    • 密文:jSaMnziall55Tdr+IZc7EKUNm/N4uwrZw1QFPw6DuirfYFJZg88j6GKLhWfNljAB
  • 解密结果:Flag = NCTF{a680107e-a49b-43e1-915b-cedd25e7835a}

3. 复现脚本

#!/usr/bin/env python3
import base64, hashlib, itertools
from Crypto.Cipher import AES

def rol8(x: int, n: int) -> int:
    x &= 0xFF
    return ((x << n) | (x >> (8 - n))) & 0xFF

def ror8(x: int, n: int) -> int:
    x &= 0xFF
    return ((x >> n) | (x << (8 - n))) & 0xFF

def stage2_encrypt_like_native(inp: bytes) -> list[int]:
    d = 0x51
    out = []
    for i, x in enumerate(inp):
        t = rol8(((13 * i + 0x42) & 0xFF) ^ d ^ x, 3)
        o = (7 * d + i + t) & 0xFF
        out.append(o)
        d = (x + d + (o ^ i)) & 0xFF
    return out

def invert_stage2_target(target: list[int]) -> bytes:
    d = 0x51
    recovered = []
    for i, o in enumerate(target):
        t = (o - (7 * d + i)) & 0xFF
        y = ror8(t, 3)
        x = y ^ d ^ ((13 * i + 0x42) & 0xFF)
        recovered.append(x)
        d = (x + d + (o ^ i)) & 0xFF
    return bytes(recovered)

题目二:NoMyBank!

1. 题目结构

  • 主程序:NoMyBank.exe(Godot 4单文件导出)
  • 扩展库:libextension.dll(加密的伪DLL)
  • 资源加密:PCK资源包嵌入EXE末尾

2. 多层保护分析

第一层:Godot资源加密

  • 加密位置:EXE文件偏移0x5194000处的GDPC头
  • 加密格式
    • 16字节:明文MD5
    • 8字节:明文长度(小端)
    • 16字节:IV
    • 后续:AES-256-CFB密文
  • 密钥提取:从模板全局地址获取32字节密钥
    d34bff62613fdd2861f6d5942c5e99a53ef3e90adbe9091b4686859d5b7dab22

第二层:伪DLL保护

  • 加密方式:RC4加密
  • 密钥G00dLuck2U
  • 解密脚本
def rc4(data: bytes, key: bytes) -> bytes:
    s = list(range(256))
    j = 0
    for i in range(256):
        j = (j + s[i] + key[i % len(key)]) & 0xff
        s[i], s[j] = s[j], s[i]
    out = bytearray(len(data))
    i = j = 0
    for idx, b in enumerate(data):
        i = (i + 1) & 0xff
        j = (j + s[i]) & 0xff
        s[i], s[j] = s[j], s[i]
        out[idx] = b ^ s[(s[i] + s[j]) & 0xff]
    return bytes(out)

第三层:运行时热补丁

  • 保护机制:DLL初始化时patch掉TEA校验函数,跳转到shellcode
  • shellcode解密:.data段中的0x491字节数据异或0xBA
  • 真实算法
    1. 魔改Base64编码
    2. 基于0x114514的字节扰动

3. 真实校验算法

魔改Base64

  • 字符表ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/
  • 编码顺序:标准Base64输出[s0,s1,s2,s3]改为[s1,s0,s3,s2]

二次扰动

seed = 0x114514
for i in range(len):
    x = out[i]
    x ^= (seed >> ((i * 8) % 24)) & 0xff
    x = rol8(x, 2) ^ 0xBA
    out[i] = x
    seed = seed * 0x1010193 + 0x12345678

4. 目标常量

56字节比较目标:

2b f7 67 5e 7c 98 ed 6d d1 8c ef 57 bb 33 22 7e
b2 1f 34 5b 36 6c 2b af bb 5b 12 d6 3c 0a 45 27
84 6c 47 ab 2f 75 78 3e 88 89 2d 7a cd 5c f6 fa
36 73 ff 6e d3 4c 1c 75

5. 最终结果

  • KeyNCTF{You_deserve_this_gift_b1bd7c719cfc}
  • GiftNCTF{Y0u_d3s3rv3_th1s_g1ft_b1bd7c719cfc}

题目三:PAY-FOR-2048

1. 题目结构

  • 技术栈:Electron + WebAssembly
  • 核心文件
    • resources/app.asar
    • wasm-build/wasm32-unknown-unknown/release/wasm_core.wasm

2. 验证流程

verify_license阶段

  1. 检查maxTile >= 256
  2. 验证key格式:NCTF-XXXX-XXXX-XXXX
  3. 规范化key:去除前缀和短横线
  4. 32位滚动哈希校验
  5. 返回sessionToken

unlock_flag阶段

  1. 检查maxTile >= 2048
  2. 重新验证sessionToken
  3. 再次规范化key
  4. 异或流解密:normalized_key + "|" + "arcade::unlock-seed"
  5. 返回flag字符串

3. 关键点:哈希碰撞

  • 问题:verify阶段仅使用32位哈希,存在碰撞
  • 解决方案:联合约束求解
    • verify哈希常量约束
    • unlock输出格式约束(NCTF{...}格式)

4. Z3求解

from z3 import *

norm = [BitVec(f'n{i}', 8) for i in range(12)]
s = Solver()

# 字符集约束
for c in norm:
    s.add(Or(And(c >= 48, c <= 57), And(c >= 65, c <= 90)))

# verify哈希约束
K = BitVecVal(((-1515890086) & 0xffffffff), 32)
p = BitVecVal(4951, 32)
for i in range(12):
    c = ZeroExt(24, norm[i])
    t = BitVecVal(40503 if (i & 1) else 17881, 32)
    v = (t + c) * BitVecVal(i + 11, 32)
    v = v ^ RotateLeft(p, 3)
    v = v + ((c << (i % 5)) ^ K)
    p = Extract(31, 0, v)
s.add(p == BitVecVal(((-57161169) & 0xffffffff), 32))

5. 最终结果

  • 规范化KeyRU57W45M2048
  • 用户输入KeyNCTF-RU57-W45M-2048
  • FlagNCTF{bff16266-c4f2-4dbb-b270-f5ded900b54c}

题目四:Vm-Encryptor

1. VM架构分析

  • 程序结构:vm-encryptor.exe + code.bin
  • VM上下文结构
struct VM {
    uint32_t ip;        // 指令指针
    uint32_t sp;        // 栈指针
    uint8_t mem[0x10000]; // 64KB内存
    uint32_t stack[0x1000]; // 4096项整型栈
    uint32_t running;   // 运行状态
    uint32_t err;       // 错误码
};

2. VM指令集

opcode 含义 opcode 含义
0x00 jmp imm32 0x10 or
0x01 jz imm32 0x11 not
0x02 jnz imm32 0x12 xor
0x03 push8 imm8 0x13 shl
0x04 push32 imm32 0x14 shr
0x05 ld8 0x15 eq
0x06 ld32 0x16 ne
0x07 pop 0x17 lt
0x08 st8 0x18 gt
0x09 st32 0x19 le
0x0A add 0x1A ge
0x0B sub 0x1B dup
0x0C mul 0x1C swap
0x0D div 0x1D call imm32
0x0E mod 0x1E ret
0x0F and 0x20 printstr

3. 三层处理函数

第一层:3字节转4字节编码

  • 算法:24位混淆 + Base64映射
  • 混淆算法
    v = (mem[src_idx] << 16) | (mem[src_idx + 1] << 8) | mem[src_idx + 2]
    v = rol24(v, 5) ^ 0x55757d
    v = rol24(v, 11) ^ 0x55757d
    v = rol24(v, 20) ^ 0x55757d
    
  • Base64表:标准Base64字母表

第二层:异或处理

  • 简单异或dst[i] = src[i] ^ 0x63

第三层:比较函数

  • 比较目标:56字节固定数据

4. 逆算法实现

def ror24(x, n):
    return ((x >> n) | (x << (24 - n))) & MASK

# 逆变换过程
x ^= 0x55757d
x = ror24(x, 20)
x ^= 0x55757d
x = ror24(x, 11)
x ^= 0x55757d
x = ror24(x, 5)

5. 最终结果

  • FlagNCTF{1578be15-ad09-4859-9193-5d52585eb485}

题目五:鸡爪流高手

1. 程序概述

  • 类型:二进制协议 + 五子棋对战 + SQLite积分系统
  • 目标:将玩家分数提升至排行榜第一
  • 漏洞:负分处理时的32位零扩展漏洞

2. 协议格式

"GAME" + be32(total_len) + u8(cmd) + payload
  • 最大负载:0x400字节
  • 命令列表
    • 1: 取Flag
    • 2: 查看分数/排名
    • 3: 排行榜概览
    • 4: 重置环境
    • 5: 开始匹配
    • 6: 查看棋盘
    • 7: 落子
    • 8: 认输/取消

3. 积分系统漏洞

积分计算公式

def score_calculate_delta(self, opp, actual):
    expected = 1.0 / (1.0 + pow(10.0, (opp - self) / 50.0))
    return lround(20.0 * (actual - expected))

漏洞点分析

  1. buggy版本不检查负分

    if (old_score <= 9 && delta < 0) {
        return old_score;  // 仅对低分保护
    }
    return old_score + delta;  // 可能返回负数
    
  2. 32位零扩展问题

    mov edx, eax           ; 32位负数零扩展
    call sqlite3_bind_int64
    
    • 负数-1 (0xFFFFFFFF) → 4294967295

4. 利用链构建

初始状态

  • 玩家分数:50
  • 对手分数池:[50, 40, 30, 20, 20, 10, 2000000]

步骤分解

  1. 击败10分bot

    • 使用必胜开局:5,3 5,4 5,5 5,6 5,7
    • 结果:玩家50→53,对手10→7
  2. 连续输给指定对手

    • 53 → 37(输给20分)
    • 37 → 23(输给20分)
    • 23 → 15(输给30分)
    • 15 → 10(输给38分)
    • 10 → -1(输给7分)
  3. 数据库存储

    • 负数-1零扩展为4294967295
    • 成为排行榜第一

5. 对手选择策略

  • 使用cmd=5随机匹配
  • 非目标对手时,用cmd=8空棋盘取消(不掉分)
  • 重复直到匹配到目标分数对手

6. 完整利用流程

  1. cmd=4重置环境
  2. 匹配10分bot并获胜
  3. 按顺序输给指定分数对手
  4. 分数下溢后执行cmd=1获取flag

总结

技术要点归纳

  1. 多层保护突破:Godot资源加密、RC4伪DLL、运行时热补丁
  2. 算法逆向:自定义加密算法、魔改Base64、虚拟机指令集
  3. 约束求解:Z3求解器在哈希碰撞场景的应用
  4. 整数漏洞利用:负分处理与32位零扩展的组合利用
  5. 协议分析:自定义二进制协议的逆向与利用

防御建议

  1. 避免在关键校验中使用弱哈希
  2. 对用户输入分数进行边界检查
  3. 使用正确的有符号扩展处理
  4. 避免在客户端存储核心校验逻辑
  5. 对资源加密使用强密钥管理

学习价值

这些题目涵盖了从基础逆向到复杂系统漏洞利用的完整技能栈,包括:

  • 移动应用逆向(Android JNI)
  • 游戏保护机制分析
  • WebAssembly逆向
  • 虚拟机设计与实现
  • 二进制协议安全
  • 整数溢出漏洞利用

通过系统学习这些题目,可以建立完整的逆向工程和漏洞利用知识体系。

相似文章
相似文章
 全屏