吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 65378|回复: 60
收起左侧

[调试逆向] 反调试与反反调试内容收集帖 方便大家学习

  [复制链接]
无止境 发表于 2014-7-9 08:03
反调试技术在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术,首先我们来看看反调试技术。

一、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);
}










免费评分

参与人数 1热心值 +1 收起 理由
薛定谔的猴子 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 无止境 发表于 2014-7-9 08:06
从零开始的反反调试日志
=============    编程环境    =====================
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



我想很多在内核之外的人,跟我一样在门外徘徊,其实,只要做,并没有那么难。  
 楼主| 无止境 发表于 2014-7-9 08:05
反反调试器跟踪”,对,没写错。

什么是“反调试器跟踪”?举个例子,用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:)
wx52pojie 发表于 2014-7-9 08:16
 楼主| 无止境 发表于 2014-7-9 08:15
第二部分:接管流程
  在第一部分完成之后,已经重载了一份新的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中的函数已全部转移到新模块中。但这还只是一个半成品,仅做测试使用,因为后面还需要改动很多。
 楼主| 无止境 发表于 2014-7-9 08:15
小结与修正
  目前已经实现了一份简单的重载内核代码,但是如果你也跟着我实现了此部分,会发现此代码根本不能使用,甚至不能拿到本机来测试,是的,新的内核还是有大量的问题,驱动加载后会导致一些程序打不开,但是并不蓝屏,有意思的现象。
  写上一篇文章的时候没有测试的那么完善,但是我拿出我之前写的重载内核代码,并没有上述问题,仔细分析代码并回忆,问题还是出在了重定位以及需要额外的处理。那么关于修复重定位的部分就需要重新写了,把修复过程分为两部分,第一次全部重定位到新模块上,第二次有选择的重定位到原模块上,为什么需要那么麻烦?这就涉及到了原始地址的获取方式问题,原始地址都存储在一个叫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[i].VirtualAddress, (PCHAR)lpFileBuffer + lpSection[i].PointerToRawData, lpSection[i].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[i] >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          ULONG_PTR *lpFixAddress = (ULONG_PTR *)((PCHAR)ImageBaseAddress + lpRelocateTable->VirtualAddress + (lpItem[i] & 0x0FFF));

          *lpFixAddress += DifferOffset;
        }
        break;
      case IMAGE_REL_BASED_ABSOLUTE://do nothing
        break;
      default:
        DbgPrint("KeFixReloc1:Found unknown type(%X).\n", (lpItem[i] >> 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[i] >> 12)
      {
      case IMAGE_REL_BASED_HIGHLOW:
        {
          PVOID lpFixAddress = (PCHAR)New + lpRelocateTable->VirtualAddress + (lpItem[i] & 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[i].VirtualAddress && Rva < lpSecHdr[i].VirtualAddress + lpSecHdr[i].SizeOfRawData)
    {
      return ((lpSecHdr[i].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搞定。
  最后,此代码仍然不能拿到本机测试,如果你想蓝的话。对于新模块最少还有两个问题没有搞定(别喷我),后续我同样会告诉大家。嗯,先这样吧。
 楼主| 无止境 发表于 2014-7-9 08:11
动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器

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

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

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

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

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

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

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

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

如今,软件安全已经成为了开发软件项目的必备组成部分,反调试则是其中关键的一环,然而,正如矛与盾的对立一样,反反调试与反调试必将永久的并立共存。为了防止软件被调试,现今的软件大多都利用了驱动来检测制止,对于关键的系统函数进行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 = <no type information>
8054696c nt!KiSwapContext = <no type information>
805fcd34 nt!VdmSwapContexts = <no type information>

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修改然后检测去吧~~

   以上只是本人的拙劣想法和见解而已,希望它对你有用~
 楼主| 无止境 发表于 2014-7-9 08:04
反调试技巧总结-原理和实现

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

    部分代码和参考资料来源:
1、<<脱壳的艺术>> hawking
2、<<windows anti-debugger reference>> Angeljyt
3、http://bbs.pediy.com
4、<<软件加密技术内幕>> 看雪学院
5、<<ANTI-UNPACKER TRICKS>> 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的另一篇文章<<Attacks on More Virtual Machine Emulators>>中有大量的描述,有兴趣的可以根据这篇文章,将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
    这个代码我也是完整从网上拷贝下来的,具体原理在<<Attacks on More Virtual Machine Emulators>>这篇文章里也有详细描述。与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<len;i++,addr++)
    {
      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
这个反调试在<<windows anti-debug reference>>里有描述,如果调试器跟踪经过下面的指令序列:


复制代码
  __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
  在<<Windows anti-debug reference>>中有讲述这个反跟踪技巧。这个所谓的"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
这个反调试是在<<ANTI-UNPACKER TRICKS>>中给出的,它主要是基于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真实数值的获得和存放问题。
 楼主| 无止境 发表于 2014-7-9 08:14
重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
  我的博客: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[i].VirtualAddress, (PCHAR)lpFileBuffer + lpSection[i].PointerToRawData, lpSection[i].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[i] = 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[i].VirtualAddress && RelocValue < lpSecHdr[i].VirtualAddress + lpSecHdr[i].SizeOfRawData)
      {
        if (lpSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE)
        {
          *lpFixAddress = (PCHAR)Old + RelocValue;
        }
        else
        {
          *lpFixAddress = (PCHAR)New + RelocValue;
        }
        return;
      }
    }
  }
  *lpFixAddress = (PCHAR)Old + RelocValue;
  return;
}
  至此,重新加载一份新的NT内核已经完成了绝大部分,还有一些细节没有处理,等到后面遇到时再告诉各位看客老爷,先卖个关子吧,涉及到的未公开结构可以从WRK中寻找,我所使用的结构在目前的x86系统中都没有改动,所以通用。具体工程代码先不打算放出来,如果大部分朋友需要的话,我会在后续文章中放出,不过我还是希望大家能够自己动手,这样收获会比纯粹的复制粘贴更多。
  下面的内容应该是HOOK了,用来接管正常的执行流程,我会抽时间写后续内容的。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-26 06:57

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表