防止软件被暴力破解
本文描述建立一个什么样的初步防护框架可以对抗破解者的暴力破解的思路,文章不涉及软件正常注册后的防护问题。
一、原理
十几年来随着软件业的发展,对于软件产品的防范问题一直是保护开发利益的主要考虑问题,一些相应从事软件防护的软件公司应运而生,产品的种类亦是品种繁多, 这些基于软件和硬件的防护产品,其防护水平更是强弱有别,但对于抵抗暴力破解还都存在着一定的问题。为此本文提出了一个将序列号与程序关键代码结合的防护方案设想,在未知正确注册码的情况下,以目前的数学技术是不能破解的。
其实现原理是使序列号与程序的某些关键代码和数据发生关系,比如用序列号或其散列值对程序的关键代码或数据进行解密(当然这些关键代码或数据事先是在软件作者那里进行加密后才发行的)。这样,即使解密者通过修改判断跳转指令可以得到一个看似注册的版本,但是不正确的注册码只会使得解密出来的那段代码或数据全是垃圾,根本无法使用。具体说来可采用如下的方法。
-
在软件程序中有一段加密过的密文C,这个C既可以是注册版本中的一段关键代码,也可以是使用注册版程序的某个功能所必需的数据。
-
当用户输入用户名和序列号之后计算解密用的密钥:密钥 = F(用户名,序列号)。
-
对密文进行解密:明文M= Decrypt(密文C,密钥)。
-
利用某种散列算法计算解出来的明文C的校验值:校验值 = Hash(明文M)。散列算法可以采用MD5、SHA等,也可以采用CRC32等。
-
检查校验值是否正确。如果校验值不正确,说明序列号不正确,就拒绝执行。
二、准备工作
先选择一段代码作为要加密的数据,你可以选择软件某个功能的核心代码,此例选择“关于菜单”这段代码。为了编程时能处理这段代码,用begindecrypt和enddecrypt两个标签将关键数据括住,编译后的数据称之为明文M。程序编译好后必须对明文M加密处理,得的数据就是密文C,这样才能发行。
// 待加密的代码 case IDM_HELP_ABOUT : begindecrypt: DialogBox (hInst, MAKEINTRESOURCE (IDD_ABOUT), hDlg, AboutDlgProc) ; enddecrypt: break; |
RegCode( )函数的作用是先取用户输入用户名和序列号之后,用户可以自定义某种算法计算解密用的密钥:密钥k = F(用户名,序列号)
然后对密文C进行解密:
明文D= Decrypt(密文C,密钥)
利用散列算法(如MD5)计算解密恢复的明文D的散例值,如散例值与加密前的明文M散例值相同,则注册成功,否则失败。这样正确的注册码会使得解密出来的那段代码可以执行,如注册码不正确,解密出来的数据全是垃圾,根本无法使用。
实际操作中,也可不利用散列算法计算明文D的校验值,而利用SEH来处理加密代码,如解密出的是乱码,则会触发异常,这样就可利用SEH告知用户注册失败提示消息。
源码如下:
// 序列号核心判断代码 BOOL RegCode ( HWND hWnd) { _asm mov address1,offset begindecrypt // 取待加密代码首地址 _asm mov address2,offset enddecrypt // 取待加密代码末地址 unsigned char M-hash [MAXINPUTLEN]={填入经计算过明文M的散列值};
密钥k = F(用户名,序列号) // 读者自行设计一个函数 Decrypt (address1,address2,k); // 调用解密处理函数 szHash= 明文D的散例值 // 计算address1到address2这段解密后代码的散列值 if(lstrcmp(szHash,M-hash)) // 比较解密后的散列值是否与明文M的相等 return TRUE; else return FALSE; } // 解密函数,将密文C数据与密钥k进行异或运算 void Decrypt (DWORD address1,DWORD address2,BYTE value) { _asm { pushad mov ecx,address2 sub ecx,address1 // ECX =需要处理数据的字节数 mov esi,address1 mov al,value decrypt: xor byte ptr[esi],al // 解密运算 inc esi loop decrypt popad } } |
三、加密算法选用
解密算法应该是整个设计的关键。确保算法应该无法让人逆推,因为程序代码并不是真正的随机数据,因此存在着攻击的可能性。所以Decrypt算法使用不对称算法很重要,否则别人可以假设明文,反推密钥进行攻击。
此例便于讲解方便,选用了最简单的XOR来加密数据,由于程序代码不是随机数据,攻击此类加密只是几分钟的事。因此实际操作时,加密数据切勿以字节来加密,并且不要用XOR来加密。
四、手动加密代码
为了能在十六进制工具中方便找到这段“关于菜单”代码,待加密的源代码用下面两行汇编代码括住:
_asm inc eax 机器码是x040
_asm dec eax 机器码是x048
这样上面的代码变成了:
// 待加密的代码 case IDM_HELP_ABOUT : _asm inc eax // 在十六进制工具中对应0x40 _asm dec eax // 在十六进制工具中对应0x48 begindecrypt: DialogBox (hInst, MAKEINTRESOURCE (IDD_ABOUT), hDlg, AboutDlgProc) ; enddecrypt: _asm inc eax // 在十六进制工具中对应0x40 _asm dec eax // 在十六进制工具中对应0x48 break; |
文件编译好后,用十六进制工具打开文件,搜索16进制数据“4048”,就能找到待加密的代码明文M。如下图黑影部分所示:
先用Hash函数计算器等工具计算这段数据(明文M)的散列值:
校验值M-hash = Hash(明文M)
将这个结果重新填入源码中,再编译。
然后将密钥k自定义一个值,加密这段代码(明文M):
k XOR M =密文C
读者可以自己写一个工具来完成异或加密或用解密工具Hiew来完成此类工作。
五、使.text区块(section)可写
在Win32平台上(包括Windows 95/98/ME/NT/2000/XP/CE),可执行文件是PE(PortableExecutable)格式。PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。文件的内容被分割为不同的区块(Section,又称区段、节等),块中包含代码或数据,各个块按页边界来对齐,块没有大小限制,是一个连续结构。每个块都有它自己在内存中的一套属性,如:这个块是否包含代码、是否只读或可读/写等。
文件编译后,.text区的属性是只读的。但是,我们的程序会向.text区块写入新的数据(xor byte ptr[esi],al指令 ),由于.text区块只读,这样会导致程序崩馈。
因此必须用PE工具,如LordPE或Prodump等改变.text区块的属性(characterics)为 0xE0000020 ,表示可读,可写,可执行。如下图所示:
还有一种方法不修改区块属性,而是编程时用“VirtualQuery”、“VirtualProtect”等函数修改内存的读写属性,这样就可直接向.text等区块写数据了。
六、重定位
文件执行时将被映像到指定内存地址中,这个初始内存地址称为基地址(ImageBase)。对于EXE的程序文件来说,Windows系统会尽量满足。不过对于DLL的动态链接库文件来说,Windows系统没有办法保证每一次DLL运行时提供相同的基地址,这样对于DLL文件,“重定位”就很重要了。
由于此例为EXE,故没有提到重定位问题的解决。对于DLL文件,把代码做为加密对象而言,如果代码中有重定位数据,则加密后的密文解密后还需要对其进行重定位,否则这段代码就算是正确解密也无法运行。希望读者注意这点。