修改SSDT做简单的进程保护

来源:互联网 发布:找对象软件 编辑:程序博客网 时间:2024/05/14 04:17
*By 炉子[0GiNr]
http://atleast.blog.cfan.com.cn
http://0ginr.com*/

M$真是照顾菜鸟 居然会给个SSDT,而且居然还把KSDT(KeServiceDescrīptorTable)导出了(当然,不排除这是M$的无奈之举,因为M$是不推荐programmer Hook他的API的,但是如果不对API做hook,那么许多事情都很难完成,这使得M$不得不提供一个“接口”,这就好比过滤驱动的提供是一样一样的)
//由于大多数看这篇文章的人都是没有写过驱动的,所以我会尽可能的将提到的函数/结构等做详细的描述。
SSDT其实就是一张表。先dump出来瞧瞧
我之前说过,导出的是KeServiceDescrīptorTable。用WinDBG来dump。


Quote:
lkd> dd KeServiceDescrīptorTable
8055a680   804e36a8 00000000 0000011c 80513eb8
8055a690   00000000 00000000 00000000 00000000
8055a6a0   00000000 00000000 00000000 00000000
8055a6b0   00000000 00000000 00000000 00000000
8055a6c0   00002730 bf80c247 00000000 00000000
8055a6d0   fa86aa80 faad83c0 80def5e0 faad83e0
8055a6e0   00000000 00000000 00000000 00000000
8055a6f0   e8f01240 01c7c671 00000000 00000000

再看看KSDT是怎么被定义的:


Copy code
typedef struct _SERVICE_DEscrīptOR_TABLE
{
   PVOID   ServiceTableBase;
   PULONG   ServiceCounterTableBase;
   ULONG   NumberOfService;
   ULONG   ParamTableBase;
}SERVICE_DEscrīptOR_TABLE,*PSERVICE_DEscrīptOR_TABLE;
extern PSERVICE_DEscrīptOR_TABLE KeServiceDescrīptorTable;

第一个是指向表基址(表头)的一个指针,第二个是服务调用数的基值(也就是服务号的起始值),第三个是服务数量,第四个是参数表的基址(参数表中每1个字节代表一个参数长度,各个参数长度的顺序和服务调用的顺序的对应的。
//虽然SSDT指向的都是一些Native API,但是在这儿,他们更多的被称为系统服务调用。
其他三个先不用管,先看ServiceTableBase——因为在大多数情况下,我们修改SSDT之前我们都是已经知道要修改的服务调用的服务调用号(索引号),并且,我们所了解到的服务调用号总是不会超过服务总数,而参数表对于我们来说也应当是已知的。
再用WinDBG把ServuceTableBase dump出来。


Quote:
lkd> dd 804e36a8
804e36a8   80580302 80579b8c 8058b7ae 805907e4
804e36b8   805905fe 806377a0 80639931 8063997a
804e36c8   8057560b 806481cf 80636f5f 8058fb85
804e36d8   8062f0a4 8057be31 8058cc26 806261bd
804e36e8   805dcf20 80568f9d 805d9ac1 805a2bb0
804e36f8   804e3cb4 806481bb 805ca22c 804f0e28
804e3708   80569649 80567d49 8058fff3 8064e1c1
804e3718   8058f8f5 80581225 8064e42f 8058b800

正如你所看到的,在ServiceTableBase中,由多个ULONG型数值排列成了一个表——这也就是前面所说的SSDT,而其中的数值就是各个服务调用的地址,我们可以修改这个表中的地址来对这些服务调用进行重定向。
//注:并非所有的服务调用都会查这张表,比如在驱动中直接调用被导出的函数,这张表就会被忽略——换句话说,这张表只能用于对r3的进程所做的防护,而且,在2k,xp以及未打sp1的2k3系统下,这也不是绝对的。
通过分析服务地址的长度以及得到ServiceTableBase的方法,我们不难得出一个结论:
服务调用的函数地址在内存中存放的地址 = ServiceTableBase + 服务函数调用号 * 4 //sizeof(ULONG)=4
换句话说,我们可以修改【ServiceTableBase + 服务函数调用号 * 4】所指向的内存数据来使得表中的函数重定位到我们自己的函数中。
由于内存中的某些部分是有写保护的,所以我们得先置cr0寄存器去掉这些地方的内存保护(不然等着PAGE_FAULT吧……)


Copy code
VOID DisableWriteProtect( PULONG pOldAttr)
{
     ULONG uAttr;
     _asm
     {
         push eax;
         mov   eax, cr0;
         mov   uAttr, eax;
         and   eax, 0FFFEFFFFh; // CR0 16 BIT = 0
         mov   cr0, eax;
         pop   eax;
     };
     *pOldAttr = uAttr; //保存原有的 CRO 属性
}

VOID EnableWriteProtect( ULONG uOldAttr )
{
     _asm
     {
         push eax;
         mov   eax, uOldAttr; //恢复原有 CR0 属性
         mov   cr0, eax;
         pop   eax;
     };
}

//注:以下代码中使用的均为usigned型,因为在ntddk中只有ULONG,UINT,UCHAR的定义,当然,如果你愿意,也可以自己定义DWORD,WORD,这只是个人习惯而已。


Copy code

void RedirectServiceAddr(ULONG ulServiceID, ULONG ulNewServiceProc)
{
     ULONG ulAddress=(ULONG)KeServiceDescrīptorTable->ServiceTableBase + ulServiceID * 4;
     ULONG uAttr;
     DisableWriteProtect(&uAttr);
     *((ULONG*)ulAddress) = ulNewServiceProc;
     EnableWriteProtect(&uAttr);
}


废话半天了,给NtOpenProcess做个钩子吧。


Copy code
NTSYSCALLAPI
NTSTATUS
NTAPI
ZwOpenProcess (
     OUT PHANDLE ProcessHandle,
     IN ACCESS_MASK DesiredAccess,
     IN POBJECT_ATTRIBUTES ObjectAttributes,
     IN PCLIENT_ID ClientId OPTIONAL
     );
typedef
NTSTATUS (__stdcall *pfnNtOpenProcess)
(
       OUT PHANDLE ProcessHandle,
       IN ACCESS_MASK DesiredAccess,
       IN POBJECT_ATTRIBUTES ObjectAttributes,
       IN PCLIENT_ID ClientId OPTIONAL
       );
pfnNtOpenProcess old_NtOpenProcess;
NTSTATUS new_NtOpenProcess(
       OUT PHANDLE ProcessHandle,
       IN ACCESS_MASK DesiredAccess,
       IN POBJECT_ATTRIBUTES ObjectAttributes,
       IN PCLIENT_ID ClientId OPTIONAL
       )
{   
     NTSTATUS status = STATUS_SUCCESS;
     //判断PID
     if   ( (long)ClientId->UniqueProcess==(long)procProcessId) )
     return STATUS_ACCESS_DENIED;
   
     //剩下的交给我们的替换程序
     status=old_NtOpenProcess(
         ProcessHandle,
         DesiredAccess,
         ObjectAttributes,
         ClientId
         );
   
     return status;
}
RedirectServiceAddr(*((UCHAR *)(ZwOpenProcess)+1),(ULONG)new_NtOpenProcess);


在r3下可以用过nativeAPI函数地址+1来获取函数的服务号,在r0为什么呢?
用WinDBG deasm一下ZwOpenProcess看看吧 。


Quote:

lkd> u ZwOpenProcess
nt!ZwOpenProcess:
804de044 b87a000000       mov     eax,7Ah
804de049 8d542404         lea     edx,[esp+4]
804de04d 9c               pushfd
804de04e 6a08             push     8
804de050 e8dc150000       call     nt!KiSystemService (804df631)
804de055 c21000           ret     10h


正如你所看到的,与r3下几乎没有什么区别,都是先把服务调用号存放的eax中,所以我们在r0下也可以用r3下那套方法,不过我们必须是对Zw系列的+1,而不是Nt系列的(Nt系列的是什么样子?别问我,问WinDBG,这算是课后作业……)
上面那份代码只做了函数重定位,对函数原始地址的获取(也就是old_NtOpenProcess的地址的获取)没有将,不过如果你从头看到这儿还没搞清楚改之前怎么获取服务调用的地址………… 我…………
把PID传入驱动的方法可以用IoCtl,简单,方便
那个pid判断其实还是有点小漏洞的,因为一个process有4个pid(这4个pid都能打开这个进程),具体什么自己查查吧,留作课后作业。