分类: 嵌入式
2012-07-22 20:32:26
在前面关于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位程序
这里用老外的一张图来简单描述下我们的托管代码是如何在目标进程内执行的。
首先使用具有注入功能的程序将一个非托管的C++DLL注入到目标进程中,然后该非托管DLL启动CLR,并加载要执行的托管DLL,最后调用CLR执行托管代码。
过程看起来很简单,这里要解决的第一个问题是创建一个C++DLL,作为CLR宿主。
打开VS2010,选择c++ Win32Project项目。
确定之后点下一步,应用类型选DLL,附加选项中选择空项目。
我创建的项目名称为:ManageCodeInvoker,如下图:
然后在 Header Files文件夹中添加头文件LoadClr.h,内容如下:
#pragma region Includes and Imports
#include
#include
#include
#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");
}
}
上面的代码是本小节的核心代码,大致分为三个部分:
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。
hr = pClrRuntimeHost->Start();//启动CLR
调用ClrRuntimeHost的Start()方法,启动CLR。
执行托管代码的方式很多,大家可参考MSDN(),这里我使用最简单的方法,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
#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注入到目标进程中就可以了。
在讨论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:
[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 //已打开对象
);
[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。
[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
}
[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。要写入的字节数。
[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);
}
首先启动TargetForInject.exe .
启动进程查看工具Process Explorer,查看TargetForInject.exe加载的DLL,如下:
此时加载的DLL肯定没有ManageCodeInvoker.dll和Message.dll。接下来,启动Injector.exe,结果很明显:
托管代码被成功执行。
我们再查看TargetForInject.exe加载的DLL:
可以看到ManageCodeInvoker.dll和Message.dll被成功加载到目标进程中。