备用



风叶林-资源最多的免费辅助教程论坛 -> 驱动保护 -> 反调试与反反调试内容收集帖 方便大家学习 [打印本页]  

啊冲 2016-02-03 10:42

反调试与反反调试内容收集帖 方便大家学习

反调试技术在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术,首先我们来看看反调试技术。

一、Windows API方法
Win32提供了两个API, IsDebuggerPresent和CheckRemoteDebuggerPresent可以用来检测当前进程是否正在被调试,以IsDebuggerPresent函数为例,例子如下:


BOOL ret = IsDebuggerPresent();
printf("ret = %d\n", ret);


破解方法很简单,就是在系统里将这两个函数hook掉,让这两个函数一直返回false就可以了,网上有很多做hook API工作的工具,也有很多工具源代码是开放的,所以这里就不细谈了。


二、查询进程PEB的BeingDebugged标志位

当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了,例子如下:


bool PebIsDebuggedApproach()
{
       char result = 0;
       __asm
       {
                      // 进程的PEB地址放在fs这个寄存器位置上
              mov eax, fs:[30h]
                          // 查询BeingDebugged标志位
              mov al, BYTE PTR [eax + 2] 
              mov result, al
       }

       return result != 0;
}


三、查询进程PEB的NtGlobal标志位 

跟第二个方法一样,当进程被调试的时候,操作系统除了修改BeingDebugged这个标志位以外,还会修改其他几个地方,其中NtDll中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位,例子如下:

bool PebNtGlobalFlagsApproach()
{
       int result = 0;

       __asm
       {
                      // 进程的PEB
              mov eax, fs:[30h]
                          // 控制堆操作函数的工作方式的标志位
              mov eax, [eax + 68h]
                          // 操作系统会加上这些标志位FLG_HEAP_ENABLE_TAIL_CHECK, 
                          // FLG_HEAP_ENABLE_FREE_CHECK and FLG_HEAP_VALIDATE_PARAMETERS,
                          // 它们的并集就是x70
                          //
                          // 下面的代码相当于C/C++的
                          //     eax = eax & 0x70
              and eax, 0x70
              mov result, eax
       }

       return result != 0;
}


四、查询进程堆的一些标志位

这个方法是第三个方法的变种,只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了,所以这个方法貌似很强大,例子如下:


bool HeapFlagsApproach()
{
       int result = 0;

       __asm
       {
                      // 进程的PEB
              mov eax, fs:[30h]
                      // 进程的堆,我们随便访问了一个堆,下面是默认的堆
              mov eax, [eax + 18h]
                          // 检查ForceFlag标志位,在没有被调试的情况下应该是
              mov eax, [eax + 10h]
              mov result, eax
       }

       return result != 0;
}
反调试技术二
五、使用NtQueryInformationProcess函数
NtQueryInformationProcess函数是一个未公开的API,它的第二个参数可以用来查询进程的调试端口。如果进程被调试,那么返回的端口值会是-1,否则就是其他的值。由于这个函数是一个未公开的函数,因此需要使用LoadLibrary和GetProceAddress的方法获取调用地址,示例代码如下:

// 声明一个函数指针。
typedef NTSTATUS (WINAPI *NtQueryInformationProcessPtr)(
       HANDLE processHandle,
       PROCESSINFOCLASS processInformationClass,
       PVOID processInformation,
       ULONG processInformationLength,
       PULONG returnLength);

bool NtQueryInformationProcessApproach()
{
       int debugPort = 0;
       HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll "));
       NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
       if ( NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL) )
              printf("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n");
       else
              return debugPort == -1;

       return false;
}

六、NtSetInformationThread方法
这个也是使用Windows的一个未公开函数的方法,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger),等于告诉操作系统,将所有附加的调试器统统取消掉。示例代码:

// 声明一个函数指针。
typedef NTSTATUS (*NtSetInformationThreadPtr)(HANDLE threadHandle,
       THREADINFOCLASS threadInformationClass,
       PVOID threadInformation,
       ULONG threadInformationLength);

void NtSetInformationThreadApproach()
{
       HMODULE hModule = LoadLibrary(TEXT("ntdll.dll"));
      NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hModule, "NtSetInformationThread");
  
       NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);
}

七、触发异常的方法
这个技术的原理是,首先,进程使用SetUnhandledExceptionFilter函数注册一个未处理异常处理函数A,如果进程没有被调试的话,那么触发一个未处理异常,会导致操作系统将控制权交给先前注册的函数A;而如果进程被调试的话,那么这个未处理异常会被调试器捕捉,这样我们的函数A就没有机会运行了。
这里有一个技巧,就是触发未处理异常的时候,如果跳转回原来代码继续执行,而不是让操作系统关闭进程。方案是在函数A里修改eip的值,因为在函数A的参数_EXCEPTION_POINTERS里,会保存当时触发异常的指令地址,所以在函数A里根据这个指令地址修改寄存器eip的值就可以了,示例代码如下:
// 进程要注册的未处理异常处理程序A
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *pei)
{
       SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)
              pei->ContextRecord->Eax);
       // 修改寄存器eip的值
       pei->ContextRecord->Eip += 2;
       // 告诉操作系统,继续执行进程剩余的指令(指令保存在eip里),而不是关闭进程
       return EXCEPTION_CONTINUE_EXECUTION;
}

bool UnhandledExceptionFilterApproach()
{
       SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
       __asm
       {
              // 将eax清零
              xor eax, eax
              // 触发一个除零异常
              div eax
       }

       return false;
}

八、调用DeleteFiber函数
如果给DeleteFiber函数传递一个无效的参数的话,DeleteFiber函数除了会抛出一个异常以外,还是将进程的LastError值设置为具体出错原因的代号。然而,如果进程正在被调试的话,这个LastError值会被修改,因此如果调试器绕过了第七步里讲的反调试技术的话,我们还可以通过验证LastError值是不是被修改过来检测调试器的存在,示例代码:
bool DeleteFiberApproach()
{
       char fib[1024] = {0};
       // 会抛出一个异常并被调试器捕获
       DeleteFiber(fib);

       // 0x57的意思是ERROR_INVALID_PARAMETER
       return (GetLastError() != 0x57);
}

啊冲 2016-02-03 10:42

从零开始的反反调试日志
=============    编程环境    =====================
VS2008+DDK+VA+DDKWIZARD
1、安装VS2008,MSDN
2、安装DDK
3、安装ddkwizard_setup
4、安装Visual Assist X
5、-> 错误1 => 找到ddkbuild.bat、ddkbuild.cmd(下载)放入/windows/system32 目录下
6、-> 错误2 => 计算机/.../环境变量 ,添加两个系统变量
    1、W7BASE = D:\WinDDK\7600.16385.1
    2、WXPBASE = D:\WinDDK\7600.16385.1
7、-> 错误3 => VS2008/Tools/Options/Projects and Solutions/VC++ Directories
    Win32/Include files =>
    添加D:\WinDDK\7600.16385.1\inc\api 到末尾,否则编译普通win32应用程序会提示错误
    添加D:\WinDDK\7600.16385.1\inc\ddk 到末尾
Other:如果安装顺序有错,导致VA无法支持DDK,则将api、ddk 添加到VA 的/Options/Projects/C/C++ Directories
    => custom/Stable include files ,一样,添加到末尾
= done =

1 : error PRJ0019: A tool returned an error code from "Performing Makefile project actions"
=>'ddkbuild.cmd' 不是内部或外部命令,也不是可运行的程序
2 :1>DDKBLD: ERROR #3: To build using type W7 you need to set the %W7BASE% environment variable to point to the Windows 7/Windows 2008 Server R2 DDK base directory!
3 :VS2008 中UNICODE_STRING 按F12 无法追踪


=============    双机调试  =======================
Windbg+VMware
1、安装VMware
2、安装Windbg(DDK里面有这个东西)
3、安装IDA
4、VMware 设置(装有xp 和win7 两个系统)
xp版:
在c:\boot.ini 文件中添加debug 的启动项
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional - debug" /fastdetect /debug /debugport=com1 /baudrate=115200

win7版:
在msconfig 中添加debug 启动项
msconfig -> 高级选项-> 调试,调试端口COM1,波特率115200

在VMware 上修改两个系统的串口设置:
打开电源时连接
此终端是服务器
另一终端是一个应用程序
i/o 模式 轮询时主动放弃CPU占用
xp:使用命名管道\\.\pipe\com_1
win7:使用命名管道\\.\pipe\com_2

5、Windbg 设置
符号路径,xp 与win7 的符号都可以放在同一个目录下,没有的话,windbg 会自动将文件下载到E:\sysbols
E:\symbols;SRV*E:\symbols*http://msdl.microsoft.com/download/symbols

建立两个windbg 快捷方式的设置,修改其中参数,分别连接两个虚拟机
xp:
D:\tools\windbg\windbg.exe -b -k com:port=\\.\pipe\com_1,baud=115200,pipe
win7版:
D:\tools\windbg\windbg.exe -b -k com:port=\\.\pipe\com_2,baud=115200,pipe

6、在windbg 下dump 两个系统
  将整个win7系统内存dump下来( Full kernel dump),耗费了我40多个小时...其中一次机器休眠了...
  (Creating a full kernel dump over the COM port is a VERY VERY slow operation.)有除COM外其他的连接方式,但我不会。。
  .dump /f e:\win7_dump.dmp

=============   驱动加载工具====================
      后面附一个源码,喜欢的可以下

=============   准备工作完成====================
      至此,我们有了一个能随时能增加功能的驱动加载工具,一份win7 dump 文件,双机调试,编程环境。

=============   从零开始分析驱动层的反调试=========
      在这以前,只有应用层的逆向经验。没接触过驱动,也不知道反调试。
      下面是我过某个游戏驱动保护的过程,这个过程从11月14号左右开始,到12月1号结束。
         游戏一开始给人的表象有:
1、游戏进程、守护进程、驱动
2、OllyICE 附加列表中无法看到目标进程,任务管理栏则可正常显示目标进程名称。
3、Windows7:去除两个内核钩子Hook后,OD可看到目标进程,但是附加时提示附加失败!(用xuetr 看的到内核钩子)
4、Windows XP:去掉两个内核钩子,游戏直接退出。
5、采用虚拟机VMware+Windbg 调试,游戏进程 启动时报错!
6、VMware+Windows 7 [Debug]:游戏启动后Windows 7系统无响应,只能重新启动系统。
7、VMware+Windows 7:游戏可正常启动。
8、OD加载游戏主程,OD崩溃,模块时发生错误,错误代码:0xc0000005(最后发现是PE结构中一个模块名字超长导致,我的OD很老了)
9、OD正常加载游戏主程之后,有被检测到的信息,多次尝试找信息出处,无果


      以上是11月17日之前的各种尝试,也是最痛苦的时候——完全找不到任何方向。之后调整了思考方向,把重心放到第5、6、7条线索上。以下是当时调试日志的主要部分,有点小修改。


2010/11/17对**的调试终于有点突破^_^
      之前一直不清楚**是如何区分系统处于Debug还是正常状态。经过对Windows的异常分发机制,了解了Debug与正常状态的流程不同,主要是KdpTrap与KdpStub两个函数对应于不同的系统。
      至此,与双机调试有关的地方有4处:KdpDebugRoutine(函数指针)、KdpBootedNodbug(bool)、KdPitchDebugger(bool)、DebuggerEnabled(bool)。
      通过修改KdpDebugRoutine 指向KdpStub ,以及另外3个标志位,可将系统从Debug修改为正常状态,Windbg将处于等待状态。**可正常执行,待**加载完毕后,将上述4个值修改回来,Windbg可重新获取话语权!
      ******
      因此,我将要做另外一个任务,一个驱动程序,可以让系统在Debug与正常状态相互切换!这样,我就可以在游戏运行期间,随时进行调试。如果有可能,最好让驱动随时与OD进行通讯。

2010/11/18  完成驱动加载工具
      完成一个通用的驱动加载工具,测试,可将Debug系统在Debug 与 正常状态间随意切换。但是对于正常系统,却无法切换成Debug。下一步要做的,就是将正常系统也能随意切换!
(这个到现在也没开始做...)

2010/11/19
1、经过测试,被转换后的系统可以进行双机调试,下断ws2_32!send 失败。
2、使用XueTr恢复两个内核钩子后,OD能够看到** 进程,附加失败
3、针对附加失败,使用双机调试查看原因!关键函数kernel32!DebugActiveProcess。

         流程kernel32!DebugActiveProcess -> ntdll!ZwDebugActiveProcess -> 功能号0x60 -> KeServiceDescriptorTable[0][0x60*4] -> nt!NtDebugActiveProcess
         上述步骤能够成功运行
         失败存在于ntdll!NtCreateThreadEx -> nt!NtCreateThreadEx:
         经过跟踪发现,最终问题在上述线路中的nt_RtlImageNtHeaderEx+0x45处,由于对象** 进程的PE头被抹去,导致此函数判断时,返回了一个失败值!
         进一步的,在不恢复内核钩子的情况下,** 的Pe头不被改写,一旦恢复之后,**的某个线程会将此PE头抹去,导致OD无法附加
(有win7 dump ,结合ida 感觉真是好)

2010/11/??
         ** 在对比黑白名单后,判断是否放行目标进程。
         通过修改黑白名单的内容,OD 可以顺利附加,但是无法读出** 的模块信息!
(不知道具体日期了,主要是从xuetr 上看到的2个内核钩子入手nt!NtReadVirtualMemory,nt!NtWriteVirtualMemory,这期间,通过这条线索搞定了它的白名单)

2010/11/22  
         制作完相关工具后,经测试,OD 能够看见目标进程,附加,但附加之后便发生错误,无法看到对象的模块信息。应该是目标进程在不断的对debugport 进行清零操作,目前发现有

         多个线程有此动作,其中有一个是在不断新建线程,新的线程就是不断对debugport 做检查。如果绕过debugport 检查?
         (这里可能会有些不准确,但确定是的某个线程在对debugport 清零,查看了不少帖子,最后线索来自看雪)

2010/11/23   ** 对debugport 清零的动作
         Windbg 对debugport 下写断点
kd> u **+0x41764
**+0x41764:
9b2fb764 8702            xchg    eax,dword ptr [edx]    //清零操作
9b2fb766 6685e9         test     cx,bp
9b2fb769 660fbae501   bt        bp,1
9b2fb76e 8b36            mov     esi,dword ptr [esi]
9b2fb770 83ecdc         sub      esp,0FFFFFFDCh
9b2fb773 0f886545ffff  js         **+0x35cde (9b2efcde)
9b2fb779 f5               cmc
9b2fb77a 3bf1            cmp     esi,ecx


         手动修改edx 值,发现od 附加后可正常存活。但是如果暂停该线程,则会导致od 附加后,很快游戏自动退出!

         使用工具对**驱动代码部分做修改(debugport清零),在多次测试中,很少的情况可以一直附加,但实体机状态下,OD很快就被检测到。在程序自退出时,有弹出守护进程被异常终止的对话框。程序自退出时,会有一个单独线程,冻结此线程,OD 会存活的比较久。
(到现在为止,还不能对游戏下断点)

2010/11/25
         OD 对游戏下断,游戏会异常退出,0x80000003

2010/11/29
         了解线程的HidePort后,制作工具可以下断点,但是OD 还会被检测到。主要的问题在于线程0x00cc0654中调用了RtlExitUserProcess 函数(该函数又调用了ZwTerminateProcess)。
         该线程会不停的创建,但未经过CreateThread API(功能号为0x58)。
现在的问题是,创建该线程是否传递了参数进来?如果未有参数传递,是否该线程检测到OD运行?!
         补充:由于游戏主线程的HidePort被设置为1,导致内核将该线程上的异常屏蔽,不分发给用户层。因此OD修改的代码int3 会引发一个异常,导致主线程退出。

2010/11/30
         在nt!NtCreatethreadEx 下断,没有相关创建0x00cc0654 线程的调用!因此,还是无法知道程序中哪里创建了线程0x00cc0654 。比较奇怪的是,该线程应该是不断的被创建的、且线程ID 总是相同,但是retn 之后,该线程便不再被创建。。(之所以这么说,是因为在该线程的入口点,总是能断下)

2010/12/01

         基本实现OD 的附加调试,但是0x00cc0654 线程是从哪里来的,如何被创建,如何检查OD? (一直未解决,太多的代码变异)


总结:
大部分的反调试还是在驱动层面,并且是已知的几个技术点
1、  反Debug系统                          debug 系统与 正常版本切换
2、  DebugPort 清零                       nop 掉相关代码段
3、  主线程HidePort 置1                          重置HidePort
4、  内核函数钩子,采用白名单方式放行。     找到白名单,手动添加
5、  0x00cc0654 线程检测                  直接将线程入口修改为retn



我想很多在内核之外的人,跟我一样在门外徘徊,其实,只要做,并没有那么难。  

啊冲 2016-02-03 10:42

反反调试器跟踪”,对,没写错。

什么是“反调试器跟踪”?举个例子,用WinDbg尝试打开“极品飞车9“的speed.exe,运行,会被提示”Unload the debugger and try again".....
这就是“反调试器跟踪”,一般有两种方法:①调用kernel32!IsDebuggerPresent②把代码放到SetUnhandledExceptionFilter设定的函数里面。通过人为触发一个unhandled exception来执行。

HOWTO:“反反调试器跟踪”?
对第一种情况,很简单,kernel32!IsDebuggerPresent的实现是这样的:
0:000> uf kernel32!IsDebuggerPresent
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\KERNEL32.dll -
KERNEL32!IsDebuggerPresent:
7c813093 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c813099 8b4030          mov     eax,dword ptr [eax+30h]
7c81309c 0fb64002        movzx   eax,byte ptr [eax+2]
7c8130a0 c3              ret
这就简单啦,把[[FS:[18]]:30]:2的值改成0,IsDebuggerPresent就返回false,这种方法就挂了:
首先,bp kernel32!IsDebuggerPresent
0:004> g
Breakpoint 0 hit
eax=00a75950 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c813093 esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent:
7c813093 64a118000000    mov     eax,dword ptr fs:[00000018h] fs:003b:00000018=7ffdf000
0:000> p
eax=7ffdf000 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c813099 esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent+0x6:
7c813099 8b4030          mov     eax,dword ptr [eax+30h] ds:0023:7ffdf030=7ffd7000
0:000> p
eax=7ffd7000 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c81309c esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent+0x9:
7c81309c 0fb64002        movzx   eax,byte ptr [eax+2]       ds:0023:7ffd7002=01
0:000> eb 0023:7ffd7002 00  <--------------------------------
0:000> g
所以稍微好一点的应用程序不会使用这种方法:)
第二种情况会麻烦一些,看如下代码:
LONG WINAPI UnhandledExceptionFilter1( struct _EXCEPTION_POINTERS* ExceptionInfo )
{
MessageBox(0,"UnhandledExceptionFilter1",0,0);
return 0;
}

int main(int, char*)
{
SetUnhandledExceptionFilter(UnhandledExceptionFilter1);
char *p=0;
*p=0;
return 0;
}
当应用程序被一个调试器attach之后,UnhandledExceptionFilter1不会被调用,从而程序可以通过这种逻辑来进行”反调试器跟踪“。
针对这种情况,可以通过下列办法让UnhandledExceptionFilter1执行,从而cheat应用程序:
0:000> bp kernel32!UnhandledExceptionFilter
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\kernel32.dll -
0:000> g
ModLoad: 76300000 7631d000   C:\WINDOWS\system32\IMM32.DLL
ModLoad: 77da0000 77e49000   C:\WINDOWS\system32\ADVAPI32.dll
ModLoad: 77e50000 77ee1000   C:\WINDOWS\system32\RPCRT4.dll
ModLoad: 62c20000 62c29000   C:\WINDOWS\system32\LPK.DLL
ModLoad: 73fa0000 7400b000   C:\WINDOWS\system32\USP10.dll
(4f4.314): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=7ffdf000 ecx=00007d06 edx=7c92eb94 esi=0012fe04 edi=0012ff0c
eip=0041155c esp=0012fe04 ebp=0012ff0c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
*** WARNING: Unable to verify checksum for tet.exe
tet!WinMain+0x3c:
0041155c c60000          mov     byte ptr [eax],0           ds:0023:00000000=??
0:000> bp Ntdll!NtQueryInformationProcess
0:000> g
Breakpoint 0 hit
eax=0012fa28 ebx=00000000 ecx=c0000005 edx=00000000 esi=00000000 edi=00000000
eip=7c862e62 esp=0012fa04 ebp=0012fff0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
kernel32!UnhandledExceptionFilter:
7c862e62 6874060000      push    674h
0:000> g
Breakpoint 1 hit
eax=ffffffff ebx=00000004 ecx=7c862c02 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e01b esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess:
7c92e01b b89a000000      mov     eax,9Ah
0:000> p
eax=0000009a ebx=00000004 ecx=7c862c02 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e020 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0x5:
7c92e020 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
0:000> p
eax=0000009a ebx=00000004 ecx=7c862c02 edx=7ffe0300 esi=0012fa28 edi=c0000005
eip=7c92e025 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0xa:
7c92e025 ff12            call    dword ptr [edx]      ds:0023:7ffe0300={ntdll!KiFastSystemCall (7c92eb8b)}
0:000> p
eax=00000000 ebx=00000004 ecx=0012f354 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e027 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0xc:
7c92e027 c21400          ret     14h
0:000> p
eax=00000000 ebx=00000004 ecx=0012f354 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c862eef esp=0012f370 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
kernel32!UnhandledExceptionFilter+0x8d:
7c862eef 85c0            test    eax,eax
0:000> r eax=80000000 <-----------------------------------
0:000> g
Breakpoint 1 hit
eax=0012f360 ebx=7c883780 ecx=00000000 edx=7c883780 esi=7c885ab4 edi=0012f8e4
eip=7c92e01b esp=0012f348 ebp=0012f364 iopl=0         nv up ei ng nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000296
ntdll!NtQueryInformationProcess:
7c92e01b b89a000000      mov     eax,9Ah
0:000> bc 1
0:000> g
ModLoad: 5adc0000 5adf7000   C:\WINDOWS\system32\uxtheme.dll
ModLoad: 74680000 746cb000   C:\WINDOWS\system32\MSCTF.dll
ModLoad: 10000000 10023000   C:\WINDOWS\system32\PROCHLP.DLL
ModLoad: 77bd0000 77bd8000   C:\WINDOWS\system32\version.dll
ModLoad: 73640000 7366e000   C:\WINDOWS\system32\msctfime.ime
ModLoad: 76990000 76acd000   C:\WINDOWS\system32\ole32.dll
ModLoad: 69760000 69776000   C:\WINDOWS\system32\faultrep.dll
ModLoad: 77bd0000 77bd8000   C:\WINDOWS\system32\VERSION.dll
ModLoad: 759d0000 75a7e000   C:\WINDOWS\system32\USERENV.dll
ModLoad: 762d0000 762e0000   C:\WINDOWS\system32\WINSTA.dll
ModLoad: 5fdd0000 5fe24000   C:\WINDOWS\system32\NETAPI32.dll
ModLoad: 76f20000 76f28000   C:\WINDOWS\system32\WTSAPI32.dll
ModLoad: 76060000 761b6000   C:\WINDOWS\system32\SETUPAPI.dll
ModLoad: 77f40000 77fb6000   C:\WINDOWS\system32\SHLWAPI.dll
ModLoad: 76d70000 76d92000   C:\WINDOWS\system32\apphelp.dll
ModLoad: 76d70000 76d92000   C:\WINDOWS\system32\Apphelp.dll
(4f4.314): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00007d06 edx=7c92eb94 esi=0012fe04 edi=0012ff0c
eip=0041155c esp=0012fe04 ebp=0012ff0c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
tet!WinMain+0x3c:
0041155c c60000          mov     byte ptr [eax],0           ds:0023:00000000=??


HAVE fun:)

啊冲 2016-02-03 10:42
第二部分:接管流程
  在第一部分完成之后,已经重载了一份新的NT内核,接下来就是接管执行流程。
  自Windows XP以后从RING3进RING0统一调用sysenter汇编指令,但仍保留了INT 2E中断,在sysenter调用之后会执行内核空间中的KiFastCallEntry函数,这个函数负责所有的应用层请求,即使是INT 2E也会走这个函数,而KiFastCallEntry在经过简单的处理后会通过KeServiceDescriptorTable或者KeServiceDescriptorTableShadow来获取内核函数指针并调用,重载内核的目的就是为了在执行内核函数的时候转移到新的模块上,从而不被其他HOOK所影响。那么需要做的就是在调用内核函数的时候转移指令,但并不是所有内核函数都由ntoskrnl实现,内核中GUI函数却是由win32k.sys实现,由于目前只重载了ntoskrnl,所以只能做到转移SSDT中的函数。额外说明一下,即使NT内核有不同的版本,但所有NT内核的导出模块名称都是ntoskrnl.exe(详见输出表),所以我所说的ntoskrnl泛指NT内核,而不是具体的文件名。
  在KiFastCallEntry中调用内核函数的地方大家很容易找到资料,最先是谁提出的我记不清了,反正现在是被某出名软件所使用,下面HOOK的地方与其他软件可能会冲突,所以在测试的时候需要保证没有安装任何安全软件。
  用户态程序调用内核函数的流程简述为:用户态->ntdll.ZwApi->sysenter->内核态->KiFastCallEntry->ServiceTable->ServiceRoutine。ServiceTable分为两种,上面已经提到了,接下来要做的就是在调用SSDT中函数的时候转移到新模块上,在WRK中可以找到这样的代码:
代码:
        mov     esi, edx                ; (esi)->User arguments
        mov     ebx, [edi]+SdNumber     ; get argument table address
        xor     ecx, ecx
        mov     cl, byte ptr [ebx+eax]  ; (ecx) = argument size
        mov     edi, [edi]+SdBase       ; get service table address
        mov     ebx, [edi+eax*4]        ; (ebx)-> service routine
        sub     esp, ecx                ; allocate space for arguments
        shr     ecx, 2                  ; (ecx) = number of argument DWORDs
        mov     edi, esp                ; (edi)->location to receive 1st arg
        //省略部分代码
        call    ebx                     ; call system service
  显然,最后一句的call指令就是调用了内核函数,而需要hook的地方就是sub esp, ecx这一句,此时,edi指向ServiceTable,eax为函数索引,ebx为函数地址,而且这一句连同下面的两句指令共7字节,可以容纳一个JMP/CALL指令,绝佳的HOOK点。至于这个地址怎么定位,就要用字节码搜索了,实在没有什么好办法,但总比直接写地址或定偏移的那种硬编码好。
代码:
PVOID __declspec(naked) _GetKiFastCallEntryAddress()
{
  __asm
  {
    MOV  ECX, 0x00000176;
    RDMSR;
    RETN;
  }
}
PVOID FindHookKiFastCallEntryAddress(PVOID lpKiFastCallEntry)
{
  /*
  sub    esp, ecx
  shr    ecx, 2
  mov    edi, esp
  */
  UCHAR HookBytes[] = {0x2B, 0xE1, 0xC1, 0xE9, 0x02, 0x8B, 0xFC};

  return RtlFindMemory(lpKiFastCallEntry, 0x300, HookBytes, sizeof(HookBytes));
}
  通过MSR寄存器获取KiFastCallEntry的函数地址,有兴趣的朋友可以翻阅WRK中系统初始化部分的代码。HOOK函数这里采用了普通的MDL方式来写入只读内存,可能会有人问了,在写入地址的一瞬间别的CPU执行到了这里怎么办,我个人认为是杞人忧天,现在的CPU都有缓存机制,也就意味着内存和CPU缓存未必同步,在你写入的一瞬间即使别的CPU执行到了这里,那也是缓存中的代码,和内存中的并不一致,有兴趣的朋友可以查找CPU的TLB、TIB的资料。当然这只是降低了写入与执行冲突的几率,并不代表完全没有可能蓝屏,但话说回来,你要修改一个可执行指令,不可避免的有几率冲突,这是完全无法避免的,所以尽可能是降低几率就可以了。
代码:
VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    pop  edx;  //save ret address
    sub  esp, ecx;
    push edx;  //restore ret address
    mov  edx, dword ptr [edi + eax * 0x04];

    shr  ecx, 2;
    mov  edi, esp;
    add  edi, 0x04;
    retn;
  }
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT lpDriverObject, IN PUNICODE_STRING lpRegPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(lpDriverObject);
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));
  return STATUS_SUCCESS;
}
  编译并测试,通过ARK查看效果。注意!_MyKiFastCallEntryFrame是在Windows 7中测试的,XP下并不通用,因为执行上下文并不一样,XP中是mov ebx, dword ptr [edi + eax * 0x04]而Windows 7是mov edx, dword ptr [edi + eax * 0x04],这里我就不做兼容性写法了,手里也没有XP的虚拟机。XP已经停止服务了,相信没有多少人打算再使用了,如果非要兼容XP,自己做下简单的修改即可,即把edx换成ebx。
名称:  001.jpg
查看次数: 1
文件大小:  38.8 KB
  测试一段时间后并没有蓝屏现象,说明HOOK成功,但是目前并没有写任何过滤内容,下面就来丰富一下过滤函数,即所有的SSDT都走新内核。并继续考虑兼容XP。
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    if (ServiceIndex == 190)
    {
      DbgPrint("Call NtOpenProcess\n");
    }
    return (ULONG_PTR)lppServiceTableBase[ServiceIndex] - (ULONG_PTR)g_lpNtoskrnlAddress + (ULONG_PTR)g_lpNewNtoskrnlAddress;
  }
  return lppServiceTableBase[ServiceIndex];
}

VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    pop  ebx;  //save ret address
    sub  esp, ecx;
    push ebx;  //restore ret address

    push eax;
    push ecx;
    push edx;

    push eax;
    push edi;
    call ServiceCallFilter;
    mov  ebx, eax;

    pop  edx;
    pop  ecx;
    pop  eax;
    mov  edx, ebx;

    shr  ecx, 2;
    mov  edi, esp;
    add  edi, 0x04;
    retn;
  }
}
  由于仅测试效果,我就暂时写190,这是NtOpenProcess的函数索引。
名称:  002.jpg
查看次数: 0
文件大小:  43.1 KB
至此所有的SSDT中的函数已全部转移到新模块中。但这还只是一个半成品,仅做测试使用,因为后面还需要改动很多。

啊冲 2016-02-03 10:42
小结与修正
  目前已经实现了一份简单的重载内核代码,但是如果你也跟着我实现了此部分,会发现此代码根本不能使用,甚至不能拿到本机来测试,是的,新的内核还是有大量的问题,驱动加载后会导致一些程序打不开,但是并不蓝屏,有意思的现象。
  写上一篇文章的时候没有测试的那么完善,但是我拿出我之前写的重载内核代码,并没有上述问题,仔细分析代码并回忆,问题还是出在了重定位以及需要额外的处理。那么关于修复重定位的部分就需要重新写了,把修复过程分为两部分,第一次全部重定位到新模块上,第二次有选择的重定位到原模块上,为什么需要那么麻烦?这就涉及到了原始地址的获取方式问题,原始地址都存储在一个叫KiServiceTable的变量中,详见WRK。为什么要获取原始地址?因为当驱动加载的时候你无法确定当前的SSDT表是否被HOOK,所以在第一次修复重定位之后去找KiServiceTable,然后再进行第二次重定位的修复(我无法保证第二次修复不会破坏KiServiceTable中的地址)。
  重新写一份代码,当然大部分还是从原来的代码复制过来,这样有利于逻辑上的思考,先不进行HOOK,把重载部分先理顺清楚,重定位修复代码修改为这个样子:
代码:
PVOID ReloadNtModule(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList)
{
  PVOID lpImageAddress = NULL;
  PKLDR_DATA_TABLE_ENTRY NtLdr = (PKLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
  PVOID lpFileBuffer;

  DbgPrint("Nt Module File is %wZ\n", &NtLdr->FullDllName);
  if (lpFileBuffer = KeGetFileBuffer(&NtLdr->FullDllName))
  {
    PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)lpFileBuffer;
    PIMAGE_NT_HEADERS lpNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);

    if (lpImageAddress = ExAllocatePool(NonPagedPool, lpNtHeader->OptionalHeader.SizeOfImage))
    {
      PUCHAR lpImageBytes = (PUCHAR)lpImageAddress;
      IMAGE_SECTION_HEADER *lpSection = IMAGE_FIRST_SECTION(lpNtHeader);
      ULONG i;

      RtlZeroMemory(lpImageAddress, lpNtHeader->OptionalHeader.SizeOfImage);
      RtlCopyMemory(lpImageBytes, lpFileBuffer, lpNtHeader->OptionalHeader.SizeOfHeaders);
      for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
      {
        RtlCopyMemory(lpImageBytes + lpSection.VirtualAddress, (PCHAR)lpFileBuffer + lpSection.PointerToRawData, lpSection.SizeOfRawData);
      }
      if (KeFixIAT(PsLoadedModuleList, lpImageAddress))
      {
        KeFixReloc1(lpImageAddress, NtLdr->DllBase);
      }
      else
      {
        ExFreePool(lpImageAddress);
        lpImageAddress = NULL;
      }
    }
    ExFreePool(lpFileBuffer);
  }
  if (lpImageAddress) DbgPrint("ImageAddress:0x%p\n", lpImageAddress);
  return lpImageAddress;
}
VOID KeFixReloc1(PVOID ImageBaseAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)ImageBaseAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)ImageBaseAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
  ULONG_PTR DifferOffset = (ULONG_PTR)ImageBaseAddress - lpNtHeader->OptionalHeader.ImageBase;

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems; i++)
    {
      switch (lpItem >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          ULONG_PTR *lpFixAddress = (ULONG_PTR *)((PCHAR)ImageBaseAddress + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF));

          *lpFixAddress += DifferOffset;
        }
        break;
      case IMAGE_REL_BASED_ABSOLUTE://do nothing
        break;
      default:
        DbgPrint("KeFixReloc1:Found unknown type(%X).\n", (lpItem >> 12));
        break;
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  lpNtHeader->OptionalHeader.ImageBase = (ULONG)ImageBaseAddress;
  return;
}
VOID KeFixReloc2(PVOID New, PVOID Old)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)New;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)New + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems; i++)
    {
      switch (lpItem >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          PVOID lpFixAddress = (PCHAR)New + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF);

          KeFixRelocEx(New, Old, lpFixAddress);
        }
        break;
      case IMAGE_REL_BASED_ABSOLUTE://do nothing
        break;
      default:
        break;
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return;
}
  重点还是KeFixRelocEx,重定位修正的是“地址的值”,希望大家能看懂,那么我就在这里做了相对比较多的判断。
  “地址”可执行,“值”可执行。
  “地址”可执行,“值”不可执行。指向原模块。如访问全局变量。
  “地址”不可执行,“值”可执行。不修改。如函数表。
  “地址”不可执行,“值”不可执行。想不出来是什么,指向原模块吧。
对于第一点,我原本是指向新模块,但测试了好半天,最后还是没有解决重载后某些程序打不开的问题。我曾尝试解析汇编,但字节组合方式太多了,我无法做出最正确的判断,既然我找不到是哪一类重定位存在问题,那么我就做没有问题的地方,如IAT,这种处理很像权限访问中的解决方式(1.我能干什么。2.我不能干什么)。最后代码变成这个样子:
代码:
BOOLEAN KeIsExecutable(PVOID ImageBase, PVOID Address)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)ImageBase;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_SECTION_HEADER *lpSecHdr = IMAGE_FIRST_SECTION(lpNtHeader);
  ULONG_PTR Rva = (ULONG_PTR)Address - (ULONG_PTR)ImageBase;
  USHORT i;

  for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
  {
    if (Rva >= lpSecHdr.VirtualAddress && Rva < lpSecHdr.VirtualAddress + lpSecHdr.SizeOfRawData)
    {
      return ((lpSecHdr.Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0);
    }
  }
  return FALSE;
}

VOID KeFixRelocEx(PVOID New, PVOID Old, PVOID FixAddress)
{
  if (KeIsExecutable(New, FixAddress))
  {
    if (KeIsExecutable(New, *(PVOID*)FixAddress))
    {
      if (KeIsIAT(New, *(PVOID*)FixAddress))
      {
        NOTHING;
      }
      else
      {
        *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
      }
    }
    else
    {
      *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
    }
  }
  else
  {
    if (KeIsExecutable(New, *(PVOID*)FixAddress))
    {
      NOTHING;
    }
    else
    {
      *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
    }
  }
  return;
}
  啰嗦了那么多,非常不完美的解决了某些程序打不开的问题,如果有兴趣的朋友可以继续深入。但回头想想重载仅仅是为了防止HOOK而带来的麻烦,不妨假设HOOK没有那么猥琐,如果你发现重载内核不能绕过某些HOOK,再对症下药吧。
第三部分:获取原始地址
  重头戏来啦!一开始说过原始SSDT函数都存储在一个叫做KiServiceTable的变量里,这个变量同样没有导出,利用字节码搜索的方式效率很低且未必找的准确,所以另求出路。网上已经有大牛研究出了一种方式,那就是利用重定位来查找。
  首先来看WRK是如何做的:(KiInitSystem)
代码:
   KeServiceDescriptorTable[0].Base = &KiServiceTable[0];
    KeServiceDescriptorTable[0].Count = NULL;
    KeServiceDescriptorTable[0].Limit = KiServiceLimit;
    KeServiceDescriptorTable[0].Number = KiArgumentTable;
    for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {
        KeServiceDescriptorTable[Index].Limit = 0;
    }

    //
    // Copy the system service descriptor table to the shadow table
    // which is used to record the Win32 system services.
    //

    RtlCopyMemory(KeServiceDescriptorTableShadow,
                  KeServiceDescriptorTable,
                  sizeof(KeServiceDescriptorTable));
  再来对比Windows 7 x86中是如何做的,IDA分析如下:
点击图片以查看大图

图片名称:        01.jpg
查看次数:        2
文件大小:        23.6 KB
文件 ID :        89288
  Ntoskrnl.exe的期望基址是00400000,所以第一句的RVA就是00395C12,来看一下重定位信息:
名称:  02.jpg
查看次数: 0
文件大小:  34.7 KB
  可以发现一个特点,有两个连续的重定位00395C14与00396C18,而第一个RVA就是KeServiceDescriptorTable,第二个就是KiServiceTable了,结合IDA对附近代码的综合判断,最后的代码就是这样:
代码:
PVOID FindKiServiceTable(PVOID lpNtoskrnlAddress)
{
  PVOID lpKeServiceDescriptorTable = KeGetProcAddress(lpNtoskrnlAddress, "KeServiceDescriptorTable");
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpNtoskrnlAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG j;

    for (j = 0; j < NumberOfItems - 1; j++)
    {
      if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW && (lpItem[j + 1] >> 12) == IMAGE_REL_BASED_HIGHLOW)
      {
        ULONG *lpFixAddress1 = (ULONG*)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));
        ULONG *lpFixAddress2 = (ULONG*)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j + 1] & 0x0FFF));
        //两个连续的重定位
        if ((ULONG)lpFixAddress2 - (ULONG)lpFixAddress1 == sizeof(ULONG))
        {
          //MOV DWORD PTR DS:[KeServiceDescriptorTable], XXX
          if (*(USHORT*)((PCHAR)lpFixAddress1 - sizeof(USHORT)) == 0x05C7)
          {
            //DbgPrint("lpFixAddress1:%08X\n", (ULONG)lpFixAddress1 - 2);
            if (*lpFixAddress1 == (ULONG)lpKeServiceDescriptorTable)
            {
              return (PVOID)*lpFixAddress2;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}
PVOID BuildKeServiceTable(PVOID lpKernelAddress, PVOID lpOrgKernelAddress)
{
  PVOID lpKiServiceTable = NULL;
  PVOID lpKeServiceTable = NULL;

  if (lpKiServiceTable = FindKiServiceTable(lpKernelAddress))
  {
    lpKeServiceTable = ExAllocatePool(NonPagedPool, KeServiceDescriptorTable->NumberOfService * sizeof(PVOID));
    
    if (lpKeServiceTable)
    {
      RtlCopyMemory(lpKeServiceTable, lpKiServiceTable, KeServiceDescriptorTable->NumberOfService * sizeof(PVOID));
      DbgPrint("BuildSSDT:0x%p\n", lpKeServiceTable);
    }    
  }
  return lpKeServiceTable;
}
  而最后的DriverEntry与ServiceCallFilter则是:
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    return g_KeServiceTable[ServiceIndex];
  }
  return lppServiceTableBase[ServiceIndex];
}

VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    push ecx;

    push eax;
    push edi;
    call ServiceCallFilter;
    mov  edx, eax;

    pop  ecx;

    pop  eax;
    sub  esp, ecx;
    shr  ecx, 2;
    mov  edi, esp;
    jmp  eax;
  }
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(DriverObject);
  g_lpNtoskrnlAddress = KeGetModuleHandle(PsLoadedModuleList, "ntoskrnl.exe");
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  g_KeServiceTable = (PVOID*)BuildKeServiceTable(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeFixReloc2(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));

  DriverObject->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}
  来来来测试一下,就用HideToolz来看效果。
名称:  03.jpg
查看次数: 0
文件大小:  57.4 KB
名称:  04.jpg
查看次数: 0
文件大小:  57.2 KB
  加载驱动咯。
名称:  05.jpg
查看次数: 0
文件大小:  88.2 KB
  好了,无视SSDT HOOK,同样无视INLINE HOOK,终于算是完成了一点像样的东西了,但这并不是全部,也不是此专题的完结,后面还有哦,我想后面应该是告诉大家如何再把SHADOW SSDT搞定。
  最后,此代码仍然不能拿到本机测试,如果你想蓝的话。对于新模块最少还有两个问题没有搞定(别喷我),后续我同样会告诉大家。嗯,先这样吧。

啊冲 2016-02-03 10:43

动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器

    现在的软件保护越来越厉害,从早先的加压缩壳、加密,发展到加密壳,虚拟机保护,以及扭曲变换等一系列的混淆手段,使得逆向一个软件的难度越来越大。究其根本原因,是人类天生不适合处理复杂的程序,在看过《代码大全2》之后,我发现作者一直在强调好的程序架构实际是在努力降低程序在管理上的复杂度,一个程序在运行时,至少在三个方面发生着变化,一个是空间:寄存器,内存的值在不断的变化;一个是时间,程序的运行顺序随时间推移而变化;第三个是语义上的变化,到底一段程序干了什么?它的目的是什么?它的实现机制是什么?看一行行的反汇编出来的代码都是很难搞懂的,更何况还有加壳加密这样的混淆手段在作怪,一段程序的二进制代码,再加上哪怕最简单的花指令混淆,都大大增加了复杂度,使得人脑理解它的原理变成一个艰巨的任务。
    
    混淆手段之所以成了逆向道路上的拦路虎,是在于人脑无法把许许多多条枯燥乏味的指令变成一个完整的概念,哪怕就是把两个数相加这样简单的事,编译成二进制代码,再加上花指令混淆,都很难理解它的本意,这就是加壳软件厉害的地方,许多恶意软件借助壳的帮助,逃脱了反病毒人员的逆向分析,在用户的电脑中为非作歹。(下面我把“壳”和混淆代码不加区分的使用了,少打几个字,读起来也简单。)
还有很多软件是用Java,VB这样以虚拟机为基础写的,要想在没有源代码的情况下移植到别的机器上,也是很难的事。而实际上如果能把这些二进制的程序先翻译成一种通用的中间语言,再把这种中间语言翻译成高级语言,就可以很好的利用它们来生产出更多的软件。

    为了达到更有效率的逆向以上这些软件的目的,首先要讨论的是能不能开发出这样一种程序,它能把程序中被混淆的东西暴露出来,去掉那些垃圾代码,把程序原始的面貌展现出来?
    
    我认为是可以的,因为一个很简单的事实:一个加了壳的软件,不论它的代码如何被混淆,它原有的功能不能变化,既不能多做,也不能少做,更不能做错。换句话说,从时间和空间上来看,原来这个程序的执行过程是一根线,加上壳,就多了壳这条线,两条线就像两根有塑料外皮的导线一样,再怎么交织在一起,两根电线中的电流是不会流到一起的,当线与线交织运行一定阶段后,一定要分开来各管各走,这样程序原来的功能才不会被破坏,无论是虚拟机还是扭曲变换,这条基本的定律是不会变的。
    
    然后我从上面这点推导出第二个观点是,壳这条线既然不影响程序的功能,那就是多余的东西,而多余的东西是可以被拿掉的。就是说程序在执行过程中,是可以不去走壳的这条线,也能行得通,而且走起来还快一点,事实上在我看了《编译原理》后发现编译器的代码优化就是起到去除冗余代码的作用的,随便多复杂的混淆代码,只要反汇编出来的汇编语句没有错误,把它放到编译器中优化一下,估计这些冗余代码都活不下去了。而逆向分析人员甚至都没必要去看。这样就大大节省了逆向的工作量。

    上面的这段话中有一个非常重要的条件,就是交给编译器去优化的代码必须是反汇编的时候没有错误的,而很多混淆代码使用了花指令这样的手段,使得静态反汇编出来的东西都是一团乱码,静态反编译因为不能再现程序中真实的变量值的变化,遇上跳转,CALL指令,搞不清是真是假,因为参数是未知的,比如"jmp eax"什么的,这样就大大影响了反汇编的质量,现今为止,没有什么方法能百分之百的静态反汇编成功。

    那么就让我们试试像OD这样的动态调试手段吧,动态调试可以真实地观察程序的运行情况,只要某个程序的片断跟踪过一次,基本上就得到了正确的反汇编代码,但是有三个问题,一是每个程序都有许多的分支,就像一棵树有许多的树梢,而调试器没办法一次走完所有的地方,所以反汇编出来的代码不完整;关于这个问题,可以把程序看成是一棵二叉树之类的东西,反正用个遍历算法,强制遍历一遍所有可用的分支,整个程序的反汇编就出来了吧。

    二是这些代码中有很多的循环,一个循环如果执行了1000遍,OD直接保存代码的话,就把这个循环体重复了1000遍,反汇编出来的代码又很多余。关于这个问题就要一边跟踪调试,一边在反汇编的基础上进行分析,建立基本块,循环体,子过程的结构,初步整理好代码。准备进一步的分析。

    三是OD还有一个很大的问题,就是它跟踪程序的过程不可逆的,我希望在调试程序时,程序运行到哪里,发现了问题,就反过来倒推,这要求保存程序每一步的状态,随时可以退回到前面任意的一个点上。这个问题也许可以试试建立一个数据库来保存相关信息,比如一个寄存器开始是什么值,后来这个值起了什么变化,放到了什么地方,又从什么地方取得了一个什么值,往往这些值是固定的,其实在程序中的各种变量值往往是固定的,从系统初始化开始,一步一步地搬动,加减运算什么的,一路上再怎么变,其实都是固定的数值关系,有些时候看上去好像每次运行都不一样的值,其实处理的方式是一样的,数据流是一样的,控制流也是一样的,要不然这个程序就有毛病了,好端端地运行它两次,结果第一次点“文件”菜单,出来的是“文件”菜单,第二次点变成“帮助”了,每次加减运算的结果都不一样,这样的程序没法用了,不等别人来逆向,自己先被用户抛弃了。所以,即使是系统调用,或者是用户输入,只要我们的调试器模拟输入的参数对路,那么一路上的流程是固定的,变量值的变化也是很容易计算的,关键是要能有个数据库来保存,来随时调用进行推算。

    综上所述,我们必须用动态跟踪的方法,获得跳转的间接路径,还要即时的反汇编出来,还要保存程序的运行状态,还要调用编译器优化来清除混淆代码。这里最好试试虚拟机的技术,在虚拟机中让程序运行,可以把程序和系统隔离开来,防止某些软件发现被调试后恶意地破坏我们的系统。

    这个“动态调试+静态反汇编优化”以去除混淆代码的设想背后是一个巨大的系统工程,想法本身不新鲜,网络上,论坛里,相关的资料和软件都有不少,比如虚拟机脱壳,比如可逆的调试器,但关键是它们没有拧成一股绳,没有在一个高层次的视野下统一起来。当然我只是个业余的编程爱好者,一个人做这样的事肯定是力所不能及的,但既然爱因斯坦那么看重人的想象力,所以我就先幻想一下,然后再一块砖一块砖的去搭建,这里先抛第一块砖吧。

啊冲 2016-02-03 10:43

反反调试思想方法探索

如今,软件安全已经成为了开发软件项目的必备组成部分,反调试则是其中关键的一环,然而,正如矛与盾的对立一样,反反调试与反调试必将永久的并立共存。为了防止软件被调试,现今的软件大多都利用了驱动来检测制止,对于关键的系统函数进行hook(包括各种SSDT hook、inline hook、iat hook等)能有效地遏制进程被打开和读写等,然而,hook是很容易定位和被恢复的,基于没有任何验校检测的hook保护技术就像一面纸墙一般不堪一击。因此,验校检查成为越来越多的反调试代码中不可或缺的一个部分,特别是对于商业性的网络游戏客户端,一旦反调试代码检测到自身的hook地址被修改或者自身的代码验校不一致时,便立刻选择结束游戏进程,甚至蓝屏或重启,以此强硬的对待那些有调试企图的人。
检测代码往往无处不在,你很难全部的定位和找到它们,而且它们相互交织检测和代码验校。另一方面,为了对抗硬件断点,在检测调试之前,往往对DR调试寄存器做了相关清除和手脚,并在之后予以恢复,检测代码往往加了VM保护,使人很难弄清程序的流程。
    我们知道,在线程切换时会根据是否是同一进程而决定知否切换cr3寄存器,即使切换了cr3,所有进程的内核空间视图是一致的(除了某些特殊页),因此当某一进程通过驱动hook内核函数后,系统所有进程都将改变执行路径,同样当我们恢复了hook之后亦是如此。要是有什么办法能打破这样的规则就
好了,当我们的调试器进程运行时执行原始的函数路径,当hook进程运行时执行它自己的hook之后的路径。我们知道,hook技术通常只是修改内核函数的开头几个字节jmp到自己的函数,或者内联修改函数的内部call地址等,不论通过什么形式的hook,一般就是修改一个dword或者几个字节,在已知hook地址和原始字节内容的情况下,恢复hook只需一个mov指令即可,虽然进程切换时并不影响内核地址空间,但是我们也可以在切换时临时修改一些字节。我们的反反调试思想是:在系统从反调试进程切换到其他进程时,恢复原始的hook地址内容,在要切换到反调试进程时,再修改为hook地址。
windows的线程切换散布在内核的各个点上,而且调用形式各不相同,主要函数包括KiSwapThread、KiSwapContext、SwapContext。在线程抢占的情景中,KiDispatchInterrupt直接调用SwapContext完成线程切换;在线程时限用完时,KiQuantumEnd调用KiSwapContext进行切换(KiSwapContext再调用SwapContext完成真正的切换);在线程自愿放弃执行时,则调用KiSwapThread,该函数又调用KiSwapContext完成执行权的转移。在此,我们看到实际完成切换的是核心汇编函数SwapContext。SwapContext也是我们需要处理的函数,在系统线程切换时,我们判断2个线程的进程之一是否含有反调试进程,有的话则进行相关动作,具体是:如果老线程是反调试进程则恢复还原原始hook地址处的内容,如果新线程是反调试进程则还原它自己的原来的hook地址。这里有一个问题,我们是直接在SwapContext函数的开头跳到我们的函数进行以上的判断和恢复吗?我们知道线程切换是系统最频繁调用的函数了,SwapContext本身就是用汇编来写的(为了保证性能),我们的处理是否得当也将直接影响到系统的整体速度,刚才提到的在函数开头进行判断显然不够优雅~在线程切换时,SwapContext会根据是否是同一进程而决定切换cr3寄存器的内容,看一下相关代码:
(代码截自XP sp3)
lkd> x nt!*SwapContext
80546a90 nt!SwapContext =
8054696c nt!KiSwapContext =
805fcd34 nt!VdmSwapContexts =

lkd>uf nt!SwapContext
.
.
.
nt!SwapContext+0x8c:
80546b1c 8b4b40          mov     ecx,dword ptr [ebx+40h]
80546b1f 894104          mov     dword ptr [ecx+4],eax
80546b22 8b6628          mov     esp,dword ptr [esi+28h]
80546b25 8b4620          mov     eax,dword ptr [esi+20h]
80546b28 894318          mov     dword ptr [ebx+18h],eax
80546b2b fb              sti
80546b2c 8b4744          mov     eax,dword ptr [edi+44h]
80546b2f 3b4644          cmp     eax,dword ptr [esi+44h]         比较是否是同一进程
80546b32 c6475000        mov     byte ptr [edi+50h],0
80546b36 7440            je      nt!SwapContext+0xe8 (80546b78)  是同一进程无需切换,直接跳过

nt!SwapContext+0xa8:
80546b38 8b7e44          mov     edi,dword ptr [esi+44h]         取EPROCESS
80546b3b 8b4b48          mov     ecx,dword ptr [ebx+48h]     
80546b3e 314834          xor     dword ptr [eax+34h],ecx
80546b41 314f34          xor     dword ptr [edi+34h],ecx
80546b44 66f74720ffff    test    word ptr [edi+20h],0FFFFh
80546b4a 7571            jne     nt!SwapContext+0x12d (80546bbd) 

nt!SwapContext+0xbc:
80546b4c 33c0            xor     eax,eax

nt!SwapContext+0xbe:
80546b4e 0f00d0          lldt    ax
80546b51 8d8b40050000    lea     ecx,[ebx+540h]
80546b57 e850afffff      call    nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
80546b5c 33c0            xor     eax,eax
80546b5e 8ee8            mov     gs,ax
80546b60 8b4718          mov     eax,dword ptr [edi+18h]         取cr3也即EPROCESS->DirectoryTableBase

80546b63 8b6b40          mov     ebp,dword ptr [ebx+40h]
80546b66 8b4f30          mov     ecx,dword ptr [edi+30h]
80546b69 89451c          mov     dword ptr [ebp+1Ch],eax
80546b6c 0f22d8          mov     cr3,eax                         完成切换
80546b6f 66894d66        mov     word ptr [ebp+66h],cx
80546b73 eb0e            jmp     nt!SwapContext+0xf3 (80546b83)
.
.
.

    为了不影响性能,我们所要做的只是在不同进程切换时做判断,若是同一进程则无需做任何处理,SwapContext函数内部本身就会做相应的判断,我们为什么不直接利用呢?地址80546b36处的je跳转是同一进程的分支,否则接下来的语句便是不同进程,我们修改80546b38处为跳到我们的函数里并进行判断:
(edi老线程,esi新线程)

cmp  dword ptr [edi+44h] , 反调试进程_EPROCESS                 
jmp  _恢复hook分支
cmp  dword ptr [esi+44h] , 反调试进程_EPROCESS  
jmp  _hook分支
mov     edi,dword ptr [esi+44h]      SwapContext函数内部地址80546b38的原指令   
jmp     80546b3b 


_恢复hook分支:
cr0去保护位
mov   [_hook地址1], 原始内容1 
mov   [_hook地址2], 原始内容2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
jmp     80546b3b

_hook分支
cr0去保护位
mov   [_hook地址1], 反调试进程hook函数地址1
mov   [_hook地址2], 反调试进程hook函数地址2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
jmp     80546b3b   

其中hook地址和值是在驱动中定位并收集好的

    如果你觉得上面的方法还是不够优雅的话,下面我就再来介绍一种相对而言稍微优雅的方法。
我们知道,windows的内存寻址是通过三级的页目录,页表来映射的,每个进程都有独立的页表,且进程的系统空间视图是共享相同的页目录的。这一次,我们就来对反调试进程的页表做相应的手脚~我们的思想方法是:修改反调试进程的页表项,让其hook代码的页面为一私有页,这样,反调试进程与其他进程将拥有不同内核代码页,其检测机制便荡然无存了。然后,再在我们的进程里恢复hook地址(当然也可以在反调试进程创建后,加载驱动前修改,这样就不用恢复了~)。
windows内核映象是如何映射的?我们来看一下:
lkd> lm
start    end        module name
804d8000 806e5000   nt         (pdb symbols)          c:\symbols\ntkrpamp.pdb\7D6290E03E32455BB0E035E38816124F1\ntkrpamp.pdb
806e5000 80705d00   hal        (pdb symbols)          c:\symbols\halmacpi.pdb\9875FD697ECA4BBB8A475825F6BF885E1\halmacpi.pdb
a32db000 a331ba80   HTTP       (pdb symbols)          c:\symbols\http.pdb\B5A46191250E412D80E9D9E9DDA2F4DA1\http.pdb
a3610000 a3613d80   vstor2_ws60   (no symbols)           
a3614000 a3665c00   srv        (pdb symbols)          c:\symbols\srv.pdb\069184FEBE104BFDA9E51021B9B472D92\srv.pdb
a368e000 a375ce00   vmx86      (no symbols)           
a3785000 a37b1180   mrxdav     (pdb symbols)          c:\symbols\mrxdav.pdb\EDD7D9E6E63B43DBA5059A72CE89286E1\mrxdav.pdb
a3a46000 a3a5a480   wdmaud     (pdb symbols)          c:\symbols\wdmaud.pdb\D3271BFD135D4C2B9B1EEED4E26003E22\wdmaud.pdb
a3ae3000 a3af2a00   vmci       (export symbols)       \??\C:\WINDOWS\system32\Drivers\vmci.sys
a3b27000 a3b2ae80   DbgMsg     (no symbols)           
a3cb3000 a3cc8880   irda       (no symbols)   
'
'
'
lkd> !pte 804d8000
               VA 804d8000
PDE at 00000000C0602010    PTE at 00000000C04026C0
contains 00000000004009E3  contains 0000000000000000
pfn 400        -GLDA--KWEV    LARGE PAGE pfn 4d8       

    其中L是指使用大页面来映射的,这表明内核的代码和数据是在一页(4m或pae下2m的大页)中,而我们要修改的只是代码页,数据页必须映射到相同物理页以维持系统的一致性。因此,我们在反调试进程中,为内核映象对应的PDE申请相应的页表,在页表中,我们将原内核映象的数据页对应的pte设置为相同的pfn,而代码页设置为我们私有页,事实上,代码页中也无需全部私有,只需要把hook函数所在的页面改为私有pfn即可,其他页面可仍为原始pfn,从而避免不必要的内存浪费。然后我们恢复hook,结果反调试进程和其他进程会拥有不同内核函数的执行路径了,反调试保护也随之为我们突破~
仔细看看,经过上面的处理真的就可以了吗?答案当然是否定的。看看上面的 !pte 804d8000命令的结果-GLDA--KWEV,其中G表示全局页,全局页标志是为了提升系统性能,因为内核地址空间是共用的,所以cpu在冲刷内部TLB时,只是冲走了没有G标志的TLB项,当然,这并不是说全局页就永远不会消失,TLB缓存项是有限的,cpu会以FIFO规则替换所有的TLB项。可能有人感到奇怪,在SwapContext函数中并没有显示的冲刷TLB的指令,这是因为:如果是同一进程中,则无需冲刷;如果是不同进程,那么在更改cr3的同时,已经隐式的执行了冲刷命令。我们的目的是在切换到反调试进程时,冲刷掉全局页,使其使用自己的私有页。那么如何做到呢?cpu内部的cr4寄存器中位7是PGE(Page Global Enable)位,为1时启用全局页功能,为0是禁止。当全局页禁用时,冲刷TLB的话则全部TLB项都会无效。所以我们上面说的修改反调试进程的pde及pte中都不得含有G标志,我们在SwapContext非同一进程的分支做如下处理:
cmp     dword ptr [esi+44h] , 反调试进程_EPROCESS
mov     edi,dword ptr [esi+44h]                     执行80546b38原始指令
jne     80546b3b                                      不是,直接跳回

mov     eax , cr4                                     eax内容无需保存,见代码即知
push    eax                                            保存cr4内容
and     eax , ~(1 << CR4_PGE)                       去PGE位
mov     cr4 , eax    
mov     cr3 , _反调试进程cr3值                       冲刷所有TLB项 
pop     cr4                                            恢复cr4
jmp     80546b3b 

这样,在反调试进程自身上下文中任何检测都将无效,因为我们根本不会碰它的任何代码逻辑,当然,上面的代码无法突破一些在任意上下文中运行代码的检测机制,比如dpctimer,workitem,Watchdog Timers以及System Threads,然而这些机制其实很可以很容易的突破,比如枚举查找系统的dpc定时器并删除是很简单的,系统线程也很容易被停掉。

再将思维发散一下,驱动在改变一个内核函数的路径时必定先要获得该函数的地址,或者一个相对的基准函数,无论其是通过静态IAT导入函数,还是手工IAT搜索,还是动态MmGetSystemRoutineAddress,还是read内核文件,我们在之前做相关手脚,在其获取函数时提供给他一个虚假地址,当然,这是原函数的一个副本,以便他能找到内部相应的hook地址。好的,让他hook修改然后检测去吧~~

   以上只是本人的拙劣想法和见解而已,希望它对你有用~

啊冲 2016-02-03 10:44
反调试技巧总结-原理和实现

一、 前言
    前段学习反调试和vc,写了antidebug-tester,经常会收到message希望交流或索要实现代码,我都没有回复。其实代码已经在编程版提供了1个版本,另其多是vc内嵌asm写的,对cracker而言,只要反下就知道了。我想代码其实意义不是很大,重要的是理解和运用。
    做个简单的总结,说明下实现原理和实现方法。也算回复了那些给我发Message的朋友。

    部分代码和参考资料来源:
1、<<脱壳的艺术>> hawking
2、<> Angeljyt
3、http://bbs.pediy.com 
4、<<软件加密技术内幕>> 看雪学院
5、<> Peter Ferrie

我将反调试技巧按行为分为两大类,一类为检测,另一类为攻击,每类中按操作对象又分了五个小类:
1、 通用调试器     包括所有调试器的通用检测方法
2、 特定调试器     包括OD、IDA等调试器,也包括相关插件,也包括虚拟环境
3、 断点           包括内存断点、普通断点、硬件断点检测
4、 单步和跟踪     主要针对单步跟踪调试
5、 补丁           包括文件补丁和内存补丁
反调试函数前缀
              检测        攻击
通用调试器     FD_        AD_
特定调试器     FS_        AS_
断点           FB_        AB_
单步和跟踪     FT_        AT_
补丁           FP_        AP_

声明:
1、本文多数都是摘录和翻译,我只是重新组合并翻译,不会有人告侵权吧。里面多是按自己的理解来说明,可能有理解错误,或有更好的实现方法,希望大家帮忙指出错误。
2、我并没有总结完全,上面的部分分类目前还只有很少的函数甚至空白,等待大家和我一起来完善和补充。我坚信如果有扎实的基础知识,丰富的想像力,灵活的运用,就会创造出更多的属于自己的反调试。而最强的反调试,通常都是自己创造的,而不是来自别人的代码。

二、 查找-通用调试器(FD_)
函数列表如下,后面会依次说明,需事先说明的是,这些反调试手段多数已家喻户晓,目前有效的不多,多数已可以通过OD的插件顺利通过,如果你想验证它们的有效性,请关闭OD的所有反反调试插件:
复制代码
bool FD_IsDebuggerPresent();
bool FD_PEB_BeingDebuggedFlag();
bool FD_PEB_NtGlobalFlags();
bool FD_Heap_HeapFlags();
bool FD_Heap_ForceFlags();
bool FD_Heap_Tail();
bool FD_CheckRemoteDebuggerPresent();
bool FD_NtQueryInfoProc_DbgPort();
bool FD_NtQueryInfoProc_DbgObjHandle();
bool FD_NtQueryInfoProc_DbgFlags();
bool FD_NtQueryInfoProc_SysKrlDbgInfo();
bool FD_SeDebugPrivilege();
bool FD_Parent_Process();
bool FD_DebugObject_NtQueryObject();
bool FD_Find_Debugger_Window();
bool FD_Find_Debugger_Process();
bool FD_Find_Device_Driver();
bool FD_Exception_Closehandle();
bool FD_Exception_Int3();
bool FD_Exception_Popf();
bool FD_OutputDebugString();
bool FD_TEB_check_in_Vista();
bool FD_check_StartupInfo();
bool FD_Parent_Process1();
bool FD_Exception_Instruction_count();
bool FD_INT_2d();
复制代码


2.1 FD_IsDebuggerPresent()
对调试器来说,IsDebuggerPresent是臭名昭著的恶意函数。不多说了,它是个检测调试的api函数。实现更简单,只要调用IsDebuggerPresent就可以了。在调用它之前,可以加如下代码,以用来检测是否在函数头有普通断点,或是否被钩挂。
  //check softbreak
  if(*(BYTE*)Func_addr==0xcc)
    return true;
  //check hook
  if(*(BYTE*)Func_addr!=0x64)
    return true;


2.2 FD_PEB_BeingDebuggedFlag
我们知道,如果程序处于调试器中,那么在PEB结构中有个beingDegug标志会被设置,直接读取它就可判断是否在调试器中。实际上IsDebuggerPresent就是这么干的。
复制代码
  __asm
  {
    mov eax, fs:[30h] ;EAX =  TEB.ProcessEnvironmentBlock
    inc eax
    inc eax
    mov eax, [eax]
    and eax,0x000000ff  ;AL  =  PEB.BeingDebugged
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.3 FD_PEB_NtGlobalFlags
PEB中还有其它FLAG表明了调试器的存在,如NtGlobalFlags。它位于PEB环境中偏移为0x68的位置,默认情况下该值为0,在win2k和其后的windows平台下,如果在调试中,它会被设置为一个特定的值。使用该标志来判断是否被调试并不可靠(如在winnt中),但这种方法却也很常用。这个标志由下面几个标志组成:
***_HEAP_ENABLE_TAIL_CHECK (0x10)
***_HEAP_ENABLE_FREE_CHECK (0x20)
***_HEAP_VALIDATE_PARAMETERS (0x40)
检测NtGlobalFlags的方法如下,这个方法在ExeCryptor中使用过。
复制代码
__asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+68h]
    and eax, 0x70
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.4 FD_Heap_HeapFlags()
同样,调试器也会在堆中留下痕迹,你可以使用kernel32_GetProcessHeap()函数,如果你不希望使用api函数(以免暴露),则可以直接在PEB中寻找。同样的,使用HeapFlags和后面提到的ForceFlags来检测调试器也不是非常可靠,但却很常用。
这个域由一组标志组成,正常情况下,该值应为2。
复制代码
  __asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+18h] ;PEB.ProcessHeap
    mov eax, [eax+0ch] ;PEB.ProcessHeap.Flags
    cmp eax, 2
    jne rt_label
    jmp rf_label
  }
复制代码


2.5 FD_Heap_ForceFlags
进程堆里另外一个标志,ForceFlags,它也由一组标志组成,正常情况下,该值应为0。

复制代码
  __asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+18h] ;PEB.ProcessHeap
    mov eax, [eax+10h] ;PEB.ProcessHeap.ForceFlags
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.6 FD_Heap_Tail
如果处于调试中,堆尾部也会留下痕迹。标志HEAP_TAIL_CHECKING_ENABLED 将会在分配的堆块尾部生成两个0xABABABAB。如果需要额外的字节来填充堆尾,HEAP_FREE_CHECKING_ENABLED标志则会生成0xFEEEFEEE。

据说Themida使用过这个反调试
复制代码
  __asm
  {
    mov eax, buff
    ;get unused_bytes
    movzx ecx, byte ptr [eax-2]
    movzx edx, word ptr [eax-8] ;size
    sub eax, ecx
    lea edi, [edx*8+eax]
    mov al, 0abh
    mov cl, 8
    repe sca**
    je rt_label
    jmp rf_label
  }
复制代码


2.7 FD_CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent是另一个检测调试的api,只是可惜它似乎只能在winxp sp1版本以后使用。它主要是用来查询一个在winnt时就有的一个数值,其内部会调用NtQueryInformationProcess(),我是这样实现的:
复制代码
  FARPROC Func_addr ;
  HMODULE hModule = GetModuleHandle("kernel32.dll");
  if (hModule==INVALID_HANDLE_VALUE)
    return false;
  (FARPROC&) Func_addr =GetProcAddress(hModule, "CheckRemoteDebuggerPresent");
  if (Func_addr != NULL) 
  {
    __asm 
    {
      push  eax;
      push  esp;
      push  0xffffffff;
      call  Func_addr;
      test  eax,eax;
      je    rf_label;
      pop    eax;
      test  eax,eax
      je    rf_label;
      jmp    rt_label;
    }
  }
复制代码


2.8 FD_NtQueryInfoProc_DbgPort
使用ntdll_NtQueryInformationProcess()来查询ProcessDebugPort可以用来检测反调试。如果进程被调试,其返回值应为0xffffffff。
下面的代码应该是从pediy里copy过来的,时间太长,不记得是哪位兄弟的代码了。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  PROCESS_DEBUG_PORT_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), ProcessDebugPort, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.DebugPort)
      return true;
    else
      return false;
复制代码


2.9 FD_NtQueryInfoProc_DbgObjHandle
  在winxp中引入了"debug object".当一个调试活动开始,一个"debug object"被创建,同也相应产生了一个句柄。使用为公开的ProcessDebugObjectHandle类,可以查询这个句柄的数值。
  代码可能还是从pediy里复制的,不记得了。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  _PROCESS_DEBUG_OBJECTHANDLE_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), (PROCESS_INFO_CLASS)0x0000001e, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.ObjectHandle)
      return true;
    else
      return false;
复制代码


2.10 FD_NtQueryInfoProc_DbgFlags();
同样的未公开的ProcessDebugFlags类,当调试器存在时,它会返回false。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  _PROCESS_DEBUG_FLAGS_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), (PROCESS_INFO_CLASS)0x0000001f, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.Debugflags)
      return false;
    else
      return true;
复制代码


2.11 FD_NtQueryInfoProc_SysKrlDbgInfo()
这个方法估计对大家用处不大,SystemKernelDebuggerInformation类同样可以用来识别调试器,只是可惜在windows下无效,据称可以用在reactOS中。
复制代码
   HMODULE hModule = GetModuleHandle("ntdll.dll"); 
    ZW_QUERY_SYSTEM_INFORMATION ZwQuerySystemInformation; 
    ZwQuerySystemInformation = (ZW_QUERY_SYSTEM_INFORMATION)GetProcAddress(hModule, "ZwQuerySystemInformation"); 
    if (ZwQuerySystemInformation == NULL) 
        return false;
    SYSTEM_KERNEL_DEBUGGER_INFORMATION Info; 
    if (STATUS_SUCCESS == ZwQuerySystemInformation(SystemKernelDebuggerInformation, &Info, sizeof(Info), NULL)) 
    { 
        if (Info.DebuggerEnabled) 
        { 
            if (Info.DebuggerNotPresent) 
                return false;
            else 
                return true;
        } 
        else 
            return false;
    } 
    else 
       return true;
复制代码


2.12 FD_SeDebugPrivilege()
  当一个进程获得SeDebugPrivilege,它就获得了对CSRSS.EXE的完全控制,这种特权也会被子进程继承,也就是说一个被调试的程序如果获得了CSRSS.EXE的进程ID,它就可以使用openprocess操作CSRSS.EXE。获得其进程ID有很多中方法,如Process32Next,或NtQuerySystemInformation,在winxp下可以使用CsrGetProcessId。
hTmp=OpenProcess(PROCESS_ALL_ACCESS,false,PID_csrss);
    if(hTmp!=NULL)
    {
      CloseHandle(hProcessSnap );
      return true;
    }


2.13 FD_Parent_Process()
通常我们都直接在windows界面下运行应用程序,这样的结果就是它的父进程为"explorer.exe",这个反调试就是检测应用程序的父进程是否为"explorer.exe",如不是则判定为处于调试器中,这也不是百分百可靠,因为有的时候你的程序是在命令行提示符下运行的。
Yoda使用了这个反调试,它使用Process32Next检测父进程,目前很多插件已经通过使Process32Next始终返回false来越过这个反调试(比如HideOD)。不过可以对代码做些简单的修正来处理这个反反调试。

2.14 FD_DebugObject_NtQueryObject();
  如前面所描述的,当一个调试活动开始,一个"debug object"被创建,同也相应产生了一个句柄。我们可以查询这个调试对象列表,并检查调试对象的数量,以实现调试器的检测。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  PNtQueryObject NtQueryObject;
  NtQueryObject = (PNtQueryObject)GetProcAddress(hModule,"NtQueryObject");

  if(NtQueryObject==NULL)
    return false;
  unsigned char szdbgobj[25]=
  "\x44\x00\x65\x00\x62\x00\x75\x00\x67\x00\x4f\x00\x62\x00\x6a\x00\x65\x00\x63\x00\x74\x00\x00\x00";
  unsigned char *psz=&szdbgobj[0];
  __asm
  {
    xor    ebx,ebx;
    push  ebx;
    push  esp;
    push  ebx;
    push  ebx;
    push  3;
    push  ebx;
    Call  dword ptr [NtQueryObject];
    pop  edi;
    push  4;
    push  1000h;
    push  edi;
    push  ebx;
      call  dword ptr [VirtualAlloc];
    push  ebx;
    push  edi;
    push  eax;
    push  3;
    push  ebx;
    xchg  esi,eax;
    Call  dword ptr [NtQueryObject];
    lodsd;
    xchg  ecx,eax;
lable1:  lodsd;
    movzx  edx,ax;
    lodsd;
    xchg  esi,eax;
    cmp    edx,16h;
    jne    label2;
    xchg  ecx,edx;
    mov    edi,psz;
    repe  cmp**;
    xchg  ecx,edx;
    jne    label2;
    cmp    dword ptr [eax],edx
    jne    rt_label;
lable2:  add    esi,edx
    and    esi,-4;
    lodsd
    loop  label1;
  }
  return false;
rt_label:
  return true;
复制代码


2.15 FD_Find_Debugger_Window();
通过列举运行的应用程序的窗口,并于常用调试相关工具比对的方法,应该很常用了,就不多说了。这个也是个可以自行增加项目的函数,你可以将一些常用的调试工具归入其中,比如OD,IDA,WindBG,SoftICE等,你也可以添加任何你需要的,比如"Import REConstructor v1.6 FINAL (C) 2001-2003 MackT/uCF","Registry Monitor - Sysinternals: www.sysinternals.com"等等。
复制代码
  //ollyice
    hWnd=CWnd::FindWindow(_T("1212121"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg v1.1
    hWnd=CWnd::FindWindow(_T("icu_dbg"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollyice pe--diy
    hWnd=CWnd::FindWindow(_T("pe--diy"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg ?-°?
    hWnd=CWnd::FindWindow(_T("ollydbg"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg ?-°?
    hWnd=CWnd::FindWindow(_T("odbydyk"),NULL);
    if (hWnd!=NULL)
    return true;
  //windbg
    hWnd=CWnd::FindWindow(_T("WinDbgFrameClass"),NULL);
    if (hWnd!=NULL)
    return true;
  //dede3.50
    hWnd=CWnd::FindWindow(_T("TDeDeMainForm"),NULL);
    if (hWnd!=NULL)
    return true;
  //IDA5.20
    hWnd=CWnd::FindWindow(_T("TIdaWindow"),NULL);
    if (hWnd!=NULL)
    return true;
  //others
    hWnd=CWnd::FindWindow(_T("TESTDBG"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("kk1"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("Eew75"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("Shadow"),NULL);
    if (hWnd!=NULL)
    return true;
  //PEiD v0.94
    hWnd=CWnd::FindWindow(NULL,"PEiD v0.94");
    if (hWnd!=NULL)
    return true;
  //RegMON
    hWnd=CWnd::FindWindow(NULL,"Registry Monitor - Sysinternals: www.sysinternals.com");
    if (hWnd!=NULL)
    return true;
  //File Monitor
    hWnd=CWnd::FindWindow(NULL,"File Monitor - Sysinternals: www.sysinternals.com");
    if (hWnd!=NULL)
    return true;
  //Import Rec v1.6
    hWnd=CWnd::FindWindow(NULL,"Import REConstructor v1.6 FINAL (C) 2001-2003 MackT/uCF");
    if (hWnd!=NULL)
    return true;
  return false;
复制代码


2.16 FD_Find_Debugger_Process();
  与上面的方法类似,区别是这个反调试用通过查询进程名字与已知的常用调试器应用程序名字进行比对,以确定是否有调试器处于运行状态。
复制代码
    if(strcmp(pe32.szExeFile,"OLLYICE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"IDAG.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"OLLYDBG.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"PEID.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"SOFTICE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"LORDPE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"IMPORTREC.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"W32DSM89.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"WINDBG.EXE")==0)
        return true;
复制代码




2.17 FD_Find_Device_Driver()
  调试工具通常会使用内核驱动,因此如果尝试是否可以打开一些调试器所用到的设备,就可判断是否存在调试器。常用的设备名称如下:
复制代码
\\.\SICE  (SoftICE)
\\.\SIWVID(SoftICE)    
\\.\NTICE  (SoftICE)    
\\.\REGVXG(RegMON)
\\.\REGVXD(RegMON)
\\.\REGSYS(RegMON)
\\.\REGSYS(RegMON)
\\.\FILEVXG(FileMON)
\\.\FILEM(FileMON)
\\.\TRW(TRW2000)
复制代码


2.18 FD_Exception_Closehandle()
  如果给CloseHandle()函数一个无效句柄作为输入参数,在无调试器时,将会返回一个错误代码,而有调试器存在时,将会触发一个EXCEPTION_INVALID_HANDLE (0xc0000008)的异常。
复制代码
  __try  
  {
    CloseHandle(HANDLE(0x00001234));
    return false;
  }
  __except(1)
  {
    return true;
  }
复制代码



2.19 FD_Exception_Int3()
  通过Int3产生异常中断的反调试比较经典。当INT3 被执行到时, 如果程序未被调试, 将会异常处理器程序继续执行。而INT3指令常被调试器用于设置软件断点,int 3会导致调试器误认为这是一个自己的断点,从而不会进入异常处理程序。

复制代码
  __asm 
  {
    push   offset exception_handler; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    test   eax,eax; check the flag 
    je    rt_label
    jmp    rf_label

exception_handler:
    mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
    mov    dword ptr [eax+0xb0],0xffffffff;set flag (ContextRecord.EAX)
    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    xor   eax,eax
    retn

rt_label:
    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
rf_label:
    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


2.20 FD_Exception_Popf()
我们都知道标志寄存器中的陷阱标志,当该标志被设置时,将产生一个单步异常。在程序中动态设置这给标志,如果处于调试器中,该异常将会被调试器捕获。
可通过下面的代码设置标志寄存器。
    pushf 
    mov dword ptr [esp], 0x100
    popf


2.21 FD_OutputDebugString()
  在有调试器存在和没有调试器存在时,OutputDebugString函数表现会有所不同。最明显的不同是, 如果有调试器存在,其后的GetLastError()的返回值为零。

  OutputDebugString("");
  tmpD=GetLastError();
  if(tmpD==0)
    return true;
  return false;


2.22 FD_TEB_check_in_Vista();
  这是从windows anti-debug reference里拷贝出来的,据说是适用于vista系统下检测调试器。我没有vista所以也没有测试。有条件的可以试下,有问题帮忙反馈给我。多谢。
复制代码
    //vista
    __asm
    {
      push   offset exception_handler; set exception handler
      push  dword ptr fs:[0h]
      mov    dword ptr fs:[0h],esp  
      xor   eax,eax;reset EAX invoke int3
      int    3h
      pop    dword ptr fs:[0h];restore exception handler
      add   esp,4
      mov eax, fs:[18h] ; teb
      add eax, 0BFCh 
      mov ebx, [eax] ; pointer to a unicode string 
      test ebx, ebx ; (ntdll.dll, gdi32.dll,...) 
      je      rf_label
      jmp    rt_label
  exception_handler:
      mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
      inc   dword ptr [eax+0xb8];set ContextRecord.EIP
      xor   eax,eax
      retn
    } 
复制代码


2.23 FD_check_StartupInfo();
  这是从pediy上拷贝来的。Window创建进程的时候会把STARTUPINFO结构中的值设为0,而通过调试器创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断是否在调试程序。
复制代码
  STARTUPINFO si;
  ZeroMemory( &si, sizeof(si) );
  si.cb = sizeof(si);
  GetStartupInfo(&si);
  if ( (si.dwX != 0) || (si.dwY !=0) 
    || (si.dwXCountChars != 0) || (si.dwYCountChars !=0 ) 
    || (si.dwFillAttribute != 0) || (si.dwXSize != 0) 
    || (si.dwYSize != 0) )
    return true;
  else  
    return false;
复制代码


2.24 FD_Parent_Process1()
与前面的FD_Parent_Process原理一样,唯一不同的是使用ZwQueryInformationProcess检测父进程,而没有使用Process32Next,这有一个好处是可以绕过OD的HideOD插件。

2.25 FD_Exception_Instruction_count()
  好像《软件加解密技术》中有提到这个反调试。
  通过注册一个异常句柄,在特定地址设置一些硬件断点,当通过这些地址时都会触发EXCEPTION_SINGLE_STEP (0x80000004)的异常,在异常处理程序中,将会调整指令指针到一条新指令,然后恢复运行。可以通过进入进程context结构来设置这些断点,有些调试器不能处理那些不是自己设置的硬件断点,从而导致一些指令将会被漏掉计数,这就形成了一个反调试。
复制代码
  __asm
  {
    xor    eax,eax;
    cdq;
    push  e_handler;
    push  dword ptr fs:[eax];
    mov    fs:[eax],esp;
    int 3;
hwbp1:  nop
hwbp2:  nop
hwbp3:  nop
hwbp4:  nop
    div    edx
    nop
    pop    dword ptr fs:[0]
    add    esp,4
    cmp    al,4;
    jne    rt_label;
    jmp    rf_label;

e_handler:
    xor    eax,eax;
    ;ExceptionRecord
    mov    ecx,dword ptr[esp+0x04]
    ;Contextrecord
    mov    edx,dword ptr[esp+0x0c]
    ;ContextEIP
    inc    byte ptr[edx+0xb8];
    
    ;ExceptionCode
    mov    ecx,dword ptr[ecx];

    ;1.EXCEPTION_INT_DIVIDE_BY_ZERO
    cmp    ecx,0xc0000094;
    jne    Ex_next2;
    ;Context_eip
    inc    byte ptr[edx+0xb8];
    mov    dword ptr[edx+0x04],eax;dr0
    mov    dword ptr[edx+0x08],eax;dr1
    mov    dword ptr[edx+0x0c],eax;dr2
    mov    dword ptr[edx+0x10],eax;dr3
    mov    dword ptr[edx+0x14],eax;dr6
    mov    dword ptr[edx+0x18],eax;dr7
    ret

    ;2.EXCEPTION_BREAKPOINT
Ex_next2:
    cmp    ecx,0x80000003;
    jne    Ex_next3;

    mov    dword ptr[edx+0x04],offset hwbp1;dr0
    mov    dword ptr[edx+0x08],offset hwbp2;dr1
    mov    dword ptr[edx+0x0c],offset hwbp3;dr2
    mov    dword ptr[edx+0x10],offset hwbp4;dr3
    mov    dword ptr[edx+0x18],0x155;dr7
    ret

    ;3.EXCEPTION_SINGLE_STEP
Ex_next3:
    cmp  ecx,0x80000004
    jne    rt_label
    ;CONTEXT_Eax
    inc    byte ptr[edx+0xb0]
    ret
  }
复制代码


2.26 FD_INT_2d()
在windows anti-debug reference中指出,如果程序未被调试这个中断将会生产一个断点异常. 被调试并且未使用跟踪标志执行这个指令, 将不会有异常产生程序正常执行. 如果被调试并且指令被跟踪, 尾随的字节将被跳过并且执行继续. 因此, 使用 INT 2Dh 能作为一个强有力的反调试和反跟踪机制。

复制代码
  __try
  {
    __asm
    {
        int 2dh
      inc eax;any opcode of singlebyte.
      ;or u can put some junkcode,"0xc8"..."0xc2"..."0xe8"..."0xe9"
    }
  return true;
  }
  __except(1)
  {
    return false;
  }
复制代码


三、  检测-专用调试器(FS_)
    这一部分是我比较喜欢的,但内容还不是很丰富,比如:
1、  针对SoftIce的检测方法有很多,但由于我从没使用过Softice,也没有条件去测试,所以没有给出太多,有兴趣的可以自己查阅资料进行补充,针对softice网上资料较多,或查阅《软件加解密技术》。
2、  同样,这里也没有给出windbg等等其它调试器的检测方法。
3、  而针对Odplugin,也只给了几种HideOD的检测。事实上,目前OD的使用者通常都使用众多的强大插件,当OD的反调试越来越普遍时,自己设计几款常用的OD插件的反调试,将会是非常有效的反调试手段。
4、  对VME的检测也只给出了两种,如想丰富这一部分可以参考Peter Ferrie的一篇anti-vme的文章(http://bbs.pediy.com/showthread.php?t=68411)。里面有非常多的anti-vme方法。

    针对专用调试器的函数列表如下:
复制代码
//find specific debugger
bool FS_OD_Exception_GuardPages();
bool FS_OD_Int3_Pushfd();
bool FS_SI_UnhandledExceptionFilter();
bool FS_ODP_Process32NextW();
bool FS_ODP_OutputDebugStringA();
bool FS_ODP_OpenProcess();
bool FS_ODP_CheckRemoteDebuggerPresent();
bool FS_ODP_ZwSetInformationThread();
bool FS_SI_Exception_Int1();
bool IsInsideVMWare_();
bool FV_VMWare_VMX();
bool FV_VPC_Exception();
int FV_VME_RedPill();//0:none,1:vmvare;2:vpc;3:others
复制代码


3.1 FS_OD_Exception_GuardPages
    “保护页异常”是一个简单的反调试技巧。当应用程序尝试执行保护页内的代码时,将会产生一个EXCEPTION_GUARD_PAGE(0x80000001)异常,但如果存在调试器,调试器有可能接收这个异常,并允许该程序继续运行,事实上,在OD中就是这样处理的,OD使用保护页来实现内存断点。
最开始实现时忘记了free申请的空间,多谢sessiondiy提醒。
复制代码
  SYSTEM_INFO sSysInfo;
  LPVOID lpvBase;
  BYTE * lptmpB;
  GetSystemInfo(&sSysInfo);
  DWORD dwPageSize=sSysInfo.dwPageSize;
  DWORD flOldProtect;

  DWORD dwErrorcode;

  lpvBase=VirtualAlloc(NULL,dwPageSize,MEM_COMMIT,PAGE_READWRITE);
  if(lpvBase==NULL)
    return false;
  
  lptmpB=(BYTE *)lpvBase;
  *lptmpB=0xc3;//retn

  VirtualProtect(lpvBase,dwPageSize,PAGE_EXECUTE_READ | PAGE_GUARD,&flOldProtect);
  
  __try
  {
    __asm  call dword ptr[lpvBase];
    VirtualFree(lpvBase,0,MEM_RELEASE);
    return true;
  }
  __except(1)
  {
    VirtualFree(lpvBase,0,MEM_RELEASE);
    return false;
  }
复制代码


3.2 FS_OD_Int3_Pushfd
    这是个最近比较牛X的反调试,据称是vmp1.64里发现的,好像ttprotect里面也有使用,我没有验证。Pediy里有帖子详细讨论,我是看到gkend的分析,才搞懂一些。下面摘自gkend分析
代码:


    int3,pushfd和int3,popfd一样的效果。只要修改int3后面的popfd为其他值,OD都能通过。老掉牙的技术又重新被用了。SEH异常机制的运用而已。
    原理:在SEH异常处理中设置了硬件断点DR0=EIP+2,并把EIP的值加2,那么应该在int3,popfd后面的指令执行时会产生单步异常。但是OD遇到前面是popfd/pushfd时,OD会自动在popfd后一指令处设置硬件断点,而VMP的seh异常处理会判断是否已经设置硬件断点,如果已经有硬件断点就不产生单步异常,所以不能正常执行。


    http://bbs.pediy.com/showthread.php?t=67737
    大家也可以仔细研究下OD下的pushfd,popfd等指令,相信利用它们可以构造很多反调试,下面是我实现的一个,不过现在看起来有点没看懂,不知当时为什么用了两个int3。
复制代码
  __asm
  {
    push   offset e_handler; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    pushfd
    nop
    nop
    nop
    nop
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    test   eax,eax; check the flag 
    je    rf_label
    jmp    rt_label

e_handler:
    push   offset e_handler1; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    nop
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4
    ;EAX = ContextRecord
    mov    ebx,eax;dr0=>ebx
    mov   eax,dword ptr [esp+0xc]
    ;set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];
    mov    dword ptr [eax+0xb0],ebx;dr0=>eax
    xor    eax,eax
    retn

e_handler1:
    ;EAX = ContextRecord
    mov   eax,dword ptr [esp+0xc]
    ;set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];
    mov    ebx,dword ptr[eax+0x04]
    mov    dword ptr [eax+0xb0],ebx;dr0=>eax
    xor    eax,eax
    retn
rt_label:
    xor  eax,eax
    inc eax
    mov esp,ebp
    pop  ebp
    retn
rf_label:
    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码




3.3 FS_SI_UnhandledExceptionFilter
    这个针对SoftIce的反调试很简单,好像是SoftIce会修改UnhandledExceptionFilter这个函数的第一个字节为CC。因此判断这个字节是否为cc,就是一种检查softice的简便方法。
复制代码
FARPROC Uaddr ;
BYTE tmpB = 0;
(FARPROC&) Uaddr =GetProcAddress ( GetModuleHandle("kernel32.dll"),"UnhandledExceptionFilter");
tmpB = *((BYTE*)Uaddr);   // 取UnhandledExceptionFilter函数第一字节
tmpB=tmpB^0x55;
if(tmpB ==0x99)           // 如该字节为CC,则SoftICE己加载
  return true;
else  
  return false;
复制代码


3.4 FS_ODP_Process32NextW
    当我在调试FD_parentprocess时,感觉总是怪怪的,使用OD时运行Process32NextW总是返回失败,搞了一个晚上,才搞懂原来是OD的插件HideOD在作怪。当HideOD的Process32NextW的选项被选中时,它会更改Process32NextW的返回值,使其始终返回false,这主要是HideOD针对FD_parentprocess这个反调试的一个反反调试。但也正是这一点暴露的它的存在。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  //1.Process32Next
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"Process32NextW");
  if (Func_addr != NULL) 
  {
    tmpW=*(WORD*)Func_addr;
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0xFF8B)//maybe option of Process32Next is selected.
        return true;
      break;
    default:
      if(tmpW==0xC033)
        return true;
      break;
    }
  }
复制代码


    但上面的代码并不完美,因为有跨平台问题,所以要先获得当前操作系统版本。目前只在win2k和winxp下进行了测试。

3.5 FS_ODP_OutputDebugStringA
    同样,HIDEOD的OutputDebugStringA选项,也对OutputDebugStringA这个api做了处理,具体修改内容我记不得了,大家可以自己比对一下。我的代码如下:
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  //2.OutputDebugStringA
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"OutputDebugStringA");
  if (Func_addr != NULL) 
  {
    tmpW=*(WORD*)Func_addr;
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0x3468)//maybe option of OutputDebugStringAt is selected.
        return true;
      break;
    default:
      if(tmpW==0x01e8)
        return true;
      break;
    }
  }
  return false;
复制代码


3.6 FS_ODP_OpenProcess
    这个据称这个是针对HideDebugger这个插件的,当这个插件开启时,它会挂钩OpenProcess这个函数,它修改了OpenProcess的前几个字节。因此检测这几个字节就可实现这个反调试。
复制代码
  FARPROC Func_addr;
  BYTE tmpB;
  //OpenProcess
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"OpenProcess");
  if (Func_addr != NULL) 
  {
    tmpB=*((BYTE*)Func_addr+6);
    if(tmpB==0xea)//HideDebugger Plugin of OD is present
        return true;
  }
  return false;
复制代码


3.7 FS_ODP_CheckRemoteDebuggerPresent
    和前面提到的两个HideOD的反调试类似,不多说了。大家可以自行比对一下开启和不开启HideOD时,CheckRemoteDebuggerPresent函数的异同,就可以设计反这个插件的反调试了。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  BYTE tmpB;
  //2.CheckRemoteDebuggerPresent
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"CheckRemoteDebuggerPresent");
  if (Func_addr != NULL) 
  {
    tmpB=*((BYTE*)Func_addr+10);
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpB!=0x74)//HideOD is present
        return true;
      break;
    default:
      break;
    }
  }
  return false;
复制代码


3.8 FS_ODP_ZwSetInformationThread
    和前面提到的几个HideOD的反调试类似,大家可以自行比对一下开启和不开启HideOD时,ZwSetInformationThread函数的异同,就可以设计反这个插件的反调试了。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  BYTE tmpB0,tmpB1;
  //2.CheckRemoteDebuggerPresent
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"ZwSetInformationThread");
  if (Func_addr != NULL) 
  {
    tmpW=*((WORD*)Func_addr+3);
    tmpB0=*((BYTE*)Func_addr+9);
    tmpB1=*((BYTE*)Func_addr+10);
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0x0300)//HideOD is present
        return true;
      break;
    case OS_win2k:
      if((tmpB0!=0xcd)&&(tmpB1!=0x2e))
        return true;
      break;
    default:
      break;
    }
  }
  return false;
复制代码


3.9 FS_SI_Exception_Int1
    通常int1的DPL为0,这表示"cd 01"机器码不能在3环下执行。如果直接执行这个中断将会产生一个保护错误,windows会产生一个 EXCEPTION_ACCESS_VIOLATION (0xc0000005)异常。然而,如果SOFTICE正在运行,它挂钩了int1,并调整其 DPL为3。这样SoftICE就可以在用户模式执行单步操作了。
    当int 1发生时,SoftICE不检查它是由于陷阱标志位还是由软件中断产生,SoftICE总是去调用原始中断1的句柄,此时将会产生一个 EXCEPTION_SINGLE_STEP (0x80000004)而不是 EXCEPTION_ACCESS_VIOLATION (0xc0000005)异常,这就形成了一个简单的反调试方法。
复制代码
  __asm
  {
    push   offset eh_int1; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset flag(EAX) invoke int3
    int    1h
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    cmp    eax,0x80000004; check the flag 
    je    rt_label_int1
    jmp    rf_label_int1

eh_int1:
    mov    eax,[esp+0x4];
    mov    ebx,dword ptr [eax];
    mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
    mov    dword ptr [eax+0xb0],ebx;set flag (ContextRecord.EAX)

    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    xor   eax,eax
    retn
  }
复制代码


3.10 FV_VMWare_VMX
    这是一个针对VMWare虚拟机仿真环境的反调试,我从网上直接拷贝的代码。
    VMWARE提供一种主机和客户机之间的通信方法,这可以被用做一种VMWare的反调试。Vmware将会处理IN (端口为0x5658/’VX’)指令,它会返回一个majic数值“VMXh”到EBX中。
    当在保护模式操作系统的3环下运行时,IN指令的执行将会产生一个异常,除非我们修改了I/O的优先级等级。然而,如果在VMWare下运行,将不会产生任何异常,同时EBX寄存器将会包含’VMXh’,ECX寄存器也会被修改为Vmware的产品ID。
    这种技巧在一些病毒中比较常用。
    针对VME的反调试,在peter Ferrie的另一篇文章<>中有大量的描述,有兴趣的可以根据这篇文章,将FV_反调试好好丰富一下。


复制代码
bool IsInsideVMWare_()
{
  bool r;
  _asm
  {
    push   edx
    push   ecx
    push   ebx

    mov    eax, 'VMXh'
    mov    ebx, 0 // any value but MAGIC VALUE
    mov    ecx, 10 // get VMWare version
    mov    edx, 'VX' // port number
    in     eax, dx // read port
                   // on return EAX returns the VERSION
    cmp    ebx, 'VMXh' // is it a reply from VMWare?
    setz   [r] // set return value

    pop    ebx
    pop    ecx
    pop    edx
  }
  return r;
}

bool FV_VMWare_VMX()
{
  __try
  {
    return IsInsideVMWare_();
  }
  __except(1) // 1 = EXCEPTION_EXECUTE_HANDLER
  {
    return false;
  }
}
复制代码


3.11 FV_VPC_Exception
    这个代码我也是完整从网上拷贝下来的,具体原理在<>这篇文章里也有详细描述。与VMWare使用一个特殊端口完成主机和客户机间通信的方法类似的是,VirtualPC靠执行非法指令产生一个异常供内核捕获。这个代码如下:
代码:


0F 3F x1 x2
0F C7 C8 y1 y2


    由这两个非法指令引起的异常将会被应用程序捕获,然而,如果VirtualPC正在运行,将不会产生异常。X1,x2的允许的数值还不知道,但有一部分已知可以使用,如0A 00,11 00…等等。


复制代码
__declspec(naked) bool FV_VPC_Exception()
{
  _asm
  {
    push ebp
    mov  ebp, esp

    mov  ecx, offset exception_handler

    push ebx
    push ecx

    push dword ptr fs:[0]
    mov  dword ptr fs:[0], esp

    mov  ebx, 0 // Flag
    mov  eax, 1 // VPC function number
  }

    // call VPC 
   _asm __emit 0Fh
   _asm __emit 3Fh
   _asm __emit 07h
   _asm __emit 0Bh

  _asm
  {
    mov eax, dword ptr ss:[esp]
    mov dword ptr fs:[0], eax

    add esp, 8

    test ebx, ebx
    
    setz al

    lea esp, dword ptr ss:[ebp-4]
    mov ebx, dword ptr ss:[esp]
    mov ebp, dword ptr ss:[esp+4]

    add esp, 8

    jmp ret1
exception_handler:
    mov ecx, [esp+0Ch]
    mov dword ptr [ecx+0A4h], -1 // EBX = -1 -> not running, ebx = 0 -> running
    add dword ptr [ecx+0B8h], 4 // -> skip past the call to VPC
    xor eax, eax // exception is handled
    ret
ret1:
    ret
  }
}
复制代码


3.12 FV_VME_RedPill
    这个方法似乎是检测虚拟机的一个简单有效的方法,虽然还不能确定它是否是100%有效。名字很有意思,红色药丸(为什么不是bluepill,哈哈)。我在网上找到了个ppt专门介绍这个方法,可惜现在翻不到了。记忆中原理是这样的,主要检测IDT的数值,如果这个数值超过了某个数值,我们就可以认为应用程序处于虚拟环境中,似乎这个方法在多CPU的机器中并不可靠。据称ScoobyDoo方法是RedPill的升级版。代码也是在网上找的,做了点小改动。有四种返回结果,可以确认是VMWare,还是VirtualPC,还是其它VME,或是没有处于VME中。


复制代码
   //return value:  0:none,1:vmvare;2:vpc;3:others
   unsigned char matrix[6];

    unsigned char redpill[] = 
        "\x0f\x01\x0d\x00\x00\x00\x00\xc3";

    HANDLE hProcess = GetCurrentProcess();

    LPVOID lpAddress = NULL;
    PDWORD lpflOldProtect = NULL;

    __try
    {
        *((unsigned*)&redpill[3]) = (unsigned)matrix;

        lpAddress = VirtualAllocEx(hProcess, NULL, 6, MEM_RESERVE|MEM_COMMIT , PAGE_EXECUTE_READWRITE);
        
        if(lpAddress == NULL)
            return 0;

        BOOL success = VirtualProtectEx(hProcess, lpAddress, 6, PAGE_EXECUTE_READWRITE , lpflOldProtect);

        if(success != 0)
             return 0;
   
        memcpy(lpAddress, redpill, 8);

        ((void(*)())lpAddress)();

        if (matrix[5]>0xd0) 
        {
          if(matrix[5]==0xff)//vmvare
            return 1;
          else if(matrix[5]==0xe8)//vitualpc
            return 2;
          else
            return 3;
        }
        else 
            return 0;
    }
    __finally
    {
        VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    }
复制代码


四、  检测-断点(FB_)
这一部分内容较少,但实际上可用的方法也比较多,我没有深入研究,不敢乱写,照抄了几个常用的方法:


//find breakpoint
bool FB_HWBP_Exception();
DWORD FB_SWBP_Memory_CRC();
bool FB_SWBP_ScanCC(BYTE * addr,int len);
bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);


4.1 FB_HWBP_Exception
  在异常处理程序中检测硬件断点,是比较常用的硬件断点检测方法。在很多地方都有提到。


复制代码
  __asm
  {
    push   offset exeception_handler; set exception handler
    push   dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor    eax,eax;reset EAX invoke int3
    int    1h
    pop    dword ptr fs:[0h];restore exception handler
    add    esp,4
    ;test if EAX was updated (breakpoint identified) 
    test   eax,eax
    jnz     rt_label
    jmp    rf_label

exeception_handler:
    ;EAX = CONTEXT record
    mov     eax,dword ptr [esp+0xc]

    ;check if Debug Registers Context.Dr0-Dr3 is not zero
    cmp     dword ptr [eax+0x04],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x08],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x0c],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x10],0
    jne     hardware_bp_found
    jmp     exception_ret

hardware_bp_found:
    ;set Context.EAX to signal breakpoint found
    mov     dword ptr [eax+0xb0],0xFFFFFFFF
exception_ret:
    ;set Context.EIP upon return
    inc       dword ptr [eax+0xb8];set ContextRecord.EIP
    inc       dword ptr [eax+0xb8];set ContextRecord.EIP
    xor     eax,eax
    retn
  }
复制代码


4.2 FB_SWBP_Memory_CRC()
  由于在一些常用调试器中,比如OD,其是将代码设置为0xcc来实现普通断点,因此当一段代码被设置了普通断点,则其中必定有代码的修改。因此对关键代码进行CRC校验则可以实现侦测普通断点。但麻烦的是每次代码修改,或更换编译环境,都要重新设置CRC校验值。
  下面的代码拷贝自《软件加解密技术》,里面完成的是对整个代码段的CRC校验,CRC校验值保存在数据段。CRC32算法实现代码网上有很多,就不列出来了。


复制代码
DWORD FB_SWBP_Memory_CRC()
{
  //打开文件以获得文件的大小
  DWORD fileSize,NumberOfBytesRW;
  DWORD CodeSectionRVA,CodeSectionSize,NumberOfRvaAndSizes,DataDirectorySize,ImageBase;
  BYTE* pMZheader;
  DWORD pPEheaderRVA;
  TCHAR  *pBuffer ;
  TCHAR szFileName[MAX_PATH]; 

  GetModuleFileName(NULL,szFileName,MAX_PATH);
  //打开文件
  HANDLE hFile = CreateFile(
    szFileName,
    GENERIC_READ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);
   if ( hFile != INVALID_HANDLE_VALUE )
   {
    //获得文件长度 :
    fileSize = GetFileSize(hFile,NULL);
    if (fileSize == 0xFFFFFFFF) return 0;
    pBuffer = new TCHAR [fileSize];     申请内存,也可用VirtualAlloc等函数申请内存
    ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);//读取文件内容
    CloseHandle(hFile);  //关闭文件
   }
   else
     return 0;
  pMZheader=(BYTE*)pBuffer; //此时pMZheader指向文件头
  pPEheaderRVA = *(DWORD *)(pMZheader+0x3c);//读3ch处的PE文件头指针
  ///定位到PE文件头(即字串“PE\0\0”处)前4个字节处,并读出储存在这里的CRC-32值:

  NumberOfRvaAndSizes=*((DWORD *)(pMZheader+pPEheaderRVA+0x74));//得到数据目录结构数量
  DataDirectorySize=NumberOfRvaAndSizes*0x8;//得到数据目录结构大小
  ImageBase=*((DWORD *)(pMZheader+pPEheaderRVA+0x34));//得到基地址
  //假设第一个区块就是代码区块
  CodeSectionRVA=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0xc));//得到代码块的RVA值
  CodeSectionSize=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0x8));///得到代码块的内存大小
  delete pBuffer;  // 释放内存
  return CRC32((BYTE*)(CodeSectionRVA+ImageBase),CodeSectionSize);
}
复制代码


4.3 FB_SWBP_ScanCC
扫描CC的方法,比照前面校验代码CRC数值的方法更直接一些,它直接在所要检测的代码区域内检测是否有代码被更改为0xCC,0xcc对应汇编指令为int3 ,对一些常用的调试器(如OD)其普通断点就是通过修改代码为int3来实现的。但使用时要注意是否正常代码中就包含CC。通常这个方法用于扫描API函数的前几个字节,比如检测常用的MessageBoxA、GetDlgItemTextA等。


复制代码
bool FB_SWBP_ScanCC(BYTE * addr,int len)
{
  FARPROC Func_addr ;
  HMODULE hModule = GetModuleHandle("USER32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule, "MessageBoxA");
  if (addr==NULL)
    addr=(BYTE *)Func_addr;//for test
  BYTE tmpB;
  int i;
  __try
  {
    for(i=0;i     {
      tmpB=*addr;
      tmpB=tmpB^0x55;
      if(tmpB==0x99)// cmp 0xcc
        return true;
    }
  }
  __except(1)
    return false;
  return false;
}
复制代码


4.4 FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);
此方法类似CRC的方法,只是这里是检测累加和。它与CRC的方法有同样的问题,就是要在编译后,计算累加和的数值,再将该值保存到数据区,重新编译。在这里创建了一个单独的线程用来监视代码段。


复制代码
DWORD WINAPI CheckSum_ThreadFunc( LPVOID lpParam ) 

  DWORD dwThrdParam[3];
  BYTE tmpB;
  DWORD Value=0;
  dwThrdParam[0]=* ((DWORD *)lpParam);
     dwThrdParam[1]=* ((DWORD *)lpParam+1);
      dwThrdParam[2]=* ((DWORD *)lpParam+2);
  BYTE *addr_begin=(BYTE *)dwThrdParam[0];
  BYTE *addr_end=(BYTE *)dwThrdParam[1];
  DWORD sumValue=dwThrdParam[2];
  for(int i=0;i<(addr_end-addr_begin);i++)
    Value=Value+*(addr_begin+i);
  /* //if sumvalue is const,it should be substract.
  DWORD tmpValue;
  Value=Value-(sumValue&0x000000FF);
  tmpValue=(sumValue&0x0000FF00)>>8;
  Value=Value-tmpValue;
  tmpValue=(sumValue&0x0000FF00)>>16;
  Value=Value-tmpValue;
  tmpValue=(sumValue&0x0000FF00)>>24;
  Value=Value-tmpValue;*/
  if (Value!=sumValue)
    MessageBox(NULL,"SWBP is found by CheckSum_ThreadFunc","CheckSum_ThreadFunc",MB_OK|MB_ICONSTOP);
    return 1; 
}

bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue)
{
    DWORD dwThreadId;
  DWORD dwThrdParam[3];
  dwThrdParam[0]=(DWORD)addr_begin;
  dwThrdParam[1]=(DWORD)addr_end;
  dwThrdParam[2]=sumValue;
    HANDLE hThread; 

    hThread = CreateThread( 
        NULL,                        // default security attributes 
        0,                           // use default stack size  
        CheckSum_ThreadFunc,         // thread function 
        &dwThrdParam[0],                // argument to thread function 
        0,                           // use default creation flags 
        &dwThreadId);                // returns the thread identifier 
    // Check the return value for success. 

   if (hThread == NULL) 
      return false;
   else 
   {
      Sleep(1000);
      CloseHandle( hThread );
    return true;
   }
}
复制代码


五、  检测-跟踪(FT_)
个人认为,反跟踪的一些技巧,多数不会非常有效,因为在调试时,多数不会被跟踪经过,除非用高超的技巧将关键代码和垃圾代码及这些反跟踪技巧融合在一起,否则很容易被发现或被无意中跳过。
函数列表如下:


复制代码
//Find Single-Step or Trace
bool FT_PushSS_PopSS();
void FT_RDTSC(unsigned int * time);
DWORD FT_GetTickCount();
DWORD FT_SharedUserData_TickCount();
DWORD FT_timeGetTime();
LONGLONG FT_QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);
bool FT_F1_IceBreakpoint();
bool FT_Prefetch_queue_nop1();
bool FT_Prefetch_queue_nop2();
复制代码


5.1 FT_PushSS_PopSS
这个反调试在<>里有描述,如果调试器跟踪经过下面的指令序列:


复制代码
  __asm
  {
    push ss    //反跟踪指令序列
    ;junk
    pop  ss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
}
复制代码


Pushf将会被执行,同时调试器无法设置压进堆栈的陷阱标志,应用程序通过检测陷阱标志就可以判断处是否被跟踪调试。


复制代码
  __asm
  {
    push ebp
    mov ebp,esp
    push ss    //反跟踪指令序列
    ;junk
    pop  ss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
    and  eax,0x00000100
    jnz  rt_label

    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
rt_label:
    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


5.2 FT_RDTSC
通过检测某段程序执行的时间间隔,可以判断出程序是否被跟踪调试,被跟踪调试的代码通常都有较大的时间延迟,检测时间间隔的方法有很多种。比如RDTSC指令,kernel32_GetTickCount函数,winmm_ timeGetTime 函数等等。
下面为RDTSC的实现代码。


复制代码
  int time_low,time_high;
  __asm
  {
    rdtsc
    mov    time_low,eax
    mov    time_high,edx
  }
复制代码


5.3 FT_GetTickCount
  GetTickCount函数检测时间间隔简单且常用。直接调用即可。具体可查MSDN。

5.4 FT_SharedUserData_TickCount
  直接调用GetTickCount函数来检测时间间隔的方法,虽然简单却容易被发现。而使用GetTickCount的内部实现代码,直接读取SharedUserData数据结构里的数据的方法,更隐蔽些。下面的代码是直接从GetTickCount里扣出来的,其应该是在位于0x7FFE0000地址处的SharedUserData数据接口里面直接取数据,不过这个代码应该有跨平台的问题,我这里没有处理。大家可以完善下。


复制代码
  DWORD tmpD;
  __asm
  {
    mov     edx, 0x7FFE0000
    mov     eax, dword ptr [edx]
    mul     dword ptr [edx+4]
    shrd    eax, edx, 0x18
    mov    tmpD,eax
  }
  return tmpD;
复制代码


5.5 FT_timeGetTime
  使用winmm里的timeGetTime的方法也可以用来检测时间间隔。直接调用这个函数即可。具体可查MSDN。

5.6 FT_QueryPerformanceCounter
  这是一种高精度时间计数器的方法,它的检测刻度最小,更精确。


  if(QueryPerformanceCounter(lpPerformanceCount))
        return lpPerformanceCount->QuadPart;
  else 
     return 0;


5.7 FT_F1_IceBreakpoint
  在<>中有讲述这个反跟踪技巧。这个所谓的"Ice breakpoint" 是Intel 未公开的指令之一, 机器码为0xF1.执行这个指令将产生单步异常.,如果程序已经被跟踪, 调试器将会以为它是通过设置标志寄存器中的单步标志位生成的正常异常. 相关的异常处理器将不会被执行到.下面是我的实现代码:


复制代码
__asm
  {
  push   offset eh_f1; set exception handler
     push  dword ptr fs:[0h]
     mov    dword ptr fs:[0h],esp  
     xor   eax,eax;reset EAX invoke int3
     _emit 0xf1
     pop    dword ptr fs:[0h];restore exception handler
     add    esp,4
  test  eax,eax
  jz    rt_label_f1
  jmp    rf_label_f1

eh_f1:
     mov eax,dword ptr[esp+0xc]
  mov    dword ptr [eax+0xb0],0x00000001;set flag (ContextRecord.EAX)
     inc dword ptr [eax+0xb8]
     xor eax,eax
     retn
rt_label_f1:
  inc    eax
  mov    esp,ebp
     pop    ebp
     retn
rf_label_f1:
  xor    eax,eax
  mov    esp,ebp
     pop    ebp
     retn
  }
复制代码


5.8 FT_Prefetch_queue_nop1
这个反调试是在<>中给出的,它主要是基于REP指令,通过REP指令来修改自身代码,在非调试态下,计算机会将该指令完整取过来,因此可以正确的执行REP这个指令,将自身代码完整修改,但在调试态下,则在修改自身的时候立即跳出。
这个反跟踪技巧个人觉得用处不大,因为只有在REP指令上使用F7单步时,才会触发这个反跟踪,而我个人在碰到REP时,通常都是F8步过。下面是利用这个CPU预取指令的特性的实现反跟踪的一种方法,正常情况下,REP指令会修改其后的跳转指令,进入正常的程序流程,但在调试态下,其无法完成对其后代码的修改,从而实现反调试。


复制代码
   DWORD oldProtect;
   DWORD tmpProtect;
   __asm
   {
    lea eax,dword ptr[oldProtect]
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    nop
label3:
    mov al,0x90
    push 0x10
    pop ecx
    mov edi,offset label3
    rep stosb
    jmp rt_label
    nop
    nop
    nop
    nop
    nop
rf_label:
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x205CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];

    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
rt_label:
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x205CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];

    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


5.9 FT_Prefetch_queue_nop2
  与5.8节类似,这是根据CPU预取指令的这个特性实现的另一种反跟踪技巧。原理是通过检测REP指令后的ECX值,来判断REP指令是否被完整执行。在正常情况下,REP指令完整执行后,ECX值应为0;但在调试态下,由于REP指令没有完整执行,ECX值为非0值。通过检测ECX值,实现反跟踪。


复制代码
  DWORD oldProtect;
  DWORD tmpProtect;
  __asm
  {
    lea eax,dword ptr[oldProtect]
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    mov ecx,0
label3:
    mov al,0x90
    push 0x10
    pop ecx
    mov edi,offset label3
    rep stosb
    nop
    nop
    nop
    nop
    nop
    nop
    push ecx
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x201CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    pop ecx

    test ecx,ecx
    jne rt_label
  }
rf_label:
  return false;
rt_label:
  return true;
复制代码


六、  检测-补丁(FP_)
这部分内容也较少,方法当然也有很多种,原理都差不多,我只选了下面三种。这几种方法通常在一些壳中较常用,用于检验文件是否被脱壳或被恶意修改。
函数列表如下:


//find Patch
bool FP_Check_FileSize(DWORD Size);
bool FP_Check_FileHashValue_CRC(DWORD CRCVALUE_origin);
bool FP_Check_FileHashValue_MD5(DWORD MD5VALUE_origin);


6.1 FP_Check_FileSize(DWORD Size)
  通过检验文件自身的大小的方法,是一种比较简单的文件校验方法,通常如果被脱壳,或被恶意修改,就可能影响到文件的大小。我用下面的代码实现。需注意的是,文件的大小要先编译一次,将首次编译得到的数值写入代码,再重新编译完成。


复制代码
  DWORD Current_Size;
  TCHAR szPath[MAX_PATH];
  HANDLE hFile;

  if( !GetModuleFileName( NULL,szPath, MAX_PATH ) )
        return FALSE;

  hFile = CreateFile(szPath, 
    GENERIC_READ ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_ALWAYS, 
    FILE_ATTRIBUTE_NORMAL, 
    NULL);
  if (hFile == INVALID_HANDLE_VALUE)
    return false;
  Current_Size=GetFileSize(hFile,NULL);
  CloseHandle(hFile);
  if(Current_Size!=Size)
    return true;
  return false;
复制代码


6.2 FP_Check_FileHashValue_CRC
  检验文件的CRC数值,是比较常用的文件校验方法,相信很多人都碰到过了,我是在《软件加解密技术》中了解到的。需注意的是文件原始CRC值的获得,及其放置位置,代码编写完成后,通常先运行一遍程序,使用调试工具获得计算得到的数值,在将这个数值写入文件中,通常这个数值不参加校验,可以放置在文件的尾部作为附加数据,也可以放在PE头中不用的域中。
  下面的代码只是个演示,没有保存CRC的真实数值,也没有单独存放。


复制代码
  DWORD fileSize,NumberOfBytesRW;
  DWORD CRCVALUE_current;
  TCHAR szFileName[MAX_PATH]; 
  TCHAR  *pBuffer ;
  GetModuleFileName(NULL,szFileName,MAX_PATH);
  HANDLE hFile = CreateFile(
    szFileName,
    GENERIC_READ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);
  if (hFile != INVALID_HANDLE_VALUE )
  {
    fileSize = GetFileSize(hFile,NULL);
    if (fileSize == 0xFFFFFFFF) return false;
    pBuffer = new TCHAR [fileSize];  
    ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);
    CloseHandle(hFile);
  }
  CRCVALUE_current=CRC32((BYTE *)pBuffer,fileSize);
  if(CRCVALUE_origin!=CRCVALUE_current)
    return true;
  return false;
复制代码


6.3 FP_Check_FileHashValue_MD5
与6.2节的原理相同,只是计算的是文件的MD5数值。仍要注意6.2节中同样的MD5真实数值的获得和存放问题。

啊冲 2016-02-03 10:44

重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
  我的博客:http://blog.csdn.net/sidyhe
  开发环境:VS2010 + WinDDK
  测试环境:VirtualDDK + VMware + Windows 7 sp1 x86
第一部分:重载镜像
  大家可以通过ARK工具来查看系统的内核模块,排在首位的一定是ntXXX.exe这个模块,这个模块就是自系统启动后加载的第一个模块,根据CPU及其不同特性的不同会加载不同的NT内核,如NTOSKRNL.EXE、NTKRNLMP.EXE、NTKRNLPA.EXE、NTKRPAMP.EXE。不同的NT模块代表不同的意义,如单CPU,多CPU,单CPU多核,是否支持PAE等都会影响所加载的NT内核,所以如果大家见到和别人不同的NT内核不要奇怪,那是因为你的CPU和别人的不一样。
  本次技术研究仅仅是针对于x86系统的Windows 7以及Windows XP,Windows x64系统由于强制数字签名以及PatchGuard技术无法实现,故不做讨论,当然如果你有办法解决这两个问题就另当别论了。至于是否兼容Windows 8 x86就有待各位验证了。
  既然要重载内核,肯定是NT内核(废话),上面说到了系统可能会加载不同名字的NT内核,那么就需要一些方法来确定当前系统所使用的内核,其中一个方法是使用ZwQuerySystemInformation传递SystemModuleInformation参数,不过在这里我不打算使用这个方法,因为太麻烦。我使用PsLoadedModuleList来确定NT内核,那问题来了,PsLoadedModuleList是一个未导出变量,这个变量记录了当前系统内核模块的信息,ZwQuerySystemInformation就是访问了PsLoadedModuleList来生成结果,如何定位这个东西呢?我不喜欢硬编码,所以我需要一种在不同系统上通用的方式来获取这个变量,经过收集资料发现在DriverEntry被调用时,第一个参数PDRIVER_OBJECT的PDRIVER_EXTENSION成员其实就是一个LDR_DATA_TABLE_ENTRY指针(参考WRK),这个与PsLoadedModuleList的类型是一致的,也就是说lpDriverObject->DriverSection是PsLoadedModuleList这个双向链表的其中一个节点,而PsLoadedModuleList是这个链表的头节点,根据大量的实践证明,lpDriverObject->DriverSection节点的下一个节点一定是PsLoadedModuleList,因为是双向循环链表嘛,那么定位这个东西就非常简单了,代码如下。
代码:
PLDR_DATA_TABLE_ENTRY PsLoadedModuleList = NULL;

VOID InitializePsLoadedModuleList(PDRIVER_OBJECT lpDriverObject)
{
  PLDR_DATA_TABLE_ENTRY ldr = (PLDR_DATA_TABLE_ENTRY)lpDriverObject->DriverSection;

  PsLoadedModuleList = (PLDR_DATA_TABLE_ENTRY)ldr->InLoadOrderLinks.Flink;
  return;
}
  找到了PsLoadedModuleList,那么链表的第一个节点就是NT内核了,可以取得文件路径,解决了重载内核的第一个问题。
  接下来就是读文件数据,并把数据部署为镜像。部署的过程与RING3的镜像一致,不熟悉的朋友可以去恶补一下PE知识。读到文件数据后,把数据部署为镜像的核心代码如下:
代码:
PVOID ReloadNtModule(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList)
{
  PVOID lpImageAddress = NULL;
  PLDR_DATA_TABLE_ENTRY NtLdr = (PLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
  PVOID lpFileBuffer;

  DbgPrint("Nt Module File is %wZ\n", &NtLdr->FullDllName);
  if (lpFileBuffer = KeGetFileBuffer(&NtLdr->FullDllName))
  {
    PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)lpFileBuffer;
    PIMAGE_NT_HEADERS lpNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);

    if (lpImageAddress = ExAllocatePool(NonPagedPool, lpNtHeader->OptionalHeader.SizeOfImage))
    {
      PUCHAR lpImageBytes = (PUCHAR)lpImageAddress;
      IMAGE_SECTION_HEADER *lpSection = IMAGE_FIRST_SECTION(lpNtHeader);
      ULONG i;

      RtlZeroMemory(lpImageAddress, lpNtHeader->OptionalHeader.SizeOfImage);
      RtlCopyMemory(lpImageBytes, lpFileBuffer, lpNtHeader->OptionalHeader.SizeOfHeaders);
      for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
      {
        RtlCopyMemory(lpImageBytes + lpSection.VirtualAddress, (PCHAR)lpFileBuffer + lpSection.PointerToRawData, lpSection.SizeOfRawData);
      }
      //代码不完整,后续补充
    }
    ExFreePool(lpFileBuffer);
  }
  if (lpImageAddress) DbgPrint("ImageAddress:0x%p\n", lpImageAddress);
  return lpImageAddress;
}
  至此解决了第二个问题,镜像已具基本雏形,了解的朋友一定知道下一步就是修复镜像了,即处理重定位以及输入表(导入表)。修复输入表没什么,难就难在重定位,重定位中包含了代码重定位和变量重定位,既然我们做的是重载内核,那么肯定是需要让原本走NT模块的流程转移到我们的新模块上,那么可以肯定的是代码重定位一定要在新模块上,至于变量,我个人的做法是指向原模块,因为即使是重载内核,也不能保证所有执行单元都会走新模块,这样保险一些,也简单一些,不过需要注意的是,变量重定位也包含IAT,所以我这里把IAT也指向新模块,否则修复输入表就没意义了,也可以防范IAT HOOK。还有,如果重定位的地方属于“可废弃”的区段(节),可以不用处理,因为原模块已经废弃了。还有还有,内核模块的导入表不存在序号导入,所以处理起来更加简单。
代码:
BOOLEAN KeFixIAT(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpImageAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  PIMAGE_IMPORT_DESCRIPTOR lpImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (ULONG)lpImageAddress);
  PVOID lpModuleAddress;

  while (lpImportDescriptor->Characteristics)
  {
    if (lpModuleAddress = KeGetModuleHandle(PsLoadedModuleList, (PCHAR)lpImageAddress + lpImportDescriptor->Name))
    {
      PIMAGE_THUNK_DATA lpThunk = (PIMAGE_THUNK_DATA)((ULONG)lpImageAddress + lpImportDescriptor->OriginalFirstThunk);
      PVOID *lpFuncTable = (PVOID*)((ULONG)lpImageAddress + lpImportDescriptor->FirstThunk);
      ULONG i;

      for (i = 0; lpThunk->u1.Ordinal; i++)
      {
        if ((lpThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0)
        {
          PIMAGE_IMPORT_BY_NAME lpName = (PIMAGE_IMPORT_BY_NAME)((PCHAR)lpImageAddress + lpThunk->u1.AddressOfData);
          PVOID lpFunc;

          if (lpFunc = KeGetProcAddress(lpModuleAddress, lpName->Name))
          {
            lpFuncTable = lpFunc;
          }
          else
          {
            DbgPrint("KeFixImageImportTable:Cannot found function : %s\n", lpName->Name);
            return FALSE;
          }
        }
        else
        {
          //impossible
        }
        lpThunk++;
      }
    }
    else
    {
      DbgPrint("KeFixImageImportTable:Cannot found Module : %s\n", (PCHAR)lpImageAddress + lpImportDescriptor->Name);
      return FALSE;
    }
    lpImportDescriptor++;
  }
  return TRUE;
}
  下面是处理重定位的代码,相对比较复杂了,这里只贴出来核心代码,即如何处理具体重定位地址的部分。
代码:
VOID KeFixRelocEx(PVOID New, PVOID Old, PVOID *lpFixAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)New;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  ULONG_PTR RelocValue = (ULONG_PTR)*lpFixAddress - lpNtHeader->OptionalHeader.ImageBase;

  if (KeFixRelocOfCheckIAT(New, (PCHAR)New + RelocValue))
  {
    *lpFixAddress = (PCHAR)New + RelocValue;
    return;
  }
  else
  {
    IMAGE_SECTION_HEADER *lpSecHdr = IMAGE_FIRST_SECTION(lpNtHeader);
    USHORT i;

    for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
    {
      if (RelocValue >= lpSecHdr.VirtualAddress && RelocValue < lpSecHdr.VirtualAddress + lpSecHdr.SizeOfRawData)
      {
        if (lpSecHdr.Characteristics & IMAGE_SCN_MEM_WRITE)
        {
          *lpFixAddress = (PCHAR)Old + RelocValue;
        }
        else
        {
          *lpFixAddress = (PCHAR)New + RelocValue;
        }
        return;
      }
    }
  }
  *lpFixAddress = (PCHAR)Old + RelocValue;
  return;
}
  至此,重新加载一份新的NT内核已经完成了绝大部分,还有一些细节没有处理,等到后面遇到时再告诉各位看客老爷,先卖个关子吧,涉及到的未公开结构可以从WRK中寻找,我所使用的结构在目前的x86系统中都没有改动,所以通用。具体工程代码先不打算放出来,如果大部分朋友需要的话,我会在后续文章中放出,不过我还是希望大家能够自己动手,这样收获会比纯粹的复制粘贴更多。
  下面的内容应该是HOOK了,用来接管正常的执行流程,我会抽时间写后续内容的。

啊冲 2016-02-03 10:45

第四部分:最后的修正(蓝屏与崩溃)
  终于走到了这里,对于新的NT内核只剩下了两个问题了(其他尚未发现),第一个就是蓝屏,最麻烦的BUG,起初在我遇到的时候也是疯掉了(当时用的还是Windows 7 x86),最后发现是因为内核异常得不到处理造成的,即POOL代码决不能异常。而造成这种现象的就是SafeSEH机制。简要说明就是在内核代码中发生了异常会多一些处理,判断SEH Handler是否有效,再执行。
  现在普遍的解决思路有两个:1.废除SafeSEH机制。2.使POOL合法化。
  先来研究一下这个SafeSEH到底是怎么回事,在发生异常时经过一些处理后会调用RtlDispatchException,进而调用RtlIsValidHandler来判断Handler是否有效,如果无效,你懂得。
代码:
BOOLEAN RtlIsValidHandler(IN PEXCEPTION_ROUTINE Handler)
{
    PULONG FunctionTable;
    ULONG FunctionTableLength;
    PVOID Base;

    FunctionTable = RtlLookupFunctionTable(Handler, &Base, &FunctionTableLength);

    if (FunctionTable && FunctionTableLength) {
        PEXCEPTION_ROUTINE FunctionEntry;
        LONG High, Middle, Low;

        if ((FunctionTable == LongToPtr(-1)) && (FunctionTableLength == (ULONG)-1)) {
            // Address is in an image that shouldn't have any handlers (like a resource only dll).
            RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), LongToPtr(-1), -1);
            return FALSE;
        }
    
        // Bias the handler value down by the image base and see if the result
        // is in the table

        (ULONG)Handler -= (ULONG)Base;
        Low = 0;
        High = FunctionTableLength;
        while (High >= Low) {
            Middle = (Low + High) >> 1;
            FunctionEntry = (PEXCEPTION_ROUTINE)FunctionTable[Middle];
            if (Handler < FunctionEntry) {
                High = Middle - 1;
            } else if (Handler > FunctionEntry) {
                Low = Middle + 1;
            } else {
                // found it
                return TRUE;
            }
        }
        // Didn't find it
        RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), FunctionTable, FunctionTableLength);

        return FALSE;
    }

    // Can't verify
    return TRUE;
}
  上面代码中的RtlLookupFunctionTable是取得一个类似函数表的东西,在PE信息中的体现则是IMAGE_NT_HEADERS.OptionalHeader[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG],用我手里的内核文件做例子,通过IDA观察到这个函数表,放着一堆RVA:
名称:  00.jpg
查看次数: 8
文件大小:  14.5 KB
  这些地址就是SEH Handler了,一共有0x12=18个,再来看KLDR的定义:
代码:
typedef struct _KLDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    PVOID ExceptionTable;
    ULONG ExceptionTableSize;
    // ULONG padding on IA64
    PVOID GpValue;
    PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG Flags;
    USHORT LoadCount;
    USHORT __Unused5;
    PVOID SectionPointer;
    ULONG CheckSum;
    // ULONG padding on IA64
    PVOID LoadedImports;
    PVOID PatchInformation;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
  很明显KLDR中的ExceptionTable应该指向IDA图示中的首地址。注意KLDR结构只能在WRK中查看,用WinDBG也看不到(dt),KLDR和LDR并不一样。还有,这个函数表里存放的就是RVA,并不会被重定位修复为VA。
  那么第一种方法就是HOOK RtlIsValidHandler,直接返回TRUE,则废掉SafeSEH。这个函数没导出,隐藏的很深,字节搜索或者重定位搜索很麻烦,但也是一种方法,不过我不用这种方式,虽然处理好之后能够很好的隐藏自己的NT内核。
  第二种方式就简单多了,虽然会被检测到存在异常模块,但又不影响什么,毕竟做的不是Rootkit。方法就是直接在PsLoadedModuleList插入一个KLDR,就那么简单。
代码:
PKLDR_DATA_TABLE_ENTRY KeGetImageLdrPointer(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
  PKLDR_DATA_TABLE_ENTRY lpTableEntry = PsLoadedModuleList;
  PKLDR_DATA_TABLE_ENTRY lpTablePointer = lpTableEntry;

  do 
  {
    if (lpTablePointer->DllBase == lpImageAddress)
    {
      return lpTablePointer;
    }
    lpTablePointer = (PKLDR_DATA_TABLE_ENTRY)(lpTablePointer->InLoadOrderLinks.Flink);
  } while (lpTableEntry != lpTablePointer);
  return NULL;
}

BOOLEAN KeInsertPsLoadedModuleList(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID NewImage, PVOID OldImage)
{
  PKLDR_DATA_TABLE_ENTRY lpNewLdr, lpSimLdr;

  if (lpSimLdr = KeGetImageLdrPointer(PsLoadedModuleList, OldImage))
  {
    if (lpNewLdr = ExAllocatePool(NonPagedPool, sizeof(KLDR_DATA_TABLE_ENTRY)))
    {
      RtlCopyMemory(lpNewLdr, lpSimLdr, sizeof(KLDR_DATA_TABLE_ENTRY));
      lpNewLdr->DllBase = NewImage;

      InsertTailList(&PsLoadedModuleList->InLoadOrderLinks, &lpNewLdr->InLoadOrderLinks);
      DbgPrint("KeInsertPsLoadedModuleList:0x%p\n", lpNewLdr);
      return TRUE;
    }
  }
  return FALSE;
}
  在曾想过是否要修正KLDR中的ExceptionTable,但感觉没必要,毕竟指向的函数表里面都是RVA,又不是VA,反正SEH Handler都一样,不改也罢。
  好了这样就解决了SafeSEH所带来的蓝屏。基本上这个驱动就可以拿来在本机测试了。
崩溃问题
  这份代码能够很好的在大部分CPU上进行工作,但有些CPU仍会发生程序崩溃的问题,这与第三部分的文章中提到的打不开程序是不同的问题。我的CPU就是如此,否则我都不会发现这个问题。
  后面的东西绝对原创,网上没有哦。
  一开始遇到这个问题同样使我疯掉了一段时间,通过各种途径找问题,最后奇妙的发现了这里:
名称:  01.jpg
查看次数: 0
文件大小:  25.4 KB
  在干净的系统中,当我使用ARK恢复所有的钩子后,居然出现了我所说的崩溃问题,这就说明了在系统初始化时自己Patch了自己,很明显的大家能够观察到有一个巨大的补丁,那就是一个修改了22字节的东西,这是啥玩意?WinDBG会告诉你的:
代码:
0: kd> uf 83e7dd50
nt!KeFlushCurrentTb:
83e7dd50 0f20e0          mov     eax,cr4
83e7dd53 0fbaf007        btr     eax,7
83e7dd57 7309            jae     nt!KeFlushCurrentTb+0x12 (83e7dd62)

nt!KeFlushCurrentTb+0x9:
83e7dd59 0f22e0          mov     cr4,eax
83e7dd5c 0c80            or      al,80h
83e7dd5e 0f22e0          mov     cr4,eax
83e7dd61 c3              ret

nt!KeFlushCurrentTb+0x12:
83e7dd62 0f20d9          mov     ecx,cr3
83e7dd65 0f22d9          mov     cr3,ecx
83e7dd68 c3              ret
  从字面上不难理解KeFlushCurrentTb是刷新了TB,TB是什么我就不知道了,个人猜测是CPU中TLB与TIB的统称,看代码也知道是刷新了页表。你也可以通过IDA来查看这部分的代码,简言之就是判断了CPU的类型,如果满足了什么条件,则Patch了这个函数。再来看恢复之后的样子:
代码:
1: kd> uf 83e7dd50
nt!KeFlushCurrentTb:
83e7dd50 0f20d8          mov     eax,cr3
83e7dd53 0f22d8          mov     cr3,eax
83e7dd56 c3              ret
  只能说这个函数在不同的CPU上可能会有不同的代码,那如何解决?难道像它一样来判断CPU进而修改这个代码?大可没有必要,我的做法就是定位到这里,然后JMP到原模块的这个地方。我想这里不会被HOOK,没有那么逆天。定位方法我也是利用的重定位,通过找到负责Patch的代码定位KeFlushCurrentTb,再JMP过去:
代码:
VOID PatchKeFlushCurrentTb(PVOID lpNewNtoskrnlAddress, PVOID lpNtoskrnlAddress)
{
  /*
  mov     edi, offset KeFlushCurrentTb
  mov     esi, offset byte_477D57
  mov     ecx, XXX
  rep movsb
  */
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNewNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpDosHeader + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems - 1; i++)
    {
      if ((lpItem >> 12) == IMAGE_REL_BASED_HIGHLOW && (lpItem[i + 1] >> 12) == IMAGE_REL_BASED_HIGHLOW)
      {
        ULONG *lpFixAddress1 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF));
        ULONG *lpFixAddress2 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem[i + 1] & 0x0FFF));

        if ((ULONG)lpFixAddress2 - (ULONG)lpFixAddress1 == 5)
        {
          if (*((PUCHAR)lpFixAddress1 - 1) == 0xBF && *((PUCHAR)lpFixAddress2 - 1) == 0xBE)
          {
            PUCHAR lpCheckBytes = (PUCHAR)lpFixAddress2 + sizeof(PVOID);

            if (lpCheckBytes[0] == 0xB9 && lpCheckBytes[5] == 0xF3 && lpCheckBytes[6] == 0xA4)
            {
              PUCHAR lpPatchAddress = (PUCHAR)*lpFixAddress1;

              lpPatchAddress[0] = 0xE9;
              *(ULONG*)&lpPatchAddress[1] = (ULONG)lpNtoskrnlAddress - (ULONG)lpNewNtoskrnlAddress - 5;
              return;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return;
}
  好了,没事儿了,新的NT内核没问题了,全部搞定,如果仍发现自己解决不了的问题可以联系我,最后的DriverEntry代码:
代码:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(DriverObject);
  g_lpNtoskrnlAddress = KeGetModuleHandle(PsLoadedModuleList, "ntoskrnl.exe");
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  g_KeServiceTable = (PVOID*)BuildKeServiceTable(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeFixReloc2(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  PatchKeFlushCurrentTb(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeInsertPsLoadedModuleList(PsLoadedModuleList, g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));

  DriverObject->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}
  这一部分告一段落,接下来继续重载win32k,搞定SHADOW SSDT。
  最后,为啥PCHunter不支持Wwindows 8.1 update 1呢?现在的最新版1.32运行不到一分钟就蓝屏,原因是触发了PatchGuard,望修复,没有ARK用真心难受。

啊冲 2016-02-03 10:45
第五部分:重载GUI内核
  不知道我的叫法对不对,实际上就是重载win32k.sys,有了前面文章的功底,我想重载一个镜像并不难了,只要稍微修改一下ReloadNtModule即可。
代码:
  g_lpWin32kAddress = KeGetModuleHandle(PsLoadedModuleList, "win32k.sys");
  g_lpNewWin32kAddress = ReloadNtModule(PsLoadedModuleList, g_lpWin32kAddress);
  KeFixReloc2(g_lpNewWin32kAddress, g_lpWin32kAddress);
  KeInsertPsLoadedModuleList(PsLoadedModuleList, g_lpNewWin32kAddress, g_lpWin32kAddress);
  这样就完事儿了,是不是很简单?
  接下来去找SHADOW SSDT的原始地址,是存储在一个叫做W32pServiceTable的变量中。大家应该知道所有的系统服务表都是存储在KeServiceDescriptorTableShadow中,最多支持四个表,默认情况下只有两个表,即SSDT与SHADOW SSDT。在系统刚初始化的时候只有一张表,因为此时win32k还没有加载,当它加载的时候会调用KeAddSystemServiceTable来添加第二张服务表,那么思路来了:
名称:  00.jpg
查看次数: 8
文件大小:  14.6 KB
  代码很简单,就是通过重定位找到调用KeAddSystemServiceTable的地方,判断附近代码,找出变量,其实在整个win32k模块只有一处调用了KeAddSystemServiceTable,很好找的。
代码:
PVOID FindW32pServiceTable(PVOID lpWin32kAddress, PVOID lpKeAddSystemServiceTable, ULONG *nApi)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpWin32kAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpWin32kAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    if (lpRelocateTable->VirtualAddress)
    {
      ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
      USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
      ULONG j;

      for (j = 0; j < NumberOfItems; j++)
      {
        if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW)
        {
          PUCHAR lpFixAddress = (PUCHAR)((PCHAR)lpWin32kAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));

          /*
          PUSH _W32pServiceTable
          CALL [KeAddSystemServiceTable]
          */
          if (*(lpFixAddress - sizeof(UCHAR)) == 0x68)
          {
            if (*(PUSHORT)(lpFixAddress + sizeof(PVOID)) == 0x15FF)
            {
              PVOID lpCallAddress = *(PVOID*)*(ULONG*)(lpFixAddress + sizeof(PVOID) + sizeof(USHORT));

              if (lpCallAddress == lpKeAddSystemServiceTable)
              {
                *nApi = *(ULONG*)*(PVOID*)((PCHAR)lpFixAddress - 11);
                return (PVOID)*(ULONG_PTR*)lpFixAddress;
              }
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}

PVOID BuildW32pServiceTable(PVOID lpNtoskrnlAddress, PVOID lpNewWin32kAddress)
{
  PVOID lpW32pServiceTable = NULL;
  PVOID lpNewW32pServiceTable = NULL;
  PVOID lpKeAddSystemServiceTable = KeGetProcAddress(lpNtoskrnlAddress, "KeAddSystemServiceTable");
  ULONG nApi;

  if (lpW32pServiceTable = FindW32pServiceTable(lpNewWin32kAddress, lpKeAddSystemServiceTable, &nApi))
  {
    if (lpNewW32pServiceTable = ExAllocatePool(NonPagedPool, nApi * sizeof(PVOID)))
    {
      RtlCopyMemory(lpNewW32pServiceTable, lpW32pServiceTable, nApi * sizeof(PVOID));
    }
  }
  return lpNewW32pServiceTable;
}
  接下来修改一下过滤函数:
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    return g_KeServiceTable[ServiceIndex];
  }
  else
  {
    return g_GuiServiceTable[ServiceIndex];
  }
}
  至此,已经重载了两个内核模块并绕过了NATIVE SSDT/SHADOW SSDT中的函数表HOOK以及INLINE HOOK,代码还有很多不完善的地方,有待大家去找出并修复,其实我也不喜欢拿来主义者和伸手党,仅仅是希望对这一块有兴趣的朋友有所帮助。
  感谢topofall指出代码中的问题。
  基本上告一段落,但还是有一些东西没有告诉大家,我想还会写一些后续文章的。
  待续。

啊冲 2016-02-03 10:45

第六部分:IDT HOOK
  对我来说IDT一直是一个很难理解的东西,尤其是段选择子什么的,这里我不敢教大家如何HOOK IDT表,我只会INLINE HOOK,囧。再说每一个CPU都有各自独立的IDT表,做HOOK的话每一个CPU都要处理,也很麻烦。
  为什么要HOOK IDT?除了正常的服务请求会经过KiFastCallEntry,那么不正常的就只能走中断门了,比如DEBUG,BREAKPOINT等等,我想这是大家有兴趣的地方。
  和之前一样,对于大部分函数表来说,在NT内核中都会有一个对应的地方来存储原始地址,初始化函数是在KiSystemStartup中,但是WRK里并没有源代码,被可恶的编译成了lib文件,所以来看IDA吧:
名称:  00.jpg
查看次数: 8
文件大小:  12.0 KB
名称:  01.jpg
查看次数: 0
文件大小:  28.6 KB
  显然这里的77E164也可以理解为是一个函数表,里面存储了中断门的线性地址,起个名字吧,我自己叫做KiTrapTable,不知道对不对。有一个特点就是这个地址表并不是元素连续的,每隔一个才是地址,也就是中断函数地址=函数表[索引*2],至于另外一个元素是什么我就不敢断言了,老办法,利用重定位来搜索:
代码:
PVOID FindKiTrapTable(PVOID lpNtoskrnlAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpNtoskrnlAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    if (lpRelocateTable->VirtualAddress)
    {
      ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
      USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
      ULONG j;

      for (j = 0; j < NumberOfItems; j++)
      {
        if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW)
        {
          PUCHAR lpFixAddress = (PUCHAR)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));

          /*
          MOV  ESI, OFFSET KiTrapTable
          MOV  ECX, 0x0800
          SHR  ECX, 2
          REP  MOVSD
          */
          if (*(lpFixAddress - sizeof(UCHAR)) == 0xBE)
          {
            UCHAR CodeBytes[] = {0xB9, 0x00, 0x08, 0x00, 0x00, 0xC1, 0xE9, 0x02, 0xF3, 0xA5};

            if (RtlEqualMemory(lpFixAddress + sizeof(PVOID), CodeBytes, sizeof(CodeBytes)))
            {
              return (PVOID)*(ULONG*)lpFixAddress;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}

PVOID BuildKiTrapTable(PVOID lpNtoskrnlAddress)
{
  PVOID lpKiTrapTable = NULL;
  PVOID lpNewKiTrapTable = NULL;

  if (lpKiTrapTable = FindKiTrapTable(lpNtoskrnlAddress))
  {
    if (lpNewKiTrapTable = ExAllocatePool(NonPagedPool, sizeof(PVOID) * 0xFF * 2))
    {
      RtlCopyMemory(lpNewKiTrapTable, lpKiTrapTable, sizeof(PVOID) * 0xFF * 2);
      DbgPrint("BuildKiTrapTable:%08X\n", lpKiTrapTable);
    }
  }
  return lpNewKiTrapTable;
}
  找到原始地址,INLINE HOOK就非常简单了,直接在函数头写JMP到新内核中或者额外处理什么,这里我就不再贴代码了,节省篇幅。
  其实HOOK IDT并没有什么很大的意义,毕竟正常的内核调用都是调用sysenter的,也是HOOK竞争最激烈的地方,把握住了这里,就可以绕过绝大部分的钩子。
  当然对于反反调试,接管IDT中的调试相关中断还是很有用的,具体怎么做那就不是我的事儿了,大家各自发挥,对症下药吧。
  也许还有后续内容,也许直接是一个总结,希望大家能够在技术上得到真正的提高,不论大家重载内核的目的纯洁与否,学到东西才是最长久的。也不要认为x86在远离我们就开始忽视,其实x64也是建立在x86的基础上,变化不是很大,对于底层要进行革命性的变化还要好久好久。

啊冲 2016-02-03 10:45
恶意软件反检测技术简介:反调试技术解析 (1)
在上篇中,我们会为读者介绍恶意软件常用来的反仿真技术。本文中,我们将向读者介绍恶意软件用以阻碍对其进行逆向工程的各种反调试技术,以帮助读者很好的理解这些技术,从而能够更有效地对恶意软件进行动态检测和分析。

一、反调试技术

反调试技术是一种常见的反检测技术,因为恶意软件总是企图监视自己的代码以检测是否自己正在被调试。为做到这一点,恶意软件可以检查自己代码是否被设置了断点,或者直接通过系统调用来检测调试器。

1.断点

为了检测其代码是否被设置断点,恶意软件可以查找指令操作码0xcc(调试器会使用该指令在断点处取得恶意软件的控制权),它会引起一个SIGTRAP。如果恶意软件代码本身建立了一个单独的处理程序的话,恶意软件也可以设置伪断点。用这种方法恶意软件可以在被设置断点的情况下继续执行其指令。

恶意软件也可以设法覆盖断点,例如有的病毒采用了反向解密循环来覆盖病毒中的断点。相反,还有的病毒则使用汉明码自我纠正自身的代码。汉明码使得程序可以检测并修改错误,但是在这里却使病毒能够检测并清除在它的代码中的断点。

2.计算校验和

恶意软件也可以计算自身的校验和,如果校验和发生变化,那么病毒会假定它正在被调试,并且其代码内部已被放置断点。VAMPiRE是一款抗反调试工具,可用来逃避断点的检测。VaMPiRE通过在内存中维护一张断点表来达到目的,该表记录已被设置的所有断点。该程序由一个页故障处理程序(PFH),一个通用保护故障处理程序(GPFH),一个单步处理程序和一个框架API组成。当一个断点被触发的时候,控制权要么传给PFH(处理设置在代码、数据或者内存映射I/O中的断点),要么传给GPFH(处理遗留的I/O断点)。单步处理程序用于存放断点,使断点可以多次使用。

3.检测调试器

在Linux系统上检测调试器有一个简单的方法,只要调用Ptrace即可,因为对于一个特定的进程而言无法连续地调用Ptrace两次以上。在Windows中,如果程序目前处于被调试状态的话,系统调用isDebuggerPresent将返回1,否则返回0。这个系统调用简单检查一个标志位,当调试器正在运行时该标志位被置1。直接通过进程环境块的第二个字节就可以完成这项检查,以下代码为大家展示的就是这种技术:

mov eax, fs:[30h]
move eax, byte [eax+2]
test eax, eax    
jne @DdebuggerDetected
在上面的代码中,eax被设置为PEB(进程环境块),然后访问PEB的第二个字节,并将该字节的内容移入eax。通过查看eax是否为零,即可完成这项检测。如果为零,则不存在调试器;否则,说明存在一个调试器。

如果某个进程为提前运行的调试器所创建的,那么系统就会给ntdll.dll中的堆操作例程设置某些标志,这些标志分别是FLG_HEAP_ENABLE_TAIL_CHECK、FLG_HEAP_ENABLE_FREE_CHECK和FLG_HEAP_VALIDATE_PARAMETERS。我们可以通过下列代码来检查这些标志:

mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne @DebuggerDetected
在上面的代码中,我们还是访问PEB,然后通过将PEB的地址加上偏移量68h到达堆操作例程所使用的这些标志的起始位置,通过检查这些标志就能知道是否存在调试器。

检查堆头部内诸如ForceFlags之类的标志也能检测是否有调试器在运行,如下所示:

mov eax, fs:[30h]
mov eax, [eax+18h] ;process heap
mov eax, [eax+10h] ;heap flags
test eax, eax
jne @DebuggerDetected
上面的代码向我们展示了如何通过PEB的偏移量来访问进程的堆及堆标志,通过检查这些内容,我们就能知道Force标志是否已经被当前运行的调试器提前设置为1了。

另一种检测调试器的方法是,使用NtQueryInformationProcess这个系统调用。我们可以将ProcessInformationClass设为7来调用该函数,这样会引用ProcessDebugPort,如果该进程正在被调试的话,该函数将返回-1。示例代码如下所示。

push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax, eax
jne @ExitError
cmp isdebugged, 0
jne @DebuggerDetected


在本例中,首先把NtQueryInformationProcess的参数压入堆栈。这些参数介绍如下:第一个是句柄(在本例中是0),第二个是进程信息的长度(在本例中为4字节),接下来是进程信息类别(在本例中是7,表示ProcessDebugPort),下一个是一个变量,用于返回是否存在调试器的信息。如果该值为非零值,那么说明该进程正运行在一个调试器下;否则,说明一切正常。最后一个参数是返回长度。使用这些参数调用NtQueryInformationProcess后的返回值位于isdebugged中。随后测试该返回值是否为0即可。

另外,还有其他一些检测调试器的方法,如检查设备列表是否含有调试器的名称,检查是否存在用于调试器的注册表键,以及通过扫描内存以检查其中是否含有调试器的代码等。

另一种非常类似于EPO的方法是,通知PE加载器通过PE头部中的线程局部存储器(TLS)表项来引用程序的入口点。这会导致首先执行TLS中的代码,而不是先去读取程序的入口点。因此,TLS在程序启动就可以完成反调试所需检测。从TLS启动时,使得病毒得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。

啊冲 2016-02-03 10:45

4.探测单步执行

恶意软件还能够通过检查单步执行来检测调试器。要想检测单步执行的话,我们可以把一个值放进堆栈指针,然后看看这个值是否还在那里。如果该值在那里,这意味着,代码正在被单步执行。当调试器单步执行一个进程时,当其取得控制时需要把某些指令压入栈,并在执行下一个指令之前将其出栈。所以,如果该值仍然在那里,就意味着其它正在运行的进程已经在使用堆栈。下面的示例代码展示了恶意软件是如何通过堆栈状态来检测单步执行的:

Mov bp,sp;选择堆栈指针
Push ax ;将ax压入堆栈
Pop ax ;从堆栈中选择该值
Cmp word ptr [bp -2],ax ;跟堆栈中的值进行比较
Jne debug ;如果不同,说明发现了调试器。  
如上面的注释所述,一个值被压入堆栈然后又被弹出。如果存在调试器,那么堆栈指针–2位置上的值就会跟刚才弹出堆栈的值有所不同,这时就可以采取适当的行动。

5.在运行时中检测速度衰减

通过观察程序在运行时是否减速,恶意代码也可以检测出调试器。如果程序在运行时速度显著放缓,那就很可能意味着代码正在单步执行。因此如果两次调用的时间戳相差甚远,那么恶意软件就需要采取相应的行动了。Linux跟踪工具包LTTng/LTTV通过观察减速问题来跟踪病毒。当LTTng/LTTV追踪程序时,它不需要在程序运行时添加断点或者从事任何分析。此外,它还是用了一种无锁的重入机制,这意味着它不会锁定任何Linux内核代码,即使这些内核代码是被跟踪的程序需要使用的部分也是如此,所以它不会导致被跟踪的程序的减速和等待。

6.指令预取

如果恶意代码篡改了指令序列中的下一条指令并且该新指令被执行了的话,那么说明一个调试器正在运行。这是指令预取所致:如果该新指令被预取,就意味着进程的执行过程中有其他程序的切入。否则,被预取和执行的应该是原来的指令。

7.自修改代码

恶意软件也可以让其他代码自行修改(自行修改其他代码),这样的一个例子是HDSpoof。这个恶意软件首先启动了一些异常处理例程,然后在运行过程中将其消除。这样一来,如果发生任何故障的话,运行中的进程会抛出一个异常,这时病毒将终止运行。此外,它在运行期间有时还会通过清除或者添加异常处理例程来篡改异常处理例程。在下面是HDSpoof清除全部异常处理例程(默认异常处理例程除外)的代码。

exception handlers before:


0x77f79bb8 ntdll.dll:executehandler2@20 + 0x003a
0x0041adc9 hdspoof.exe+0x0001adc9
0x77e94809 __except_handler3


exception handlers after:


0x77e94809 __except_handler3


0x41b770: 8b44240c       mov      eax,dword ptr [esp+0xc]
0x41b774: 33c9           xor      ecx,ecx               
0x41b776: 334804         xor      ecx,dword ptr [eax+0x4]
0x41b779: 334808         xor      ecx,dword ptr [eax+0x8]
0x41b77c: 33480c         xor      ecx,dword ptr [eax+0xc]
0x41b77f: 334810         xor      ecx,dword ptr [eax+0x10]
0x41b782: 8b642408       mov      esp,dword ptr [esp+0x8]
0x41b786: 648f0500000000 pop      dword ptr fs:[0x0]    


下面是HDSpoof创建一个新的异常处理程序的代码。

0x41f52b: add      dword ptr [esp],0x9ca
0x41f532: push     dword ptr [dword ptr fs:[0x0]
0x41f539: mov      dword ptr fs:[0x0],esp
8.覆盖调试程序信息

一些恶意软件使用各种技术来覆盖调试信息,这会导致调试器或者病毒本身的功能失常。通过钩住中断INT 1和INT 3(INT 3是调试器使用的操作码0xCC),恶意软件还可能致使调试器丢失其上下文。这对正常运行中的病毒来说毫无妨碍。另一种选择是钩住各种中断,并调用另外的中断来间接运行病毒代码。

下面是Tequila 病毒用来钩住INT 1的代码:

new_interrupt_one:

   push bp
   mov bp,sp
   cs cmp b[0a],1      ;masm mod. needed
   je 0506             ;masm mod. needed
   cmp w[bp+4],09b4
   ja 050b             ;masm mod. needed
   push ax
   push es
   les ax,[bp+2]
   cs mov w[09a0],ax   ;masm mod. needed
   cs mov w[09a2],es   ;masm mod. needed
   cs mov b[0a],1
   pop es
   pop ax
   and w[bp+6],0feff
   pop bp
   iret

一般情况下,当没有安装调试器的时候,钩子例程被设置为IRET。V2Px使用钩子来解密带有INT 1和INT 3的病毒体。在代码运行期间,会不断地用到INT 1和INT 3向量,有关计算是通过中断向量表来完成的。

一些病毒还会清空调试寄存器(DRn的内容。有两种方法达此目的,一是使用系统调用NtGetContextThread和NtSetContextThread。而是引起一个异常,修改线程上下文,然后用新的上下文恢复正常运行,如下所示:

push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
xor eax, eax
div eax ;generate exception
pop fs:[0]
add esp, 4
;continue execution
;...
handler:
mov ecx, [esp+0Ch] ;skip div
add dword ptr [ecx+0B8h], 2 ;skip div
mov dword ptr [ecx+04h], 0 ;clean dr0
mov dword ptr [ecx+08h], 0 ;clean dr1
mov dword ptr [ecx+0Ch], 0 ;clean dr2
mov dword ptr [ecx+10h], 0 ;clean dr3
mov dword ptr [ecx+14h], 0 ;clean dr6
mov dword ptr [ecx+18h], 0 ;clean dr7
xor eax, eax
ret
上面的第一行代码将处理程序的偏移量压入堆栈,以确保当异常被抛出时它自己的处理程序能取得控制权。之后进行相应设置,包括用自己异或自己的方式将eax设为0,以将控制权传送给该处理程序。div eax 指令会引起异常,因为eax为0,所以AX将被除以零。该处理程序然后跳过除法指令,清空dr0-dr7,同样也把eax置0,表示异常将被处理,然后恢复运行。

啊冲 2016-02-03 10:45

9.解除调试器线程

我们可以通过系统调用NtSetInformationThread从调试器拆卸线程。为此,将ThreadInformationClass设为0x11(ThreadHideFromDebugger)来调用NtSetInformationThread,如果存在调试器的话,这会将程序的线程从调试器拆下来。以下代码就是一个例子:

push 0
push 0
push 11h ;ThreadHideFromDebugger
push -2
call NtSetInformationThread
在本例中,首先将NtSetInformationThread的参数压入堆栈,然后调用该函数来把程序的线程从调试器中去掉。这是因为这里的0用于线程的信息长度和线程信息,传递的-2用于线程句柄,传递的11h用于线程信息类别,这里的值表示ThreadHideFromDebugger。

10.解密

解密可以通过各种防止调试的方式来进行。有的解密依赖于特定的执行路径。如果这个执行路径没被沿用,比如由于在程序中的某个地方启动了一个调试器,那么解密算法使用的值就会出错,因此程序就无法正确进行自身的解密。HDSpoof使用的就是这种技术。

一些病毒使用堆栈来解密它们的代码,如果在这种病毒上使用调试器,就会引起解密失败,因为在调试的时候堆栈为INT 1所用。使用这种技术的一个例子是W95/SK病毒,它在堆栈中解密和构建其代码;另一个例子是Cascade病毒,它将堆栈指针寄存器作为一个解密密钥使用。代码如下所示:

lea   si, Start   ; position to decrypt
mov   sp, 0682  ; length of encrypted body

Decrypt:

xor   [si], si    ; decryption key/counter 1
xor   [si], sp  ; decryption key/counter 2
inc   si    ; increment one counter
dec   sp    ; decrement the other
jnz   Decrypt   ; loop until all bytes are decrypted
Start:            ; Virus body

对于Cascade病毒如何使用堆栈指针来解密病毒体,上面代码中的注释已经做了很好的说明。相反,Cryptor病毒将其密钥存储在键盘缓冲区中,这些密钥会被调试器破坏。Tequila使用解密器的代码作为解密钥,因此如果解密器被调试器修改后,那么该病毒就无法解密了。下面是Tequila用于解密的代码:

perform_encryption_decryption:

   mov bx,0
   mov si,0960
   mov cx,0960
  mov dl,b[si]
   xor b[bx],dl
   inc si
   inc bx
   cmp si,09a0
   jb 0a61             ;masm mod. needed
   mov si,0960
   loop 0a52           ;masm mod. needed
   ret

the_file_decrypting_routine:

   push cs
   pop ds
   mov bx,4
   mov si,0964
   mov cx,0960
   mov dl,b[si]
   add b[bx],dl
   inc si
   inc bx
   cmp si,09a4
   jb 0a7e             ;masm mod. needed
   mov si,0964
   loop 0a6f           ;masm mod. needed
   jmp 0390            ;masm mod. needed


人们正在研究可用于将来的新型反调试技术,其中一个项目的课题是关于多处器计算机的,因为当进行调试时,多处理器中的一个会处于闲置状态。这种新技术使用并行处理技术来解密代码。

二、逆转录病毒

逆转录病毒会设法禁用反病毒软件,比如可以通过携带一列进程名,并杀死正在运行的与表中同名的那些进程。许多逆转录病毒还把进程从启动列表中踢出去,这样该进程就无法在系统引导期间启动了。这种类型的恶意软件还会设法挤占反病毒软件的CPU时间,或者阻止反病毒软件连接到反病毒软件公司的服务器以使其无法更新病毒库。

三、混合技术

W32.Gobi病毒是一个多态逆转录病毒,它结合了EPO和其他一些反调试技术。该病毒还会在TCP端口666上打开一个后门。

Simile(又名Metaphor)是一个非常有名的复合型病毒,它含有大约14,000行汇编代码。这个病毒通过寻找API调用ExitProcess()来使用EPO,它还是一个多态病毒,因为它使用多态解密技术。它的90%代码都是用于多态解密,该病毒的主体和多态解密器在每次感染新文件时,都会放到一个半随机的地方。Simile的第一个有效载荷只在3月、6月、9月或12月份才会激活。在这些月份的17日变体A和B显示它们的消息。变体C在这些月份的第18日显示它的消息。变体A和B中的第二个有效载荷只有在五月14日激活,而变体C中的第二个有效载荷只在7月14日激活。

Ganda是一个使用EPO的逆转录病毒。它检查启动进程列表,并用一个return指令替换每个启动进程的第一个指令。这会使所有防病毒程序变得毫无用处。

四、小结

本文中,我们介绍了恶意软件用以阻碍对其进行逆向工程的若干反调试技术,同时介绍了逆转录病毒和各种反检测技术的组合。我们应该很好的理解这些技术,只有这样才能够更有效地对恶意软件进行动态检测和分析。

啊冲 2016-02-03 10:46

恶意软件反检测技术简介:模拟器超限技术
恶意软件是一种招人恨的代码,因为它们专干坏事,如泄漏个人隐私、造成数据丢失,等等。而杀毒软件公司则不断想办法检测并阻止恶意软件。如此一来,猫和老鼠的大戏从此开演了。一般来说,杀毒软件要想防御某种恶意软件需要经过以下过程:收集到样本,分析样本,升级病毒库,之后杀毒软件才能够识别该恶意软件。而反检测技术,就是在恶意软件的分析阶段设置障碍,让分析人员无法或难以对恶意代码进行分析,这主要包括两类技术,一种是反调试技术,一种是反仿真技术,或叫做反虚拟执行技术。

无论是处于分析恶意软件的目的,还是防止软件被分析的目的,了解恶意软件常用的反检测手段都是很有必要的,而本文的目的就是在于,向读者们介绍目前的反调试和反仿真技术,并提供代码样本,以供读者在识别恶意代码时作为练手之用。

一、引言

病毒作者使用反调试与反仿真技术的目的在于为逆向分析恶意软件制造障碍,理想的情况下是使得逆向工程师无法分析恶意软件,退一步讲即使可以分析也会让分析过程更为缓慢。当病毒在一个仿真器或调试器中运行时,这些技术会使逆向工程过程变得举步维艰,他们企图以此逃避检查。恶意代码可以利用多种不同的方法来“忽悠”动态检测和其他如仿真器和调试器之类的分析机制,通常情况下每种病毒都会采用其中的一种甚至多种。

对于这些病毒作者用于使对病毒的逆向过程变缓的方法,本文将分别进行讲解,并为它们提供了示例。本文会概述各种反调试与反仿真技术,以使得读者对它们有所了解。下面我们开始介绍反仿真技术。

二、反仿真技术

仿真器为人们提供了一个受限制的环境(即安装在主机操作系统之上的操作系统的映像),我们可以在这个环境中动态地分析程序。例如,一台运行Linux操作系统的机器可以在其虚拟机上安装并运行Windows XP系统。一个仿真器也将包括对CPU和内存的仿真,以及其它硬件的仿真,还有控制器的仿真。这为我们提供了一种安全的方式来动态地分析程序,因为这种环境下,无论做什么都不会对底层的操作系统造成损害。

然而,这一系统也有其缺点。首先,花在仿真上的时间越多,在对目标程序进行实际分析之前所必须等待的时间也就相应越长。其次,仿真过程是缓慢的,因为不仅对目标程序实施监控需要开销,而且对操作系统和硬件进行仿真同样也需要开销。QEMU,一种硬件和操作系统仿真器,其仿真出来的硬件比物理硬件的速度有四分之一到十分之一之间的下降,软件在其上运行的速度只是在内存管理单元上运行时的二分之一。同时,因为QEMU不具备任何动态监控功能,这会进一步降低速度。最后一个缺点是,恶意软件可以利用各种反仿真技术来“忽悠”模拟器,这些技术分为三类:比耐力型、比智力型以及过度扩展模拟器。

三、跟模拟器比耐力

因为仿真的代价较高,所以模拟器通常只运行代码的前几百条指令来检测恶意软件,后面的指令通常不予理会。在英特尔的X86系统上,人们普遍相信,只要运行1000条指令便足以检测出恶意代码,同时还能保持较短的运行时间。如果在这段时间内没有运行被视为恶意的指令,那么模拟器就不会再将该代码作为恶意软件而继续检查。跟模拟器比耐力的方法有很多,如基于特定概率的感染技术,在运行恶意代码之前先将良性代码运行特定的时间,或通过入口点迷惑技术来达到目的。

1.概率式感染

一些恶意软件的恶意代码并不是每次都会执行,而是按照一定的概率发作。例如,某病毒在运行时执行恶意代码的概率是10%,这就意味着,模拟器将其运行若干次,才有可能检测到恶意代码。

2.运行良性代码

有的恶意软件会在每次启动时先运行良性代码,并且良性代码运行一段时间后,才开始运行恶意代码。这样做的目的是让模拟器运行指定数量的指令,让它觉得该代码在这段时间内没有做恶意的事情。等危险期过后,它就会露出本来面目:感染并危害系统。

3.入口点迷惑技术

入口点迷惑技术(EPO)是一种将附带的恶意代码放到一个文件的特定的部分的方法。恶意软件不会一上来就执行恶意代码,相反,它能查找对ExitProcess()API函数的调用,然后用一个跳至恶意代码的转移指令来覆盖这些调用。用这种方法,代码将在可执行文件的出口处,而非入口处运行。它还可以寻找一个特定的代码序列,然后用恶意代码本身或跳至恶意代码的转移指令来覆盖之。所以,入口点迷惑技术后,能够使病毒在将代码拷贝至新的位置之后、实际覆盖它之前运行恶意代码。

四、跟模拟器比智力

为了智取模拟器,一些恶意软件使用基于时间的触发器,或者其它的条件转移,或者使用不同的解密技术(如果它是采用加密的恶意软件的话)。基于时间的触发器可以只有在下午3点或指定的日期运行恶意代码。恶意软件也可以检测某些条件是否成立,如查看自身是否正在被调试或仿真,如果条件成立,则执行良性代码。

解密技术也可以将解密循环分布到代码的各处,或者使用多轮解密技术。多轮解密技术已经被W32/Harrier、W32/Coke和W32/Zelly用过了,其中第一个解密器解密第二个解密器,第二个解密器又解密第三个解密器,以此类推。为了靠小聪明玩弄模拟器,恶意软件也可以仅仅解密所需的代码块。

RDA.Fighter病毒使用蛮力解密技术,这意味着,该病毒不会储存解密密钥,所以它必须尝试所有可能的解密密钥,以便解密本身。这是非常有用的,因为如果反病毒公司使用了不同的蛮力解密方法的话,对病毒解密是非常困难的,它需要大量的仿真指令。还有一种可能,就是只要模拟器正在运行,病毒就不对自身进行解密。下面是病毒RDA Fighter用于解密的代码。

setup:
        xor ebx,ebx

iterate:

       mov esi,[ebp + hostOffset]
       mov edi,esi
       mov ecx,[ebp + host_size]
       inc ebx

decrypt:

       lodsb
       xor al,bl
       stosb
       loop decrypt

check:

       mov esi,[ebp + hostOffset]
       push esi
       mov ecx,[ebp + host_size]
       push ecx
       mov eax,[ebp + __ADDR_CheckSum]  ; whatever this happens to be
       call eax
       test eax,eax
       jnz iterate
       mov esi,[ebp + hostOffset]
       jmp esi

在上面的例子中,我们看到setup段将ebx设为0,并且它只运行一次。然后,在循环部分将esi和edi设为被加密的代码的起点,将ecx设为被加密的代码的尺寸,ebx每次递增1。用于解密的基本块会逐字节遍历被加密的代码,并用ebx中的密钥来异或每一字节。当ecx 变成零时,循环终止。之后,由负责检查的代码段来查看当前的代码是否跟预设的校验和是否匹配,如果匹配的话,那么说明代码已经成功解密。如果不匹配,则返回迭代循环并再次尝试下一个密码。如果已经解密,那么它会跳转到新解密代码处。

W95/Fono使用非线性解密算法,即病毒的加密部分是无法使用通常的线性方式来解密的。因为病毒无法一个字节接一个字节地解密,所以它能够迷惑仿真器。W95/Fono使用一个密钥表和解密器来完成基于该表的置换。明文字母中的每个符号对应于密文中的另一个不同的符号。例如:A和L相对应,Z和F相对应,等等。因此,使用这个加密方法后,病毒的各个部分是以半随机的顺序解密的,并且每个位置仅出现一次。此外,W95/Drill和(W32,Linux)/Simile.D也使用非线性解密方法。

W95/Silcer和W95/Resure装入内存时,它们强迫Windows的加载器对受感染的程序的映像进行重定位。映像的重定位处理决定了病毒体的解密处理,因为病毒解密时必须进行特定的重定位处理。W95/Resurrel是继W95/Resure之后出现的一种病毒,该病毒会把刚感染的文件的基地址设为0xBFxxxxxx(其中XXXXXX是一个由API调用GetTickCount()返回的随机值)。然后,它为自己代码部分的每个DWORD值添加一个重定位表项,并对每个DWORD进行加密,方法是将代码部分的DWORD加上基址值,然后减去0x400000。当应用程序被执行时,会因为基地址错误,或者一个地址位于KERNEL32.DLL中而导致程序的映像无法照现在的样子装入内存。这样,系统装入程序就需要把代码重定位到一个有效地址,这实际上也是病毒的解密工作要做的事情。记住,病毒越难以解密,检测起来就越难。

五、模拟器超限技术

所谓模拟器超限技术,是指执行一组将导致仿真器崩溃或可以表明仿真器正在运行的指令。调用仿真器不支持的未公开指令就是一种导致仿真器掷出异常并停止运行的方法。W95/Vulcano就是这样一个例子,它使用了非正式的CPU指令SALC。用来检测仿真或导致正在运行的仿真器崩溃(如果有的话)的另一种方法是,尝试一次访问大量内存,大到什么程度呢?如果机器上安装了2G内存,那么你就一次访问1G以上的内存。这通常是不能有效执行的,因为大多数的操作系统,以及仿真器,将阻止程序的这种行为。

检测是否正在运行仿真器的一种方法是,将每次调用都返回不同的值的函数调用两次,例如,我们可以对任何时间函数调用两次,然后检查两个返回值之差的大小。在Windows系统下,我们可以通过kernel32!QueryPerformanceCounter(包装有ZwQueryPerformaceCounter),kernel32!GetTickCounter达到上述目的;或通过RDTSC(读时间戳计数器)指令查询自从机器启动以来目前已经执行的机器周期数也能达到目的。下面是一个例子:

push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
rdtsc
push eax
xor eax, eax
div eax ;trigger exception
rdtsc
sub eax, [esp] ;ticks delta
add esp, 4
pop fs:[0]
add esp, 4
cmp eax, 10000h ;threshold
jb @not_debugged
@debugged:
...
@not_debugged:
...
handler:
mov ecx, [esp+0Ch]
add dword ptr [ecx+0B8h], 2 ;skip div
xor eax, eax
ret

上例向我们展示了如何利用RDTSC指令来检测是否存在调试器。如果存在的话,RDTSC被调用两次后返回的值会有变化。然后用这个差异跟一个阈值(在本例中它是10000h)进行比较。如果差异较大,那么就表明存在调试器,所以它会执行一个跳离恶意代码的jump指令。

虽然导入各种迷惑性的程序库不能导致仿真器停止运行,但是如果不导入这些库,恶意软件本身代码就无法运行。

恶意软件还可以寻找网页,看看它是否可以使用互联网。这样会导致当病毒的代码所在机器当前没有连接到互联网的时候,它就不执行;另外,当仿真器运行时病毒的代码也会停止运行,因为大多数仿真器不允许访问互联网。

使用协处理器浮点运算单元(FPU)指令是另一种过分扩展仿真器的方法,因为大部分仿真器都不会仿真FPU指令。Prizzy polymorphic engine (PPE)能够产生43条不同的协处理器指令供其多态解密器使用。如果不提供这些FPU指令的话,就无法对Prizzy进行解密。

同理,恶意软件也可以使用MMX指令。这个指令集为X86架构新增了了8个寄存器。恶意软件可以通过CPUID指令检查是否支持MMX。使用这种技术恶意软件的例子是W32/Legacy和W32/Thorin。

恶意软件还可以设置一个异常处理程序,执行一些无用的代码块,然后间接执行自己的处理程序,以将控制权传送到多态解密器的另一部分。这一诡计之所以得逞,是因为仿真器不能处理异常。3.7部分给出了类似于这种技术的例子。

六、小结

本文中,我们介绍了恶意软件用以阻碍对其进行逆向工程的若干技术,并着重讲解了当前恶意软件常用的反仿真技术。我们应该很好的理解这些技术,以便能够更有效地对恶意软件进行动态检测和分析。在下篇中,我们会为读者介绍恶意软件常用来的反调试技术。

啊冲 2016-02-03 10:46

当PE文件经过某种手工修改后 会导致OD无法识别该文件PE文件
于是构成反调试


下面通过使用OD调试OD找到OD代码不合理的地方 修改之
由于OD里面空闲的空间很少 所以我不得不修改了某些看似无用的代码 来存放我的代码
当然也可以加节什么的 如果大家使用中出现了情况 
请将样本PE文件 发于我邮箱:[email protected] 
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
第一种BUG OD在判断NT选项头时候 不是读取数据目录的个数 在动态的进行计算的
而是直接判断SizeOfOptionHeader是否为E0 
而数据目录是最小可以指定为2项 最多就没有试过是多少了

总而言之 数据目录是可以动态变化的 于是选项头也就是变长的了
所以OD判断选项头是否为E0是错误滴...
所以.....


第二个BUG是 在读取导入表名称的时候 没有检查长度 如果在导入表中构造一个超级长的DLL名称 比如大于256个字节 OD具体怎么做的 我没有怎么调试 
这个也是可以构成反调试的 这个BUG 我只是粗略的跟踪了一下 
找到了一个关键点 并修复之 使用中可能会出现BUG

啊冲 2016-02-03 10:46

修改程序PE头手工实现反调试
1、实验目的:了解手工实现修改程序PE头的原理、实修改程序PE头手工实现反调试与免杀
前提:特征码经过准确定位之后,用此方法可完成反调试,有时可以实现免杀
(1)PE加载到内存的过程
PE指Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行体文件格式。PE结构如图1,最开始的部分是 MZP区,然后是DOS stub区(即注释区,此区域只对DOS命令行式下执行的可执行文件才生效),然后是
表1:PE结构
当一个可执行文件被加载到内存中执行时,内存首先会读取 DOS MZ header区(即MZP区)的内容,当读到PE文件头(PE header)所在位置(DOS MZ header 里的 PE header 偏移量)时,会跳过DOS stub区域,直接跳至PE header所在区域,并由PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header的尾部。PE装载器再读取节表中的节信息,并采用文件映射方法将这些节映射到内存,同时附上节表里指定的节属性。 PE文件映射入内存后,PE装载器将处理PE文件中类似 import table(引入表)逻辑部分。
(2)修改程序PE头实现反调试的原理
因为现在90%以上的程序都是视窗界面(Win32环境下)的运行的,所以DOS stub区域不会被加载到内存当中的。所以可以把PE区段上移至DOS stub区段,再修改PE区段中的关联设置,保证其有效性。只要程序的有效性不被破坏,程序便可正常运行。这样可以在不影响程序正常运行的前提下简单实现反调试。
2、实验步骤
先在高级语言环境下编制一个程序,并生成jqm.exe程序。
(1)用ollydbg将jqm.exe程序加载,如图1:第一条语句所在的地址单元为00467938,对应的反汇编语句为push ebp,它是程序的入口点。

(2)用十六进制文本编辑器c32asm加载jqm.exe,如图2。图中最左列为相对位移,中间列为相对位移的地址单元所对应的十六进制数据,最右列为这些十六进制数据所对应的ASCII字符。最右列中有“MZP”标志的为MZP区段,有“This program”标志的为DOS stub(即注释区段),有“PE”标志的为PE区段。可以看出:PE区段的起始位置为00000100,它在MZP区段中表示为00 01 00 00(因实验机采用的是intel的CPU,其数据是以倒序方式存贮,所以用00 01 00 00来表示00000100)。
图2
(3)确定PE可选头大小:找到相对位移为00000100处(PE区段的起始位置)。找到它所对应的ASCII字符,距离“PE”字样的下一行出现“?”,它表示可选头的大小,对应的十六进制数据为 e0 00(读成00e0),这个十六进进制数据相当于十进制中的224,如图3:

(4)找出PE可选头的有效区域:“?”后的第4位光标处为可选头的起始位置,它的坐标值为00000118(从 “?”后至第4位之前为区段标志)。从00000118起至选224字节至000001f8处结束,选中的区域为PE可选头的区域。如图4:
图4
(5)移动PE区段:将光标从相对位移00000100处选起至000001f8处结束(此处为PE区段),鼠标右击—拷贝。光标移至00000050处(此处作为新PE区段的起始位置),从此处选至000001f8处—右键—粘贴。其结果如图5:

图5的00000148至000001f8处为重复区域,选中此区域,将此区域用0填充掉。
(6)修改MZP区段中PE区段的起始位置:现在新PE区段的起始位置变为了00000050,在MZP区中将出现PE头起始位置的十六进制数据由 00 01 00 00改为50 00 00 00,如图6:
图6
(7)修改可选头的大小:①可选头的起始位置变为了00000068(“?”后的第4位),终止位置仍为000001f8,选中此区域,此区域大小为400字节(即十六进制的190)。如图7:

图7
②光标移至“?”处,将“?”对应的十六进制数据由原来的e0 00变为 90 01。如图8:
图8
(8)保存:将文件进行保存,双击jqm.exe,程序仍然可以正常运行。
(9)测试:用ollydbg加载jqm.exe,出现如图9的提示,反调试成功。在准确得到特征码之后用此方法还可以实现病毒程序的文件免杀

啊冲 2016-02-03 10:46
OD载入程序就自动退出是比较恼人的,还没开始调试呢就退出了,这可让人如何是好。初学破解的人一定会遇到这个问题,怎样解决呢,网上虽然有零星的介绍但都不全面,以下是我总结的一些,希望能对各位初学者有所帮助。
(比如Peid、FI查壳查不到,OD一载入就退出,这极有可能是VMProtect的保护(虚拟机保护),用EXEinfo可以查出来一些版本的VMP,如果有这个提示那就更确定无疑了“A debugger has been found running in your system.Please, unload it from memory and restart your program”。)
1.更换几个OD试试,OllyICE、Shadow、加强版等
2.用附加的方式加载程序,文件-->附加,能解决很多问题
3.OD目录下,将475K 的DbgHelp.dll文件换成近1M大小的DbgHelp.dll文件,475K的有溢出漏洞,这条比较关键
4.使用StrongOD插件,(StrongOD+原版OD试试),这条比较关键
5.StrongOD中选择CreateAsRestrict
6.尝试命令bp ExitProcess,看能否发现什么线索
7.改变ollydbg.ini中的驱动名称,修改版的OD不需要自己改     
DriverName                 -      驱动文件名,设备对象名
DriverKey                     -     和驱动通信的key
HideWindow                 -    是否隐藏窗口,1为隐藏,0为不隐藏
HideProcess                 -     是否隐藏od进程,1为隐藏,0为不隐藏
ProtectProcess             -     是否隐藏保护Od进程,1为保护,0为不保护
8.改OD窗体类名,用的修改版的话一般都改过了,不需要自己再改
方法如下:
主窗体类名:
引用:
VA:004B7218
Offset:000B6018
各子窗体类名:
引用:
VA:004B565B ~ 004B568A
Offset:000B445B ~ 000B448A
改成任意,可以过GetWindow检测
9.手动修改程序“导出表”中的“函数名数目”值,上面方法不管用再试试它
方法:使用“LordPE”打开要编辑的PE程序,然后依次选择[目录]->[导出表对应的“..”按钮],把“函数名数目”的值减1,并点击“保存”按钮,就OK了。为了好看些,也可以把“函数数目”和“函数名数目”的值都同时减1并保存,效果一样。
   解释:一般情况下EXE不会加“导出表”,如果加了,就应该给出所导出的API函数。当我们打开这类PE程序(EXE版)时,会发现它存在“导出表”,但“导出表”中并没有导出的API函数。同时“函数数目”和“函数名数目”的值都比原PE程序设置的值大了1(如:EXE版“导出表”列表中显示了0个导出的API函数,壳将其“函数数目”和“函数名数目”的值都设置成了1;DLL版“导出表”列表中显示了0xD个导出的API函数,壳将其“函数数目”和“函数名数目”的值都设置成了0xE。)。所以我们将其减1,就OK了。被修改过的PE程序,可以正常运行,不会有任何影响。

这只是我的一点总结,附加方式加载、替换DBGHELP.DLL、使用StrongOD插件和修改导出表函数名数目的方法是可行的,能够解决一些问题。当然这些方法可能并不全面。

ANTI-OD原因解读:
概括来说:TLS回调函数在入口点之前执行,并进行了ANTI-OD的操作.
具体请看:TLS数据初始化和TLS回调函数都会在入口点之前执行,也就是说TLS是程序最开始运行的地方,因此可以在这里防止ANTI-OD的代码,检测并关闭OD。
应对方法:
  默认情况下OllyDbg载入程序将会暂停在入口点,应该配置一下OllyDbg使其在TLS回调被调用之前中断在实际的loader。
  通过“选项->调试选项->事件->第一次中断于->系统断点”来设置中断于ntdll.dll内的实际loader代码。这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。例如:在内存映像的.text代码段上设置内存访问断点,就可以断在TLS回调函数里。

更多TLS内容请看我的两篇博文:
TLS回调函数,Anti-od原理分析:http://hi.baidu.com/tjt999/blog/item...808f7eff1.html
TLS回调函数,Anti-od实例: http://hi.baidu.com/tjt999/blog/item...f359bf7f3.html

更多反调试知识请看《脱壳的艺术》和我的
《各种反调试技术原理与实例》: http://bbs.pediy.com/showthread.php?t=106143
如需交流请进群:1684360

实例代码:程序见附件,用原版OD测试,参考了某位大虾的代码。
.386
.model   flat,stdcall
option   casemap:none
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib

.data?
dwTLS_Index dd  ?

OPTION    DOTNAME
;; 定义一个TLS节          
.tls  SEGMENT                        
TLS_Start LABEL  DWORD
dd    0100h    dup ("slt.")
TLS_End   LABEL  DWORD
.tls   ENDS
OPTION    NODOTNAME

.data
TLS_CallBackStart  dd  TlsCallBack0
TLS_CallBackEnd    dd  0
szTitle            db  "Hello TLS",0
szInTls            db  "我在TLS里",0
szInNormal         db  "我在正常代码内",0
szClassName        db  "ollydbg"        ; OD 类名
;这里需要注意的是,必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置,
;其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。
PUBLIC _tls_used
_tls_used IMAGE_TLS_DIRECTORY

.code
;***************************************************************
;; TLS的回调函数
TlsCallBack0 proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID  
     mov     eax,dwReason ;判断dwReason发生的条件
     cmp     eax,DLL_PROCESS_ATTACH  ; 在进行加载时被调用
     jnz     ExitTlsCallBack0
     invoke  FindWindow,addr szClassName,NULL  ;通过类名进行检测
     .if     eax     ;找到
             invoke    SendMessage,eax,WM_CLOSE,NULL,NULL
     .endif
     invoke  MessageBox,NULL,addr szInTls,addr szTitle,MB_OK
     mov     dword ptr[TLS_Start],0  
     xor     eax,eax
     inc     eax
ExitTlsCallBack0: 
     ret
TlsCallBack0   ENDP
;****************************************************************
Start:
    invoke   MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK
    invoke   ExitProcess, 1
    end  Start

啊冲 2016-02-03 10:46

发现OD的处理

一、如何获取OD窗口的句柄

1.已经获取了窗口类名或标题:FindWindow

2.没有获取窗口类名或标题:GetForeGroundWindow返回前台窗口,这里就是OD的窗口句柄了。注意这种方法更为重要,因为大多数情况下不会知道OD的窗口类名。

invoke IsDebuggerPresent

              .if     eax

                      invoke GetForegroundWindow   ;获得的是OD的窗口句柄

                      invoke SendMessage,eax,WM_CLOSE,NULL,NULL

              .endif

二、获取OD窗口句柄后的处理

(1)向窗口发送WM_CLOSE消息

              invoke  FindWindow,addr szClassName,NULL  ;通过类名进行检测

              .if    eax     ;找到

                      mov     hWinOD,eax

invoke     MessageBox,NULL,offset szFound,offset szCaption,MB_OK                     invoke   SendMessage,hWinOD,WM_CLOSE,NULL,NULL

              .endif

(2)终止相关进程,根据窗口句柄获取进程ID,根据进程ID获取进程句柄,

_GetODProcID    proc

        LOCAL  @hWinOD              ;窗口句柄

        LOCAL  @hProcessOD           ;进程句柄

        LOCAL  @idProcessOD          ;进程ID

       invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

       .if    eax     ;找到

             mov       @hWinOD,eax        ;窗口句柄  

             invoke   GetWindowThreadProcessId,@hWinOD,addr @idProcessOD  

;获取进程ID在@idProcessOD里

             invoke   OpenProcess,PROCESS_TERMINATE,TRUE,@idProcessOD     

;获取进程句柄在返回值里

             .if    eax                     ;获取句柄成功

                     mov      @hProcessOD,eax

               invoke   TerminateProcess,@hProcessOD,200    ;利用句柄终止进程

                     invoke     CloseHandle,@hProcessOD            ;关闭进程句柄

                     invoke   MessageBox,NULL,addr szClose,addr szMerry,MB_OK

             .else                          ;获取句柄失败,多因权限问题

                     invoke    MessageBox,NULL,addr szFail,addr szCaption,MB_OK

             .endif                         .

       .endif               

       ret

_GetODProcIDendp

1.    窗口类名、窗口名

(1)      FindWindow

(2)      EnumWindow函数调用后,系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用GetWindowText得到窗口标题,用strstr等函数查找有无Ollydbg字符串。StrStr(大小写敏感,对应的StrStrI大小写不敏感)函数返回str2第一次出现在str1中的位置,如果没有找到,返回NULL。

(3)      GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,这样就可以向其发送WM_CLOSE消息将其关闭了。

(1)FindWindow

szClassName     db     'ollydbg',0

        invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

              .if       eax     ;找到

                    jmp   debugger_found    

              .endif               

(2)EnumWindow

.386

.modelflat,stdcall

optioncasemap:none

includewindows.inc

includeuser32.inc

includelibuser32.lib

includekernel32.inc

includelibkernel32.lib

include  Shlwapi.inc

includelib Shlwapi.lib   ;strstr



            .const

szTitle     db       'ollydbg',0       

szCaption   db       '结果',0

szFindOD    db       '发现目标窗口',0

szText      db       '枚举已结束,没提示发现目标,则没有找到目标窗口',0

            .code

;定义回调函数

_CloseWnd procuses ebx edi esi,_hWnd,_lParam

         LOCAL  @szBuffer[1024]:BYTE   ;接收窗口标题

         invoke IsWindowVisible,_hWnd

         .if eax ;是否是可见的窗口

             invoke GetWindowText,_hWnd,addr@szBuffer,sizeof @szBuffer

             invoke StrStrI,addr@szBuffer,offset szTitle  ;查找标题中有无字符串,不带I的大小写敏感

             .if eax

                 invoke   MessageBox,NULL,addr szFindOD,addrszCaption,MB_OK

                 invoke   PostMessage,_hWnd,WM_CLOSE,0,0  ;关闭目标

             .endif

         .endif

         mov eax,TRUE ;返回true 时,EnumWindows继续枚举下一个窗口,false退出枚举.

         ret

_CloseWnd endp



start:

           invoke   EnumWindows,addr _CloseWnd,NULL

;EnumWindows调用,系统枚举所有顶级窗口,为每个窗口调用一次回调函数

           invoke   MessageBox,NULL,addr szText,addrszCaption,MB_OK

           invoke   ExitProcess,NULL

           end start

1.   检测调试器进程

枚举进程列表,看是否有调试器进程(OLLYDBG.EXE,windbg.exe等)。

利用kernel32!ReadProcessMemory()读取进程内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。

              .386

              .model flat, stdcall

              option casemap :none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib      kernel32.lib

               .const

stSysProc       db     'OLLYDBG.EXE',0

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

_GetProcList    proc

       LOCAL  @stProcessEntry:PROCESSENTRY32

       LOCAL  @hSnapShot

       invoke  CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,NULL

       mov    @hSnapShot,eax                                    

       mov    @stProcessEntry.dwSize,sizeof @stProcessEntry

       invoke Process32First,@hSnapShot,addr @stProcessEntry

       .while eax

              invokelstrcmp,addr @stProcessEntry.szExeFile,addr stSysProc

              .if    eax == 0       ;为0,说明进程名相同

                  push 20

                 invoke  MessageBox,NULL,addrszFound,addr szCaption,MB_OK

              .endif             

              invokeProcess32Next,@hSnapShot,addr @stProcessEntry                     

       .endw

       pop    eax

       .if    eax != 20

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

       .endif

       ret

_GetProcListendp



start:

              invoke  _GetProcList

              invoke     ExitProcess,NULL

              end  start

1.    父进程是否是Explorer

原理:通常进程的父进程是explorer.exe(双击执行的情况下),否则可能程序被调试。

下面是实现这种检查的一种方法:

1.通过TEB(TEB.ClientId)或者使用GetCurrentProcessId()来检索当前进程的PID

2.用Process32First/Next()得到所有进程的列表,注意explorer.exe的PID(通过PROCESSENTRY32.szExeFile)和通过PROCESSENTRY32.th32ParentProcessID获得的当前进程的父进程PID。Explorer进程ID也可以通过桌面窗口类和名称获得。

3.如果父进程的PID不是explorer.exe,cmd.exe,Services.exe的PID,则目标进程很可能被调试

对策:OllyAdvanced提供的方法是让Process32Next()总是返回fail,使进程枚举失效,PID检查将会被跳过。这些是通过补丁kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。

(1)通过桌面类和名称获得Explorer的PID 源码见附件

                .data?    

szDesktopClass       db    'Progman',0                ;桌面的窗口类

szDesktopWindow  db    'ProgramManager',0         ;桌面的窗口名称

dwProcessID     dd  ?                        ;保存进程ID

dwThreadID      dd  ?                       ;保存线程ID

                .code

invoke     FindWindow,addr szDesktopClass,addrszDesktopWindow  ;获取桌面窗口句柄

invoke     GetWindowThreadProcessId,eax,offsetdwProcessID      ;获取EXPLORER进程ID

mov     dwThreadID,eax                     ;线程ID

(2)通过进程列表快照获得Explorer的PID 源码见附件

szExplorer      db     'EXPLORER.EXE',0

dwParentID     dd     ?

dwExplorerID   dd     ?

_ProcTest  proc

        local @stProcess:PROCESSENTRY32         ;每一个进程的信息

              local  @hSnapShot                    ;快照句柄     

          pushad                          

          

        invoke GetCurrentProcessId

        mov    ebx,eax                ;当前进程ID

              invoke     RtlZeroMemory,addr @stProcess,sizeof @stProcess ; 0初始化进程信息结构

              mov      @stProcess.dwSize,sizeof@stProcess             ;手工填写结构大小

              invoke     CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0;获取进程列表快照

              mov       @hSnapShot,eax                                  ;快照句柄

              invoke     Process32First,@hSnapShot,addr @stProcess       ;第一个进程

              .while     eax

                .if  ebx [email protected]               ;是当前进程吗?

                      mov  eax,@stProcess.th32ParentProcessID      ;是,则保存父进程ID

                      mov  dwParentID,eax                  

                .endif

                invoke   lstrcmp,addr @stProcess.szExeFile,addrszExplorer ;Explorer进程ID

              .if   eax == 0       ;为0,说明进程名相同                       

                       mov eax,@stProcess.th32ProcessID

                       mov dwExplorerID,eax

               .endif                   

                   invoke  Process32Next,@hSnapShot,addr @stProcess ;下一个进程

              .endw

              invoke     CloseHandle,@hSnapShot  ;关闭快照

             

              mov  ebx,dwParentID

        .if ebx == dwExplorerID    ;父进程ID与EXPLORER进程ID比较                                  invoke MessageBox,NULL,offset szNotFound,offset szCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,offset szFound,offsetszCaption,MB_OK

              .endif

        popad

              ret

_ProcTest endp

1.    RDTSC/ GetTickCount时间敏感程序段

当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。

(1)RDTSC

将计算机启动以来的CPU运行周期数放到EDX:EAX里面,EDX是高位,EAX是低位。

如果CR4的TSD(timestamp disabled)置位,则rdtsc在ring3下运行会导致异常(特权指令),所以进入ring0,把这个标记置上,然后Hook OD的WaitForDebugEvent,拦截异常事件,当异常代码为特权指令时,把异常处的opcode读出检查,如果是rdtsc,把eip加2,SetThreadContext,edx:eax的返回由你了。

(2)GetTickCount 源码见附件

invoke GetTickCount           ;第一次调用

              mov     ebx,eax                ;结果保存在ebx里

              mov     ecx,10                 ;延时开始

              mov     edx,6                  ;单步走,放慢速度     

            mov     ecx,10                 ;延时结束

              invoke  GetTickCount           ;第二次调用

              sub     eax,ebx                ;计算差值

              .if     eax > 1000          ;假定大于1000ms,就说明有调试器  

                jmp   debugger_found

              .endif                  

2.    StartupInfo结构

原理:Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断OD是否在调试程序.

if (Info.dwX<>0) or(Info.dwY<>0) or (Info.dwXCountChars<>0) or(Info.dwYCountChars<>0) or   (Info.dwFillAttribute<>0) or (Info.dwXSize<>0) or(Info.dwYSize<>0) then  “有调试器”

*******************************************************************************

结构体

typedef struct _STARTUPINFO

{

   DWORD cb;            0000

   PSTR lpReserved;        0004

   PSTR lpDesktop;         0008

   PSTR lpTitle;            000D

   DWORD dwX;          0010

   DWORD dwY;           0014

  DWORD dwXSize;        0018

   DWORD dwYSize;        001D

   DWORD dwXCountChars;  0020

   DWORDdwYCountChars;  0024

   DWORDdwFillAttribute;   0028

   DWORD dwFlags;         002D

   WORD wShowWindow;    0030

   WORD cbReserved2;       0034

   PBYTE lpReserved2;       0038

   HANDLE hStdInput;       003D

   HANDLE hStdOutput;      0040

   HANDLE hStdError;       0044

} STARTUPINFO, *LPSTARTUPINFO;

_ProcTest  proc

                LOCAL  @stStartupInfo:STARTUPINFO       

               pushad           

                  invoke  GetStartupInfo,addr @stStartupInfo

                  cmp     @stStartupInfo.dwX,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwY,0

                jnz      foundDebugger

                  cmp     @stStartupInfo.dwXCountChars,0

               jnz      foundDebugger

                  cmp     @stStartupInfo.dwYCountChars,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwFillAttribute,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwXSize,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwYSize,0

                  jnz      foundDebugger                

     noDebugger: “无调试器”

                jmp     TestOver

  foundDebugger: “有调试器”

       TestOver:       

                popad

               ret

_ProcTest endp

1.    BeingDebugged

kernel32!IsDebuggerPresent() API检测进程环境块(PEB)中的BeingDebugged标志检查这个标志以确定进程是否正在被用户模式的调试器调试。

每个进程都有PEB结构,一般通过TEB间接得到PEB地址

Fs:[0]指向当前线程的TEB结构,偏移为0处是线程信息块结构TIB

TIB偏移18H处是self字段,是TIB的反身指针,指向TIB(也是PEB)首地址

TEB偏移30H处是指向PEB结构的指针

PEB偏移2H处,就是BeingDebugged字段,Uchar类型

(1)      调用IsDebuggerPresent函数,间接读BeingDebugged字段

(2)      利用地址直接读BeingDebugged字段

对策:

(1)      数据窗口中Ctrl+G fs:[30] 查看PEB数据,将PEB.BeingDebugged标志置0

(2)      Ollyscript命令"dbh"可以补丁这个标志

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

               ;调用函数IsDebuggerPresent

              invoke  IsDebuggerPresent

              .if     eax

                     invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

              .endif                               

                ;直接去读字段

                assume fs:nothing

                mov     eax,fs:[30h]

                movzx   eax,byte ptr [eax+2]

              .if     eax

                     invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

              .endif                 

              invoke     ExitProcess,NULL

              end  start

1.    PEB.NtGlobalFlag, Heap.HeapFlags, Heap.ForceFlags

(1)通常程序没有被调试时,PEB另一个成员NtGlobalFlag(偏移0x68)值为0,如果进程被调试通常值为0x70(代表下述标志被设置):

FLG_HEAP_ENABLE_TAIL_CHECK(0X10)

FLG_HEAP_ENABLE_FREE_CHECK(0X20)

FLG_HEAP_VALIDATE_PARAMETERS(0X40)

这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意PEB.NtGlobalFlag的默认值可以通过gflags.exe工具或者在注册表以下位置创建条目来修改:

HKLM\Software\Microsoft\WindowsNt\CurrentVersion\Image File Execution Options

assume fs:nothing

                mov     eax,fs:[30h]

                mov     eax,[eax+68h]

                and     eax,70h

(2)由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。正常情况下系统为进程创建第一个堆时会将Flags和ForceFlags分别设为2(HEAP_GROWABLE)和0 。当进程被调试时,这两个标志通常被设为50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。

assume fs:nothing

              mov     ebx,fs:[30h]     ;ebx指向PEB

            mov     eax,[ebx+18h]   ;PEB.ProcessHeap

            cmp      dword ptr [eax+0ch],2    ;PEB.ProcessHeap.Flags

            jne        debugger_found

                 cmp dword ptr [eax+10h],0         ;PEB.ProcessHeap.ForceFlags

                 jne   debugger_found

这些标志位都是因为BeingDebugged引起的。系统创建进程的时候设置BeingDebugged=TRUE,后来NtGlobalFlag根据这个标记设置FLG_VALIDATE_PARAMETERS等标记。在为进程创建堆时,又由于NtGlobalFlag的作用,堆的Flags被设置了一些标记,这个Flags随即被填充到ProcessHeap的Flags和ForceFlags中,同时堆中被填充了很多BAADF00D之类的东西(HeapMagic,也可用来检测调试)。

一次性解决这些状态见加密解密P413

.386

.model flat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

              assume  fs:nothing

              mov     ebx,fs:[30h]  ;ebx指向PEB

              

              ;PEB.NtGlobalFlag

            mov    eax,[ebx+68h]

            cmp     eax,70h

                  je     debugger_found                   



                ;PEB.ProcessHeap

             mov    eax,[ebx+18h]



             ;PEB.ProcessHeap.Flags

             cmp      dwordptr [eax+0ch],2       

             jne debugger_found

              

              ;PEB.ProcessHeap.ForceFlags

                  cmp      dword ptr [eax+10h],0

                   jne debugger_found

             

             invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

             jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK     

                                  

          exit:   invoke     ExitProcess,NULL

                 end  start

1.     DebugPort:CheckRemoteDebuggerPresent()/NtQueryInformationProcess()

Kernel32!CheckRemoteDebuggerPresent()是用于确定是否有调试器被附加到进程。

BOOL CheckRemoteDebuggerPresent(

HANDLE   hProcess,

PBOOL     pbDebuggerPresent

)

Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。

这个API内部调用了ntdll!NtQueryInformationProcess(),由它完成检测工作。

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .data?

dwResult        dd     ?

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

                invoke  GetCurrentProcessId

                invoke  OpenProcess,PROCESS_ALL_ACCESS,NULL,eax             

                invoke  CheckRemoteDebuggerPresent,eax,addr dwResult

                cmp     dword ptr dwResult,0

                jne     debugger_found   

                         

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK                    exit:    invoke       ExitProcess,NULL

                      end  start

ntdll!NtQueryInformationProcess()有5个参数。

为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7)。

NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员,这个成员是系统用来与调试器通信的端口句柄。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation将被置为0xFFFFFFFF,否则ProcessInformation将被置为0。

ZwQueryInformationProcess(

IN HANDLEProcessHandle,

INPROCESSINFOCLASS ProcessInformationClass,

OUT PVOIDProcessInformation,

IN ULONGProcessInformationLength,

OUT PULONGReturnLength OPTIONAL

);

.386

.modelflat,stdcall

optioncasemap:none



include    windows.inc

include    user32.inc

includelibuser32.lib

include    kernel32.inc

includelibkernel32.lib

include   ntdll.inc        ;这两个

includelib ntdll.lib

               .data?

dwResult        dd     ?

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0



              .code

start:

                invoke  GetCurrentProcessId

                invoke  OpenProcess,PROCESS_ALL_ACCESS,NULL,eax 

                invoke  ZwQueryInformationProcess,eax,7,offsetdwResult,4,NULL   

                cmp     dwResult,0               

                jne     debugger_found   

      

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK     

                                  

       exit: invoke     ExitProcess,NULL

              end  start

1.    SetUnhandledExceptionFilter/ Debugger Interrupts

调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用,Debugger Interrupts就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。注意测试时,在异常处理里取消选中INT3 breaks 和 Singal-stepbreak

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

lpOldHandler  dd    ?

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

; ExceptionHandler 异常处理程序

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov    [edi].regEax,0FFFFFFFFH  ;设置EAX

              mov     [edi].regEip,offset SafePlace

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret

_Handler endp



start:

              invoke    SetUnhandledExceptionFilter,addr_Handler

              mov lpOldHandler,eax

             

        xor eax,eax       ;清零eax

        int    3             ;产生异常,然后_Handler被调用

SafePlace:

              test       eax,eax

              je   debugger_found



              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

        jmp    exit

debugger_found: invoke  MessageBox,NULL,addr szFound,addr szCaption,MB_OK                         exit:       invoke    SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。

另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断"和"单步中断"复选框来完成设置。

1.   Trap Flag单步标志异常

TF=1的时候,会触发单步异常。该方法属于异常处理,不过比较特殊:未修改的OD无论是F9还是F8都不能处理异常,有插件的OD在F9时能正确处理,F8时不能正确处理。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib      kernel32.lib

              .data

lpOldHandler  dd    ?

szCaption       db     '检测结果',0

szFound         db     '程序未收到异常,说明有调试器',0

szNotFound      db     '程序处理了异常而到达安全位置,没有调试器',0

             .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov       lpOldHandler,eax

              pushfd ;push    eflags

              or      dword ptr [esp],100h   ;TF=1

              popfd

              nop

              jmp     die            

  SafePlace:      

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

       die:  invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

       exit:  invoke       SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

2.    SeDebugPrivilege 进程权限

默认情况下进程没有SeDebugPrivilege权限,调试时,会从调试器继承这个权限,可以通过打开CSRSS.EXE进程间接地使用SeDebugPrivilege确定进程是否被调试。注意默认情况下这一权限仅仅授予了Administrators组的成员。可以使用ntdll!CsrGetProcessId() API获取CSRSS.EXE的PID,也可以通过枚举进程来得到CSRSS.EXE的PID。

实例测试中,OD载入后,第一次不能正确检测,第二次可以,不知为何。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

include           kernel32.inc

include      ntdll.inc

includelib      user32.lib

includelib      kernel32.lib

includelib    ntdll.lib

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

               invoke CsrGetProcessId  ;ntdll!CsrGetProcessId获取CSRSS.EXE的PID

               invoke OpenProcess,PROCESS_QUERY_INFORMATION,NULL,eax

               test    eax,eax

               jnz   debugger_found

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK                         exit: invoke       ExitProcess,NULL

              end  start

3.   DebugObject:NtQueryObject()

未完成,期待做过这个测试的朋友指点一下!

除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。逆向论坛中讨论的一个有趣的方法就是检查DebugObject类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一

个DebugObject类型的对象。

DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3):

NTSTATUS NTAPI NtQueryObject(

IN    HANDLE                                         ObjectHandle,

IN    OBJECT_INFORMATION_CLASS   ObjectInformationClass,

OUT   PVOID                                           ObjectInformation,

IN    ULONG                                           Length,

OUT   PULONG                                        ResultLength

)

这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:

typedef struct _OBJECT_ALL_INFORMATION{

ULONG                                          NumberOfObjectsTypes;

OBJECT_TYPE_INFORMATION         ObjectTypeInformation[1];

}

检测例程将遍历拥有如下结构的ObjectTypeInformation数组:

typedef struct _OBJECT_TYPE_INFORMATION{

[00] UNICODE_STRING        TypeName;

[08] ULONG                          TotalNumberofHandles;

[0C] ULONG                  TotalNumberofObjects;

...more fields...

}

TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects 或 TotalNumberofHandles 是否为非0值。

1.   OllyDbg:Guard Pages

这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。

除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。

页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。

示例

下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。

对策

由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会
被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。

如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode, 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION即可。

实例:

                   .386

                   .modelflat,stdcall

                   optioncasemap:none

include                 windows.inc

include                 user32.inc

includelib        user32.lib

include                 kernel32.inc

includelib          kernel32.lib

                   .data

lpOldHandler      dd     ?

dwOldType   dd      ?

                   .const

szCaption      db      '检测结果',0

szFound        db      '检测到调试器',0

szNotFound     db      '没有调试器',0

                   .code

_Handler    proc  _lpExceptionPoint                 

                   pushad

                   mov  esi,_lpExceptionPoint

                   assume       esi:ptr EXCEPTION_POINTERS

                   mov  edi,[esi].ContextRecord

                   assume       edi:ptr CONTEXT

                   mov     [edi].regEax,0FFFFFFFFH   ;检测标志

                   mov     [edi].regEip,offset SafePlace

                   assume       esi:nothing,edi:nothing

                   popad

                   mov  eax,EXCEPTION_CONTINUE_EXECUTION

                   ret

_Handler    endp



start:

                   invoke        SetUnhandledExceptionFilter,addr_Handler

                   mov  lpOldHandler,eax

                                  

         invoke VirtualAlloc,NULL,1000H,MEM_COMMIT,PAGE_READWRITE ;分配内存

        push    eax                             

        mov    byte ptr [eax],0C3H ;写一个 RETN 到保留内存,以便下面的调用

       invoke VirtualProtect,eax,1000h,PAGE_EXECUTE_READ or PAGE_GUARD,addr dwOldType

        xor     eax,eax         ;检测标志

        pop     ecx

        call     ecx         ;执行保留内存代码,触发异常  

SafePlace:

                   test  eax,eax      ;检测标志

                   je     debugger_found                

                   invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

         jmp     exit

debugger_found: invoke MessageBox,NULL,addr szFound,addr szCaption,MB_OK

  exit:        invoke VirtualFree,ecx,1000H,MEM_DECOMMIT

              invoke       SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

                   invoke        ExitProcess,NULL

                   end    start

1.   Software Breakpoint Detection

软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。

(1)      实例一   普通断点

注意:在被保护的代码区域下INT3断点进行测试

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib     kernel32.lib

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:          jmp     CodeEnd    

   CodeStart:  mov     eax,ecx  ;被保护的程序段

                nop

                push    eax

                push    ecx

                pop     ecx

                pop     eax

   CodeEnd:    

                cld               ;检测代码开始

               mov     edi,offset CodeStart

               mov     ecx,offset CodeEnd -offset CodeStart

               mov     al,0CCH

               repne   scasb               

                jz      debugger_found      

                         

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

          jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

              end  start

(1)      实例二 函数断点bp

利用GetProcAddress函数获取API的地址

注意:检测时,BPMessageBoxA

.386

.modelflat,stdcall

optioncasemap:none

includewindows.inc

includeuser32.inc

includelibuser32.lib

includekernel32.inc

includelibkernel32.lib



            .const

szKernelDll  db       'user32.dll',0

szAPIMessboxdb        'MessageBoxA',0

szCaption    db       '结果',0

szFound       db       '发现API断点',0

szNotFound   db       '未发现断点',0

            .code

start:

           invoke  GetModuleHandle,addr szKernelDll

          invoke   GetProcAddress,eax,addrszAPIMessbox  ;API地址

           cld               ;检测代码开始

           mov     edi,eax  ;API开始位置

           mov     ecx,100H ;检测100字节

           mov     al,0CCH  ;CC

          repne   scasb                

           jz      debugger_found      

                         

         invoke  MessageBox,NULL,addrszNotFound,addr szCaption,MB_OK

           jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

                end start

1.    Hardware Breakpoints

硬件断点是通过设置名为Dr0到Dr7的调试寄存器来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的CONTEXT结构,该结构可以通过传递给异常处理例程的ContextRecord参数来访问。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib    kernel32.lib

              .data

lpOldHandler  dd    ?

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              cmp     [edi].iDr0,0     ;检测硬件断点

              jne     debugger_found           

              cmp     [edi].iDr1,0

              jne     debugger_found    

              cmp     [edi].iDr2,0

              jne     debugger_found    

              cmp     [edi].iDr3,0

              jne     debugger_found                  

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     TestOver

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK 

       TestOver:assume      esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov lpOldHandler,eax         

                xor eax,eax       ;清零eax

                mov     dword ptr [eax],0    ;产生异常,然后_Handler被调用               

SafePlace:          invoke     SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

1.    PatchingDetectionCodeChecksumCalculation补丁检测,代码检验和

补丁检测技术能识别壳的代码是否被修改,也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。

实例:改动被保护代码的话,CHECKSUM需要修改,通过OD等找出该值

注意:在被保护代码段下F2断点或修改字节来测试

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib     kernel32.lib

CHECKSUM        EQU    915Ch       ;改动被保护代码的话,需要修改

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:          jmp     CodeEnd    

   CodeStart:  mov     eax,ecx  ;被保护的程序段

                nop

                push    eax

                push    ecx

                pop    ecx

                pop     eax

   CodeEnd:                   

                mov       esi,CodeStart

                mov       ecx,CodeEnd - CodeStart

                xor eax,eax

checksum_loop:

                movzx    ebx,byte ptr [esi]

                add        eax,ebx

                rol eax,1

                inc esi

                loop       checksum_loop

               

                cmp       eax,CHECKSUM

                jne debugger_found            

                         

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

          jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

              end  start

1.    block input封锁键盘、鼠标输入

user32!BlockInput() API 阻断键盘和鼠标的输入。

典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。

示例:源码看附件

BlockInput()参数fBlockIt,true,键盘和鼠标事件被阻断;false,键盘和鼠标事件解除阻断:

; Block input

push                     TRUE

call               [BlockInput]



;...Unpackingcode...



;Unblock input

push                     FALSE

call               [BlockInput]

对策

(1)最简单的方法就是补丁 BlockInput()使它直接返回。

(2)同时按CTRL+ALT+DELETE键手工解除阻断。

2.   EnableWindow禁用窗口

与BlockInput异曲同工,也是禁用窗口然后再解禁

在资源管理器里直接双击运行的话,会使当前的资源管理器窗口被禁用。

在OD里面的话,就会使OD窗口被禁用。

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib          user32.lib

include           kernel32.inc

includelib     kernel32.lib

              .const

szCaption       db     '结果',0

szEnableFalse   db     '窗口已经禁用',0

szEnableTrue    db     '窗口已经恢复',0

              .code

start:         

                invoke  GetForegroundWindow

                mov     ebx,eax

                invoke  EnableWindow,eax,FALSE

                invoke  MessageBox,NULL,addr szEnableFalse,addrszCaption,MB_OK

                nop

                invoke  EnableWindow,ebx,TRUE

                invoke  MessageBox,NULL,addr szEnableTrue,addrszCaption,MB_OK

                nop

                  invoke     ExitProcess,NULL

              end  start

1.   ThreadHideFromDebugger

ntdll!NtSetInformationThread()用来设置一个线程的相关信息。把ThreadInformationClass参数设为ThreadHideFromDebugger(11H)可以禁止线程产生调试事件。

ntdll!NtSetInformationThread的参数列表如下。ThreadHandle通常设为当前线程的句柄(0xFFFFFFFE):

NTSTATUS NTAPI NtSetInformationThread(

IN  HANDLE                                           ThreadHandle,

IN  THREAD_INFORMATION_CLASS      ThreadInformaitonClass,

IN  PVOID                                              ThreadInformation,

IN  ULONG                                             ThreadInformationLength

);

ThreadHideFromDebugger内部设置内核结构ETHREAD的HideThreadFromDebugger成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用。

invoke GetCurrentThread

invoke NtSetInformationThread,eax,11H,NULL,NULL

对策:

(1)在ntdll!NtSetInformationThread()里下断,断下来后,操纵EIP防止API调用到达内核
(2)Olly Advanced插件也有补这个API的选项。补过之后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将不再深入内核仅仅执行一个简单的返回。

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelib  user32.lib

includelib  kernel32.lib

include    ntdll.inc

includelib  ntdll.lib

               .const    

szCaption       db     '确定以后看看效果',0

szNotice        db     '汇编代码会消失哦',0

szResult        db     '看到效果了吗?没有则稍等',0

              .code

start:

                invoke  MessageBox,NULL,addr szNotice,addrszCaption,MB_OK

                invoke  GetCurrentThread

                invoke  NtSetInformationThread,eax,11H,NULL,NULL   

                invoke  MessageBox,NULL,addr szResult,addrszCaption,MB_OK   

                mov     eax,ebx ;其它指令                         

              invoke     ExitProcess,NULL

              end  start            

1.    DisablingBreakpoints禁用硬件断点

;执行过后,OD查看硬件断点还存在,但实际已经不起作用了

;利用CONTEXT结构,该结构利用异常处理获得,异常处理完后会自动写回

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

lpOldHandler  dd    ?

              .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              xor     eax,eax

              mov     [edi].iDr0,eax

              mov     [edi].iDr1,eax

              mov     [edi].iDr2,eax

              mov     [edi].iDr3,eax             

mov     [edi].iDr6,eax

              mov     [edi].iDr7,eax

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov lpOldHandler,eax

             

                xor eax,eax       ;清零eax

                mov     dword ptr [eax],0    ;产生异常,然后_Handler被调用               

SafePlace:          invoke     SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

1.   OllyDbg:OutputDebugString() Format String Bug

OutputDebugString函数用于向调试器发送一个格式化的串,Ollydbg会在底端显示相应的信息。OllyDbg存在格式化字符串溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于Ollydbg对传递给kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受255个字节,但没对参数进行检查,所以导致缓冲区溢出。

例如:printf函数:%d,当所有参数压栈完毕后调用printf函数的时候,printf并不能检测参数的正确性,只是机械地从栈中取值作为参数,这样堆栈就被破坏了,栈中信息泄漏。。

示例:下面这个简单的示例将导致OllyDbg抛出违规访问异常或不可预期的终止。

szFormatStr     db    '%s%s',0

push   offset szFormatStr

call   OutputDebugString

对策:补丁 kernel32!OutputDebugStringA()入口使之直接返回

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

szFormatStr     db    '%s%s',0

szCaption       db     '呵呵',0

szNotice        db     '执行已结束,看到效果了吗?',0



              .code

start:

              push    offset szFormatStr

              call    OutputDebugString

              invoke  MessageBox,NULL,addr szNotice,addrszCaption,MB_OK         

              invoke     ExitProcess,NULL

              end  start

2.   TLS Callbacks

使用Thread Local Storage (TLS)回调函数可以实现在实际的入口点之前执行反调试的代码,这也是OD载入程序就退出的原因所在。(Anti-OD)

线程本地存储器可以将数据与执行的特定线程联系起来,一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块。动态绑定(运行时)线程特定数据是通过 TLS API(TlsAlloc、TlsGetValue、TlsSetValue 和 TlsFree)的方式支持的。除了现有的 API 实现,Win32 和 Visual C++ 编译器现在还支持静态绑定(加载时间)基于线程的数据。当使用_declspec(thread)声明的TLS变量
时,编译器把它们放入一个叫.tls的区块里。当应用程序加载到内存时,系统寻找可执行文件中的.tls区块,并动态的分配一个足够大的内存块,以便存放TLS变量。系统也将一个指向已分配内存的指针放到TLS数组里,这个数组由FS:[2CH]指向。

数据目录表中第9索引的IMAGE_DIRECTORY_ENTRY_TLS条目的VirtualAddress指向TLS数据,如果非零,这里是一个IMAGE_TLS_DIRECTORY结构,如下:

IMAGE_TLS_DIRECTORY32   STRUC

  StartAddressOfRawData  DWORD  ?   ; 内存起始地址,用于初始化新线程的TLS

  EndAddressOfRawData   DWORD  ?   ; 内存终止地址

AddressOfIndex         DWORD ?   ; 运行库使用该索引来定位线程局部数据

AddressOfCallBacks     DWORD ?   ; PIMAGE_TLS_CALLBACK函数指针数组的地址

SizeOfZeroFill          DWORD ?   ; 用0填充TLS变量区域的大小

Characteristics           DWORD ?   ; 保留,目前为0

IMAGE_TLS_DIRECTORY32   ENDS

AddressOfCallBacks 是线程建立和退出时的回调函数,包括主线程和其它线程。当一个线程创建或销毁时,在列表中的每一个函数被调用。一般程序没有回调函数,这个列表是空的。TLS数据初始化和TLS回调函数调用都在入口点之前执行,也就是说TLS是程序最开始运行的地方。程序退出时,TLS回调函数再被执行一次。回调函数:

TLS_CALLBACK proto Dllhandle : LPVOID, Reason : DWORD,Reserved : LPVOID

参数如下:

Dllhandle : 为模块的句柄

Reason可取以下值:

DLL_PROCESS_ATTACH 1 : 启动一个新进程被加载

DLL_THREAD_ATTACH 2 : 启动一个新线程被加载

DLL_THREAD_DETACH 3 : 终止一个新线程被加载

DLL_PROCESS_DETACH 0 : 终止一个新进程被加载

Reserverd:用于保留,设置为0

IMAGE_TLS_DIRECTORY结构中的地址是虚拟地址,而不是RVA。这样,如果可执行文件不是从基地址装入,则这些地址会通过基址重定位修正。而且IMAGE_TLS_DIRECTORY本身不在.TLS区块中,而在.rdata里。

TLS回调可以使用诸如pedump之类的PE文件分析工具来识别。如果可执行文件中存在TLS条目,数据条目将会显示出来。

Data directory

EXPORT                                rva:00000000      size:00000000

IMPORT                                 rva:00061000      size:000000E0

:::

TLS                                         rva:000610E0      size:00000018

:::

IAT                                          rva:00000000      size:00000000

DELAY_IMPORT                  rva:00000000      size:00000000

COM_DESCRPTR                rva:00000000      size:00000000

unused                                    rva:00000000      size:00000000

接着显示TLS条目的实际内容。AddressOfCallBacks成员指向一个以null结尾的回调函数数组。

TLS directory:

StartAddressOfRawData:                          00000000

EndAddressOfRawData:                           00000000

AddressOfIndex:                              004610F8

AddressOfCallBacks:                       004610FC

SizeOfZeroFill:                                          00000000

Characteristics:                                          00000000

在这个例子中,RVA 0x4610fc指向回调函数指针(0x490f43和0x44654e):

默认情况下OllyDbg载入程序将会暂停在入口点,应该配置一下OllyDbg使其在TLS回调被调用之前中断在实际的loader。

通过“选项->调试选项->事件->第一次中断于->系统断点”来设置中断于ntdll.dll内的实际loader代码。这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。例如,在内存映像的.text代码段上设置内存访问断点,可以断在TLS回调函数。

.386

.model   flat,stdcall

option   casemap:none

includewindows.inc

includeuser32.inc

includekernel32.inc

includelibuser32.lib

includelibkernel32.lib



.data?

dwTLS_Indexdd  ?



OPTION    DOTNAME

;; 定义一个TLS节         

.tls  SEGMENT                       

TLS_StartLABEL  DWORD

dd   0100h    dup ("slt.")

TLS_End   LABEL DWORD

.tls   ENDS

OPTION    NODOTNAME



.data

TLS_CallBackStart  dd TlsCallBack0

TLS_CallBackEnd    dd  0

szTitle            db "Hello TLS",0

szInTls            db "我在TLS里",0

szInNormal         db "我在正常代码内",0

szClassName        db "ollydbg"        ; OD 类名

;这里需要注意的是,必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置,

;其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。

PUBLIC_tls_used

_tls_usedIMAGE_TLS_DIRECTORY



.code

;***************************************************************

;; TLS的回调函数

TlsCallBack0proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID 

     mov    eax,dwReason ;判断dwReason发生的条件

     cmp    eax,DLL_PROCESS_ATTACH  ; 在进行加载时被调用

     jnz    ExitTlsCallBack0

     invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

     .if    eax     ;找到

             invoke    SendMessage,eax,WM_CLOSE,NULL,NULL

     .endif

     invoke MessageBox,NULL,addr szInTls,addr szTitle,MB_OK

     mov    dword ptr[TLS_Start],0 

     xor    eax,eax

     inc    eax

ExitTlsCallBack0:

     ret

TlsCallBack0   ENDP

;****************************************************************

Start:

    invoke  MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK

    invoke  ExitProcess, 1

    end Start



反反调试技术

本人脱壳逆向的水平不高,这里仅说一下本人的一点体会:

对于初学者来说主要是利用StrongOD等各种插件,这些插件能够躲过上面所说的很多检测。有了一定基础以后就可以根据各种反调试方法的弱点寻求反反调试的途径了。

v2680267313 2016-04-30 23:36
用户被禁言,该主题自动屏蔽!


查看完整版本: [-- 反调试与反反调试内容收集帖 方便大家学习 --]

风叶林-资源最多的免费辅助教程论坛 -> 驱动保护 -> 反调试与反反调试内容收集帖 方便大家学习 [打印本页] 登录 -> 注册 -> 回复主题 -> 发表主题

啊冲 2016-02-03 10:42

反调试与反反调试内容收集帖 方便大家学习

反调试技术在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术,首先我们来看看反调试技术。

一、Windows API方法
Win32提供了两个API, IsDebuggerPresent和CheckRemoteDebuggerPresent可以用来检测当前进程是否正在被调试,以IsDebuggerPresent函数为例,例子如下:


BOOL ret = IsDebuggerPresent();
printf("ret = %d\n", ret);


破解方法很简单,就是在系统里将这两个函数hook掉,让这两个函数一直返回false就可以了,网上有很多做hook API工作的工具,也有很多工具源代码是开放的,所以这里就不细谈了。


二、查询进程PEB的BeingDebugged标志位

当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了,例子如下:


bool PebIsDebuggedApproach()
{
       char result = 0;
       __asm
       {
                      // 进程的PEB地址放在fs这个寄存器位置上
              mov eax, fs:[30h]
                          // 查询BeingDebugged标志位
              mov al, BYTE PTR [eax + 2] 
              mov result, al
       }

       return result != 0;
}


三、查询进程PEB的NtGlobal标志位 

跟第二个方法一样,当进程被调试的时候,操作系统除了修改BeingDebugged这个标志位以外,还会修改其他几个地方,其中NtDll中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位,例子如下:

bool PebNtGlobalFlagsApproach()
{
       int result = 0;

       __asm
       {
                      // 进程的PEB
              mov eax, fs:[30h]
                          // 控制堆操作函数的工作方式的标志位
              mov eax, [eax + 68h]
                          // 操作系统会加上这些标志位FLG_HEAP_ENABLE_TAIL_CHECK, 
                          // FLG_HEAP_ENABLE_FREE_CHECK and FLG_HEAP_VALIDATE_PARAMETERS,
                          // 它们的并集就是x70
                          //
                          // 下面的代码相当于C/C++的
                          //     eax = eax & 0x70
              and eax, 0x70
              mov result, eax
       }

       return result != 0;
}


四、查询进程堆的一些标志位

这个方法是第三个方法的变种,只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了,所以这个方法貌似很强大,例子如下:


bool HeapFlagsApproach()
{
       int result = 0;

       __asm
       {
                      // 进程的PEB
              mov eax, fs:[30h]
                      // 进程的堆,我们随便访问了一个堆,下面是默认的堆
              mov eax, [eax + 18h]
                          // 检查ForceFlag标志位,在没有被调试的情况下应该是
              mov eax, [eax + 10h]
              mov result, eax
       }

       return result != 0;
}
反调试技术二
五、使用NtQueryInformationProcess函数
NtQueryInformationProcess函数是一个未公开的API,它的第二个参数可以用来查询进程的调试端口。如果进程被调试,那么返回的端口值会是-1,否则就是其他的值。由于这个函数是一个未公开的函数,因此需要使用LoadLibrary和GetProceAddress的方法获取调用地址,示例代码如下:

// 声明一个函数指针。
typedef NTSTATUS (WINAPI *NtQueryInformationProcessPtr)(
       HANDLE processHandle,
       PROCESSINFOCLASS processInformationClass,
       PVOID processInformation,
       ULONG processInformationLength,
       PULONG returnLength);

bool NtQueryInformationProcessApproach()
{
       int debugPort = 0;
       HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll "));
       NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
       if ( NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL) )
              printf("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n");
       else
              return debugPort == -1;

       return false;
}

六、NtSetInformationThread方法
这个也是使用Windows的一个未公开函数的方法,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger),等于告诉操作系统,将所有附加的调试器统统取消掉。示例代码:

// 声明一个函数指针。
typedef NTSTATUS (*NtSetInformationThreadPtr)(HANDLE threadHandle,
       THREADINFOCLASS threadInformationClass,
       PVOID threadInformation,
       ULONG threadInformationLength);

void NtSetInformationThreadApproach()
{
       HMODULE hModule = LoadLibrary(TEXT("ntdll.dll"));
      NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hModule, "NtSetInformationThread");
  
       NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);
}

七、触发异常的方法
这个技术的原理是,首先,进程使用SetUnhandledExceptionFilter函数注册一个未处理异常处理函数A,如果进程没有被调试的话,那么触发一个未处理异常,会导致操作系统将控制权交给先前注册的函数A;而如果进程被调试的话,那么这个未处理异常会被调试器捕捉,这样我们的函数A就没有机会运行了。
这里有一个技巧,就是触发未处理异常的时候,如果跳转回原来代码继续执行,而不是让操作系统关闭进程。方案是在函数A里修改eip的值,因为在函数A的参数_EXCEPTION_POINTERS里,会保存当时触发异常的指令地址,所以在函数A里根据这个指令地址修改寄存器eip的值就可以了,示例代码如下:
// 进程要注册的未处理异常处理程序A
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *pei)
{
       SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)
              pei->ContextRecord->Eax);
       // 修改寄存器eip的值
       pei->ContextRecord->Eip += 2;
       // 告诉操作系统,继续执行进程剩余的指令(指令保存在eip里),而不是关闭进程
       return EXCEPTION_CONTINUE_EXECUTION;
}

bool UnhandledExceptionFilterApproach()
{
       SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
       __asm
       {
              // 将eax清零
              xor eax, eax
              // 触发一个除零异常
              div eax
       }

       return false;
}

八、调用DeleteFiber函数
如果给DeleteFiber函数传递一个无效的参数的话,DeleteFiber函数除了会抛出一个异常以外,还是将进程的LastError值设置为具体出错原因的代号。然而,如果进程正在被调试的话,这个LastError值会被修改,因此如果调试器绕过了第七步里讲的反调试技术的话,我们还可以通过验证LastError值是不是被修改过来检测调试器的存在,示例代码:
bool DeleteFiberApproach()
{
       char fib[1024] = {0};
       // 会抛出一个异常并被调试器捕获
       DeleteFiber(fib);

       // 0x57的意思是ERROR_INVALID_PARAMETER
       return (GetLastError() != 0x57);
}

啊冲 2016-02-03 10:42

从零开始的反反调试日志
=============    编程环境    =====================
VS2008+DDK+VA+DDKWIZARD
1、安装VS2008,MSDN
2、安装DDK
3、安装ddkwizard_setup
4、安装Visual Assist X
5、-> 错误1 => 找到ddkbuild.bat、ddkbuild.cmd(下载)放入/windows/system32 目录下
6、-> 错误2 => 计算机/.../环境变量 ,添加两个系统变量
    1、W7BASE = D:\WinDDK\7600.16385.1
    2、WXPBASE = D:\WinDDK\7600.16385.1
7、-> 错误3 => VS2008/Tools/Options/Projects and Solutions/VC++ Directories
    Win32/Include files =>
    添加D:\WinDDK\7600.16385.1\inc\api 到末尾,否则编译普通win32应用程序会提示错误
    添加D:\WinDDK\7600.16385.1\inc\ddk 到末尾
Other:如果安装顺序有错,导致VA无法支持DDK,则将api、ddk 添加到VA 的/Options/Projects/C/C++ Directories
    => custom/Stable include files ,一样,添加到末尾
= done =

1 : error PRJ0019: A tool returned an error code from "Performing Makefile project actions"
=>'ddkbuild.cmd' 不是内部或外部命令,也不是可运行的程序
2 :1>DDKBLD: ERROR #3: To build using type W7 you need to set the %W7BASE% environment variable to point to the Windows 7/Windows 2008 Server R2 DDK base directory!
3 :VS2008 中UNICODE_STRING 按F12 无法追踪


=============    双机调试  =======================
Windbg+VMware
1、安装VMware
2、安装Windbg(DDK里面有这个东西)
3、安装IDA
4、VMware 设置(装有xp 和win7 两个系统)
xp版:
在c:\boot.ini 文件中添加debug 的启动项
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional - debug" /fastdetect /debug /debugport=com1 /baudrate=115200

win7版:
在msconfig 中添加debug 启动项
msconfig -> 高级选项-> 调试,调试端口COM1,波特率115200

在VMware 上修改两个系统的串口设置:
打开电源时连接
此终端是服务器
另一终端是一个应用程序
i/o 模式 轮询时主动放弃CPU占用
xp:使用命名管道\\.\pipe\com_1
win7:使用命名管道\\.\pipe\com_2

5、Windbg 设置
符号路径,xp 与win7 的符号都可以放在同一个目录下,没有的话,windbg 会自动将文件下载到E:\sysbols
E:\symbols;SRV*E:\symbols*http://msdl.microsoft.com/download/symbols

建立两个windbg 快捷方式的设置,修改其中参数,分别连接两个虚拟机
xp:
D:\tools\windbg\windbg.exe -b -k com:port=\\.\pipe\com_1,baud=115200,pipe
win7版:
D:\tools\windbg\windbg.exe -b -k com:port=\\.\pipe\com_2,baud=115200,pipe

6、在windbg 下dump 两个系统
  将整个win7系统内存dump下来( Full kernel dump),耗费了我40多个小时...其中一次机器休眠了...
  (Creating a full kernel dump over the COM port is a VERY VERY slow operation.)有除COM外其他的连接方式,但我不会。。
  .dump /f e:\win7_dump.dmp

=============   驱动加载工具====================
      后面附一个源码,喜欢的可以下

=============   准备工作完成====================
      至此,我们有了一个能随时能增加功能的驱动加载工具,一份win7 dump 文件,双机调试,编程环境。

=============   从零开始分析驱动层的反调试=========
      在这以前,只有应用层的逆向经验。没接触过驱动,也不知道反调试。
      下面是我过某个游戏驱动保护的过程,这个过程从11月14号左右开始,到12月1号结束。
         游戏一开始给人的表象有:
1、游戏进程、守护进程、驱动
2、OllyICE 附加列表中无法看到目标进程,任务管理栏则可正常显示目标进程名称。
3、Windows7:去除两个内核钩子Hook后,OD可看到目标进程,但是附加时提示附加失败!(用xuetr 看的到内核钩子)
4、Windows XP:去掉两个内核钩子,游戏直接退出。
5、采用虚拟机VMware+Windbg 调试,游戏进程 启动时报错!
6、VMware+Windows 7 [Debug]:游戏启动后Windows 7系统无响应,只能重新启动系统。
7、VMware+Windows 7:游戏可正常启动。
8、OD加载游戏主程,OD崩溃,模块时发生错误,错误代码:0xc0000005(最后发现是PE结构中一个模块名字超长导致,我的OD很老了)
9、OD正常加载游戏主程之后,有被检测到的信息,多次尝试找信息出处,无果


      以上是11月17日之前的各种尝试,也是最痛苦的时候——完全找不到任何方向。之后调整了思考方向,把重心放到第5、6、7条线索上。以下是当时调试日志的主要部分,有点小修改。


2010/11/17对**的调试终于有点突破^_^
      之前一直不清楚**是如何区分系统处于Debug还是正常状态。经过对Windows的异常分发机制,了解了Debug与正常状态的流程不同,主要是KdpTrap与KdpStub两个函数对应于不同的系统。
      至此,与双机调试有关的地方有4处:KdpDebugRoutine(函数指针)、KdpBootedNodbug(bool)、KdPitchDebugger(bool)、DebuggerEnabled(bool)。
      通过修改KdpDebugRoutine 指向KdpStub ,以及另外3个标志位,可将系统从Debug修改为正常状态,Windbg将处于等待状态。**可正常执行,待**加载完毕后,将上述4个值修改回来,Windbg可重新获取话语权!
      ******
      因此,我将要做另外一个任务,一个驱动程序,可以让系统在Debug与正常状态相互切换!这样,我就可以在游戏运行期间,随时进行调试。如果有可能,最好让驱动随时与OD进行通讯。

2010/11/18  完成驱动加载工具
      完成一个通用的驱动加载工具,测试,可将Debug系统在Debug 与 正常状态间随意切换。但是对于正常系统,却无法切换成Debug。下一步要做的,就是将正常系统也能随意切换!
(这个到现在也没开始做...)

2010/11/19
1、经过测试,被转换后的系统可以进行双机调试,下断ws2_32!send 失败。
2、使用XueTr恢复两个内核钩子后,OD能够看到** 进程,附加失败
3、针对附加失败,使用双机调试查看原因!关键函数kernel32!DebugActiveProcess。

         流程kernel32!DebugActiveProcess -> ntdll!ZwDebugActiveProcess -> 功能号0x60 -> KeServiceDescriptorTable[0][0x60*4] -> nt!NtDebugActiveProcess
         上述步骤能够成功运行
         失败存在于ntdll!NtCreateThreadEx -> nt!NtCreateThreadEx:
         经过跟踪发现,最终问题在上述线路中的nt_RtlImageNtHeaderEx+0x45处,由于对象** 进程的PE头被抹去,导致此函数判断时,返回了一个失败值!
         进一步的,在不恢复内核钩子的情况下,** 的Pe头不被改写,一旦恢复之后,**的某个线程会将此PE头抹去,导致OD无法附加
(有win7 dump ,结合ida 感觉真是好)

2010/11/??
         ** 在对比黑白名单后,判断是否放行目标进程。
         通过修改黑白名单的内容,OD 可以顺利附加,但是无法读出** 的模块信息!
(不知道具体日期了,主要是从xuetr 上看到的2个内核钩子入手nt!NtReadVirtualMemory,nt!NtWriteVirtualMemory,这期间,通过这条线索搞定了它的白名单)

2010/11/22  
         制作完相关工具后,经测试,OD 能够看见目标进程,附加,但附加之后便发生错误,无法看到对象的模块信息。应该是目标进程在不断的对debugport 进行清零操作,目前发现有

         多个线程有此动作,其中有一个是在不断新建线程,新的线程就是不断对debugport 做检查。如果绕过debugport 检查?
         (这里可能会有些不准确,但确定是的某个线程在对debugport 清零,查看了不少帖子,最后线索来自看雪)

2010/11/23   ** 对debugport 清零的动作
         Windbg 对debugport 下写断点
kd> u **+0x41764
**+0x41764:
9b2fb764 8702            xchg    eax,dword ptr [edx]    //清零操作
9b2fb766 6685e9         test     cx,bp
9b2fb769 660fbae501   bt        bp,1
9b2fb76e 8b36            mov     esi,dword ptr [esi]
9b2fb770 83ecdc         sub      esp,0FFFFFFDCh
9b2fb773 0f886545ffff  js         **+0x35cde (9b2efcde)
9b2fb779 f5               cmc
9b2fb77a 3bf1            cmp     esi,ecx


         手动修改edx 值,发现od 附加后可正常存活。但是如果暂停该线程,则会导致od 附加后,很快游戏自动退出!

         使用工具对**驱动代码部分做修改(debugport清零),在多次测试中,很少的情况可以一直附加,但实体机状态下,OD很快就被检测到。在程序自退出时,有弹出守护进程被异常终止的对话框。程序自退出时,会有一个单独线程,冻结此线程,OD 会存活的比较久。
(到现在为止,还不能对游戏下断点)

2010/11/25
         OD 对游戏下断,游戏会异常退出,0x80000003

2010/11/29
         了解线程的HidePort后,制作工具可以下断点,但是OD 还会被检测到。主要的问题在于线程0x00cc0654中调用了RtlExitUserProcess 函数(该函数又调用了ZwTerminateProcess)。
         该线程会不停的创建,但未经过CreateThread API(功能号为0x58)。
现在的问题是,创建该线程是否传递了参数进来?如果未有参数传递,是否该线程检测到OD运行?!
         补充:由于游戏主线程的HidePort被设置为1,导致内核将该线程上的异常屏蔽,不分发给用户层。因此OD修改的代码int3 会引发一个异常,导致主线程退出。

2010/11/30
         在nt!NtCreatethreadEx 下断,没有相关创建0x00cc0654 线程的调用!因此,还是无法知道程序中哪里创建了线程0x00cc0654 。比较奇怪的是,该线程应该是不断的被创建的、且线程ID 总是相同,但是retn 之后,该线程便不再被创建。。(之所以这么说,是因为在该线程的入口点,总是能断下)

2010/12/01

         基本实现OD 的附加调试,但是0x00cc0654 线程是从哪里来的,如何被创建,如何检查OD? (一直未解决,太多的代码变异)


总结:
大部分的反调试还是在驱动层面,并且是已知的几个技术点
1、  反Debug系统                          debug 系统与 正常版本切换
2、  DebugPort 清零                       nop 掉相关代码段
3、  主线程HidePort 置1                          重置HidePort
4、  内核函数钩子,采用白名单方式放行。     找到白名单,手动添加
5、  0x00cc0654 线程检测                  直接将线程入口修改为retn



我想很多在内核之外的人,跟我一样在门外徘徊,其实,只要做,并没有那么难。  

啊冲 2016-02-03 10:42

反反调试器跟踪”,对,没写错。

什么是“反调试器跟踪”?举个例子,用WinDbg尝试打开“极品飞车9“的speed.exe,运行,会被提示”Unload the debugger and try again".....
这就是“反调试器跟踪”,一般有两种方法:①调用kernel32!IsDebuggerPresent②把代码放到SetUnhandledExceptionFilter设定的函数里面。通过人为触发一个unhandled exception来执行。

HOWTO:“反反调试器跟踪”?
对第一种情况,很简单,kernel32!IsDebuggerPresent的实现是这样的:
0:000> uf kernel32!IsDebuggerPresent
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\KERNEL32.dll -
KERNEL32!IsDebuggerPresent:
7c813093 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c813099 8b4030          mov     eax,dword ptr [eax+30h]
7c81309c 0fb64002        movzx   eax,byte ptr [eax+2]
7c8130a0 c3              ret
这就简单啦,把[[FS:[18]]:30]:2的值改成0,IsDebuggerPresent就返回false,这种方法就挂了:
首先,bp kernel32!IsDebuggerPresent
0:004> g
Breakpoint 0 hit
eax=00a75950 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c813093 esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent:
7c813093 64a118000000    mov     eax,dword ptr fs:[00000018h] fs:003b:00000018=7ffdf000
0:000> p
eax=7ffdf000 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c813099 esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent+0x6:
7c813099 8b4030          mov     eax,dword ptr [eax+30h] ds:0023:7ffdf030=7ffd7000
0:000> p
eax=7ffd7000 ebx=00180dd0 ecx=013b2b48 edx=013ce3d8 esi=0012f018 edi=0012f0d4
eip=7c81309c esp=0012efec ebp=0012f000 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
KERNEL32!IsDebuggerPresent+0x9:
7c81309c 0fb64002        movzx   eax,byte ptr [eax+2]       ds:0023:7ffd7002=01
0:000> eb 0023:7ffd7002 00  <--------------------------------
0:000> g
所以稍微好一点的应用程序不会使用这种方法:)
第二种情况会麻烦一些,看如下代码:
LONG WINAPI UnhandledExceptionFilter1( struct _EXCEPTION_POINTERS* ExceptionInfo )
{
MessageBox(0,"UnhandledExceptionFilter1",0,0);
return 0;
}

int main(int, char*)
{
SetUnhandledExceptionFilter(UnhandledExceptionFilter1);
char *p=0;
*p=0;
return 0;
}
当应用程序被一个调试器attach之后,UnhandledExceptionFilter1不会被调用,从而程序可以通过这种逻辑来进行”反调试器跟踪“。
针对这种情况,可以通过下列办法让UnhandledExceptionFilter1执行,从而cheat应用程序:
0:000> bp kernel32!UnhandledExceptionFilter
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\kernel32.dll -
0:000> g
ModLoad: 76300000 7631d000   C:\WINDOWS\system32\IMM32.DLL
ModLoad: 77da0000 77e49000   C:\WINDOWS\system32\ADVAPI32.dll
ModLoad: 77e50000 77ee1000   C:\WINDOWS\system32\RPCRT4.dll
ModLoad: 62c20000 62c29000   C:\WINDOWS\system32\LPK.DLL
ModLoad: 73fa0000 7400b000   C:\WINDOWS\system32\USP10.dll
(4f4.314): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=7ffdf000 ecx=00007d06 edx=7c92eb94 esi=0012fe04 edi=0012ff0c
eip=0041155c esp=0012fe04 ebp=0012ff0c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
*** WARNING: Unable to verify checksum for tet.exe
tet!WinMain+0x3c:
0041155c c60000          mov     byte ptr [eax],0           ds:0023:00000000=??
0:000> bp Ntdll!NtQueryInformationProcess
0:000> g
Breakpoint 0 hit
eax=0012fa28 ebx=00000000 ecx=c0000005 edx=00000000 esi=00000000 edi=00000000
eip=7c862e62 esp=0012fa04 ebp=0012fff0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
kernel32!UnhandledExceptionFilter:
7c862e62 6874060000      push    674h
0:000> g
Breakpoint 1 hit
eax=ffffffff ebx=00000004 ecx=7c862c02 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e01b esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess:
7c92e01b b89a000000      mov     eax,9Ah
0:000> p
eax=0000009a ebx=00000004 ecx=7c862c02 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e020 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0x5:
7c92e020 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
0:000> p
eax=0000009a ebx=00000004 ecx=7c862c02 edx=7ffe0300 esi=0012fa28 edi=c0000005
eip=7c92e025 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0xa:
7c92e025 ff12            call    dword ptr [edx]      ds:0023:7ffe0300={ntdll!KiFastSystemCall (7c92eb8b)}
0:000> p
eax=00000000 ebx=00000004 ecx=0012f354 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c92e027 esp=0012f358 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
ntdll!NtQueryInformationProcess+0xc:
7c92e027 c21400          ret     14h
0:000> p
eax=00000000 ebx=00000004 ecx=0012f354 edx=7c92eb94 esi=0012fa28 edi=c0000005
eip=7c862eef esp=0012f370 ebp=0012fa00 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
kernel32!UnhandledExceptionFilter+0x8d:
7c862eef 85c0            test    eax,eax
0:000> r eax=80000000 <-----------------------------------
0:000> g
Breakpoint 1 hit
eax=0012f360 ebx=7c883780 ecx=00000000 edx=7c883780 esi=7c885ab4 edi=0012f8e4
eip=7c92e01b esp=0012f348 ebp=0012f364 iopl=0         nv up ei ng nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000296
ntdll!NtQueryInformationProcess:
7c92e01b b89a000000      mov     eax,9Ah
0:000> bc 1
0:000> g
ModLoad: 5adc0000 5adf7000   C:\WINDOWS\system32\uxtheme.dll
ModLoad: 74680000 746cb000   C:\WINDOWS\system32\MSCTF.dll
ModLoad: 10000000 10023000   C:\WINDOWS\system32\PROCHLP.DLL
ModLoad: 77bd0000 77bd8000   C:\WINDOWS\system32\version.dll
ModLoad: 73640000 7366e000   C:\WINDOWS\system32\msctfime.ime
ModLoad: 76990000 76acd000   C:\WINDOWS\system32\ole32.dll
ModLoad: 69760000 69776000   C:\WINDOWS\system32\faultrep.dll
ModLoad: 77bd0000 77bd8000   C:\WINDOWS\system32\VERSION.dll
ModLoad: 759d0000 75a7e000   C:\WINDOWS\system32\USERENV.dll
ModLoad: 762d0000 762e0000   C:\WINDOWS\system32\WINSTA.dll
ModLoad: 5fdd0000 5fe24000   C:\WINDOWS\system32\NETAPI32.dll
ModLoad: 76f20000 76f28000   C:\WINDOWS\system32\WTSAPI32.dll
ModLoad: 76060000 761b6000   C:\WINDOWS\system32\SETUPAPI.dll
ModLoad: 77f40000 77fb6000   C:\WINDOWS\system32\SHLWAPI.dll
ModLoad: 76d70000 76d92000   C:\WINDOWS\system32\apphelp.dll
ModLoad: 76d70000 76d92000   C:\WINDOWS\system32\Apphelp.dll
(4f4.314): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00007d06 edx=7c92eb94 esi=0012fe04 edi=0012ff0c
eip=0041155c esp=0012fe04 ebp=0012ff0c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
tet!WinMain+0x3c:
0041155c c60000          mov     byte ptr [eax],0           ds:0023:00000000=??


HAVE fun:)

啊冲 2016-02-03 10:42
第二部分:接管流程
  在第一部分完成之后,已经重载了一份新的NT内核,接下来就是接管执行流程。
  自Windows XP以后从RING3进RING0统一调用sysenter汇编指令,但仍保留了INT 2E中断,在sysenter调用之后会执行内核空间中的KiFastCallEntry函数,这个函数负责所有的应用层请求,即使是INT 2E也会走这个函数,而KiFastCallEntry在经过简单的处理后会通过KeServiceDescriptorTable或者KeServiceDescriptorTableShadow来获取内核函数指针并调用,重载内核的目的就是为了在执行内核函数的时候转移到新的模块上,从而不被其他HOOK所影响。那么需要做的就是在调用内核函数的时候转移指令,但并不是所有内核函数都由ntoskrnl实现,内核中GUI函数却是由win32k.sys实现,由于目前只重载了ntoskrnl,所以只能做到转移SSDT中的函数。额外说明一下,即使NT内核有不同的版本,但所有NT内核的导出模块名称都是ntoskrnl.exe(详见输出表),所以我所说的ntoskrnl泛指NT内核,而不是具体的文件名。
  在KiFastCallEntry中调用内核函数的地方大家很容易找到资料,最先是谁提出的我记不清了,反正现在是被某出名软件所使用,下面HOOK的地方与其他软件可能会冲突,所以在测试的时候需要保证没有安装任何安全软件。
  用户态程序调用内核函数的流程简述为:用户态->ntdll.ZwApi->sysenter->内核态->KiFastCallEntry->ServiceTable->ServiceRoutine。ServiceTable分为两种,上面已经提到了,接下来要做的就是在调用SSDT中函数的时候转移到新模块上,在WRK中可以找到这样的代码:
代码:
        mov     esi, edx                ; (esi)->User arguments
        mov     ebx, [edi]+SdNumber     ; get argument table address
        xor     ecx, ecx
        mov     cl, byte ptr [ebx+eax]  ; (ecx) = argument size
        mov     edi, [edi]+SdBase       ; get service table address
        mov     ebx, [edi+eax*4]        ; (ebx)-> service routine
        sub     esp, ecx                ; allocate space for arguments
        shr     ecx, 2                  ; (ecx) = number of argument DWORDs
        mov     edi, esp                ; (edi)->location to receive 1st arg
        //省略部分代码
        call    ebx                     ; call system service
  显然,最后一句的call指令就是调用了内核函数,而需要hook的地方就是sub esp, ecx这一句,此时,edi指向ServiceTable,eax为函数索引,ebx为函数地址,而且这一句连同下面的两句指令共7字节,可以容纳一个JMP/CALL指令,绝佳的HOOK点。至于这个地址怎么定位,就要用字节码搜索了,实在没有什么好办法,但总比直接写地址或定偏移的那种硬编码好。
代码:
PVOID __declspec(naked) _GetKiFastCallEntryAddress()
{
  __asm
  {
    MOV  ECX, 0x00000176;
    RDMSR;
    RETN;
  }
}
PVOID FindHookKiFastCallEntryAddress(PVOID lpKiFastCallEntry)
{
  /*
  sub    esp, ecx
  shr    ecx, 2
  mov    edi, esp
  */
  UCHAR HookBytes[] = {0x2B, 0xE1, 0xC1, 0xE9, 0x02, 0x8B, 0xFC};

  return RtlFindMemory(lpKiFastCallEntry, 0x300, HookBytes, sizeof(HookBytes));
}
  通过MSR寄存器获取KiFastCallEntry的函数地址,有兴趣的朋友可以翻阅WRK中系统初始化部分的代码。HOOK函数这里采用了普通的MDL方式来写入只读内存,可能会有人问了,在写入地址的一瞬间别的CPU执行到了这里怎么办,我个人认为是杞人忧天,现在的CPU都有缓存机制,也就意味着内存和CPU缓存未必同步,在你写入的一瞬间即使别的CPU执行到了这里,那也是缓存中的代码,和内存中的并不一致,有兴趣的朋友可以查找CPU的TLB、TIB的资料。当然这只是降低了写入与执行冲突的几率,并不代表完全没有可能蓝屏,但话说回来,你要修改一个可执行指令,不可避免的有几率冲突,这是完全无法避免的,所以尽可能是降低几率就可以了。
代码:
VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    pop  edx;  //save ret address
    sub  esp, ecx;
    push edx;  //restore ret address
    mov  edx, dword ptr [edi + eax * 0x04];

    shr  ecx, 2;
    mov  edi, esp;
    add  edi, 0x04;
    retn;
  }
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT lpDriverObject, IN PUNICODE_STRING lpRegPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(lpDriverObject);
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));
  return STATUS_SUCCESS;
}
  编译并测试,通过ARK查看效果。注意!_MyKiFastCallEntryFrame是在Windows 7中测试的,XP下并不通用,因为执行上下文并不一样,XP中是mov ebx, dword ptr [edi + eax * 0x04]而Windows 7是mov edx, dword ptr [edi + eax * 0x04],这里我就不做兼容性写法了,手里也没有XP的虚拟机。XP已经停止服务了,相信没有多少人打算再使用了,如果非要兼容XP,自己做下简单的修改即可,即把edx换成ebx。
名称:  001.jpg
查看次数: 1
文件大小:  38.8 KB
  测试一段时间后并没有蓝屏现象,说明HOOK成功,但是目前并没有写任何过滤内容,下面就来丰富一下过滤函数,即所有的SSDT都走新内核。并继续考虑兼容XP。
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    if (ServiceIndex == 190)
    {
      DbgPrint("Call NtOpenProcess\n");
    }
    return (ULONG_PTR)lppServiceTableBase[ServiceIndex] - (ULONG_PTR)g_lpNtoskrnlAddress + (ULONG_PTR)g_lpNewNtoskrnlAddress;
  }
  return lppServiceTableBase[ServiceIndex];
}

VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    pop  ebx;  //save ret address
    sub  esp, ecx;
    push ebx;  //restore ret address

    push eax;
    push ecx;
    push edx;

    push eax;
    push edi;
    call ServiceCallFilter;
    mov  ebx, eax;

    pop  edx;
    pop  ecx;
    pop  eax;
    mov  edx, ebx;

    shr  ecx, 2;
    mov  edi, esp;
    add  edi, 0x04;
    retn;
  }
}
  由于仅测试效果,我就暂时写190,这是NtOpenProcess的函数索引。
名称:  002.jpg
查看次数: 0
文件大小:  43.1 KB
至此所有的SSDT中的函数已全部转移到新模块中。但这还只是一个半成品,仅做测试使用,因为后面还需要改动很多。

啊冲 2016-02-03 10:42
小结与修正
  目前已经实现了一份简单的重载内核代码,但是如果你也跟着我实现了此部分,会发现此代码根本不能使用,甚至不能拿到本机来测试,是的,新的内核还是有大量的问题,驱动加载后会导致一些程序打不开,但是并不蓝屏,有意思的现象。
  写上一篇文章的时候没有测试的那么完善,但是我拿出我之前写的重载内核代码,并没有上述问题,仔细分析代码并回忆,问题还是出在了重定位以及需要额外的处理。那么关于修复重定位的部分就需要重新写了,把修复过程分为两部分,第一次全部重定位到新模块上,第二次有选择的重定位到原模块上,为什么需要那么麻烦?这就涉及到了原始地址的获取方式问题,原始地址都存储在一个叫KiServiceTable的变量中,详见WRK。为什么要获取原始地址?因为当驱动加载的时候你无法确定当前的SSDT表是否被HOOK,所以在第一次修复重定位之后去找KiServiceTable,然后再进行第二次重定位的修复(我无法保证第二次修复不会破坏KiServiceTable中的地址)。
  重新写一份代码,当然大部分还是从原来的代码复制过来,这样有利于逻辑上的思考,先不进行HOOK,把重载部分先理顺清楚,重定位修复代码修改为这个样子:
代码:
PVOID ReloadNtModule(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList)
{
  PVOID lpImageAddress = NULL;
  PKLDR_DATA_TABLE_ENTRY NtLdr = (PKLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
  PVOID lpFileBuffer;

  DbgPrint("Nt Module File is %wZ\n", &NtLdr->FullDllName);
  if (lpFileBuffer = KeGetFileBuffer(&NtLdr->FullDllName))
  {
    PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)lpFileBuffer;
    PIMAGE_NT_HEADERS lpNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);

    if (lpImageAddress = ExAllocatePool(NonPagedPool, lpNtHeader->OptionalHeader.SizeOfImage))
    {
      PUCHAR lpImageBytes = (PUCHAR)lpImageAddress;
      IMAGE_SECTION_HEADER *lpSection = IMAGE_FIRST_SECTION(lpNtHeader);
      ULONG i;

      RtlZeroMemory(lpImageAddress, lpNtHeader->OptionalHeader.SizeOfImage);
      RtlCopyMemory(lpImageBytes, lpFileBuffer, lpNtHeader->OptionalHeader.SizeOfHeaders);
      for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
      {
        RtlCopyMemory(lpImageBytes + lpSection.VirtualAddress, (PCHAR)lpFileBuffer + lpSection.PointerToRawData, lpSection.SizeOfRawData);
      }
      if (KeFixIAT(PsLoadedModuleList, lpImageAddress))
      {
        KeFixReloc1(lpImageAddress, NtLdr->DllBase);
      }
      else
      {
        ExFreePool(lpImageAddress);
        lpImageAddress = NULL;
      }
    }
    ExFreePool(lpFileBuffer);
  }
  if (lpImageAddress) DbgPrint("ImageAddress:0x%p\n", lpImageAddress);
  return lpImageAddress;
}
VOID KeFixReloc1(PVOID ImageBaseAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)ImageBaseAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)ImageBaseAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
  ULONG_PTR DifferOffset = (ULONG_PTR)ImageBaseAddress - lpNtHeader->OptionalHeader.ImageBase;

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems; i++)
    {
      switch (lpItem >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          ULONG_PTR *lpFixAddress = (ULONG_PTR *)((PCHAR)ImageBaseAddress + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF));

          *lpFixAddress += DifferOffset;
        }
        break;
      case IMAGE_REL_BASED_ABSOLUTE://do nothing
        break;
      default:
        DbgPrint("KeFixReloc1:Found unknown type(%X).\n", (lpItem >> 12));
        break;
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  lpNtHeader->OptionalHeader.ImageBase = (ULONG)ImageBaseAddress;
  return;
}
VOID KeFixReloc2(PVOID New, PVOID Old)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)New;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)New + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems; i++)
    {
      switch (lpItem >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          PVOID lpFixAddress = (PCHAR)New + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF);

          KeFixRelocEx(New, Old, lpFixAddress);
        }
        break;
      case IMAGE_REL_BASED_ABSOLUTE://do nothing
        break;
      default:
        break;
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return;
}
  重点还是KeFixRelocEx,重定位修正的是“地址的值”,希望大家能看懂,那么我就在这里做了相对比较多的判断。
  “地址”可执行,“值”可执行。
  “地址”可执行,“值”不可执行。指向原模块。如访问全局变量。
  “地址”不可执行,“值”可执行。不修改。如函数表。
  “地址”不可执行,“值”不可执行。想不出来是什么,指向原模块吧。
对于第一点,我原本是指向新模块,但测试了好半天,最后还是没有解决重载后某些程序打不开的问题。我曾尝试解析汇编,但字节组合方式太多了,我无法做出最正确的判断,既然我找不到是哪一类重定位存在问题,那么我就做没有问题的地方,如IAT,这种处理很像权限访问中的解决方式(1.我能干什么。2.我不能干什么)。最后代码变成这个样子:
代码:
BOOLEAN KeIsExecutable(PVOID ImageBase, PVOID Address)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)ImageBase;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_SECTION_HEADER *lpSecHdr = IMAGE_FIRST_SECTION(lpNtHeader);
  ULONG_PTR Rva = (ULONG_PTR)Address - (ULONG_PTR)ImageBase;
  USHORT i;

  for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
  {
    if (Rva >= lpSecHdr.VirtualAddress && Rva < lpSecHdr.VirtualAddress + lpSecHdr.SizeOfRawData)
    {
      return ((lpSecHdr.Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0);
    }
  }
  return FALSE;
}

VOID KeFixRelocEx(PVOID New, PVOID Old, PVOID FixAddress)
{
  if (KeIsExecutable(New, FixAddress))
  {
    if (KeIsExecutable(New, *(PVOID*)FixAddress))
    {
      if (KeIsIAT(New, *(PVOID*)FixAddress))
      {
        NOTHING;
      }
      else
      {
        *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
      }
    }
    else
    {
      *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
    }
  }
  else
  {
    if (KeIsExecutable(New, *(PVOID*)FixAddress))
    {
      NOTHING;
    }
    else
    {
      *(ULONG_PTR*)FixAddress = *(ULONG_PTR*)FixAddress - (ULONG_PTR)New + (ULONG_PTR)Old;
    }
  }
  return;
}
  啰嗦了那么多,非常不完美的解决了某些程序打不开的问题,如果有兴趣的朋友可以继续深入。但回头想想重载仅仅是为了防止HOOK而带来的麻烦,不妨假设HOOK没有那么猥琐,如果你发现重载内核不能绕过某些HOOK,再对症下药吧。
第三部分:获取原始地址
  重头戏来啦!一开始说过原始SSDT函数都存储在一个叫做KiServiceTable的变量里,这个变量同样没有导出,利用字节码搜索的方式效率很低且未必找的准确,所以另求出路。网上已经有大牛研究出了一种方式,那就是利用重定位来查找。
  首先来看WRK是如何做的:(KiInitSystem)
代码:
   KeServiceDescriptorTable[0].Base = &KiServiceTable[0];
    KeServiceDescriptorTable[0].Count = NULL;
    KeServiceDescriptorTable[0].Limit = KiServiceLimit;
    KeServiceDescriptorTable[0].Number = KiArgumentTable;
    for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {
        KeServiceDescriptorTable[Index].Limit = 0;
    }

    //
    // Copy the system service descriptor table to the shadow table
    // which is used to record the Win32 system services.
    //

    RtlCopyMemory(KeServiceDescriptorTableShadow,
                  KeServiceDescriptorTable,
                  sizeof(KeServiceDescriptorTable));
  再来对比Windows 7 x86中是如何做的,IDA分析如下:
点击图片以查看大图

图片名称:        01.jpg
查看次数:        2
文件大小:        23.6 KB
文件 ID :        89288
  Ntoskrnl.exe的期望基址是00400000,所以第一句的RVA就是00395C12,来看一下重定位信息:
名称:  02.jpg
查看次数: 0
文件大小:  34.7 KB
  可以发现一个特点,有两个连续的重定位00395C14与00396C18,而第一个RVA就是KeServiceDescriptorTable,第二个就是KiServiceTable了,结合IDA对附近代码的综合判断,最后的代码就是这样:
代码:
PVOID FindKiServiceTable(PVOID lpNtoskrnlAddress)
{
  PVOID lpKeServiceDescriptorTable = KeGetProcAddress(lpNtoskrnlAddress, "KeServiceDescriptorTable");
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpNtoskrnlAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG j;

    for (j = 0; j < NumberOfItems - 1; j++)
    {
      if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW && (lpItem[j + 1] >> 12) == IMAGE_REL_BASED_HIGHLOW)
      {
        ULONG *lpFixAddress1 = (ULONG*)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));
        ULONG *lpFixAddress2 = (ULONG*)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j + 1] & 0x0FFF));
        //两个连续的重定位
        if ((ULONG)lpFixAddress2 - (ULONG)lpFixAddress1 == sizeof(ULONG))
        {
          //MOV DWORD PTR DS:[KeServiceDescriptorTable], XXX
          if (*(USHORT*)((PCHAR)lpFixAddress1 - sizeof(USHORT)) == 0x05C7)
          {
            //DbgPrint("lpFixAddress1:%08X\n", (ULONG)lpFixAddress1 - 2);
            if (*lpFixAddress1 == (ULONG)lpKeServiceDescriptorTable)
            {
              return (PVOID)*lpFixAddress2;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}
PVOID BuildKeServiceTable(PVOID lpKernelAddress, PVOID lpOrgKernelAddress)
{
  PVOID lpKiServiceTable = NULL;
  PVOID lpKeServiceTable = NULL;

  if (lpKiServiceTable = FindKiServiceTable(lpKernelAddress))
  {
    lpKeServiceTable = ExAllocatePool(NonPagedPool, KeServiceDescriptorTable->NumberOfService * sizeof(PVOID));
    
    if (lpKeServiceTable)
    {
      RtlCopyMemory(lpKeServiceTable, lpKiServiceTable, KeServiceDescriptorTable->NumberOfService * sizeof(PVOID));
      DbgPrint("BuildSSDT:0x%p\n", lpKeServiceTable);
    }    
  }
  return lpKeServiceTable;
}
  而最后的DriverEntry与ServiceCallFilter则是:
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    return g_KeServiceTable[ServiceIndex];
  }
  return lppServiceTableBase[ServiceIndex];
}

VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
  __asm
  {
    push ecx;

    push eax;
    push edi;
    call ServiceCallFilter;
    mov  edx, eax;

    pop  ecx;

    pop  eax;
    sub  esp, ecx;
    shr  ecx, 2;
    mov  edi, esp;
    jmp  eax;
  }
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(DriverObject);
  g_lpNtoskrnlAddress = KeGetModuleHandle(PsLoadedModuleList, "ntoskrnl.exe");
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  g_KeServiceTable = (PVOID*)BuildKeServiceTable(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeFixReloc2(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));

  DriverObject->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}
  来来来测试一下,就用HideToolz来看效果。
名称:  03.jpg
查看次数: 0
文件大小:  57.4 KB
名称:  04.jpg
查看次数: 0
文件大小:  57.2 KB
  加载驱动咯。
名称:  05.jpg
查看次数: 0
文件大小:  88.2 KB
  好了,无视SSDT HOOK,同样无视INLINE HOOK,终于算是完成了一点像样的东西了,但这并不是全部,也不是此专题的完结,后面还有哦,我想后面应该是告诉大家如何再把SHADOW SSDT搞定。
  最后,此代码仍然不能拿到本机测试,如果你想蓝的话。对于新模块最少还有两个问题没有搞定(别喷我),后续我同样会告诉大家。嗯,先这样吧。

啊冲 2016-02-03 10:43

动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器

    现在的软件保护越来越厉害,从早先的加压缩壳、加密,发展到加密壳,虚拟机保护,以及扭曲变换等一系列的混淆手段,使得逆向一个软件的难度越来越大。究其根本原因,是人类天生不适合处理复杂的程序,在看过《代码大全2》之后,我发现作者一直在强调好的程序架构实际是在努力降低程序在管理上的复杂度,一个程序在运行时,至少在三个方面发生着变化,一个是空间:寄存器,内存的值在不断的变化;一个是时间,程序的运行顺序随时间推移而变化;第三个是语义上的变化,到底一段程序干了什么?它的目的是什么?它的实现机制是什么?看一行行的反汇编出来的代码都是很难搞懂的,更何况还有加壳加密这样的混淆手段在作怪,一段程序的二进制代码,再加上哪怕最简单的花指令混淆,都大大增加了复杂度,使得人脑理解它的原理变成一个艰巨的任务。
    
    混淆手段之所以成了逆向道路上的拦路虎,是在于人脑无法把许许多多条枯燥乏味的指令变成一个完整的概念,哪怕就是把两个数相加这样简单的事,编译成二进制代码,再加上花指令混淆,都很难理解它的本意,这就是加壳软件厉害的地方,许多恶意软件借助壳的帮助,逃脱了反病毒人员的逆向分析,在用户的电脑中为非作歹。(下面我把“壳”和混淆代码不加区分的使用了,少打几个字,读起来也简单。)
还有很多软件是用Java,VB这样以虚拟机为基础写的,要想在没有源代码的情况下移植到别的机器上,也是很难的事。而实际上如果能把这些二进制的程序先翻译成一种通用的中间语言,再把这种中间语言翻译成高级语言,就可以很好的利用它们来生产出更多的软件。

    为了达到更有效率的逆向以上这些软件的目的,首先要讨论的是能不能开发出这样一种程序,它能把程序中被混淆的东西暴露出来,去掉那些垃圾代码,把程序原始的面貌展现出来?
    
    我认为是可以的,因为一个很简单的事实:一个加了壳的软件,不论它的代码如何被混淆,它原有的功能不能变化,既不能多做,也不能少做,更不能做错。换句话说,从时间和空间上来看,原来这个程序的执行过程是一根线,加上壳,就多了壳这条线,两条线就像两根有塑料外皮的导线一样,再怎么交织在一起,两根电线中的电流是不会流到一起的,当线与线交织运行一定阶段后,一定要分开来各管各走,这样程序原来的功能才不会被破坏,无论是虚拟机还是扭曲变换,这条基本的定律是不会变的。
    
    然后我从上面这点推导出第二个观点是,壳这条线既然不影响程序的功能,那就是多余的东西,而多余的东西是可以被拿掉的。就是说程序在执行过程中,是可以不去走壳的这条线,也能行得通,而且走起来还快一点,事实上在我看了《编译原理》后发现编译器的代码优化就是起到去除冗余代码的作用的,随便多复杂的混淆代码,只要反汇编出来的汇编语句没有错误,把它放到编译器中优化一下,估计这些冗余代码都活不下去了。而逆向分析人员甚至都没必要去看。这样就大大节省了逆向的工作量。

    上面的这段话中有一个非常重要的条件,就是交给编译器去优化的代码必须是反汇编的时候没有错误的,而很多混淆代码使用了花指令这样的手段,使得静态反汇编出来的东西都是一团乱码,静态反编译因为不能再现程序中真实的变量值的变化,遇上跳转,CALL指令,搞不清是真是假,因为参数是未知的,比如"jmp eax"什么的,这样就大大影响了反汇编的质量,现今为止,没有什么方法能百分之百的静态反汇编成功。

    那么就让我们试试像OD这样的动态调试手段吧,动态调试可以真实地观察程序的运行情况,只要某个程序的片断跟踪过一次,基本上就得到了正确的反汇编代码,但是有三个问题,一是每个程序都有许多的分支,就像一棵树有许多的树梢,而调试器没办法一次走完所有的地方,所以反汇编出来的代码不完整;关于这个问题,可以把程序看成是一棵二叉树之类的东西,反正用个遍历算法,强制遍历一遍所有可用的分支,整个程序的反汇编就出来了吧。

    二是这些代码中有很多的循环,一个循环如果执行了1000遍,OD直接保存代码的话,就把这个循环体重复了1000遍,反汇编出来的代码又很多余。关于这个问题就要一边跟踪调试,一边在反汇编的基础上进行分析,建立基本块,循环体,子过程的结构,初步整理好代码。准备进一步的分析。

    三是OD还有一个很大的问题,就是它跟踪程序的过程不可逆的,我希望在调试程序时,程序运行到哪里,发现了问题,就反过来倒推,这要求保存程序每一步的状态,随时可以退回到前面任意的一个点上。这个问题也许可以试试建立一个数据库来保存相关信息,比如一个寄存器开始是什么值,后来这个值起了什么变化,放到了什么地方,又从什么地方取得了一个什么值,往往这些值是固定的,其实在程序中的各种变量值往往是固定的,从系统初始化开始,一步一步地搬动,加减运算什么的,一路上再怎么变,其实都是固定的数值关系,有些时候看上去好像每次运行都不一样的值,其实处理的方式是一样的,数据流是一样的,控制流也是一样的,要不然这个程序就有毛病了,好端端地运行它两次,结果第一次点“文件”菜单,出来的是“文件”菜单,第二次点变成“帮助”了,每次加减运算的结果都不一样,这样的程序没法用了,不等别人来逆向,自己先被用户抛弃了。所以,即使是系统调用,或者是用户输入,只要我们的调试器模拟输入的参数对路,那么一路上的流程是固定的,变量值的变化也是很容易计算的,关键是要能有个数据库来保存,来随时调用进行推算。

    综上所述,我们必须用动态跟踪的方法,获得跳转的间接路径,还要即时的反汇编出来,还要保存程序的运行状态,还要调用编译器优化来清除混淆代码。这里最好试试虚拟机的技术,在虚拟机中让程序运行,可以把程序和系统隔离开来,防止某些软件发现被调试后恶意地破坏我们的系统。

    这个“动态调试+静态反汇编优化”以去除混淆代码的设想背后是一个巨大的系统工程,想法本身不新鲜,网络上,论坛里,相关的资料和软件都有不少,比如虚拟机脱壳,比如可逆的调试器,但关键是它们没有拧成一股绳,没有在一个高层次的视野下统一起来。当然我只是个业余的编程爱好者,一个人做这样的事肯定是力所不能及的,但既然爱因斯坦那么看重人的想象力,所以我就先幻想一下,然后再一块砖一块砖的去搭建,这里先抛第一块砖吧。

啊冲 2016-02-03 10:43

反反调试思想方法探索

如今,软件安全已经成为了开发软件项目的必备组成部分,反调试则是其中关键的一环,然而,正如矛与盾的对立一样,反反调试与反调试必将永久的并立共存。为了防止软件被调试,现今的软件大多都利用了驱动来检测制止,对于关键的系统函数进行hook(包括各种SSDT hook、inline hook、iat hook等)能有效地遏制进程被打开和读写等,然而,hook是很容易定位和被恢复的,基于没有任何验校检测的hook保护技术就像一面纸墙一般不堪一击。因此,验校检查成为越来越多的反调试代码中不可或缺的一个部分,特别是对于商业性的网络游戏客户端,一旦反调试代码检测到自身的hook地址被修改或者自身的代码验校不一致时,便立刻选择结束游戏进程,甚至蓝屏或重启,以此强硬的对待那些有调试企图的人。
检测代码往往无处不在,你很难全部的定位和找到它们,而且它们相互交织检测和代码验校。另一方面,为了对抗硬件断点,在检测调试之前,往往对DR调试寄存器做了相关清除和手脚,并在之后予以恢复,检测代码往往加了VM保护,使人很难弄清程序的流程。
    我们知道,在线程切换时会根据是否是同一进程而决定知否切换cr3寄存器,即使切换了cr3,所有进程的内核空间视图是一致的(除了某些特殊页),因此当某一进程通过驱动hook内核函数后,系统所有进程都将改变执行路径,同样当我们恢复了hook之后亦是如此。要是有什么办法能打破这样的规则就
好了,当我们的调试器进程运行时执行原始的函数路径,当hook进程运行时执行它自己的hook之后的路径。我们知道,hook技术通常只是修改内核函数的开头几个字节jmp到自己的函数,或者内联修改函数的内部call地址等,不论通过什么形式的hook,一般就是修改一个dword或者几个字节,在已知hook地址和原始字节内容的情况下,恢复hook只需一个mov指令即可,虽然进程切换时并不影响内核地址空间,但是我们也可以在切换时临时修改一些字节。我们的反反调试思想是:在系统从反调试进程切换到其他进程时,恢复原始的hook地址内容,在要切换到反调试进程时,再修改为hook地址。
windows的线程切换散布在内核的各个点上,而且调用形式各不相同,主要函数包括KiSwapThread、KiSwapContext、SwapContext。在线程抢占的情景中,KiDispatchInterrupt直接调用SwapContext完成线程切换;在线程时限用完时,KiQuantumEnd调用KiSwapContext进行切换(KiSwapContext再调用SwapContext完成真正的切换);在线程自愿放弃执行时,则调用KiSwapThread,该函数又调用KiSwapContext完成执行权的转移。在此,我们看到实际完成切换的是核心汇编函数SwapContext。SwapContext也是我们需要处理的函数,在系统线程切换时,我们判断2个线程的进程之一是否含有反调试进程,有的话则进行相关动作,具体是:如果老线程是反调试进程则恢复还原原始hook地址处的内容,如果新线程是反调试进程则还原它自己的原来的hook地址。这里有一个问题,我们是直接在SwapContext函数的开头跳到我们的函数进行以上的判断和恢复吗?我们知道线程切换是系统最频繁调用的函数了,SwapContext本身就是用汇编来写的(为了保证性能),我们的处理是否得当也将直接影响到系统的整体速度,刚才提到的在函数开头进行判断显然不够优雅~在线程切换时,SwapContext会根据是否是同一进程而决定切换cr3寄存器的内容,看一下相关代码:
(代码截自XP sp3)
lkd> x nt!*SwapContext
80546a90 nt!SwapContext =
8054696c nt!KiSwapContext =
805fcd34 nt!VdmSwapContexts =

lkd>uf nt!SwapContext
.
.
.
nt!SwapContext+0x8c:
80546b1c 8b4b40          mov     ecx,dword ptr [ebx+40h]
80546b1f 894104          mov     dword ptr [ecx+4],eax
80546b22 8b6628          mov     esp,dword ptr [esi+28h]
80546b25 8b4620          mov     eax,dword ptr [esi+20h]
80546b28 894318          mov     dword ptr [ebx+18h],eax
80546b2b fb              sti
80546b2c 8b4744          mov     eax,dword ptr [edi+44h]
80546b2f 3b4644          cmp     eax,dword ptr [esi+44h]         比较是否是同一进程
80546b32 c6475000        mov     byte ptr [edi+50h],0
80546b36 7440            je      nt!SwapContext+0xe8 (80546b78)  是同一进程无需切换,直接跳过

nt!SwapContext+0xa8:
80546b38 8b7e44          mov     edi,dword ptr [esi+44h]         取EPROCESS
80546b3b 8b4b48          mov     ecx,dword ptr [ebx+48h]     
80546b3e 314834          xor     dword ptr [eax+34h],ecx
80546b41 314f34          xor     dword ptr [edi+34h],ecx
80546b44 66f74720ffff    test    word ptr [edi+20h],0FFFFh
80546b4a 7571            jne     nt!SwapContext+0x12d (80546bbd) 

nt!SwapContext+0xbc:
80546b4c 33c0            xor     eax,eax

nt!SwapContext+0xbe:
80546b4e 0f00d0          lldt    ax
80546b51 8d8b40050000    lea     ecx,[ebx+540h]
80546b57 e850afffff      call    nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
80546b5c 33c0            xor     eax,eax
80546b5e 8ee8            mov     gs,ax
80546b60 8b4718          mov     eax,dword ptr [edi+18h]         取cr3也即EPROCESS->DirectoryTableBase

80546b63 8b6b40          mov     ebp,dword ptr [ebx+40h]
80546b66 8b4f30          mov     ecx,dword ptr [edi+30h]
80546b69 89451c          mov     dword ptr [ebp+1Ch],eax
80546b6c 0f22d8          mov     cr3,eax                         完成切换
80546b6f 66894d66        mov     word ptr [ebp+66h],cx
80546b73 eb0e            jmp     nt!SwapContext+0xf3 (80546b83)
.
.
.

    为了不影响性能,我们所要做的只是在不同进程切换时做判断,若是同一进程则无需做任何处理,SwapContext函数内部本身就会做相应的判断,我们为什么不直接利用呢?地址80546b36处的je跳转是同一进程的分支,否则接下来的语句便是不同进程,我们修改80546b38处为跳到我们的函数里并进行判断:
(edi老线程,esi新线程)

cmp  dword ptr [edi+44h] , 反调试进程_EPROCESS                 
jmp  _恢复hook分支
cmp  dword ptr [esi+44h] , 反调试进程_EPROCESS  
jmp  _hook分支
mov     edi,dword ptr [esi+44h]      SwapContext函数内部地址80546b38的原指令   
jmp     80546b3b 


_恢复hook分支:
cr0去保护位
mov   [_hook地址1], 原始内容1 
mov   [_hook地址2], 原始内容2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
jmp     80546b3b

_hook分支
cr0去保护位
mov   [_hook地址1], 反调试进程hook函数地址1
mov   [_hook地址2], 反调试进程hook函数地址2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
jmp     80546b3b   

其中hook地址和值是在驱动中定位并收集好的

    如果你觉得上面的方法还是不够优雅的话,下面我就再来介绍一种相对而言稍微优雅的方法。
我们知道,windows的内存寻址是通过三级的页目录,页表来映射的,每个进程都有独立的页表,且进程的系统空间视图是共享相同的页目录的。这一次,我们就来对反调试进程的页表做相应的手脚~我们的思想方法是:修改反调试进程的页表项,让其hook代码的页面为一私有页,这样,反调试进程与其他进程将拥有不同内核代码页,其检测机制便荡然无存了。然后,再在我们的进程里恢复hook地址(当然也可以在反调试进程创建后,加载驱动前修改,这样就不用恢复了~)。
windows内核映象是如何映射的?我们来看一下:
lkd> lm
start    end        module name
804d8000 806e5000   nt         (pdb symbols)          c:\symbols\ntkrpamp.pdb\7D6290E03E32455BB0E035E38816124F1\ntkrpamp.pdb
806e5000 80705d00   hal        (pdb symbols)          c:\symbols\halmacpi.pdb\9875FD697ECA4BBB8A475825F6BF885E1\halmacpi.pdb
a32db000 a331ba80   HTTP       (pdb symbols)          c:\symbols\http.pdb\B5A46191250E412D80E9D9E9DDA2F4DA1\http.pdb
a3610000 a3613d80   vstor2_ws60   (no symbols)           
a3614000 a3665c00   srv        (pdb symbols)          c:\symbols\srv.pdb\069184FEBE104BFDA9E51021B9B472D92\srv.pdb
a368e000 a375ce00   vmx86      (no symbols)           
a3785000 a37b1180   mrxdav     (pdb symbols)          c:\symbols\mrxdav.pdb\EDD7D9E6E63B43DBA5059A72CE89286E1\mrxdav.pdb
a3a46000 a3a5a480   wdmaud     (pdb symbols)          c:\symbols\wdmaud.pdb\D3271BFD135D4C2B9B1EEED4E26003E22\wdmaud.pdb
a3ae3000 a3af2a00   vmci       (export symbols)       \??\C:\WINDOWS\system32\Drivers\vmci.sys
a3b27000 a3b2ae80   DbgMsg     (no symbols)           
a3cb3000 a3cc8880   irda       (no symbols)   
'
'
'
lkd> !pte 804d8000
               VA 804d8000
PDE at 00000000C0602010    PTE at 00000000C04026C0
contains 00000000004009E3  contains 0000000000000000
pfn 400        -GLDA--KWEV    LARGE PAGE pfn 4d8       

    其中L是指使用大页面来映射的,这表明内核的代码和数据是在一页(4m或pae下2m的大页)中,而我们要修改的只是代码页,数据页必须映射到相同物理页以维持系统的一致性。因此,我们在反调试进程中,为内核映象对应的PDE申请相应的页表,在页表中,我们将原内核映象的数据页对应的pte设置为相同的pfn,而代码页设置为我们私有页,事实上,代码页中也无需全部私有,只需要把hook函数所在的页面改为私有pfn即可,其他页面可仍为原始pfn,从而避免不必要的内存浪费。然后我们恢复hook,结果反调试进程和其他进程会拥有不同内核函数的执行路径了,反调试保护也随之为我们突破~
仔细看看,经过上面的处理真的就可以了吗?答案当然是否定的。看看上面的 !pte 804d8000命令的结果-GLDA--KWEV,其中G表示全局页,全局页标志是为了提升系统性能,因为内核地址空间是共用的,所以cpu在冲刷内部TLB时,只是冲走了没有G标志的TLB项,当然,这并不是说全局页就永远不会消失,TLB缓存项是有限的,cpu会以FIFO规则替换所有的TLB项。可能有人感到奇怪,在SwapContext函数中并没有显示的冲刷TLB的指令,这是因为:如果是同一进程中,则无需冲刷;如果是不同进程,那么在更改cr3的同时,已经隐式的执行了冲刷命令。我们的目的是在切换到反调试进程时,冲刷掉全局页,使其使用自己的私有页。那么如何做到呢?cpu内部的cr4寄存器中位7是PGE(Page Global Enable)位,为1时启用全局页功能,为0是禁止。当全局页禁用时,冲刷TLB的话则全部TLB项都会无效。所以我们上面说的修改反调试进程的pde及pte中都不得含有G标志,我们在SwapContext非同一进程的分支做如下处理:
cmp     dword ptr [esi+44h] , 反调试进程_EPROCESS
mov     edi,dword ptr [esi+44h]                     执行80546b38原始指令
jne     80546b3b                                      不是,直接跳回

mov     eax , cr4                                     eax内容无需保存,见代码即知
push    eax                                            保存cr4内容
and     eax , ~(1 << CR4_PGE)                       去PGE位
mov     cr4 , eax    
mov     cr3 , _反调试进程cr3值                       冲刷所有TLB项 
pop     cr4                                            恢复cr4
jmp     80546b3b 

这样,在反调试进程自身上下文中任何检测都将无效,因为我们根本不会碰它的任何代码逻辑,当然,上面的代码无法突破一些在任意上下文中运行代码的检测机制,比如dpctimer,workitem,Watchdog Timers以及System Threads,然而这些机制其实很可以很容易的突破,比如枚举查找系统的dpc定时器并删除是很简单的,系统线程也很容易被停掉。

再将思维发散一下,驱动在改变一个内核函数的路径时必定先要获得该函数的地址,或者一个相对的基准函数,无论其是通过静态IAT导入函数,还是手工IAT搜索,还是动态MmGetSystemRoutineAddress,还是read内核文件,我们在之前做相关手脚,在其获取函数时提供给他一个虚假地址,当然,这是原函数的一个副本,以便他能找到内部相应的hook地址。好的,让他hook修改然后检测去吧~~

   以上只是本人的拙劣想法和见解而已,希望它对你有用~

啊冲 2016-02-03 10:44
反调试技巧总结-原理和实现

一、 前言
    前段学习反调试和vc,写了antidebug-tester,经常会收到message希望交流或索要实现代码,我都没有回复。其实代码已经在编程版提供了1个版本,另其多是vc内嵌asm写的,对cracker而言,只要反下就知道了。我想代码其实意义不是很大,重要的是理解和运用。
    做个简单的总结,说明下实现原理和实现方法。也算回复了那些给我发Message的朋友。

    部分代码和参考资料来源:
1、<<脱壳的艺术>> hawking
2、<> Angeljyt
3、http://bbs.pediy.com 
4、<<软件加密技术内幕>> 看雪学院
5、<> Peter Ferrie

我将反调试技巧按行为分为两大类,一类为检测,另一类为攻击,每类中按操作对象又分了五个小类:
1、 通用调试器     包括所有调试器的通用检测方法
2、 特定调试器     包括OD、IDA等调试器,也包括相关插件,也包括虚拟环境
3、 断点           包括内存断点、普通断点、硬件断点检测
4、 单步和跟踪     主要针对单步跟踪调试
5、 补丁           包括文件补丁和内存补丁
反调试函数前缀
              检测        攻击
通用调试器     FD_        AD_
特定调试器     FS_        AS_
断点           FB_        AB_
单步和跟踪     FT_        AT_
补丁           FP_        AP_

声明:
1、本文多数都是摘录和翻译,我只是重新组合并翻译,不会有人告侵权吧。里面多是按自己的理解来说明,可能有理解错误,或有更好的实现方法,希望大家帮忙指出错误。
2、我并没有总结完全,上面的部分分类目前还只有很少的函数甚至空白,等待大家和我一起来完善和补充。我坚信如果有扎实的基础知识,丰富的想像力,灵活的运用,就会创造出更多的属于自己的反调试。而最强的反调试,通常都是自己创造的,而不是来自别人的代码。

二、 查找-通用调试器(FD_)
函数列表如下,后面会依次说明,需事先说明的是,这些反调试手段多数已家喻户晓,目前有效的不多,多数已可以通过OD的插件顺利通过,如果你想验证它们的有效性,请关闭OD的所有反反调试插件:
复制代码
bool FD_IsDebuggerPresent();
bool FD_PEB_BeingDebuggedFlag();
bool FD_PEB_NtGlobalFlags();
bool FD_Heap_HeapFlags();
bool FD_Heap_ForceFlags();
bool FD_Heap_Tail();
bool FD_CheckRemoteDebuggerPresent();
bool FD_NtQueryInfoProc_DbgPort();
bool FD_NtQueryInfoProc_DbgObjHandle();
bool FD_NtQueryInfoProc_DbgFlags();
bool FD_NtQueryInfoProc_SysKrlDbgInfo();
bool FD_SeDebugPrivilege();
bool FD_Parent_Process();
bool FD_DebugObject_NtQueryObject();
bool FD_Find_Debugger_Window();
bool FD_Find_Debugger_Process();
bool FD_Find_Device_Driver();
bool FD_Exception_Closehandle();
bool FD_Exception_Int3();
bool FD_Exception_Popf();
bool FD_OutputDebugString();
bool FD_TEB_check_in_Vista();
bool FD_check_StartupInfo();
bool FD_Parent_Process1();
bool FD_Exception_Instruction_count();
bool FD_INT_2d();
复制代码


2.1 FD_IsDebuggerPresent()
对调试器来说,IsDebuggerPresent是臭名昭著的恶意函数。不多说了,它是个检测调试的api函数。实现更简单,只要调用IsDebuggerPresent就可以了。在调用它之前,可以加如下代码,以用来检测是否在函数头有普通断点,或是否被钩挂。
  //check softbreak
  if(*(BYTE*)Func_addr==0xcc)
    return true;
  //check hook
  if(*(BYTE*)Func_addr!=0x64)
    return true;


2.2 FD_PEB_BeingDebuggedFlag
我们知道,如果程序处于调试器中,那么在PEB结构中有个beingDegug标志会被设置,直接读取它就可判断是否在调试器中。实际上IsDebuggerPresent就是这么干的。
复制代码
  __asm
  {
    mov eax, fs:[30h] ;EAX =  TEB.ProcessEnvironmentBlock
    inc eax
    inc eax
    mov eax, [eax]
    and eax,0x000000ff  ;AL  =  PEB.BeingDebugged
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.3 FD_PEB_NtGlobalFlags
PEB中还有其它FLAG表明了调试器的存在,如NtGlobalFlags。它位于PEB环境中偏移为0x68的位置,默认情况下该值为0,在win2k和其后的windows平台下,如果在调试中,它会被设置为一个特定的值。使用该标志来判断是否被调试并不可靠(如在winnt中),但这种方法却也很常用。这个标志由下面几个标志组成:
***_HEAP_ENABLE_TAIL_CHECK (0x10)
***_HEAP_ENABLE_FREE_CHECK (0x20)
***_HEAP_VALIDATE_PARAMETERS (0x40)
检测NtGlobalFlags的方法如下,这个方法在ExeCryptor中使用过。
复制代码
__asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+68h]
    and eax, 0x70
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.4 FD_Heap_HeapFlags()
同样,调试器也会在堆中留下痕迹,你可以使用kernel32_GetProcessHeap()函数,如果你不希望使用api函数(以免暴露),则可以直接在PEB中寻找。同样的,使用HeapFlags和后面提到的ForceFlags来检测调试器也不是非常可靠,但却很常用。
这个域由一组标志组成,正常情况下,该值应为2。
复制代码
  __asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+18h] ;PEB.ProcessHeap
    mov eax, [eax+0ch] ;PEB.ProcessHeap.Flags
    cmp eax, 2
    jne rt_label
    jmp rf_label
  }
复制代码


2.5 FD_Heap_ForceFlags
进程堆里另外一个标志,ForceFlags,它也由一组标志组成,正常情况下,该值应为0。

复制代码
  __asm
  {
    mov eax, fs:[30h]
    mov eax, [eax+18h] ;PEB.ProcessHeap
    mov eax, [eax+10h] ;PEB.ProcessHeap.ForceFlags
    test eax, eax
    jne rt_label
    jmp rf_label
  }
复制代码


2.6 FD_Heap_Tail
如果处于调试中,堆尾部也会留下痕迹。标志HEAP_TAIL_CHECKING_ENABLED 将会在分配的堆块尾部生成两个0xABABABAB。如果需要额外的字节来填充堆尾,HEAP_FREE_CHECKING_ENABLED标志则会生成0xFEEEFEEE。

据说Themida使用过这个反调试
复制代码
  __asm
  {
    mov eax, buff
    ;get unused_bytes
    movzx ecx, byte ptr [eax-2]
    movzx edx, word ptr [eax-8] ;size
    sub eax, ecx
    lea edi, [edx*8+eax]
    mov al, 0abh
    mov cl, 8
    repe sca**
    je rt_label
    jmp rf_label
  }
复制代码


2.7 FD_CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent是另一个检测调试的api,只是可惜它似乎只能在winxp sp1版本以后使用。它主要是用来查询一个在winnt时就有的一个数值,其内部会调用NtQueryInformationProcess(),我是这样实现的:
复制代码
  FARPROC Func_addr ;
  HMODULE hModule = GetModuleHandle("kernel32.dll");
  if (hModule==INVALID_HANDLE_VALUE)
    return false;
  (FARPROC&) Func_addr =GetProcAddress(hModule, "CheckRemoteDebuggerPresent");
  if (Func_addr != NULL) 
  {
    __asm 
    {
      push  eax;
      push  esp;
      push  0xffffffff;
      call  Func_addr;
      test  eax,eax;
      je    rf_label;
      pop    eax;
      test  eax,eax
      je    rf_label;
      jmp    rt_label;
    }
  }
复制代码


2.8 FD_NtQueryInfoProc_DbgPort
使用ntdll_NtQueryInformationProcess()来查询ProcessDebugPort可以用来检测反调试。如果进程被调试,其返回值应为0xffffffff。
下面的代码应该是从pediy里copy过来的,时间太长,不记得是哪位兄弟的代码了。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  PROCESS_DEBUG_PORT_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), ProcessDebugPort, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.DebugPort)
      return true;
    else
      return false;
复制代码


2.9 FD_NtQueryInfoProc_DbgObjHandle
  在winxp中引入了"debug object".当一个调试活动开始,一个"debug object"被创建,同也相应产生了一个句柄。使用为公开的ProcessDebugObjectHandle类,可以查询这个句柄的数值。
  代码可能还是从pediy里复制的,不记得了。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  _PROCESS_DEBUG_OBJECTHANDLE_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), (PROCESS_INFO_CLASS)0x0000001e, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.ObjectHandle)
      return true;
    else
      return false;
复制代码


2.10 FD_NtQueryInfoProc_DbgFlags();
同样的未公开的ProcessDebugFlags类,当调试器存在时,它会返回false。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess; 
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess"); 
    if (ZwQueryInformationProcess == NULL) 
    return false;
  _PROCESS_DEBUG_FLAGS_INFO ProcessInfo; 
  if (STATUS_SUCCESS != ZwQueryInformationProcess(GetCurrentProcess( ), (PROCESS_INFO_CLASS)0x0000001f, &ProcessInfo, sizeof(ProcessInfo), NULL)) 
    return false;
  else 
    if(ProcessInfo.Debugflags)
      return false;
    else
      return true;
复制代码


2.11 FD_NtQueryInfoProc_SysKrlDbgInfo()
这个方法估计对大家用处不大,SystemKernelDebuggerInformation类同样可以用来识别调试器,只是可惜在windows下无效,据称可以用在reactOS中。
复制代码
   HMODULE hModule = GetModuleHandle("ntdll.dll"); 
    ZW_QUERY_SYSTEM_INFORMATION ZwQuerySystemInformation; 
    ZwQuerySystemInformation = (ZW_QUERY_SYSTEM_INFORMATION)GetProcAddress(hModule, "ZwQuerySystemInformation"); 
    if (ZwQuerySystemInformation == NULL) 
        return false;
    SYSTEM_KERNEL_DEBUGGER_INFORMATION Info; 
    if (STATUS_SUCCESS == ZwQuerySystemInformation(SystemKernelDebuggerInformation, &Info, sizeof(Info), NULL)) 
    { 
        if (Info.DebuggerEnabled) 
        { 
            if (Info.DebuggerNotPresent) 
                return false;
            else 
                return true;
        } 
        else 
            return false;
    } 
    else 
       return true;
复制代码


2.12 FD_SeDebugPrivilege()
  当一个进程获得SeDebugPrivilege,它就获得了对CSRSS.EXE的完全控制,这种特权也会被子进程继承,也就是说一个被调试的程序如果获得了CSRSS.EXE的进程ID,它就可以使用openprocess操作CSRSS.EXE。获得其进程ID有很多中方法,如Process32Next,或NtQuerySystemInformation,在winxp下可以使用CsrGetProcessId。
hTmp=OpenProcess(PROCESS_ALL_ACCESS,false,PID_csrss);
    if(hTmp!=NULL)
    {
      CloseHandle(hProcessSnap );
      return true;
    }


2.13 FD_Parent_Process()
通常我们都直接在windows界面下运行应用程序,这样的结果就是它的父进程为"explorer.exe",这个反调试就是检测应用程序的父进程是否为"explorer.exe",如不是则判定为处于调试器中,这也不是百分百可靠,因为有的时候你的程序是在命令行提示符下运行的。
Yoda使用了这个反调试,它使用Process32Next检测父进程,目前很多插件已经通过使Process32Next始终返回false来越过这个反调试(比如HideOD)。不过可以对代码做些简单的修正来处理这个反反调试。

2.14 FD_DebugObject_NtQueryObject();
  如前面所描述的,当一个调试活动开始,一个"debug object"被创建,同也相应产生了一个句柄。我们可以查询这个调试对象列表,并检查调试对象的数量,以实现调试器的检测。
复制代码
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  PNtQueryObject NtQueryObject;
  NtQueryObject = (PNtQueryObject)GetProcAddress(hModule,"NtQueryObject");

  if(NtQueryObject==NULL)
    return false;
  unsigned char szdbgobj[25]=
  "\x44\x00\x65\x00\x62\x00\x75\x00\x67\x00\x4f\x00\x62\x00\x6a\x00\x65\x00\x63\x00\x74\x00\x00\x00";
  unsigned char *psz=&szdbgobj[0];
  __asm
  {
    xor    ebx,ebx;
    push  ebx;
    push  esp;
    push  ebx;
    push  ebx;
    push  3;
    push  ebx;
    Call  dword ptr [NtQueryObject];
    pop  edi;
    push  4;
    push  1000h;
    push  edi;
    push  ebx;
      call  dword ptr [VirtualAlloc];
    push  ebx;
    push  edi;
    push  eax;
    push  3;
    push  ebx;
    xchg  esi,eax;
    Call  dword ptr [NtQueryObject];
    lodsd;
    xchg  ecx,eax;
lable1:  lodsd;
    movzx  edx,ax;
    lodsd;
    xchg  esi,eax;
    cmp    edx,16h;
    jne    label2;
    xchg  ecx,edx;
    mov    edi,psz;
    repe  cmp**;
    xchg  ecx,edx;
    jne    label2;
    cmp    dword ptr [eax],edx
    jne    rt_label;
lable2:  add    esi,edx
    and    esi,-4;
    lodsd
    loop  label1;
  }
  return false;
rt_label:
  return true;
复制代码


2.15 FD_Find_Debugger_Window();
通过列举运行的应用程序的窗口,并于常用调试相关工具比对的方法,应该很常用了,就不多说了。这个也是个可以自行增加项目的函数,你可以将一些常用的调试工具归入其中,比如OD,IDA,WindBG,SoftICE等,你也可以添加任何你需要的,比如"Import REConstructor v1.6 FINAL (C) 2001-2003 MackT/uCF","Registry Monitor - Sysinternals: www.sysinternals.com"等等。
复制代码
  //ollyice
    hWnd=CWnd::FindWindow(_T("1212121"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg v1.1
    hWnd=CWnd::FindWindow(_T("icu_dbg"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollyice pe--diy
    hWnd=CWnd::FindWindow(_T("pe--diy"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg ?-°?
    hWnd=CWnd::FindWindow(_T("ollydbg"),NULL);
    if (hWnd!=NULL)
    return true;
  //ollydbg ?-°?
    hWnd=CWnd::FindWindow(_T("odbydyk"),NULL);
    if (hWnd!=NULL)
    return true;
  //windbg
    hWnd=CWnd::FindWindow(_T("WinDbgFrameClass"),NULL);
    if (hWnd!=NULL)
    return true;
  //dede3.50
    hWnd=CWnd::FindWindow(_T("TDeDeMainForm"),NULL);
    if (hWnd!=NULL)
    return true;
  //IDA5.20
    hWnd=CWnd::FindWindow(_T("TIdaWindow"),NULL);
    if (hWnd!=NULL)
    return true;
  //others
    hWnd=CWnd::FindWindow(_T("TESTDBG"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("kk1"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("Eew75"),NULL);
    if (hWnd!=NULL)
    return true;
    hWnd=CWnd::FindWindow(_T("Shadow"),NULL);
    if (hWnd!=NULL)
    return true;
  //PEiD v0.94
    hWnd=CWnd::FindWindow(NULL,"PEiD v0.94");
    if (hWnd!=NULL)
    return true;
  //RegMON
    hWnd=CWnd::FindWindow(NULL,"Registry Monitor - Sysinternals: www.sysinternals.com");
    if (hWnd!=NULL)
    return true;
  //File Monitor
    hWnd=CWnd::FindWindow(NULL,"File Monitor - Sysinternals: www.sysinternals.com");
    if (hWnd!=NULL)
    return true;
  //Import Rec v1.6
    hWnd=CWnd::FindWindow(NULL,"Import REConstructor v1.6 FINAL (C) 2001-2003 MackT/uCF");
    if (hWnd!=NULL)
    return true;
  return false;
复制代码


2.16 FD_Find_Debugger_Process();
  与上面的方法类似,区别是这个反调试用通过查询进程名字与已知的常用调试器应用程序名字进行比对,以确定是否有调试器处于运行状态。
复制代码
    if(strcmp(pe32.szExeFile,"OLLYICE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"IDAG.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"OLLYDBG.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"PEID.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"SOFTICE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"LORDPE.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"IMPORTREC.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"W32DSM89.EXE")==0)
        return true;
    if(strcmp(pe32.szExeFile,"WINDBG.EXE")==0)
        return true;
复制代码




2.17 FD_Find_Device_Driver()
  调试工具通常会使用内核驱动,因此如果尝试是否可以打开一些调试器所用到的设备,就可判断是否存在调试器。常用的设备名称如下:
复制代码
\\.\SICE  (SoftICE)
\\.\SIWVID(SoftICE)    
\\.\NTICE  (SoftICE)    
\\.\REGVXG(RegMON)
\\.\REGVXD(RegMON)
\\.\REGSYS(RegMON)
\\.\REGSYS(RegMON)
\\.\FILEVXG(FileMON)
\\.\FILEM(FileMON)
\\.\TRW(TRW2000)
复制代码


2.18 FD_Exception_Closehandle()
  如果给CloseHandle()函数一个无效句柄作为输入参数,在无调试器时,将会返回一个错误代码,而有调试器存在时,将会触发一个EXCEPTION_INVALID_HANDLE (0xc0000008)的异常。
复制代码
  __try  
  {
    CloseHandle(HANDLE(0x00001234));
    return false;
  }
  __except(1)
  {
    return true;
  }
复制代码



2.19 FD_Exception_Int3()
  通过Int3产生异常中断的反调试比较经典。当INT3 被执行到时, 如果程序未被调试, 将会异常处理器程序继续执行。而INT3指令常被调试器用于设置软件断点,int 3会导致调试器误认为这是一个自己的断点,从而不会进入异常处理程序。

复制代码
  __asm 
  {
    push   offset exception_handler; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    test   eax,eax; check the flag 
    je    rt_label
    jmp    rf_label

exception_handler:
    mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
    mov    dword ptr [eax+0xb0],0xffffffff;set flag (ContextRecord.EAX)
    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    xor   eax,eax
    retn

rt_label:
    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
rf_label:
    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


2.20 FD_Exception_Popf()
我们都知道标志寄存器中的陷阱标志,当该标志被设置时,将产生一个单步异常。在程序中动态设置这给标志,如果处于调试器中,该异常将会被调试器捕获。
可通过下面的代码设置标志寄存器。
    pushf 
    mov dword ptr [esp], 0x100
    popf


2.21 FD_OutputDebugString()
  在有调试器存在和没有调试器存在时,OutputDebugString函数表现会有所不同。最明显的不同是, 如果有调试器存在,其后的GetLastError()的返回值为零。

  OutputDebugString("");
  tmpD=GetLastError();
  if(tmpD==0)
    return true;
  return false;


2.22 FD_TEB_check_in_Vista();
  这是从windows anti-debug reference里拷贝出来的,据说是适用于vista系统下检测调试器。我没有vista所以也没有测试。有条件的可以试下,有问题帮忙反馈给我。多谢。
复制代码
    //vista
    __asm
    {
      push   offset exception_handler; set exception handler
      push  dword ptr fs:[0h]
      mov    dword ptr fs:[0h],esp  
      xor   eax,eax;reset EAX invoke int3
      int    3h
      pop    dword ptr fs:[0h];restore exception handler
      add   esp,4
      mov eax, fs:[18h] ; teb
      add eax, 0BFCh 
      mov ebx, [eax] ; pointer to a unicode string 
      test ebx, ebx ; (ntdll.dll, gdi32.dll,...) 
      je      rf_label
      jmp    rt_label
  exception_handler:
      mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
      inc   dword ptr [eax+0xb8];set ContextRecord.EIP
      xor   eax,eax
      retn
    } 
复制代码


2.23 FD_check_StartupInfo();
  这是从pediy上拷贝来的。Window创建进程的时候会把STARTUPINFO结构中的值设为0,而通过调试器创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断是否在调试程序。
复制代码
  STARTUPINFO si;
  ZeroMemory( &si, sizeof(si) );
  si.cb = sizeof(si);
  GetStartupInfo(&si);
  if ( (si.dwX != 0) || (si.dwY !=0) 
    || (si.dwXCountChars != 0) || (si.dwYCountChars !=0 ) 
    || (si.dwFillAttribute != 0) || (si.dwXSize != 0) 
    || (si.dwYSize != 0) )
    return true;
  else  
    return false;
复制代码


2.24 FD_Parent_Process1()
与前面的FD_Parent_Process原理一样,唯一不同的是使用ZwQueryInformationProcess检测父进程,而没有使用Process32Next,这有一个好处是可以绕过OD的HideOD插件。

2.25 FD_Exception_Instruction_count()
  好像《软件加解密技术》中有提到这个反调试。
  通过注册一个异常句柄,在特定地址设置一些硬件断点,当通过这些地址时都会触发EXCEPTION_SINGLE_STEP (0x80000004)的异常,在异常处理程序中,将会调整指令指针到一条新指令,然后恢复运行。可以通过进入进程context结构来设置这些断点,有些调试器不能处理那些不是自己设置的硬件断点,从而导致一些指令将会被漏掉计数,这就形成了一个反调试。
复制代码
  __asm
  {
    xor    eax,eax;
    cdq;
    push  e_handler;
    push  dword ptr fs:[eax];
    mov    fs:[eax],esp;
    int 3;
hwbp1:  nop
hwbp2:  nop
hwbp3:  nop
hwbp4:  nop
    div    edx
    nop
    pop    dword ptr fs:[0]
    add    esp,4
    cmp    al,4;
    jne    rt_label;
    jmp    rf_label;

e_handler:
    xor    eax,eax;
    ;ExceptionRecord
    mov    ecx,dword ptr[esp+0x04]
    ;Contextrecord
    mov    edx,dword ptr[esp+0x0c]
    ;ContextEIP
    inc    byte ptr[edx+0xb8];
    
    ;ExceptionCode
    mov    ecx,dword ptr[ecx];

    ;1.EXCEPTION_INT_DIVIDE_BY_ZERO
    cmp    ecx,0xc0000094;
    jne    Ex_next2;
    ;Context_eip
    inc    byte ptr[edx+0xb8];
    mov    dword ptr[edx+0x04],eax;dr0
    mov    dword ptr[edx+0x08],eax;dr1
    mov    dword ptr[edx+0x0c],eax;dr2
    mov    dword ptr[edx+0x10],eax;dr3
    mov    dword ptr[edx+0x14],eax;dr6
    mov    dword ptr[edx+0x18],eax;dr7
    ret

    ;2.EXCEPTION_BREAKPOINT
Ex_next2:
    cmp    ecx,0x80000003;
    jne    Ex_next3;

    mov    dword ptr[edx+0x04],offset hwbp1;dr0
    mov    dword ptr[edx+0x08],offset hwbp2;dr1
    mov    dword ptr[edx+0x0c],offset hwbp3;dr2
    mov    dword ptr[edx+0x10],offset hwbp4;dr3
    mov    dword ptr[edx+0x18],0x155;dr7
    ret

    ;3.EXCEPTION_SINGLE_STEP
Ex_next3:
    cmp  ecx,0x80000004
    jne    rt_label
    ;CONTEXT_Eax
    inc    byte ptr[edx+0xb0]
    ret
  }
复制代码


2.26 FD_INT_2d()
在windows anti-debug reference中指出,如果程序未被调试这个中断将会生产一个断点异常. 被调试并且未使用跟踪标志执行这个指令, 将不会有异常产生程序正常执行. 如果被调试并且指令被跟踪, 尾随的字节将被跳过并且执行继续. 因此, 使用 INT 2Dh 能作为一个强有力的反调试和反跟踪机制。

复制代码
  __try
  {
    __asm
    {
        int 2dh
      inc eax;any opcode of singlebyte.
      ;or u can put some junkcode,"0xc8"..."0xc2"..."0xe8"..."0xe9"
    }
  return true;
  }
  __except(1)
  {
    return false;
  }
复制代码


三、  检测-专用调试器(FS_)
    这一部分是我比较喜欢的,但内容还不是很丰富,比如:
1、  针对SoftIce的检测方法有很多,但由于我从没使用过Softice,也没有条件去测试,所以没有给出太多,有兴趣的可以自己查阅资料进行补充,针对softice网上资料较多,或查阅《软件加解密技术》。
2、  同样,这里也没有给出windbg等等其它调试器的检测方法。
3、  而针对Odplugin,也只给了几种HideOD的检测。事实上,目前OD的使用者通常都使用众多的强大插件,当OD的反调试越来越普遍时,自己设计几款常用的OD插件的反调试,将会是非常有效的反调试手段。
4、  对VME的检测也只给出了两种,如想丰富这一部分可以参考Peter Ferrie的一篇anti-vme的文章(http://bbs.pediy.com/showthread.php?t=68411)。里面有非常多的anti-vme方法。

    针对专用调试器的函数列表如下:
复制代码
//find specific debugger
bool FS_OD_Exception_GuardPages();
bool FS_OD_Int3_Pushfd();
bool FS_SI_UnhandledExceptionFilter();
bool FS_ODP_Process32NextW();
bool FS_ODP_OutputDebugStringA();
bool FS_ODP_OpenProcess();
bool FS_ODP_CheckRemoteDebuggerPresent();
bool FS_ODP_ZwSetInformationThread();
bool FS_SI_Exception_Int1();
bool IsInsideVMWare_();
bool FV_VMWare_VMX();
bool FV_VPC_Exception();
int FV_VME_RedPill();//0:none,1:vmvare;2:vpc;3:others
复制代码


3.1 FS_OD_Exception_GuardPages
    “保护页异常”是一个简单的反调试技巧。当应用程序尝试执行保护页内的代码时,将会产生一个EXCEPTION_GUARD_PAGE(0x80000001)异常,但如果存在调试器,调试器有可能接收这个异常,并允许该程序继续运行,事实上,在OD中就是这样处理的,OD使用保护页来实现内存断点。
最开始实现时忘记了free申请的空间,多谢sessiondiy提醒。
复制代码
  SYSTEM_INFO sSysInfo;
  LPVOID lpvBase;
  BYTE * lptmpB;
  GetSystemInfo(&sSysInfo);
  DWORD dwPageSize=sSysInfo.dwPageSize;
  DWORD flOldProtect;

  DWORD dwErrorcode;

  lpvBase=VirtualAlloc(NULL,dwPageSize,MEM_COMMIT,PAGE_READWRITE);
  if(lpvBase==NULL)
    return false;
  
  lptmpB=(BYTE *)lpvBase;
  *lptmpB=0xc3;//retn

  VirtualProtect(lpvBase,dwPageSize,PAGE_EXECUTE_READ | PAGE_GUARD,&flOldProtect);
  
  __try
  {
    __asm  call dword ptr[lpvBase];
    VirtualFree(lpvBase,0,MEM_RELEASE);
    return true;
  }
  __except(1)
  {
    VirtualFree(lpvBase,0,MEM_RELEASE);
    return false;
  }
复制代码


3.2 FS_OD_Int3_Pushfd
    这是个最近比较牛X的反调试,据称是vmp1.64里发现的,好像ttprotect里面也有使用,我没有验证。Pediy里有帖子详细讨论,我是看到gkend的分析,才搞懂一些。下面摘自gkend分析
代码:


    int3,pushfd和int3,popfd一样的效果。只要修改int3后面的popfd为其他值,OD都能通过。老掉牙的技术又重新被用了。SEH异常机制的运用而已。
    原理:在SEH异常处理中设置了硬件断点DR0=EIP+2,并把EIP的值加2,那么应该在int3,popfd后面的指令执行时会产生单步异常。但是OD遇到前面是popfd/pushfd时,OD会自动在popfd后一指令处设置硬件断点,而VMP的seh异常处理会判断是否已经设置硬件断点,如果已经有硬件断点就不产生单步异常,所以不能正常执行。


    http://bbs.pediy.com/showthread.php?t=67737
    大家也可以仔细研究下OD下的pushfd,popfd等指令,相信利用它们可以构造很多反调试,下面是我实现的一个,不过现在看起来有点没看懂,不知当时为什么用了两个int3。
复制代码
  __asm
  {
    push   offset e_handler; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    pushfd
    nop
    nop
    nop
    nop
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    test   eax,eax; check the flag 
    je    rf_label
    jmp    rt_label

e_handler:
    push   offset e_handler1; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset EAX invoke int3
    int    3h
    nop
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4
    ;EAX = ContextRecord
    mov    ebx,eax;dr0=>ebx
    mov   eax,dword ptr [esp+0xc]
    ;set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];
    mov    dword ptr [eax+0xb0],ebx;dr0=>eax
    xor    eax,eax
    retn

e_handler1:
    ;EAX = ContextRecord
    mov   eax,dword ptr [esp+0xc]
    ;set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];
    mov    ebx,dword ptr[eax+0x04]
    mov    dword ptr [eax+0xb0],ebx;dr0=>eax
    xor    eax,eax
    retn
rt_label:
    xor  eax,eax
    inc eax
    mov esp,ebp
    pop  ebp
    retn
rf_label:
    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码




3.3 FS_SI_UnhandledExceptionFilter
    这个针对SoftIce的反调试很简单,好像是SoftIce会修改UnhandledExceptionFilter这个函数的第一个字节为CC。因此判断这个字节是否为cc,就是一种检查softice的简便方法。
复制代码
FARPROC Uaddr ;
BYTE tmpB = 0;
(FARPROC&) Uaddr =GetProcAddress ( GetModuleHandle("kernel32.dll"),"UnhandledExceptionFilter");
tmpB = *((BYTE*)Uaddr);   // 取UnhandledExceptionFilter函数第一字节
tmpB=tmpB^0x55;
if(tmpB ==0x99)           // 如该字节为CC,则SoftICE己加载
  return true;
else  
  return false;
复制代码


3.4 FS_ODP_Process32NextW
    当我在调试FD_parentprocess时,感觉总是怪怪的,使用OD时运行Process32NextW总是返回失败,搞了一个晚上,才搞懂原来是OD的插件HideOD在作怪。当HideOD的Process32NextW的选项被选中时,它会更改Process32NextW的返回值,使其始终返回false,这主要是HideOD针对FD_parentprocess这个反调试的一个反反调试。但也正是这一点暴露的它的存在。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  //1.Process32Next
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"Process32NextW");
  if (Func_addr != NULL) 
  {
    tmpW=*(WORD*)Func_addr;
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0xFF8B)//maybe option of Process32Next is selected.
        return true;
      break;
    default:
      if(tmpW==0xC033)
        return true;
      break;
    }
  }
复制代码


    但上面的代码并不完美,因为有跨平台问题,所以要先获得当前操作系统版本。目前只在win2k和winxp下进行了测试。

3.5 FS_ODP_OutputDebugStringA
    同样,HIDEOD的OutputDebugStringA选项,也对OutputDebugStringA这个api做了处理,具体修改内容我记不得了,大家可以自己比对一下。我的代码如下:
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  //2.OutputDebugStringA
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"OutputDebugStringA");
  if (Func_addr != NULL) 
  {
    tmpW=*(WORD*)Func_addr;
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0x3468)//maybe option of OutputDebugStringAt is selected.
        return true;
      break;
    default:
      if(tmpW==0x01e8)
        return true;
      break;
    }
  }
  return false;
复制代码


3.6 FS_ODP_OpenProcess
    这个据称这个是针对HideDebugger这个插件的,当这个插件开启时,它会挂钩OpenProcess这个函数,它修改了OpenProcess的前几个字节。因此检测这几个字节就可实现这个反调试。
复制代码
  FARPROC Func_addr;
  BYTE tmpB;
  //OpenProcess
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"OpenProcess");
  if (Func_addr != NULL) 
  {
    tmpB=*((BYTE*)Func_addr+6);
    if(tmpB==0xea)//HideDebugger Plugin of OD is present
        return true;
  }
  return false;
复制代码


3.7 FS_ODP_CheckRemoteDebuggerPresent
    和前面提到的两个HideOD的反调试类似,不多说了。大家可以自行比对一下开启和不开启HideOD时,CheckRemoteDebuggerPresent函数的异同,就可以设计反这个插件的反调试了。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  BYTE tmpB;
  //2.CheckRemoteDebuggerPresent
  HMODULE hModule = GetModuleHandle("kernel32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"CheckRemoteDebuggerPresent");
  if (Func_addr != NULL) 
  {
    tmpB=*((BYTE*)Func_addr+10);
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpB!=0x74)//HideOD is present
        return true;
      break;
    default:
      break;
    }
  }
  return false;
复制代码


3.8 FS_ODP_ZwSetInformationThread
    和前面提到的几个HideOD的反调试类似,大家可以自行比对一下开启和不开启HideOD时,ZwSetInformationThread函数的异同,就可以设计反这个插件的反调试了。
复制代码
  int OSVersion;
  FARPROC Func_addr;
  WORD tmpW;
  BYTE tmpB0,tmpB1;
  //2.CheckRemoteDebuggerPresent
  HMODULE hModule = GetModuleHandle("ntdll.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule,"ZwSetInformationThread");
  if (Func_addr != NULL) 
  {
    tmpW=*((WORD*)Func_addr+3);
    tmpB0=*((BYTE*)Func_addr+9);
    tmpB1=*((BYTE*)Func_addr+10);
    OSVersion=myGetOSVersion();
    switch(OSVersion)
    {
    case OS_winxp:
      if(tmpW!=0x0300)//HideOD is present
        return true;
      break;
    case OS_win2k:
      if((tmpB0!=0xcd)&&(tmpB1!=0x2e))
        return true;
      break;
    default:
      break;
    }
  }
  return false;
复制代码


3.9 FS_SI_Exception_Int1
    通常int1的DPL为0,这表示"cd 01"机器码不能在3环下执行。如果直接执行这个中断将会产生一个保护错误,windows会产生一个 EXCEPTION_ACCESS_VIOLATION (0xc0000005)异常。然而,如果SOFTICE正在运行,它挂钩了int1,并调整其 DPL为3。这样SoftICE就可以在用户模式执行单步操作了。
    当int 1发生时,SoftICE不检查它是由于陷阱标志位还是由软件中断产生,SoftICE总是去调用原始中断1的句柄,此时将会产生一个 EXCEPTION_SINGLE_STEP (0x80000004)而不是 EXCEPTION_ACCESS_VIOLATION (0xc0000005)异常,这就形成了一个简单的反调试方法。
复制代码
  __asm
  {
    push   offset eh_int1; set exception handler
    push  dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor   eax,eax;reset flag(EAX) invoke int3
    int    1h
    pop    dword ptr fs:[0h];restore exception handler
    add   esp,4

    cmp    eax,0x80000004; check the flag 
    je    rt_label_int1
    jmp    rf_label_int1

eh_int1:
    mov    eax,[esp+0x4];
    mov    ebx,dword ptr [eax];
    mov   eax,dword ptr [esp+0xc];EAX = ContextRecord
    mov    dword ptr [eax+0xb0],ebx;set flag (ContextRecord.EAX)

    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    inc   dword ptr [eax+0xb8];set ContextRecord.EIP
    xor   eax,eax
    retn
  }
复制代码


3.10 FV_VMWare_VMX
    这是一个针对VMWare虚拟机仿真环境的反调试,我从网上直接拷贝的代码。
    VMWARE提供一种主机和客户机之间的通信方法,这可以被用做一种VMWare的反调试。Vmware将会处理IN (端口为0x5658/’VX’)指令,它会返回一个majic数值“VMXh”到EBX中。
    当在保护模式操作系统的3环下运行时,IN指令的执行将会产生一个异常,除非我们修改了I/O的优先级等级。然而,如果在VMWare下运行,将不会产生任何异常,同时EBX寄存器将会包含’VMXh’,ECX寄存器也会被修改为Vmware的产品ID。
    这种技巧在一些病毒中比较常用。
    针对VME的反调试,在peter Ferrie的另一篇文章<>中有大量的描述,有兴趣的可以根据这篇文章,将FV_反调试好好丰富一下。


复制代码
bool IsInsideVMWare_()
{
  bool r;
  _asm
  {
    push   edx
    push   ecx
    push   ebx

    mov    eax, 'VMXh'
    mov    ebx, 0 // any value but MAGIC VALUE
    mov    ecx, 10 // get VMWare version
    mov    edx, 'VX' // port number
    in     eax, dx // read port
                   // on return EAX returns the VERSION
    cmp    ebx, 'VMXh' // is it a reply from VMWare?
    setz   [r] // set return value

    pop    ebx
    pop    ecx
    pop    edx
  }
  return r;
}

bool FV_VMWare_VMX()
{
  __try
  {
    return IsInsideVMWare_();
  }
  __except(1) // 1 = EXCEPTION_EXECUTE_HANDLER
  {
    return false;
  }
}
复制代码


3.11 FV_VPC_Exception
    这个代码我也是完整从网上拷贝下来的,具体原理在<>这篇文章里也有详细描述。与VMWare使用一个特殊端口完成主机和客户机间通信的方法类似的是,VirtualPC靠执行非法指令产生一个异常供内核捕获。这个代码如下:
代码:


0F 3F x1 x2
0F C7 C8 y1 y2


    由这两个非法指令引起的异常将会被应用程序捕获,然而,如果VirtualPC正在运行,将不会产生异常。X1,x2的允许的数值还不知道,但有一部分已知可以使用,如0A 00,11 00…等等。


复制代码
__declspec(naked) bool FV_VPC_Exception()
{
  _asm
  {
    push ebp
    mov  ebp, esp

    mov  ecx, offset exception_handler

    push ebx
    push ecx

    push dword ptr fs:[0]
    mov  dword ptr fs:[0], esp

    mov  ebx, 0 // Flag
    mov  eax, 1 // VPC function number
  }

    // call VPC 
   _asm __emit 0Fh
   _asm __emit 3Fh
   _asm __emit 07h
   _asm __emit 0Bh

  _asm
  {
    mov eax, dword ptr ss:[esp]
    mov dword ptr fs:[0], eax

    add esp, 8

    test ebx, ebx
    
    setz al

    lea esp, dword ptr ss:[ebp-4]
    mov ebx, dword ptr ss:[esp]
    mov ebp, dword ptr ss:[esp+4]

    add esp, 8

    jmp ret1
exception_handler:
    mov ecx, [esp+0Ch]
    mov dword ptr [ecx+0A4h], -1 // EBX = -1 -> not running, ebx = 0 -> running
    add dword ptr [ecx+0B8h], 4 // -> skip past the call to VPC
    xor eax, eax // exception is handled
    ret
ret1:
    ret
  }
}
复制代码


3.12 FV_VME_RedPill
    这个方法似乎是检测虚拟机的一个简单有效的方法,虽然还不能确定它是否是100%有效。名字很有意思,红色药丸(为什么不是bluepill,哈哈)。我在网上找到了个ppt专门介绍这个方法,可惜现在翻不到了。记忆中原理是这样的,主要检测IDT的数值,如果这个数值超过了某个数值,我们就可以认为应用程序处于虚拟环境中,似乎这个方法在多CPU的机器中并不可靠。据称ScoobyDoo方法是RedPill的升级版。代码也是在网上找的,做了点小改动。有四种返回结果,可以确认是VMWare,还是VirtualPC,还是其它VME,或是没有处于VME中。


复制代码
   //return value:  0:none,1:vmvare;2:vpc;3:others
   unsigned char matrix[6];

    unsigned char redpill[] = 
        "\x0f\x01\x0d\x00\x00\x00\x00\xc3";

    HANDLE hProcess = GetCurrentProcess();

    LPVOID lpAddress = NULL;
    PDWORD lpflOldProtect = NULL;

    __try
    {
        *((unsigned*)&redpill[3]) = (unsigned)matrix;

        lpAddress = VirtualAllocEx(hProcess, NULL, 6, MEM_RESERVE|MEM_COMMIT , PAGE_EXECUTE_READWRITE);
        
        if(lpAddress == NULL)
            return 0;

        BOOL success = VirtualProtectEx(hProcess, lpAddress, 6, PAGE_EXECUTE_READWRITE , lpflOldProtect);

        if(success != 0)
             return 0;
   
        memcpy(lpAddress, redpill, 8);

        ((void(*)())lpAddress)();

        if (matrix[5]>0xd0) 
        {
          if(matrix[5]==0xff)//vmvare
            return 1;
          else if(matrix[5]==0xe8)//vitualpc
            return 2;
          else
            return 3;
        }
        else 
            return 0;
    }
    __finally
    {
        VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    }
复制代码


四、  检测-断点(FB_)
这一部分内容较少,但实际上可用的方法也比较多,我没有深入研究,不敢乱写,照抄了几个常用的方法:


//find breakpoint
bool FB_HWBP_Exception();
DWORD FB_SWBP_Memory_CRC();
bool FB_SWBP_ScanCC(BYTE * addr,int len);
bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);


4.1 FB_HWBP_Exception
  在异常处理程序中检测硬件断点,是比较常用的硬件断点检测方法。在很多地方都有提到。


复制代码
  __asm
  {
    push   offset exeception_handler; set exception handler
    push   dword ptr fs:[0h]
    mov    dword ptr fs:[0h],esp  
    xor    eax,eax;reset EAX invoke int3
    int    1h
    pop    dword ptr fs:[0h];restore exception handler
    add    esp,4
    ;test if EAX was updated (breakpoint identified) 
    test   eax,eax
    jnz     rt_label
    jmp    rf_label

exeception_handler:
    ;EAX = CONTEXT record
    mov     eax,dword ptr [esp+0xc]

    ;check if Debug Registers Context.Dr0-Dr3 is not zero
    cmp     dword ptr [eax+0x04],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x08],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x0c],0
    jne     hardware_bp_found
    cmp     dword ptr [eax+0x10],0
    jne     hardware_bp_found
    jmp     exception_ret

hardware_bp_found:
    ;set Context.EAX to signal breakpoint found
    mov     dword ptr [eax+0xb0],0xFFFFFFFF
exception_ret:
    ;set Context.EIP upon return
    inc       dword ptr [eax+0xb8];set ContextRecord.EIP
    inc       dword ptr [eax+0xb8];set ContextRecord.EIP
    xor     eax,eax
    retn
  }
复制代码


4.2 FB_SWBP_Memory_CRC()
  由于在一些常用调试器中,比如OD,其是将代码设置为0xcc来实现普通断点,因此当一段代码被设置了普通断点,则其中必定有代码的修改。因此对关键代码进行CRC校验则可以实现侦测普通断点。但麻烦的是每次代码修改,或更换编译环境,都要重新设置CRC校验值。
  下面的代码拷贝自《软件加解密技术》,里面完成的是对整个代码段的CRC校验,CRC校验值保存在数据段。CRC32算法实现代码网上有很多,就不列出来了。


复制代码
DWORD FB_SWBP_Memory_CRC()
{
  //打开文件以获得文件的大小
  DWORD fileSize,NumberOfBytesRW;
  DWORD CodeSectionRVA,CodeSectionSize,NumberOfRvaAndSizes,DataDirectorySize,ImageBase;
  BYTE* pMZheader;
  DWORD pPEheaderRVA;
  TCHAR  *pBuffer ;
  TCHAR szFileName[MAX_PATH]; 

  GetModuleFileName(NULL,szFileName,MAX_PATH);
  //打开文件
  HANDLE hFile = CreateFile(
    szFileName,
    GENERIC_READ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);
   if ( hFile != INVALID_HANDLE_VALUE )
   {
    //获得文件长度 :
    fileSize = GetFileSize(hFile,NULL);
    if (fileSize == 0xFFFFFFFF) return 0;
    pBuffer = new TCHAR [fileSize];     申请内存,也可用VirtualAlloc等函数申请内存
    ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);//读取文件内容
    CloseHandle(hFile);  //关闭文件
   }
   else
     return 0;
  pMZheader=(BYTE*)pBuffer; //此时pMZheader指向文件头
  pPEheaderRVA = *(DWORD *)(pMZheader+0x3c);//读3ch处的PE文件头指针
  ///定位到PE文件头(即字串“PE\0\0”处)前4个字节处,并读出储存在这里的CRC-32值:

  NumberOfRvaAndSizes=*((DWORD *)(pMZheader+pPEheaderRVA+0x74));//得到数据目录结构数量
  DataDirectorySize=NumberOfRvaAndSizes*0x8;//得到数据目录结构大小
  ImageBase=*((DWORD *)(pMZheader+pPEheaderRVA+0x34));//得到基地址
  //假设第一个区块就是代码区块
  CodeSectionRVA=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0xc));//得到代码块的RVA值
  CodeSectionSize=*((DWORD *)(pMZheader+pPEheaderRVA+0x78+DataDirectorySize+0x8));///得到代码块的内存大小
  delete pBuffer;  // 释放内存
  return CRC32((BYTE*)(CodeSectionRVA+ImageBase),CodeSectionSize);
}
复制代码


4.3 FB_SWBP_ScanCC
扫描CC的方法,比照前面校验代码CRC数值的方法更直接一些,它直接在所要检测的代码区域内检测是否有代码被更改为0xCC,0xcc对应汇编指令为int3 ,对一些常用的调试器(如OD)其普通断点就是通过修改代码为int3来实现的。但使用时要注意是否正常代码中就包含CC。通常这个方法用于扫描API函数的前几个字节,比如检测常用的MessageBoxA、GetDlgItemTextA等。


复制代码
bool FB_SWBP_ScanCC(BYTE * addr,int len)
{
  FARPROC Func_addr ;
  HMODULE hModule = GetModuleHandle("USER32.dll"); 
  (FARPROC&) Func_addr =GetProcAddress ( hModule, "MessageBoxA");
  if (addr==NULL)
    addr=(BYTE *)Func_addr;//for test
  BYTE tmpB;
  int i;
  __try
  {
    for(i=0;i     {
      tmpB=*addr;
      tmpB=tmpB^0x55;
      if(tmpB==0x99)// cmp 0xcc
        return true;
    }
  }
  __except(1)
    return false;
  return false;
}
复制代码


4.4 FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue);
此方法类似CRC的方法,只是这里是检测累加和。它与CRC的方法有同样的问题,就是要在编译后,计算累加和的数值,再将该值保存到数据区,重新编译。在这里创建了一个单独的线程用来监视代码段。


复制代码
DWORD WINAPI CheckSum_ThreadFunc( LPVOID lpParam ) 

  DWORD dwThrdParam[3];
  BYTE tmpB;
  DWORD Value=0;
  dwThrdParam[0]=* ((DWORD *)lpParam);
     dwThrdParam[1]=* ((DWORD *)lpParam+1);
      dwThrdParam[2]=* ((DWORD *)lpParam+2);
  BYTE *addr_begin=(BYTE *)dwThrdParam[0];
  BYTE *addr_end=(BYTE *)dwThrdParam[1];
  DWORD sumValue=dwThrdParam[2];
  for(int i=0;i<(addr_end-addr_begin);i++)
    Value=Value+*(addr_begin+i);
  /* //if sumvalue is const,it should be substract.
  DWORD tmpValue;
  Value=Value-(sumValue&0x000000FF);
  tmpValue=(sumValue&0x0000FF00)>>8;
  Value=Value-tmpValue;
  tmpValue=(sumValue&0x0000FF00)>>16;
  Value=Value-tmpValue;
  tmpValue=(sumValue&0x0000FF00)>>24;
  Value=Value-tmpValue;*/
  if (Value!=sumValue)
    MessageBox(NULL,"SWBP is found by CheckSum_ThreadFunc","CheckSum_ThreadFunc",MB_OK|MB_ICONSTOP);
    return 1; 
}

bool FB_SWBP_CheckSum_Thread(BYTE *addr_begin,BYTE *addr_end,DWORD sumValue)
{
    DWORD dwThreadId;
  DWORD dwThrdParam[3];
  dwThrdParam[0]=(DWORD)addr_begin;
  dwThrdParam[1]=(DWORD)addr_end;
  dwThrdParam[2]=sumValue;
    HANDLE hThread; 

    hThread = CreateThread( 
        NULL,                        // default security attributes 
        0,                           // use default stack size  
        CheckSum_ThreadFunc,         // thread function 
        &dwThrdParam[0],                // argument to thread function 
        0,                           // use default creation flags 
        &dwThreadId);                // returns the thread identifier 
    // Check the return value for success. 

   if (hThread == NULL) 
      return false;
   else 
   {
      Sleep(1000);
      CloseHandle( hThread );
    return true;
   }
}
复制代码


五、  检测-跟踪(FT_)
个人认为,反跟踪的一些技巧,多数不会非常有效,因为在调试时,多数不会被跟踪经过,除非用高超的技巧将关键代码和垃圾代码及这些反跟踪技巧融合在一起,否则很容易被发现或被无意中跳过。
函数列表如下:


复制代码
//Find Single-Step or Trace
bool FT_PushSS_PopSS();
void FT_RDTSC(unsigned int * time);
DWORD FT_GetTickCount();
DWORD FT_SharedUserData_TickCount();
DWORD FT_timeGetTime();
LONGLONG FT_QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);
bool FT_F1_IceBreakpoint();
bool FT_Prefetch_queue_nop1();
bool FT_Prefetch_queue_nop2();
复制代码


5.1 FT_PushSS_PopSS
这个反调试在<>里有描述,如果调试器跟踪经过下面的指令序列:


复制代码
  __asm
  {
    push ss    //反跟踪指令序列
    ;junk
    pop  ss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
}
复制代码


Pushf将会被执行,同时调试器无法设置压进堆栈的陷阱标志,应用程序通过检测陷阱标志就可以判断处是否被跟踪调试。


复制代码
  __asm
  {
    push ebp
    mov ebp,esp
    push ss    //反跟踪指令序列
    ;junk
    pop  ss    //反跟踪指令序列
    pushf    //反跟踪指令序列
    ;junk
    pop eax    //反跟踪指令序列
    and  eax,0x00000100
    jnz  rt_label

    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
rt_label:
    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


5.2 FT_RDTSC
通过检测某段程序执行的时间间隔,可以判断出程序是否被跟踪调试,被跟踪调试的代码通常都有较大的时间延迟,检测时间间隔的方法有很多种。比如RDTSC指令,kernel32_GetTickCount函数,winmm_ timeGetTime 函数等等。
下面为RDTSC的实现代码。


复制代码
  int time_low,time_high;
  __asm
  {
    rdtsc
    mov    time_low,eax
    mov    time_high,edx
  }
复制代码


5.3 FT_GetTickCount
  GetTickCount函数检测时间间隔简单且常用。直接调用即可。具体可查MSDN。

5.4 FT_SharedUserData_TickCount
  直接调用GetTickCount函数来检测时间间隔的方法,虽然简单却容易被发现。而使用GetTickCount的内部实现代码,直接读取SharedUserData数据结构里的数据的方法,更隐蔽些。下面的代码是直接从GetTickCount里扣出来的,其应该是在位于0x7FFE0000地址处的SharedUserData数据接口里面直接取数据,不过这个代码应该有跨平台的问题,我这里没有处理。大家可以完善下。


复制代码
  DWORD tmpD;
  __asm
  {
    mov     edx, 0x7FFE0000
    mov     eax, dword ptr [edx]
    mul     dword ptr [edx+4]
    shrd    eax, edx, 0x18
    mov    tmpD,eax
  }
  return tmpD;
复制代码


5.5 FT_timeGetTime
  使用winmm里的timeGetTime的方法也可以用来检测时间间隔。直接调用这个函数即可。具体可查MSDN。

5.6 FT_QueryPerformanceCounter
  这是一种高精度时间计数器的方法,它的检测刻度最小,更精确。


  if(QueryPerformanceCounter(lpPerformanceCount))
        return lpPerformanceCount->QuadPart;
  else 
     return 0;


5.7 FT_F1_IceBreakpoint
  在<>中有讲述这个反跟踪技巧。这个所谓的"Ice breakpoint" 是Intel 未公开的指令之一, 机器码为0xF1.执行这个指令将产生单步异常.,如果程序已经被跟踪, 调试器将会以为它是通过设置标志寄存器中的单步标志位生成的正常异常. 相关的异常处理器将不会被执行到.下面是我的实现代码:


复制代码
__asm
  {
  push   offset eh_f1; set exception handler
     push  dword ptr fs:[0h]
     mov    dword ptr fs:[0h],esp  
     xor   eax,eax;reset EAX invoke int3
     _emit 0xf1
     pop    dword ptr fs:[0h];restore exception handler
     add    esp,4
  test  eax,eax
  jz    rt_label_f1
  jmp    rf_label_f1

eh_f1:
     mov eax,dword ptr[esp+0xc]
  mov    dword ptr [eax+0xb0],0x00000001;set flag (ContextRecord.EAX)
     inc dword ptr [eax+0xb8]
     xor eax,eax
     retn
rt_label_f1:
  inc    eax
  mov    esp,ebp
     pop    ebp
     retn
rf_label_f1:
  xor    eax,eax
  mov    esp,ebp
     pop    ebp
     retn
  }
复制代码


5.8 FT_Prefetch_queue_nop1
这个反调试是在<>中给出的,它主要是基于REP指令,通过REP指令来修改自身代码,在非调试态下,计算机会将该指令完整取过来,因此可以正确的执行REP这个指令,将自身代码完整修改,但在调试态下,则在修改自身的时候立即跳出。
这个反跟踪技巧个人觉得用处不大,因为只有在REP指令上使用F7单步时,才会触发这个反跟踪,而我个人在碰到REP时,通常都是F8步过。下面是利用这个CPU预取指令的特性的实现反跟踪的一种方法,正常情况下,REP指令会修改其后的跳转指令,进入正常的程序流程,但在调试态下,其无法完成对其后代码的修改,从而实现反调试。


复制代码
   DWORD oldProtect;
   DWORD tmpProtect;
   __asm
   {
    lea eax,dword ptr[oldProtect]
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    nop
label3:
    mov al,0x90
    push 0x10
    pop ecx
    mov edi,offset label3
    rep stosb
    jmp rt_label
    nop
    nop
    nop
    nop
    nop
rf_label:
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x205CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];

    xor eax,eax
    mov esp,ebp
    pop ebp
    retn
rt_label:
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x205CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];

    xor eax,eax
    inc eax
    mov esp,ebp
    pop ebp
    retn
  }
复制代码


5.9 FT_Prefetch_queue_nop2
  与5.8节类似,这是根据CPU预取指令的这个特性实现的另一种反跟踪技巧。原理是通过检测REP指令后的ECX值,来判断REP指令是否被完整执行。在正常情况下,REP指令完整执行后,ECX值应为0;但在调试态下,由于REP指令没有完整执行,ECX值为非0值。通过检测ECX值,实现反跟踪。


复制代码
  DWORD oldProtect;
  DWORD tmpProtect;
  __asm
  {
    lea eax,dword ptr[oldProtect]
    push eax
    push 0x40
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    mov ecx,0
label3:
    mov al,0x90
    push 0x10
    pop ecx
    mov edi,offset label3
    rep stosb
    nop
    nop
    nop
    nop
    nop
    nop
    push ecx
    ;write back
    mov dword ptr[label3],0x106a90b0
    mov dword ptr[label3+0x4],0x201CBF59
    mov dword ptr[label3+0x8],0xAAF30040
    mov dword ptr[label3+0xc],0x90909090
    mov dword ptr[label3+0x6],offset label3
    lea eax, dword ptr[tmpProtect];
    ;restore protect
    push eax
    push oldProtect
    push 0x10
    push offset label3;
    call dword ptr [VirtualProtect];
    pop ecx

    test ecx,ecx
    jne rt_label
  }
rf_label:
  return false;
rt_label:
  return true;
复制代码


六、  检测-补丁(FP_)
这部分内容也较少,方法当然也有很多种,原理都差不多,我只选了下面三种。这几种方法通常在一些壳中较常用,用于检验文件是否被脱壳或被恶意修改。
函数列表如下:


//find Patch
bool FP_Check_FileSize(DWORD Size);
bool FP_Check_FileHashValue_CRC(DWORD CRCVALUE_origin);
bool FP_Check_FileHashValue_MD5(DWORD MD5VALUE_origin);


6.1 FP_Check_FileSize(DWORD Size)
  通过检验文件自身的大小的方法,是一种比较简单的文件校验方法,通常如果被脱壳,或被恶意修改,就可能影响到文件的大小。我用下面的代码实现。需注意的是,文件的大小要先编译一次,将首次编译得到的数值写入代码,再重新编译完成。


复制代码
  DWORD Current_Size;
  TCHAR szPath[MAX_PATH];
  HANDLE hFile;

  if( !GetModuleFileName( NULL,szPath, MAX_PATH ) )
        return FALSE;

  hFile = CreateFile(szPath, 
    GENERIC_READ ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_ALWAYS, 
    FILE_ATTRIBUTE_NORMAL, 
    NULL);
  if (hFile == INVALID_HANDLE_VALUE)
    return false;
  Current_Size=GetFileSize(hFile,NULL);
  CloseHandle(hFile);
  if(Current_Size!=Size)
    return true;
  return false;
复制代码


6.2 FP_Check_FileHashValue_CRC
  检验文件的CRC数值,是比较常用的文件校验方法,相信很多人都碰到过了,我是在《软件加解密技术》中了解到的。需注意的是文件原始CRC值的获得,及其放置位置,代码编写完成后,通常先运行一遍程序,使用调试工具获得计算得到的数值,在将这个数值写入文件中,通常这个数值不参加校验,可以放置在文件的尾部作为附加数据,也可以放在PE头中不用的域中。
  下面的代码只是个演示,没有保存CRC的真实数值,也没有单独存放。


复制代码
  DWORD fileSize,NumberOfBytesRW;
  DWORD CRCVALUE_current;
  TCHAR szFileName[MAX_PATH]; 
  TCHAR  *pBuffer ;
  GetModuleFileName(NULL,szFileName,MAX_PATH);
  HANDLE hFile = CreateFile(
    szFileName,
    GENERIC_READ,
    FILE_SHARE_READ, 
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);
  if (hFile != INVALID_HANDLE_VALUE )
  {
    fileSize = GetFileSize(hFile,NULL);
    if (fileSize == 0xFFFFFFFF) return false;
    pBuffer = new TCHAR [fileSize];  
    ReadFile(hFile,pBuffer, fileSize, &NumberOfBytesRW, NULL);
    CloseHandle(hFile);
  }
  CRCVALUE_current=CRC32((BYTE *)pBuffer,fileSize);
  if(CRCVALUE_origin!=CRCVALUE_current)
    return true;
  return false;
复制代码


6.3 FP_Check_FileHashValue_MD5
与6.2节的原理相同,只是计算的是文件的MD5数值。仍要注意6.2节中同样的MD5真实数值的获得和存放问题。

啊冲 2016-02-03 10:44

重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
  我的博客:http://blog.csdn.net/sidyhe
  开发环境:VS2010 + WinDDK
  测试环境:VirtualDDK + VMware + Windows 7 sp1 x86
第一部分:重载镜像
  大家可以通过ARK工具来查看系统的内核模块,排在首位的一定是ntXXX.exe这个模块,这个模块就是自系统启动后加载的第一个模块,根据CPU及其不同特性的不同会加载不同的NT内核,如NTOSKRNL.EXE、NTKRNLMP.EXE、NTKRNLPA.EXE、NTKRPAMP.EXE。不同的NT模块代表不同的意义,如单CPU,多CPU,单CPU多核,是否支持PAE等都会影响所加载的NT内核,所以如果大家见到和别人不同的NT内核不要奇怪,那是因为你的CPU和别人的不一样。
  本次技术研究仅仅是针对于x86系统的Windows 7以及Windows XP,Windows x64系统由于强制数字签名以及PatchGuard技术无法实现,故不做讨论,当然如果你有办法解决这两个问题就另当别论了。至于是否兼容Windows 8 x86就有待各位验证了。
  既然要重载内核,肯定是NT内核(废话),上面说到了系统可能会加载不同名字的NT内核,那么就需要一些方法来确定当前系统所使用的内核,其中一个方法是使用ZwQuerySystemInformation传递SystemModuleInformation参数,不过在这里我不打算使用这个方法,因为太麻烦。我使用PsLoadedModuleList来确定NT内核,那问题来了,PsLoadedModuleList是一个未导出变量,这个变量记录了当前系统内核模块的信息,ZwQuerySystemInformation就是访问了PsLoadedModuleList来生成结果,如何定位这个东西呢?我不喜欢硬编码,所以我需要一种在不同系统上通用的方式来获取这个变量,经过收集资料发现在DriverEntry被调用时,第一个参数PDRIVER_OBJECT的PDRIVER_EXTENSION成员其实就是一个LDR_DATA_TABLE_ENTRY指针(参考WRK),这个与PsLoadedModuleList的类型是一致的,也就是说lpDriverObject->DriverSection是PsLoadedModuleList这个双向链表的其中一个节点,而PsLoadedModuleList是这个链表的头节点,根据大量的实践证明,lpDriverObject->DriverSection节点的下一个节点一定是PsLoadedModuleList,因为是双向循环链表嘛,那么定位这个东西就非常简单了,代码如下。
代码:
PLDR_DATA_TABLE_ENTRY PsLoadedModuleList = NULL;

VOID InitializePsLoadedModuleList(PDRIVER_OBJECT lpDriverObject)
{
  PLDR_DATA_TABLE_ENTRY ldr = (PLDR_DATA_TABLE_ENTRY)lpDriverObject->DriverSection;

  PsLoadedModuleList = (PLDR_DATA_TABLE_ENTRY)ldr->InLoadOrderLinks.Flink;
  return;
}
  找到了PsLoadedModuleList,那么链表的第一个节点就是NT内核了,可以取得文件路径,解决了重载内核的第一个问题。
  接下来就是读文件数据,并把数据部署为镜像。部署的过程与RING3的镜像一致,不熟悉的朋友可以去恶补一下PE知识。读到文件数据后,把数据部署为镜像的核心代码如下:
代码:
PVOID ReloadNtModule(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList)
{
  PVOID lpImageAddress = NULL;
  PLDR_DATA_TABLE_ENTRY NtLdr = (PLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
  PVOID lpFileBuffer;

  DbgPrint("Nt Module File is %wZ\n", &NtLdr->FullDllName);
  if (lpFileBuffer = KeGetFileBuffer(&NtLdr->FullDllName))
  {
    PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)lpFileBuffer;
    PIMAGE_NT_HEADERS lpNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);

    if (lpImageAddress = ExAllocatePool(NonPagedPool, lpNtHeader->OptionalHeader.SizeOfImage))
    {
      PUCHAR lpImageBytes = (PUCHAR)lpImageAddress;
      IMAGE_SECTION_HEADER *lpSection = IMAGE_FIRST_SECTION(lpNtHeader);
      ULONG i;

      RtlZeroMemory(lpImageAddress, lpNtHeader->OptionalHeader.SizeOfImage);
      RtlCopyMemory(lpImageBytes, lpFileBuffer, lpNtHeader->OptionalHeader.SizeOfHeaders);
      for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
      {
        RtlCopyMemory(lpImageBytes + lpSection.VirtualAddress, (PCHAR)lpFileBuffer + lpSection.PointerToRawData, lpSection.SizeOfRawData);
      }
      //代码不完整,后续补充
    }
    ExFreePool(lpFileBuffer);
  }
  if (lpImageAddress) DbgPrint("ImageAddress:0x%p\n", lpImageAddress);
  return lpImageAddress;
}
  至此解决了第二个问题,镜像已具基本雏形,了解的朋友一定知道下一步就是修复镜像了,即处理重定位以及输入表(导入表)。修复输入表没什么,难就难在重定位,重定位中包含了代码重定位和变量重定位,既然我们做的是重载内核,那么肯定是需要让原本走NT模块的流程转移到我们的新模块上,那么可以肯定的是代码重定位一定要在新模块上,至于变量,我个人的做法是指向原模块,因为即使是重载内核,也不能保证所有执行单元都会走新模块,这样保险一些,也简单一些,不过需要注意的是,变量重定位也包含IAT,所以我这里把IAT也指向新模块,否则修复输入表就没意义了,也可以防范IAT HOOK。还有,如果重定位的地方属于“可废弃”的区段(节),可以不用处理,因为原模块已经废弃了。还有还有,内核模块的导入表不存在序号导入,所以处理起来更加简单。
代码:
BOOLEAN KeFixIAT(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpImageAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  PIMAGE_IMPORT_DESCRIPTOR lpImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (ULONG)lpImageAddress);
  PVOID lpModuleAddress;

  while (lpImportDescriptor->Characteristics)
  {
    if (lpModuleAddress = KeGetModuleHandle(PsLoadedModuleList, (PCHAR)lpImageAddress + lpImportDescriptor->Name))
    {
      PIMAGE_THUNK_DATA lpThunk = (PIMAGE_THUNK_DATA)((ULONG)lpImageAddress + lpImportDescriptor->OriginalFirstThunk);
      PVOID *lpFuncTable = (PVOID*)((ULONG)lpImageAddress + lpImportDescriptor->FirstThunk);
      ULONG i;

      for (i = 0; lpThunk->u1.Ordinal; i++)
      {
        if ((lpThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0)
        {
          PIMAGE_IMPORT_BY_NAME lpName = (PIMAGE_IMPORT_BY_NAME)((PCHAR)lpImageAddress + lpThunk->u1.AddressOfData);
          PVOID lpFunc;

          if (lpFunc = KeGetProcAddress(lpModuleAddress, lpName->Name))
          {
            lpFuncTable = lpFunc;
          }
          else
          {
            DbgPrint("KeFixImageImportTable:Cannot found function : %s\n", lpName->Name);
            return FALSE;
          }
        }
        else
        {
          //impossible
        }
        lpThunk++;
      }
    }
    else
    {
      DbgPrint("KeFixImageImportTable:Cannot found Module : %s\n", (PCHAR)lpImageAddress + lpImportDescriptor->Name);
      return FALSE;
    }
    lpImportDescriptor++;
  }
  return TRUE;
}
  下面是处理重定位的代码,相对比较复杂了,这里只贴出来核心代码,即如何处理具体重定位地址的部分。
代码:
VOID KeFixRelocEx(PVOID New, PVOID Old, PVOID *lpFixAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)New;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  ULONG_PTR RelocValue = (ULONG_PTR)*lpFixAddress - lpNtHeader->OptionalHeader.ImageBase;

  if (KeFixRelocOfCheckIAT(New, (PCHAR)New + RelocValue))
  {
    *lpFixAddress = (PCHAR)New + RelocValue;
    return;
  }
  else
  {
    IMAGE_SECTION_HEADER *lpSecHdr = IMAGE_FIRST_SECTION(lpNtHeader);
    USHORT i;

    for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
    {
      if (RelocValue >= lpSecHdr.VirtualAddress && RelocValue < lpSecHdr.VirtualAddress + lpSecHdr.SizeOfRawData)
      {
        if (lpSecHdr.Characteristics & IMAGE_SCN_MEM_WRITE)
        {
          *lpFixAddress = (PCHAR)Old + RelocValue;
        }
        else
        {
          *lpFixAddress = (PCHAR)New + RelocValue;
        }
        return;
      }
    }
  }
  *lpFixAddress = (PCHAR)Old + RelocValue;
  return;
}
  至此,重新加载一份新的NT内核已经完成了绝大部分,还有一些细节没有处理,等到后面遇到时再告诉各位看客老爷,先卖个关子吧,涉及到的未公开结构可以从WRK中寻找,我所使用的结构在目前的x86系统中都没有改动,所以通用。具体工程代码先不打算放出来,如果大部分朋友需要的话,我会在后续文章中放出,不过我还是希望大家能够自己动手,这样收获会比纯粹的复制粘贴更多。
  下面的内容应该是HOOK了,用来接管正常的执行流程,我会抽时间写后续内容的。

啊冲 2016-02-03 10:45

第四部分:最后的修正(蓝屏与崩溃)
  终于走到了这里,对于新的NT内核只剩下了两个问题了(其他尚未发现),第一个就是蓝屏,最麻烦的BUG,起初在我遇到的时候也是疯掉了(当时用的还是Windows 7 x86),最后发现是因为内核异常得不到处理造成的,即POOL代码决不能异常。而造成这种现象的就是SafeSEH机制。简要说明就是在内核代码中发生了异常会多一些处理,判断SEH Handler是否有效,再执行。
  现在普遍的解决思路有两个:1.废除SafeSEH机制。2.使POOL合法化。
  先来研究一下这个SafeSEH到底是怎么回事,在发生异常时经过一些处理后会调用RtlDispatchException,进而调用RtlIsValidHandler来判断Handler是否有效,如果无效,你懂得。
代码:
BOOLEAN RtlIsValidHandler(IN PEXCEPTION_ROUTINE Handler)
{
    PULONG FunctionTable;
    ULONG FunctionTableLength;
    PVOID Base;

    FunctionTable = RtlLookupFunctionTable(Handler, &Base, &FunctionTableLength);

    if (FunctionTable && FunctionTableLength) {
        PEXCEPTION_ROUTINE FunctionEntry;
        LONG High, Middle, Low;

        if ((FunctionTable == LongToPtr(-1)) && (FunctionTableLength == (ULONG)-1)) {
            // Address is in an image that shouldn't have any handlers (like a resource only dll).
            RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), LongToPtr(-1), -1);
            return FALSE;
        }
    
        // Bias the handler value down by the image base and see if the result
        // is in the table

        (ULONG)Handler -= (ULONG)Base;
        Low = 0;
        High = FunctionTableLength;
        while (High >= Low) {
            Middle = (Low + High) >> 1;
            FunctionEntry = (PEXCEPTION_ROUTINE)FunctionTable[Middle];
            if (Handler < FunctionEntry) {
                High = Middle - 1;
            } else if (Handler > FunctionEntry) {
                Low = Middle + 1;
            } else {
                // found it
                return TRUE;
            }
        }
        // Didn't find it
        RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), FunctionTable, FunctionTableLength);

        return FALSE;
    }

    // Can't verify
    return TRUE;
}
  上面代码中的RtlLookupFunctionTable是取得一个类似函数表的东西,在PE信息中的体现则是IMAGE_NT_HEADERS.OptionalHeader[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG],用我手里的内核文件做例子,通过IDA观察到这个函数表,放着一堆RVA:
名称:  00.jpg
查看次数: 8
文件大小:  14.5 KB
  这些地址就是SEH Handler了,一共有0x12=18个,再来看KLDR的定义:
代码:
typedef struct _KLDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    PVOID ExceptionTable;
    ULONG ExceptionTableSize;
    // ULONG padding on IA64
    PVOID GpValue;
    PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG Flags;
    USHORT LoadCount;
    USHORT __Unused5;
    PVOID SectionPointer;
    ULONG CheckSum;
    // ULONG padding on IA64
    PVOID LoadedImports;
    PVOID PatchInformation;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
  很明显KLDR中的ExceptionTable应该指向IDA图示中的首地址。注意KLDR结构只能在WRK中查看,用WinDBG也看不到(dt),KLDR和LDR并不一样。还有,这个函数表里存放的就是RVA,并不会被重定位修复为VA。
  那么第一种方法就是HOOK RtlIsValidHandler,直接返回TRUE,则废掉SafeSEH。这个函数没导出,隐藏的很深,字节搜索或者重定位搜索很麻烦,但也是一种方法,不过我不用这种方式,虽然处理好之后能够很好的隐藏自己的NT内核。
  第二种方式就简单多了,虽然会被检测到存在异常模块,但又不影响什么,毕竟做的不是Rootkit。方法就是直接在PsLoadedModuleList插入一个KLDR,就那么简单。
代码:
PKLDR_DATA_TABLE_ENTRY KeGetImageLdrPointer(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
  PKLDR_DATA_TABLE_ENTRY lpTableEntry = PsLoadedModuleList;
  PKLDR_DATA_TABLE_ENTRY lpTablePointer = lpTableEntry;

  do 
  {
    if (lpTablePointer->DllBase == lpImageAddress)
    {
      return lpTablePointer;
    }
    lpTablePointer = (PKLDR_DATA_TABLE_ENTRY)(lpTablePointer->InLoadOrderLinks.Flink);
  } while (lpTableEntry != lpTablePointer);
  return NULL;
}

BOOLEAN KeInsertPsLoadedModuleList(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID NewImage, PVOID OldImage)
{
  PKLDR_DATA_TABLE_ENTRY lpNewLdr, lpSimLdr;

  if (lpSimLdr = KeGetImageLdrPointer(PsLoadedModuleList, OldImage))
  {
    if (lpNewLdr = ExAllocatePool(NonPagedPool, sizeof(KLDR_DATA_TABLE_ENTRY)))
    {
      RtlCopyMemory(lpNewLdr, lpSimLdr, sizeof(KLDR_DATA_TABLE_ENTRY));
      lpNewLdr->DllBase = NewImage;

      InsertTailList(&PsLoadedModuleList->InLoadOrderLinks, &lpNewLdr->InLoadOrderLinks);
      DbgPrint("KeInsertPsLoadedModuleList:0x%p\n", lpNewLdr);
      return TRUE;
    }
  }
  return FALSE;
}
  在曾想过是否要修正KLDR中的ExceptionTable,但感觉没必要,毕竟指向的函数表里面都是RVA,又不是VA,反正SEH Handler都一样,不改也罢。
  好了这样就解决了SafeSEH所带来的蓝屏。基本上这个驱动就可以拿来在本机测试了。
崩溃问题
  这份代码能够很好的在大部分CPU上进行工作,但有些CPU仍会发生程序崩溃的问题,这与第三部分的文章中提到的打不开程序是不同的问题。我的CPU就是如此,否则我都不会发现这个问题。
  后面的东西绝对原创,网上没有哦。
  一开始遇到这个问题同样使我疯掉了一段时间,通过各种途径找问题,最后奇妙的发现了这里:
名称:  01.jpg
查看次数: 0
文件大小:  25.4 KB
  在干净的系统中,当我使用ARK恢复所有的钩子后,居然出现了我所说的崩溃问题,这就说明了在系统初始化时自己Patch了自己,很明显的大家能够观察到有一个巨大的补丁,那就是一个修改了22字节的东西,这是啥玩意?WinDBG会告诉你的:
代码:
0: kd> uf 83e7dd50
nt!KeFlushCurrentTb:
83e7dd50 0f20e0          mov     eax,cr4
83e7dd53 0fbaf007        btr     eax,7
83e7dd57 7309            jae     nt!KeFlushCurrentTb+0x12 (83e7dd62)

nt!KeFlushCurrentTb+0x9:
83e7dd59 0f22e0          mov     cr4,eax
83e7dd5c 0c80            or      al,80h
83e7dd5e 0f22e0          mov     cr4,eax
83e7dd61 c3              ret

nt!KeFlushCurrentTb+0x12:
83e7dd62 0f20d9          mov     ecx,cr3
83e7dd65 0f22d9          mov     cr3,ecx
83e7dd68 c3              ret
  从字面上不难理解KeFlushCurrentTb是刷新了TB,TB是什么我就不知道了,个人猜测是CPU中TLB与TIB的统称,看代码也知道是刷新了页表。你也可以通过IDA来查看这部分的代码,简言之就是判断了CPU的类型,如果满足了什么条件,则Patch了这个函数。再来看恢复之后的样子:
代码:
1: kd> uf 83e7dd50
nt!KeFlushCurrentTb:
83e7dd50 0f20d8          mov     eax,cr3
83e7dd53 0f22d8          mov     cr3,eax
83e7dd56 c3              ret
  只能说这个函数在不同的CPU上可能会有不同的代码,那如何解决?难道像它一样来判断CPU进而修改这个代码?大可没有必要,我的做法就是定位到这里,然后JMP到原模块的这个地方。我想这里不会被HOOK,没有那么逆天。定位方法我也是利用的重定位,通过找到负责Patch的代码定位KeFlushCurrentTb,再JMP过去:
代码:
VOID PatchKeFlushCurrentTb(PVOID lpNewNtoskrnlAddress, PVOID lpNtoskrnlAddress)
{
  /*
  mov     edi, offset KeFlushCurrentTb
  mov     esi, offset byte_477D57
  mov     ecx, XXX
  rep movsb
  */
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNewNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpDosHeader + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
    USHORT  *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
    ULONG i;

    for (i = 0; i < NumberOfItems - 1; i++)
    {
      if ((lpItem >> 12) == IMAGE_REL_BASED_HIGHLOW && (lpItem[i + 1] >> 12) == IMAGE_REL_BASED_HIGHLOW)
      {
        ULONG *lpFixAddress1 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem & 0x0FFF));
        ULONG *lpFixAddress2 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem[i + 1] & 0x0FFF));

        if ((ULONG)lpFixAddress2 - (ULONG)lpFixAddress1 == 5)
        {
          if (*((PUCHAR)lpFixAddress1 - 1) == 0xBF && *((PUCHAR)lpFixAddress2 - 1) == 0xBE)
          {
            PUCHAR lpCheckBytes = (PUCHAR)lpFixAddress2 + sizeof(PVOID);

            if (lpCheckBytes[0] == 0xB9 && lpCheckBytes[5] == 0xF3 && lpCheckBytes[6] == 0xA4)
            {
              PUCHAR lpPatchAddress = (PUCHAR)*lpFixAddress1;

              lpPatchAddress[0] = 0xE9;
              *(ULONG*)&lpPatchAddress[1] = (ULONG)lpNtoskrnlAddress - (ULONG)lpNewNtoskrnlAddress - 5;
              return;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return;
}
  好了,没事儿了,新的NT内核没问题了,全部搞定,如果仍发现自己解决不了的问题可以联系我,最后的DriverEntry代码:
代码:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
  PVOID lpHookKiFastCallEntryAddress;
  UCHAR HookCode[7];

  DbgPrint("Driver Load.\n");
  InitializePsLoadedModuleList(DriverObject);
  g_lpNtoskrnlAddress = KeGetModuleHandle(PsLoadedModuleList, "ntoskrnl.exe");
  g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
  g_KeServiceTable = (PVOID*)BuildKeServiceTable(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeFixReloc2(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  PatchKeFlushCurrentTb(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  KeInsertPsLoadedModuleList(PsLoadedModuleList, g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
  lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
  HookCode[0] = 0xE8;
  *(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
  HookCode[5] = 0x90;
  HookCode[6] = 0x90;
  RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));

  DriverObject->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}
  这一部分告一段落,接下来继续重载win32k,搞定SHADOW SSDT。
  最后,为啥PCHunter不支持Wwindows 8.1 update 1呢?现在的最新版1.32运行不到一分钟就蓝屏,原因是触发了PatchGuard,望修复,没有ARK用真心难受。

啊冲 2016-02-03 10:45
第五部分:重载GUI内核
  不知道我的叫法对不对,实际上就是重载win32k.sys,有了前面文章的功底,我想重载一个镜像并不难了,只要稍微修改一下ReloadNtModule即可。
代码:
  g_lpWin32kAddress = KeGetModuleHandle(PsLoadedModuleList, "win32k.sys");
  g_lpNewWin32kAddress = ReloadNtModule(PsLoadedModuleList, g_lpWin32kAddress);
  KeFixReloc2(g_lpNewWin32kAddress, g_lpWin32kAddress);
  KeInsertPsLoadedModuleList(PsLoadedModuleList, g_lpNewWin32kAddress, g_lpWin32kAddress);
  这样就完事儿了,是不是很简单?
  接下来去找SHADOW SSDT的原始地址,是存储在一个叫做W32pServiceTable的变量中。大家应该知道所有的系统服务表都是存储在KeServiceDescriptorTableShadow中,最多支持四个表,默认情况下只有两个表,即SSDT与SHADOW SSDT。在系统刚初始化的时候只有一张表,因为此时win32k还没有加载,当它加载的时候会调用KeAddSystemServiceTable来添加第二张服务表,那么思路来了:
名称:  00.jpg
查看次数: 8
文件大小:  14.6 KB
  代码很简单,就是通过重定位找到调用KeAddSystemServiceTable的地方,判断附近代码,找出变量,其实在整个win32k模块只有一处调用了KeAddSystemServiceTable,很好找的。
代码:
PVOID FindW32pServiceTable(PVOID lpWin32kAddress, PVOID lpKeAddSystemServiceTable, ULONG *nApi)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpWin32kAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpWin32kAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    if (lpRelocateTable->VirtualAddress)
    {
      ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
      USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
      ULONG j;

      for (j = 0; j < NumberOfItems; j++)
      {
        if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW)
        {
          PUCHAR lpFixAddress = (PUCHAR)((PCHAR)lpWin32kAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));

          /*
          PUSH _W32pServiceTable
          CALL [KeAddSystemServiceTable]
          */
          if (*(lpFixAddress - sizeof(UCHAR)) == 0x68)
          {
            if (*(PUSHORT)(lpFixAddress + sizeof(PVOID)) == 0x15FF)
            {
              PVOID lpCallAddress = *(PVOID*)*(ULONG*)(lpFixAddress + sizeof(PVOID) + sizeof(USHORT));

              if (lpCallAddress == lpKeAddSystemServiceTable)
              {
                *nApi = *(ULONG*)*(PVOID*)((PCHAR)lpFixAddress - 11);
                return (PVOID)*(ULONG_PTR*)lpFixAddress;
              }
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}

PVOID BuildW32pServiceTable(PVOID lpNtoskrnlAddress, PVOID lpNewWin32kAddress)
{
  PVOID lpW32pServiceTable = NULL;
  PVOID lpNewW32pServiceTable = NULL;
  PVOID lpKeAddSystemServiceTable = KeGetProcAddress(lpNtoskrnlAddress, "KeAddSystemServiceTable");
  ULONG nApi;

  if (lpW32pServiceTable = FindW32pServiceTable(lpNewWin32kAddress, lpKeAddSystemServiceTable, &nApi))
  {
    if (lpNewW32pServiceTable = ExAllocatePool(NonPagedPool, nApi * sizeof(PVOID)))
    {
      RtlCopyMemory(lpNewW32pServiceTable, lpW32pServiceTable, nApi * sizeof(PVOID));
    }
  }
  return lpNewW32pServiceTable;
}
  接下来修改一下过滤函数:
代码:
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
  if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
  {
    return g_KeServiceTable[ServiceIndex];
  }
  else
  {
    return g_GuiServiceTable[ServiceIndex];
  }
}
  至此,已经重载了两个内核模块并绕过了NATIVE SSDT/SHADOW SSDT中的函数表HOOK以及INLINE HOOK,代码还有很多不完善的地方,有待大家去找出并修复,其实我也不喜欢拿来主义者和伸手党,仅仅是希望对这一块有兴趣的朋友有所帮助。
  感谢topofall指出代码中的问题。
  基本上告一段落,但还是有一些东西没有告诉大家,我想还会写一些后续文章的。
  待续。

啊冲 2016-02-03 10:45

第六部分:IDT HOOK
  对我来说IDT一直是一个很难理解的东西,尤其是段选择子什么的,这里我不敢教大家如何HOOK IDT表,我只会INLINE HOOK,囧。再说每一个CPU都有各自独立的IDT表,做HOOK的话每一个CPU都要处理,也很麻烦。
  为什么要HOOK IDT?除了正常的服务请求会经过KiFastCallEntry,那么不正常的就只能走中断门了,比如DEBUG,BREAKPOINT等等,我想这是大家有兴趣的地方。
  和之前一样,对于大部分函数表来说,在NT内核中都会有一个对应的地方来存储原始地址,初始化函数是在KiSystemStartup中,但是WRK里并没有源代码,被可恶的编译成了lib文件,所以来看IDA吧:
名称:  00.jpg
查看次数: 8
文件大小:  12.0 KB
名称:  01.jpg
查看次数: 0
文件大小:  28.6 KB
  显然这里的77E164也可以理解为是一个函数表,里面存储了中断门的线性地址,起个名字吧,我自己叫做KiTrapTable,不知道对不对。有一个特点就是这个地址表并不是元素连续的,每隔一个才是地址,也就是中断函数地址=函数表[索引*2],至于另外一个元素是什么我就不敢断言了,老办法,利用重定位来搜索:
代码:
PVOID FindKiTrapTable(PVOID lpNtoskrnlAddress)
{
  IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNtoskrnlAddress;
  IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
  IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpNtoskrnlAddress + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

  while (lpRelocateTable->SizeOfBlock)
  {
    if (lpRelocateTable->VirtualAddress)
    {
      ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
      USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
      ULONG j;

      for (j = 0; j < NumberOfItems; j++)
      {
        if ((lpItem[j] >> 12) == IMAGE_REL_BASED_HIGHLOW)
        {
          PUCHAR lpFixAddress = (PUCHAR)((PCHAR)lpNtoskrnlAddress + lpRelocateTable->VirtualAddress + (lpItem[j] & 0x0FFF));

          /*
          MOV  ESI, OFFSET KiTrapTable
          MOV  ECX, 0x0800
          SHR  ECX, 2
          REP  MOVSD
          */
          if (*(lpFixAddress - sizeof(UCHAR)) == 0xBE)
          {
            UCHAR CodeBytes[] = {0xB9, 0x00, 0x08, 0x00, 0x00, 0xC1, 0xE9, 0x02, 0xF3, 0xA5};

            if (RtlEqualMemory(lpFixAddress + sizeof(PVOID), CodeBytes, sizeof(CodeBytes)))
            {
              return (PVOID)*(ULONG*)lpFixAddress;
            }
          }
        }
      }
    }
    lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
  }
  return NULL;
}

PVOID BuildKiTrapTable(PVOID lpNtoskrnlAddress)
{
  PVOID lpKiTrapTable = NULL;
  PVOID lpNewKiTrapTable = NULL;

  if (lpKiTrapTable = FindKiTrapTable(lpNtoskrnlAddress))
  {
    if (lpNewKiTrapTable = ExAllocatePool(NonPagedPool, sizeof(PVOID) * 0xFF * 2))
    {
      RtlCopyMemory(lpNewKiTrapTable, lpKiTrapTable, sizeof(PVOID) * 0xFF * 2);
      DbgPrint("BuildKiTrapTable:%08X\n", lpKiTrapTable);
    }
  }
  return lpNewKiTrapTable;
}
  找到原始地址,INLINE HOOK就非常简单了,直接在函数头写JMP到新内核中或者额外处理什么,这里我就不再贴代码了,节省篇幅。
  其实HOOK IDT并没有什么很大的意义,毕竟正常的内核调用都是调用sysenter的,也是HOOK竞争最激烈的地方,把握住了这里,就可以绕过绝大部分的钩子。
  当然对于反反调试,接管IDT中的调试相关中断还是很有用的,具体怎么做那就不是我的事儿了,大家各自发挥,对症下药吧。
  也许还有后续内容,也许直接是一个总结,希望大家能够在技术上得到真正的提高,不论大家重载内核的目的纯洁与否,学到东西才是最长久的。也不要认为x86在远离我们就开始忽视,其实x64也是建立在x86的基础上,变化不是很大,对于底层要进行革命性的变化还要好久好久。

啊冲 2016-02-03 10:45
恶意软件反检测技术简介:反调试技术解析 (1)
在上篇中,我们会为读者介绍恶意软件常用来的反仿真技术。本文中,我们将向读者介绍恶意软件用以阻碍对其进行逆向工程的各种反调试技术,以帮助读者很好的理解这些技术,从而能够更有效地对恶意软件进行动态检测和分析。

一、反调试技术

反调试技术是一种常见的反检测技术,因为恶意软件总是企图监视自己的代码以检测是否自己正在被调试。为做到这一点,恶意软件可以检查自己代码是否被设置了断点,或者直接通过系统调用来检测调试器。

1.断点

为了检测其代码是否被设置断点,恶意软件可以查找指令操作码0xcc(调试器会使用该指令在断点处取得恶意软件的控制权),它会引起一个SIGTRAP。如果恶意软件代码本身建立了一个单独的处理程序的话,恶意软件也可以设置伪断点。用这种方法恶意软件可以在被设置断点的情况下继续执行其指令。

恶意软件也可以设法覆盖断点,例如有的病毒采用了反向解密循环来覆盖病毒中的断点。相反,还有的病毒则使用汉明码自我纠正自身的代码。汉明码使得程序可以检测并修改错误,但是在这里却使病毒能够检测并清除在它的代码中的断点。

2.计算校验和

恶意软件也可以计算自身的校验和,如果校验和发生变化,那么病毒会假定它正在被调试,并且其代码内部已被放置断点。VAMPiRE是一款抗反调试工具,可用来逃避断点的检测。VaMPiRE通过在内存中维护一张断点表来达到目的,该表记录已被设置的所有断点。该程序由一个页故障处理程序(PFH),一个通用保护故障处理程序(GPFH),一个单步处理程序和一个框架API组成。当一个断点被触发的时候,控制权要么传给PFH(处理设置在代码、数据或者内存映射I/O中的断点),要么传给GPFH(处理遗留的I/O断点)。单步处理程序用于存放断点,使断点可以多次使用。

3.检测调试器

在Linux系统上检测调试器有一个简单的方法,只要调用Ptrace即可,因为对于一个特定的进程而言无法连续地调用Ptrace两次以上。在Windows中,如果程序目前处于被调试状态的话,系统调用isDebuggerPresent将返回1,否则返回0。这个系统调用简单检查一个标志位,当调试器正在运行时该标志位被置1。直接通过进程环境块的第二个字节就可以完成这项检查,以下代码为大家展示的就是这种技术:

mov eax, fs:[30h]
move eax, byte [eax+2]
test eax, eax    
jne @DdebuggerDetected
在上面的代码中,eax被设置为PEB(进程环境块),然后访问PEB的第二个字节,并将该字节的内容移入eax。通过查看eax是否为零,即可完成这项检测。如果为零,则不存在调试器;否则,说明存在一个调试器。

如果某个进程为提前运行的调试器所创建的,那么系统就会给ntdll.dll中的堆操作例程设置某些标志,这些标志分别是FLG_HEAP_ENABLE_TAIL_CHECK、FLG_HEAP_ENABLE_FREE_CHECK和FLG_HEAP_VALIDATE_PARAMETERS。我们可以通过下列代码来检查这些标志:

mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne @DebuggerDetected
在上面的代码中,我们还是访问PEB,然后通过将PEB的地址加上偏移量68h到达堆操作例程所使用的这些标志的起始位置,通过检查这些标志就能知道是否存在调试器。

检查堆头部内诸如ForceFlags之类的标志也能检测是否有调试器在运行,如下所示:

mov eax, fs:[30h]
mov eax, [eax+18h] ;process heap
mov eax, [eax+10h] ;heap flags
test eax, eax
jne @DebuggerDetected
上面的代码向我们展示了如何通过PEB的偏移量来访问进程的堆及堆标志,通过检查这些内容,我们就能知道Force标志是否已经被当前运行的调试器提前设置为1了。

另一种检测调试器的方法是,使用NtQueryInformationProcess这个系统调用。我们可以将ProcessInformationClass设为7来调用该函数,这样会引用ProcessDebugPort,如果该进程正在被调试的话,该函数将返回-1。示例代码如下所示。

push 0
push 4
push offset isdebugged
push 7 ;ProcessDebugPort
push -1
call NtQueryInformationProcess
test eax, eax
jne @ExitError
cmp isdebugged, 0
jne @DebuggerDetected


在本例中,首先把NtQueryInformationProcess的参数压入堆栈。这些参数介绍如下:第一个是句柄(在本例中是0),第二个是进程信息的长度(在本例中为4字节),接下来是进程信息类别(在本例中是7,表示ProcessDebugPort),下一个是一个变量,用于返回是否存在调试器的信息。如果该值为非零值,那么说明该进程正运行在一个调试器下;否则,说明一切正常。最后一个参数是返回长度。使用这些参数调用NtQueryInformationProcess后的返回值位于isdebugged中。随后测试该返回值是否为0即可。

另外,还有其他一些检测调试器的方法,如检查设备列表是否含有调试器的名称,检查是否存在用于调试器的注册表键,以及通过扫描内存以检查其中是否含有调试器的代码等。

另一种非常类似于EPO的方法是,通知PE加载器通过PE头部中的线程局部存储器(TLS)表项来引用程序的入口点。这会导致首先执行TLS中的代码,而不是先去读取程序的入口点。因此,TLS在程序启动就可以完成反调试所需检测。从TLS启动时,使得病毒得以能够在调试器启动之前就开始运行,因为一些调试器是在程序的主入口点处切入的。

啊冲 2016-02-03 10:45

4.探测单步执行

恶意软件还能够通过检查单步执行来检测调试器。要想检测单步执行的话,我们可以把一个值放进堆栈指针,然后看看这个值是否还在那里。如果该值在那里,这意味着,代码正在被单步执行。当调试器单步执行一个进程时,当其取得控制时需要把某些指令压入栈,并在执行下一个指令之前将其出栈。所以,如果该值仍然在那里,就意味着其它正在运行的进程已经在使用堆栈。下面的示例代码展示了恶意软件是如何通过堆栈状态来检测单步执行的:

Mov bp,sp;选择堆栈指针
Push ax ;将ax压入堆栈
Pop ax ;从堆栈中选择该值
Cmp word ptr [bp -2],ax ;跟堆栈中的值进行比较
Jne debug ;如果不同,说明发现了调试器。  
如上面的注释所述,一个值被压入堆栈然后又被弹出。如果存在调试器,那么堆栈指针–2位置上的值就会跟刚才弹出堆栈的值有所不同,这时就可以采取适当的行动。

5.在运行时中检测速度衰减

通过观察程序在运行时是否减速,恶意代码也可以检测出调试器。如果程序在运行时速度显著放缓,那就很可能意味着代码正在单步执行。因此如果两次调用的时间戳相差甚远,那么恶意软件就需要采取相应的行动了。Linux跟踪工具包LTTng/LTTV通过观察减速问题来跟踪病毒。当LTTng/LTTV追踪程序时,它不需要在程序运行时添加断点或者从事任何分析。此外,它还是用了一种无锁的重入机制,这意味着它不会锁定任何Linux内核代码,即使这些内核代码是被跟踪的程序需要使用的部分也是如此,所以它不会导致被跟踪的程序的减速和等待。

6.指令预取

如果恶意代码篡改了指令序列中的下一条指令并且该新指令被执行了的话,那么说明一个调试器正在运行。这是指令预取所致:如果该新指令被预取,就意味着进程的执行过程中有其他程序的切入。否则,被预取和执行的应该是原来的指令。

7.自修改代码

恶意软件也可以让其他代码自行修改(自行修改其他代码),这样的一个例子是HDSpoof。这个恶意软件首先启动了一些异常处理例程,然后在运行过程中将其消除。这样一来,如果发生任何故障的话,运行中的进程会抛出一个异常,这时病毒将终止运行。此外,它在运行期间有时还会通过清除或者添加异常处理例程来篡改异常处理例程。在下面是HDSpoof清除全部异常处理例程(默认异常处理例程除外)的代码。

exception handlers before:


0x77f79bb8 ntdll.dll:executehandler2@20 + 0x003a
0x0041adc9 hdspoof.exe+0x0001adc9
0x77e94809 __except_handler3


exception handlers after:


0x77e94809 __except_handler3


0x41b770: 8b44240c       mov      eax,dword ptr [esp+0xc]
0x41b774: 33c9           xor      ecx,ecx               
0x41b776: 334804         xor      ecx,dword ptr [eax+0x4]
0x41b779: 334808         xor      ecx,dword ptr [eax+0x8]
0x41b77c: 33480c         xor      ecx,dword ptr [eax+0xc]
0x41b77f: 334810         xor      ecx,dword ptr [eax+0x10]
0x41b782: 8b642408       mov      esp,dword ptr [esp+0x8]
0x41b786: 648f0500000000 pop      dword ptr fs:[0x0]    


下面是HDSpoof创建一个新的异常处理程序的代码。

0x41f52b: add      dword ptr [esp],0x9ca
0x41f532: push     dword ptr [dword ptr fs:[0x0]
0x41f539: mov      dword ptr fs:[0x0],esp
8.覆盖调试程序信息

一些恶意软件使用各种技术来覆盖调试信息,这会导致调试器或者病毒本身的功能失常。通过钩住中断INT 1和INT 3(INT 3是调试器使用的操作码0xCC),恶意软件还可能致使调试器丢失其上下文。这对正常运行中的病毒来说毫无妨碍。另一种选择是钩住各种中断,并调用另外的中断来间接运行病毒代码。

下面是Tequila 病毒用来钩住INT 1的代码:

new_interrupt_one:

   push bp
   mov bp,sp
   cs cmp b[0a],1      ;masm mod. needed
   je 0506             ;masm mod. needed
   cmp w[bp+4],09b4
   ja 050b             ;masm mod. needed
   push ax
   push es
   les ax,[bp+2]
   cs mov w[09a0],ax   ;masm mod. needed
   cs mov w[09a2],es   ;masm mod. needed
   cs mov b[0a],1
   pop es
   pop ax
   and w[bp+6],0feff
   pop bp
   iret

一般情况下,当没有安装调试器的时候,钩子例程被设置为IRET。V2Px使用钩子来解密带有INT 1和INT 3的病毒体。在代码运行期间,会不断地用到INT 1和INT 3向量,有关计算是通过中断向量表来完成的。

一些病毒还会清空调试寄存器(DRn的内容。有两种方法达此目的,一是使用系统调用NtGetContextThread和NtSetContextThread。而是引起一个异常,修改线程上下文,然后用新的上下文恢复正常运行,如下所示:

push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
xor eax, eax
div eax ;generate exception
pop fs:[0]
add esp, 4
;continue execution
;...
handler:
mov ecx, [esp+0Ch] ;skip div
add dword ptr [ecx+0B8h], 2 ;skip div
mov dword ptr [ecx+04h], 0 ;clean dr0
mov dword ptr [ecx+08h], 0 ;clean dr1
mov dword ptr [ecx+0Ch], 0 ;clean dr2
mov dword ptr [ecx+10h], 0 ;clean dr3
mov dword ptr [ecx+14h], 0 ;clean dr6
mov dword ptr [ecx+18h], 0 ;clean dr7
xor eax, eax
ret
上面的第一行代码将处理程序的偏移量压入堆栈,以确保当异常被抛出时它自己的处理程序能取得控制权。之后进行相应设置,包括用自己异或自己的方式将eax设为0,以将控制权传送给该处理程序。div eax 指令会引起异常,因为eax为0,所以AX将被除以零。该处理程序然后跳过除法指令,清空dr0-dr7,同样也把eax置0,表示异常将被处理,然后恢复运行。

啊冲 2016-02-03 10:45

9.解除调试器线程

我们可以通过系统调用NtSetInformationThread从调试器拆卸线程。为此,将ThreadInformationClass设为0x11(ThreadHideFromDebugger)来调用NtSetInformationThread,如果存在调试器的话,这会将程序的线程从调试器拆下来。以下代码就是一个例子:

push 0
push 0
push 11h ;ThreadHideFromDebugger
push -2
call NtSetInformationThread
在本例中,首先将NtSetInformationThread的参数压入堆栈,然后调用该函数来把程序的线程从调试器中去掉。这是因为这里的0用于线程的信息长度和线程信息,传递的-2用于线程句柄,传递的11h用于线程信息类别,这里的值表示ThreadHideFromDebugger。

10.解密

解密可以通过各种防止调试的方式来进行。有的解密依赖于特定的执行路径。如果这个执行路径没被沿用,比如由于在程序中的某个地方启动了一个调试器,那么解密算法使用的值就会出错,因此程序就无法正确进行自身的解密。HDSpoof使用的就是这种技术。

一些病毒使用堆栈来解密它们的代码,如果在这种病毒上使用调试器,就会引起解密失败,因为在调试的时候堆栈为INT 1所用。使用这种技术的一个例子是W95/SK病毒,它在堆栈中解密和构建其代码;另一个例子是Cascade病毒,它将堆栈指针寄存器作为一个解密密钥使用。代码如下所示:

lea   si, Start   ; position to decrypt
mov   sp, 0682  ; length of encrypted body

Decrypt:

xor   [si], si    ; decryption key/counter 1
xor   [si], sp  ; decryption key/counter 2
inc   si    ; increment one counter
dec   sp    ; decrement the other
jnz   Decrypt   ; loop until all bytes are decrypted
Start:            ; Virus body

对于Cascade病毒如何使用堆栈指针来解密病毒体,上面代码中的注释已经做了很好的说明。相反,Cryptor病毒将其密钥存储在键盘缓冲区中,这些密钥会被调试器破坏。Tequila使用解密器的代码作为解密钥,因此如果解密器被调试器修改后,那么该病毒就无法解密了。下面是Tequila用于解密的代码:

perform_encryption_decryption:

   mov bx,0
   mov si,0960
   mov cx,0960
  mov dl,b[si]
   xor b[bx],dl
   inc si
   inc bx
   cmp si,09a0
   jb 0a61             ;masm mod. needed
   mov si,0960
   loop 0a52           ;masm mod. needed
   ret

the_file_decrypting_routine:

   push cs
   pop ds
   mov bx,4
   mov si,0964
   mov cx,0960
   mov dl,b[si]
   add b[bx],dl
   inc si
   inc bx
   cmp si,09a4
   jb 0a7e             ;masm mod. needed
   mov si,0964
   loop 0a6f           ;masm mod. needed
   jmp 0390            ;masm mod. needed


人们正在研究可用于将来的新型反调试技术,其中一个项目的课题是关于多处器计算机的,因为当进行调试时,多处理器中的一个会处于闲置状态。这种新技术使用并行处理技术来解密代码。

二、逆转录病毒

逆转录病毒会设法禁用反病毒软件,比如可以通过携带一列进程名,并杀死正在运行的与表中同名的那些进程。许多逆转录病毒还把进程从启动列表中踢出去,这样该进程就无法在系统引导期间启动了。这种类型的恶意软件还会设法挤占反病毒软件的CPU时间,或者阻止反病毒软件连接到反病毒软件公司的服务器以使其无法更新病毒库。

三、混合技术

W32.Gobi病毒是一个多态逆转录病毒,它结合了EPO和其他一些反调试技术。该病毒还会在TCP端口666上打开一个后门。

Simile(又名Metaphor)是一个非常有名的复合型病毒,它含有大约14,000行汇编代码。这个病毒通过寻找API调用ExitProcess()来使用EPO,它还是一个多态病毒,因为它使用多态解密技术。它的90%代码都是用于多态解密,该病毒的主体和多态解密器在每次感染新文件时,都会放到一个半随机的地方。Simile的第一个有效载荷只在3月、6月、9月或12月份才会激活。在这些月份的17日变体A和B显示它们的消息。变体C在这些月份的第18日显示它的消息。变体A和B中的第二个有效载荷只有在五月14日激活,而变体C中的第二个有效载荷只在7月14日激活。

Ganda是一个使用EPO的逆转录病毒。它检查启动进程列表,并用一个return指令替换每个启动进程的第一个指令。这会使所有防病毒程序变得毫无用处。

四、小结

本文中,我们介绍了恶意软件用以阻碍对其进行逆向工程的若干反调试技术,同时介绍了逆转录病毒和各种反检测技术的组合。我们应该很好的理解这些技术,只有这样才能够更有效地对恶意软件进行动态检测和分析。

啊冲 2016-02-03 10:46

恶意软件反检测技术简介:模拟器超限技术
恶意软件是一种招人恨的代码,因为它们专干坏事,如泄漏个人隐私、造成数据丢失,等等。而杀毒软件公司则不断想办法检测并阻止恶意软件。如此一来,猫和老鼠的大戏从此开演了。一般来说,杀毒软件要想防御某种恶意软件需要经过以下过程:收集到样本,分析样本,升级病毒库,之后杀毒软件才能够识别该恶意软件。而反检测技术,就是在恶意软件的分析阶段设置障碍,让分析人员无法或难以对恶意代码进行分析,这主要包括两类技术,一种是反调试技术,一种是反仿真技术,或叫做反虚拟执行技术。

无论是处于分析恶意软件的目的,还是防止软件被分析的目的,了解恶意软件常用的反检测手段都是很有必要的,而本文的目的就是在于,向读者们介绍目前的反调试和反仿真技术,并提供代码样本,以供读者在识别恶意代码时作为练手之用。

一、引言

病毒作者使用反调试与反仿真技术的目的在于为逆向分析恶意软件制造障碍,理想的情况下是使得逆向工程师无法分析恶意软件,退一步讲即使可以分析也会让分析过程更为缓慢。当病毒在一个仿真器或调试器中运行时,这些技术会使逆向工程过程变得举步维艰,他们企图以此逃避检查。恶意代码可以利用多种不同的方法来“忽悠”动态检测和其他如仿真器和调试器之类的分析机制,通常情况下每种病毒都会采用其中的一种甚至多种。

对于这些病毒作者用于使对病毒的逆向过程变缓的方法,本文将分别进行讲解,并为它们提供了示例。本文会概述各种反调试与反仿真技术,以使得读者对它们有所了解。下面我们开始介绍反仿真技术。

二、反仿真技术

仿真器为人们提供了一个受限制的环境(即安装在主机操作系统之上的操作系统的映像),我们可以在这个环境中动态地分析程序。例如,一台运行Linux操作系统的机器可以在其虚拟机上安装并运行Windows XP系统。一个仿真器也将包括对CPU和内存的仿真,以及其它硬件的仿真,还有控制器的仿真。这为我们提供了一种安全的方式来动态地分析程序,因为这种环境下,无论做什么都不会对底层的操作系统造成损害。

然而,这一系统也有其缺点。首先,花在仿真上的时间越多,在对目标程序进行实际分析之前所必须等待的时间也就相应越长。其次,仿真过程是缓慢的,因为不仅对目标程序实施监控需要开销,而且对操作系统和硬件进行仿真同样也需要开销。QEMU,一种硬件和操作系统仿真器,其仿真出来的硬件比物理硬件的速度有四分之一到十分之一之间的下降,软件在其上运行的速度只是在内存管理单元上运行时的二分之一。同时,因为QEMU不具备任何动态监控功能,这会进一步降低速度。最后一个缺点是,恶意软件可以利用各种反仿真技术来“忽悠”模拟器,这些技术分为三类:比耐力型、比智力型以及过度扩展模拟器。

三、跟模拟器比耐力

因为仿真的代价较高,所以模拟器通常只运行代码的前几百条指令来检测恶意软件,后面的指令通常不予理会。在英特尔的X86系统上,人们普遍相信,只要运行1000条指令便足以检测出恶意代码,同时还能保持较短的运行时间。如果在这段时间内没有运行被视为恶意的指令,那么模拟器就不会再将该代码作为恶意软件而继续检查。跟模拟器比耐力的方法有很多,如基于特定概率的感染技术,在运行恶意代码之前先将良性代码运行特定的时间,或通过入口点迷惑技术来达到目的。

1.概率式感染

一些恶意软件的恶意代码并不是每次都会执行,而是按照一定的概率发作。例如,某病毒在运行时执行恶意代码的概率是10%,这就意味着,模拟器将其运行若干次,才有可能检测到恶意代码。

2.运行良性代码

有的恶意软件会在每次启动时先运行良性代码,并且良性代码运行一段时间后,才开始运行恶意代码。这样做的目的是让模拟器运行指定数量的指令,让它觉得该代码在这段时间内没有做恶意的事情。等危险期过后,它就会露出本来面目:感染并危害系统。

3.入口点迷惑技术

入口点迷惑技术(EPO)是一种将附带的恶意代码放到一个文件的特定的部分的方法。恶意软件不会一上来就执行恶意代码,相反,它能查找对ExitProcess()API函数的调用,然后用一个跳至恶意代码的转移指令来覆盖这些调用。用这种方法,代码将在可执行文件的出口处,而非入口处运行。它还可以寻找一个特定的代码序列,然后用恶意代码本身或跳至恶意代码的转移指令来覆盖之。所以,入口点迷惑技术后,能够使病毒在将代码拷贝至新的位置之后、实际覆盖它之前运行恶意代码。

四、跟模拟器比智力

为了智取模拟器,一些恶意软件使用基于时间的触发器,或者其它的条件转移,或者使用不同的解密技术(如果它是采用加密的恶意软件的话)。基于时间的触发器可以只有在下午3点或指定的日期运行恶意代码。恶意软件也可以检测某些条件是否成立,如查看自身是否正在被调试或仿真,如果条件成立,则执行良性代码。

解密技术也可以将解密循环分布到代码的各处,或者使用多轮解密技术。多轮解密技术已经被W32/Harrier、W32/Coke和W32/Zelly用过了,其中第一个解密器解密第二个解密器,第二个解密器又解密第三个解密器,以此类推。为了靠小聪明玩弄模拟器,恶意软件也可以仅仅解密所需的代码块。

RDA.Fighter病毒使用蛮力解密技术,这意味着,该病毒不会储存解密密钥,所以它必须尝试所有可能的解密密钥,以便解密本身。这是非常有用的,因为如果反病毒公司使用了不同的蛮力解密方法的话,对病毒解密是非常困难的,它需要大量的仿真指令。还有一种可能,就是只要模拟器正在运行,病毒就不对自身进行解密。下面是病毒RDA Fighter用于解密的代码。

setup:
        xor ebx,ebx

iterate:

       mov esi,[ebp + hostOffset]
       mov edi,esi
       mov ecx,[ebp + host_size]
       inc ebx

decrypt:

       lodsb
       xor al,bl
       stosb
       loop decrypt

check:

       mov esi,[ebp + hostOffset]
       push esi
       mov ecx,[ebp + host_size]
       push ecx
       mov eax,[ebp + __ADDR_CheckSum]  ; whatever this happens to be
       call eax
       test eax,eax
       jnz iterate
       mov esi,[ebp + hostOffset]
       jmp esi

在上面的例子中,我们看到setup段将ebx设为0,并且它只运行一次。然后,在循环部分将esi和edi设为被加密的代码的起点,将ecx设为被加密的代码的尺寸,ebx每次递增1。用于解密的基本块会逐字节遍历被加密的代码,并用ebx中的密钥来异或每一字节。当ecx 变成零时,循环终止。之后,由负责检查的代码段来查看当前的代码是否跟预设的校验和是否匹配,如果匹配的话,那么说明代码已经成功解密。如果不匹配,则返回迭代循环并再次尝试下一个密码。如果已经解密,那么它会跳转到新解密代码处。

W95/Fono使用非线性解密算法,即病毒的加密部分是无法使用通常的线性方式来解密的。因为病毒无法一个字节接一个字节地解密,所以它能够迷惑仿真器。W95/Fono使用一个密钥表和解密器来完成基于该表的置换。明文字母中的每个符号对应于密文中的另一个不同的符号。例如:A和L相对应,Z和F相对应,等等。因此,使用这个加密方法后,病毒的各个部分是以半随机的顺序解密的,并且每个位置仅出现一次。此外,W95/Drill和(W32,Linux)/Simile.D也使用非线性解密方法。

W95/Silcer和W95/Resure装入内存时,它们强迫Windows的加载器对受感染的程序的映像进行重定位。映像的重定位处理决定了病毒体的解密处理,因为病毒解密时必须进行特定的重定位处理。W95/Resurrel是继W95/Resure之后出现的一种病毒,该病毒会把刚感染的文件的基地址设为0xBFxxxxxx(其中XXXXXX是一个由API调用GetTickCount()返回的随机值)。然后,它为自己代码部分的每个DWORD值添加一个重定位表项,并对每个DWORD进行加密,方法是将代码部分的DWORD加上基址值,然后减去0x400000。当应用程序被执行时,会因为基地址错误,或者一个地址位于KERNEL32.DLL中而导致程序的映像无法照现在的样子装入内存。这样,系统装入程序就需要把代码重定位到一个有效地址,这实际上也是病毒的解密工作要做的事情。记住,病毒越难以解密,检测起来就越难。

五、模拟器超限技术

所谓模拟器超限技术,是指执行一组将导致仿真器崩溃或可以表明仿真器正在运行的指令。调用仿真器不支持的未公开指令就是一种导致仿真器掷出异常并停止运行的方法。W95/Vulcano就是这样一个例子,它使用了非正式的CPU指令SALC。用来检测仿真或导致正在运行的仿真器崩溃(如果有的话)的另一种方法是,尝试一次访问大量内存,大到什么程度呢?如果机器上安装了2G内存,那么你就一次访问1G以上的内存。这通常是不能有效执行的,因为大多数的操作系统,以及仿真器,将阻止程序的这种行为。

检测是否正在运行仿真器的一种方法是,将每次调用都返回不同的值的函数调用两次,例如,我们可以对任何时间函数调用两次,然后检查两个返回值之差的大小。在Windows系统下,我们可以通过kernel32!QueryPerformanceCounter(包装有ZwQueryPerformaceCounter),kernel32!GetTickCounter达到上述目的;或通过RDTSC(读时间戳计数器)指令查询自从机器启动以来目前已经执行的机器周期数也能达到目的。下面是一个例子:

push offset handler
push dword ptr fs:[0]
mov fs:[0],esp
rdtsc
push eax
xor eax, eax
div eax ;trigger exception
rdtsc
sub eax, [esp] ;ticks delta
add esp, 4
pop fs:[0]
add esp, 4
cmp eax, 10000h ;threshold
jb @not_debugged
@debugged:
...
@not_debugged:
...
handler:
mov ecx, [esp+0Ch]
add dword ptr [ecx+0B8h], 2 ;skip div
xor eax, eax
ret

上例向我们展示了如何利用RDTSC指令来检测是否存在调试器。如果存在的话,RDTSC被调用两次后返回的值会有变化。然后用这个差异跟一个阈值(在本例中它是10000h)进行比较。如果差异较大,那么就表明存在调试器,所以它会执行一个跳离恶意代码的jump指令。

虽然导入各种迷惑性的程序库不能导致仿真器停止运行,但是如果不导入这些库,恶意软件本身代码就无法运行。

恶意软件还可以寻找网页,看看它是否可以使用互联网。这样会导致当病毒的代码所在机器当前没有连接到互联网的时候,它就不执行;另外,当仿真器运行时病毒的代码也会停止运行,因为大多数仿真器不允许访问互联网。

使用协处理器浮点运算单元(FPU)指令是另一种过分扩展仿真器的方法,因为大部分仿真器都不会仿真FPU指令。Prizzy polymorphic engine (PPE)能够产生43条不同的协处理器指令供其多态解密器使用。如果不提供这些FPU指令的话,就无法对Prizzy进行解密。

同理,恶意软件也可以使用MMX指令。这个指令集为X86架构新增了了8个寄存器。恶意软件可以通过CPUID指令检查是否支持MMX。使用这种技术恶意软件的例子是W32/Legacy和W32/Thorin。

恶意软件还可以设置一个异常处理程序,执行一些无用的代码块,然后间接执行自己的处理程序,以将控制权传送到多态解密器的另一部分。这一诡计之所以得逞,是因为仿真器不能处理异常。3.7部分给出了类似于这种技术的例子。

六、小结

本文中,我们介绍了恶意软件用以阻碍对其进行逆向工程的若干技术,并着重讲解了当前恶意软件常用的反仿真技术。我们应该很好的理解这些技术,以便能够更有效地对恶意软件进行动态检测和分析。在下篇中,我们会为读者介绍恶意软件常用来的反调试技术。

啊冲 2016-02-03 10:46

当PE文件经过某种手工修改后 会导致OD无法识别该文件PE文件
于是构成反调试


下面通过使用OD调试OD找到OD代码不合理的地方 修改之
由于OD里面空闲的空间很少 所以我不得不修改了某些看似无用的代码 来存放我的代码
当然也可以加节什么的 如果大家使用中出现了情况 
请将样本PE文件 发于我邮箱:[email protected] 
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
第一种BUG OD在判断NT选项头时候 不是读取数据目录的个数 在动态的进行计算的
而是直接判断SizeOfOptionHeader是否为E0 
而数据目录是最小可以指定为2项 最多就没有试过是多少了

总而言之 数据目录是可以动态变化的 于是选项头也就是变长的了
所以OD判断选项头是否为E0是错误滴...
所以.....


第二个BUG是 在读取导入表名称的时候 没有检查长度 如果在导入表中构造一个超级长的DLL名称 比如大于256个字节 OD具体怎么做的 我没有怎么调试 
这个也是可以构成反调试的 这个BUG 我只是粗略的跟踪了一下 
找到了一个关键点 并修复之 使用中可能会出现BUG

啊冲 2016-02-03 10:46

修改程序PE头手工实现反调试
1、实验目的:了解手工实现修改程序PE头的原理、实修改程序PE头手工实现反调试与免杀
前提:特征码经过准确定位之后,用此方法可完成反调试,有时可以实现免杀
(1)PE加载到内存的过程
PE指Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行体文件格式。PE结构如图1,最开始的部分是 MZP区,然后是DOS stub区(即注释区,此区域只对DOS命令行式下执行的可执行文件才生效),然后是
表1:PE结构
当一个可执行文件被加载到内存中执行时,内存首先会读取 DOS MZ header区(即MZP区)的内容,当读到PE文件头(PE header)所在位置(DOS MZ header 里的 PE header 偏移量)时,会跳过DOS stub区域,直接跳至PE header所在区域,并由PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header的尾部。PE装载器再读取节表中的节信息,并采用文件映射方法将这些节映射到内存,同时附上节表里指定的节属性。 PE文件映射入内存后,PE装载器将处理PE文件中类似 import table(引入表)逻辑部分。
(2)修改程序PE头实现反调试的原理
因为现在90%以上的程序都是视窗界面(Win32环境下)的运行的,所以DOS stub区域不会被加载到内存当中的。所以可以把PE区段上移至DOS stub区段,再修改PE区段中的关联设置,保证其有效性。只要程序的有效性不被破坏,程序便可正常运行。这样可以在不影响程序正常运行的前提下简单实现反调试。
2、实验步骤
先在高级语言环境下编制一个程序,并生成jqm.exe程序。
(1)用ollydbg将jqm.exe程序加载,如图1:第一条语句所在的地址单元为00467938,对应的反汇编语句为push ebp,它是程序的入口点。

(2)用十六进制文本编辑器c32asm加载jqm.exe,如图2。图中最左列为相对位移,中间列为相对位移的地址单元所对应的十六进制数据,最右列为这些十六进制数据所对应的ASCII字符。最右列中有“MZP”标志的为MZP区段,有“This program”标志的为DOS stub(即注释区段),有“PE”标志的为PE区段。可以看出:PE区段的起始位置为00000100,它在MZP区段中表示为00 01 00 00(因实验机采用的是intel的CPU,其数据是以倒序方式存贮,所以用00 01 00 00来表示00000100)。
图2
(3)确定PE可选头大小:找到相对位移为00000100处(PE区段的起始位置)。找到它所对应的ASCII字符,距离“PE”字样的下一行出现“?”,它表示可选头的大小,对应的十六进制数据为 e0 00(读成00e0),这个十六进进制数据相当于十进制中的224,如图3:

(4)找出PE可选头的有效区域:“?”后的第4位光标处为可选头的起始位置,它的坐标值为00000118(从 “?”后至第4位之前为区段标志)。从00000118起至选224字节至000001f8处结束,选中的区域为PE可选头的区域。如图4:
图4
(5)移动PE区段:将光标从相对位移00000100处选起至000001f8处结束(此处为PE区段),鼠标右击—拷贝。光标移至00000050处(此处作为新PE区段的起始位置),从此处选至000001f8处—右键—粘贴。其结果如图5:

图5的00000148至000001f8处为重复区域,选中此区域,将此区域用0填充掉。
(6)修改MZP区段中PE区段的起始位置:现在新PE区段的起始位置变为了00000050,在MZP区中将出现PE头起始位置的十六进制数据由 00 01 00 00改为50 00 00 00,如图6:
图6
(7)修改可选头的大小:①可选头的起始位置变为了00000068(“?”后的第4位),终止位置仍为000001f8,选中此区域,此区域大小为400字节(即十六进制的190)。如图7:

图7
②光标移至“?”处,将“?”对应的十六进制数据由原来的e0 00变为 90 01。如图8:
图8
(8)保存:将文件进行保存,双击jqm.exe,程序仍然可以正常运行。
(9)测试:用ollydbg加载jqm.exe,出现如图9的提示,反调试成功。在准确得到特征码之后用此方法还可以实现病毒程序的文件免杀

啊冲 2016-02-03 10:46
OD载入程序就自动退出是比较恼人的,还没开始调试呢就退出了,这可让人如何是好。初学破解的人一定会遇到这个问题,怎样解决呢,网上虽然有零星的介绍但都不全面,以下是我总结的一些,希望能对各位初学者有所帮助。
(比如Peid、FI查壳查不到,OD一载入就退出,这极有可能是VMProtect的保护(虚拟机保护),用EXEinfo可以查出来一些版本的VMP,如果有这个提示那就更确定无疑了“A debugger has been found running in your system.Please, unload it from memory and restart your program”。)
1.更换几个OD试试,OllyICE、Shadow、加强版等
2.用附加的方式加载程序,文件-->附加,能解决很多问题
3.OD目录下,将475K 的DbgHelp.dll文件换成近1M大小的DbgHelp.dll文件,475K的有溢出漏洞,这条比较关键
4.使用StrongOD插件,(StrongOD+原版OD试试),这条比较关键
5.StrongOD中选择CreateAsRestrict
6.尝试命令bp ExitProcess,看能否发现什么线索
7.改变ollydbg.ini中的驱动名称,修改版的OD不需要自己改     
DriverName                 -      驱动文件名,设备对象名
DriverKey                     -     和驱动通信的key
HideWindow                 -    是否隐藏窗口,1为隐藏,0为不隐藏
HideProcess                 -     是否隐藏od进程,1为隐藏,0为不隐藏
ProtectProcess             -     是否隐藏保护Od进程,1为保护,0为不保护
8.改OD窗体类名,用的修改版的话一般都改过了,不需要自己再改
方法如下:
主窗体类名:
引用:
VA:004B7218
Offset:000B6018
各子窗体类名:
引用:
VA:004B565B ~ 004B568A
Offset:000B445B ~ 000B448A
改成任意,可以过GetWindow检测
9.手动修改程序“导出表”中的“函数名数目”值,上面方法不管用再试试它
方法:使用“LordPE”打开要编辑的PE程序,然后依次选择[目录]->[导出表对应的“..”按钮],把“函数名数目”的值减1,并点击“保存”按钮,就OK了。为了好看些,也可以把“函数数目”和“函数名数目”的值都同时减1并保存,效果一样。
   解释:一般情况下EXE不会加“导出表”,如果加了,就应该给出所导出的API函数。当我们打开这类PE程序(EXE版)时,会发现它存在“导出表”,但“导出表”中并没有导出的API函数。同时“函数数目”和“函数名数目”的值都比原PE程序设置的值大了1(如:EXE版“导出表”列表中显示了0个导出的API函数,壳将其“函数数目”和“函数名数目”的值都设置成了1;DLL版“导出表”列表中显示了0xD个导出的API函数,壳将其“函数数目”和“函数名数目”的值都设置成了0xE。)。所以我们将其减1,就OK了。被修改过的PE程序,可以正常运行,不会有任何影响。

这只是我的一点总结,附加方式加载、替换DBGHELP.DLL、使用StrongOD插件和修改导出表函数名数目的方法是可行的,能够解决一些问题。当然这些方法可能并不全面。

ANTI-OD原因解读:
概括来说:TLS回调函数在入口点之前执行,并进行了ANTI-OD的操作.
具体请看:TLS数据初始化和TLS回调函数都会在入口点之前执行,也就是说TLS是程序最开始运行的地方,因此可以在这里防止ANTI-OD的代码,检测并关闭OD。
应对方法:
  默认情况下OllyDbg载入程序将会暂停在入口点,应该配置一下OllyDbg使其在TLS回调被调用之前中断在实际的loader。
  通过“选项->调试选项->事件->第一次中断于->系统断点”来设置中断于ntdll.dll内的实际loader代码。这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。例如:在内存映像的.text代码段上设置内存访问断点,就可以断在TLS回调函数里。

更多TLS内容请看我的两篇博文:
TLS回调函数,Anti-od原理分析:http://hi.baidu.com/tjt999/blog/item...808f7eff1.html
TLS回调函数,Anti-od实例: http://hi.baidu.com/tjt999/blog/item...f359bf7f3.html

更多反调试知识请看《脱壳的艺术》和我的
《各种反调试技术原理与实例》: http://bbs.pediy.com/showthread.php?t=106143
如需交流请进群:1684360

实例代码:程序见附件,用原版OD测试,参考了某位大虾的代码。
.386
.model   flat,stdcall
option   casemap:none
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib

.data?
dwTLS_Index dd  ?

OPTION    DOTNAME
;; 定义一个TLS节          
.tls  SEGMENT                        
TLS_Start LABEL  DWORD
dd    0100h    dup ("slt.")
TLS_End   LABEL  DWORD
.tls   ENDS
OPTION    NODOTNAME

.data
TLS_CallBackStart  dd  TlsCallBack0
TLS_CallBackEnd    dd  0
szTitle            db  "Hello TLS",0
szInTls            db  "我在TLS里",0
szInNormal         db  "我在正常代码内",0
szClassName        db  "ollydbg"        ; OD 类名
;这里需要注意的是,必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置,
;其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。
PUBLIC _tls_used
_tls_used IMAGE_TLS_DIRECTORY

.code
;***************************************************************
;; TLS的回调函数
TlsCallBack0 proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID  
     mov     eax,dwReason ;判断dwReason发生的条件
     cmp     eax,DLL_PROCESS_ATTACH  ; 在进行加载时被调用
     jnz     ExitTlsCallBack0
     invoke  FindWindow,addr szClassName,NULL  ;通过类名进行检测
     .if     eax     ;找到
             invoke    SendMessage,eax,WM_CLOSE,NULL,NULL
     .endif
     invoke  MessageBox,NULL,addr szInTls,addr szTitle,MB_OK
     mov     dword ptr[TLS_Start],0  
     xor     eax,eax
     inc     eax
ExitTlsCallBack0: 
     ret
TlsCallBack0   ENDP
;****************************************************************
Start:
    invoke   MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK
    invoke   ExitProcess, 1
    end  Start

啊冲 2016-02-03 10:46

发现OD的处理

一、如何获取OD窗口的句柄

1.已经获取了窗口类名或标题:FindWindow

2.没有获取窗口类名或标题:GetForeGroundWindow返回前台窗口,这里就是OD的窗口句柄了。注意这种方法更为重要,因为大多数情况下不会知道OD的窗口类名。

invoke IsDebuggerPresent

              .if     eax

                      invoke GetForegroundWindow   ;获得的是OD的窗口句柄

                      invoke SendMessage,eax,WM_CLOSE,NULL,NULL

              .endif

二、获取OD窗口句柄后的处理

(1)向窗口发送WM_CLOSE消息

              invoke  FindWindow,addr szClassName,NULL  ;通过类名进行检测

              .if    eax     ;找到

                      mov     hWinOD,eax

invoke     MessageBox,NULL,offset szFound,offset szCaption,MB_OK                     invoke   SendMessage,hWinOD,WM_CLOSE,NULL,NULL

              .endif

(2)终止相关进程,根据窗口句柄获取进程ID,根据进程ID获取进程句柄,

_GetODProcID    proc

        LOCAL  @hWinOD              ;窗口句柄

        LOCAL  @hProcessOD           ;进程句柄

        LOCAL  @idProcessOD          ;进程ID

       invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

       .if    eax     ;找到

             mov       @hWinOD,eax        ;窗口句柄  

             invoke   GetWindowThreadProcessId,@hWinOD,addr @idProcessOD  

;获取进程ID在@idProcessOD里

             invoke   OpenProcess,PROCESS_TERMINATE,TRUE,@idProcessOD     

;获取进程句柄在返回值里

             .if    eax                     ;获取句柄成功

                     mov      @hProcessOD,eax

               invoke   TerminateProcess,@hProcessOD,200    ;利用句柄终止进程

                     invoke     CloseHandle,@hProcessOD            ;关闭进程句柄

                     invoke   MessageBox,NULL,addr szClose,addr szMerry,MB_OK

             .else                          ;获取句柄失败,多因权限问题

                     invoke    MessageBox,NULL,addr szFail,addr szCaption,MB_OK

             .endif                         .

       .endif               

       ret

_GetODProcIDendp

1.    窗口类名、窗口名

(1)      FindWindow

(2)      EnumWindow函数调用后,系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用GetWindowText得到窗口标题,用strstr等函数查找有无Ollydbg字符串。StrStr(大小写敏感,对应的StrStrI大小写不敏感)函数返回str2第一次出现在str1中的位置,如果没有找到,返回NULL。

(3)      GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,这样就可以向其发送WM_CLOSE消息将其关闭了。

(1)FindWindow

szClassName     db     'ollydbg',0

        invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

              .if       eax     ;找到

                    jmp   debugger_found    

              .endif               

(2)EnumWindow

.386

.modelflat,stdcall

optioncasemap:none

includewindows.inc

includeuser32.inc

includelibuser32.lib

includekernel32.inc

includelibkernel32.lib

include  Shlwapi.inc

includelib Shlwapi.lib   ;strstr



            .const

szTitle     db       'ollydbg',0       

szCaption   db       '结果',0

szFindOD    db       '发现目标窗口',0

szText      db       '枚举已结束,没提示发现目标,则没有找到目标窗口',0

            .code

;定义回调函数

_CloseWnd procuses ebx edi esi,_hWnd,_lParam

         LOCAL  @szBuffer[1024]:BYTE   ;接收窗口标题

         invoke IsWindowVisible,_hWnd

         .if eax ;是否是可见的窗口

             invoke GetWindowText,_hWnd,addr@szBuffer,sizeof @szBuffer

             invoke StrStrI,addr@szBuffer,offset szTitle  ;查找标题中有无字符串,不带I的大小写敏感

             .if eax

                 invoke   MessageBox,NULL,addr szFindOD,addrszCaption,MB_OK

                 invoke   PostMessage,_hWnd,WM_CLOSE,0,0  ;关闭目标

             .endif

         .endif

         mov eax,TRUE ;返回true 时,EnumWindows继续枚举下一个窗口,false退出枚举.

         ret

_CloseWnd endp



start:

           invoke   EnumWindows,addr _CloseWnd,NULL

;EnumWindows调用,系统枚举所有顶级窗口,为每个窗口调用一次回调函数

           invoke   MessageBox,NULL,addr szText,addrszCaption,MB_OK

           invoke   ExitProcess,NULL

           end start

1.   检测调试器进程

枚举进程列表,看是否有调试器进程(OLLYDBG.EXE,windbg.exe等)。

利用kernel32!ReadProcessMemory()读取进程内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。

              .386

              .model flat, stdcall

              option casemap :none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib      kernel32.lib

               .const

stSysProc       db     'OLLYDBG.EXE',0

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

_GetProcList    proc

       LOCAL  @stProcessEntry:PROCESSENTRY32

       LOCAL  @hSnapShot

       invoke  CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,NULL

       mov    @hSnapShot,eax                                    

       mov    @stProcessEntry.dwSize,sizeof @stProcessEntry

       invoke Process32First,@hSnapShot,addr @stProcessEntry

       .while eax

              invokelstrcmp,addr @stProcessEntry.szExeFile,addr stSysProc

              .if    eax == 0       ;为0,说明进程名相同

                  push 20

                 invoke  MessageBox,NULL,addrszFound,addr szCaption,MB_OK

              .endif             

              invokeProcess32Next,@hSnapShot,addr @stProcessEntry                     

       .endw

       pop    eax

       .if    eax != 20

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

       .endif

       ret

_GetProcListendp



start:

              invoke  _GetProcList

              invoke     ExitProcess,NULL

              end  start

1.    父进程是否是Explorer

原理:通常进程的父进程是explorer.exe(双击执行的情况下),否则可能程序被调试。

下面是实现这种检查的一种方法:

1.通过TEB(TEB.ClientId)或者使用GetCurrentProcessId()来检索当前进程的PID

2.用Process32First/Next()得到所有进程的列表,注意explorer.exe的PID(通过PROCESSENTRY32.szExeFile)和通过PROCESSENTRY32.th32ParentProcessID获得的当前进程的父进程PID。Explorer进程ID也可以通过桌面窗口类和名称获得。

3.如果父进程的PID不是explorer.exe,cmd.exe,Services.exe的PID,则目标进程很可能被调试

对策:OllyAdvanced提供的方法是让Process32Next()总是返回fail,使进程枚举失效,PID检查将会被跳过。这些是通过补丁kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。

(1)通过桌面类和名称获得Explorer的PID 源码见附件

                .data?    

szDesktopClass       db    'Progman',0                ;桌面的窗口类

szDesktopWindow  db    'ProgramManager',0         ;桌面的窗口名称

dwProcessID     dd  ?                        ;保存进程ID

dwThreadID      dd  ?                       ;保存线程ID

                .code

invoke     FindWindow,addr szDesktopClass,addrszDesktopWindow  ;获取桌面窗口句柄

invoke     GetWindowThreadProcessId,eax,offsetdwProcessID      ;获取EXPLORER进程ID

mov     dwThreadID,eax                     ;线程ID

(2)通过进程列表快照获得Explorer的PID 源码见附件

szExplorer      db     'EXPLORER.EXE',0

dwParentID     dd     ?

dwExplorerID   dd     ?

_ProcTest  proc

        local @stProcess:PROCESSENTRY32         ;每一个进程的信息

              local  @hSnapShot                    ;快照句柄     

          pushad                          

          

        invoke GetCurrentProcessId

        mov    ebx,eax                ;当前进程ID

              invoke     RtlZeroMemory,addr @stProcess,sizeof @stProcess ; 0初始化进程信息结构

              mov      @stProcess.dwSize,sizeof@stProcess             ;手工填写结构大小

              invoke     CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0;获取进程列表快照

              mov       @hSnapShot,eax                                  ;快照句柄

              invoke     Process32First,@hSnapShot,addr @stProcess       ;第一个进程

              .while     eax

                .if  ebx [email protected]               ;是当前进程吗?

                      mov  eax,@stProcess.th32ParentProcessID      ;是,则保存父进程ID

                      mov  dwParentID,eax                  

                .endif

                invoke   lstrcmp,addr @stProcess.szExeFile,addrszExplorer ;Explorer进程ID

              .if   eax == 0       ;为0,说明进程名相同                       

                       mov eax,@stProcess.th32ProcessID

                       mov dwExplorerID,eax

               .endif                   

                   invoke  Process32Next,@hSnapShot,addr @stProcess ;下一个进程

              .endw

              invoke     CloseHandle,@hSnapShot  ;关闭快照

             

              mov  ebx,dwParentID

        .if ebx == dwExplorerID    ;父进程ID与EXPLORER进程ID比较                                  invoke MessageBox,NULL,offset szNotFound,offset szCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,offset szFound,offsetszCaption,MB_OK

              .endif

        popad

              ret

_ProcTest endp

1.    RDTSC/ GetTickCount时间敏感程序段

当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。

(1)RDTSC

将计算机启动以来的CPU运行周期数放到EDX:EAX里面,EDX是高位,EAX是低位。

如果CR4的TSD(timestamp disabled)置位,则rdtsc在ring3下运行会导致异常(特权指令),所以进入ring0,把这个标记置上,然后Hook OD的WaitForDebugEvent,拦截异常事件,当异常代码为特权指令时,把异常处的opcode读出检查,如果是rdtsc,把eip加2,SetThreadContext,edx:eax的返回由你了。

(2)GetTickCount 源码见附件

invoke GetTickCount           ;第一次调用

              mov     ebx,eax                ;结果保存在ebx里

              mov     ecx,10                 ;延时开始

              mov     edx,6                  ;单步走,放慢速度     

            mov     ecx,10                 ;延时结束

              invoke  GetTickCount           ;第二次调用

              sub     eax,ebx                ;计算差值

              .if     eax > 1000          ;假定大于1000ms,就说明有调试器  

                jmp   debugger_found

              .endif                  

2.    StartupInfo结构

原理:Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断OD是否在调试程序.

if (Info.dwX<>0) or(Info.dwY<>0) or (Info.dwXCountChars<>0) or(Info.dwYCountChars<>0) or   (Info.dwFillAttribute<>0) or (Info.dwXSize<>0) or(Info.dwYSize<>0) then  “有调试器”

*******************************************************************************

结构体

typedef struct _STARTUPINFO

{

   DWORD cb;            0000

   PSTR lpReserved;        0004

   PSTR lpDesktop;         0008

   PSTR lpTitle;            000D

   DWORD dwX;          0010

   DWORD dwY;           0014

  DWORD dwXSize;        0018

   DWORD dwYSize;        001D

   DWORD dwXCountChars;  0020

   DWORDdwYCountChars;  0024

   DWORDdwFillAttribute;   0028

   DWORD dwFlags;         002D

   WORD wShowWindow;    0030

   WORD cbReserved2;       0034

   PBYTE lpReserved2;       0038

   HANDLE hStdInput;       003D

   HANDLE hStdOutput;      0040

   HANDLE hStdError;       0044

} STARTUPINFO, *LPSTARTUPINFO;

_ProcTest  proc

                LOCAL  @stStartupInfo:STARTUPINFO       

               pushad           

                  invoke  GetStartupInfo,addr @stStartupInfo

                  cmp     @stStartupInfo.dwX,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwY,0

                jnz      foundDebugger

                  cmp     @stStartupInfo.dwXCountChars,0

               jnz      foundDebugger

                  cmp     @stStartupInfo.dwYCountChars,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwFillAttribute,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwXSize,0

                  jnz      foundDebugger

                  cmp     @stStartupInfo.dwYSize,0

                  jnz      foundDebugger                

     noDebugger: “无调试器”

                jmp     TestOver

  foundDebugger: “有调试器”

       TestOver:       

                popad

               ret

_ProcTest endp

1.    BeingDebugged

kernel32!IsDebuggerPresent() API检测进程环境块(PEB)中的BeingDebugged标志检查这个标志以确定进程是否正在被用户模式的调试器调试。

每个进程都有PEB结构,一般通过TEB间接得到PEB地址

Fs:[0]指向当前线程的TEB结构,偏移为0处是线程信息块结构TIB

TIB偏移18H处是self字段,是TIB的反身指针,指向TIB(也是PEB)首地址

TEB偏移30H处是指向PEB结构的指针

PEB偏移2H处,就是BeingDebugged字段,Uchar类型

(1)      调用IsDebuggerPresent函数,间接读BeingDebugged字段

(2)      利用地址直接读BeingDebugged字段

对策:

(1)      数据窗口中Ctrl+G fs:[30] 查看PEB数据,将PEB.BeingDebugged标志置0

(2)      Ollyscript命令"dbh"可以补丁这个标志

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

               ;调用函数IsDebuggerPresent

              invoke  IsDebuggerPresent

              .if     eax

                     invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

              .endif                               

                ;直接去读字段

                assume fs:nothing

                mov     eax,fs:[30h]

                movzx   eax,byte ptr [eax+2]

              .if     eax

                     invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

              .else

                     invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

              .endif                 

              invoke     ExitProcess,NULL

              end  start

1.    PEB.NtGlobalFlag, Heap.HeapFlags, Heap.ForceFlags

(1)通常程序没有被调试时,PEB另一个成员NtGlobalFlag(偏移0x68)值为0,如果进程被调试通常值为0x70(代表下述标志被设置):

FLG_HEAP_ENABLE_TAIL_CHECK(0X10)

FLG_HEAP_ENABLE_FREE_CHECK(0X20)

FLG_HEAP_VALIDATE_PARAMETERS(0X40)

这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意PEB.NtGlobalFlag的默认值可以通过gflags.exe工具或者在注册表以下位置创建条目来修改:

HKLM\Software\Microsoft\WindowsNt\CurrentVersion\Image File Execution Options

assume fs:nothing

                mov     eax,fs:[30h]

                mov     eax,[eax+68h]

                and     eax,70h

(2)由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。正常情况下系统为进程创建第一个堆时会将Flags和ForceFlags分别设为2(HEAP_GROWABLE)和0 。当进程被调试时,这两个标志通常被设为50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。

assume fs:nothing

              mov     ebx,fs:[30h]     ;ebx指向PEB

            mov     eax,[ebx+18h]   ;PEB.ProcessHeap

            cmp      dword ptr [eax+0ch],2    ;PEB.ProcessHeap.Flags

            jne        debugger_found

                 cmp dword ptr [eax+10h],0         ;PEB.ProcessHeap.ForceFlags

                 jne   debugger_found

这些标志位都是因为BeingDebugged引起的。系统创建进程的时候设置BeingDebugged=TRUE,后来NtGlobalFlag根据这个标记设置FLG_VALIDATE_PARAMETERS等标记。在为进程创建堆时,又由于NtGlobalFlag的作用,堆的Flags被设置了一些标记,这个Flags随即被填充到ProcessHeap的Flags和ForceFlags中,同时堆中被填充了很多BAADF00D之类的东西(HeapMagic,也可用来检测调试)。

一次性解决这些状态见加密解密P413

.386

.model flat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

              assume  fs:nothing

              mov     ebx,fs:[30h]  ;ebx指向PEB

              

              ;PEB.NtGlobalFlag

            mov    eax,[ebx+68h]

            cmp     eax,70h

                  je     debugger_found                   



                ;PEB.ProcessHeap

             mov    eax,[ebx+18h]



             ;PEB.ProcessHeap.Flags

             cmp      dwordptr [eax+0ch],2       

             jne debugger_found

              

              ;PEB.ProcessHeap.ForceFlags

                  cmp      dword ptr [eax+10h],0

                   jne debugger_found

             

             invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

             jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK     

                                  

          exit:   invoke     ExitProcess,NULL

                 end  start

1.     DebugPort:CheckRemoteDebuggerPresent()/NtQueryInformationProcess()

Kernel32!CheckRemoteDebuggerPresent()是用于确定是否有调试器被附加到进程。

BOOL CheckRemoteDebuggerPresent(

HANDLE   hProcess,

PBOOL     pbDebuggerPresent

)

Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。

这个API内部调用了ntdll!NtQueryInformationProcess(),由它完成检测工作。

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelibuser32.lib

includelibkernel32.lib

                .data?

dwResult        dd     ?

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

                invoke  GetCurrentProcessId

                invoke  OpenProcess,PROCESS_ALL_ACCESS,NULL,eax             

                invoke  CheckRemoteDebuggerPresent,eax,addr dwResult

                cmp     dword ptr dwResult,0

                jne     debugger_found   

                         

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK                    exit:    invoke       ExitProcess,NULL

                      end  start

ntdll!NtQueryInformationProcess()有5个参数。

为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7)。

NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员,这个成员是系统用来与调试器通信的端口句柄。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation将被置为0xFFFFFFFF,否则ProcessInformation将被置为0。

ZwQueryInformationProcess(

IN HANDLEProcessHandle,

INPROCESSINFOCLASS ProcessInformationClass,

OUT PVOIDProcessInformation,

IN ULONGProcessInformationLength,

OUT PULONGReturnLength OPTIONAL

);

.386

.modelflat,stdcall

optioncasemap:none



include    windows.inc

include    user32.inc

includelibuser32.lib

include    kernel32.inc

includelibkernel32.lib

include   ntdll.inc        ;这两个

includelib ntdll.lib

               .data?

dwResult        dd     ?

                .const    

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0



              .code

start:

                invoke  GetCurrentProcessId

                invoke  OpenProcess,PROCESS_ALL_ACCESS,NULL,eax 

                invoke  ZwQueryInformationProcess,eax,7,offsetdwResult,4,NULL   

                cmp     dwResult,0               

                jne     debugger_found   

      

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK     

                                  

       exit: invoke     ExitProcess,NULL

              end  start

1.    SetUnhandledExceptionFilter/ Debugger Interrupts

调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用,Debugger Interrupts就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。注意测试时,在异常处理里取消选中INT3 breaks 和 Singal-stepbreak

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

lpOldHandler  dd    ?

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

; ExceptionHandler 异常处理程序

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov    [edi].regEax,0FFFFFFFFH  ;设置EAX

              mov     [edi].regEip,offset SafePlace

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret

_Handler endp



start:

              invoke    SetUnhandledExceptionFilter,addr_Handler

              mov lpOldHandler,eax

             

        xor eax,eax       ;清零eax

        int    3             ;产生异常,然后_Handler被调用

SafePlace:

              test       eax,eax

              je   debugger_found



              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

        jmp    exit

debugger_found: invoke  MessageBox,NULL,addr szFound,addr szCaption,MB_OK                         exit:       invoke    SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。

另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断"和"单步中断"复选框来完成设置。

1.   Trap Flag单步标志异常

TF=1的时候,会触发单步异常。该方法属于异常处理,不过比较特殊:未修改的OD无论是F9还是F8都不能处理异常,有插件的OD在F9时能正确处理,F8时不能正确处理。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib      kernel32.lib

              .data

lpOldHandler  dd    ?

szCaption       db     '检测结果',0

szFound         db     '程序未收到异常,说明有调试器',0

szNotFound      db     '程序处理了异常而到达安全位置,没有调试器',0

             .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov       lpOldHandler,eax

              pushfd ;push    eflags

              or      dword ptr [esp],100h   ;TF=1

              popfd

              nop

              jmp     die            

  SafePlace:      

                invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

       die:  invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

       exit:  invoke       SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

2.    SeDebugPrivilege 进程权限

默认情况下进程没有SeDebugPrivilege权限,调试时,会从调试器继承这个权限,可以通过打开CSRSS.EXE进程间接地使用SeDebugPrivilege确定进程是否被调试。注意默认情况下这一权限仅仅授予了Administrators组的成员。可以使用ntdll!CsrGetProcessId() API获取CSRSS.EXE的PID,也可以通过枚举进程来得到CSRSS.EXE的PID。

实例测试中,OD载入后,第一次不能正确检测,第二次可以,不知为何。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

include           kernel32.inc

include      ntdll.inc

includelib      user32.lib

includelib      kernel32.lib

includelib    ntdll.lib

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:

               invoke CsrGetProcessId  ;ntdll!CsrGetProcessId获取CSRSS.EXE的PID

               invoke OpenProcess,PROCESS_QUERY_INFORMATION,NULL,eax

               test    eax,eax

               jnz   debugger_found

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK                         exit: invoke       ExitProcess,NULL

              end  start

3.   DebugObject:NtQueryObject()

未完成,期待做过这个测试的朋友指点一下!

除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。逆向论坛中讨论的一个有趣的方法就是检查DebugObject类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一

个DebugObject类型的对象。

DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3):

NTSTATUS NTAPI NtQueryObject(

IN    HANDLE                                         ObjectHandle,

IN    OBJECT_INFORMATION_CLASS   ObjectInformationClass,

OUT   PVOID                                           ObjectInformation,

IN    ULONG                                           Length,

OUT   PULONG                                        ResultLength

)

这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:

typedef struct _OBJECT_ALL_INFORMATION{

ULONG                                          NumberOfObjectsTypes;

OBJECT_TYPE_INFORMATION         ObjectTypeInformation[1];

}

检测例程将遍历拥有如下结构的ObjectTypeInformation数组:

typedef struct _OBJECT_TYPE_INFORMATION{

[00] UNICODE_STRING        TypeName;

[08] ULONG                          TotalNumberofHandles;

[0C] ULONG                  TotalNumberofObjects;

...more fields...

}

TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects 或 TotalNumberofHandles 是否为非0值。

1.   OllyDbg:Guard Pages

这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。

除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。

页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。

示例

下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。

对策

由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会
被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。

如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode, 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION即可。

实例:

                   .386

                   .modelflat,stdcall

                   optioncasemap:none

include                 windows.inc

include                 user32.inc

includelib        user32.lib

include                 kernel32.inc

includelib          kernel32.lib

                   .data

lpOldHandler      dd     ?

dwOldType   dd      ?

                   .const

szCaption      db      '检测结果',0

szFound        db      '检测到调试器',0

szNotFound     db      '没有调试器',0

                   .code

_Handler    proc  _lpExceptionPoint                 

                   pushad

                   mov  esi,_lpExceptionPoint

                   assume       esi:ptr EXCEPTION_POINTERS

                   mov  edi,[esi].ContextRecord

                   assume       edi:ptr CONTEXT

                   mov     [edi].regEax,0FFFFFFFFH   ;检测标志

                   mov     [edi].regEip,offset SafePlace

                   assume       esi:nothing,edi:nothing

                   popad

                   mov  eax,EXCEPTION_CONTINUE_EXECUTION

                   ret

_Handler    endp



start:

                   invoke        SetUnhandledExceptionFilter,addr_Handler

                   mov  lpOldHandler,eax

                                  

         invoke VirtualAlloc,NULL,1000H,MEM_COMMIT,PAGE_READWRITE ;分配内存

        push    eax                             

        mov    byte ptr [eax],0C3H ;写一个 RETN 到保留内存,以便下面的调用

       invoke VirtualProtect,eax,1000h,PAGE_EXECUTE_READ or PAGE_GUARD,addr dwOldType

        xor     eax,eax         ;检测标志

        pop     ecx

        call     ecx         ;执行保留内存代码,触发异常  

SafePlace:

                   test  eax,eax      ;检测标志

                   je     debugger_found                

                   invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

         jmp     exit

debugger_found: invoke MessageBox,NULL,addr szFound,addr szCaption,MB_OK

  exit:        invoke VirtualFree,ecx,1000H,MEM_DECOMMIT

              invoke       SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

                   invoke        ExitProcess,NULL

                   end    start

1.   Software Breakpoint Detection

软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。

(1)      实例一   普通断点

注意:在被保护的代码区域下INT3断点进行测试

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib     kernel32.lib

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:          jmp     CodeEnd    

   CodeStart:  mov     eax,ecx  ;被保护的程序段

                nop

                push    eax

                push    ecx

                pop     ecx

                pop     eax

   CodeEnd:    

                cld               ;检测代码开始

               mov     edi,offset CodeStart

               mov     ecx,offset CodeEnd -offset CodeStart

               mov     al,0CCH

               repne   scasb               

                jz      debugger_found      

                         

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

          jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

              end  start

(1)      实例二 函数断点bp

利用GetProcAddress函数获取API的地址

注意:检测时,BPMessageBoxA

.386

.modelflat,stdcall

optioncasemap:none

includewindows.inc

includeuser32.inc

includelibuser32.lib

includekernel32.inc

includelibkernel32.lib



            .const

szKernelDll  db       'user32.dll',0

szAPIMessboxdb        'MessageBoxA',0

szCaption    db       '结果',0

szFound       db       '发现API断点',0

szNotFound   db       '未发现断点',0

            .code

start:

           invoke  GetModuleHandle,addr szKernelDll

          invoke   GetProcAddress,eax,addrszAPIMessbox  ;API地址

           cld               ;检测代码开始

           mov     edi,eax  ;API开始位置

           mov     ecx,100H ;检测100字节

           mov     al,0CCH  ;CC

          repne   scasb                

           jz      debugger_found      

                         

         invoke  MessageBox,NULL,addrszNotFound,addr szCaption,MB_OK

           jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

                end start

1.    Hardware Breakpoints

硬件断点是通过设置名为Dr0到Dr7的调试寄存器来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的CONTEXT结构,该结构可以通过传递给异常处理例程的ContextRecord参数来访问。

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib    kernel32.lib

              .data

lpOldHandler  dd    ?

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              cmp     [edi].iDr0,0     ;检测硬件断点

              jne     debugger_found           

              cmp     [edi].iDr1,0

              jne     debugger_found    

              cmp     [edi].iDr2,0

              jne     debugger_found    

              cmp     [edi].iDr3,0

              jne     debugger_found                  

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

                jmp     TestOver

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK 

       TestOver:assume      esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov lpOldHandler,eax         

                xor eax,eax       ;清零eax

                mov     dword ptr [eax],0    ;产生异常,然后_Handler被调用               

SafePlace:          invoke     SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

1.    PatchingDetectionCodeChecksumCalculation补丁检测,代码检验和

补丁检测技术能识别壳的代码是否被修改,也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。

实例:改动被保护代码的话,CHECKSUM需要修改,通过OD等找出该值

注意:在被保护代码段下F2断点或修改字节来测试

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib      user32.lib

include           kernel32.inc

includelib     kernel32.lib

CHECKSUM        EQU    915Ch       ;改动被保护代码的话,需要修改

              .const

szCaption       db     '检测结果',0

szFound         db     '检测到调试器',0

szNotFound      db     '没有调试器',0

              .code

start:          jmp     CodeEnd    

   CodeStart:  mov     eax,ecx  ;被保护的程序段

                nop

                push    eax

                push    ecx

                pop    ecx

                pop     eax

   CodeEnd:                   

                mov       esi,CodeStart

                mov       ecx,CodeEnd - CodeStart

                xor eax,eax

checksum_loop:

                movzx    ebx,byte ptr [esi]

                add        eax,ebx

                rol eax,1

                inc esi

                loop       checksum_loop

               

                cmp       eax,CHECKSUM

                jne debugger_found            

                         

              invoke  MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK

          jmp     exit

debugger_found:invoke  MessageBox,NULL,addr szFound,addrszCaption,MB_OK

          exit:    invoke     ExitProcess,NULL

              end  start

1.    block input封锁键盘、鼠标输入

user32!BlockInput() API 阻断键盘和鼠标的输入。

典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。

示例:源码看附件

BlockInput()参数fBlockIt,true,键盘和鼠标事件被阻断;false,键盘和鼠标事件解除阻断:

; Block input

push                     TRUE

call               [BlockInput]



;...Unpackingcode...



;Unblock input

push                     FALSE

call               [BlockInput]

对策

(1)最简单的方法就是补丁 BlockInput()使它直接返回。

(2)同时按CTRL+ALT+DELETE键手工解除阻断。

2.   EnableWindow禁用窗口

与BlockInput异曲同工,也是禁用窗口然后再解禁

在资源管理器里直接双击运行的话,会使当前的资源管理器窗口被禁用。

在OD里面的话,就会使OD窗口被禁用。

                .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib          user32.lib

include           kernel32.inc

includelib     kernel32.lib

              .const

szCaption       db     '结果',0

szEnableFalse   db     '窗口已经禁用',0

szEnableTrue    db     '窗口已经恢复',0

              .code

start:         

                invoke  GetForegroundWindow

                mov     ebx,eax

                invoke  EnableWindow,eax,FALSE

                invoke  MessageBox,NULL,addr szEnableFalse,addrszCaption,MB_OK

                nop

                invoke  EnableWindow,ebx,TRUE

                invoke  MessageBox,NULL,addr szEnableTrue,addrszCaption,MB_OK

                nop

                  invoke     ExitProcess,NULL

              end  start

1.   ThreadHideFromDebugger

ntdll!NtSetInformationThread()用来设置一个线程的相关信息。把ThreadInformationClass参数设为ThreadHideFromDebugger(11H)可以禁止线程产生调试事件。

ntdll!NtSetInformationThread的参数列表如下。ThreadHandle通常设为当前线程的句柄(0xFFFFFFFE):

NTSTATUS NTAPI NtSetInformationThread(

IN  HANDLE                                           ThreadHandle,

IN  THREAD_INFORMATION_CLASS      ThreadInformaitonClass,

IN  PVOID                                              ThreadInformation,

IN  ULONG                                             ThreadInformationLength

);

ThreadHideFromDebugger内部设置内核结构ETHREAD的HideThreadFromDebugger成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用。

invoke GetCurrentThread

invoke NtSetInformationThread,eax,11H,NULL,NULL

对策:

(1)在ntdll!NtSetInformationThread()里下断,断下来后,操纵EIP防止API调用到达内核
(2)Olly Advanced插件也有补这个API的选项。补过之后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将不再深入内核仅仅执行一个简单的返回。

.386

.modelflat,stdcall

optioncasemap:none

include    windows.inc

include    user32.inc

include    kernel32.inc

includelib  user32.lib

includelib  kernel32.lib

include    ntdll.inc

includelib  ntdll.lib

               .const    

szCaption       db     '确定以后看看效果',0

szNotice        db     '汇编代码会消失哦',0

szResult        db     '看到效果了吗?没有则稍等',0

              .code

start:

                invoke  MessageBox,NULL,addr szNotice,addrszCaption,MB_OK

                invoke  GetCurrentThread

                invoke  NtSetInformationThread,eax,11H,NULL,NULL   

                invoke  MessageBox,NULL,addr szResult,addrszCaption,MB_OK   

                mov     eax,ebx ;其它指令                         

              invoke     ExitProcess,NULL

              end  start            

1.    DisablingBreakpoints禁用硬件断点

;执行过后,OD查看硬件断点还存在,但实际已经不起作用了

;利用CONTEXT结构,该结构利用异常处理获得,异常处理完后会自动写回

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

lpOldHandler  dd    ?

              .code

_Handler proc _lpExceptionPoint        

              pushad

              mov esi,_lpExceptionPoint

              assume    esi:ptr EXCEPTION_POINTERS

              mov edi,[esi].ContextRecord

              assume    edi:ptr CONTEXT

              mov [edi].regEip,offset SafePlace

              xor     eax,eax

              mov     [edi].iDr0,eax

              mov     [edi].iDr1,eax

              mov     [edi].iDr2,eax

              mov     [edi].iDr3,eax             

mov     [edi].iDr6,eax

              mov     [edi].iDr7,eax

              assume    esi:nothing,edi:nothing

              popad

              mov eax,EXCEPTION_CONTINUE_EXECUTION

              ret         

_Handler endp



start:

              invoke     SetUnhandledExceptionFilter,addr _Handler

              mov lpOldHandler,eax

             

                xor eax,eax       ;清零eax

                mov     dword ptr [eax],0    ;产生异常,然后_Handler被调用               

SafePlace:          invoke     SetUnhandledExceptionFilter,lpOldHandler   ;取消异常处理函数

              invoke     ExitProcess,NULL

              end  start

1.   OllyDbg:OutputDebugString() Format String Bug

OutputDebugString函数用于向调试器发送一个格式化的串,Ollydbg会在底端显示相应的信息。OllyDbg存在格式化字符串溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于Ollydbg对传递给kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受255个字节,但没对参数进行检查,所以导致缓冲区溢出。

例如:printf函数:%d,当所有参数压栈完毕后调用printf函数的时候,printf并不能检测参数的正确性,只是机械地从栈中取值作为参数,这样堆栈就被破坏了,栈中信息泄漏。。

示例:下面这个简单的示例将导致OllyDbg抛出违规访问异常或不可预期的终止。

szFormatStr     db    '%s%s',0

push   offset szFormatStr

call   OutputDebugString

对策:补丁 kernel32!OutputDebugStringA()入口使之直接返回

              .386

              .model flat,stdcall

              option casemap:none

include           windows.inc

include           user32.inc

includelib       user32.lib

include           kernel32.inc

includelib       kernel32.lib

              .data

szFormatStr     db    '%s%s',0

szCaption       db     '呵呵',0

szNotice        db     '执行已结束,看到效果了吗?',0



              .code

start:

              push    offset szFormatStr

              call    OutputDebugString

              invoke  MessageBox,NULL,addr szNotice,addrszCaption,MB_OK         

              invoke     ExitProcess,NULL

              end  start

2.   TLS Callbacks

使用Thread Local Storage (TLS)回调函数可以实现在实际的入口点之前执行反调试的代码,这也是OD载入程序就退出的原因所在。(Anti-OD)

线程本地存储器可以将数据与执行的特定线程联系起来,一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块。动态绑定(运行时)线程特定数据是通过 TLS API(TlsAlloc、TlsGetValue、TlsSetValue 和 TlsFree)的方式支持的。除了现有的 API 实现,Win32 和 Visual C++ 编译器现在还支持静态绑定(加载时间)基于线程的数据。当使用_declspec(thread)声明的TLS变量
时,编译器把它们放入一个叫.tls的区块里。当应用程序加载到内存时,系统寻找可执行文件中的.tls区块,并动态的分配一个足够大的内存块,以便存放TLS变量。系统也将一个指向已分配内存的指针放到TLS数组里,这个数组由FS:[2CH]指向。

数据目录表中第9索引的IMAGE_DIRECTORY_ENTRY_TLS条目的VirtualAddress指向TLS数据,如果非零,这里是一个IMAGE_TLS_DIRECTORY结构,如下:

IMAGE_TLS_DIRECTORY32   STRUC

  StartAddressOfRawData  DWORD  ?   ; 内存起始地址,用于初始化新线程的TLS

  EndAddressOfRawData   DWORD  ?   ; 内存终止地址

AddressOfIndex         DWORD ?   ; 运行库使用该索引来定位线程局部数据

AddressOfCallBacks     DWORD ?   ; PIMAGE_TLS_CALLBACK函数指针数组的地址

SizeOfZeroFill          DWORD ?   ; 用0填充TLS变量区域的大小

Characteristics           DWORD ?   ; 保留,目前为0

IMAGE_TLS_DIRECTORY32   ENDS

AddressOfCallBacks 是线程建立和退出时的回调函数,包括主线程和其它线程。当一个线程创建或销毁时,在列表中的每一个函数被调用。一般程序没有回调函数,这个列表是空的。TLS数据初始化和TLS回调函数调用都在入口点之前执行,也就是说TLS是程序最开始运行的地方。程序退出时,TLS回调函数再被执行一次。回调函数:

TLS_CALLBACK proto Dllhandle : LPVOID, Reason : DWORD,Reserved : LPVOID

参数如下:

Dllhandle : 为模块的句柄

Reason可取以下值:

DLL_PROCESS_ATTACH 1 : 启动一个新进程被加载

DLL_THREAD_ATTACH 2 : 启动一个新线程被加载

DLL_THREAD_DETACH 3 : 终止一个新线程被加载

DLL_PROCESS_DETACH 0 : 终止一个新进程被加载

Reserverd:用于保留,设置为0

IMAGE_TLS_DIRECTORY结构中的地址是虚拟地址,而不是RVA。这样,如果可执行文件不是从基地址装入,则这些地址会通过基址重定位修正。而且IMAGE_TLS_DIRECTORY本身不在.TLS区块中,而在.rdata里。

TLS回调可以使用诸如pedump之类的PE文件分析工具来识别。如果可执行文件中存在TLS条目,数据条目将会显示出来。

Data directory

EXPORT                                rva:00000000      size:00000000

IMPORT                                 rva:00061000      size:000000E0

:::

TLS                                         rva:000610E0      size:00000018

:::

IAT                                          rva:00000000      size:00000000

DELAY_IMPORT                  rva:00000000      size:00000000

COM_DESCRPTR                rva:00000000      size:00000000

unused                                    rva:00000000      size:00000000

接着显示TLS条目的实际内容。AddressOfCallBacks成员指向一个以null结尾的回调函数数组。

TLS directory:

StartAddressOfRawData:                          00000000

EndAddressOfRawData:                           00000000

AddressOfIndex:                              004610F8

AddressOfCallBacks:                       004610FC

SizeOfZeroFill:                                          00000000

Characteristics:                                          00000000

在这个例子中,RVA 0x4610fc指向回调函数指针(0x490f43和0x44654e):

默认情况下OllyDbg载入程序将会暂停在入口点,应该配置一下OllyDbg使其在TLS回调被调用之前中断在实际的loader。

通过“选项->调试选项->事件->第一次中断于->系统断点”来设置中断于ntdll.dll内的实际loader代码。这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。例如,在内存映像的.text代码段上设置内存访问断点,可以断在TLS回调函数。

.386

.model   flat,stdcall

option   casemap:none

includewindows.inc

includeuser32.inc

includekernel32.inc

includelibuser32.lib

includelibkernel32.lib



.data?

dwTLS_Indexdd  ?



OPTION    DOTNAME

;; 定义一个TLS节         

.tls  SEGMENT                       

TLS_StartLABEL  DWORD

dd   0100h    dup ("slt.")

TLS_End   LABEL DWORD

.tls   ENDS

OPTION    NODOTNAME



.data

TLS_CallBackStart  dd TlsCallBack0

TLS_CallBackEnd    dd  0

szTitle            db "Hello TLS",0

szInTls            db "我在TLS里",0

szInNormal         db "我在正常代码内",0

szClassName        db "ollydbg"        ; OD 类名

;这里需要注意的是,必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置,

;其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。

PUBLIC_tls_used

_tls_usedIMAGE_TLS_DIRECTORY



.code

;***************************************************************

;; TLS的回调函数

TlsCallBack0proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID 

     mov    eax,dwReason ;判断dwReason发生的条件

     cmp    eax,DLL_PROCESS_ATTACH  ; 在进行加载时被调用

     jnz    ExitTlsCallBack0

     invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

     .if    eax     ;找到

             invoke    SendMessage,eax,WM_CLOSE,NULL,NULL

     .endif

     invoke MessageBox,NULL,addr szInTls,addr szTitle,MB_OK

     mov    dword ptr[TLS_Start],0 

     xor    eax,eax

     inc    eax

ExitTlsCallBack0:

     ret

TlsCallBack0   ENDP

;****************************************************************

Start:

    invoke  MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK

    invoke  ExitProcess, 1

    end Start



反反调试技术

本人脱壳逆向的水平不高,这里仅说一下本人的一点体会:

对于初学者来说主要是利用StrongOD等各种插件,这些插件能够躲过上面所说的很多检测。有了一定基础以后就可以根据各种反调试方法的弱点寻求反反调试的途径了。

v2680267313 2016-04-30 23:36
用户被禁言,该主题自动屏蔽!


查看完整版本: [-- 反调试与反反调试内容收集帖 方便大家学习 --] [-- top --]


Powered by phpwind v8.7 Code ©2003-2011 phpwind 
Gzip enabled

你可能感兴趣的:(备用)