这是我觉得一种非常好的Hook技术,自己也非常喜欢,我先引用Windows核心编程里的讲解,最后在文后附上一个封装好的类,这种方法非常适合在别人的程序里隐藏自己的程序,因此得到了广大木马爱好者的青眯。
插入D L L的第三种方法是使用远程线程。这种方法具有更大的灵活性。它要求你懂得若干个Wi n d o w s特性、如进程、线程、线程同步、虚拟内存管理、D L L和U n i c o d e等(如果对这些特性不清楚,请参阅本书中的有关章节)。Wi n d o w s的大多数函数允许进程只对自己进行操作。这是很好的一个特性,因为它能够防止一个进程破坏另一个进程的运行。但是,有些函数却允许一个进程对另一个进程进行操作。这些函数大部分最初是为调试程序和其他工具设计的。不过任何函数都可以调用这些函数。
这个D L L插入方法基本上要求目标进程中的线程调用L o a d L i b r a r y函数来加载必要的D L L。由于除了自己进程中的线程外,我们无法方便地控制其他进程中的线程,因此这种解决方案要求我们在目标进程中创建一个新线程。由于是自己创建这个线程,因此我们能够控制它执行什么代码。幸好,Wi n d o w s提供了一个称为C r e a t e R e m o t e T h r e a d的函数,使我们能够非常容易地在另一个进程中创建线程:
HANDLE CreateRemoteThread(
HANDLE hProcess,
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);C r e a t e R e m o t e T h r e a d与C r e a t e T h r e a d很相似,差别在于它增加了一个参数h P r o c e s s。该参数指明拥有新创建线程的进程。参数p f n S t a r t A d d r指明线程函数的内存地址。当然,该内存地址与远程进程是相关的。线程函数的代码不能位于你自己进程的地址空间中。
注意在Windows 2000中,更常用的函数CreateThread是在内部以下面的形式来实现的:
HANDLE CreateThread(PSECURITY_ATTRIBUTES psa, DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam,
DWORD fdwCreate, PDWORD pdwThreadID)
{
return(CreateRemoteThread(GetCurrentProcess(), psa, dwStackSize,
pfnStartAddr, pvParam, fdwCreate, pdwThreadID));
}在Windows 98中,C r e a t e R e m o t e T h r e a d函数不存在有用的实现代码,它只是返回N U L L。调用G e t L a s t E r r o r函数将返回E R R O R _ C A L L _ N O T _ I M P L E M E N T E D(C r e a t e T h r e a d函数包含用于在调用进程中创建线程的完整的实现代码)。由于C r e a t e R e m o t e T h r e a d没有实现,因此,在Windows 98下,不能使用本方法来插入D L L。
好了,现在你已经知道如何在另一个进程中创建线程了,但是,如何才能让该线程加载我们的D L L呢?答案很简单,那就是需要该线程调用L o a d L i b r a r y函数:
HINSTANCE LoadLibrary(PCTSTR pszLibFile);如果观察Wi n B a s e . h文件中的L o a d L i b r a r y函数,你将会发现下面的代码:
HINSTANCE WINAPI LoadLibraryA(LPCSTR pszLibFileName);
HINSTANCE WINAPI LoadLibraryW(LPCWSTR pszLibFileName);
#ifdef UNICODE
#define LoadLibrary LoadLibraryW
#else
#define LoadLibrary LoadLibraryA
#endif // !UNICODE实际上有两个L o a d L i b r a r y函数,即L o a d L i b r a r y A和L o a d L i b r a r y W。这两个函数之间存在的唯一差别是,传递给函数的参数类型不同。如果将库的文件名作为A N S I字符串来存储,那么必须调用L o a d L i b r a r y A(A是指A N S I)。如果将文件名作为U n i c o d e字符串来存储,那么必须调用L o a d L i b r a r y W(W是指通配符)。不存在单个L o a d L i b r a r y的情况,只有L o a d L i b r a r y A和L o a d L i b r a r y W。对于大多数应用程序来说,L o a d L i b r a r y宏可以扩展为L o a d L i b r a r y A。
幸好L o a d L i b r a r y函数的原型与一个线程函数的原型是相同的。下面是一个线程函数的原型:
DWORD WINAPI ThreadFunc(PVOID pvParam);这两个函数的原型并不完全相同,不过它们非常相似。两个函数都接受单个参数,并且都返回一个值。另外,两个函数都使用相同的调用规则。这是非常幸运的,因为我们要做的事情是创建一个新线程,并且使线程函数的地址成为L o a d L i b r a r y A或L o a d L i b r a r y W函数的地址。本质上,我们必须进行的操作是执行类似下面的一行代码:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryA, "C:\\MyLib.dll", 0, NULL);或者,如果喜欢U n i c o d e,则执行下面这行代码:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);当在远程进程中创建新线程时,该线程将立即调用L o a d L i b r a r y A(或L o a d L i b r a r y W)函数,并将D L L的路径名的地址传递给它。这是非常容易的。但是这里存在另外两个问题。
第一个问题是,不能像我在上面展示的那样,将L o a d L i b r a r y A或L o a d L i b r a r y W作为第四个参数传递给C r e a t e R e m o t e T h r e a d。原因很简单。当你编译或者链接一个程序时,产生的二进制代码包含一个输入节(第1 9章中做了介绍)。这一节由一系列输入函数的形式替换程序(t h u n k)组成。所以,当你的代码调用一个函数如L o a d L i b r a r y A时,链接程序将生成一个对你模块的输入节中的形实替换程序的调用。接着,该形实替换程序便转移到实际的函数。
如果在对C r e a t e R e m o t e T h r e a d的调用中使用一个对L o a d L i b r a r y A的直接引用,这将在你的模块的输入节中转换成L o a d L i b r a r y A的形实替换程序的地址。将形实替换程序的地址作为远程线程的起始地址来传递,会导致远程线程开始执行一些令人莫名其妙的东西。其结果很可能造成访问违规。若要强制直接调用L o a d L i b r a r y A函数,避开形实替换程序,必须通过调用G e t P r o c A d d r e s s函数,获取L o a d L i b r a r y A的准确内存位置。
对C r e a t e R e m o t e T h r e a d进行调用的前提是,K e r n e l 3 2 . d l l已经被同时映射到本地和远程进程的地址空间中。每个应用程序都需要K e r n e l 3 2 . d l l,根据我的经验,系统将K e r n e l 3 2 . d l l映射到每个进程的同一个地址。因此,必须调用下面的C r e a t e R e m o t e T h r e a d函数:
// Get the real address of LoadLibraryA in Kernel32.dll.
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, "C:\\MyLib.dll", 0, NULL);或者,如果喜欢U n i c o d e的话,调用下面的函数:
// Get the real address of LoadLibraryW in Kernel32.dll.
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);好了,这就解决了第一个问题。第二个问题与D L L路径名字符串有关。字符串“ C : \ \M y L i b . d l l”是在调用进程的地址空间中。该字符串的地址已经被赋予新创建的远程线程,该线程将它传递给L o a d L i b r a r y A。但是,当L o a d L i b r a r y A取消对内存地址的引用时, D L L路径名字符串将不再存在,远程进程的线程就可能引发访问违规;向用户显示一个未处理的异常条件消息框,并且远程进程终止运行。记住,这是远程进程终止运行,不是你的进程终止运行。你可能成功地终止另一个进程的运行,而你的进程则继续正常地运行。
为了解决这个问题,必须将D L L的路径名字符串放入远程进程的地址空间中。然后,当C r e a t e R e m o t e T h r e a d函数被调用时,我们必须将我们放置该字符串的地址(相对于远程进程的地址)传递给它。同样,Wi n d o w s提供了一个函数,即Vi r t u a l A l l o c E x,使得一个进程能够分配另一个进程的地址空间中的内存:
PVOID VirtualAllocEx(
HANDLE hProcess,
PVOID pvAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect);另一个函数则使我们能够释放该内存:
BOOL VirtualFreeEx(
HANDLE hProcess,
PVOID pvAddress,
SIZE_T dwSize,
DWORD dwFreeType);这两个函数与它们的非E x版本的函数(第1 5章已经做了介绍)是类似的。唯一的差别是这两个函数需要一个进程的句柄作为其第一个参数。这个句柄用于指明执行操作时所在的进程。
一旦为该字符串分配了内存,我们还需要一种方法将该字符串从我们的进程的地址空间拷贝到远程进程的地址空间中。Wi n d o w s提供了一些函数,使得一个进程能够从另一个进程的地址空间中读取数据,并将数据写入另一个进程的地址空间。
BOOL ReadProcessMemory(
HANDLE hProcess,
PVOID pvAddressRemote,
PVOID pvBufferLocal,
DWORD dwSize,
PDWORD pdwNumBytesRead);
BOOL WriteProcessMemory(
HANDLE hProcess,
PVOID pvAddressRemote,
PVOID pvBufferLocal,
DWORD dwSize,
PDWORD pdwNumBytesWritten);远程进程由h P r o c e s s参数来标识。参数p v A d d r e s s R e m o t e用于指明远程进程中的地址,参数p v B u ff e r L o c a l是本地进程中的内存地址,参数d w S i z e是需要传送的字节数,p d w N u m B y t e s R e a d和p d w N u m B y t e s Wr i t t e n用于指明实际传送的字节数。当函数返回时,可以查看这两个参数的值。
既然已经知道了要进行操作,下面让我们将必须执行的操作步骤做一个归纳:
1) 使用Vi r t u a l A l l o c E x函数,分配远程进程的地址空间中的内存。
2) 使用Wr i t e P r o c e s s M e m o r y函数,将D L L的路径名拷贝到第一个步骤中已经分配的内存中。
3) 使用G e t P r o c A d d r e s s函数,获取L o a d L i b r a r y A或L o a d L i b r a t y W函数的实地址(在K e r n e l 3 2 . d l l中)。
4) 使用C r e a t e R e m o t e T h r e a d函数,在远程进程中创建一个线程,它调用正确的L o a d L i b r a r y函数,为它传递第一个步骤中分配的内存的地址。
这时, D L L已经被插入远程进程的地址空间中,同时D L L的D l l M a i n函数接收到一个D L L _ P R O C E S S _ AT TA C H通知,并且能够执行需要的代码。当D l l M a i n函数返回时,远程线程从它对L o a d L i b r a r y的调用返回到B a s e T h r e a d S t a r t 函数(第6 章中已经介绍)。然后B a s e T h r e a d S t a r t调用E x i t T h r e a d,使远程线程终止运行。
现在远程进程拥有第一个步骤中分配的内存块,而D L L则仍然保留在它的地址空间中。若要将它删除,需要在远程线程退出后执行下面的步骤:
5) 使用Vi r t u a l F r e e E x函数,释放第一个步骤中分配的内存。
6) 使用G e t P r o c A d d r e s s函数,获得F r e e L i b r a r y函数的实地址(在K e r n e l 3 2 . d l l中)。
7) 使用C r e a t e R e m o t e T h r e a d函数,在远程进程中创建一个线程,它调用F r e e L i b r a r y函数,传递远程D L L的H I N S TA N C E。
附一个封装好的类: