记得2003年QQ尾巴病毒肆虐的时候,就已经有些尾巴病毒的变种在使用这种技术了。到了2004年初,我曾经尝试着仿真了一个QQ尾巴病毒,但独是跳过了DLL的远程加载技术。直到最近在学校论坛上看到了几位朋友在探讨这一技术,便忍不住将这一尘封已久的技术从我的记忆中拣了出来,以满足广大的技术爱好者们。
必备知识
在阅读本文之前,你需要了解以下几个API函数:
·OpenProcess - 用于打开要寄生的目标进程。
·VirtualAllocEx/VirtualFreeEx - 用于在目标进程中分配/释放内存空间。
·WriteProcessMemory - 用于在目标进程中写入要加载的DLL名称。
·CreateRemoteThread - 远程加载DLL的核心内容,用于控制目标进程调用API函数。
·LoadLibrary - 目标进程通过调用此函数来加载病毒DLL。
在此我只给出了简要的函数说明,关于函数的详细功能和介绍请参阅MSDN。
示例程序
我将在以下的篇幅中用一个简单的示例Virus.exe来实现这一技术。这个示例的界面如下图:
首先运行Target.exe,这个文件是一个用Win32 Application向导生成的“Hello, World”程序,用来作为寄生的目标进程。
然后在界面的编辑控件中输入进程的名称“Target.exe”,单击“注入DLL”按钮,这时候Virus.exe就会将当前目录下的DLL.dll注入至Target.exe进程中。
在注入DLL.dll之后,你也可以单击“卸载DLL”来将已经注入的DLL卸载。
模拟的病毒体DLL.dll
这是一个简单的Win32 DLL程序,它仅由一个入口函数DllMain组成:
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved )
{
switch ( fdwReason )
{
case DLL_PROCESS_ATTACH:
{
MessageBox( NULL, _T("DLL已进入目标进程。"), _T("信息"), MB_ICONINFORMATION );
}
break;
case DLL_PROCESS_DETACH:
{
MessageBox( NULL, _T("DLL已从目标进程卸载。"), _T("信息"), MB_ICONINFORMATION );
}
break;
}
return TRUE;
}
如你所见,这里我在DLL被加载和卸载的时候调用了MessageBox,这是用来显示我的远程注入/卸载工作是否成功完成。而对于一个真正的病毒体来说,它往往就是处理DLL_PROCESS_ATTACH事件,在其中加入了启动病毒代码的部分:
case DLL_PROCESS_ATTACH:
{
StartVirus();
}
break;
注入!
现在要开始我们的注入工作了。首先,我们需要找到目标进程:
DWORD FindTarget( LPCTSTR lpszProcess )
{
DWORD dwRet = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof( PROCESSENTRY32 );
Process32First( hSnapshot, &pe32 );
do
{
if ( lstrcmpi( pe32.szExeFile, lpszProcess ) == 0 )
{
dwRet = pe32.th32ProcessID;
break;
}
} while ( Process32Next( hSnapshot, &pe32 ) );
CloseHandle( hSnapshot );
return dwRet;
}
这里我使用了Tool Help函数库,当然如果你是NT系统的话,也可以选择PSAPI函数库。这段代码的目的就是通过给定的进程名称来在当前系统中查找相应的进程,并返回该进程的ID。得到进程ID后,就可以调用OpenProcess来打开目标进程了:
// 打开目标进程
HANDLE hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessID );
现在有必要说一下OpenProcess第一个参数所指定的三种权限。在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都相互独立。如果一个进程需要完成跨进程的工作的话,那么它必须拥有目标进程的相应操作权限。在这里,PROCESS_CREATE_THREAD表示我可以通过返回的进程句柄在该进程中创建新的线程,也就是调用CreateRemoteThread的权限;同理,PROCESS_VM_OPERATION则表示在该进程中分配/释放内存的权限,也就是调用VirtualAllocEx/VirtualFreeEx的权限;PROCESS_VM_WRITE表示可以向该进程的地址空间写入数据,也就是调用WriteProcessMemory的权限。
至此目标进程已经打开,那么我们该如何来将DLL注入其中呢?在这之前,我请你看一行代码,是如何在本进程内显式加载DLL的:
HMODULE hDll = LoadLibrary( "DLL.dll" );
那么,如果能控制目标进程调用LoadLibrary,不就可以完成DLL的远程注入了么?的确是这样,我们可以通过CreateRemoteThread将LoadLibrary作为目标进程的一个线程来启动,这样就可以完成“控制目标进程调用LoadLibrary”的工作了。到这里,也许你会想当然地写下类似这样的代码:
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)"DLL.dll", 0, &dwID );
不过结果肯定会让你大失所望——注入DLL失败!
那么现在让我们来分析一下失败的原因吧。我是前说过,在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都是相互独立的。在这里,我们当作参数传入的字符串"DLL.dll"其实是一个数值,它表示这个字符串位于Virus.exe地址空间之中的地址,而这个地址在传给Target.exe之后,它指向的东西就失去了有效性。举个例子来说,譬如A、B两栋大楼,我住在A楼的401;那么B楼的401住的是谁我当然不能确定——也就是401这个门牌号在B楼失去了有效性,而且如果我想要入住B楼的话,我就必须请B楼的楼长为我在B楼中安排新的住处(当然这个新的住处是否401也就不一定了)。
由此看来,我就需要做这么一系列略显繁杂的手续——首先在Target.exe目标进程中分配一段内存空间,然后向这段空间写入我要加载的DLL名称,最后再调用CreateRemoteThread。这段代码就成了这样:
// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = lstrlenA( lpszDll ) + 1;
LPVOID lpBuf = VirtualAllocEx( hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE );
if ( NULL == lpBuf )
{
CloseHandle( hProcess );
// 失败处理
}
if ( WriteProcessMemory( hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten ) )
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if ( dwWritten != dwSize )
{
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hProcess );
// 失败处理
}
}
else
{
CloseHandle( hProcess );
// 失败处理
}
// 使目标进程调用LoadLibrary,加载DLL
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );
需要说的有两点,一是由于我要在目标进程中为ANSI字符串来分配内存空间,所以这里凡是和目标进程相关的部分,都明确使用了后缀为“A”的API函数——当然,如果要使用Unicode字符串的话,可以换作后缀是“W”的API;第二,在这里LoadLibrary的指针我是取的本进程的LoadLibraryA的地址,这是因为LoadLibraryA/LoadLibraryW位于kernel32.dll之中,而Win32下每个应用程序都会把kernel32.dll加载到进程地址空间中一个固定的地址,所以这里的函数地址在Target.exe中也是有效的。
在调用LoadLibrary完毕之后,我们就可以做收尾工作了:
// 等待LoadLibrary加载完毕
WaitForSingleObject( hThread, INFINITE );
// 释放目标进程中申请的空间
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hThread );
CloseHandle( hProcess );
在此解释一下WaitForSingleObject一句。由于我们是通过CreateRemoteThread在目标进程中另外开辟了一个LoadLibrary的线程,所以我们必须等待这个线程运行完毕才能够释放那段先前申请的内存。
好了,现在你可以尝试着整理这些代码并编译运行。运行Target.exe,然后开启一个有模块查看功能的进程查看工具(在这里我使用我的July)来查看Target.exe的模块,你会发现在注入DLL之前,Target.exe中并没有DLL.dll的存在:
在调用了注入代码之后,DLL.dll就位于Target.exe的模块列表之中了:
矛盾相生
记得2004年初我将QQ尾巴病毒成功仿真后,有很多网友询问我如何才能杀毒,不过我都没有回答——因为当时我研究的重点并非病毒的寄生特性。这一寄生特性直到今天可以说我才仿真完毕,那么,我就将解毒的方法也一并公开吧。
和DLL的注入过程类似,只不过在这里使用了两个API:GetModuleHandle和FreeLibrary。出于篇幅考虑,我略去了与注入部分相似或相同的代码:
// 使目标进程调用GetModuleHandle,获得DLL在目标进程中的句柄
DWORD dwHandle, dwID;
LPVOID pFunc = GetModuleHandleA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );
// 等待GetModuleHandle运行完毕
WaitForSingleObject( hThread, INFINITE );
// 获得GetModuleHandle的返回值
GetExitCodeThread( hThread, &dwHandle );
// 释放目标进程中申请的空间
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hThread );
// 使目标进程调用FreeLibrary,卸载DLL
pFunc = FreeLibrary;
hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)dwHandle, 0, &dwID );
// 等待FreeLibrary卸载完毕
WaitForSingleObject( hThread, INFINITE );
CloseHandle( hThread );
CloseHandle( hProcess );
用这个方法可以卸载一个进程中的DLL模块,当然包括那些非病毒体的DLL。所以,这段代码还是谨慎使用为好。
在完成卸载之后,如果没有别的程序加载这个DLL,你就可以将它删除了。
到此为止,整个的技术细节我就讲完了。不过,我还是不能免俗地再次强调:李马发表本文及其源代码仅作技术讨论之用,请勿将此技术用作破坏之途,李马本人也不为各种可能出现的后果负任何责任。