吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 224|回复: 5
上一主题 下一主题
收起左侧

[CrackMe] 一个CrackMe,c++写的,随便爆破,随便ai分析,无壳无花

[复制链接]
跳转到指定楼层
楼主
qq465881818 发表于 2026-9-19 19:10 回帖奖励
CM是什么?Crackme是什么?这是什么东西?楼主发的什么?
他们都是一些公开给别人尝试破解的小程序,制作 Crackme 的人可能是程序员,想测试一下自己的软件保护技术,也可能是一位 Cracker,想挑战一下其它 Cracker 的破解实力,也可能是一些正在学习破解的人,自己编一些小程序给自己破解,KeyGenMe是要求别人做出它的 keygen (序号产生器), ReverseMe 要求别人把它的算法做出逆向分析, UnpackMe 是要求别人把它成功脱壳,本版块禁止回复非技术无关水贴。

总是玩你们的crackme 我也写个,咱不搞虚拟化也不搞控制流混淆,毕竟d810也能还原个七七八八。咱也不偷偷摸摸的开辟内存用完就抹除。咱就上个干干净净的crackme,我甚至字符串都不加密。随便爆破随便ai分析,看看能撑几分钟。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册[Register]

x

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
heyou666 发表于 2026-9-19 20:38
本帖最后由 heyou666 于 2026-9-19 20:44 编辑

交作业了 以下内容来自 fb5

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9x2o47D0QzyeeVSy
WiMbVXi8npubUzhU3MDu9uz8IXWhRANCAAQ2Z1gW2usnz4D6a11R52K/9BZY1khe
bNfB6InO5Gx7Kw9m+Pm8Mz9nTQAHrGTqEXJ6gtl18n+zAheqd5E+13ma
-----END PRIVATE KEY-----

注册码不是从机器码推导出来的,而是对机器码做的数字签名。推导方向是单向的,具体链条是这样(以你给的这对值为例):[color=oklch(0.87 0 0)]
① 取机器码 hex(程序从 MachineGuid+卷序列号算出并显示)   F19892BA3FA30D246076D5E08C75D7BC99077EA1476136BBA6108C962134ECB4② 拼消息字符串(逆向 main 0x140001904 处的两段立即数得到前缀)   M = "CMX/Verify/v2|F19892BA...ECB4"          ← 78 字节③ 算摘要   z = SHA256(M) = 0d80e20e4eab991363dbfcf5bbe1acf8...④ 用 P-256 私钥 d 对 z 做 ECDSA 签名,得两个 256 位整数   r, s = ECDSA_sign(z, d)⑤ 拼接输出   注册码 = "LX2-" + r(32字节大端hex) + s(32字节大端hex)   ← 128 个 hex 字符而程序那边的验证是反过来的:它用内嵌公钥检查你交的 (r,s) 是不是对①②③那条消息的合法签名(BCryptVerifySignature,0x140001ef0)。没有私钥,④ 这步在数学上走不通——这正是作者的设计,所以破解的本质是第④步用的私钥是我们自己的:patch 时已把程序内嵌公钥换成了 d·G,本文件把 d = 0xF71DA8E3…2175 内嵌,自带的 sign() 就能在任何机器上离线签发。

[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
crackme_solver.py —— 「离线验证 Crackme v2 (ECDSA-P256 加固版)」破解工具(单文件/零依赖)只依赖 Python 3.8+
【原理:机器码如何"变成"注册码】
  注册码不是由机器码算出来的,而是"机器码的数字签名":
    机器码 MX2-XXXX  = SHA256("CMX/Machine/v2|" + MachineGuid + "|" + 卷序列号%08X) 的 hex
                       (程序自己算,来自注册表+磁盘,用户无法指定)
    消息   M         = "CMX/Verify/v2|" + 机器码hex        ← 64 个大写 hex 字符
    摘要   z         = SHA256(M)                            ← 32 字节
    注册码 LX2-YYYY  = ECDSA-P256签名(z), r(32B) || s(32B) 转 128 个大写 hex

  验证方向是反的:程序内嵌公钥,用 BCryptVerifySignature 检查你交的 (r,s) 是不是
  对 M 的合法签名。没有私钥就无法对任意机器码签出合法注册码 —— 所以破解 = 把程序
  内嵌的公钥换成自己的(`patch` 子命令做的事),再用自己私钥签发(`keygen`)。
  本文件内嵌的私钥即 patch 后程序里的那把公钥对应的私钥。

  注意:同一机器码的合法注册码有无穷多个(签名随机数 k 可任选),本工具用 RFC6979
  确定性 k,同一输入永远得到同一输出;之前 cryptography 库生成的另一枚注册码同样有效。

【用法】
  python3 crackme_solver.py test                     # 自检(验证样例对)
  python3 crackme_solver.py keygen  MX2-<64hex>      # 机器码 -> 注册码
  python3 crackme_solver.py verify  MX2-.. LX2-..    # 离线复验
  python3 crackme_solver.py patch crackme.exe out.exe# 替换公钥材料(.text 不动)
  python3 crackme_solver.py machine <guid> <卷序列号> # 参考:复算机器码

  patch 后还需一次性的环境安装(程序只读不写):
    1. 把 seal 基准值以 DPAPI 密文写入注册表 HKCU\\Software\\OfflineCrackme\\TextSeal
       (solution/sealwrite.c,wine 下: wine sealwrite.exe <seal值>,本工具 patch 会打印该值)
    2. 运行 patch 后的程序,拿到 MX2-,keygen 出 LX2-,在 "> " 提示符输入即可 [√]
"""

import hashlib
import hmac
import sys

# ────────────────────────── 逆向得到的固定常数 ──────────────────────────
VERIFY_TAG   = "CMX/Verify/v2|"
MACHINE_TAG  = "CMX/Machine/v2|"
PREFIX_IN    = "MX2-"
PREFIX_OUT   = "LX2-"
FNV_BASIS    = 0x14650FB0739D0383        # main 内联 FNV-1a64 的自定义基准值
FNV_PRIME    = 0x100000001B3
TEXT_OFF     = 0x400                     # .text 文件偏移
TEXT_SIZE    = 0x19600                   # .text SizeOfRawData
KEYMAT_OFF   = 0x25011                   # .data 公钥 XOR 材料(VA 0x140027011)
TEXT_SEAL    = "2D3117E8DD257598"        # 原始 .text 的 FNV-1a64,TextSeal 内容

# ────────────────────────── 内嵌密钥(自己生成的,非作者私钥) ──────────────────────────
PRIV_D = 0xF71DA8E3B0F4433C9E7954B25A231B5578BC9E9B9B533854DCC0EEF6ECFC2175

# ────────────────────────── NIST P-256(secp256r1) ──────────────────────────
P  = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
A  = P - 3
B  = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
N  = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
G  = (GX, GY)

M64 = (1 << 64) - 1


def _ec_add(pt, q):
    """仿射坐标椭圆曲线点加,None 为无穷远点。"""
    if pt is None:
        return q
    if q is None:
        return pt
    x1, y1 = pt
    x2, y2 = q
    if x1 == x2 and (y1 + y2) % P == 0:
        return None
    if pt == q:
        lam = (3 * x1 * x1 + A) * pow(2 * y1, -1, P) % P
    else:
        lam = (y2 - y1) * pow(x2 - x1, -1, P) % P
    x3 = (lam * lam - x1 - x2) % P
    return (x3, (lam * (x1 - x3) - y1) % P)


def _ec_mul(k, pt):
    """倍加法标量乘。"""
    r = None
    while k:
        if k & 1:
            r = _ec_add(r, pt)
        pt = _ec_add(pt, pt)
        k >>= 1
    return r


PUB = _ec_mul(PRIV_D, G)                 # 内嵌私钥对应的公钥 X||Y


def _rfc6979_k(z, d):
    """RFC 6979 确定性随机数(HMAC-SHA256),使签名可复现。"""
    x = d.to_bytes(32, "big")
    h1 = z.to_bytes(32, "big")
    v = b"\x01" * 32
    k = b"\x00" * 32
    k = hmac.new(k, v + b"\x00" + x + h1, hashlib.sha256).digest()
    v = hmac.new(k, v, hashlib.sha256).digest()
    k = hmac.new(k, v + b"\x01" + x + h1, hashlib.sha256).digest()
    v = hmac.new(k, v, hashlib.sha256).digest()
    while True:
        v = hmac.new(k, v, hashlib.sha256).digest()
        cand = int.from_bytes(v, "big")
        if 1 <= cand < N:
            yield cand
        k = hmac.new(k, v + b"\x00", hashlib.sha256).digest()
        v = hmac.new(k, v, hashlib.sha256).digest()


def sign(msg: bytes, d: int = PRIV_D):
    """ECDSA-P256 签名 SHA256(msg),返回 (r, s)。"""
    z = int.from_bytes(hashlib.sha256(msg).digest(), "big") % N
    for k in _rfc6979_k(z, d):
        r = _ec_mul(k, G)[0] % N
        s = pow(k, -1, N) * (z + r * d) % N
        if r and s:
            return r, s


def verify(msg: bytes, r: int, s: int, q=PUB) -> bool:
    """ECDSA-P256 验签(与程序里 BCryptVerifySignature 等价)。"""
    if not (1 <= r < N and 1 <= s < N):
        return False
    z = int.from_bytes(hashlib.sha256(msg).digest(), "big") % N
    w = pow(s, -1, N)
    pt = _ec_add(_ec_mul(z * w % N, G), _ec_mul(r * w % N, q))
    return pt is not None and pt[0] % N == r


# ────────────────────────── 面向 crackme 的封装 ──────────────────────────

def keygen(machine_code: str) -> str:
    """MX2-机器码 -> LX2-注册码。"""
    m = machine_code.strip().upper().removeprefix(PREFIX_IN)
    if len(m) != 64 or any(c not in "0123456789ABCDEF" for c in m):
        raise ValueError("机器码应为 64 个 hex 字符(可带 MX2- 前缀)")
    r, s = sign((VERIFY_TAG + m).encode())
    return PREFIX_OUT + (r.to_bytes(32, "big") + s.to_bytes(32, "big")).hex().upper()


def check(machine_code: str, license_code: str) -> bool:
    """离线复验一对机器码/注册码。"""
    m = machine_code.strip().upper().removeprefix(PREFIX_IN)
    lic = license_code.strip().upper().removeprefix(PREFIX_OUT)
    if len(m) != 64 or len(lic) != 128:
        return False
    r = int.from_bytes(bytes.fromhex(lic[:64]), "big")
    s = int.from_bytes(bytes.fromhex(lic[64:]), "big")
    return verify((VERIFY_TAG + m).encode(), r, s)


def machine_code(guid: str, volume_serial: int) -> str:
    """参考实现:MachineGuid + 卷序列号 -> 机器码(需在目标机上取这两个值)。"""
    digest = hashlib.sha256(("%s%s|%08X" % (MACHINE_TAG, guid, volume_serial)).encode())
    return digest.hexdigest().upper()


def fnv64(data: bytes, h: int = FNV_BASIS) -> int:
    for b in data:
        h = ((h ^ b) * FNV_PRIME) & M64
    return h


def _text_samples(text: bytes) -> bytes:
    """复现 main 0x140001dc0 的采样:每 (size>>6) 字节取 FNV 状态最高字节。"""
    step = len(text) >> 6
    h = FNV_BASIS
    out = bytearray()
    for i, byte in enumerate(text):
        h = ((h ^ byte) * FNV_PRIME) & M64
        if i % step == 0:
            out.append(h >> 56)
            if len(out) == 64:
                break
    return bytes(out)


def patch(src: str, dst: str) -> str:
    """用内嵌公钥替换 .data 公钥材料,生成 crackme_patched.exe;.text 不动。"""
    with open(src, "rb") as f:
        data = bytearray(f.read())
    text = bytes(data[TEXT_OFF:TEXT_OFF + TEXT_SIZE])
    pub = PUB[0].to_bytes(32, "big") + PUB[1].to_bytes(32, "big")
    if data[0x25000:0x25010] != b"PUBSTOREDV1MARK1":
        raise ValueError("输入不是匹配的 crackme.exe(找不到 PUBSTOREDV1MARK1)")
    data[KEYMAT_OFF:KEYMAT_OFF + 64] = bytes(a ^ b for a, b in zip(pub, _text_samples(text)))
    with open(dst, "wb") as f:
        f.write(bytes(data))
    return "%016X" % fnv64(text)


# ────────────────────────── 命令行 ──────────────────────────

KNOWN_MACHINE = "F19892BA3FA30D246076D5E08C75D7BC99077EA1476136BBA6108C962134ECB4"
KNOWN_LICENSE = ("LX2-84D70786C289AF939CCD2D5AC114EB779DF7D7F407A516F15353FE9D8610D94C"
                 "7756CE2E8145CD03FA1BCACA842E34DBC13BEB7F7BF825572BC7C4B93836631A")


def selftest():
    assert check(KNOWN_MACHINE, KNOWN_LICENSE), "已知注册码对验证失败"
    mine = keygen(KNOWN_MACHINE)
    assert check(KNOWN_MACHINE, mine), "本工具签名自验失败"
    print("[√] 自检通过")
    print("    已知注册码(cryptography 库签) 验证 = True")
    print("    本工具签名(RFC6979 确定性 k)  验证 = True")
    print("    keygen(%s) = %s" % (KNOWN_MACHINE, mine))


def main(argv):
    if len(argv) == 1:
        selftest()
        print(__doc__)
        return 0
    cmd = argv[1]
    if cmd == "test":
        selftest()
    elif cmd == "keygen" and len(argv) == 3:
        print(keygen(argv[2]))
    elif cmd == "verify" and len(argv) == 4:
        print("[√] 验证成功" if check(argv[2], argv[3]) else "[×] 验证失败")
    elif cmd == "patch" and len(argv) == 4:
        seal = patch(argv[2], argv[3])
        print("patched -> %s" % argv[3])
        print("TEXT_SEAL = %s (需经 DPAPI 写入 HKCU\\Software\\OfflineCrackme\\TextSeal)" % seal)
    elif cmd == "machine" and len(argv) == 4:
        print("MX2-" + machine_code(argv[2], int(argv[3], 0)))
    else:
        print(__doc__)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

3#
 楼主| qq465881818 发表于 2026-9-19 20:54 |楼主
本帖最后由 qq465881818 于 2026-9-19 20:57 编辑
heyou666 发表于 2026-9-19 20:38
交作业了 以下内容来自 fb5

-----BEGIN PRIVATE KEY-----

你的py验证没通过 ,这个crackme的核心破解思路是伪造 公钥或者爆破,因为是非对称加密,你们是无法得到私钥的。你的fable5分析的用的你自己的私钥。
4#
西风 发表于 2026-9-19 21:14
我还是喜欢爆的。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册[Register]

x
5#
 楼主| qq465881818 发表于 2026-9-19 21:25 |楼主
西风 发表于 2026-9-19 21:14
我还是喜欢爆的。

可以可以
6#
FructusGardeni 发表于 2026-9-19 22:27
  1. 摘要(TL;DR)

样本是什么
一个离线机器绑定授权校验程序。它读取本机 MachineGuid 与系统盘卷序列号,经 SHA-256 生成"机器码" MX2-<64 hex>,再要求用户输入 LX2-<128 hex>(即 64 字节 raw r||s 的 ECDSA-P256 签名)。签名消息为 "CMX/Verify/v2|" + <机器码后 64 位 hex>,公钥以 XOR 密钥流加密存放于 .data。程序另有 --seal 模式,用于打印/比对 .text 段的自校验值 TextSeal(自定义 FNV-1a-64),期望值由作者工具以 DPAPI 密文写入注册表 HKCU\Software\OfflineCrackme\TextSeal

密码学本体是否可靠
可靠。ECDSA-P256 + SHA-256 的验签实现对标准库(bcrypt)调用完全正确,签名编码为 P1363 raw r||s,域参数、消息格式、哈希均无缺陷。在没有作者私钥的前提下,纯"算号"在数学上不可行(ECDLP)。

但保护体系仍然被完全击穿,原因不在密码学,而在工程实现:

# 缺陷 CWE 后果
F-1 公钥密文的密钥流仅由 .text 派生,而 .text 在攻击时不必修改 CWE-1188 / CWE-656 可用同一密钥流把攻击者自己的公钥写回 .data
F-2 .data 中仅 16 字节 PUBSTOREDV1MARK1 标记参与完整性校验,其后 64 字节公钥密文完全不受保护 CWE-353 / CWE-345 公钥可被任意替换而不触发任何检查
F-3 注册表封印是用户级 DPAPI 密文,而 DPAPI 的加密密钥属于当前登录用户 CWE-522 / CWE-269 任何本机用户都能自行生成合法封印
F-4 最终判定链末端的 jnz单点决定性分支 CWE-693 2 字节 NOP 即可强制进入成功分支(无注册码/无封印)
F-5 --seal 只读不写,二进制中不存在任何注册表写入路径 CWE-754 封印机制无法自举,攻击者可随意控制其内容
F-6 反调试仅 IsDebuggerPresent + CheckRemoteDebuggerPresent,且结果只与最终判定 AND CWE-693 无实际防护价值

攻击路径 A(推荐,端到端通过,退出码 0)

  1. SHA256("AZOTH-crackme-v2-keygen-demo-2026") 派生自己的 P-256 私钥 d_att;
  2. Q_att = d_att·G 的 64 字节大端 X||Y 异或同一密钥流后写回 file offset 0x25011..0x25050(只改 64 字节数据,不改任何代码);
  3. d_att"CMX/Verify/v2|" + hex_upper(machineFP) 签名,输出 LX2-<128 hex>;
  4. CryptProtectData 写入 HKCU\Software\OfflineCrackme\TextSeal = DPAPI("%016x" % FNV(.text));
  5. 运行 → 全部 8 个判定项同时通过。

攻击路径 B(更简)
把 file offset 0x109775 47(jnz short loc_140001CE0)改成 90 90(nop;nop),任意注册码即可打印"验证成功"。


1. 任务、环境与交付物

1.1 工单

逆向分析 E:\pi-profile\逆向\crackme.rar,输出完整详细的报告。

口径声明:解包 → 静态/动态逆向 → 还原注册算法 → 产出可复现的完整报告 + 可运行的 keygen / 破解工具。

1.2 工作目录与产物

E:\pi-profile\20260919-2210_crackme-reverse\
├─ TASK.md                        任务状态载体
├─ docs\
│   ├─ REPORT.md                  ← 本报告
│   └─ NOTES.md                   逆向工作笔记(地址/常量/伪代码速查)
├─ sample\
│   ├─ crackme.rar                原始压缩包(76558 B)
│   └─ extract\crackme.exe        解包后的原始样本(未修改)
├─ src\
│   ├─ crackme_lib.py             算法复现库(含 DPAPI/注册表/PE 操作)
│   ├─ keygen.py                  破解 + 注册码生成器(攻击路径 A)
│   ├─ patch_bypass.py            2 字节 NOP 补丁器 + 证据矩阵脚本(攻击路径 B)
│   ├─ verify_code.py             注册码独立校验器(纯 Python P-256)
│   └─ strings_extract.py         ASCII/UTF-16 字符串提取器
├─ keys\
│   ├─ pubkey_derived.txt         从 .data 解出的作者公钥(X/Y, hex)
│   ├─ pubkey_raw.bin             作者公钥 raw X||Y(64 字节)
│   ├─ crackme_keygen.exe         攻击路径 A 产物(换公钥版,可正常通过)
│   └─ crackme_nop.exe            攻击路径 B 产物(2 字节补丁版)
└─ logs\
    ├─ strings_all.txt / strings.json
    ├─ keygen_run.txt             keygen 运行记录
    ├─ evidence_matrix.txt        6 组对照实验结果
    ├─ evidence_outputs.txt       每组实验的完整程序输出
    └─ ida\
        ├─ crackme.exe.asm        IDA 反汇编(51,918 行)
        ├─ crackme.exe.i64        IDA 数据库
        ├─ decompiled.c           Hex-Rays 反编译(557,568 B / 557 个函数)
        └─ decompile_all.py       Hex-Rays 批量反编译脚本

1.3 分析环境

用途 工具 版本 / 路径
解包 bsdtar (Windows 内置) C:\Windows\System32\tar.exe — libarchive 3.8.8
静态反汇编/反编译 IDA Pro 8.3 (Hex-Rays) E:\Tools\MetaSword\Tools\Reverse\IDA\idat64.exe
辅助静态 radare2 E:\Tools\MetaSword\Tools\Reverse\radare2\radare2.exe
动态调试 x64dbg E:\Tools\MetaSword\Tools\Reverse\x64dbg\x64\
脚本 Python 3.14.7(cryptography 50.0.0 / pycryptodome 3.23.0 / capstone 5.0.7 / pefile)

Python 环境中没有 ecdsa 包,故 verify_code.py 内含一套完全独立的纯 Python P-256 实现(见 §5.4),避免"用库验库"的循环论证。

1.4 解包

本机未安装 7-Zip / WinRAR / Bandizip(which unrar rar 7z 全空)。压缩包为 RAR5 格式(签名 Rar!\x1a\x07\x01\x00),未加密。Windows 自带 tar.exe 基于 libarchive,原生支持 RAR5 读取:

cd sample/extract && /c/Windows/System32/tar.exe -xvf ../crackme.rar
# → crackme.exe   (退出码 0)

2. 静态分析

2.1 PE 结构概览

Machine              : 0x8664  (AMD64)
NumberOfSections     : 6
TimeDateStamp        : 1789744644  → 2026-09-18 15:17:24 UTC
Linker version       : 14.51      (Visual Studio 2022 v17.11+)
Subsystem            : 3          (CONSOLE)
ImageBase            : 0x140000000
AddressOfEntryPoint  : RVA 0x2B70
SizeOfImage          : 0x2D000
SizeOfHeaders        : 0x400
DllCharacteristics   : 0x8160     (HIGH_ENTROPY_VA | DYNAMIC_BASE | NX_COMPAT | TERMINAL_SERVER_AWARE)
Characteristics      : 0x22       (EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE)
CheckSum             : 0x0        (未计算)
SizeOfStackReserve   : 0x100000   SizeOfHeapReserve : 0x100000
Data Directories     : 有 DEBUG (IMAGE_DEBUG_TYPE_CODEVIEW, RVA 0x23A40 / size 0x1C) 与 LOAD_CONFIG
                      无 TLS、无延迟导入、无导出、无 SECURITY(未签名)
Rich Header          : 存在

2.2 节表

名称 RVA VirtualSize PointerToRaw SizeOfRawData 特征
.text 0x1000 0x19450 0x400 0x19600 6.480 RX
.rdata 0x1B000 0xB4FC 0x19A00 0xB600 5.085 R
.data 0x27000 0x16C0 0x25000 0xC00 2.211 RW
.pdata 0x29000 0x153C 0x25C00 0x1600 5.075 R
.fptable 0x2B000 0x100 0x27200 0x200 0.000 RW
.reloc 0x2C000 0x678 0x27400 0x800 4.931 R
  • 熵值全部正常,无壳、无加密壳、无 native 混淆(无 OLLVM / 无控制流平坦化痕迹)。
  • .fptable 为本样本特有节名,内容为 0 填充,代码中无任何引用 → 疑似链接器自定义节或预留槽位。
  • RVA→文件偏移换算(报告全文通用):
    • .text : file = RVA - 0x1000 + 0x400
    • .rdata: file = RVA - 0x1B000 + 0x19A00
    • .data : file = RVA - 0x27000 + 0x25000

2.3 导入表(保护机制的骨架)

bcrypt.dll — 密码学原语

BCryptOpenAlgorithmProvider   BCryptCloseAlgorithmProvider
BCryptImportKeyPair           BCryptVerifySignature     BCryptDestroyKey
BCryptGetProperty             BCryptCreateHash          BCryptHashData
BCryptFinishHash              BCryptDestroyHash

CRYPT32.dll

CryptUnprotectData            ← 仅解密(DPAPI);全程序不存在 CryptProtectData

ADVAPI32.dll

RegGetValueA                  ← 仅读取;全程序不存在 RegSetValueExA / RegCreateKeyEx

KERNEL32.dll(与授权逻辑直接相关的部分)

IsDebuggerPresent              CheckRemoteDebuggerPresent   GetCurrentProcess
GetWindowsDirectoryA           GetVolumeInformationA        GetModuleFileNameA / W
CreateFileA / W                ReadFile                     SetFilePointer / SetFilePointerEx
VirtualProtect                 GetStdHandle                 GetConsoleScreenBufferInfo
SetConsoleTextAttribute        SetConsoleOutputCP           ReadConsoleW / WriteConsoleW
QueryPerformanceCounter        GetSystemTimeAsFileTime      LoadLibraryExW / GetProcAddress
GetProcessHeap / HeapAlloc / HeapReAlloc / HeapFree / HeapSize
FlsAlloc / FlsGetValue / FlsSetValue / FlsFree
InitializeCriticalSectionEx    EncodePointer                SetEnvironmentVariableW
ExitProcess / TerminateProcess
(其余为 MSVC UCRT / CRT 启动集)

关键观察:程序从不写注册表,也从不用 CryptProtectData 生成封印。封印只能由作者的外部工具生成(见 F-5)。

2.4 字符串

.rdata 起始 RVA 0x1B000 / file 0x19A00。程序自有的字符串全部集中在其头部(0x1B3F0 ~ 0x1B78x),其余为 CRT/bcrypt 静态依赖。

RVA 内容 用途
0x1B3F0 PUBSTOREDV1MARK1 .data 完整性标记(16 字节,无 NUL)
0x1B400 --seal 封印诊断模式开关
0x1B407 TEXT_SEAL=%016llx\n 打印 .text FNV
0x1B41A MISSING 封印缺失
0x1B422 OK 封印匹配
0x1B425 MISMATCH 封印不匹配
0x1B42E SEALREG=%s\n 封印状态行
0x1B43A FNV_FAIL=%s\n .text 读取失败原因
0x1B447 机器码: MX2-%s\n 机器码显示格式
0x1B45C / 0x1B45D \n / \n 提示前缀
0x1B45F %s 请输入注册码 (LX2-..., 输入 q 退出):\n > 交互提示
0x1B498 按回车键退出... 退出提示
0x1B4AE GetModuleFileName 失败标签
0x1B4C0 CreateFile 失败标签
0x1B4CB ReadHeader 失败标签
0x1B4D6 DOS 失败标签
0x1B4DA NT 失败标签
0x1B4DD NoText 失败标签
0x1B4E4 Malloc 失败标签
0x1B4EB ReadText 失败标签
0x1B4F4 TextSeal 注册表值名
0x1B4FD Software\OfflineCrackme 注册表路径
0x1B515 MachineGuid HKLM 值名
0x1B521 SOFTWARE\Microsoft\Cryptography HKLM 路径
0x1B541 %08X 卷序列号格式
0x1B546 0123456789ABCDEF hex 表
0x1B557 / 0x1B559 / 0x1B55E q / quit / exit 退出命令
0x1B564 (UTF-16) SHA256 bcrypt 算法名
0x1B572 (UTF-16) HashDigestLength bcrypt 属性名
0x1B594 (UTF-16) ECDSA_P256 bcrypt 算法名
0x1B5AA (UTF-16) ECCPUBLICBLOB bcrypt blob 类型
0x1B5C6 离线验证 Crackme v2 (ECDSA-P256 加固版) 标题
0x1B5F7 50 × = 分隔线
0x1B62A 机器码: (无法读取本机指纹) 指纹失败分支(同时置 byte_140027BB8 = 1)
0x1B652 (请把上面这台机器的机器码发给作者以获取注册码) 指纹成功分支,无条件打印
0x1B699 \n[×] 验证失败:注册码无效或与当前机器不匹配。 失败
0x1B6DC 请核对机器码后重新向作者索取注册码。\n 失败
0x1B718 \n[√] 验证成功!注册码有效,感谢支持正版 :)\n 成功
0x1B757 \n[×] 未输入注册码。 空输入

IDA 的反编译器把 4 个宽字符串渲染成单字符 "E" / "H",实为 L"ECCPUBLICBLOB"L"HashDigestLength",已在反汇编中核对。

2.5 函数地图与调用链

start  (VA 0x140002B70, RVA 0x2B70)
  └─ sub_1400029F0   CRT 启动: __scrt_common_main_seh 等价物
       ├─ sub_140002BE0 / sub_140002B90 / sub_140002CC0 ... 安全 cookie / 异常表注册
       └─ sub_140001000(argc, argv)          ★★ 全部 crackme 逻辑 ★★

保护逻辑只存在于 3 个函数中(已用交叉引用确认无遗漏):

函数 地址 职责
sub_140001000 0x140001000 主流程:横幅、机器码、输入、解析、反调试、自校验、签名验证、判定
sub_140002190 0x140002190 读取并解密注册表封印(DPAPI)
sub_1400022E0 0x1400022E0 计算 32 字节机器指纹(SHA-256)

辅助函数(全部为薄封装):

函数 等价 API 函数 等价 API
sub_140002480 bcrypt SHA-256 sub_1400025B0 读取自身 .text 节原始字节
sub_1400027D0 vfprintf(stdout, ...) sub_140002840 vsnprintf
sub_140006040 __acrt_iob_func sub_1400060CC _fileno
sub_1400060F4 _isatty sub_1400064F0 fflush
sub_1400066C0 fgets sub_1400066D0 isspace
sub_140006870 strcmp sub_140006A10 free
sub_140006B74 getchar sub_140006FA0 strtoull(s, NULL, 16)
sub_140009CD0 malloc sub_1400191E0 memset
sub_1400195A0 memcpy sub_140019C30 strcmp
sub_140019CB0 strncpy sub_140019E30 strlen

sub_140002900 / 29C0 / 29D0 / 2B90 / 2BE0 / 2C20 / 2CC0 / 2D60 / 2D90 / 2DC0 / 2E00 / 2EC0 … 2FB0 全部为 MSVC CRT 启动与安全机制(_security_cookie = 0x2B992DDFA232__fastfail、Fls 回调、setlocale 等),不含任何 crackme 逻辑

2.6 构建痕迹

  • Linker 14.51 + CONSOLE + Rich Header + SizeOfHeaders=0x400 + EncodePointer / FlsAlloc / __security_cookie → 标准 MSVC x64 控制台工程,未经过混淆器。
  • .pdata 完整 → 未开启 /GUARD:NO 之类裁剪,无手工汇编。
  • 无 TLS 回调、无延迟导入、无资源节。

3. 程序行为逐步还原

以下伪代码直接对应 logs/ida/decompiled.csub_140001000 的逐段,变量名保留 IDA 命名以便与反编译输出对照。

3.1 顶层分支

if ( a1 < 2 || strcmp(argv[1], "--seal") )      // a1 = argc
{
    /* ───────── 正常验证模式 ───────── */
}
/* ───────── 封印诊断模式(fall-through) ───────── */
return 0;

3.2 机器码计算与显示

SetConsoleOutputCP(0xFDE9);                      // 65001 = UTF-8
v5 = sub_1400022E0(pbOutput);                    // 32 字节机器指纹

for (j = 1; j != 33; j += 2)                     // 32 字节 → 64 个 hex 字符
{
    v14 = pbOutput[j - 1];
    v116[j*2 - 2] = "0123456789ABCDEF"[v14 >> 4];    // 实际写入 v117[0..63]
    v116[j*2 - 1] = "0123456789ABCDEF"[v14 & 0xF];
    v15 = pbOutput[j];
    v117[j*2 ... ] = ...;                            // 同上,偶数位置
}
v119 = 0;

v117 是一段 64 字节连续栈缓冲(IDA 反编译把它拆成 v117 + v118[3],实际 rbp-0x428 ~ rbp-0x3E8),存放机器指纹的 64 个大写 hex 字符

显示:

if ( v5 )   // 指纹读取成功(正常路径)
{
    printf("  机器码: MX2-%s\n", v117);                     // byte_14001B447
    printf("  (请把上面这台机器的机器码发给作者以获取注册码)");  // word_14001B652 ← 无条件打印
}
else        // 指纹读取失败(无 MachineGuid / 卷序列号取不到 / WindowsDirectory < 3)
{
    printf("  机器码: (无法读取本机指纹)");                    // word_14001B62A
    byte_140027BB8 = 1;                                    // 见 §3.10:共享"异常"标志 → 必定失败
}

修正(2026-09-19 实机复核):word_14001B652 属于指纹成功分支,无条件打印(实测程序输出确实同时出现机器码与该提示);word_14001B62A 才是失败分支。

byte_140027BB8两处共用的"异常"标志:①指纹读取失败(decompiled.c:176);②检测到调试器(decompiled.c:358)。二者都会把最终判定里的 (byte_140027BB8 ^ 1) 项清零 → 必定失败。

sub_1400022E0 — 机器指纹生成

__int64 sub_1400022E0(PUCHAR pbOutput)
{
    memset(pvData, 0, 128);
    cbGuid = 128;
    if ( RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography",
                      "MachineGuid", RRF_RT_REG_SZ /*2*/, NULL, pvData, &cbGuid)
      || GetWindowsDirectoryA(Buffer, 0x104) < 3 )
        return 0;                                      // 失败

    RootPathName = Buffer[0];                          // 系统盘盘符, 如 'C'
    strcpy(pcbData, ":\\");                            // → "C:"
    GetVolumeInformationA(&RootPathName, 0, 0, &serial, 0, 0, 0, 0);

    memcpy(pbInput, "CMX/Machine/v2|", 15);
    v4 = strlen(pvData);                               // MachineGuid 长度
    memcpy(pbInput + 15, pvData, v4);
    pbInput[v4 + 15] = '|';
    v5 = snprintf(pbInput + v4 + 16, "%08X", serial);  // 8 位大写 hex 卷序列号

    return sub_140002480(pbInput, v4 + v5 + 16, pbOutput);   // SHA-256, 共 60 字节
}

规范:SHA-256( "CMX/Machine/v2|" + MachineGuid + "|" + %08X(系统盘卷序列号) ) → 32 字节 → 大写 hex → 前缀 MX2-

3.3 输入与格式校验

memset(buf, 0, 512);
if (argc < 2) {
    printf("%s  请输入注册码 (LX2-..., 输入 q 退出):\n  > ", prefix);
    fgets(buf, 512, stdin);
} else {
    strncpy(buf, argv[1], 511);
}
// isspace 双向裁剪得到有效长度 v33

if (argc >= 2 && 输入为空) { printf("\n[×] 未输入注册码。\n"); 等待回车; return 1; }
if (交互模式 && (strcmp(buf,"q")==0 || strcmp(buf,"quit")==0 || strcmp(buf,"exit")==0)) return 0;

if ( v33 == 132 && *(DWORD*)buf == 0x2D32584C )     // 0x2D32584C → "LX2-"
{ /* 进入解析 */ }
  • 0x2D32584C 小端字节序为 4C 58 32 2D = ASCII "LX2-"
  • 132 = 4 + 128:即 LX2- 后恰好 128 个十六进制字符(64 字节签名的 hex)。

3.4 注册码解析

for (k = 0; k != 64; ++k)
{
    v37 = buf[4 + 2*k];        // 高 nibble 字符
    v40 = buf[4 + 2*k + 1];    // 低 nibble 字符
    // 0-9 → c-48 ; a-f → c-87 ; A-F → c-55 ; 其他 → -1 并 break
    v35 = ((v38 | v41) >= 0);  // 解析成功标志
    if ((v38 | v41) < 0) break;
    pbSignature[k] = (16 * v38) | v41;
}

解析结果 pbSignature[0..63]ECDSA P1363 raw 签名 r(32BE) || s(32BE)

3.5 反调试

if ( IsDebuggerPresent()
  || (CheckRemoteDebuggerPresent(GetCurrentProcess(), &flag) && flag) )
{
    byte_140027BB8 = 1;            // 只在最终判定中 AND 进去
}

仅用于把 v77 的判定位置 0,无主动干扰、无 NtGlobalFlag/PEB 检查、无 ptrace 等价物、无时间差检测、无自篡改。F-6

3.6 TextSeal 自校验

sub_1400025B0 读取自身文件:

GetModuleFileNameA(0, Filename, 0x104);
hFile = CreateFileA(Filename, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
ReadFile(hFile, Buffer, 0x1000, &n, 0);            // 读节表头
if (*(WORD*)Buffer != 0x5A4D /*MZ*/)  return fail("DOS");
if (*(DWORD*)(Buffer + e_lfanew) != 0x4550 /*PE\0\0*/) return fail("NT");
// 遍历 6 个节头, 匹配节名 ".text"
if (节名 != ".text")                   return fail("NoText");
SetFilePointer(hFile, SectionHeader->PointerToRawData, 0, FILE_BEGIN);
ReadFile(hFile, malloc(SectionHeader->SizeOfRawData), SizeOfRawData, &n, 0);
*a1 = SizeOfRawData;                                // = 0x19600

于是 TextSeal = FNV-1a-64(.text 段原始字节),其中:

offset_basis = 0x14650FB0739D0383    ← 非标准! 二进制内硬编码
prime        = 0x100000001B3         ← 标准 FNV-1a 64 位素数
h = prime * (byte ^ h)               ← 逐字节

主流程在 4 处独立重复这段 FNV 计算(编译器把 4 字节展开成 h=P*(P*(P*(P*(h^b0)^b1)^b2)^b3) 的等价形式),分别存入 v45v2v63v70;并两次调用 sub_140002190 读取注册表封印:

v45 = FNV(.text);                                   // 第 1 次
if ( sub_140002190(&stored) ) {                      // 读注册表(DPAPI)
    v2  = FNV(.text);                                // 第 2 次
    v53 = readOK & (v2 == stored);
} else v53 = 0;

/* ... 签名验证 ... */

v63 = FNV(.text);                                    // 第 3 次
if ( sub_140002190(&stored) ) {
    v70 = FNV(.text);                                // 第 4 次
    v73 = readOK & (v70 == stored);
} else v73 = 0;

sub_140002190 — 封印读取(DPAPI)

if ( !RegGetValueA(HKEY_CURRENT_USER, "Software\\OfflineCrackme", "TextSeal",
                   RRF_RT_REG_BINARY /*8*/, NULL, pvData, &cbData /*=1024*/) )
{
    pDataIn = { pvData, cbData };
    if ( CryptUnprotectData(&pDataIn, NULL, NULL, NULL, NULL, 0, &pDataOut) )
    {
        memset(v9, 0, 128);
        memcpy(v9, pDataOut.pbData, min(pDataOut.cbData, 127));
        LocalFree(pDataOut.pbData);
        v5 = strtoull(v9, NULL, 16);                  // 期望 16 个 hex 字符
        *a1 = v5;
        return v5 != 0;
    }
}
return 0;

注册表值 HKCU\Software\OfflineCrackme\TextSeal 必须是 REG_BINARY 且内容为 DPAPI 密文,明文是 16 字符的十六进制 TextSeal 值

3.7 签名消息与密钥流派生

// 消息 = "CMX/Verify/v2|" + 64 个机器指纹 hex 字符
memcpy(v124, "CMX/Verify/v2|", 14);
*(__m128i*)&v124[14] = _mm_load_si128(&v117);        // 前 16 字符
*(__m128i*)&v124[30] = _mm_load_si128(&v118[0]);     // 次 16 字符
*(__m128i*)&v124[46] = v118[1];
*(__m128i*)&v124[62] = v118[2];
v59 = sub_140002480(v124, 0x4E /*78*/, pbHash);      // SHA-256

公钥解密:

BCryptOpenAlgorithmProvider(&hAlg, L"ECDSA_P256", 0, 0);

// ① 完整性标记检查(唯一被校验的 .data 内容)
if ( _mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(&xmmword_140027000), MARKER))
     != 0xFFFF )                                    // MARKER = "PUBSTOREDV1MARK1"
    goto fail;                                       // .data RVA 0x27000 前 16 字节

// ② 读取自身 .text 原始字节
v84 = sub_1400025B0(&size);                          // size = 0x19600
if (!v84 || size <= 0x3F) goto fail;

// ③ 从 .text 派生 64 字节密钥流
h = 0x14650FB0739D0383;
n = 0;
for (i = 0; i < size && n < 64; ++i)
{
    h = 0x100000001B3 * (v84[i] ^ h);
    if (i % (size >> 6) == 0)                        // size>>6 = 103936>>6 = 1624
        pbInput[n++] = HIBYTE(h);                    // 取高 8 位
}

// ④ 解密(RVA 0x27011 起 64 字节)
v89 = xmmword_140027011 ^ pbInput[ 0..15];           // → X[ 0..15]
v90 = xmmword_140027021 ^ pbInput[16..31];           // → X[16..31]
v91 = xmmword_140027031 ^ pbInput[32..47];           // → Y[ 0..15]
v92 = xmmword_140027041 ^ pbInput[48..63];           // → Y[16..31]
*(QWORD*)pbInput      = 0x2031534345;                // "ECS1" + 0x20 + 000000
                                                     // (BCRYPT_ECDSA_PUBLIC_P256_MAGIC=0x31534345, cbKey=32)
*(__m128i*)&pbInput[8]  = v89;
*(__m128i*)&pbInput[24] = v90;
*(__m128i*)&pbInput[40] = v91;
*(__m128i*)&pbInput[56] = v92;                       // → 72 字节 BCRYPT_ECCKEY_BLOB

关键:四条 xmm 载入全部发生在四条 store 之前(见反汇编 0x1400019DC..0x140001A0F),同一块栈缓冲先当密钥流、后被改写为公钥 blob,逻辑正确无自覆盖。

.data 布局(RVA 0x27000 / file 0x25000):

偏移 长度 内容 是否被完整性校验
+0x00 16 PUBSTOREDV1MARK1 ✅ 是(唯一)
+0x10 1 0x00 ❌ 否
+0x11 64 keystream[i] XOR pubkey_raw[i](X‖Y)
+0x51 0 ❌ 否

3.8 ECDSA 验签与最终判定

BCryptImportKeyPair(hAlg, 0, L"ECCPUBLICBLOB", &hKey, pbInput, 0x48, 0);
v60 = (BCryptVerifySignature(hKey, NULL, pbHash, 0x20, pbSignature, 0x40, 0) == 0);

// 机器指纹确定性复查
v2 = (sub_1400022E0(v134) && 逐字节(pbOutput,v134) && 逐字节(v122,v135));

v77 =  v73                    // 注册表封印(FNV == stored) 第 2 次
     & (v63 == v45)           // 4 次 FNV 互相一致
     & v59                    // SHA-256 成功
     & v35                    // 注册码 hex 解析成功
     & v2                     // 机器指纹可复现
     & v60                    // ★ ECDSA 验签通过
     & v53                    // 注册表封印 第 1 次
     & (byte_140027BB8 ^ 1);  // 无反调试器

if (v77 == 1)  绿色 + "\n[√] 验证成功!注册码有效,感谢支持正版 :)\n";
else           红色 + "\n[×] 验证失败:注册码无效或与当前机器不匹配。" +
                      "    请核对机器码后重新向作者索取注册码。\n";

if (argc >= 2) { printf("按回车键退出..."); 等待回车; }
return v77 ^ 1;                                            // 退出码: 0 = 成功

反汇编末端的决定性分支(RVA 0x1C97,file 0x1097):

140001C8D  movzx   ebx, cs:byte_140027BB8
140001C94  xor     bl, 1
140001C97  and     bl, r14b
140001C9A  and     bl, al
140001C9C  cmp     bl, 1
140001C9E  75 47   jnz     short loc_140001CE0      ← ★ 单点决定成功/失败
140001CA0  ...
140001CE0  ...     (红色失败分支)

3.9 --seal 诊断模式

v99 = sub_1400025B0(&size);                          // 读 .text
if (!v99) { … 报告 FNV_FAIL=%s … }
v102 = FNV(.text);                                   // 逐字节 FNV-1a
*(QWORD*)&stored = 0;
v105 = sub_140002190(&stored);                       // 读注册表封印
printf("TEXT_SEAL=%016llx\n", v102);
s = v105 ? (stored == v102 ? "OK" : "MISMATCH") : "MISSING";
printf("SEALREG=%s\n", s);
if (v102 == 0 && qword_140027BB0)
    printf("FNV_FAIL=%s\n", qword_140027BB0);        // 具体失败标签
return r13d;                                          // = 0

该模式只读不写,不产生任何注册表写入。


4. 算法规格(可移植描述)

4.1 常量表

名称 出现位置
FNV offset basis 0x14650FB0739D0383 所有自校验与密钥流派生(非标准)
FNV prime 0x100000001B3 同上(标准 FNV-1a 64)
密钥流 step SizeOfRawData(.text) >> 6 = 103936 >> 6 = 1624 公钥解密
.data 公钥密文偏移 RVA 0x27011 / file 0x25011,64 字节 公钥解密
.data 标记偏移 RVA 0x27000 / file 0x25000,16 字节 完整性检查
机器指纹前缀 "CMX/Machine/v2 | " (15 字节)
签名消息前缀 "CMX/Verify/v2 | " (14 字节)
P-256 曲线 secp256r1 / NIST P-256 / prime256v1 L"ECDSA_P256"
注册码前缀 "LX2-" → 小端 DWORD 0x2D32584C 格式校验
机器码前缀 "MX2-" 显示格式
签名格式 P1363 raw r‖s,各 32 字节大端,共 64 字节 BCryptVerifySignature(..., 0x40, 0)
哈希 SHA-256(32 字节) L"SHA256"
BCRYPT blob magic 0x31534345 = ECS1(BCRYPT_ECDSA_PUBLIC_P256_MAGIC),cbKey = 32 BCRYPT_ECCKEY_BLOB
注册表路径 HKCU\Software\OfflineCrackme / 值名 TextSeal / REG_BINARY sub_140002190
完整性标记 PUBSTOREDV1MARK1 .data+0x00

4.2 机器码算法

guid   = HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid          (REG_SZ)
drive  = GetWindowsDirectoryA()[0]                                  (如 'C')
serial = GetVolumeInformationA(drive + ":\")->VolumeSerialNumber    (DWORD)
msg    = "CMX/Machine/v2|" + guid + "|" + sprintf("%08X", serial)   (60 字节)
fp     = SHA-256(msg)                                               (32 字节)
机器码 = "MX2-" + hex_upper(fp)                                      (68 字符)
签名消息 = "CMX/Verify/v2|" + hex_upper(fp)                          (78 字节)
z      = SHA-256(签名消息)                                           (32 字节)
注册码 = "LX2-" + hex_upper(ECDSA_P256_sign(d_author, z))            (132 字符)

4.3 公钥提取算法

keystream[0..63]:
    h = 0x14650FB0739D0383
    for i in 0 .. 103935:
        h = (0x100000001B3 * (text_raw[i] XOR h)) mod 2^64
        if i mod 1624 == 0: emit HIBYTE(h)
pubkey_raw = (file[0x25011 .. 0x25050]) XOR keystream
X = pubkey_raw[0..31]; Y = pubkey_raw[32..63]

5. 动态验证与实验结果

5.1 --seal 与 FNV 复算

$ ./crackme.exe --seal
TEXT_SEAL=2d3117e8dd257598
SEALREG=MISSING

Python 独立复算:

FNV-1a-64(offset=0x14650FB0739D0383, prime=0x100000001B3) over .text raw(0x400..0x19A00)
  → 2d3117e8dd257598      ✅ 与程序输出完全一致
FNV-1a-64 over 整个文件   → f468f83ee1861716   (参考值,非程序使用)

SEALREG=MISSING 证实:程序自身从未写入过封印,且当前系统无该键值。

5.2 公钥提取

keystream (64 B):
  44426e92952be4ebfe5a2bb355eda304e0135cf5d933918b0790d58bb024f1b1
  aa6dc92e79715d4a7f88d872587ecda71752ee6845880a367771ecee41d5ba50
密文 file[0x25011..0x25050]:
  6aa4f1e5f5e865f5e965fa99c8f9e7415dcd4d75bf67653ae8c89d359147e644
  3700e6fdf00a9bfa72f8682144d5cd6c86a8278ee85d2a2ae3a4de1b1deb1ca4
XOR ⇒
  X = 0x2ee69f7760c3811e173fd12a9d144445bdde11806654f4b1ef5848be216317f5
  Y = 0x9d6d2fd3897bc6b00d70b0531cab00cb91fac9e6add5201c94d532f55c3ea6f4
曲线校验 (Y² - (X³ + aX + b)) mod p == 0  →  True     ✅

a = p − 3,
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B,
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF

公钥落在 P-256 上,证明解密与密钥流派生完全正确,不是巧合的随机字节。

5.3 机器码复算

本机实测:

MachineGuid    = d863bef0-aed9-4a1f-82a5-a9a62318e7d5
C: 卷序列号    = 0x1A8455F9
程序输出       = MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
Python 复算    = 6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB   ✅ 一致
签名消息       = CMX/Verify/v2|6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
SHA-256(消息)  = 2999451ce8d57c506ca81675e62ffc5cb042c292622c229c158518ca227a17bf

5.4 验签复算(纯 Python,零依赖)

src/verify_code.py 内含独立的 P-256 实现(仿射坐标 + 扩展欧几里得求逆 + 双击点乘),不依赖 cryptography/ecdsa/OpenSSL:

$ python src/verify_code.py --exe keys/crackme_keygen.exe "LX2-4632...5F22"
注册码          : LX2-463225DC4908210F9CCEF92606D34322FE92A0662384865E282719635F4DA04C...
长度            : 132
ECDSA-P256 验签 : 通过

同时 keygen.py --run 中程序本体也给出了 [√] 验证成功两条独立路径(纯 Python 数学 + Windows bcrypt)结论一致

5.5 证据矩阵

src/patch_bypass.py --evidence 自动执行 6 组对照实验(完整输出见 logs/evidence_outputs.txt):

# 二进制 注册表封印 退出码 判定 说明
1 crackme.exe(原版) 1 失败 原版 + 攻击者注册码,无封印
2 crackme.exe(原版) 有(原 .text FNV) 1 失败 即使封印齐全,原版仍需作者私钥
3 crackme_nop.exe 1 仅打印成功 2 字节 NOP 只影响打印分支,退出码仍为真实 v77^1
4 crackme_keygen.exe 1 失败 换公钥版 + 攻击者注册码;封印缺失 → 失败
5 crackme_keygen.exe 有(伪造) 0 成功 端到端完全通过(攻击路径 A)
6 crackme_nop.exe 1 仅打印成功 2 字节 NOP 补丁 + 任意注册码,无需封印

实验 2 是本次分析最重要的对照:它证明密码学本体确实无法被"算号"攻破 —— 换个说法:作者的 ECDSA 部署是有效的,失效的是它的外包装。
实验 5 证明:不掌握任何作者密钥、不改动一个代码字节,即可获得真实的端到端通过(退出码 0)。


6. 安全性评估

6.1 密码学实现评审(结论:正确)

检查点 结果
曲线 P-256 标准参数,无 custom curve 后门
公钥导入 BCRYPT_ECCKEY_BLOB + ECCPUBLICBLOB,magic/长度校验正确
公钥点校验 bcrypt 导入时验证点在曲线上
签名格式 P1363 raw r‖s,64 字节,cbSignature=0x40 一致
验签 API 参数 pPaddingInfo=NULL,dwFlags=0,无算法降级空间
哈希 SHA-256,HashDigestLength 运行时校验为 32(sub_140002480)
消息绑定 消息含机器指纹 → 签名与机器绑定,无跨机重放
消息分隔符 `"CMX/Verify/v2
随机数依赖 验证侧不需要随机数
侧信道 BCryptVerifySignature 为常量时间实现

不存在可被利用的密码学缺陷。纯 keygen(不含补丁)在数学上被 ECDLP 阻断。

6.2 工程实现缺陷

F-1 / F-2 —— 公钥可被替换(致命)

公钥的"加密"密钥流完全由 .text 内容决定,而 .text 与公钥密文之间没有任何绑定关系。攻击者不需要修改 .text 就能重写公钥密文;同时 .data 中唯一被校验的内容是前 16 字节标记,64 字节公钥密文处于完整性保护的空窗。

// 唯一的 .data 完整性检查 —— 只覆盖 16 字节:
if (_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_loadu_si128(&xmmword_140027000), "PUBSTOREDV1MARK1")) != 0xFFFF)
    goto fail;

F-3 —— 封印使用用户级 DPAPI(致命)

CryptUnprotectData(..., dwFlags = 0) 的密钥由当前用户的主密钥派生。程序只读取封印、从不生成封印,而 CryptProtectData 对任何本机用户都可用 → 封印对攻击者完全透明。若改用机器级 DPAPI(CRYPTPROTECT_LOCAL_MACHINE)或由作者服务端签名,难度会显著提升(但仍无法阻止路径 A)。

F-4 —— 单点决定性分支(致命,但只能骗打印)

判定链最终坍缩为一个 cmp bl,1 / jnz。把 jnz(2 字节)改成 nop;nop 即进入成功分支。作者显然意识到了单点风险,故把 FNV 计算冗余了 4 份 —— 但所有冗余都汇入同一个 bl,冗余完全失效。

另外,return v77 ^ 1 使用的是真实的 v77,而非分支结果,因此 NOP 补丁后程序会"打印成功但退出码为 1"。这是作者唯一意外的"反制",但毫无实际意义。

F-5 —— --seal 只读不写

二进制中不存在 RegSetValueExA / RegCreateKeyEx / CryptProtectData 导入。封印必须由作者外部工具安装 → 攻击者拥有封印的完全控制权,自校验形同虚设。

F-6 —— 反调试形同虚设

IsDebuggerPresent() + CheckRemoteDebuggerPresent(),结果只 AND 进最终判定。既不干扰调试器,也不检测硬件断点/单步陷阱,对 x64dbg / IDA 调试无任何阻力(本报告全程未遇到任何调试对抗)。

其他观察

  • .fptable 节为 0 填充且无引用 —— 噪音。
  • CheckSum = 0 —— 未参与校验。
  • 无 PE 签名 —— 可被任意修改后重打包。
  • 4 次 FNV 计算完全冗余,可合并为 1 次 + 1 个比较,代码体积亦能缩小。

6.3 攻击路径 A — 公钥替换 + 封印伪造(推荐,0 代码修改)

仅修改 .data 中 64 字节密文 + 写入注册表;.text 与所有代码字节保持原样

# 1) 派生攻击者私钥
d_att = int.from_bytes(sha256(b"AZOTH-crackme-v2-keygen-demo-2026").digest(), "big") % n

# 2) 把自己的公钥按同一密钥流加密后写回 .data
ks      = keystream(.text)                     # 未变,因为 .text 未改
Q_att   = d_att * G                            # 64 字节 X||Y 大端
patched = exe[:0x25011] + xor(Q_att, ks) + exe[0x25051:]
#          ↑ 16 字节 PUBSTOREDV1MARK1 标记完好, 完整性检查通过

# 3) 对机器码签名
z    = SHA256("CMX/Verify/v2|" + hex_upper(machine_fp))
code = "LX2-" + hex_upper(ecdsa_p256_sign(d_att, z))   # raw r||s, 64 字节

# 4) 伪造 DPAPI 封印
blob = CryptProtectData(b"%016x" % FNV(.text))        # 用户级 DPAPI
registry.SetValue(HKCU\Software\OfflineCrackme, "TextSeal", REG_BINARY, blob)

# 5) 运行 → 8/8 判定项通过, 退出码 0

一键执行:

python src/keygen.py --run

实测输出(节选,完整见 logs/keygen_run.txt):

STEP 1/3 — 把自己生成的 P-256 公钥写入 .data(数据补丁)
私钥(SHA256 派生): 4fbe981093468fa365b6a6e6c3b0583c420d3793a9155020d140d5bcdc951ee6
新公钥 X         : 4338bdc6275722d8c8dd4eb2513a94d29bfbce37eb3b795d223f96302d07e5e6
新公钥 Y         : b36fee2e0378581300e378fefff843584ac5361a68605b343063483498b77f2a
新公钥在曲线上   : True
TextSeal 未变    : 2d3117e8dd257598

STEP 2/3 — 用我们的私钥对机器码签名, 生成 LX2- 注册码
机器码 : MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
注册码 : LX2-463225DC4908210F9CCEF92606D34322FE92A0662384865E282719635F4DA04C
              33DDD73C9372D729219FE2B69E0C104626CE49CB75DE9F923227B644BA115F22
注册码长度: 132 (期望 132)
独立验签通过 : True

STEP 3/3 — 伪造 DPAPI 注册表封印
封印明文      : 2d3117e8dd257598
DPAPI 密文长度: 246 字节

验证 — 运行破解版
  [√] 验证成功!注册码有效,感谢支持正版 :)
exit code : 0

为什么这个攻击能成立(因果链)

  1. 公钥密文的密钥来源(.text)与公钥密文的存储位置(.data)之间没有密码学绑定 → 只要 .text 不变,就能用同一密钥流加密任何数据;
  2. .data 的完整性检查只覆盖 16 字节标记 → 64 字节公钥密文是自由写入区;
  3. 封印的信任根是"当前用户",而攻击者就是当前用户 → 封印可自签;
  4. 因此验签、自校验、反调试三项全部可被"合法"满足。

6.4 攻击路径 B — 2 字节 NOP

file offset 0x1097:  75 47  (jnz short loc_140001CE0)
                 →   90 90  (nop ; nop)
python src/patch_bypass.py --make
./keys/crackme_nop.exe "<任意 132 字符的 LX2- 串>"

效果:直接打印 [√] 验证成功,注册码内容任意、无需封印、无需签名。
代价:.text 被修改 → TextSeal2d3117e8dd257598 变为 dd2564e237e67884;但因为该分支已被 NOP,v53/v73 的取值不再影响控制流。退出码仍为 1(return v77 ^ 1 用的是真实 v77)。

若希望退出码也归零,只需额外把 return v77 ^ 1 的求值改为常量 —— 或直接采用路径 A。

6.5 其他可行路径

路径 做法 备注
内存补丁 运行时把 BCryptVerifySignature 返回值改写为 0(或 hook 0x140001C9Ejnz) 需调试器/注入,触发反调试标志但不影响 NOP 后的逻辑
Hook bcrypt IAT hook BCryptVerifySignature / BCryptImportKeyPair 同上
封印伪造 + 原版 只伪造封印 → 仍然失败(实验 2),证明密码学有效 反例,非攻击
私钥恢复 需作者签名样本 + nonce 重用 本样本不提供任何签名,不可行
曲线攻击 P-256 无已知有效攻击 不可行

6.6 缺陷严重度汇总

ID 缺陷 CWE 可利用性 严重度
F-2 .data 公钥密文不受完整性保护 CWE-353 Missing Support for Integrity Check 无需私钥、无需调试器 严重
F-1 公钥密钥流可由只读数据重建 CWE-1188 / CWE-656 同上 严重
F-3 授权封印基于用户级 DPAPI CWE-522 / CWE-269 本机任意用户 严重
F-4 判定链单点分支 CWE-693 Protection Mechanism Failure 2 字节补丁 严重
F-5 封印只读不写 CWE-754 削弱 F-1/F-2 的补偿
F-6 反调试仅两个 API 且不主动干预 CWE-693 无阻力

7. 复现步骤

# ── 0. 解包(RAR5, 无密码, 使用 Windows 内置 bsdtar) ──────────────
cd sample/extract && /c/Windows/System32/tar.exe -xvf ../crackme.rar

# ── 1. 静态反汇编 / 反编译 ────────────────────────────────────────
"E:/Tools/MetaSword/Tools/Reverse/IDA/idat64.exe" -B -A crackme.exe
"E:/Tools/MetaSword/Tools/Reverse/IDA/idat64.exe" -A -S"decompile_all.py" crackme.exe
#   注意: 不要附加多余参数(如 -parm.txt), 否则 IDA 以 exit=4 静默失败

# ── 2. 算法复现自检(公钥提取 + 机器码 + FNV, 无需私钥) ────────────
python src/keygen.py --selftest

# ── 3. 注册码独立校验(纯 Python P-256) ────────────────────────────
python src/verify_code.py --demo
python src/verify_code.py --exe keys/crackme_keygen.exe "LX2-<128hex>"

# ── 4. 攻击路径 A: 公钥替换 + 封印伪造 + 生成注册码 ──────────────
python src/keygen.py --run

# ── 5. 攻击路径 B: 2 字节 NOP 补丁 ────────────────────────────────
python src/patch_bypass.py --make

# ── 6. 6 组对照实验 ──────────────────────────────────────────────
python src/patch_bypass.py --evidence

# ── 7. 清理(删除注册表封印并恢复) ────────────────────────────────
python src/keygen.py --restore

8. 给作者的加固建议

按收益/成本排序:

  1. 把公钥移出可写节并做完整性绑定(针对 F-1/F-2)
    • 将公钥(或其哈希)纳入 RSA/ECDSA 自签名;或在 .text 中对公钥做编译期嵌入(立即数),使密钥流派生与公钥存储不可分离;
    • 至少要哈希全部 80 字节(标记 + 1 + 64 密文 + 填充),而非仅前 16 字节。
  2. 封印改用机器级 DPAPI 或作者签名(针对 F-3)
    • CryptProtectData(..., CRYPTPROTECT_LOCAL_MACHINE),或干脆让封印本身是一段作者私钥签名;
    • 更彻底:把"binary hash → 授权"的映射放在作者服务端准入,而不是落在客户端可写的注册表。
  3. 消除单点分支(针对 F-4)
    • 把判定结果用于派生后续数据(如异或密钥、输出解码表),而不是仅做一次 cmp/jnz;
    • 采用多路分散判定 + 早期降级,使补丁必须同时改动多处。
  4. --seal 双向化(针对 F-5)
    • 让模式本身具备写入能力并做幂等/权限校验,或彻底移除该诊断入口(它泄露了内部哈希算法与失败标签)。
  5. 提高反向工程成本(针对 F-6 及整体)
    • 引入反调试主动对抗(硬件断点检测、时序检测);
    • 关键字符串/常量异或加密,避免明文泄露算法名(ECDSA_P256ECCPUBLICBLOB)与失败原因标签。
  6. 降低授权承担的风险
    • 由于客户端离线校验原理上不可能绝对安全,建议叠加:授权有效期、在线激活/吊销、版本号绑定、签名者身份可追索性(每份授权单独签名,泄露后可定位)。

9. 附录

附录 A — 关键地址/偏移速查

VA RVA file offset
start 0x140002B70 0x2B70 0x1B70
CRT 启动 sub_1400029F0 0x1400029F0 0x29F0 0x19F0
主逻辑 sub_140001000 0x140001000 0x1000 0x400
机器指纹 sub_1400022E0 0x1400022E0 0x22E0 0x12E0
SHA-256 sub_140002480 0x140002480 0x2480 0x1480
读 .text sub_1400025B0 0x1400025B0 0x25B0 0x15B0
读封印 sub_140002190 0x140002190 0x2190 0x1190
决定性分支 jnz 0x140001C97 0x1C97 0x1097
ECDSA 验签调用 ~0x140001E8x
.data 完整性标记 0x140027000 0x27000 0x25000
.data 公钥密文(64B) 0x140027011 0x27011 0x25011
.text 节数据 0x140001000 0x1000 0x400(长度 0x19600)

附录 B — 关键伪代码清单

docs/NOTES.mdlogs/ida/decompiled.c。核心三段:

<details><summary>1. 密钥流派生 + 公钥解密(main, VA ≈ 0x140001DA4..0x140001F00)</summary>

v84 = sub_1400025B0(&size);                 // .text raw, size = 0x19600
h = 0x14650FB0739D0383; n = 0;
for (i = 0; i < size && n < 64; ++i) {
    h = 0x100000001B3 * (v84[i] ^ h);
    if (i % (size >> 6) == 0) pbInput[n++] = HIBYTE(h);
}
v89 = xmmword_140027011 ^ pbInput[ 0..15];
v90 = xmmword_140027021 ^ pbInput[16..31];
v91 = xmmword_140027031 ^ pbInput[32..47];
v92 = xmmword_140027041 ^ pbInput[48..63];
*(QWORD*)pbInput = 0x2031534345;            // "ECS1" + 0x20
*(__m128i*)&pbInput[ 8] = v89;
*(__m128i*)&pbInput[24] = v90;
*(__m128i*)&pbInput[40] = v91;
*(__m128i*)&pbInput[56] = v92;
BCryptImportKeyPair(hAlg, 0, L"ECCPUBLICBLOB", &hKey, pbInput, 0x48, 0);
v60 = BCryptVerifySignature(hKey, 0, pbHash, 0x20, pbSignature, 0x40, 0) == 0;

</details>

<details><summary>2. 机器指纹(sub_1400022E0)</summary>

RegGetValueA(HKLM, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid",
             RRF_RT_REG_SZ, NULL, pvData, &cbGuid /*128*/);
GetWindowsDirectoryA(Buffer, 0x104);
RootPathName = Buffer[0];
strcpy(pcbData, ":\\");
GetVolumeInformationA(&RootPathName, 0, 0, &serial, 0, 0, 0, 0);
memcpy(pbInput, "CMX/Machine/v2|", 15);
v4 = strlen(pvData);
memcpy(pbInput + 15, pvData, v4);
pbInput[v4 + 15] = '|';
v5 = snprintf(pbInput + v4 + 16, "%08X", serial);
return sub_140002480(pbInput, v4 + v5 + 16, pbOutput);   // SHA-256

</details>

<details><summary>3. 封印读取(sub_140002190)</summary>

if (!RegGetValueA(HKCU, "Software\\OfflineCrackme", "TextSeal",
                  RRF_RT_REG_BINARY, NULL, pvData, &cbData /*1024*/)) {
    pDataIn = { pvData, cbData };
    if (CryptUnprotectData(&pDataIn, NULL, NULL, NULL, NULL, 0, &pDataOut)) {
        memcpy(v9, pDataOut.pbData, min(pDataOut.cbData, 127));
        LocalFree(pDataOut.pbData);
        v5 = strtoull(v9, NULL, 16);
        *a1 = v5;
        return v5 != 0;
    }
}
return 0;

</details>

附录 C — 提取到的作者公钥

曲    线 : secp256r1 (NIST P-256)
X = 0x2EE69F7760C3811E173FD12A9D144445BDDE11806654F4B1EF5848BE216317F5
Y = 0x9D6D2FD3897BC6B00D70B0531CAB00CB91FAC9E6ADD5201C94D532F55C3EA6F4
raw X||Y (64 B, hex):
  2ee69f7760c3811e173fd12a9d144445bdde11806654f4b1ef5848be216317f5
  9d6d2fd3897bc6b00d70b0531cab00cb91fac9e6add5201c94d532f55c3ea6f4
私钥     : 不可得(ECDLP)

附录 D — 本机授权数据(实测样本)

MachineGuid  = d863bef0-aed9-4a1f-82a5-a9a62318e7d5
C: 卷序列号  = 0x1A8455F9
机器码       = MX2-6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
TextSeal     = 0x2d3117e8dd257598
签名消息     = CMX/Verify/v2|6E2ADBE3D09691AB36BB61F94DA2A251DB898FB173BAB3755C865B39D8E9E9FB
z = SHA-256  = 2999451ce8d57c506ca81675e62ffc5cb042c292622c229c158518ca227a17bf

附录 E — 术语与缩写

缩写 说明
IAT 导入地址表
Raw r‖s / P1363 ECDSA 签名的一种定长编码(两个 32 字节大端整数拼接),区别于 DER/ASN.1
DPAPI Windows 数据保护 API;CryptProtectData / CryptUnprotectData
BCRYPT_ECCKEY_BLOB bcrypt 的 ECC 公钥二进制结构(magic + cbKey + X + Y)
TextSeal 本样本自定义名词:.text 段的自校验哈希值
ECDLP 椭圆曲线离散对数问题

报告结束。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-20 05:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表