总版主
|
阅读:2864回复:10
Windows 驱动编码中的简单反阅读手法(1)
本文作者上海楚狂人,有问题请联系QQ16191935,Email mfc_tan_wen@163.com
好像驱网文件版的windows内核驱动开发者都深受反汇编的困绕。这篇文章讨论在驱动编码中采用特殊的编码手法,来简单的防止反汇编阅读。避免“一览无余”的状况。这种方法不同于加壳加密。加壳和加密的方法比较单一,从而容易被人以同样单一的方法整体解密。同时驱动中进行加密稍显复杂,有时有一些稳定性方面的障碍。 刘涛涛的“扭曲变换”是根本的防止逆向解决方案。但是需要付出巨大的艰辛。 下面的一些方法是比较简单的操作手法。这样的做法显然不能完全防止逆向。但是能起到一定的“遮掩”作用。好处是随时可以做。而且可以个人根据代码的重要程度,决定哪些部位做,那些部位不做。 1.混淆字符串 在代码中,出现了直接字符串是非常令人恼火的一件事。往往这些常数字符串在反汇编的时候会直接被人看见,对反工程者是最好的引导。 以下的代码都会暴露我们的字符串: 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)); 比较明显的缺陷是书写常数字符串的时候变得麻烦。我一直在追寻更简单的写法。但是遗憾的是,我还没有找到。怎么说呢?如果你觉得隐蔽是值得的,那就这样做。你甚至可以用更复杂的加密算法。只要你能算出密文,然后填写在代码常数中。不过那样修改代码变得太困难了。如果你能写一个预编译工具自动修改代码,确实是一个好办法。不过对于一种仅仅防止肉眼直观看到字符串的方式,更复杂的加密方法往往没必要。因为无论多复杂的算法,解密算法都很容易在你自己的代码里找到。 2.字符串混淆的实例 下面我们用一个实例来试试。 写一个简单的驱动如下: #include <ntifs.h> NTSTATUS DriverEntry( PDRIVER_OBJECT driver, PUNICODE_STRING reg) { UNICODE_STRING my_str; RtlInitUnicodeString(&my_str,L"hello,world"); DbgPrint("%wZ",&my_str); return STATUS_SUCCESS; } 以上函数的Release版本用IDA反汇编的效果是这样的: MyDriverEntry proc near ; CODE XREF: start:loc_1402B DestinationString= UNICODE_STRING ptr -8 push ebp mov ebp, esp push ecx push ecx push offset SourceString ; "hello,world" lea eax, [ebp+DestinationString] push eax ; DestinationString call ds:RtlInitUnicodeString lea eax, [ebp+DestinationString] push eax push offset Format ; "%wZ" call DbgPrint pop ecx pop ecx xor eax, eax leave retn 8 MyDriverEntry endp 上面的情况,很容易读懂这段代码打印了一个"Hello,world".现在把c代码改成下面这样的: wchar_t *dazeEstrW(wchar_t *str, wchar_t key) { wchar_t *p = str; while( ((*p) ^= key) != '\0') { p++; } return str; } NTSTATUS DriverEntry( PDRIVER_OBJECT driver, PUNICODE_STRING reg) { wchar_t buffer[] = { L'h' ^ 0x3a,L'e' ^ 0x3a,L'l' ^ 0x3a, L'l' ^ 0x3a,L'o' ^ 0x3a,L'r' ^ 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; dazeEstrW(buffer,0x3a); RtlInitUnicodeString(&my_str,buffer); DbgPrint("%wZ",&my_str); return STATUS_SUCCESS; } IDA反汇编的结果变成下面这样: MyDriverEntry proc near ; CODE XREF: start:loc_1402Bj DestinationString= UNICODE_STRING ptr -28h SourceString = word ptr -20h var_1C = word ptr -1Ch var_1A = word ptr -1Ah var_18 = word ptr -18h var_16 = word ptr -16h var_14 = word ptr -14h var_12 = word ptr -12h var_10 = word ptr -10h var_E = word ptr -0Eh var_C = word ptr -0Ch var_A = word ptr -0Ah var_8 = word ptr -8 var_4 = dword ptr -4 push ebp mov ebp, esp sub esp, 28h mov eax, dword_13000 mov [ebp+var_4], eax push 3Ah lea eax, [ebp+SourceString] push eax mov [ebp+SourceString], 52h mov word ptr [ebp-1Eh], 5Fh mov [ebp+var_1C], 56h mov [ebp+var_1A], 56h mov [ebp+var_18], 55h mov [ebp+var_16], 48h mov [ebp+var_14], 4Dh mov [ebp+var_12], 55h mov [ebp+var_10], 48h mov [ebp+var_E], 56h mov [ebp+var_C], 5Eh mov [ebp+var_A], 14h mov [ebp+var_8], 3Ah call sub_11000 lea eax, [ebp+SourceString] push eax ; SourceString lea eax, [ebp+DestinationString] push eax ; DestinationString call ds:RtlInitUnicodeString lea eax, [ebp+DestinationString] push eax push offset Format ; "%wZ" call DbgPrint pop ecx pop ecx mov ecx, [ebp+var_4] xor eax, eax call sub_110CD leave retn 8 MyDriverEntry endp 这样一来,就多少有些挑战性了。调试的时候,当然可以看见正确的字符串。但是静态反汇编的时候,要看明白那段52h,5fh,56h,55h...是什么就不是一件简单的事情了。遗憾的是,这里用了RtlInitUnicodeString和DbgPrint。我们的读者还是很容易了解这段代码做了什么。下面尝试隐藏那些函数存在的信息。 对于那些对技术有兴趣而进行发掘的破解者来说(出于兴趣而不是处于巨大的经济利益),很多时候,调用的api函数是除了字符串之外,对他们最重要的引导之一。一些有经验的windows内核程序反汇编者,对程序流程的兴趣不大,但是看看几个关键的调用,就已经明白了十之八九。所以,让对方在静态反汇编的时候看不到直观的windows内核api函数调用,是非常有意义的。 3.内核函数 对于RtlInitUnicodeString这样简单的函数,本质上没有必要隐藏。但是要隐藏也比较简单,虽然我的方法比较蠢:自己写一个。由于这个函数涉及到字符串的混淆处理,其实后面会发现写一个还是有点好处的: VOID dazeRtlInitUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PWCHAR SourceString, IN ULONG length, IN WCHAR key, ) { if(key != 0) dazeEstrW(SourceString,key); DestinationString->Buffer = SourceString; DestinationString->Length = length - sizeof(WCHAR); DestinationString->MaximumLength = length; } 上面这个函数的用法是,你可以当普通的RltInitUnicodeString用,那么key填写为0,用法如下: WCHAR buf[] = { L"Hello,world" }; UNICODE_STRING str; dazeRtlInitUnicodeString(&str,buf,sizeof(buf),0); 这样反汇编的时候当然看不见RtlInitUnicodeString这个调用了。这是显然的。 需要隐藏的一般都是关键调用。这些调用给人以对这个系统体系的结构理解的指导。比如说如下一个驱动: NTSTATUS DriverEntry( PDRIVER_OBJECT driver, PUNICODE_STRING reg) { UNICODE_STRING name_str; RtlInitUnicodeString( &name_str, L"\\FileSystem\\Filters\\MyFilterCDO" ); PDEVICE_OBJECT my_cdo; status = IoCreateDevice( driver, 0, //has no device extension &name_string, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &my_cdo ); return status; } 这个驱动生成了一个有名字的控制对象。这个信息反汇编一下一定会马上真相大白。下面我们DIY那个关键的调用。其实我们没有DIY,只是封装了一下。并且用MmGetSystemRoutine获得函数地址。 typedef NTSTATUS (*DAZE_IoCreateDevice)( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName OPTIONAL, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject ); NTSTATUS dazeIoCreateDevice( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName OPTIONAL, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject) { wchar_t name_buf[] = { 'I' ^ 0x98, 'o' ^ 0x98, 'C' ^ 0x98, 'r' ^ 0x98, 'e' ^ 0x98, 'a' ^ 0x98, 't' ^ 0x98, 'e' ^ 0x98, 'D' ^ 0x98, 'e' ^ 0x98, 'v' ^ 0x98, 'i' ^ 0x98, 'c' ^ 0x98, 'e' ^ 0x98, '\0' ^ 0x98}; UNICODE_STRING name_str; DAZE_IoCreateDevice function_pt; dazeRtlInitUnicodeString(&name_str,name_buf,sizeof(name_buf),0x98); function_pt = (DAZE_IoCreateDevice)MmGetSystemRoutineAddress(&name_str); if(function_pt != NULL) return function_pt( DriverObject,DeviceExtensionSize, DeviceName,DeviceType, DeviceCharacteristics, Exclusive,DeviceObject); return STATUS_UNSUCCESSFUL; } 4.内核函数隐藏反汇编结果 让人觉得有成就感的是,反汇编的结果,驱动里的Import表中,IoCreateDevice这个名字消失了。由于我们用了前面的字符串处理方法,同时字符串中也看不到这个信息。以上这个函数反汇编的结果是这样的: sub_11054 proc near ; CODE XREF: sub_11130+2Bp SystemRoutineName= UNICODE_STRING ptr -2Ch var_24 = dword ptr -24h var_20 = word ptr -20h var_1E = word ptr -1Eh var_1C = word ptr -1Ch var_1A = word ptr -1Ah var_18 = word ptr -18h var_16 = word ptr -16h var_14 = word ptr -14h var_12 = word ptr -12h var_10 = word ptr -10h var_E = word ptr -0Eh var_C = word ptr -0Ch var_A = word ptr -0Ah var_8 = word ptr -8 var_4 = dword ptr -4 arg_0 = dword ptr 8 arg_4 = dword ptr 0Ch arg_8 = dword ptr 10h arg_C = dword ptr 14h arg_10 = dword ptr 18h arg_14 = dword ptr 1Ch arg_18 = dword ptr 20h push ebp mov ebp, esp sub esp, 2Ch mov eax, dword_13000 push 98h mov [ebp+var_4], eax push 1Eh lea eax, [ebp+var_24] push eax lea eax, [ebp+SystemRoutineName] push eax mov word ptr [ebp+var_24], 0D1h mov word ptr [ebp+var_24+2], 0F7h mov [ebp+var_20], 0DBh mov [ebp+var_1E], 0EAh mov [ebp+var_1C], 0FDh mov [ebp+var_1A], 0F9h mov [ebp+var_18], 0ECh mov [ebp+var_16], 0FDh mov [ebp+var_14], 0DCh mov [ebp+var_12], 0FDh mov [ebp+var_10], 0EEh mov [ebp+var_E], 0F1h mov [ebp+var_C], 0FBh mov [ebp+var_A], 0FDh mov [ebp+var_8], 98h call sub_11024 lea eax, [ebp+SystemRoutineName] push eax ; SystemRoutineName call ds:MmGetSystemRoutineAddress test eax, eax jz short loc_110F7 push [ebp+arg_18] push [ebp+arg_14] push [ebp+arg_10] push [ebp+arg_C] push [ebp+arg_8] push [ebp+arg_4] push [ebp+arg_0] call eax jmp short loc_110FC loc_110F7: ; CODE XREF: sub_11054+88j mov eax, 0C0000001h loc_110FC: ; CODE XREF: sub_11054+A1j mov ecx, [ebp+var_4] call sub_11176 leave retn 1Ch sub_11054 endp 以上的反汇编结果,可以确定调用了某个函数。但是究竟是什么函数,需要进行解密或者动态调试。直接阅读的困难度就大大增加了。 5.系统函数隐藏通用写法 写一个封装的隐藏型函数还是有一定工作量的。但是通过使用宏定义,可以让工作简化。而且好处是,你这些工作不会浪费。如果你写了一个头文件和一个C文件,那么只要加入到你的任何工程中,你的那个工程就可以享受“隐藏函数”的服务。这些工作是值得的。 以下这个宏定义把剩余工作变成简单流程: #define DAZE_FUNC_BODY(func_name,key) \ UNICODE_STRING name_str; \ DAZE_##func_name function_pt; \ dazeRtlInitUnicodeString(&name_str,name_buf,sizeof(name_buf),key); \ function_pt = (DAZE_##func_name)MmGetSystemRoutineAddress(&name_str); \ if(function_pt != NULL) \ return function_pt( (1)把函数原型从帮助中拷贝出来,稍加修改(加两个括弧、一个星号和一个DAZE_前缀)如下: typedef NTSTATUS (*DAZE_IoCreateDevice)( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName OPTIONAL, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject ); 然后开始封装,封装固定的方法如下: NTSTATUS dazeIoCreateDevice( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName OPTIONAL, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject) { wchar_t name_buf[] = { 'I' ^ 0x98, 'o' ^ 0x98, 'C' ^ 0x98, 'r' ^ 0x98, 'e' ^ 0x98, 'a' ^ 0x98, 't' ^ 0x98, 'e' ^ 0x98, 'D' ^ 0x98, 'e' ^ 0x98, 'v' ^ 0x98, 'i' ^ 0x98, 'c' ^ 0x98, 'e' ^ 0x98, '\0' ^ 0x98}; DAZE_FUNC_BODY(IoCreateDevice,0x98) DriverObject,DeviceExtensionSize, DeviceName,DeviceType, DeviceCharacteristics, Exclusive,DeviceObject); return STATUS_UNSUCCESSFUL; } 先写加密字符串来表示函数名。然后用宏,最后填上所有的参数,最后返回不成功即可。 如果你觉得你调用的这个函数不想轻易让人看见,就这样做吧。 上面的手法只“遮盖”了字符串和函数调用。下面再增加一些简单方法来“遮盖”各种数据、处理流程等等。 |
最新喜欢:snox |
沙发#
发布于:2008-03-20 16:36
难得一见的好贴,顶!
|
|
|
板凳#
发布于:2008-03-20 18:02
顶,学习了
|
|
地板#
发布于:2008-03-20 20:04
受教了,好贴!
|
|
地下室#
发布于:2008-03-21 13:16
不错,学习了
|
|
|
5楼#
发布于:2008-03-21 14:36
MmGetSystemRoutineAddress也应该不要直接用,不然人家直接在那地方下个断点,你所有的函数也就一览无遗了.自己枚举PE来定位所有函数,同时在里面多设点陷阱,加上反调试功能,嘿嘿......
|
|
|
6楼#
发布于:2008-03-22 10:47
这个一定要顶!
|
|
7楼#
发布于:2008-03-25 15:52
好贴 顶一个
|
|
8楼#
发布于:2008-03-27 19:15
其他一些方法:
(1)使用try except修改代码执行流程,比如func_a调用函数func_b,函数func_b调用函数func_c,在函数func_c中构造一个异常,在函数func_a中捕获。 (2)使用花指令修改函数体的实现,导致反汇编器无法正常识别。 (3)所有的函数全部使用动态分配的内存中的函数表调用,不使用任何直接调用。函数表在运行的过程中,动态修改。 (4)使用自己编写的PE的导出表解析工具,在运行的时候动态构造函数调用表。 (5)所有字符串全部加密 (6)关闭所有调试信息 (7)在驱动内构造一个解释器,在可执行程序存放中间语言执行代码 |
|
|
9楼#
发布于:2008-03-27 19:18
(8)加密核心函数,运行的时候动态解密,函数调用完毕后,就动态释放。
|
|
|
10楼#
发布于:2008-03-31 22:33
good
|
|
|