当前位置:首页 > 操作系统 > Windows

windows SSDT和驱动保护

  对于windwos逆向人员来说,不论是写外挂、写病毒/木马,都需要打开其他内存的空间,改写某些关键数据,达到改变其原有执行流程的目的。那么日常的工作肯定涉及到openprocess、readprocessmemory、writeprocessmemory等函数;这些函数都是怎么被调用的了?

  1、windows提供了大量的系统函数供3层的应用调用。这些函数被统一编号,入口地址放在一张表里,编号就是索引,通过编号就能找到函数的入口地址;64位的表结构可以通过windbg查看,如下:

     技术分享图片

      x64的SSDT表和32位比复杂了一些,为了便于读者理解,我用不同颜色(黄、绿、蓝、橙)做了标注;开发人员在3环调用openprocess、readprocessmemory、writeprocessmemory等函数,最终都会通过这个表找到对应的内核入口地址,进而跳转到内核空间执行;具体的函数实现可以通过逆向ntdll.dll、kerner32.dll、ntoskrl.exe等内核文件查看,这里不赘述(SSDT hook已经烂大街了,google一下资料大堆);各大厂商最初的驱动保护就是hook SSDT表的关键函数,一旦发现第三方程序打开自己的进程,直接返回false,达到保护自己进程数据不被篡改的目的;今天演示一下hook terminalprocess函数,让其无法关闭计算器或记事本的进程;

  2、通过微软官网查询得知:windwos提供的terminalProcess函数在kerner32.dll中:

技术分享图片

       技术分享图片

   用IDA打开kernerl32.dll,切换到import,发现terminalProcess是从ntdll.dll导入的

       技术分享图片

  继续追查ntdll.dll,在export找到目标函数,双击进入函数体,如下:

   技术分享图片

  这个函数有两个重要信息:

  (1)mov eax, 2Ch: 2c=44,是系统调用号(同一函数在windwos不同版本的调用号是不一样的,我刚开始做实验时总是蓝屏,调试了好长时间才发现是调用号搞错了),也就是terminalprocess在SSDT中的编号,根据这个编号就能找到函数的入口地址(当然不是直接现成地展示在表内,而要经过一些简单地计算)

       (2)通过syscall进入内核

   3、核心代码(下面的参考【3】);注意:本人的测试环境是win10.0.16299.125,调用号是0x2c;其他版本的系统可能不一样,建议读者自己用IDA查查ntdll.dll,否则直接蓝屏

#include <span>"</span><span>hook.h</span><span>"</span><span>
#include </span><span>"</span><span>asmUtil.h</span><span>"</span><span>

PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
NTTERMINATEPROCESS NtTerminateProcess </span>=<span> NULL;
ULONG OldTpVal;

</span><span>/*</span><span>
用户点击关闭,系统会调用原NtTerminateProcess,并传递ProcessHandle和ExitStatus两个参数;但SSDT已经被改成了KeBugCheckEx,所以
会先执行KeBugCheckEx。进入后又执行jmp,跳转到我们自己定义的Fake_NtTerminateProcess。这时EIP变了好几次,但是堆栈一直没变,所以
Fake_NtTerminateProcess的参数就是原NtTerminateProcess的参数ProcessHandle和ExitStatus;所以后续也能重新调回NtTerminateProcess
走原来正常的流程;
</span><span>*/</span><span>
NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus)
{
    </span><span>//</span><span>Dbg_Break();</span><span>    PEPROCESS Process;
    </span><span>//</span><span> 通过进程句柄来获取该进程所对应的FileObject对象,由于这里是进程对象,自然获得的是EPROCESS对象</span>
    NTSTATUS st = ObReferenceObjectByHandle(ProcessHandle, <span>0</span>, *PsProcessType, KernelMode, &<span>Process, NULL);
    DbgPrint(</span><span>"</span><span>
-------Fake_NtTerminateProcess called! NT_SUCCESS(st):% d------------------------
</span><span>"</span><span>,NT_SUCCESS(st));
    DbgPrint(</span><span>"</span><span>
-------Fake_NtTerminateProcess called!  st:% d------------------------
</span><span>"</span><span>, st);
    </span><span>if</span> (NT_SUCCESS(st)) <span>//</span><span>#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)</span><span>    {
        </span><span>//</span><span>if (!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe"))
        </span><span>//</span><span>if (strcmp(PsGetProcessImageFileName(Process), "Calculator.exe") == 0)</span>
        DbgPrint(<span>"</span><span>
-------PsGetProcessImageFileName(Process):% s------------------------
</span><span>"</span><span>, PsGetProcessImageFileName(Process));
        </span><span>if</span> ((!_stricmp(PsGetProcessImageFileName(Process), <span>"</span><span>Calculator.exe</span><span>"</span><span>))
            </span>|| (!_stricmp(PsGetProcessImageFileName(Process), <span>"</span><span>notepad.exe</span><span>"</span><span>)))
        {
            </span><span>//</span><span>ObDeReferenceObject(&Process);</span><span>return</span><span> STATUS_ACCESS_DENIED;
        }
        </span><span>else</span><span> 
        {
            </span><span>//</span><span>ObDeReferenceObject(&Process);</span><span>/*</span><span>这个已经被挂钩了,会不会形成死循环????</span><span>*/</span><span>return</span><span> NtTerminateProcess(ProcessHandle, ExitStatus);
        }
            
    }
    </span><span>else</span><span> 
    {
        </span><span>return</span><span> STATUS_ACCESS_DENIED;
    }
        
}
</span><span>/*</span><span>关闭内核页面写保护</span><span>*/</span><span>
KIRQL WPOFFx64()
{
    KIRQL irql </span>=<span> KeRaiseIrqlToDpcLevel();
    UINT64 cr0 </span>=<span> __readcr0();
    cr0 </span>&= <span>0xfffffffffffeffff</span><span>;
    __writecr0(cr0);
    _disable();
    </span><span>return</span><span> irql;
}
</span><span>/*</span><span>打开内核页面写保护</span><span>*/</span><span>void</span><span> WPONx64(KIRQL irql)
{
    UINT64 cr0 </span>=<span> __readcr0();
    cr0 </span>|= <span>0x10000</span><span>;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}
</span><span>//</span><span> win10的变了,用下面的替代</span><span>ULONGLONG GetKeServiceDescriptorTable64_win10()
{
    PUCHAR StartSearchAddress </span>= (PUCHAR)__readmsr(<span>0xC0000082</span><span>);
    PUCHAR EndSearchAddress </span>= StartSearchAddress + <span>0x500</span><span>;
    PUCHAR i </span>=<span> NULL;
    UCHAR b1 </span>= <span>0</span>, b2 = <span>0</span>, b3 = <span>0</span><span>;
    ULONG templong </span>= <span>0</span><span>;
    ULONGLONG addr </span>= <span>0</span><span>;
    </span><span>for</span> (i = StartSearchAddress; i < EndSearchAddress; i++<span>)
    {
        </span><span>if</span> (MmIsAddressValid(i) && MmIsAddressValid(i + <span>1</span>) && MmIsAddressValid(i + <span>2</span><span>))
        {
            b1 </span>= *<span>i;
            b2 </span>= *(i + <span>1</span><span>);
            b3 </span>= *(i + <span>2</span><span>);
            </span><span>if</span> (b1 == <span>0x4c</span> && b2 == <span>0x8d</span> && b3 == <span>0x15</span>) <span>//</span><span>4c8d15</span><span>            {
                memcpy(</span>&templong, i + <span>3</span>, <span>4</span><span>);
                addr </span>= (ULONGLONG)templong + (ULONGLONG)i + <span>7</span><span>;
                </span><span>return</span><span> addr;
            }
        }
    }
    </span><span>return</span><span>0</span><span>;
}

</span><span>/*</span><span>
根据调用号找到目标内核函数地址
kd> x nt!KeServiceDescriptorTable
fffff803`4e9a1880 nt!KeServiceDescriptorTable = <no type information>
kd> dq fffff803`4e9a1880
fffff803`4e9a1880  fffff803`4e839c10 00000000`00000000
fffff803`4e9a1890  00000000`000001d0 fffff803`4e83a354

注意事项:
1、这4个都是指针,都是8字节的;
ServiceTableBase:fffff803`4e839c10
ServiceCounterTableBase:00000000`00000000
NumberOfServices:00000000`000001d0
ParamTableBase:fffff803`4e83a354
2、ServiceTableBase存储的是4字节的偏移:
(2.2) 第0x2c=44号函数NtTerminateProcess偏移:
kd> dd fffff803`4e839c10+0x29*4
fffff803`4e839cb4  fd9c8d00 01a27c00 01a99001 02150f00;注意低位在后面
0x29函数偏移:fffff803`4e839c10 + 02150f00>>4 =fffff803`4e839c10 + 2150F0? = FFFF F803 4EA4 ED00,和下面NtTerminateProcess的起始地址是吻合的:

kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc          mov     r11,rsp
fffff803`4ea4ed03 49895b10        mov     qword ptr [r11+10h],rbx
fffff803`4ea4ed07 49897320        mov     qword ptr [r11+20h],rsi

</span><span>*/</span><span>
ULONGLONG GetSSDTFuncCurAddr(ULONG id)
{
    LONG dwtmp </span>= <span>0</span><span>;
    PULONG ServiceTableBase </span>=<span> NULL;
    ServiceTableBase </span>= (PULONG)KeServiceDescriptorTable-><span>ServiceTableBase;
    dwtmp </span>=<span> ServiceTableBase[id];
    dwtmp </span>= dwtmp >> <span>4</span><span>;
    </span><span>return</span> (LONGLONG)dwtmp +<span> (ULONGLONG)ServiceTableBase;
}

</span><span>/*</span><span>
(2.3)反过来求偏移
(2.3.1)kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc          mov     r11,rsp

(2.3.2)差距:
nt!NtTerminateProcess:fffff803`4ea4ed00 - ServiceTableBase:fffff803`4e839c10 = 21 50F0
(2.3.3)偏移:
21 50F0 << 4 = 215 0F00
</span><span>*/</span><span>
ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
    ULONG dwtmp </span>= <span>0</span><span>;
    PULONG ServiceTableBase </span>=<span> NULL;
    ServiceTableBase </span>= (PULONG)KeServiceDescriptorTable-><span>ServiceTableBase;
    dwtmp </span>= (ULONG)(FuncAddr -<span> (ULONGLONG)ServiceTableBase);
    </span><span>return</span> dwtmp << <span>4</span><span>;
}

</span><span>/*</span><span>
SSDT在ntoskrnl中;内核函数和用户自己的驱动不在一个4GB空间,32位的偏移是直接跳不过去的;
修改这个偏移地址的值,使之跳转到  KeBugCheckEx ,然后在 x KeBugCheckEx  
的头部写一个 2 12  字节的  mov - - jmp ,这是一个可以跨越  4GB  ! 的跳转,跳到我们的函数里!
</span><span>*/</span><span>
VOID FuckKeBugCheckEx()
{
    KIRQL irql;
    ULONGLONG myfun;
    UCHAR jmp_code[] </span>= <span>"</span><span>x48xB8xFFxFFxFFxFFxFFxFFxFFx00xFFxE0</span><span>"</span><span>;
    </span><span>/*</span><span>通过jmp跳转,而不是call,可以让Fake_NtTerminateProcess直接利用原NtTerminateProcess
    留下的参数</span><span>*/</span><span>
    myfun </span>=<span> (ULONGLONG)Fake_NtTerminateProcess;
    memcpy(jmp_code </span>+ <span>2</span>, &myfun, <span>8</span><span>);
    irql </span>=<span> WPOFFx64();
    memset(KeBugCheckEx, </span><span>0x90</span>, <span>15</span><span>);
    memcpy(KeBugCheckEx, jmp_code, </span><span>12</span><span>);
    WPONx64(irql);
}

</span><span>/*</span><span>
填写KeBugCheckEx的地址
在KeBugCheckEx填写jmp,跳到Fake_NtTerminateProcess
不能直接填写Fake_NtTerminateProcess的地址,因为它们不再同一个4GB
</span><span>*/</span><span>
VOID HookSSDT(PSYSTEM_SERVICE_TABLE received)
{
    KIRQL irql;
    ULONGLONG dwtmp </span>= <span>0</span><span>;
    PULONG ServiceTableBase </span>=<span> NULL;
    KeServiceDescriptorTable </span>=<span> received;
    </span><span>//</span><span>get old address
    </span><span>//</span><span>Dbg_Break();</span>
    NtTerminateProcess = (NTTERMINATEPROCESS)GetSSDTFuncCurAddr(<span>44</span><span>);
    DbgPrint(</span><span>"</span><span>
------------------------Old_NtTerminateProcess: %llx-----------------------
</span><span>"</span><span>, (ULONGLONG)NtTerminateProcess);
    </span><span>//</span><span>set kebugcheckex
    </span><span>//</span><span>Dbg_Break();</span><span>    FuckKeBugCheckEx();
    </span><span>//</span><span>show new address</span>
    ServiceTableBase = (PULONG)KeServiceDescriptorTable-><span>ServiceTableBase;
    </span><span>//</span><span>OldTpVal = ServiceTableBase[41];    </span><span>//</span><span>win7编号是0x29 = 41</span>
    OldTpVal = ServiceTableBase[<span>44</span>];    <span>//</span><span>win10逆向ntdll的时候发现编号是0x2c = 44</span>
    irql =<span> WPOFFx64();
    </span><span>/*</span><span>
    我们挂钩的函数是KeBugCheckEx,所以把该函数的偏移算出来(只有32位,在4GB内)
    把SSDT原本terminalProcess的地方替换掉(都在SSDT,在同一个4GB范围内)
    这样一旦调用terminalProcess,实际会调用KeBugCheckEx,然后再到我们自己的代码;
    </span><span>*/</span><span>
    ServiceTableBase[</span><span>44</span>] =<span> GetOffsetAddress((ULONGLONG)KeBugCheckEx);
    WPONx64(irql);
    DbgPrint(</span><span>"</span><span>
------------------------KeBugCheckEx: %llx-----------------------
</span><span>"</span><span>, (ULONGLONG)KeBugCheckEx);
    DbgPrint(</span><span>"</span><span>
------------------------New_NtTerminateProcess: %llx-----------------------
</span><span>"</span>, GetSSDTFuncCurAddr(<span>44</span><span>));
}

VOID UnhookSSDT()
{
    KIRQL irql;
    PULONG ServiceTableBase </span>=<span> NULL;
    ServiceTableBase </span>= (PULONG)KeServiceDescriptorTable-><span>ServiceTableBase;
    </span><span>//</span><span>set value</span>
    irql =<span> WPOFFx64();
    ServiceTableBase[</span><span>44</span>] = GetOffsetAddress((ULONGLONG)NtTerminateProcess);    <span>//</span><span>OldTpVal;</span><span>//</span><span>直接填写这个旧值也行</span><span>    WPONx64(irql);
    </span><span>//</span><span>没必要恢复KeBugCheckEx的内容了,反正执行到KeBugCheckEx时已经完蛋了。</span>
    DbgPrint(<span>"</span><span>
------------------------NtTerminateProcess: %llx-----------------------
</span><span>"</span>, GetSSDTFuncCurAddr(<span>44</span><span>));
}


ULONGLONG SearchforKeServiceDescriptorTable64(ULONGLONG StartSearchAddress, ULONGLONG EndSearchAddress)
{
    UCHAR b1 </span>= <span>0</span>, b2 = <span>0</span>, b3 = <span>0</span><span>;
    ULONG templong </span>= <span>0</span><span>;
    ULONGLONG KeServiceDescriptorTable </span>= <span>0</span><span>;

    </span><span>//</span><span>地址效验</span><span>if</span> (MmIsAddressValid(StartSearchAddress) == FALSE)<span>return</span><span> NULL;
    </span><span>if</span> (MmIsAddressValid(EndSearchAddress) == FALSE)<span>return</span><span> NULL;

    </span><span>for</span> (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++<span>)
    {
        </span><span>if</span> (MmIsAddressValid(i) && MmIsAddressValid(i + <span>1</span>) && MmIsAddressValid(i + <span>2</span><span>))
        {
            b1 </span>= *<span>i;
            b2 </span>= *(i + <span>1</span><span>);
            b3 </span>= *(i + <span>2</span><span>);
            </span><span>//</span><span>if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15);</span><span>//</span><span>4c8d15
            </span><span>//</span><span>if (b1 == "4c" && b2 == "8d" && b3 == "15" );</span><span>//</span><span>4c8d15
            </span><span>//</span><span>if (*i == 0x4c && *(i + 1) == 0x8d && *(i + 2) == 0x15);</span><span>//</span><span> 不能有;号,否则下面的代码一定会执行</span><span>if</span> (b1 == <span>0x4c</span> && b2 == <span>0x8d</span> && b3 == <span>0x15</span><span>)
            {
                DbgPrint(</span><span>"</span><span>
--------- StartSearchAddress: %llx----------------- -------------
</span><span>"</span><span>, StartSearchAddress);
                DbgPrint(</span><span>"</span><span>
--------- matched targetAddress: %llx----------------- -------------
</span><span>"</span><span>, i);
                DbgPrint(</span><span>"</span><span>
--------- targetAddress offset: %d----------------- -------------
</span><span>"</span>, (i -<span> StartSearchAddress));
                </span><span>//</span><span>Dbg_Break();</span>
                memcpy(&templong, i + <span>3</span>, <span>4</span><span>);
                KeServiceDescriptorTable </span>= (ULONGLONG)templong + (ULONGLONG)i + <span>7</span>;<span>//</span><span>i是当前地址,templong是相对SSDT的偏移</span>
                DbgPrint(<span>"</span><span>
--------- KeServiceDescriptorTable: %llx----------------- -------------
</span><span>"</span><span>, KeServiceDescriptorTable);
                </span><span>return</span><span> KeServiceDescriptorTable;
                </span><span>//</span><span>当前地址 + 长度 + 数值
                </span><span>//</span><span>fffff800`03c8c772+7 + 002320c7 = FFFFF80003EBE840</span><span>/*</span><span>
                fffff800`03c8c772 4c8d15c7202300  lea     r10,[nt!KeServiceDescriptorTable (fffff800`03ebe840)]
                fffff800`03c8c779 4c8d1d00212300  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`03ebe880)]
                </span><span>*/</span><span>
            }
        }
    }
    </span><span>return</span><span> NULL;
}

</span><span>//</span><span>获取SSDT KeServiceDescriptorTable</span><span>ULONGLONG GetKeServiceDescriptorTable64()
{
    PUCHAR pKiSystemCall64 </span>= (PUCHAR)__readmsr(<span>0xc0000082</span>);<span>//</span><span>rdmsr c0000082   </span><span>//</span><span>定位KiSystemCall64</span>
    PUCHAR EndSearchAddress = pKiSystemCall64 + <span>0x500</span>;<span>//</span><span>在1280个字节的范围内搜索</span>
    ULONGLONG KeServiceDescriptorTable = <span>0</span><span>;

    KeServiceDescriptorTable </span>=<span> SearchforKeServiceDescriptorTable64(pKiSystemCall64, EndSearchAddress);
    </span><span>if</span><span> (KeServiceDescriptorTable)
    {
        </span><span>return</span><span> KeServiceDescriptorTable;
    }
        
    </span><span>//</span><span>msr[0xc0000082]变成了KiSystemCall64Shadow函数
    </span><span>//</span><span>原来我们64位搜索KeServiceDescriptorTable是通过msr的0xc0000082获得KiSystemCall64字段, 
    </span><span>//</span><span>但是现在msr[0xc0000082]变成了KiSystemCall64Shadow函数, 而且这个函数无法直接搜索到KeServiceDescriptorTable。</span>
    ULONGLONG KiSystemServiceUser = <span>0</span><span>;
    ULONGLONG templong </span>= <span>0xffffffffffffffff</span><span>;
    </span><span>for</span> (PUCHAR i = pKiSystemCall64; i < EndSearchAddress + <span>0xff</span>; i++)<span>//</span><span>在pKiSystemCall64的0x5ff=1535字节范围内查找</span><span>    {
        </span><span>if</span> (*(PUCHAR)i == <span>0xe9</span> && *(PUCHAR)(i + <span>5</span>) == <span>0xc3</span>)<span>//</span><span>找到KiSystemServiceUser
        </span><span>//</span><span>if (*(PUCHAR)i == "e9" && *(PUCHAR)(i + 5) == "c3")</span><span>//</span><span>找到KiSystemServiceUser</span><span>        {
            </span><span>//</span><span>fffff803`23733383 e9631ae9ff      jmp     nt!KiSystemServiceUser(fffff803`235c4deb)
            </span><span>//</span><span>fffff803`23733388 c3              ret</span>
            RtlCopyMemory(&templong, (PUCHAR)(i + <span>1</span>), <span>4</span><span>);
            KiSystemServiceUser </span>= templong + <span>5</span> + i;<span>//</span><span>KiSystemServiceUser</span>
            EndSearchAddress = KiSystemServiceUser + <span>0x500</span><span>;
            KeServiceDescriptorTable </span>=<span> SearchforKeServiceDescriptorTable64(KiSystemServiceUser, EndSearchAddress);
            </span><span>return</span><span> KeServiceDescriptorTable;
        }
    }
    </span><span>return</span><span>0</span><span>;
}</span>

  4、效果:想要关闭计算器,直接弹框拒绝访问;

       技术分享图片

       windbg也看到了打印的日志,说明自己写的Fake_NtTerminateProcess函数已经被调用;

       技术分享图片

   其他的窗口能够随意结束;

  这次没刻意做驱动隐藏,还是被PCHUNTER发现了:

       技术分享图片

  SSDT hook是好多年以前的老办法了;因为驱动在0环,和windows 内核平起平坐,权力相当大。为了保护自己的客户端,各个厂家都在争先恐后地hook,把内核搞得一团糟,严重影响了用户体验;微软终于坐不住了,近些年在64位的windows做了以下改动:

  • 增加PG保护,一旦发现自己的内核代码被改,大概率会直接蓝屏
  • 增加驱动签名。运行的驱动必须强制签名。一旦发现某些驱动改内核,直接吊销签名的资格

       那么问题又来了,既然不让hook SSDT,各大厂家怎么知道自己的客户端有没有被逆向人员搞了?微软又提供了新的解决方案:注册回调函数;一旦第三方调用openprocess、readprocessmemory、writeprocessmemory等函数搞事,自己的客户端就能收到通知,然后采取响应的措施;回调函数的具体用法见下方【1】;

参考:

1、https://www.write-bug.com/article/2170.html   基于ObRegisterCallbacks实现的线程和进程监控及其保护

2、https://www.cnblogs.com/freesec/p/7623675.html  windows 64位 系统非HOOK方式监控进程创建

3、http://www.m5home.com/bbs/thread-8378-1-1.html  x64 SSDT正确的偏移计算,一键恢复SSDT表中的所有HOOK

原文:https://www.cnblogs.com/theseventhson/p/13942017.html


【说明】本文章由站长整理发布,文章内容不代表本站观点,如文中有侵权行为,请与本站客服联系(QQ:254677821)!

相关教程推荐

其他课程推荐