下面是网上的160个CrackMe的部分逆向笔记,包括逆向思路、注册机实现和逆向中常用的知识整理
注意:逆向前一定要先操作一下软件,熟悉软件的运行现象;逆向一定要自己操作一遍,看是很难看会的(高手除外)
文章目录
CrackMe002和CrackMe003是同一个作者用VB写的小程序;CrackMe003多了一个NAG窗口,难度也不是很大
1.现象
运行软件,会先弹出一个NAG窗口,什么也不能操作,几秒钟后会弹出验证用户名和序列号的主窗口
要求:需要去掉NAG窗口和逆向出正确的序列号
2.NAG窗口破解
思路1:Timer思路(特殊场景)
前置知识
- 基地址: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(要注意好大小端)
- 1.OD加载程序,内存窗口中
局限性:计时器默认名如果不是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 ; <jmp.&MSVBVM50.#100>
#说明:入口点处的push压入stack的数据就是VBHeader首地址,即VBHeader首地址是4067D4
step 2.找VBHeader中偏移4C处的内容
在内存窗口中,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
(记住这个值,后面会用到)
key 2.连续3次浮点运算
用户名计算出临时序列号会经历3次浮点运算操作,操作套路基本一样(如果不熟悉浮点运算,通过单步调试观察栈帧,一样可以得到下面的结果);下面直接给出了3次操作的截图
-
第一次操作:
622332 -> 622334
-
第二次操作:
622334*3-2=1867000
-
第三次操作:
1867000+15 -> 1867015
注册机
经过上面key1和key2,很容易写出简单的注册机
#include "StdAfx.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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, 对应的序列号:%f\n", 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"
#include <windows.h>
int 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逆向完了,想学逆向就尽量不要爆破掉程序就完事,尽量多学点里面的基础知识
5.参考
- 1.VB程序逆向反汇编常见的函数 - 笨笨D幸福 - 博客园 (cnblogs.com)
- 2.常用软件可以在这里下载:爱盘 - 最新的在线破解工具包 (52pojie.cn)
- 3.160个Crackme_鬼手56的博客-CSDN博客
- 4.《使用OllyDbg从零开始Cracking》系列的文章也不错