吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1271|回复: 6
上一主题 下一主题
收起左侧

[CrackMe] 超高难度crackme,欢迎各位大佬来挑战

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

  ## Obsidian VM Crackme

  ## 基本信息

  - 程序名称:Obsidian VM
  - 运行平台:Windows x64
  - 编写语言:C++20
  - 编译器:LLVM Clang / LLVM-MinGW
  - 程序架构:64 位 PE
  - Access Token 长度:32 字符
  - 难度定位:高难度

  ## 加壳情况

  本程序已加壳。

  - 使用壳:UPX 5.2.0
  - 压缩算法:LZMA
  - 压缩参数:--best --lzma
  - 加壳前大小:460,288 字节
  - 加壳后大小:142,336 字节

  UPX 主要作为第一层静态分析障碍。分析者通常需要先识别并脱壳,才能更方便地研究内部校验逻辑。

  ## 混淆机制

  程序包含多种代码与数据混淆措施。

  ### 自定义虚拟机

  Access Token 不由普通字符串比较函数直接验证,而是交给一个自定义字节码虚拟机处理。

  虚拟机具有独立的:

  - 操作码
  - 程序计数器
  - 输入游标
  - 虚拟寄存器
  - 状态混合指令
  - 终止指令

  核心验证逻辑被拆分为多条虚拟指令,增加静态分析和反编译难度。

  ### 操作码加密

  虚拟机操作码没有以明文形式直接保存。每条操作码均通过动态变化的密钥编码,解码结果依赖:

  - 当前密钥
  - 操作码位置
  - 前一个编码字节

  这会干扰直接搜索操作码、线性扫描和简单的字节码提取脚本。

  ### 状态耦合校验

  输入内容会依次进入三个相互关联的 64 位状态寄存器。

  每轮计算包含:

  - 异或
  - 64 位循环移位
  - 整数乘法
  - 索引扰动
  - 状态间交叉依赖

  后续状态依赖前一状态的计算结果,因此不能简单地逐字符独立比较。

  ### 常量隐藏

  正确 Access Token 不以明文字符串形式存放在程序中。

  最终状态常量经过掩码处理,程序只在运行过程中恢复用于比较的目标值。直接使用 strings 等工具无法获取正确 Token。

  ## 花指令

  程序包含花指令和反汇编干扰数据。

  主要形式包括:

  - 永远不会成立的不透明条件
  - 不可达的伪代码分支
  - 虚假状态运算
  - 无效寄存器扰动
  - 容易干扰线性反汇编的字节序列
  - 伪造的 CALL 和跳转字节组合

  这些内容不会影响正常执行,但可能导致反汇编器产生错误指令边界、虚假控制流或无意义伪代码。

  ## 反调试

  程序具备基础反调试能力,包括:

  - IsDebuggerPresent
  - CheckRemoteDebuggerPresent
  - 调试状态参与最终验证

  反调试结果不会单独显示警告,而是用于污染最终比较结果。在调试器环境中,即使输入正确,也可能显示 Token 无效。

  ## 编译保护

  发行版本启用了多项编译和链接保护:

  - -O3 高级优化
  - LTO 链接时优化
  - 符号剥离
  - 无用代码与数据节清理
  - 栈保护
  - DEP / NX
  - ASLR / Dynamic Base
  - 静态链接主要 C++ 运行组件

  优化和 LTO 会改变函数结构、内联部分逻辑,并降低源码结构与反编译结果之间的对应程度。

  ## 挑战目标

  分析者需要完成以下工作:

  1. 识别并处理 UPX 壳。
  2. 定位真正的输入校验入口。
  3. 绕过或处理反调试逻辑。
  4. 区分真实控制流、虚假分支和花指令。
  5. 分析加密的虚拟机操作码。
  6. 还原三个耦合的 64 位状态算法。
  7. 求出能够产生正确终态的 32 字符 Access Token


欢迎大家crack

本帖子中包含更多资源

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

x

免费评分

参与人数 2吾爱币 +2 热心值 +1 收起 理由
lingyun011 + 1 + 1 热心回复!
peiki + 1 我很赞同!

查看全部评分

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

推荐
xbc66 发表于 2026-7-20 13:02
这篇文章记录了我对 52pojie 论坛上发布的 Obsidian VM Crackme 的完整逆向分析过程。这是一个定位为高难度的 Windows x64 逆向挑战,要求分析者还原一个自定义虚拟机并求出 32 字符的 Access Token。

一、样本概览

程序名称:Obsidian VM
编写语言:C++20
编译器:LLVM Clang / LLVM-MinGW
运行平台:Windows x64
架构:64 位 PE
壳:UPX 5.2.0,压缩算法 LZMA,参数 --best --lzma
加壳前大小:460,288 字节
加壳后大小:142,336 字节
Access Token 长度:32 字符
镜像基址:0x140000000

二、脱壳

UPX 5.2.0 的壳很好处理,直接 upx -d 脱壳即可。如果用调试器手动脱,追到末尾的 popad 后的 jmp 指令,目的地就是原始入口点 start(0x1400013D0)。

三、定位输入校验入口

脱壳后在 IDA 中搜索字符串,很快找到三处关键提示:

0x140067D28  "Obsidian VM / access token: "
0x140067D45  "[+] vault unlocked"
0x140067D59  "[-] invalid token"

交叉引用全部指向同一个函数 sub_14005B190(0x14005B190),它负责读取 stdin 输入,构造 std::string,然后调用 sub_14005BB10 做校验。

sub_14005BB10 入口处第一段代码就直接判长度:

cmp  qword ptr [rcx+8], 0x20   ; 输入必须恰好 32 字节
jnz  reject

不足 32 字节直接判错,没有商量的余地。

四、反调试

在 sub_14005BB10 内部,最后比较前会调用 sub_14005BD20:

CheckRemoteDebuggerPresent(GetCurrentProcess(), &flag);
result = (flag | IsDebuggerPresent()) != 0;

这个返回值被 OR 进最终结果。也就是说即使你输入了正确的 token,只要调试器还在跑,程序就告诉你 token 无效。反调试不是独立生效的,它被巧妙地做了器化——污染校验结果而不是弹窗警告,第一次遇到的人很容易以为是 token 不对而反复尝试。

绕过很简单:在 0x14005BBBC 处把 movzx eax, al 改成 xor eax, eax(机器码 31 C0),让反调试结果恒为零。或者在调试器里把 PEB 的 BeingDebugged 字段清掉。

顺便说一下 sub_14005C080,它只是 __stack_chk_fail——栈 canary 被破坏时触发,不是反调试组件。

五、花指令与虚假控制流

sub_14005BBF0 是 VM 的核心循环函数。循环体内在每字节处理完之后有一段代码:

v11 = 0xD1B54A32D192ED03
...
if ((v11 & 0xF) == 2)
    call sub_14005BD80

进入 sub_14005BD80(0x14005BD80):

push rax
mov  rax, [rcx]     ; rax = *v11
or   rax, 1         ; 强制最低位为 1
imul rax, rax       ; rax = (v11|1)^2
mov  [rsp+8+var_8], rax
mov  rax, [rsp+8+var_8]
and  eax, 7
cmp  eax, 2
jnz  skip           ; 不相等则跳转
call near ptr loc_14005BD9C+4   ; 花指令: call 落在一个 jmp 上
jmp  loc_14005BD9C              ; 自旋
skip:
mov  rax, [rcx]
rol  rax, 7
xor  [rcx], rax     ; v11 ^= rol(v11, 7)
pop  rax
retn

这里做了一件很巧妙的事:v11 被强制设为奇数后平方,而奇数平方的二进制最低三位永远是 001,也就是 &7 永远等于 1,绝不可能等于 2。因此那个 call 指令永不被执行,是不透明谓词的典型应用。

实际每次执行的代码只是 v11 ^= rol(v11, 7),修改一个本地变量,对 s1/s2/s3 三状态完全无影响。所以整段花指令可以直接忽略,不影响校验逻辑。

六、所谓的“虚拟机操作码”

按照题目描述,程序内有一个自定义字节码虚拟机,操作码通过动态密钥加密,解码依赖当前密钥、操作码位置和前一个编码字节。听上去相当唬人。

实际反汇编 sub_14005BBF0 之后发现,VM 就是一个迭代 32 次的循环。一个 context buffer V 在栈上,布局如下:

V+0x00  codeptr    指向 32 字节输入数据
V+0x08  codelen    32(输入长度)
V+0x10  状态 s1    64 位
V+0x18  状态 s2    64 位
V+0x20  状态 s3    64 位
V+0x28  pc         程序计数器
V+0x30  cur        当前字节

每次循环:

movzx edx, byte ptr [rdx+rcx]   ; 从输入读一个字节
mov   [rdi+30h], dl              ; 存入 cur
; --- 然后驱动三状态更新 ---

没有操作码调度表,没有分支,没有 halt 指令。所谓的“操作码加密”就是前面那段 v11 花指令混淆的文学化描述。32 个输入字节本身就是 VM 的 32 条指令,直接逐字节进入状态机混合。

七、三状态算法的完整还原

7.1 常量

四个常量全部来自 SplitMix64 和 SipHash 家族:

C1 = 0x9E3779B97F4A7C15   SplitMix64 黄金比例 gamma
C2 = 0xBF58476D1CE4E5B9   SplitMix64 finalizer 第一乘子
C3 = 0x94D049BB133111EB   SplitMix64 finalizer 第二乘子
C4 = 0x2545F4914F6CDD1D   SipHash 偏移常量

7.2 每字节的状态更新(汇编级确认,非反编译器猜测)

对于每个输入字节 b(下标 i 从 0 到 31):

s1 = rotl64(s1 ^ (i * C1 + b), 13) * C2        (1)
h  = (uint8_t)s1                                  (取低 8 位)
s2 = rotl64(s2 + (h ^ b) * C3, 17)             (2)
s3 = rotl64(s3 ^ (s2 + b * C4), 29)            (3)

rotl64(x,n) = ((x << n) | (x >> (64-n))) & 0xFFFFFFFFFFFFFFFF,n 自动对 63 取余。

注意这是链式耦合:s2 依赖 s1 的低 8 位,s3 依赖完整的 s2。三个状态不能独立计算,必须按顺序串行。这是题目强调“后续状态依赖前一状态的计算结果”的具体体现。

7.3 种子(初始状态)

sub_14005BB10 在调用 VM 前把以下三个 64 位常量写入 context:

s1_seed = 0x243F6A8885A308D3
s2_seed = 0x13198A2E03707344
s3_seed = 0xA4093822299F31D0

这三个值是 π 的小数部分的十六进制表示的前 24 个字节(24 个 hex digits × 4 bits = 96 bits,对应 π 的二进制前 96 位)。这是验证输入正确性的唯一参照起点。

7.4 目标(正确终态)

sub_14005BB10 末尾用三条 XOR 指令与常量比较:

mov  rax, 0x18815E9762BE90CC
xor  rax, [rsp+var_30]           ; var_30 = s1_final
mov  rcx, 0x4FBF764270C8D81A
xor  rcx, [rsp+var_28]           ; var_28 = s2_final
or   rcx, rax
mov  rdi, 0x67945584CF762BBF
xor  rdi, [rsp+var_20]           ; var_20 = s3_final
or   rdi, rcx
call sub_14005BD20                ; 反调试
movzx eax, al
or   rax, rdi
setz al                           ; 所有条件清零时 al=1

正确终态常量:

s1_target = 0x18815E9762BE90CC
s2_target = 0x4FBF764270C8D81A
s3_target = 0x67945584CF762BBF

八、正向验证

为了确认公式还原无误,我用 Python 实现了正向状态机。关键验证:

H(b"") = (0x243F6A8885A308D3, 0x13198A2E03707344, 0xA4093822299F31D0)

空输入精确返回三个种子值,证明实现与汇编一致。完整实现保存在 obsidian_forward.py。

九、能否求出 Access Token?

这是整个挑战中最困难的部分,也是我目前未能彻底完成的一步。

9.1 数学结构

输入 32 字节 = 256 bits 的自由度。
约束 3 × 64 bits = 192 bits 的目标状态。
差值 = 64 bits,意味着在全字节空间(0-255)中大约存在 2^64 个有效 token。

9.2 单步可逆但整体不可逆

每个字节的变换在给定字节 b 和后期状态 (s1',s2',s3') 时是完全可逆的:

s1_old = ror64(s1' * inv(C2), 13) ^ (i * C1 + b)
h      = (uint8_t)s1'
s2_old = ror64(s2', 17) - (h ^ b) * C3
s3_old = ror64(s3', 29) ^ (s2' + b * C4)

因此从目标终态向后回推时,每个字节 b(0 到 255 任选)都能算出唯一的前驱状态。问题是没有任何中间状态可以剪枝——只有回推到第 0 步并恰好命中种子 (π1,π2,π3) 时才知道找对了路。在无剪枝的情况下这是 256^32 的树,等同于单向搜索。

9.3 尝试过的方法

z3-solver(Python, bit-vector 64-bit):
- 任意字节(0-255),60 秒返回 unknown
- 可打印 ASCII(33-126),1100 秒返回 unknown
- s1-轨迹参数化(用 s1 中间值替代字节变量),5 万秒无解

候选字符串猜测:用 ObsidianVM、flag{、vault_unlocked 等几十个前缀拼凑至 32 字符并计算正向哈希,均不命中。

9.4 根本原因

这个散列是 SplitMix64/SipHash 家族的设计——经典的 avalanche 型变换,有意识地让输入的任何单比特变化引发输出的大面积翻转。乘加轮转的组合在 SMT 求解器面前是高度非线性的,而对于暴力枚举来说 2^192 的搜索空间过于庞大。

作者本人生成这个 token 是可行的(随机选一个 token,正向跑 32 轮得到终态,把终态硬编码进程序即可),但从终态反推 token 在无额外信息的情况下等价于原像攻击。

十、关键地址速查表

sub_14005B190        0x14005B190   main wrapper(stdin 读取,调用校验)
sub_14005BB10        0x14005BB10   token 校验函数
sub_14005BBF0        0x14005BBF0   VM 核心循环(状态机实现)
sub_14005BD20        0x14005BD20   反调试检查
sub_14005BD80        0x14005BD80   花指令/junk 处理器(可忽略)
sub_14005C080        0x14005C080   __stack_chk_fail(栈保护,非反调试)
"Obsidian VM /..."   0x140067D28   stdout 提示
"[+] vault unlocked" 0x140067D45   成功字符串
"[-] invalid token"  0x140067D59   失败字符串

C1  0x9E3779B97F4A7C15   黄金比例 gamma
C2  0xBF58476D1CE4E5B9   fmix 乘子 1
C3  0x94D049BB133111EB   fmix 乘子 2
C4  0x2545F4914F6CDD1D   SipHash 常量

s1_seed    0x243F6A8885A308D3   π hex 1
s2_seed    0x13198A2E03707344   π hex 2
s3_seed    0xA4093822299F31D0   π hex 3

s1_target  0x18815E9762BE90CC   目标 1
s2_target  0x4FBF764270C8D81A   目标 2
s3_target  0x67945584CF762BBF   目标 3

十一、总结

任务 1 到 6(脱壳、定位入口、绕过反调试、区分花指令、分析操作码加密、还原三状态算法)已经完整完成。散列公式经正向验证确认无误,所有常量、种子、目标值均已提取并确认。

任务 7(求出 32 字符 Access Token)在当前已知信息下无法在合理时间内完成。这个挑战的作者通过一个 192-bit 的哈希锁定了一个只有他自己知道答案的 token——而逆向分析到这步,也已经把整个程序的校验逻辑完整吃透了。如果哪天出题人或者某个解题者公布了那串 32 字符,往 obsidian_forward.py 里一喂就能验证真伪。
obsidian_forward.py:C1=0x9E3779B97F4A7C15;C2=0xBF58476D1CE4E5B9;C3=0x94D049BB133111EB;C4=0x2545F4914F6CDD1D
M=0xFFFFFFFFFFFFFFFF
def rol(x,n):
    n&=63
    if n==0: return x
    return ((x<<n)|(x>>(64-n)))&M
def H(inp):
    s1,s2,s3=0x243F6A8885A308D3,0x13198A2E03707344,0xA4093822299F31D0
    for i,b in enumerate(inp):
        v6=b; v5=i
        s1=(rol((s1^((v5*C1+v6)&M)),13)*C2)&M
        h=s1&0xFF
        s2=rol((s2+((h^v6)*C3)&M)&M,17)
        s3=rol((s3^((s2+v6*C4)&M))&M,29)
    return s1,s2,s3
if __name__=="__main__":
    print("pi seeds -> H of empty:", [hex(x) for x in H(b"")])
    print("A"*32, [hex(x) for x in H(b"A"*32)])
    print("0"*32, [hex(x) for x in H(b"0"*32)])
    print(hex(0x18815E9762BE90CC),hex(0x4FBF764270C8D81A),hex(0x67945584CF762BBF))
else:
    def verify(tok):
        r=H(tok.encode())
        print("verify", repr(tok), [hex(x) for x in r])
        return r
3#
lbdcyy 发表于 2026-7-18 08:09
看看也是进步啊

免费评分

参与人数 1吾爱币 -20 违规 +1 收起 理由
勇者为王 -20 + 1 CM版块禁止回复『与主题无关非技术内容』,违者重罚!

查看全部评分

4#
tvrc330 发表于 2026-7-18 09:08
没得悬赏没动力啊,兄弟。

免费评分

参与人数 1吾爱币 -20 违规 +1 收起 理由
勇者为王 -20 + 1 CM版块禁止回复『与主题无关非技术内容』,违者重罚!

查看全部评分

5#
Sound 发表于 2026-7-19 16:51
Unpackme? crackme? reverse me? keygenme?  你这属于哪一个? 怪不得没人玩。
6#
xbc66 发表于 2026-7-20 12:46
1-6都完成了,但是你这个7是不是有点问题啊,这不是数学上的原像攻击吗?要是谁资助我一台超算就能算出来了
7#
xbc66 发表于 2026-7-20 12:56
7的散列是 SplitMix64/SipHash 家族的 avalanche 型变换,单项不可逆,这你叫我怎么破?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-7-20 20:45

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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