共享软件加密
过分相信Windows注册表的复杂性
信息以明文的形式写在程序中
图(1) 可执行文件中的字符串
C/C++程序的设计者稍微修改一下代码就可以避免这种简单的信息泄露,比如字符串“RegKey”,可以在程序中这样声明字符串数据:
static LPCTSTR lpszKey = _T(“SfhLfz”);
其实就是将每个字母向后移了一位(数值上增加1),使用前稍作处理:
TCHAR szBuffer{32};
lstrcpy(szBuffer,lpszKey);
for(int i = 0; i
加密或验证部分与软件主体部分耦合度太低
if(CheckRegCode(szRegCode) == TRUE)
{
//注册信息正确,执行正常的功能
}
else
{
//注册信息不正确,提示错误
}
这种封装看起来使程序代码结构良好,便于代码的组织与维护,但是也为破解者提供了良机。这样的程序通过反汇编之后通常有以下结构:
push 00406070 ‘字符串szRegCode参数入栈
call 00401050 ‘调用CheckRegCode函数
test eax,eax ‘判断CheckRegCode返回值
je 00401029 ‘跳转到出错位置
‘ 注册信息正确,顺序执行
从上面的汇编代码可以看到,只需将执行跳转的je指令(机器码是740B)改成两个NOP(CPU空操作指令,机器码是90)就等于忽略了对CheckRegCode函数返回值的判断,也就是说,无论这个函数返回TRUE还是FALSE,真正的功能代码都会执行,对注册码的校验就形同虚设。破解者根据指令的内存偏移地址计算出在可执行程序文件中的文件偏移位置,直接修改可执行文件就达到了破解这个功能的目的。此外,破解者还知道校验函数的位置是00401050,也就可以直接修改这个函数的代码,将函数开始部分的代码改成:
mov eax,1 ‘ 机器码是B801
ret ‘ 机器码是C3
也就是说根本不判断注册码,直接返回1(校验成功的标志),就能够使整个软件的注册码校验功能失效,软件被彻底的破解。
BOOL WINAPI CheckRegCode(LPCTSTR lpszCode);
破解者只需编写一个同名的动态库,也实现一个同名且同类型的导出函数,函数内容仅仅是返回TRUE:
BOOL WINAPI CheckRegCode(LPCTSTR lpszCode)
{
return TRUE;
}
可见,对于软件加密来说,应该尽量增加加密模块与主程序的耦合度,将加密或校验代码嵌入到程序代码中,虽然给代码的组织和维护带来了困难,但是提高了软件的安全性,正所谓有得即有失,鱼和熊掌不可兼得。
对注册码进行明文比较
“用户信息/注册码”方式的验证过程其实就是验证用户信息到注册码之间的数学映射关系,这个映射关系通常是由软件开发者制定的,而且映射关系越复杂就越不容易被破解。以用户信息做自变量,E表示映射函数,则该映射关系可被表示为以下映射函数:
注册码 = E(用户信息) 映射函数(1)
TCHAR szUserName[32];//存放破解者输入的用户信息(假设是Cracker)
TCHAR szRegCode[64]; //存放破解者输入的注册码(假的,假设是ababababab)
TCHAR szRealRegCode[64];
CalculateRegCode(szUserName,szRealRegCode);//szRealRegCode得到了内部计算的正确注册码
if(lstrcmp(szRegCode,szRealRegCode) == 0)
{
//输入的注册信息正确
}
else
{
//输入的注册信息不正确
}
这段代码的问题在于CalculateRegCode函数调用之后,内存中就存在了正确的注册码的明文,保存在内存地址szRealRegCode处,通常情况下破解者不知道这个内存位置,但是借助SoftICE之类的调试软件就很容易跟踪到这个位置。比如,程序为了验证注册信息是否正确需要从注册窗口界面得到用户信息和用户输入的注册码,这些操作最终是通过调用Windows的API GetWindowText(根据编码的不同可能有GetWindowTextA和GetWindowTextW两个版本)或GetDlgItemText(根据编码的不同可能有GetDlgItemTextA和GetDlgItemTextW两个版本)完成的,破解者利用调试软件在这些函数上下断点就可以跟踪到szRegCode(存放破解者输入的假注册码)的内存地址。当然,破解者还可以利用内存搜索得到szRegCode的内存地址,在SoftICE中可以使用以下命令得到szRegCode的内存地址:
s 30: 0 1 FFFFFFFF “ababababab”
在内存地址szRegCode处下内存断点,当有对szRegCode地址进行操作时SoftICE就会中断,以上面的程序为例,就会在lstrcmp调用的时候中断,分析lstrcmp调用前后的代码(通常有以下类似的结构):
push 00408580 ‘szRealRegCode地址入栈
push 00408560 ‘szUserName地址入栈
call 00401080 ‘调用CalculateRegCode计算正确的注册码,存放在szRealRegCode地址00408580处
push 00408580 ‘szRealRegCode地址入栈(已经得到正确的注册码)
push 00408520 ‘szRegCode地址入栈(假的注册码)
call dword ptr [00405050] ‘调用KERNEL32.lstrcmpA比较
test eax, eax ‘判断结果
jne 0040104E ‘注册码比较不正确,跳转到出错位置
‘信息正确,顺序执行正常的功能
由此可知,在调用lstrcmp中断时,CalculateRegCode已经计算出来正确的注册码,存放在内存地址00408580处,使用SoftICE的D命令查看内存就可以看到注册码,结果如图(2)所示:
图(2) 内存中的注册码
防止这种情况出现的关键是避免注册码以明文的形式出现在内存中,考察映射函数(1),其实只要修改映射函数的输入和输出设计,不返回计算出的正确注册码明文,而是返回一个注册码经过散列(hash)计算之后得到的散列值,就可以避免出现这种情况。例如:
散列值1 = E1(用户信息) 映射函数(2)
散列值2 = MD5(用户注册码)
整个注册信息校验过程首先使用映射函数E1根据用户信息在内部计算正确的注册码,但是不直接输出这个注册码,而是利用MD5或CRC32之类的不可逆散列函数计算出注册码的散列值并输出这个散列值1,然后用MD5或CRC32直接计算用户输入的注册码得到散列值2,最后通过比较散列值1和散列值2来判断注册码是否正确。整个过程中正确的注册码只是短暂存在于CalculateRegCode内部,破解者即使通过汇编代码调试跟踪到了比较散列值的地方,也无法得到正确的注册码,只能通过分析CalculateRegCode函数的内部映射方式获得注册码,只要软件设计的映射方式足够复杂就能够让大多数破解者知难而退。
共享软件中负责注册码校验的代码是软件中最关键的代码,也是软件破解者首先要找的地方。通过对校验代码的分析,破解者不仅能够利用前面介绍的方法从内存中抓取正确的注册码,还能了解软件的整个加密策略,甚至写出注册机。所以,尽量隐藏这部分代码,让破解者无法确定它们的位置也就能够极大地提高软件的安全性。
出错提示信息紧跟在加密判断之后出现
BOOL bCheckValid = CheckValue(注册码…);
if(bCheckValid)
{
MessageBox(…”注册成功!”…);
}
else
{
MessageBox(…”注册信息不正确!”…);
}
共享软件加密的其他常见问题
使用时间限制功能的共享软件,用该避免使用GetSystemTime或GetLocalTime获得时间,因为破解者通常会在这些众所周知的API上下断点来捕捉程序中的破绽,不仅如此,这两个函数还很容易被“欺骗”,用户只需要修改Windows系统的时间就可以控制这两个API函数返回在“有效期内的”时间,从而使软件永不过期。其实有很多种获取时间的方法,Windows系统的一些关键文件通常只是在安装时创建一次,获得这些文件的创建时间就是一个很好的获取时间的方法。
共享软件加密总结
共享软件加密参考文献
[1] 冉林仓.Win32汇编语言实用教程.北京:清华大学出版 ,2004.
[2] Matt Pietrek.An In-Depth Look into the Win32 Portable Executable File Format.MSDN Magazine,2002
[3] 段刚.软件加密技术内幕.北京:电子工业出版 ,2004.
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!