下面是网上的160个CrackMe的部分逆向笔记,包括逆向思路、注册机实现和逆向中常用的知识整理
CrackMe003:NAG窗口(4C法)和浮点计算注意:逆向前一定要先操作一下软件,熟悉软件的运行现象;逆向一定要自己操作一遍,看是很难看会的(高手除外)
CrackMe002和CrackMe003是同一个作者用VB写的小程序;CrackMe003多了一个NAG窗口,难度也不是很大
1.现象运行软件,会先弹出一个NAG窗口,什么也不能操作,几秒钟后会弹出验证用户名和序列号的主窗口
要求:需要去掉NAG窗口和逆向出正确的序列号
前置知识
- 基地址:PE文件加载到内存后被称为映像文件(Image File),映像文件优先载入的地址是由PE文件中的ImageBase决定(相对地址),默认是0x400000
- 第一行代码地址:是由PE文件的AddressOfEntryPoint(即脱壳中的EP)决定,默认是0x01000
因此,正常的程序在0x401000(= 0x400000 + 0x01000)开始执行第一行代码
- 1.猜测
程序的NAG几秒钟后会自动消失,猜测可能是类似定时器的机制控制的
注意:逆向就是要大胆猜测,然后实验猜测是否正确?有时要实验很多种才会成功,其实这与调试程序bug是一样的思路
- 2.实验
在内存窗口中,先定位到0x401000位置,然后Ctrl+B搜索Timer(Timer是VB程序默认的定时器变量),搜索结果如下:
VB中计时器的单位是ms,上图中0x1B58 = 7000ms = 7s;重新运行程序,看一看是不是大概7s时间,NAG窗口消失,发现果然是,说明猜测是正确的(可以说是运气好,但是只有逆向多了才会有这种好运气的)
- 3.破解思路
因此,Timer思路就是将0x1B5改成一个很小的值(比如0x001);现象就是NAG一闪而过,很难被捕捉到,给人的感觉就是NAG窗口被去掉了
-
4.破解操作
下面是破解的操作步骤
- 1.OD加载程序,内存窗口中Ctrl+G,定位到0x401000
- 2.Ctrl+B搜索Timer,修改计数器变量对应秒数为0x0001(要注意好大小端)
局限性:计时器默认名如果不是Timer,内存中搜索的方法就会失效
思路2:偏移4C思路(通用法) VB程序启动顺序VB程序的每个窗口都有启动顺序,这个顺序就保存在程序开始执行位置的偏移4C附近定义的一个结构体中
-
当前顺序:现在程序中会先启动NAG窗口,后启动序列号验证窗口
-
破解思路:如果找到启动顺序位置,将NAG窗口放在序列号验证窗口后面,自然就自动去除掉NAG窗口了(想法是好的,就是不知道怎么实现?继续看下面吧…)
下面是VB程序可能的框架:
VB涉及到的相关结构体:
//VBHeader结构定义
Signature array[1..4] of char; //00H,签名,必须为VB5!
LanguageDll array[1..14] of char; //06H,语言链接库名,通常为”*"或者vb6chsdll
...
AGUTTable DWORD; //4CH,指针,指向Form GuI描述表,***关键位置***
...
//AGUTTable结构定义(大小是0x50)
Signature DWORD //00H,必须是50000000
FomID TGUID //04H,可能是以GUID方式命名的formID
Index BYTE //24H,窗体的序号,***关键位置***
Flag1 BYTE //28H,第一个窗体的启动标志,可能是90 也可能是10
AGUIDescriptionTable DWORD //48H,指针指向以“FFCC…“开始的FormGUI表
Flag3 DWORD //4CH,意义不明
AGUTTable结构体中,偏移是0x24的Index就是启动顺序;索引到AGUTTable位置是通过VBHeader为基准,偏移0x4C;因此使用修改启动顺序的方法也叫4C思路
破解步骤 step 1.找VBHeader结构起始地址OD加载程序,没有加壳的程序默认会停在OEP处,VB程序典型的OEP附近汇编如下,多调试几次VB程序自己就会发现
00401164 .- FF25 4CB14000 jmp dword ptr ds:[40B14C] ; MSVBVM50.EVENT_SINK_Release 0040116A $- FF25 8CB14000 jmp dword ptr ds:[40B18C] ; MSVBVM50.ThunRTMain 00401170 > $ 68 D4674000 push 4067D4 ; 00401170是OEP指向地址,OD加载后会停在这行 00401175 . E8 F0FFFFFF call 0040116A ;step 2.找VBHeader中偏移4C处的内容#说明:入口点处的push压入stack的数据就是VBHeader首地址,即VBHeader首地址是4067D4
在内存窗口中,Ctrl+G,定位到4067D4地址,偏移4C处(AGUTTable结构)的内容是0x406868
step 3.更改窗口的启动顺序内存窗口中,定位到0x406868,会发现2块类似的结构(AGUTTable结构),每块的大小是0x50个bytes,查看每块结构的偏移0x24处
-
NAG窗口:启动顺序是00,即第一个被程序加载的窗口
-
序列号验证窗口:启动顺序是01
去掉NAG窗口方法:直接将NAG窗口和序列号验证窗口的启动顺序互换,保存二进制修改
经验证,的确不再显示NAG窗口了;至此,NAG被去掉了,使用了2种方式,推荐使用第2种
3.序列号破解这个程序与CrackMe002的逆向思路是一样的,暴力破解和直接通过函数栈帧找到正确序列号的逻辑在CrackMe002里有介绍,下面是总结的操作流程:
-
1.先输入错误的用户名和序列号(test123/456),根据错误弹窗里的字符串先找到调用弹窗的代码和关键跳转
-
2.在关键跳转所在函数起始位置设置断点,反复调试函数起始位置和关键跳转之间的代码,分析根据用户名生成序列号的逻辑
笨方法,继续用上面的思路破解CrackMe003的序列号;为了写出注册机,下面将调试时发关键节点整理成了笔记(一定要自己操作一下,你才会知道下面说的是什么意思)
key 1.用户名生成临时序列号调试中发现了如下计算临时序列号的公式:
临时序列号 = 用户名长度 * 0x15B38 + 用户名第一个字符的ASCII码,结果用十进制表示的字符串
示例:用户名test123对应的临时序列号(字符串)是622332(记住这个值,后面会用到)
用户名计算出临时序列号会经历3次浮点运算操作,操作套路基本一样(如果不熟悉浮点运算,通过单步调试观察栈帧,一样可以得到下面的结果);下面直接给出了3次操作的截图
-
第一次操作:622332 -> 622334
-
第二次操作:622334*3-2=1867000
-
第三次操作:1867000+15 -> 1867015
经过上面key1和key2,很容易写出简单的注册机
#include "StdAfx.h" #include#include #include int main() { char user_name[100] = {0}; //用户名 printf("请输入用户名:"); scanf_s("%s", user_name, 100); //计算序列号 int user_serial = strlen(user_name) * 0x15B38 + user_name[0];//用户名计算临时序列号 float serial = 0.0; serial = user_serial + 2.0; //第一次操作 serial = serial * 3.0 -2.0; //第二次操作 serial = serial + 15.0; //第三次操作 printf("用户名:%s, 对应的序列号:%fn", user_name, serial); system("pause"); return 0; }
验证结果如下,说明整个逆向的思路和注册机是正确的
如果你是第一次逆向浮点运算,可以看一下我以前整理的浮点汇编的笔记(直接粘贴在下面了);如果想学一下C++代码逆向成汇编的典型特征,推荐看《C++反汇编与逆向分析》;如果是大牛,可以直接忽略下面
4.扩展:浮点类型的汇编CrackMe003的生成序列号算法使用了浮点的相关运算,下面简要介绍逆向中浮点类型的相关知识
C++中,32位操作系统中2种浮点数据类型:
- 单精度(float):内存中占用4个bytes
- 双精度(double):8个bytes
浮点类型的编码就不详细介绍了,对于逆向来说,只要知道基本格式(符号位 + 指数部分 + 科学计数法中的位数部分)就行,下面主要介绍一下浮点数指令
浮点寄存器-
区别:普通数据类型操作使用的是通用寄存器(eax、ecx等),浮点数据类型操作使用的是浮点寄存器(ST0 ~ ST7)
-
stack实现:浮点寄存器是通过使用栈(stack)来实现的,共8个栈空间组成(ST0 ~ ST7),且每个浮点寄存器占用8个bytes,浮点寄存器使用类似于入栈和出栈的操作
-
使用顺序:每次操作的都是ST0,从而影响其他浮点寄存器(ST1 ~ ST7)
- 入栈:如果ST0中没有数据,直接放进ST0;如果ST0中有数据,ST7数据丢弃,依次将ST6数据放入ST7,…,ST0数据放入ST1,新数据放入ST0
- 出栈:执行相反操作
| 指令 | 使用方法 | 说明 |
|---|---|---|
| fld/fild | fld/fild PUSH_DATA | 将浮点数/整数PUSH_DATA压入ST0中 |
| fst/fist | fst/fist POP_ADDR | 将ST0中的数据以浮点/整数形式存入POP_ADDR地址中 |
| fstp/fistp | fstp/fistpPOP_ADDR | 相对于fst/fist,会多执行一次出栈操作 |
| fldz、fld1 | fldz、fld1 | 将0.0、1.0压入ST0中 |
| fadd | fadd addr | 将addr地址内的数据与ST0做加法,结果存入ST0中 |
| fcom | fcom addr | 将addr地址内的数据与ST0进行实数比较,影响对应标志位 |
| ftst | ftst | 比较ST0是否是0.0,影响对应标志位 |
说明:以p结尾的指令基本都会执行一次出栈操作;其他运算指令与普通指令类似,只需要在指令前面加一个f即可
示例Debug版本的浮点操作示例(C++代码与反汇编代码对应)
#include "stdafx.h" #includeint main(int argc, char* argv[]) { ;---生成栈帧和stack上分配部分空间的初始化代码--- int test_int = 3; 0096B65E mov dword ptr [test_int],3 float test_float = (float)test_int; //通过ST0做中转,将int转成float类型 0096B665 fild dword ptr [test_int] ;将整数test_int压入ST0中 0096B668 fstp dword ptr [test_float] ;将ST0中的数据以浮点的形式存入test_float,且ST0中数据出栈 printf("test_float=%f", test_float); //float数据通过ST0中转后,放入stack上预留好空间 0096B66B fld dword ptr [test_float] ;将浮点数test_float压入ST0中 0096B66E sub esp,8 ;浮点数作为变参函数的参数,需要转换成双精度浮点存储在stack上 0096B671 fstp qword ptr [esp] ;将ST0中的数据以浮点的形式存入eap执行的地址,且ST0中数据出栈 0096B674 push offset string "test_float=%f" (9BDC7Ch) 0096B679 call @ILT+4360(_printf) (96A10Dh) 0096B67E add esp,0Ch test_int = (int)test_float; 0096B681 fld dword ptr [test_float] ;将浮点数test_float压入ST0中 0096B684 call @ILT+2085(__ftol2_sse) (96982Ah) ;调用_ftol函数进行浮点数转换 0096B689 mov dword ptr [test_int],eax return 0; 0096B69D xor eax,eax }
简要结论:
-
1.通过ST0寄存器实现整数和浮点数的转换
-
2.float类型的浮点数占用4个bytes空间,但是在实际处理时都是按照8个bytes方式进行处理
-
3.浮点数作为参数,不能直接压入stack中;因为push指令只能传4个bytes大小,会出现数据损失(printf以整数方式输出浮点数会出现错误的根因);作为返回值也有类似的处理
至此,CrackMe003逆向完了,想学逆向就尽量不要爆破掉程序就完事,尽量多学点里面的基础知识
参考:- 1.VB程序逆向反汇编常见的函数 - 笨笨D幸福 - 博客园 (cnblogs.com)
- 2.常用软件可以在这里下载:爱盘 - 最新的在线破解工具包 (52pojie.cn)
- 3.【反汇编练习】160个CrackME索引目录1~160建议收藏备用
- 4.160个Crackme_鬼手56的博客-CSDN博客
- 5.《使用OllyDbg从零开始Cracking》系列的文章也不错



