标 题:
对双机调试的探索
作 者: farmtest
时 间: 2014-01-21,12:26:15
链 接: http://bbs.pediy.com/showthread.php?t=183925
最近学习游戏保护,发现VM+WINDBG双机调试不能使用,以游戏保护*P为例,经过论坛搜索,发现有以下几点动了手脚:
1. kdDebuggerEnabled变量不停的清零,清零的代码还做了检测,所以意味着不能轻易修改*P的代码,
kdDebuggerEnabled是windows全局变量,用来标识内核调试是否被启用
开启调试状态:*(PBYTE)KdDebuggerEnabled=0x01;
禁止调试状态:*(PBYTE)KdDebuggerEnabled=0x00;
2. KiDebugRoutine变量不停设置为KdpStub函数, 其写入的代码同上做了检测,不能随便修改
KiDebugRoutine是函数指针,内核调试引擎的异常处理回调函数指针。当内核调试引擎活动时,它指向KdpTrap函数,否则指向KdpStub函数
3. KdSendPacket和KdReceivePacket函数的IAT HOOK,且在HOOK的地方也做了检测,所以同样不能简单修改来达到目的
KdSendPacket KDCOM函数,发送数据包
KdReceivePacket KDCOM函数,接收数据包
以上就是*p的3块防护及其含义,下面一块一块来探索一番
第一块:
正常情况下ctrl+break中断到windbg,
代码:
1: kd> dd kdDebuggerEnabled
83f9d96c 00000001 00000000 00000000 db1dbbbb
发现此时kdDebuggerEnabled为1,
代码:
1: kd> ed kdDebuggerEnabled 0
1: kd> dd kdDebuggerEnabled
83f9d96c 00000000 00000000 00000000 db1dbbbb
修改后F5虚拟机跑起来,但是ctrl+break不能断下了
首先想到观察下ctrl+break的函数流程, 看看这个变量对流程中的哪个地方造成了阻碍,
双机调试正常情况下ctrl+break中断到windbg栈回溯如下:
代码:
00 807eaae0 83eb40cb nt!RtlpBreakWithStatusInstruction
01 807eaae8 83eb409d nt!KdCheckForDebugBreak+0x22
02 807eab98 83eb937f nt!KeUpdateRunTime+0x164
03 807eac20 83eb0e0d nt!PoIdle+0x524
04 807eac24 00000000 nt!KiIdleLoop+0xd
或者
代码:
00 807eaae0 83e950cb nt!RtlpBreakWithStatusInstruction
01 807eaae8 83e9509d nt!KdCheckForDebugBreak+0x22
02 807eab18 8422f430 nt!KeUpdateRunTime+0x164
03 807eab18 937d05d6 hal!HalpClockInterruptPn+0x158
...
无论从哪里发起最后的几个函数流程都是一样KeUpdateRunTime-> KdCheckForDebugBreak-> RtlpBreakWithStatusInstruction
自然想看看这几个函数中的代码,看看是否和kdDebuggerEnabled有关,
代码:
1: kd> uf KeUpdateRunTime
后ctrl+F搜索kdDebuggerEnabled发现一处代码
83eb4082 803d6cd9f98300 cmp byte ptr [nt!KdDebuggerEnabled (83f9d96c)],0
83eb4089 7412 je nt!KeUpdateRunTime+0x164 (83eb409d)
83eb408b a1740ff783 mov eax,dword ptr [nt!KiPollSlot (83f70f74)]
83eb4090 3b86cc030000 cmp eax,dword ptr [esi+3CCh]
83eb4096 7505 jne nt!KeUpdateRunTime+0x164 (83eb409d)
83eb4098 e80c000000 call nt!KdCheckForDebugBreak (83eb40a9)
83eb409d 5f pop edi
如果kdDebuggerEnabled为0则跳到83eb409d,这样就不会执行KdCheckForDebugBreak函数了,但是流程中KdCheckForDebugBreak函数必须要执行此函数后,ctrl+break才能断下,所以证明了kdDebuggerEnabled设置为0直接导致ctrl+break的失效,以此类推对余下的两个函数进行反汇编,发现如下代码:
代码:
1: kd> uf KdCheckForDebugBreak
83eb40a9 803d271df68300 cmp byte ptr [nt!KdPitchDebugger (83f61d27)],0
83eb40b0 7519 jne nt!KdCheckForDebugBreak+0x22 (83eb40cb)
83eb40b2 803d6cd9f98300 cmp byte ptr [nt!KdDebuggerEnabled (83f9d96c)],0
83eb40b9 7410 je nt!KdCheckForDebugBreak+0x22 (83eb40cb)
83eb40bb e81f000000 call nt!KdPollBreakIn (83eb40df)
83eb40c0 84c0 test al,al
83eb40c2 7407 je nt!KdCheckForDebugBreak+0x22 (83eb40cb)
83eb40c4 6a01 push 1
83eb40c6 e801000000 call nt!DbgBreakPointWithStatus (83eb40cc)
83eb40cb c3 ret
可见此函数也用到了kdDebuggerEnabled变量,除此之外还可以发现2个问题:
1. 除了kdDebuggerEnabled变量,貌似KdPitchDebugger变量也比较重要,百度以下发现: 如果KdPitchDebugger为TRUE,即在bcd中指定了nodebug,这样也就不支持调试了,所以也算一个内核调试是否启用的标志,
2. 流程中KdCheckForDebugBreak调用的是RtlpBreakWithStatusInstruction,但是代码中却没有发现他的调用,唯一调用的两个函数是KdPollBreakIn和DbgBreakPointWithStatus,抱着试一试的心态反汇编这两个函数发现:
代码:
1: kd> u DbgBreakPointWithStatus
nt!DbgBreakPointWithStatus:
83eb40cc 8b442404 mov eax,dword ptr [esp+4]
nt!RtlpBreakWithStatusInstruction:
83eb40d0 cc int 3
83eb40d1 c20400 ret 4
发现RtlpBreakWithStatusInstruction其实是DbgBreakPointWithStatus的一个标签,所以流程中KdCheckForDebugBreak实际上是调用的DbgBreakPointWithStatus
由于KdCheckForDebugBreak中有对KdPollBreakIn的调用,所以顺道反汇编以下KdPollBreakIn函数,偶然也发现一处和kdDebuggerEnabled相关的代码:
代码:
83eb40fa 381d6cd9f983 cmp byte ptr [nt!KdDebuggerEnabled (83f9d96c)],bl
总结一下上面的发现: 当把kdDebuggerEnabled设置为0后,ctrl+break失效,跟踪流程发现kdDebuggerEnabled和KdPitchDebugger标志直接影响中断到windbg,反汇编流程中的函数发现:
包含KdDebuggerEnabled的函数有:
KeUpdateRunTime
KdCheckForDebugBreak
KdPollBreakIn
包含KdPitchDebugger的函数有:
KdCheckForDebugBreak
KdPollBreakIn
接下来就是编程解决问题,思路是转移变量,把这些函数中的KdDebuggerEnabled和KdPitchDebugger变量设置为自己驱动模块中的全局变量,部分代码如下:
代码:
//KdDebuggerEnabled变量存储
BOOLEAN gKdDebuggerEnabled=TRUE;
//KdPitchDebugger变量存储
BOOLEAN gKdPitchDebugger=FALSE;
//转移win7中的KdDebuggerEnabled和KdPitchDebugger变量
void MoveVariable_Win7(IN BOOL b***)
{
ULONG ulAddr, ulAddr2;
//----------------------------改写KeUpdateRunTime中的KdDebuggerEnabled
//得到原内核的KeUpdateRunTime地址
ulAddr=GetOriginalProcAddr(L"KeUpdateRunTime");
//定位KdDebuggerEnabled
//特征码
//83ec9082 803d6c29fb8300 cmp byte ptr [nt!KdDebuggerEnabled (83fb296c)],0
//83ec9089 7412 je nt!KeUpdateRunTime+0x164 (83ec909d)
//83ec908b a1745ff883 mov eax,dword ptr [nt!KiPollSlot (83f85f74)]
UCHAR szSig1[10] = {0x80, 0x3d, '?', '?', '?', '?', '?', 0x74, 0x12, 0xa1};
//特征码定位KdDebuggerEnabled
ulAddr=SearchCode((PUCHAR)ulAddr, szSig1, 10, 0x200);
ulAddr+=2;
//改写KdDebuggerEnabled变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKdDebuggerEnabled;
PageProtect(TRUE);
//----------------------------改写KdCheckForDebugBreak中的KdDebuggerEnabled
//得到KdCheckForDebugBreak地址
//特征码
//83ec9098 e80c000000 call nt!KdCheckForDebugBreak (83ec90a9)
//83ec909d 5f pop edi
UCHAR szSig2[6] = {0xe8, '?', '?', '?', '?', 0x5f};
ulAddr=SearchCode((PUCHAR)ulAddr, szSig2, 6, 0x100);
ulAddr=ulAddr+*(PULONG)((PUCHAR)ulAddr+1)+5;
//记录KdCheckForDebugBreak函数地址 用于KdPitchDebugger变量的处理
ulAddr2=ulAddr;
//特征码定位KdDebuggerEnabled
//83ec90b2 803d6c29fb8300 cmp byte ptr [nt!KdDebuggerEnabled (83fb296c)],0
//83ec90b9 7410 je nt!KdCheckForDebugBreak+0x22 (83ec90cb)
//83ec90bb e81f000000 call nt!KdPollBreakIn (83ec90df)
UCHAR szSig3[10] = {0x80, 0x3d, '?', '?', '?', '?', '?', 0x74, 0x10, 0xe8};
ulAddr=SearchCode((PUCHAR)ulAddr, szSig3, 10, 0x100);
ulAddr+=2;
//改写KdDebuggerEnabled变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKdDebuggerEnabled;
PageProtect(TRUE);
//----------------------------改写KdCheckForDebugBreak中的KdPitchDebugger
//定位KdPitchDebugger变量
//由于KdPitchDebugger变量在函数开头偏移2处 所以直接偏移定位KdPitchDebugger
//83eb30a9 803d270df68300 cmp byte ptr [nt!KdPitchDebugger (83f60d27)],0
//83eb30b0 7519 jne nt!KdCheckForDebugBreak+0x22 (83eb30cb)
ulAddr2+=2;
//改写KdPitchDebugger变量
PageProtect(FALSE);
*(PULONG)ulAddr2=(ULONG)&gKdPitchDebugger;
PageProtect(TRUE);
//----------------------------改写KdPollBreakIn中的KdDebuggerEnabled
//得到KdPollBreakIn地址
//特征码
//83ebe0bb e81f000000 call nt!KdPollBreakIn (83ebe0df)
//83ebe0c0 84c0 test al,al
UCHAR szSig4[7] = {0xe8, '?', '?', '?', '?', 0x84, 0xc0};
ulAddr=SearchCode((PUCHAR)ulAddr, szSig4, 7, 0x100);
ulAddr=ulAddr+*(PULONG)((PUCHAR)ulAddr+1)+5;
//记录KdPollBreakIn函数地址 用于KdPitchDebugger变量的处理
ulAddr2=ulAddr;
//特征码定位KdDebuggerEnabled
//83ebe0f7 885dff mov byte ptr [ebp-1],bl
//83ebe0fa 381d6c79fa83 cmp byte ptr [nt!KdDebuggerEnabled (83fa796c)],bl
//83ebe100 0f84c0000000 je nt!KdPollBreakIn+0xe7 (83ebe1c6)
UCHAR szSig5[11] = {0x88, 0x5d, 0xff, 0x38, 0x1d, '?', '?', '?', '?', 0x0f, 0x84};
ulAddr=SearchCode((PUCHAR)ulAddr, szSig5, 11, 0x100);
ulAddr+=5;
//改写KdDebuggerEnabled变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKdDebuggerEnabled;
PageProtect(TRUE);
//----------------------------改写KdPollBreakIn中的KdPitchDebugger
//特征码定位KdPitchDebugger
//83eb30e6 33db xor ebx,ebx
//83eb30e8 381d270df683 cmp byte ptr [nt!KdPitchDebugger (83f60d27)],bl
//83eb30ee 7407 je nt!KdPollBreakIn+0x18 (83eb30f7)
UCHAR szSig6[10] = {0x33, 0xdb, 0x38, 0x1d, '?', '?', '?', '?', 0x74, 0x07};
ulAddr2=SearchCode((PUCHAR)ulAddr2, szSig6, 10, 0x100);
ulAddr2+=4;
//改写KdPitchDebugger变量
PageProtect(FALSE);
*(PULONG)ulAddr2=(ULONG)&gKdPitchDebugger;
PageProtect(TRUE);
}
经过以上转移以后 再次清零KdDebuggerEnabled变量后,也可以ctrl+break断点到windbg中,第一块问题就基本解决了
第二块:
正常情况下观察KiDebugRoutine
代码:
1: kd> dd KiDebugRoutine
83fa2b20 841664f2 83f16683 00000000 0311870a
83fa2b30 00000bb8 00000011 5385d2ba d717548f
83fa2b40 83eb7d5c 00000000 00000191 83eb83a4
83fa2b50 986fa000 00000000 00000339 986fb02c
83fa2b60 00000100 00000000 00000000 83fa2b68
83fa2b70 00000340 00000340 00000007 00000000
83fa2b80 86cdb6b8 86cdb5f0 86ce3838 86ce39c8
83fa2b90 86ce3900 00000000 86d37040 00000000
1: kd> u 841664f2
nt!KdpTrap:
841664f2 8bff mov edi,edi
841664f4 55 push ebp
841664f5 8bec mov ebp,esp
841664f7 51 push ecx
841664f8 51 push ecx
841664f9 8b4510 mov eax,dword ptr [ebp+10h]
841664fc 33d2 xor edx,edx
841664fe 813803000080 cmp dword ptr [eax],80000003h
将其设置为KdpStub函数
代码:
1: kd> ed KiDebugRoutine KdpStub
1: kd> dd KiDebugRoutine
83fa2b20 83f16983 83f16683 00000000 0311870a
83fa2b30 00000bb8 00000011 5385d2ba d717548f
83fa2b40 83eb7d5c 00000000 00000191 83eb83a4
83fa2b50 986fa000 00000000 00000339 986fb02c
83fa2b60 00000100 00000000 00000000 83fa2b68
83fa2b70 00000340 00000340 00000007 00000000
83fa2b80 86cdb6b8 86cdb5f0 86ce3838 86ce39c8
83fa2b90 86ce3900 00000000 86d37040 00000000
1: kd> u 83f16983
nt!KdpStub:
83f16983 8bff mov edi,edi
83f16985 55 push ebp
83f16986 8bec mov ebp,esp
83f16988 53 push ebx
83f16989 56 push esi
83f1698a 8b7510 mov esi,dword ptr [ebp+10h]
83f1698d 33db xor ebx,ebx
83f1698f 813e03000080 cmp dword ptr [esi],80000003h
Windbg F5运行起来,接着ctrl+break发现VM死机了,看来这个影响还蛮大的,重启后IDA打开内核搜索全部KiDebugRoutine,发现调用它的代码全部都在KiDispatchException中,反汇编KiDispatchException搜索KiDebugRoutine相关代码,过程和第一块一样,发现以下三个结果:
代码:
83f04027 ff15208bfb83 call dword ptr [nt!KiDebugRoutine (83fb8b20)]
83f0404f ff15208bfb83 call dword ptr [nt!KiDebugRoutine (83fb8b20)]
83f040c8 ff15208bfb83 call dword ptr [nt!KiDebugRoutine (83fb8b20)]
看来先前修改了KiDebugRoutine,造成的死机就是在KiDispatchException中出现的问题,现在编程解决问题,思路和上一块一样,转移KiDebugRoutine变量,部分代码如下:
代码:
//KiDebugRoutine变量存储
ULONG gKiDebugRoutine=0;
//转移win7中的KiDebugRoutine变量
void MoveKiDebugRoutine_Win7(IN BOOL b***)
{
//得到KdpTrap地址
ULONG ulAddr=GetOriginalProcAddr(L"KdpTrap");
//设置自定义的KiDebugRoutine变量
gKiDebugRoutine=ulAddr;
//得到原内核KiDispatchException地址
ulAddr=GetOriginalProcAddr(L"KiDispatchException");
//---------------------------------------第一处KiDebugRoutine
//特征码定位KiDebugRoutine
//83eb5027 ff15209bf683 call dword ptr [nt!KiDebugRoutine (83f69b20)]
//83eb502d 84c0 test al,al
UCHAR szSig[8] = {0xff, 0x15, '?', '?', '?', '?', 0x84, 0xc0};
ulAddr=SearchCode((PUCHAR)ulAddr, szSig, 8, 0x200);
if(!ulAddr)
{
KdPrint(("\n[MoveKiDebugRoutine_Win7]: The signature is not found in KiDispatchException"));
return;
}
ulAddr+=2;
//改写KiDebugRoutine变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKiDebugRoutine;
PageProtect(TRUE);
//---------------------------------------第二处KiDebugRoutine
//特征码与前边一样
ulAddr=SearchCode((PUCHAR)ulAddr, szSig, 8, 0x200);
ulAddr+=2;
//改写KiDebugRoutine变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKiDebugRoutine;
PageProtect(TRUE);
//---------------------------------------第三处KiDebugRoutine
//特征码与前边一样
ulAddr=SearchCode((PUCHAR)ulAddr, szSig, 8, 0x200);
ulAddr+=2;
//改写KiDebugRoutine变量
PageProtect(FALSE);
*(PULONG)ulAddr=(ULONG)&gKiDebugRoutine;
PageProtect(TRUE);
}
这样一来第二块问题也基本解决了,这个和第一块原理一样,接下来就是第三块了
第三块:
反汇编KdSendPacket
代码:
1: kd> u KdSendPacket
nt!KdSendPacket:
83e60200 ff25cc01e583 jmp dword ptr [nt!_imp__KdSendPacket (83e501cc)]
83e60206 90 nop
83e60207 90 nop
83e60208 90 nop
83e60209 90 nop
83e6020a 90 nop
1: kd> dd 83e501cc
83e501cc 80bc757c 80bc7170 80bc7160 80bc709c
83e501dc 80bc7150 00000000 8c8e3a04 8c8b5cb8
83e501ec 8c8b35bc 8c8e485c 8c8b5db0 8c8e37e0
83e501fc 8c8e7db2 8c8e4b4a 8c8e7df2 8c8e7538
83e5020c 8c8e4fd2 8c8c19d0 8c8b3586 8c8e2b44
83e5021c 8c8e4468 8c8e4a40 8c8e3c96 8c8e2f5e
83e5022c 8c8e2894 8c8c19d8 8c8e6e3e 8c8bde0c
83e5023c 8c8e810a 8c8e416a 8c8b5cee 8c8e7fc0
1: kd> dd 83e501c8
83e501c8 80bc7300 80bc757c 80bc7170 80bc7160
1: kd> u 80bc757c
80bc757c 8bff mov edi,edi
80bc757e 55 push ebp
80bc757f 8bec mov ebp,esp
80bc7581 8b4510 mov eax,dword ptr [ebp+10h]
80bc7584 83ec10 sub esp,10h
80bc7587 53 push ebx
80bc7588 33db xor ebx,ebx
80bc758a 56 push esi
可以发现其他call KdSendPacket其实是通过jmp访问了83e501cc地址里边的80bc757c,这里的83e501cc也就是iat导入表中的地址,KdSendPacket函数属于kdcom.dll,内核文件是引入的这个dll,IAT HOOK也就是把83e501cc地址里边的80bc757c修改成了自己的函数地址,80bc757c本来是真正的KdSendPacket函数地址,KdReceivePacket函数原理也是一样,解决的思路是把call KdSendPacket的代码修改为call 80bc757c这样就不经过iat表了,当然此方法牵涉的函数量比较大,部分代码实现如下:
代码:
//转移KdSendPacket和KdReceivePacket相关的信息记录
typedef struct _CODE_SIGN_KD
{
UCHAR szFunSig[30]; //函数特征码
ULONG ulSigLen; //特征码长度
ULONG ulOffset; //关键指令相对特征码的偏移
PCHAR pFunName; //函数名称
} CODE_SIGN_KD;
//需要转移的KdSendPacket相关的函数信息
CODE_SIGN_KD gKdSendPacketInfo_win7[]={
{{0x6A, 0x02, 0xC7, 0x45, 0xC0, 0x46, 0x31, 0, 0, 0xE8, '?', '?', '?', '?', 0xE9, '?', '?', '?', '?', 0x6A, 0x38}, 21, 9, "1.KdpSendWaitContinue"},
{{0x50, 0x56, 0xE8, '?', '?', '?', '?', 0xE9, '?', '?', '?', '?', 0x53}, 13, 2, "2.KdpSendWaitContinue"},
{{0xFF, 0x75, 0x98, 0x6A, 0x07, 0xE8, '?', '?', '?', '?', 0x80, 0x3D, '?', '?', '?', '?', 0x00, 0x0F, 0x84, 0x66, 0xF9, 0xFF, 0xFF}, 23, 5, "3.KdpSendWaitContinue"},
{{0x66, 0x89, 0x74, 0x24, 0x20, 0x89, 0x7C, 0x24, 0x24, 0xE8, '?', '?', '?', '?', 0xE8, '?', '?', '?', '?', 0x5F}, 20, 9, "4.KdpPrintString"},
{{0x51, 0x8D, 0x45, 0xF8, 0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0x5F, 0x5E}, 14, 7, "5.KdGetInternalBreakpoint"},
{{0x53, 0x8D, 0x45, 0xF4, 0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0x5F, 0x5E}, 14, 7, "6.KdpGetContext"},
{{0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0x5B, 0xC9}, 10, 3, "7.KdpSetContext"},
{{0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0xC9, 0xC3}, 10, 3, "8.KdpReadPhysicalMemory"},
{{0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0x8B, 0x46, 0x14, 0xEB, 0x1B}, 13, 3, "9.KdpWriteBreakPointEx"},
{{0x6A, 0x02, 0xC7, 0x46, 0x08, 0x01, 0x00, 0x00, 0xC0, 0xE8, '?', '?', '?', '?', 0x8B, 0x46, 0x08, 0x5F}, 18, 9, "10.KdpWriteBreakPointEx"},
{{0x50, 0x6A, 0x02, 0xE8, '?', '?', '?', '?', 0x5F, 0x5B}, 10, 3, "11.KdpRestoreBreakPointEx"},
{{0x6A, 0x02, 0x89, 0x7D, 0xD0, 0xE8, '?', '?', '?', '?', 0x5E, 0x5B}, 12, 5, "12.KdpSearchMemory"},
{{0x89, 0x4E, 0x08, 0x89, 0x75, 0xDC, 0xE8, '?', '?', '?', '?', 0x5F, 0x5B}, 13, 6, "13.KdpFillMemory"},
{{0x89, 0x4C, 0x24, 0x24, 0x89, 0x74, 0x24, 0x1C, 0xE8, '?', '?', '?', '?', 0x5E, 0x8B, 0xE5}, 16, 8, "14.KdpSendTraceData"},
{{0x89, 0x4C, 0x24, 0x2C, 0x89, 0x74, 0x24, 0x24, 0xE8, '?', '?', '?', '?', 0x6A, 0x10, 0x58}, 16, 8, "15.KdpPromptString"},
{{0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x80, 0x3D, '?', '?', '?', '?', 0x00, 0x0F, 0x84, 0x79, 0xFF, 0xFF, 0xFF}, 21, 3, "16.KdpCreateRemoteFile"},
{{0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x38, 0x1D, '?', '?', '?', '?', 0x75, 0x49}, 16, 3, "17.KdpReadRemoteFile"},
{{0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x38, 0x1D, '?', '?', '?', '?', 0x74, 0xAD}, 16, 3, "18.KdpCloseRemoteFile"},
{0}
};
//需要转移的KdReceivePacket相关的函数信息
CODE_SIGN_KD gKdReceivePacketInfo_win7[]={
{{0x50, 0x6A, 0x08, 0xE8, '?', '?', '?', '?', 0x85, 0xC0, 0x75, 0x02, 0xB3, 0x01}, 14, 3, "1.KdpPollBreakInWithPortLock"},
{{0x53, 0x6A, 0x08, 0xE8, '?', '?', '?', '?', 0x85, 0xC0, 0x75, 0x0B, 0xC6, 0x45, 0xFF, 0x01}, 16, 3, "2.KdPollBreakIn"},
{{0x5E, 0x56, 0xE8, 0x05, 0xE8, '?', '?', '?', '?', 0x0F, 0x84, 0x5E, 0x06, 0x00, 0x00, 0x83, 0xF8, 0x01}, 18, 2, "3.KdpSendWaitContinue"},
{{0x50, 0x6A, 0x03, 0xE8, '?', '?', '?', '?', 0x83, 0xF8, 0x02, 0x74, 0x3A, 0x85, 0xC0}, 15, 3, "4.KdpPromptString"},
{{0x8D, 0x45, 0xE4, 0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x83, 0xF8, 0x01, 0x74, 0xE7, 0x85, 0xC0}, 18, 6, "5.KdpCreateRemoteFile"},
{{0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x83, 0xF8, 0x01, 0x74, 0xE7, 0x3B, 0xC3, 0x75, 0x16}, 17, 3, "6.KdpReadRemoteFile"},
{{0x50, 0x6A, 0x0B, 0xE8, '?', '?', '?', '?', 0x83, 0xF8, 0x01, 0x74, 0xE7, 0x3B, 0xC3, 0x74, 0x2D}, 17, 3, "7.KdpCloseRemoteFile"},
{0}
};
//HOOK KdSendPacket相关函数
void HookKdSendPacket(IN BOOL b***)
{
//得到KdSendPacket函数地址 存在于kdcom模块
ULONG ulKdSendPacketAddr = GetOriginalProcAddr(L"KdSendPacket");
//得到内核中PAGEKD节区的内存基地址和大小
ULONG ulBase, ulSize, ulCode;
GetPeSectionInfo(GetVarKernelBase(), "PAGEKD", &ulBase, &ulSize);
//循环特征码 转移KdSendPacket函数调用
for(int i=0; gKdSendPacketInfo_win7[i].ulOffset; i++)
{
//得到特征码地址
ulCode=SearchCode((PUCHAR)ulBase, gKdSendPacketInfo_win7[i].szFunSig, gKdSendPacketInfo_win7[i].ulSigLen, ulSize);
if(ulCode==0)
{
KdPrint(("\n[HookKdSendPacket]: Error! FunName:%s, codeAddr:0x%x, i:%d\n ", gKdSendPacketInfo_win7[i].pFunName, ulCode, i));
return;
}
//得到修改点地址
ulCode+=gKdSendPacketInfo_win7[i].ulOffset;
//设置CALL指令
ReplaceProcAddr(ulKdSendPacketAddr, ulCode, FALSE);
}
}
//HOOK KdReceivePacket相关函数
void HookKdReceivePacket(IN BOOL b***)
{
//得到KdReceivePacket函数地址 存在于kdcom模块
ULONG ulKdReceivePacketAddr = GetOriginalProcAddr(L"KdReceivePacket");
//得到内核中PAGEKD节区和.text节区的内存基地址和大小
ULONG ulBase, ulSize, ulBase2, ulSize2, ulCode;
GetPeSectionInfo(GetVarKernelBase(), "PAGEKD", &ulBase, &ulSize);
GetPeSectionInfo(GetVarKernelBase(), ".text", &ulBase2, &ulSize2);
//循环特征码 转移KdReceivePacket函数调用
for(int i=0; gKdReceivePacketInfo_win7[i].ulOffset; i++)
{
ASSERT(arraysize(gKdReceivePacketInfo_win7)>i);
//得到特征码地址
ulCode=SearchCode((PUCHAR)ulBase, gKdReceivePacketInfo_win7[i].szFunSig, gKdReceivePacketInfo_win7[i].ulSigLen, ulSize);
if(ulCode==0)
{
ulCode=SearchCode((PUCHAR)ulBase2, gKdReceivePacketInfo_win7[i].szFunSig, gKdReceivePacketInfo_win7[i].ulSigLen, ulSize2);
if(ulCode==0)
{
KdPrint(("\n[HookKdReceivePacket]: Error! FunName:%s, codeAddr:0x%x, i:%d\n ", gKdReceivePacketInfo_win7[i].pFunName, ulCode, i));
return;
}
}
//得到修改点地址
ulCode+=gKdReceivePacketInfo_win7[i].ulOffset;
//保存修改点
gKdReceivePacketSave[i].ulFixFunAddr=ulCode;
//保存修改代码
RtlCopyMemory((PUCHAR)gKdReceivePacketSave[i].szFunCode, (PUCHAR)ulCode, 5);
//设置CALL指令
ReplaceProcAddr(ulKdReceivePacketAddr, ulCode, FALSE);
}
}
经过以上的替换基本就可以完成第三块的功能了,需要说明的是,上面的函数特征码是通过IDA的全部搜索功能完成的,后经尝试并不需要替换全部的函数,这个可以自行实验删减
继续升级:
上面三块经编译测试发现已经可以ctrl+break断在windbg中了,如图:
但是用PCHunter观察内核修改,发现修改的地方全部显示出来了,多且乱,这样一来很容易被封堵掉,具体如图:
这里显示的是47个修改点,再对比下正常情况下的内核修改
只有17个修改点,可见动的手脚太大了,所以需要进一步升级程序,目标是是尽可能减少原内核的修改,思路是加载一个新内核,把调试相关的流程引入到新内核中,然后在新内核中进行以上修改,这样一来基本满足先前的目标了.具体原理如下:
第一点:
在双机调试win7时,在windbg中用ctrl+break中断到windbg后,栈回溯有以下结果:
代码:
00 83f71a80 83ec50cb nt!RtlpBreakWithStatusInstruction
01 83f71a88 83ec509d nt!KdCheckForDebugBreak+0x22
02 83f71ab8 83ec4f2b nt!KeUpdateRunTime+0x164
03 83f71b14 83ec9c17 nt!KeUpdateSystemTime+0x613
或者
00 807eaae0 83e950cb nt!RtlpBreakWithStatusInstruction
01 807eaae8 83e9509d nt!KdCheckForDebugBreak+0x22
02 807eab18 8422f430 nt!KeUpdateRunTime+0x164
03 807eab18 937d05d6 hal!HalpClockInterruptPn+0x158
可以发现各个流程都会经过KeUpdateRunTime,这里HOOK这个函数,让流程进入新内核
第二点:
调试的符号加载过程是通过KdpSendWaitContinue来发送消息的 这个函数的调用来源于
KdpReportLoadSymbolsStateChange和KdpReportCommandStringStateChange 栈回溯如下:
代码:
00 9051b21c 8412f4ca nt!KdpReportLoadSymbolsStateChange+0x2
01 9051b240 8412f570 nt!KdpSymbol+0x53
02 9051b270 83eb702d nt!KdpTrap+0x7e
03 9051b80c 83e40e06 nt!KiDispatchException+0x16d
04 9051b874 83e416a8 nt!CommonDispatchException+0x4a
05 9051b874 83e1a579 nt!KiTrap03+0xb8
06 9051b900 83e1b3e3 nt!DbgLoadImageSymbols+0x48
07 9051b91c 83fcaa5d nt!DbgLoadImageSymbolsUnicode+0x23
08 9051b958 83fc746d nt!MiDriverLoadSucceeded+0x183
09 9051b9dc 84051b1a nt!MmLoadSystemImage+0x720
0a 9051bb28 83e4021a nt!NtSetSystemInformation+0x967
0b 9051bb28 83e3f2dd nt!KiFastCallEntry+0x12a
0c 9051bbec 840164dc nt!ZwSetSystemInformation+0x11
0d 9051bc00 83e7fa6b nt!IopProcessWorkItem+0x23
0e 9051bc50 8400afda nt!ExpWorkerThread+0x10d
0f 9051bc90 83eb31f9 nt!PspSystemThreadStartup+0x9e
10 00000000 00000000 nt!KiThreadStartup+0x19
这是一个新线程来执行的,所以必须在其中找一个函数来HOOK,让其进入新内核,这样才便于修改,
暂时定为HOOK KiDispatchException函数让其转入新内核
第三点:
KdpSendWaitContinue函数中有个switch的代码,新内核KdpSendWaitContinue中switch反汇编如下:
代码:
8907e9ea ff2485b4a01384 jmp dword ptr nt!KdpSendWaitContinue+0x770 (841480b4)[eax*4]
8907e9ea是指令地址,跳向的地方是841480b4,它也是switch的跳转表了 内容如下:
代码:
841480b4 841479f1 84147a4e 84147b12 84147b27
841480c4 84147b3b 84147b77 84148052 84147bb3
841480d4 84147c08 84147c52 84147c8f 84147e6d
841480e4 8414805c 84147aac 84147abe 84148011
841480f4 84147e7a 84147ed5 84147ee0 84147eed
84148104 84147cc8 84147d0c 84147f05 84147f66
84148114 84147f81 84148072 84148011 84148011
84148124 84148011 84148011 84148011 84148011
84148134 84148087 84147f46 84147d43 84147d87
84148144 84148011 84148011 84147f92 84147dcb
84148154 84147e2a 84147a82 84147efa 84147fa2
84148164 84147fb2 90909090 55ff8b90 5351ec8b
查看原内核地址:
start end module name
83e1b000 8422d000 nt (pdb symbols)
可以发现switch中的跳转是到原内核,如果执行了switch跳转指令,流程又会回到原内核,所以跳转表需要相应修改
第四点:
关于int3断点和调试信息打印KdPrint都是要经过KiDispatchException的,前边第二点修改以后这里也就OK了
总结:
调试有3个流程,
一个基本的流程通过HOOK KeUpdateRunTime搞定
一个加载符号的流程,通过HOOK KiDispatchException搞定
一个int3断点和调试信息打印,也是通过HOOK KiDispatchException搞定
当整个流程都在新内核中以后 就可以任意的修改新内核了 不容易被检测到 一劳永逸
具体代码见附件,代码只是代表思路,里边有一些自定义函数,除加载新内核外,其他功能比较单一,如有需要自行实现,加载新内核的代码可以搜索论坛得到,升级后的程序,运行后ctrl+break断到windbg中,查看栈回溯可以看到:
代码:
Break instruction exception - code 80000003 (first chance)
885ee0d0 cc int 3
0: kd> kn
# ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
00 83f37ab8 83e8af2b 0x885ee0d0
01 83f37b14 83e8fc17 nt!KeUpdateSystemTime+0x613
02 83f37b14 936015d6 nt!KeUpdateSystemTimeAssist+0x13
03 83f37b98 83e9037f intelppm!C1Halt+0x4
04 83f37c20 83e87e0d nt!PoIdle+0x524
05 83f37c24 00000000 nt!KiIdleLoop+0xd
的确是进入新内核了,再程序运行前后修改点的变化
运行前:
运行后:
这里增加了2个修改点,就正是为了实现进入新内核而HOOK的两个函数,至此就升级完毕了.
后记:
从这个双机调试的探索可以看出来,其实这就是内核调试的一部分基本原理,在张老师的<软件调试>一书中全部都有,但是现在*P只是用了一部分来做修改,以后还有很大升级空间,所以跟着知识走才是王道,有空之余完全可以把<软件调试>上的内核调试知识,写入代码中,这样一来就不怕其他程序升级造成影响了,如果后期这两个函数被检测,可以根据栈回溯上下来换成其他函数, ,目的只有一个进入新内核,可换的函数从数量上来说还是可观的,此方法整体看来比较繁杂,但是后面用起来比较方便,不受至于其他程序,自己按照调试流程来就行,一劳永逸.