吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2608|回复: 9
收起左侧

[CrackMe] (C++20) - 基于语言标准的crackme (最终版比肩VMP壳)

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

本帖最后由 sakura-galaxy 于 2026-1-20 11:56 编辑

大家好,
这个是C++20系列的crackme,总共有三个crackme,不同于简单加壳或混淆的crackme,这个系列希望通过C++的一些语言特性与代码逻辑实现。
整个系列开放源代码在github/Tokisaki-Galaxy/crackme99 ,如果没有思路可以去看看源代码。接下来时间会在站内放出制作思路。
破解目标是密钥或者输入任意字符达到验证成功。三个crackme均存在密钥,并且密钥很简单可以被短时间暴力碰撞出来。

另外全系列crackme难度跨度较大(具体是beta到gamma,难度跨度非常大),Gamma版本已经几乎达到了商业VMProtect的水平。

Alpha版本
设计上是系列最简单的crackme,但是只喜欢F10的宝宝会疯的。
(其实设计上有点荒谬的233)
在破解的时候,若看到stdxxx就不要再往下追了,是C++标准库。
Alpha版本的目标不同于其他版本,最好可以实现逆向自定义字节码并绕过编译时字符串加密(如果目标是混过关,只是考验基本功)。
困难点在于
内部的 VM 调度器使用协程实现。线性执行流被粉碎成堆分配的帧,使标准的“单步跳过 (F10)几乎不可能
一打开 IDA,会看到一堆乱七八糟的 STL 符号(std::_Visit_helper, std::coroutine_handle),代码量比实际逻辑大 50 倍。
Alpha 版本的真正威力不在于它的校验算法多难,而在于通过 C++20 特性建立了一道不同于其他crackme的理解障碍

Beta版本
设计上论坛上一些高手能达到的水平,新手可能完全没有头绪。
困难点在于
挑一些能说的,还有一些要自己发现才好玩。
标准的控制流图几乎完全失效,没有if-else判断,几乎都是throw/catch处理
里面有某些反调试方法


Gamma版本
这个系列的终极形态
设计时候没有想过有人能不参考源代码纯解出来(不过也有可能有大佬没考虑到,其实夸张了),理论上应该是密码学安全的。强度应该与VMProtect类似。
这个完全是炫技的东西。
困难点在于
多态指令流,代码-数据纠缠(指令不以静态形式存在于二进制文件中)
无崩溃执行,即使输入错误的密钥,VM 也会在安全沙箱内执行“垃圾”逻辑。没有任何崩溃来帮助你定位错误。

全系列无壳,Alpha和Beta版本如果还给出pdb就太简单了,所以只在Gamma版本给出pdb。(如果能不参考pdb和源代码解出来那真的是超级无敌厉害了)
希望大家玩得开心,一会发教程。

以下是x64版本




以下是x32版本


如果无法访问论坛附件链接下载,也可以通过github的release下载(github/Tokisaki-Galaxy/crackme99)

本帖子中包含更多资源

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

x

免费评分

参与人数 4吾爱币 +4 热心值 +4 收起 理由
BrutusScipio + 1 + 1 用心讨论,共获提升!
156608225 + 2 + 1 用心讨论,共获提升!
jaffa + 1 谢谢@Thanks!
hewok + 1 + 1 用心讨论,共获提升!

查看全部评分

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

wangnet 发表于 2026-1-19 17:05
赞一个,感谢~·

免费评分

参与人数 1吾爱币 -15 违规 +1 收起 理由
bian96 -15 + 1 警告:CM区等技术板块禁止回复与主题无关非技术内容,违者重罚!

查看全部评分

techliu 发表于 2026-1-20 11:14
学习一下

免费评分

参与人数 1吾爱币 -15 违规 +1 收起 理由
bian96 -15 + 1 警告:CM区等技术板块禁止回复与主题无关非技术内容,违者重罚!

查看全部评分

化身千万 发表于 2026-1-20 13:12
本帖最后由 化身千万 于 2026-1-20 13:16 编辑

有趣有趣,得从源代码学习一下你对cpp 20 的用法了,开开眼界
——————
源代码已经看了,的确感触良多,编译期的计算,居然能影响这么多东西,学无止境啊

————
最后,请问一下你源代码中虚拟机这一块该怎么理解?对于虚拟机我一点都不了解,其他的都看明白了
 楼主| sakura-galaxy 发表于 2026-1-20 13:46
本帖最后由 sakura-galaxy 于 2026-1-20 13:48 编辑
化身千万 发表于 2026-1-20 13:12
有趣有趣,得从源代码学习一下你对cpp 20 的用法了,开开眼界
——————
源代码已经看了,的确感触良 ...

写C++20的这个crackme是感觉,好多人还抱着老旧的知识和标准,对软件保护仅限于普通的虚拟机保护和加壳保护,基本上没怎么重视纯代码逻辑上的玩法,想让大家看看纯粹从逻辑上能玩到什么程度。
另外大致解释一下虚拟机,这里的虚拟机(crackme和软件保护领域),不同于java和vmware那种,不是为了跨平台或者兼容,而是为了让代码变得不可读。(附带效果是运行缓慢)
相当于你发明了一门谁也听不懂的外星语言(自定义字节码 OpCode)。并且写了一个翻译官(虚拟机解释器 Dispatcher)。程序运行时,翻译官拿出一张写满外星语的纸,翻译一句,执行一句。这就导致破解者虽然能看到“翻译官”在干活,但他不知道“外星语”每个词是什么意思。他必须先查出这门语言的语法(逆向 ISA 指令集),才能理解程序在做什么。

三个版本的crackme虚拟机也有强度差异
Alpha:混淆执行流。利用协程让你找不着翻译官在哪。
Beta对抗调试器。利用异常和时间检测,实现一调就错,一看就断。
Gamma:数学性闭环。
如果想知道更多虚拟机的东西,比如每个版本的虚拟机里面具体技术细节(干了什么),等我有时间完整写个文章来找你

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
化身千万 + 1 + 1 好的,关注你了
evea + 1 + 1 谢谢@Thanks!

查看全部评分

BrutusScipio 发表于 2026-1-22 00:47
sakura-galaxy 发表于 2026-1-20 13:46
写C++20的这个crackme是感觉,好多人还抱着老旧的知识和标准,对软件保护仅限于普通的虚拟机保护和加壳保 ...

有栈协程应该容易调试?
自定义翻译这个问题的话,密文够多总会发现一些规律,除非每次加入某个随机值初始化
 楼主| sakura-galaxy 发表于 2026-1-22 09:18
BrutusScipio 发表于 2026-1-22 00:47
有栈协程应该容易调试?
自定义翻译这个问题的话,密文够多总会发现一些规律,除非每次加入某个随机值初 ...

对的,你对于密文和明文可能是一一对应的破解思路很对!所以在后面版本里面,尤其是gamma版本里面,加入了动态对应,加密方式与输入相关。类似恩格斯密码机那种,让每一个密文在第一次对应add,第二次就对应cmp,第三次可能就对应jmp,完全无法静态分析对应关系。
156608225 发表于 2026-1-24 16:34
这里协程也被当作一种复杂的jmp来使用了,跟利用异常跳转的意思差不多
hxr1023 发表于 2026-10-31 23:43
本帖最后由 hxr1023 于 2026-10-31 23:45 编辑

对32位Gamma进行分析。思路是沿着输出代码往回找,确认文字的生成方式,再把解密数据的来源换掉。原来的虚拟机照跑,补丁只接管最后的输出解密。测试用了 `test`、`wrong-key` 和空输入,三组都能显示目标字符串。

整个改动集中在三处:输出循环的一处 XOR、`.text` 尾部的一块填充区,以及节表里的 `VirtualSize`。下面按制作过程展开,两个中间版本出错的地方也一并留着。

## 从输出循环下手

程序收到一行输入后,先算散列,再跑自定义 VM。输出用的不是直接存放的成功字符串,而是一段密文,解密时会读取 VM 的寄存器。

找到的输出循环在这里,以下地址都按默认基址写:

```asm
00404855  mov  dl, byte ptr [edx+edi]
00404858  mov  eax, edi
0040485A  mov  ebx, dword ptr [esi+40h]
0040485D  and  eax, 0Fh
00404860  xor  dl, byte ptr [ecx+eax*8]
00404863  mov  ecx, ebx
00404865  mov  dword ptr [ebp-24h], ecx
```

`EDI` 是输出索引,`DL` 刚取到一个密文字节。`EAX` 经 `and 0Fh` 后只剩 0—15,再到 `ECX` 指向的虚拟寄存器数组里取值。每个寄存器占 8 字节,所以地址里有 `eax*8`;实际参与 XOR 的只有低字节。

这段对应的关系是:

P = C XOR (R[i & 15] & 0xff)

补丁就选在 `0x404860`。不改 VM 怎么算,只把这次读内存换成读取补丁里的 16 字节表。这样前面的输入散列、VM 运算和等待流程都还在,最后输出不再受它们算出的寄存器值影响。

## 把解密表算出来

程序里的密文共 45 字节:

```text
69 45 50 4d 58 5f 5e 5f c0 4b 5e 43 45 44 59 8d
0a 7e 56 4f 0a 79 4b 47 c1 4b 0a 49 45 58 4f 8c
43 59 1e 4e 43 4d 59 45 c0 5c 4f 4e 04
```

准备显示的是这句:

```text
Congratulations! The Gamma core is dissolved.
```

拿密文和这句明文逐字节异或,反算解密数据:

```text
K[i & 15] = C XOR P
```

前两项:

```text
0x69 XOR 0x43 ('C') = 0x2a
0x45 XOR 0x6f ('o') = 0x2a
```

整句算完,得到下面的循环表:

```text
2a 2a 3e 2a 2a 3e 2a 2a ac 2a 2a 2a 2a 2a 2a ac
```

45 个位置按模 16 检查了一遍,没有互相冲突的项。补丁里直接放这 16 字节就够了。

这里用的是已选定的输出文字反推解密表,没有得到输入密码。后面的补丁也是围绕这张表做的。

## 原位置只留一个 JMP

要覆盖的两条指令,机器码一共 5 字节:

```asm
32 14 C1    xor dl, byte ptr [ecx+eax*8]
8B CB       mov ecx, ebx
```

正好能放一个 `E9 rel32`。新代码放到 `.text` 尾部,做完 XOR,再补上被盖掉的 `mov ecx,ebx`,最后回到 `0x404865`。

选中的位置是 VA `0x404f70`。它在 `.text` 原有有效范围之后的填充里,从对应文件偏移 `0x4370` 开始,35 字节全为 0。该节有读、执行属性,填充区也没越过它的原始数据边界,所需代码和表都装得下。

节表里相关数值如下:

```text
VirtualAddress   = 0x1000
PointerToRawData = 0x400
SizeOfRawData    = 0x4000
VirtualSize      = 0x3f65
```

两处地址换成文件偏移:

```text
原位置:
VA  = 0x404860
RVA = 0x404860 - 0x400000 = 0x4860
FOA = 0x400 + (0x4860 - 0x1000) = 0x3c60

洞穴位置:
VA  = 0x404f70
RVA = 0x4f70
FOA = 0x400 + (0x4f70 - 0x1000) = 0x4370
```

调试器里按实际模块基址加 RVA 定位;写文件用上面的 FOA。这里没把其他位置的全零区当作候选,只用了这段节尾填充。

入口跳转的位移:

```text
rel32 = 目标 - 下一条指令
      = 0x404f70 - (0x404860 + 5)
      = 0x70b
```

所以 `0x3c60` 处改成:

```text
E9 0B 07 00 00
```

两条原指令都是完整覆盖,回跳点定在它们后面的 `0x404865`。

## 第一版:地址写死,运行就异常

早期版本在洞穴里直接用了类似 `[eax+0x404f7d]` 的绝对地址。文件改完能写出来,实际运行却只打印了横幅和输入提示,随后访问异常,退出码 `3221225477`。

回头看新增代码,那个绝对地址没有对应的重定位项。模块若被 ASLR 挪走,它仍会访问旧位置。当时没有记录进程的实际加载基址,所以这次异常的直接原因没有完整取证,但这处寻址本身确实有问题,继续保留它没有意义。

后面改用 CALL/POP 取当前位置,解密表也跟代码摆在一起,不再往新增指令里塞绝对 VA。

最终保留下来的洞穴代码是:

```asm
00404F70  push ecx
00404F71  call 00404F76
00404F76  pop  ecx
00404F77  xor  dl, byte ptr [ecx+eax+0Dh]
00404F7B  pop  ecx
00404F7C  mov  ecx, ebx
00404F7E  jmp  00404865

00404F83  ; 16字节解密表
```

`E8 00 00 00 00` 调到下一条指令,压栈的返回地址就是 `0x404f76` 对应的运行时地址。`pop ecx` 取出它,后面按相对距离访问表。整个模块换个基址,代码和表的间距不变,两个 JMP 的相对位移也不变,因此不需要给新增代码补绝对地址的重定位。

原 XOR 按 8 字节寄存器槽寻址,新表每项只占 1 字节,所以补丁里是 `ecx+eax+0x0d`,不再乘 8。

## 第二版:没崩了,文字还是错的

换成相对寻址后,中间版本把表的距离写成了 `0x0e`。这次程序能正常结束,退出码 0,不过输出仍然是乱码。

按最终布局重新算了一遍:

```text
CALL返回地址:0x404f76
表起点:     0x404f83

0x404f83 - 0x404f76 = 0x0d
```

多算了一个字节。把 XOR 的位移改成 `0x0d` 后,输出才对上。

这里的 19 字节代码布局也就固定了下来:

| VA | 字节 | 内容 |
|---|---|---|
| `0x404f70` | `51` | `push ecx` |
| `0x404f71` | `e8 00 00 00 00` | `call 0x404f76` |
| `0x404f76` | `59` | `pop ecx` |
| `0x404f77` | `32 54 01 0d` | `xor dl,[ecx+eax+0x0d]` |
| `0x404f7b` | `59` | `pop ecx` |
| `0x404f7c` | `8b cb` | `mov ecx,ebx` |
| `0x404f7e` | `e9 e2 f8 ff ff` | `jmp 0x404865` |
| `0x404f83` | 16字节 | 解密表 |

回跳的位移按 JMP 结束位置算:

```text
0x404865 - (0x404f7e + 5)
= 0x404865 - 0x404f83
= -0x71e
```

写成有符号 32 位小端数就是 `e2 f8 ff ff`,加上 E9 得到 `E9 E2 F8 FF FF`。脚本用 `struct.pack("<i", -0x71e)` 生成。

寄存器和栈也在这个版本一起核对了。入口的 JMP 覆盖了原 MOV,洞穴里的 `mov ecx,ebx` 就负责把它补回来。设进入补丁时 ESP 为 S:

```text
push ecx      S-4   保存原ECX
call          S-8   压入返回地址
pop ecx       S-4   取运行时位置
xor           S-4
pop ecx       S     恢复原ECX
mov ecx,ebx   S     执行被覆盖的MOV
jmp           S
```

回到 `0x404865` 时,DL 是新解出的字节,ECX 等于 EBX,EAX、EBX、ESI、EDI、EBP 保持预期值,ESP 回到 S。

这段还能缩短:第二个 POP 恢复的 ECX 随即被 MOV 覆盖。但现有版本已经验证,文章和脚本保留相同字节,不另做一份优化版。CALL/POP 的做法也只在当前样本环境下验证,没有扩展测试额外返回地址保护。

## 把节尾的声明一起改掉

洞穴的代码加表一共 35 字节,末尾 RVA 为:

```text
0x4f70 + 35 = 0x4f93
```

`.text` 从 RVA `0x1000` 开始,新 `VirtualSize` 应为:

```text
0x4f93 - 0x1000 = 0x3f93
```

文件偏移 `0x200`,由 `65 3f 00 00` 改成 `93 3f 00 00`。这里从新内容末尾重新算长度,包含了原有效范围到洞穴起点之间的空隙,不是在旧 `VirtualSize` 上直接加 35。

`SizeOfRawData=0x4000` 已经足够,文件仍是 26624 字节。入口点、导入表、下一节布局以及 `SizeOfImage=0xa000` 都没动。样本没有 Authenticode 证书,原 PE CheckSum 为 0,这次没有重算。

最终文件修改只有下面三块:

| 文件偏移 | 原内容 | 修改后 |
|---|---|---|
| `0x3c60` | `32 14 c1 8b cb` | `e9 0b 07 00 00` |
| `0x4370` | 35字节的0 | 洞穴代码和解密表 |
| `0x200` | `65 3f 00 00` | `93 3f 00 00` |

洞穴完整字节:


51 e8 00 00 00 00 59 32 54 01 0d 59 8b cb e9 e2
f8 ff ff 2a 2a 3e 2a 2a 3e 2a 2a ac 2a 2a 2a 2a
2a 2a ac
idontkonw 发表于 2026-11-4 08:14
Alpha和Beta都是洒洒水,Gamma是真上强度了
算法不过关被吊打, 扔给deepseek也没解出来,如果想不到巧妙的办法缩小搜索空间几乎没有办法穷举
止步于FNV seed = 0x207be042e01e40ca
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-11-7 05:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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