将托管dll注入到非托管进程中
Binhua Liu
为什么要写这篇文章
1,如果你想注入带窗体的dll,C#写界面比C++容易的多;
2,或许你想利用.net的某些功能,比如利用.Net Remoting从外部控制被注入的dll;
3,或许你是一个C#程序员,使用C#的时候总感觉更舒适些,比如笔者。同时,你希望必要时也能在宿主中调用C++函数,提供更大的灵活性,本文的方法也能做到。
注入托管dll的不同之处
首先,为什么托管dll 不能像非托管dll那样用LoadLibrary注入? 我们知道,.net语言,如C#,VB.net等,都是运行在CLR(公共语言运行时)上的,也就是我们通常所说的虚拟机,而我们所说的非托管进程是没有加载虚拟机的。那为什么托管dll一定要在CLR上运行?托管dll虽然符合windows的PE格式规范,但是代码是以IL的形式保存在.Text 区的,而不是机器码,CLR会在运行时JIT编译成机器码再交给操作系统执行,这也就为什么托管代码称之为”托管”的意义。
所以,要想注入托管dll,首先需要在目标进程中启动CLR,然后让CLR来加载managed dll。
注入的方式
首先,我们注入一个非托管的dll,再通过它加载CLR并加载托管dll。所以工程需要3个模块:注入器,一个注入的非托管dll和注入的托管dll。
我们首先看如何注入非托管dll,这里是通过远程线程来实现的,如果你已经熟悉这个技术,可以跳过:
InjectDemo.cpp:
int _tmain(int argc, _TCHAR* argv[]) { int pid; void *pNativeDllRemote; FARPROC pLoadLibrary; TCHAR szNativeDllPath[_MAX_PATH]=_T("D:\\Code\\InjectDemo\\Debug\\NativeDll.dll"); cout<<"input the process id to inject"<<endl; cin>>pid; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,0,pid); if(hProcess==0) return 1; HMODULE hKernel32 = ::GetModuleHandle(_T("Kernel32")); if(sizeof(TCHAR)==2) pLoadLibrary= ::GetProcAddress(hKernel32,"LoadLibraryW"); //if path is unicode, use "LoadLibraryW" else pLoadLibrary= ::GetProcAddress(hKernel32,"LoadLibraryA"); pNativeDllRemote=VirtualAllocEx(hProcess,NULL,sizeof(szNativeDllPath),MEM_COMMIT,PAGE_READWRITE); ::WriteProcessMemory(hProcess,pNativeDllRemote,(void*)szNativeDllPath,sizeof(szNativeDllPath),NULL); HANDLE hThread = CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)pLoadLibrary,pNativeDllRemote,0,NULL); ::WaitForSingleObject(hThread,INFINITE); //DWORD exitcode; //GetExitCodeThread(hThread,&exitcode); ::CloseHandle(hThread); return 0; }
这段代码通过远程线程注入Native.dll,网上这方面文章很多,也可以根据不同的需要采用服务输入法或者Hook等方式注入。需要注意的是,我们通过GetProcAddress获取LoadLibrary的函数地址,事实上获取的是注入器中该函数的虚拟地址,而不是宿主的。由于LoadLibrary函数位于系统dll中,在每个进程中都被加载到相同的虚拟地址上,所以我们才能这么做。LoadLibrary函数的参数,dll的路径字符串需要通过VirtualAllocEx和WriteProcessMemory在宿主进程上创建,而不能把注入器上的字符串地址传给LoadLibrary。总之,必须记住的一点是,远程线程是在另一个虚拟地址空间上执行的,远程执行的函数体本身或者他引用的虚拟地址都不能是注入器进程中的虚拟地址,而必须是宿主进程的虚拟地址。
再来看被注入的非托管的NativeDll.dll的代码:
NativeDll.cpp:
#include <windows.h> #include "stdafx.h" #include "NativeDll.h" #include "MSCorEE.h" #include "metahost.h" DWORD CALLBACK StartTheDotNetRuntime(LPVOID lp) { HRESULT hr = S_OK; ICLRMetaHost *m_pMetaHost = NULL; ICLRRuntimeInfo *m_pRuntimeInfo = NULL; ICLRRuntimeHost *pClrHost = NULL; hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*) &m_pMetaHost); if (hr != S_OK) return hr; hr = m_pMetaHost->GetRuntime (L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*) &m_pRuntimeInfo); if (hr != S_OK) return hr; hr = m_pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*) &pClrHost ); if (FAILED(hr)) return hr; HRESULT hrStart = pClrHost->Start(); DWORD dwRet = 0; hr = pClrHost->ExecuteInDefaultAppDomain( L"d:\\Code\\InjectDemo\\Debug\\ManagedDll.dll", L"ManagedDll.Class1", L"Start", L"nothing to post", &dwRet); hr = pClrHost->Stop(); pClrHost->Release(); return S_OK; } BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved ) { switch(fdwReason) { case DLL_PROCESS_ATTACH: CreateThread(0,0,StartTheDotNetRuntime,0,0,0); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; default: break; } return true; }
这段代码是本文的重点,我们在NativeDll.dll的DllMain函数中创建一个线程来加载加载CLR,CLR事实上是一组COM服务器,为了可以调用它们,我们需要引用头文件 "MSCorEE.h"和"metahost.h",并在链接器中添加对MSCorEE.lib的链接。 GetRuntime函数用于指定加载的CLR的版本,需要填写完整的版本号,根据我的测试,2.0和4.0可以成功加载,但是3.5似乎不行。在成功启动CLR后,执行ExecuteInDefaultAppDomain加载指定的托管dll并执行它的静态方法。执行结束后,停止并释放CLR。
然后,在托管的ManagedDll.dll中,我们可以打开一个窗口:
Class1.cs:
using System.Windows.Forms; namespace ManagedDll { public class Class1 { public static int Start(string argument) { Application.Run(new MainForm()); return 0; } } }
或者开启一个.net Remoting服务,这样注入器就可以在外部控制该进程了:
using System.Threading; using System.Runtime.Remoting; namespace ManagedDll { public class Class1 { public static int Start(string argument) { RemotingConfiguration.Configure("ManagedDll.dll.config"); while (true) { Thread.Sleep(1000); } } } }
需要注意的是,使用.net Remoting,你必须把托管dll放在和宿主同一个目录下,否则反射机制会失败。这种注入方式提供了很大的灵活性,你可以把逻辑代码写在ManagedDll.dll中,也可以写在NativeDll.dll中并导出,在ManagedDll.dll中引用,再通过窗口或者.net Remoting调用。
参考
http://www.codingthewheel.com/archives/how-to-inject-a-managed-assembly-dll
http://windows-internals.blogspot.com/2009/02/injecting-code-using-createremotethread.html
Binhua Liu原创,写于2011/8/4。