基础知识
    物极必反。我们已经研究了阅读Windows内核的方法,现在开始讨论在我们自己的驱动编码中采用特殊的编码方法,来简单地防止反汇编阅读。这是有趣的一种事态:一方面我们研究如何阅读别人的(尤其是MS的)代码;另一方面,我们不得不采取措施保护自己的技术不被他人简单地窃取。
    我这里要用到的这种方法不同于加壳或者加密。加壳和加密的方法比较单一,从而容易被人以同样单一的方法整体解密,同时内核驱动中进行加密稍显复杂,难以做得稳定,而且我手头没有成熟的整体加密的代码。实际上代码如果在运行,则一定必须被解密,因此从内存中得到解密的代码并不困难。所以我并不太倾向于使用加壳或者加密的方案。
    刘涛涛的“扭曲变换”是根本的防止逆向解决方案,但是需要付出巨大的努力才得以实现。
    下面的一些方法是比较简单的。这样的做法显然不能完全防止逆向,但是能起到一定的“遮掩”作用。好处是随时可以做,而且可以个人根据代码的重要程度,决定哪些部位做,哪些部位不做。无论有多简陋,有胜于无。
刘涛涛的扭曲变换
    由于破解技术的发展,实际上大部分商业软件的合法性验证机制都变成了仅具有装饰性,而且一些难以开发的底层软件的技术细节,往往在发行不久就被人理解并发表在网上。这成为许多开发者厌恶的事(当然另一些开发者则乐于此道)。
    于是如何让编译后的机器代码更难以理解就成为一个切实有用的研究方向。编译器有效地优化了代码,但这仅仅是将代码变得更简单。一般地说,简单的东西总是比复杂的要容易理解,虽然和源代码的关系变得模糊了。
    国内著名技术牛人刘涛涛在网上发表“扭曲变换”的思想。这个思想和“优化”完全相反,主旨不是让代码更简单、效率更高,而是让代码更加复杂——毫无疑问效率也更低。但是相比技术秘密的泄露,效率的损失其实并不那么重要了。
    这种复杂性有着无限的潜力。比如,读者计划一次从北京到上海的旅行,最近的直线航行路线当然只有一条。但是如果想要路线更长更复杂的话,则完全有无限种可能:你可以先从北京抵达纽约,在纽约休息后前往南极,然后北上到达撒哈拉沙漠,最后再前往上海。复杂的代码使破解者无法找到验证相关代码的所在,也使试图窃取技术的读者一无所获。
    刘涛涛的工具可以将编译后的obj文件进行扭曲,链接后生成的可执行文件变得极难阅读,而且可以反复地执行下去:一段代码可以无限地变大变长——功能却是和原来等价的。这真是不可思议的技术。
技术细节
    在代码中,出现了直接字符串是非常不妥的一件事。往往这些常数字符串在反汇编的时候会直接被人看见,对反工程者是最好的引导。
    以下的代码都会暴露我们的字符串:
char *str = "mystr";
wchar_t buffer[2] = { L"hello,world."};
UNICODE_STRING my_str;
RtlInitUnicodeString(&my_str,L"hello,world");
    隐藏这些常数字符串并不能完全防止逆向工程,但是毫无疑问的是,会给逆向工程增加很多麻烦。
    隐藏这些字符串的手法是:在写代码的时候并不写字符串的明文,而是书写密文,总是在使用之前解密。这样的手法的后果是,在静态反汇编的时候,反工程者是看不见字符串的。但是,反汇编者显然会在调试的时候看见字符串。
    在调试的时候才看见合法的字符串比静态反汇编的时候看见字符串要麻烦许多,因为一般只有对一段代码有兴趣才去调试它。而之所以对那段代码有兴趣,有些时候是因为看见了感兴趣的字符串。全部跟踪调试所有的代码,是艰巨的任务,只有具有重大价值的目标才值得那样去做。
    想保护代码,请首先保密常数字符串。
    不过有点要注意的是,你不能把所有的字符串都用同样的一招进行加密,至少不能用相同的密钥;否则,也许一个简单的解密程序就把你所有的字符串恢复为明文了。
    考虑:
char *str = "mystr";
    改为:
char str[] = { 'm' ^ 0x12, 'y' ^ 0x12, 's' ^ 0x12 ,'t' ^ 0x12,'r' ^ 0x12,'\0' ^ 0x12}
    如果认为异或是最简单的加密方法,那么0x12就是这里的密钥。现在编译出来的字符串已经面目全非了,当然,要解密这个字符串需要一个函数。
char *dazeEstr(char *str, char key)
{  
char *p = str;
while( ((*p) ^=  key) != '\0')
{    p++;    }
return str;
}
    你可以试试静态反汇编下面的代码:
char *str = { 'm' ^ 0x12, 'y' ^ 0x12, 's' ^ 0x12 ,'t' ^ 0x12,'r' ^ 0x12,'\0' ^ 0x12};
dazeEstr(str,0x12);
printf(str);
    当然你通过分析一定会知道printf的结果。调试也可以知道结论,但是比直接用眼睛可以看见可是麻烦多了。当然这样写代码也有些让人抓狂,但是,你总是可以先按自己的本来的习惯写完代码,然后把关键的字符串这样处理。
    下面是一个宽字符的处理方法。
wchar_t *dazeEstrW(wchar_t *str, wchar_t key)
{  
wchar_t *p = str;
while( ((*p) ^=  key) != '\0')
{    p++;    }
return str;
}
    下面的代码:
UNICODE_STRING my_str;
RtlInitUnicodeString(&my_str,L"hello,world");
    其实总是可以改为:
wchar_t buffer[] = { L"hello,world."};
UNICODE_STRING my_str;
RtlInitUnicodeString(&my_str,buffer);
    那么加密的写法是:
wchar_t buffer[] = {
L'h' ^ 0x3a,L'e' ^ 0x3a,L'l' ^ 0x3a,L'l' ^ 0x3a,
L'o' ^ 0x3a,L' ^ 0x3a,'L'w' ^ 0x3a,L'o' ^ 0x3a,
L'r' ^ 0x3a,L'l' ^ 0x3a,L'd' ^ 0x3a,L'.' ^ 0x3a,
L'\0' ^ 0x3a};
UNICODE_STRING my_str;
RtlInitUnicodeString(&my_str,dazeEstrW(buffer,0x3a));
    比较明显的缺陷是书写常数字符串的时候变得麻烦。我一直在追寻更简单的写法,但是遗憾的是,我还没有找到。怎么说呢?如果你觉得隐蔽是值得的,那就这样做。你甚至可以用更复杂的加密算法,只要你能算出密文,然后填写在代码常数中,不过那样修改代码变得太困难了。如果你能写一个预编译工具自动修改代码,确实是一个好办法,不过对于一种仅仅防止肉眼直观看到字符串的方式,更复杂的加密方法往往没必要,因为无论多复杂的算法,解密算法都很容易在你自己的代码里找到。
 
本文节选自电子工业出版社2008年10月出版的 《天书夜读——从汇编语言到Windows内核编程》