托管代碼的進程注入&CLR宿主
在前面關於CLR寄宿的幾篇博客(CLR寄宿(上) MSCOREE.DLL,CLR寄宿(中) 托管exe文件的加載和執行,CLR寄宿(下) 托管宿主)中,介紹了常用的宿主接口。宿主接口,允許我們使用非托管代碼創建CLR宿主,從而啟動CLR,運行托管代碼,控制垃圾回收……等一系列功能。本篇博文要講解的是使用CLR宿主的一個場景——進程注入。
進程注入是一種將代碼注入到已有進程地址空間內,並執行的技術。進程注入的技術有很多,本文基於LoadDLL&CreateRemoteThread技術來講解。
一般而言,我們會將要執行的代碼編譯到DLL文件里,然后加載到目標進程內執行。對於一個非托管DLL直接加載並執行就可以了,但是如果想把一個托管DLL加載到進程中並執行就要費一番周折,因為托管代碼是不能直接執行的,要經過CLR的二次編譯。如何解決這個問題呢?
因為環境對進程注入的影響很大,我這里先列出我實驗的環境,再具體講解。
系統:windows 7 ,64位
.net :4.0
開發工具:vs2010 sp1
測試程序:均為32位程序
1.1 實現非托管代碼調用托管代碼
這里用老外的一張圖來簡單描述下我們的托管代碼是如何在目標進程內執行的。
首先使用具有注入功能的程序將一個非托管的C++DLL注入到目標進程中,然后該非托管DLL啟動CLR,並加載要執行的托管DLL,最后調用CLR執行托管代碼。
過程看起來很簡單,這里要解決的第一個問題是創建一個C++DLL,作為CLR宿主。
打開VS2010,選擇c++ Win32Project項目。
確定之后點下一步,應用類型選DLL,附加選項中選擇空項目。
我創建的項目名稱為:ManageCodeInvoker,如下圖:
然后在 Header Files文件夾中添加頭文件LoadClr.h,內容如下:
#pragma region Includes and Imports
#include <windows.h>
#include<stdio.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")
#import "mscorlib.tlb" raw_interfaces_only \
high_property_prefixes("_get","_put","_putref") \
rename("ReportEvent", "InteropServices_ReportEvent")
using namespace mscorlib;
#pragma endregion
namespace ManageCodeInvoker
{
class MyClrHost
{
public:
static __declspec(dllexport) void ExcuteManageCode(PCWSTR pszVersion,PCWSTR pszAssemblyName, PCWSTR pszClassName,PCWSTR pszMethodName,PCWSTR argument);
static __declspec(dllexport) void Test();
};
}
上面代碼聲明了兩個函數,ExcuteManageCode和Test。ExcuteManageCode各參數解釋如下:
1) pszVersion:.NET 運行時版本。
2) pszAssemblyName:程序集名稱。
3) pszClassName:類名稱。
4) pszMethodName:方法名稱。
5) argument:方法參數。
Test()函數這里用來做測試,直接調用ExcuteManageCode方法。
在Source Files文件夾中添加dllmain.cpp和MyClrHost.cpp文件,如下圖:
MyClrHost.cpp文件中內容如下:
#include "LoadClr.h"
namespace ManageCodeInvoker
{
void MyClrHost:: ExcuteManageCode(PCWSTR pszVersion,PCWSTR pszAssemblyPath, PCWSTR pszClassName,PCWSTR pszMethodName,PCWSTR argument)
{
HRESULT hr;
ICLRMetaHost *pMetaHost = NULL;
ICLRRuntimeInfo *pRuntimeInfo = NULL;
ICLRRuntimeHost *pClrRuntimeHost = NULL;
DWORD dwLengthRet;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));//創建實例
if(FAILED(hr))
{
goto Cleanup;
}
hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo));//獲取CLR信息
if (FAILED(hr))
{
goto Cleanup;
}
BOOL fLoadable;
hr = pRuntimeInfo->IsLoadable(&fLoadable);
if (FAILED(hr))
{
goto Cleanup;
}
if (!fLoadable)
{
goto Cleanup;
}
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, //初始化ClrRuntimeHost
IID_PPV_ARGS(&pClrRuntimeHost));
if (FAILED(hr))
{
wprintf(L"ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx\n", hr);
goto Cleanup;
}
hr = pClrRuntimeHost->Start();//啟動CLR
if (FAILED(hr))
{
wprintf(L"CLR failed to start w/hr 0x%08lx\n", hr);
goto Cleanup;
}
//執行代碼
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyPath,
pszClassName, pszMethodName, argument,&dwLengthRet);
pClrRuntimeHost->Stop();
if (FAILED(hr))
{
goto Cleanup;
}
Cleanup:
if (pMetaHost)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (pRuntimeInfo)
{
pRuntimeInfo->Release();
pRuntimeInfo = NULL;
}
}
void MyClrHost::Test()
{
ManageCodeInvoker::MyClrHost::ExcuteManageCode(L"v4.0.30319",L"E:\\Message.dll",L"Message.Message",L"Show",L"HelloWord");
}
}
上面的代碼是本小節的核心代碼,大致分為三個部分:
1)初始化ClrRuntimeHost
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));一句,創建ICLRMetaHost 實例,這里字段為pMetaHost。
hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo)),創建ICLRRuntimeInfo實例,這里字段為pRuntimeInfo。
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, //初始化ClrRuntimeHost
IID_PPV_ARGS(&pClrRuntimeHost));
這一句初始化ClrRuntimeHost實例,至此,啟動CLR之前的准備工作結束。下一步為啟動CLR。
2)啟動CLR
hr = pClrRuntimeHost->Start();//啟動CLR
調用ClrRuntimeHost的Start()方法,啟動CLR。
3)執行托管代碼
執行托管代碼的方式很多,大家可參考MSDN(http://msdn.microsoft.com/zh-cn/library/ms164408.aspx),這里我使用最簡單的方法,ExecuteInDefaultAppDomain方法:
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyPath,
pszClassName, pszMethodName, argument,&dwLengthRet);
pClrRuntimeHost->Stop();
該函數各參數說明如下圖:
注意:ExecuteInDefaultAppDomain方法所調用的方法必須具有下列簽名:
static int pwzMethodName (String pwzArgument)
其中pwzMethodName表示被調用的方法的名稱,pwzArgument表示作為參數傳遞給該方法的字符串值。如果 HRESULT 值設置為 S_OK,則將pReturnValue設置為被調用的方法返回的整數值。否則,不設置pReturnValue。
從CLR的啟動到托管代碼的執行,都做了介紹,內容不是很多,還有什么疑惑,可留言討論。
Test()方法內容如下:
void MyClrHost::Test()
{
MyClrHost::ExcuteManageCode(L"v4.0.30319",L"E:\\Message.dll",L"Message.Message",L"Show",L"HelloWord");
}
在Test()方法中,我用本機的.NET版本和用來測試托管代碼Message.dll來調用ExcuteManageCode方法。
修改dllmain.cpp的內容如下:
#include<Windows.h>
#include "LoadClr.h"
bool APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
ManageCodeInvoker::MyClrHost::Test();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在DllMain函數中,調用Test方法,這樣當DLL被加載的時候,就會執行Test方法-> ExcuteManageCode方法->執行托管代碼 Message.Show(message).
這里大家還沒看到要執行的托管代碼Message.dll的實際內容,下面我們共同來實現它。
創建一個名為Message的DLL項目,目標平台為x86,添加Message Class,內容如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Message
{
public class Message
{
public static int Show(string message)
{
MessageBox.Show(message);
return 100;
}
}
}
為方便起見,編譯win32 DLL項目ManageCodeInvoker和.NET x86 項目Message,將生成的DLL放到一個測試目錄中(我放到本地磁盤E:下)。
第一問題,非托管代碼調用托管DLL的問題解決了,只需要將DLL 文件ManageCodeInvoker.DLL注入到目標進程中就可以了。
1.2 進程注入
在討論LoadDLL&CreateRemoteThread進程注入的原理之前,先准備目標進程,創建一個C# 控制台項目,名為TargetForInject,內容如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TargetForInject
{
class Program
{
static void Main(string[] args)
{
while (true)
{
}
}
}
}
TargetForInject.exe 啟動后會處於等待狀態。
使用LoadDLL&CreateRemoteThread技術進行進程注入的步驟如下(調用的函數為Windows API):
1) 調用OpenProcess函數打開目標進程,獲取目標進程的句柄。
2) 通過GetProcAddress方法獲取目標進程中加載的kernel32.dll的LoadLibraryA方法的地址。
3) 調用VirtualAllocEx函數,在目標進程內開辟空間用來存儲要注入的DLL的實際路徑。
4) 調用WriteProcessMemory函數,將要注入的DLL的路徑寫入開辟的內存中。
5) 調用CreateRemoteThread函數,在目標進程中創建新的線程,執行LoadLibraryA方法,LoadLibraryA方法根據寫入的目標DLL的路徑加載DLL到內存中並執行該DLL的DLLMain方法。
6) 等待線程結束,退出。
進程注入的流程已經清楚了,業務你要說也太簡單了,就是調API,事實上也確實如此,就是調調API。下面我們按部就班的實現進程注入的功能,新創建一個名為Injector的c#控制台項目,添加類Inject。
首先聲明各個要調用的API:
OpenProcess
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
ProcessAccessFlags dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
UInt32 dwProcessId);
OpenProcess 函數用來打開一個已存在的進程對象,並返回進程的句柄。
函數原型 :
HANDLE OpenProcess(
DWORD dwDesiredAccess, //渴望得到的訪問權限(標志)
BOOL bInheritHandle, // 是否繼承句柄
DWORD dwProcessId// 進程標示符
);
在聲明OpenProcess函數時,使用了ProcessAccessFlags枚舉,定義如下:
[Flags]
enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VMOperation = 0x00000008,
VMRead = 0x00000010,
VMWrite = 0x00000020,
DupHandle = 0x00000040,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
Synchronize = 0x00100000
}
ProcessAccessFlags枚舉定義了打開目標進程之后,獲得的句柄有哪些操作權限。
CloseHandle
[DllImport("kernel32.dll", SetLastError = true)]
public static extern Int32 CloseHandle(
IntPtr hObject);
CLOSEHANDLE函數關閉一個內核對象。其中包括文件、文件映射、進程、線程、安全和同步對象等。在CreateThread成功之后會返回一個hThread的handle,且內核對象的計數加1,CloseHandle之后,引用計數減1,當變為0時,系統刪除內核對象。
該函數原型:
BOOL CloseHandle(
HANDLE hObject //已打開對象
);
GetProcAddress
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetProcAddress(
IntPtr hModule,
string lpProcName);
GetProcAddress函數被用來檢索在DLL中的輸出函數地址。
函數原型:
FARPROC GetProcAddress(
HMODULE hModule, // DLL模塊句柄
LPCSTR lpProcName // 函數名
);
參數說明:
hModule 。[in] 包含此函數的DLL模塊的句柄。LoadLibrary、AfxLoadLibrary或者GetModuleHandle函數可以返回此句柄。
lpProcName 。[in] 包含函數名的以NULL結尾的字符串,或者指定函數的序數值。如果此參數是一個序數值,它必須在一個字的底字節,高字節必須為0。
返回值:
如果函數調用成功,返回值是DLL中的輸出函數地址。
如果函數調用失敗,返回值是NULL。得到進一步的錯誤信息,調用函數GetLastError。
GetModuleHandle
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(
string lpModuleName);
GetModuleHandle函數用來獲取一個應用程序或動態鏈接庫的模塊句柄。
函數原型:
HMODULE WINAPI GetModuleHandle(
LPCTSTR lpModuleName
);
參數說明:
lpModuleName指定模塊名,這通常是與模塊的文件名相同的一個名字。例如,NOTEPAD.EXE程序的模塊文件名就叫作NOTEPAD。
返回值:
如執行成功成功,則返回模塊句柄。0表示失敗。會設置GetLastError。
VirtualAllocEx
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
VirtualAllocEx 函數的作用是在指定進程的虛擬空間保留或提交內存區域,除非指定MEM_RESET參數,否則將該內存區域置0。
函數原形:
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
參數說明:
hProcess。申請內存所在的進程句柄。
lpAddress。保留頁面的內存地址;一般用NULL自動分配 。
dwSize。欲分配的內存大小,字節單位;注意實際分配的內存大小是頁內存大小的整數倍
在聲明中使用了AllocationType枚舉,指定申請內存的操作類型,定義如下:
[Flags]
public enum AllocationType
{
Commit = 0x1000,
Reserve = 0x2000,
Decommit = 0x4000,
Release = 0x8000,
Reset = 0x80000,
Physical = 0x400000,
TopDown = 0x100000,
WriteWatch = 0x200000,
LargePages = 0x20000000
}
MemoryProtection枚舉指定對內存區域的操作權限,定義如下:
[Flags]
public enum MemoryProtection
{
Execute = 0x10,
ExecuteRead = 0x20,
ExecuteReadWrite = 0x40,
ExecuteWriteCopy = 0x80,
NoAccess = 0x01,
ReadOnly = 0x02,
ReadWrite = 0x04,
WriteCopy = 0x08,
GuardModifierflag = 0x100,
NoCacheModifierflag = 0x200,
WriteCombineModifierflag = 0x400
}
WriteProcessMemory
[DllImport("kernel32.dll", SetLastError = true)]
public static extern Int32 WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
string buffer,
uint size,
out IntPtr lpNumberOfBytesWritten);
WriteProcessMemory函數用來寫入數據到某一進程的內存區域。入口區必須可以訪問,否則操作將失敗。
函數原型:
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten
);
參數:
hProcess。由OpenProcess返回的進程句柄。
如參數傳數據為INVALID_HANDLE_VALUE 為目標進程為自身進程。
lpBaseAddress。要寫的內存首地址。在寫入之前,此函數將先檢查目標地址是否可用,並能容納待寫入的數據。
lpBuffer。指向要寫的數據的指針。
nSize。要寫入的字節數。
CreateRemoteThread
[DllImport("kernel32.dll")]
public static extern IntPtr CreateRemoteThread(IntPtr hProcess,
IntPtr lpThreadAttributes, uint dwStackSize, IntPtr
lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
CreateRemoteThread函數用來創建一個在其它進程地址空間中運行的線程(也稱:創建遠程線程)。
函數原型:
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
參數說明:
hProcess [in]
線程所屬進程的進程句柄。該句柄必須具有 PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE,和PROCESS_VM_READ 訪問權限.
lpThreadAttributes [in]
一個指向 SECURITY_ATTRIBUTES 結構的指針, 該結指定了線程的安全屬性。
dwStackSize [in]
線程初始大小,以字節為單位,如果該值設為0,那么使用系統默認大小。
lpStartAddress [in]
在遠程進程的地址空間中,該線程的線程函數的起始地址。
lpParameter [in]
傳給線程函數的參數。
dwCreationFlags [in]
線程的創建標志。
聲明完需要的Windows API之后,我們就可以按原計划編寫代碼了:
public static bool DoInject(
Process pToBeInjected,
string sDllPath,
out string sError)
{
IntPtr hwnd = IntPtr.Zero;
if (!CRT(pToBeInjected, sDllPath, out sError, out hwnd)) //CreateRemoteThread
{
//close the handle, since the method wasn't able to get to that
if (hwnd != (IntPtr)0)
WINAPI.CloseHandle(hwnd);
return false;
}
int wee = Marshal.GetLastWin32Error();
return true;
}
private static bool CRT(
Process pToBeInjected,
string sDllPath,
out string sError,
out IntPtr hwnd)
{
sError = String.Empty; //in case we encounter no errors
IntPtr hndProc = WINAPI.OpenProcess(
ProcessAccessFlags.CreateThread|
ProcessAccessFlags.VMOperation|
ProcessAccessFlags.VMRead|
ProcessAccessFlags.VMWrite|
ProcessAccessFlags.QueryInformation,
false,
(uint)pToBeInjected.Id);
hwnd = hndProc;
if (hndProc == (IntPtr)0)
{
sError = "Unable to attatch to process.\n";
sError += "Error code: " + Marshal.GetLastWin32Error();
return false;
}
IntPtr lpLLAddress = WINAPI.GetProcAddress(
WINAPI.GetModuleHandle("kernel32.dll"),
"LoadLibraryA");
if (lpLLAddress == (IntPtr)0)
{
sError = "Unable to find address of \"LoadLibraryA\".\n";
sError += "Error code: " + Marshal.GetLastWin32Error();
return false;
}
// byte[] bytes = CalcBytes(sDllPath);
IntPtr lpAddress = WINAPI.VirtualAllocEx(
hndProc,
(IntPtr)null,
(uint)sDllPath.Length+1,
AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
if (lpAddress == (IntPtr)0)
{
if (lpAddress == (IntPtr)0)
{
sError = "Unable to allocate memory to target process.\n";
sError += "Error code: " + Marshal.GetLastWin32Error();
return false;
}
}
IntPtr ipTmp = IntPtr.Zero;
WINAPI.WriteProcessMemory(
hndProc,
lpAddress,
sDllPath,
(uint)sDllPath.Length + 1,
out ipTmp);
if (Marshal.GetLastWin32Error() != 0)
{
sError = "Unable to write memory to process.";
sError += "Error code: " + Marshal.GetLastWin32Error();
return false;
}
IntPtr ipThread = WINAPI.CreateRemoteThread(
hndProc,
(IntPtr)null,
0,
lpLLAddress,
lpAddress,
0,
(IntPtr)null);
if (ipThread == (IntPtr)0)
{
sError = "Unable to load dll into memory.";
sError += "Error code: " + Marshal.GetLastWin32Error();
return false;
}
return true;
}
上面的代碼就是調用API,我就不過多的解釋了,完整代碼會附在文后。
在Main方法中,先獲取目標進程的實例,然后調用DoInject方法來實施注入。
static void Main(string[] args)
{
Process p = Process.GetProcessesByName("TargetForInject")[0];
string message="";
Inject.DoInject(p, @"e:\ManageCodeInvoker.dll", out message);
}
1.3 測試
首先啟動TargetForInject.exe .
啟動進程查看工具Process Explorer,查看TargetForInject.exe加載的DLL,如下:
此時加載的DLL肯定沒有ManageCodeInvoker.dll和Message.dll。接下來,啟動Injector.exe,結果很明顯:
托管代碼被成功執行。
我們再查看TargetForInject.exe加載的DLL:
可以看到ManageCodeInvoker.dll和Message.dll被成功加載到目標進程中。
Demo下載:http://files.cnblogs.com/xuanhun/InjectProcess.rar