前言
摸鱼了挺久,发现自己还没发过破解方面的帖子,于是自娱自乐写个程序加壳并逆向分析出注册机
本文主要分为两个部分:
带壳破解部分侧重于逆向分析及注册机的编写
脱壳破解部分侧重于脱壳
PS:本帖重点在分析,而非结果,单纯的爆破十分简单,稍微难点的地方在脱壳和逆向分析注册机上(虽然都很简单,大佬轻喷)
程序说明
程序载入后会随机生成一个数(机器码)
然后要求输入对应的注册码
错误则弹出消息框 fail:
成功则弹出消息框success:
PS:这个小程序十分简单,有兴趣的可以自己试试去Crack一波
用到的工具
调试工具:OD
脱壳相关工具:
PEID(查壳)
LordPE(转储)
ImpREC(修复IAT)
PS:上述工具在论坛的XP虚拟机中都有集成
带壳破解
先来说说带壳破解,破解之前再说说常用的两个破解手段
常用破解手段
- 搜索相关字符串
- 下断相关API函数
采用手段
带壳破解这里采用第二种,也就是下断相关API函数进行(因为在带壳的情况下,字符串被加密了,无法通过搜索字符串来检索)
下断哪个API函数?
根据前面程序无论是正确还是错误都会弹出对话框,于是就可以对MessageBoxA
这个函数进行下断
开始破解
追踪相关代码
1.将程序载入OD
2.下断相关API函数
通过前面的分析得知要对MessageBoxA
这个函数进行下断
于是在窗口的左下角部分输入bp MessageBoxA并回车
bp MessageBoxA
3.触发断点
下完断点以后,按快捷键F9,让程序跑起来,然后返回程序,随便输入一个错误的注册码来触发断点
4.断点触发后
断点成功被触发,此时注意右下角的堆栈窗口,可以看到窗口的相关信息
5.根据堆栈追踪
断点触发后,就可以通过堆栈中的内容来追踪相关的代码了
通过选中堆栈的调用的那一行,然后回车即可跳转到相关代码
6.相关代码
回车后跳转到了MessageBoxA
执行完的下一行,向上滑动即可得到相关的代码
分析相关代码
相关代码
通过前面的一系列操作,得到了相关的代码:
004011C6 |. 3B4D F4 cmp ecx,[local.3]
004011C9 |. /75 1F jnz short crackMe.004011EA
004011CB |. |8BF4 mov esi,esp
004011CD |. |6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL
004011CF |. |68 54704200 push crackMe.00427054 ; |Title = "nice!"
004011D4 |. |68 24704200 push crackMe.00427024 ; |Text = "success!
"
004011D9 |. |6A 00 push 0x0 ; |hOwner = NULL
004011DB |. |FF15 ECF24200 call dword ptr ds:[0x42F2EC] ; \MessageBoxA
004011E1 |. |3BF4 cmp esi,esp
004011E3 |. |E8 18020000 call crackMe.00401400
004011E8 |. |EB 1D jmp short crackMe.00401207
004011EA |> \8BF4 mov esi,esp
004011EC |. 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL
004011EE |. 68 1C814200 push crackMe.0042811C ; |Title = "try again!"
004011F3 |. 68 1C704200 push crackMe.0042701C ; |Text = "fail!
"
004011F8 |. 6A 00 push 0x0 ; |hOwner = NULL
004011FA |. FF15 ECF24200 call dword ptr ds:[0x42F2EC] ; \MessageBoxA
00401200 |. 3BF4 cmp esi,esp
爆破
很显然这里的判断跳转语句
004011C6 |. 3B4D F4 cmp ecx,[local.3]
004011C9 |. /75 1F jnz short crackMe.004011EA
就是用来判断注册码是否匹配的
只要将这里的跳转语句nop掉,就可以实现爆破,破解就完成了
选中这行反汇编语句,右键 二进制→用NOP填充即可完成爆破
爆破效果
因为先前程序已经验证失败了,于是重新加载程序,提前将判断语句NOP掉
加载完程序后按快捷键Ctrl+G并输入先前判断跳转语句的地址
然后将其Nop掉
此时再随便输入一个注册码试试:
爆破成功! 可以看到爆破十分简单,所以并不是这里的重点,重点在于如何逆向分析并写出注册机
注册机
找到正确的注册码
回到先前的比较语句
004011C6 |. 3B4D F4 cmp ecx,[local.3]
004011C9 |. /75 1F jnz short crackMe.004011EA
在比较语句这里F2下个断点
然后返回程序随便输入一个注册码触发断点
触发断点后观察ecx和[local.3]的值
ecx:
可以看到ECX为F71对应十进制为3953,应该就是正确的注册码
[local.3]:
这里的[local.3]其实是OD为了方便查看显示的,其实际的汇编代码为(双击反汇编语句即可看到)
即:
cmp ecx,dword ptr ss:[ebp-0xC]
于是观察ebp-0xC的值,在先前输入bp MessageBoxA的地方输入dd ebp-0xC来查看该地址存放的数据
可以看到存储的数据为0x262对应十进制为610,也就是输入的注册码
到这里,就找到了正确的注册码存储在ecx中,于是向上追溯ecx的来源即可
追溯注册码来源
从比较语句向上追溯ecx的来源:
004011B5 |. 8B55 F8 mov edx,[local.2]
004011B8 |. 52 push edx
004011B9 |. E8 4CFEFFFF call crackMe.0040100A
004011BE |. 83C4 04 add esp,0x4
004011C1 |. 8B45 F8 mov eax,[local.2]
004011C4 |. 8B08 mov ecx,dword ptr ds:[eax]
004011C6 |. 3B4D F4 cmp ecx,[local.3]
向上一行一行分析:
004011C4 |. 8B08 mov ecx,dword ptr ds:[eax]
可以看到ecx=[eax],于是接着找eax的来源
004011C1 |. 8B45 F8 mov eax,[local.2]
eax=[local.2],于是接着找[local.2]的来源
004011B5 |. 8B55 F8 mov edx,[local.2]
004011B8 |. 52 push edx
004011B9 |. E8 4CFEFFFF call crackMe.0040100A
004011BE |. 83C4 04 add esp,0x4
再向上会看到调用了一个call,并且这个call压入的参数就是[local.2],于是有可能这个[local.2]是在这个call中被赋值的
为了验证这一点,得重新运行程序,然后在call这里下个断点
然后随便输入一个注册码,使得程序断下,并观察此时[local.2]的值
此时会发现[local.2]=00931E80,看起来像是个地址,于是再dd 00931E80查看一下
可以看到该地址存储的数据为312F对应十进制为12591,发现它就是machine code
所以得知这里的local.2实际上就是一个指向machine code的指针
又因为注册码一般都是机器码通过某种运算的得到的,这边的这个函数又将机器码作为参数,所以可以推断这个函数是用于计算注册码的,为了验证这一点,可以观察call执行前后local.2指向地址的值的变化
call执行前:local.2指向地址存储的数据为312F对应十进制为12591,存储的数据为machine code,即上面的结果
然后再看看call执行后的结果,按快捷键F8单步步过,观察结果
call执行后:可以看到当call执行后,local.2指向地址存储的数据发生了变化
从312F变成了2D0F(对应十进制11535)为注册码
于是应证了前面的推测:这个call为计算注册码的函数
分析计算注册码函数
找到了计算注册码的函数后,就开始分析
进入函数内部
选中注册码函数,连续按2次回车进入到函数内部
函数内部
函数内部的代码并不多,于是直接贴出代码
004010E0 /> \55 push ebp
004010E1 |. 8BEC mov ebp,esp
004010E3 |. 83EC 40 sub esp,0x40
004010E6 |. 53 push ebx
004010E7 |. 56 push esi
004010E8 |. 57 push edi
004010E9 |. 8D7D C0 lea edi,[local.16]
004010EC |. B9 10000000 mov ecx,0x10
004010F1 |. B8 CCCCCCCC mov eax,0xCCCCCCCC
004010F6 |. F3:AB rep stos dword ptr es:[edi]
004010F8 |. 8B45 08 mov eax,[arg.1]
004010FB |. 3E:8B18 mov ebx,dword ptr ds:[eax]
004010FE |. B9 10060000 mov ecx,0x610
00401103 |. 2BD9 sub ebx,ecx
00401105 |. 33D9 xor ebx,ecx
00401107 |. 3E:8918 mov dword ptr ds:[eax],ebx
0040110A |. 5F pop edi ; 00931E80
0040110B |. 5E pop esi ; 00931E80
0040110C |. 5B pop ebx ; 00931E80
0040110D |. 83C4 40 add esp,0x40
00401110 |. 3BEC cmp ebp,esp
00401112 |. E8 E9020000 call crackMe.00401400
00401117 |. 8BE5 mov esp,ebp
00401119 |. 5D pop ebp ; 00931E80
0040111A \. C3 retn
分析代码
函数内部初始化堆栈之类的代码略去,提取出关键代码
004010F8 |. 8B45 08 mov eax,[arg.1]
004010FB |. 3E:8B18 mov ebx,dword ptr ds:[eax]
004010FE |. B9 10060000 mov ecx,0x610
00401103 |. 2BD9 sub ebx,ecx
00401105 |. 33D9 xor ebx,ecx
00401107 |. 3E:8918 mov dword ptr ds:[eax],ebx
1.将参数赋值给eax
004010F8 |. 8B45 08 mov eax,[arg.1]
这里的arg.1就是先前压入的参数,也就是先前的local.2
2.取出eax中的值,并赋值给ebx
004010FB |. 3E:8B18 mov ebx,dword ptr ds:[eax]
结合先前的local.2中存储的为指向机器码的地址,这里其实就是将机器码赋值给ebx
3.将0x610赋值给ecx
004010FE |. B9 10060000 mov ecx,0x610
将一个常量赋值给ecx,留作后面运算
4.将ebx减去ecx
00401103 |. 2BD9 sub ebx,ecx
结合前面的分析就是:机器码=机器码-0x610
5.将ebx和ecx进行异或运算,结果保存在ebx中
00401105 |. 33D9 xor ebx,ecx
结合前面的分析就是:机器码= (机器码-0x610) 异或 0x610
6.将ebx赋值给[eax]
00401107 |. 3E:8918 mov dword ptr ds:[eax],ebx
将结果保存到要返回的内容中
得出注册码计算公式
通过上面的分析得到注册码计算公式为:
注册码 = (机器码 - 0x610) xor 0x610
注册机代码
通过注册码计算公式写出对应的注册机代码:
int getRegisterCode(int machineCode){
return (machineCode-0x610) ^ 0x610;
}
脱壳破解
前面安排了带壳破解,接着安排脱壳破解
脱壳的一般流程
- 查壳
- 找OEP(程序入口点)
- 修复IAT
查壳
打开前面提到的工具PEID
,然后载入程序得到:
可以看到是个ORiEN壳
对待不同的壳有不同的技巧,对于ORiEN这种壳,它使用了不少花指令,但可以使用ESP定律脱掉
找OEP(程序入口点)
知道了壳后就用OD载入程序,并开始找OEP
OD载入后
先F8单步步过,跳转到对应的位置
单步过跳转后
可以看到跳转到了pushad指令(一般看到这个指令可以试着用ESP定律尝试一下)
于是再按F8单步步过一次
ESP定律
当程序走过pushad后就可以使用ESP定律来定位OEP了
选中右边寄存器的ESP,右键 HW break [ESP](该操作为对ESP设置硬件断点)
操作后通过 调试→硬件断点,可以看到多了一个对ESP的硬件断点
断点断下
然后按快捷键F9使得程序跑起来,等待断点断下
断点断下后,记得删除先前的硬件断点
接下来按几次F8(单步步过)即可到达OEP
在到达OEP前的最后一个跳转为:
为一个跨段的远跳转(判断达到OEP的一个可能要素)
到达OEP
如何判断OEP:
- 跨段跳转
- 根据编译器对应的OEP格式判断(不同编译器的OEP并不相同,可以通过编译器判断OEP特征)
因为程序为C语言编写,上述的OEP也满足C语言的OEP格式,关于各种语言的OEP格式这里就不再罗列
转储文件
找到OEP后,先将文件转储一份出来
打开前面提到的工具LordPE
找到OD正在调试的程序crackme.exe,然后选中并右键 完整转存
修复IAT
转储完后,打开前面提到的工具ImpREC
然后选中crackme.exe
然后返回OD,将OEP的地址复制出来
选中先前找到的OEP首地址,右键 用OllyDump脱壳调试进程
复制完OEP:34A0后再回到ImpREC
先修改OEP,然后依次点击自动查找IAT和获取输入表
获取完后再点击无效函数,发现有两个无效函数
记录下这两个无效的函数地址,需要手动修复
0043EF8A和0043EF21
修复无效函数
先处理0043EF8A
回到OD,重新载入程序,然后先选中内存窗口,然后按Ctrl+G跳转到0043EF8A
然后在跳转得到的地址右键 断点→内存访问
设置完断点后按F9使程序跑起来,注意观察寄存器窗口
第一次断下后并没有什么收获,继续按F9使程序运行,观察寄存器窗口
重复上述操作(按F9后观察寄存器窗口)
在第11次左右断下后,可以观察到寄存器窗口:
此时的eax为kernel32.GetCommandLineA的地址,于是我们得到了要修正的函数为GetCommandLineA
返回ImpREC
双击我们要修复的函数
将前面得到的函数名称填入并修复
修复后:
可以用同样的方法修复剩下的那个无效函数0043EE21
通过内存断点可以得到该函数为ExitProcess,将其修复后:
到此为止,看似好像已经修复完成了,但这里会发现修复的函数只包含了kernel32.dll
而MessageBoxA函数是属于user32.dll的函数,并没有被修复,也就是修复不完全
于是还要修复MessageBoxA函数,IAT函数一般为连续存储,但中间可能会分段,这里的识别不全就是因为少了user32.dll区段的函数识别,于是需要手动补全
因为IAT函数为连续存储,所以可以从先前IAT函数的地址继续往下找
先前IAT函数的的地址和大小为:
于是可以从RVA:2F1A8+F0=2F298 继续向下找有没有遗漏的IAT函数
回到OD,重新加载完程序后按F9让程序跑起来后选中内存窗口按Ctrl+G,再选中RVA 填入地址后跳转
可以看到跳转到了kernel32模块的函数尾部
从这里继续向下寻找是否有遗漏的IAT函数
然后可以找到遗漏了一个MessageBoxA,于是需要将MessageBoxA也修复进去
记下这里的地址为0042F2EC,RVA为:2F2EC
回到ImpREC
修改RVA为前面得到的2F2EC
Size为4(因为只有1个MessageBoxA函数)
然后点击获取输入表即可
PS:因为用OD重新加载了程序所以这里也要重新附加,并且重复一遍之前修复无效函数的操作
获取完输入表后可以看到:
到此为止,IAT就已经修复完毕了
导出修复后的文件
点击转储到文件,并选择之前转储的文件dumped.exe,确定即可
到此为止就脱壳完毕了
验证脱壳
脱壳完毕后,验证一下能否正常运行
打开脱壳后的文件,发现可以正常运行
然后再用PEID
查一下壳
可以看到此时的壳已经成功脱掉了
脱壳后的破解
前面带壳破解时限于壳的保护没有采用直接搜索字符串的方式来破解
于是在脱壳后再用OD载入,采用字符串搜索进行破解
OD载入后可以看到程序直接停在了OEP处,也验证了脱壳的成功
然后右键 中文搜索引擎→搜索ASCII
然后在中文搜索引擎中就可以轻松找到相关的字符串,后面的破解也就不再赘述
正向代码
附上程序的正向代码,方便大家对比逆向
// crackMe.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void getRand(int* n){
srand((unsigned)time(NULL));
*n=rand();
}
void encode(int* n){
_asm{
mov eax,n
mov ebx,dword ptr ds:[eax]
mov ecx,0x610
xor ebx,ecx
add ebx,ecx
mov dword ptr ds:[eax],ebx
}
}
void decode(int* n){
_asm{
mov eax,n
mov ebx,dword ptr ds:[eax]
mov ecx,0x610
sub ebx,ecx
xor ebx,ecx
mov dword ptr ds:[eax],ebx
}
}
int getRegisterCode(int machineCode){
return (machineCode-0x610) ^ 0x610;
}
int main(int argc, char* argv[])
{
int* a=(int*)malloc(sizeof(int));
int* b=(int*)malloc(sizeof(int));
int input;
getRand(a);
encode(a);
printf("your machine code is:%d\n",*a);
printf("please enter the register code\n");
*b=*a;
printf("%d\n",getRegisterCode(*a));
scanf("%d",&input);
decode(b);
if(*b==input){
MessageBoxA(0,"success!\n","nice!",0);
}else{
MessageBoxA(0,"fail!\n","try again!",0);
}
return 0;
}
补充
本人的逆向水平有限,属于萌新水准,文中可能会有谬误之处,望大家指正
这篇文章希望能帮助到想要入门的萌新们
这里的CrackMe为本人用VC6.0编写,为了方便理解并没有采用很复杂的加密
对于CrackMe的加壳也是使用论坛虚拟机中集成的ORiEN v2.12
CrackMe程序本身肯定是没有问题的,但鉴于其跑在虚拟机中,怕会被其它病毒感染,所以想要研究的小伙伴们可以选择自己编译一份(正向代码在前面已经给出),然后自己加壳;或者也可以下载我提供的文件,但建议在虚拟机中运行
提供的附件包含了加壳程序CrackMe.exe、转储文件dumped.exe、脱壳后的程序dumped_.exe