-
-
[原创]对某个CAD绘图控件的分析
-
发表于:
2012-5-3 16:49
7284
-
【文章标题】: 某个CAD绘图控件的分析
【文章作者】: LiXMX
【保护方式】: 序列号
【编写语言】: Borland Delphi 6.0 - 7.0
【使用工具】: PEiD,Regmon_fix,DEDE,UltraEdit
【操作平台】: Win XP
【软件介绍】: 一款Delphi和C++Builder环境下的矢量图绘制工具插件,功能简单但是较实用。
【作者声明】: 本人菜鸟一枚,发帖原因是从注册后到现在没发过像样的文章,注册好多年了,惭愧哇,主要是技术不到家啊。如果和版规冲突,请管理员删除,谢谢。
【分析原因】: 本菜分析它的原因仅仅是因为这个软件的授权方式让我很不爽(PS:我已经花钱买下来了>_<)。目前使用序列号方式授权,每次重装系统都要重新申请,虽
然提供加密狗,但是加密狗居然只支持Delphi,不能再C++Builder中使用,让只会C/C++的本小菜很受伤,深感受到了歧视,所以破解之。
-------------------------------------------------------------------------------
【流程】:
初看对方提供的程序,分为三个部分:控件安装程序、机器码生成程序、加密狗一枚。
加密狗:
这个就不说了,等着过段时间在研究一下吧。
机器码生成程序:
该程序运行后就会生成一个8位机器码,机器码一般多为CpuID,分区卷序号之类的。
然后通过“只要不重装系统,每次的码都是一样的”排除了CpuID,初步判断是分区卷序号。
之后使用PEiD查看Import,发现确实有GetVolumeInformation函数被调用,几乎就确定是分区卷序号了。
最搞笑的是当我在CMD中输入dir命令时,我发现C盘的序列号赫然就是生成的机器码,晕,明文啊明文……
再看看他把我的注册号放到哪里了?
使用Regmon_fix监视了一下,发现是写道注册表中了。
那么大概就可以确定他的工作流程了:
1.获取C盘的序列号作为机器码发送给作者;
2.作者根据用户提供的机器码算出注册码后发送给用户;
3.用户使用“机器码生成程序”导入注册码到本地注册表中;
4.控件在启动时会检查C盘的序列号和注册表中的注册码是否匹配。
所以到了这里,第一种破解方式已经有了:用户只要把自己的C盘的序列号修改成和正版用户一样的就可以正常使用该控件了。
控件安装程序:
知道了大概的授权流程了之后,从授权流程的4步就可以看出,控件本身在启动时也需要获取C盘的序列号,一边使用C盘的序列号和注册码进行某种校验。
于是我们从控件程序本身下手,首先查看控件安装目录中的全部DCU文件,看看有没有什么可以的DCU文件。
于是发现了两个的文件:Encryption.hpp 和 Encryption.dcu(十分明目张胆的文件名啊)。
既然如此那就先看看他吧,看看是个诱饵还是个果子。
在Encryption.hpp头文件中看到有三个函数定义:
extern PACKAGE AnsiString __fastcall GetDiskSerialID(char cDriveName);
extern PACKAGE AnsiString __fastcall Encrypt(const AnsiString str);
extern PACKAGE AnsiString __fastcall Decrypt(const AnsiString str);
我勒个去的,这不就是:获取磁盘序号,编码,解码三个步骤,全齐了(看来作者压根就没想过防破解啥的,嘿嘿,不差钱哦)
之后,使用DEDE的DCU Dump功能获得该DCU的反汇编代码。
在DCU中的USES中发现有如下声明:
Windows
{
T:DWORD, A:SetErrorMode,
A:SEM_FAILCRITICALERRORS,
A:MAX_PATH,
A:GetVolumeInformation
}
出现了GetVolumeInformation,看来是找对地方了。
既然有了GetVolumeInformation,那么之后就应该是使用Encrypt和Decrypt对C盘序号和注册码进行加密解密了。
最初我的想法是修改DCU中判断注册码正误的相关跳转,来一个爆破算了,但是继续往下看,发现作者在这里又给了我一个惊喜……
extern PACKAGE AnsiString __fastcall Encrypt(const AnsiString str)函数的反汇编如下:
PS:不得不感慨,DEDE确实是DELPHI程序员的噩梦哇!!!
function Encrypt (str: System.AnsiString): System.AnsiString;
var
result Result: System.AnsiString;
i: System.Integer;
sn: System.AnsiString;
begin
00000000 : 55 PUSH EBP
00000001 : 8B EC MOV EBP,ESP
00000003 : 83 C4 EC ADD ESP,-20
00000006 : 53 PUSH EBX
00000007 : 33 C9 XOR ECX,ECX // ECX清零
00000009 : 89 4D EC MOV DWORD PTR [EBP-20],ECX // 初始化为0,此处应该是编译器生成的temp临时变量,存放密码异或的结果
0000000C : 89 4D F0 MOV DWORD PTR [EBP-16{sn}],ECX // sn初始化为0(用于存放处理后的VolumeSerial)
0000000F : 89 55 F8 MOV DWORD PTR [EBP-8{Result}],EDX // result用于返回结果
00000012 : 89 45 FC MOV DWORD PTR [EBP-4{str}],EAX // str就是输入的VolumeSerial
00000015 : 33 C0 XOR EAX,EAX // EAX清零
00000017 : 55 PUSH EBP
00000018 : 68(79 00 00 00 PUSH Encrypt{0x3A}+121
0000001D : 64 FF 30 PUSH DWORD PTR FS:[EAX]
00000020 : 64 89 20 MOV DWORD PTR FS:[EAX],ESP
00000023 : C7 45 F4 01 00 00 00 MOV DWORD PTR [EBP-12{i}],$00000001 // i计数器初值为1,应该是个for循环起始位置
0000002A : 8D 45 EC LEA EAX,DWORD PTR [EBP-20]
0000002D : 8A 55 F4 MOV DL,BYTE PTR [EBP-12{i}] // i放入到DL中
00000030 : 8B 4D F4 MOV ECX,DWORD PTR [EBP-12{i}] // 计数器i放入到ECX中
00000033 : 8B 5D FC MOV EBX,DWORD PTR [EBP-4{str}] // str的首地址放入到EBX中
00000036 : 32 54 0B FF XOR DL,BYTE PTR [EBX+ECX-1] // str[i]和DL异或
0000003A : E8(00 00 00 00 CALL @LStrFromChar{0x21} // 异或后的结果转换成char型
0000003F : 8B 55 EC MOV EDX,DWORD PTR [EBP-20] // @LStrCat的参数,fastcall调用使用了EDX寄存器
00000042 : 8D 45 F0 LEA EAX,DWORD PTR [EBP-16{sn}] // @LStrCat的参数,fastcall调用使用了EAX寄存器
00000045 : E8(00 00 00 00 CALL @LStrCat{0x22} // 追加EDX中的内容到字符串sn尾部
0000004A : FF 45 F4 INC DWORD PTR [EBP-12{i}] // i计数器自增
0000004D : 83 7D F4 09 CMP DWORD PTR [EBP-12{i}],9 // i计数器和9比较
00000051 : 75 D7 JNE -41; (0x2A) // 是否是小于9,小于9继续循环,大于等于则终止(因为VolumeSerial是8个字符长度)
00000053 : 8B 45 F8 MOV EAX,DWORD PTR [EBP-8{Result}] // @LStrAsg的参数,fastcall调用使用了EAX寄存器
00000056 : 8B 55 F0 MOV EDX,DWORD PTR [EBP-16{sn}] // @LStrAsg的参数,fastcall调用使用了EDX寄存器
00000059 : E8(00 00 00 00 CALL @LStrAsg{0x26} // @LStrAsg用于将字符串sn赋值给Result
0000005E : 33 C0 XOR EAX,EAX
00000060 : 5A POP EDX
00000061 : 59 POP ECX
00000062 : 59 POP ECX
00000063 : 64 89 10 MOV DWORD PTR FS:[EAX],EDX
00000066 : 68(80 00 00 00 PUSH Encrypt{0x3A}+128
0000006B : 8D 45 EC LEA EAX,DWORD PTR [EBP-20]
0000006E : BA 02 00 00 00 MOV EDX,$00000002
00000073 : E8(00 00 00 00 CALL @LStrArrayClr{0x29}
00000078 : C3 RET NEAR
00000079 : E9(00 00 00 00 JMP @HandleFinally{0x23}
0000007E : EB EB JMP -21; (0x6B)
00000080 : 5B POP EBX
00000081 : 8B E5 MOV ESP,EBP
00000083 : 5D POP EBP
00000084 : C3 RET NEAR
end;
通过读代码发现关键的编码部分是一个for循环,而且使用的是xor运算……
这下子Decrypt函数连看都不用看了,关键代码肯定是一样的,因为A xor B xor B = A
上边的汇编的核心代码换成C++大概就是:
AnsiString __fastcall Encrypt(AnsiString str)
{
String sn = "";
for(int i=1; i<9; i++)
{
sn.cat_sprintf("%c", str[i]^((char)i) );
}
return sn;
}
如果函数入口的str存放的是C盘序列号,那么返回的就是注册码,
反之如果函数入口的str存放的事注册码,那么返回的就是C盘序列号。
所以最后发现注册码的算法仅仅是将C盘序列号的每一个字符和自己在字符串中的偏移量进行了异或运算,要制作注册机的话,使用上面那个函数就足够了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)