程序注入是一種将代碼注入到已有程序位址空間内,并執行的技術。程序注入的技術有很多,本文基于LoadDLL&CreateRemoteThread技術來講解。
一般而言,我們會将要執行的代碼編譯到DLL檔案裡,然後加載到目标程序内執行。對于一個非托管DLL直接加載并執行就可以了,但是如果想把一個托管DLL加載到程序中并執行就要費一番周折,因為托管代碼是不能直接執行的,要經過CLR的二次編譯。如何解決這個問題呢?
因為環境對程序注入的影響很大,我這裡先列出我實驗的環境,再具體講解。
系統:windows 7 ,64位
.net :4.0
開發工具:vs2010 sp1
測試程式:均為32位程式
這裡用老外的一張圖來簡單描述下我們的托管代碼是如何在目标程序内執行的。

首先使用具有注入功能的程式将一個非托管的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 (!fLoadable)
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, //初始化ClrRuntimeHost
IID_PPV_ARGS(&pClrRuntimeHost));
wprintf(L"ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx\n", hr);
hr = pClrRuntimeHost->Start();//啟動CLR
wprintf(L"CLR failed to start w/hr 0x%08lx\n", hr);
//執行代碼
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyPath,
pszClassName, pszMethodName, argument,&dwLengthRet);
pClrRuntimeHost->Stop();
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");
}
上面的代碼是本小節的核心代碼,大緻分為三個部分:
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
這一句初始化ClrRuntimeHost執行個體,至此,啟動CLR之前的準備工作結束。下一步為啟動CLR。
hr = pClrRuntimeHost->Start();//啟動CLR
調用ClrRuntimeHost的Start()方法,啟動CLR。
執行托管代碼的方式很多,大家可參考MSDN(http://msdn.microsoft.com/zh-cn/library/ms164408.aspx),這裡我使用最簡單的方法,ExecuteInDefaultAppDomain方法:
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyPath,
該函數各參數說明如下圖:
注意: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>
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:
}
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注入到目标程序中就可以了。
在讨論LoadDLL&CreateRemoteThread程序注入的原理之前,先準備目标程序,建立一個C# 控制台項目,名為TargetForInject,内容如下:
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:
[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枚舉定義了打開目标程序之後,獲得的句柄有哪些操作權限。
[DllImport("kernel32.dll", SetLastError = true)]
public static extern Int32 CloseHandle(
IntPtr hObject);
CLOSEHANDLE函數關閉一個核心對象。其中包括檔案、檔案映射、程序、線程、安全和同步對象等。在CreateThread成功之後會傳回一個hThread的handle,且核心對象的計數加1,CloseHandle之後,引用計數減1,當變為0時,系統删除核心對象。
該函數原型:
BOOL CloseHandle(
HANDLE hObject //已打開對象
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。
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(
string lpModuleName);
GetModuleHandle函數用來擷取一個應用程式或動态連結庫的子產品句柄。
函數原型:
HMODULE WINAPI GetModuleHandle(
LPCTSTR lpModuleName
);
lpModuleName指定子產品名,這通常是與子產品的檔案名相同的一個名字。例如,NOTEPAD.EXE程式的子產品檔案名就叫作NOTEPAD。
傳回值:
如執行成功成功,則傳回子產品句柄。0表示失敗。會設定GetLastError。
[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
public static extern Int32 WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
string buffer,
uint size,
out IntPtr lpNumberOfBytesWritten);
WriteProcessMemory函數用來寫入資料到某一程序的記憶體區域。入口區必須可以通路,否則操作将失敗。
BOOL WriteProcessMemory(
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten
參數:
hProcess。由OpenProcess傳回的程序句柄。
如參數傳資料為INVALID_HANDLE_VALUE 為目标程序為自身程序。
lpBaseAddress。要寫的記憶體首位址。在寫入之前,此函數将先檢查目标位址是否可用,并能容納待寫入的資料。
lpBuffer。指向要寫的資料的指針。
nSize。要寫入的位元組數。
[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(
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();
IntPtr lpLLAddress = WINAPI.GetProcAddress(
WINAPI.GetModuleHandle("kernel32.dll"),
"LoadLibraryA");
if (lpLLAddress == (IntPtr)0)
sError = "Unable to find address of \"LoadLibraryA\".\n";
// 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(
lpAddress,
sDllPath,
(uint)sDllPath.Length + 1,
out ipTmp);
if (Marshal.GetLastWin32Error() != 0)
sError = "Unable to write memory to process.";
IntPtr ipThread = WINAPI.CreateRemoteThread(
0,
lpLLAddress,
(IntPtr)null);
if (ipThread == (IntPtr)0)
sError = "Unable to load dll into memory.";
上面的代碼就是調用API,我就不過多的解釋了,完整代碼會附在文後。
在Main方法中,先擷取目标程序的執行個體,然後調用DoInject方法來實施注入。
static void Main(string[] args)
Process p = Process.GetProcessesByName("TargetForInject")[0];
string message="";
Inject.DoInject(p, @"e:\ManageCodeInvoker.dll", out message);
首先啟動TargetForInject.exe .
啟動程序檢視工具Process Explorer,檢視TargetForInject.exe加載的DLL,如下:
此時加載的DLL肯定沒有ManageCodeInvoker.dll和Message.dll。接下來,啟動Injector.exe,結果很明顯:
托管代碼被成功執行。
我們再檢視TargetForInject.exe加載的DLL:
可以看到ManageCodeInvoker.dll和Message.dll被成功加載到目标程序中。
<a href="http://www.cnblogs.com/xuanhun/archive/2012/06/24/2559224.html">.NET安全揭秘系列博文索引</a>
本文轉自玄魂部落格園部落格,原文連結:http://www.cnblogs.com/xuanhun/archive/2012/07/22/2603983.html,如需轉載請自行聯系原作者