厩把浴俊
⌒凑馄牡档氖焙蛴捎谖易愎徊耍龅搅瞬簧傥侍猓嘈籦kbll,a1rsupply和SobeIt的指点,还有TCH的辛勤劳动,才有这篇文档的诞生,本文中可能存在一些错误,这些错误都是由于我的失误造成的,如果您有什么意见和看法,欢迎来http://www.itaq.org指出,或者E-mail:zf35@citiz.net
【概述】
在服务器上实现对进程创建的控制有很大的意义,通过监控进程的创建,我们可以让被允许运行的进程正确创建,而未被允许的程序则会创建失败,这样就可以防止未知木马,病毒和蠕虫对服务器的威胁。要实现上述目的,必须hook windows创建进程相关的API,根据《inside the windows NT》和《Native API Reference》中记载,加上softIce的实际跟踪,windows创建进程的API调用流程如下:
【代码 】
CreateProcessA- CreateProcessW- CreateProcessInternalW-…-最终调用ZwCreateProcess
本文档中我们选用CreateProcessW来实现我们的目的,当然你也可以使用其它几个API。本文档的演示代码稍做改动可应用于任意Ring3函数。
对于hook一个API而言,可使用的办法有很多,本文选用改写函数入口点的办法来实现挂接CreateProcessW,更多的详细资料请参阅SobeIt写的《windows下hook API的几种办法》。
【copy-on-write】
最初试验时,我使用softice的 a CreateProcessW改写函数入口点的代码,F5切换回windows之后发现一切如愿以偿,但是当我编写程序修改CreateProcessW入口点代码时,发现所做的改动仅对本进程有效,而对于系统的其他进程没有产生任何影响。用softice跟踪后发现本进程中CreateProcessW的虚拟地址被映射到了一个新的,与其它进程不同的物理地址上,如果你读过Webcrazy的《copy-on-write机制》一文,就不难看出这是copy-on-write机制产生的影响。对于系统的dll,每个dll都被映射在不同进程的相同的虚拟地址上,而这些虚拟地址又指向相同的物理地址,通过这种机制,系统实现最低的资源消耗.当某个进程试图改写物理内存中的数据时,为了不对其它进程产生影响,系统自动新分配一块物理内存,把原物理内存中的数据复制过去,改写,然后把改写内存的那个进程的虚拟地址重新映射到新的物理内存上去,而其它进程则还是映射在原来的物理内存上,这就是“写时复制技术”(copy-on-write),那么系统是如何判断何时应该使用copy-on-write呢?这是以虚拟地址的PTE来决定的,当PTE中copy-on-write标志被置位时,任何对该虚拟地址的写操作都将导致一个copy-on-writ
【三种可行的办法】
为了实现全局hook,我们不能被copy-on-write机制所限制住,目前我想到了三种办法来达到我们的目的。
1. 通过驱动来修改页表项(PTE)的属性,使CreateProcessW对应的虚拟地址失去copy-on-write的属性,这样在本身进程中对CreateProcessW入口点代码的修改会对系统中所有进程生效,从而实现全局hook。
2. 通过windows本身提供的一个对象\\phymem来对物理内存进行直接读写,先定位本身进程的Eprocess(KTEB)(PS:如何在Ring3下定位任意进程的Eprocess请参考我之前写的《获取进程的Eprocess》一文),获得Eprocess之后,可以得到进程的页目录,然后利用\\phymem读取存放页目录的物理内存的内容,再模拟操作系统进行虚拟地址-物理内存地址的转换,最终得到CreateProcessW所对应的物理地址,利用\\phymem我们避开copy-on-write机制,直接改写CreateProcessW。
3. 通过最常规的手段来达到目的,先枚举系统中所有进程,然后通过VirtualQueryEx,VirtualAllocEx,VirtualProtectEx等函数修改每个进程的页面属性,分配新的空间等。最后将我们的代码用WriteProcessMemory写到各进程的空间中,利用改写CreateProcessW入口为Jmp *******来跳到我们的代码中,改变函数的执行流程。
以上三个办法中,方法1只是一个构想,还没成为现实,有空的话我回去试试看的,当然页欢迎各位高手去实现,然后mail一份代码给我:P方法2我写了一份完整的代码来实现它,但是在本文档中不进行讨论,否则文档会变的很长,我将在另一份文档中专门说明这种办法的具体实现。方法3使本文讨论的重点,下面就方法3进行详细说明。
查询CreateProcessW的基址及属性
这里我们使用VirtualQueryEx这个函数,其原型如下:
SIZE_T
VirtualQueryEx
(
HANDLE hProcess,
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength
);
参数说明:
HANDLE hProcess 想要查询内存信息的进程句柄
LPCVOID lpAddress 指向想要查询内存区域的指针
PMEMORY_BASIC_INFORMATION lpBuffer 指向MEMORY_BASIC_INFORMATION结构的指针
SIZE_T dwLength lpBuffer的大小
调用这个函数之后,相关的信息存放在lpBuffer指向的结构中
修改CreateProcessW的页属性
对于一个页来说,有如下几种属性:
PAGE_EXECUTE
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_EXECUTE_WRITECOPY
PAGE_NOACCESS
PAGE_READONLY
PAGE_READWRITE
PAGE_WRITECOPY
我们通过VirtualProtectEx来修改页的属性:
BOOL
VirtualProtectEx
(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
参数说明:
HANDLE hProce
ss 进程句柄
LPVOID lpAddress 指向想要修改的内存区域的指针
SIZE_T dwSize 修改的内存区域的大小
DWORD flNewProtect 新的页属性
PDWORD lpflOldProtect 指向保存老的页属性的内存的指针
从后面的代码中我们可以看到,为了改写函数入口点代码,我们必须赋予它PAGE_EXECUTE_READWRITE属性。
在进程中分配可用空间
光修改函数入口点代码是不够的。我们必须自己编写一段code来接管CreateProcessW的工作,由于进程空间是相互隔离的,为了达到全局hook的目标,我们必须向每个进程索要一块空间来存放我们的代码,这就要用到VirtualAllocEx这个函数了,VirtualAllocEx原型如下:
LPVOID
VirtualAllocEx
(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
参数说明:
HANDLE hProcess 进程句柄
LPVOID lpAddress 指向分配内存区域的指针
SIZE_T dwSize 分配的区域的大小
DWORD flAllocationType 内存类型
DWORD flProtect 新内存的属性
我们使用WriteProcessMemory这个函数来向远程进程写入我们的代码和数据,其原型如下:
BOOL
WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesWritten
);
参数说明:
HANDLE hProcess 进程句柄
LPVOID lpBaseAddress 指向写入地址的指针
LPCVOID lpBuffer 指向写入数据的指针
SIZE_T nSize lpBuffer的大小
SIZE_T* lpNumberOfBytesWritten 实际写入的字节数
编译器的魔术
我在使用WriteProcessMemory把我自己写的一个函数JmpToAddress的内容写入远程进程空间时,发现无论我的JmpToAddress的内容是什么,写入空间的都是E9****这几个字节,这令我非常困惑,从机器码来看,这是一条相对跳转指令。那么它又是从何而来呢,为了搞清楚这个问题,我用VC调试了一下,在watch窗口中输入JmpToAddress,显示出JmpToAddress的虚拟地址0x00410XXX,然后打开memory窗口,查看这段内存中存放的内容,发现确实是JmpToAddress的代码,这就奇怪了,那神秘的E9****是从何而来呢,于是我请教了a1rsupply,他告诉我VC的调试版本会生成跳转表,这下真相大白,原来是编译器玩的魔术。
为了向远程进程正确写入代码,我们必须自己计算真正的函数地址,下面我写了一段代码来计算真正的函数地址:
__asm
{
pushad
lea eax,JmpToAddress
mov ecx,JmpToAddress
shr ecx,8
add eax,ecx
add eax,5
mov JmpAfterCalc,eax
popad
}
解决麻烦的定位问题
在编写代码的过程中,我遇到的另一个较大的问题就是如何定位地址。我编写的JmpToAddress()函数如下:
void __declspec(naked) JmpToAddress(void)
{
__a
sm jmp [HookedAddr]
}
在本地进程中这句代码没什么问题,但是当它被写入远端进程后便会产生种种问题,我们来看看它的汇编代码,如下
jmp [00401Cxxx]
我们注意到本进程中这个虚拟地址里存放的是HookedAddr的地址,但是在远程进程中,这个地址指向的是别的什么东西,jmp过去会产生不可预料的结果,为了实现正确的行为,我们先用WriteProcessMemory向远程进程写入HookedAddr的内容,然后用一个相对地址引用它
void __declspec(naked) JmpAddress(void)
{
__asm call flag
flag:
__asm pop eax
__asm add eax,0x0e
__asm mov ebx,[eax]
__asm jmp ebx
}
pop eax后,eax里面存放的就是本条指令的虚拟的地址,加上一个固定值后,[eax]就是我们通过WritePr