.Net内存程序集的DUMP(ProFile篇)
.Net内存程序集的DUMP(ProFile篇)
作者:RZH 网名:看雪_grassdrago
引言
在DOTNET加解密过程中,我们经常会碰到从内存中转贮.NET程序集的场景。会经常使用那些神奇的DUMP工具,特别是分析整体加解密保护的程序集时,感觉很爽,当然这是因为它的保护很弱。于是我们想了解和学习如何完成类似的功能。本文将简单地介绍Profiling API的一些概念并通过它完成相同的工作。
Profiling API简介
.net为了帮助开发人员进行应用程序的内存、垃圾回收、线程、堆栈、程序集、类、方法、性能等低层分析,提供了我们使用Profiling API编写分析器或代码探查器的机制,它是CLR的一部分。要求探查器必须被编写为COM服务器并实现IcorProfilerCallback2接口[.net2.0环境]或IcorProfilerCallback[.net1.0环境]接口,这个COM服务器将作为被监视进程的一部分运行并在事件发生时接收通知。
那么怎么启动它?通常我们会写一个Loder也可以手动完成,要做以下几点工作:
1. 注册你的COM,可以通过命令行:regsvr32 XXX.dll实现。
2. 设置环境变量COR_PROFILER为此COM的GUID,告诉.net由它来完成分析探查工作,可通过命令行:SET COR_PROFILER={xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}完成。 注意CLR仅能通过此变量装一个分析器。
3. 设置环境变量COR_ENABLE_PROFILING为1,告诉.net启动Profiling功能,可通过命令行:SET COR_ENABLE_PROFILING=1完成。
4. 启动要监视的进程。
下面我们来看看IcorProfilerCallback接口中我们关注的几个事件,及要在其中完成的相应工作:
interface ICorProfilerCallback : IUnknown
{
HRESULT Initialize( [in] IUnknown *pICorProfilerInfoUnk);
// 初始化代码探查器
//其它略。。。
HRESULT ModuleLoadFinished([in] ModuleID moduleId,[in] HRESULT hrStatus);
// 模块加载完成时,可执行模块的代码已完整地呈现在内存中,此时我们转贮代码
//其它略。。。
}
另外:请留意.net的版本,并实现相应接口,否则不会如你期望的那样运行。
更多的内容请参考下面几篇文章及MSDN帮助文档:
《使用 .NET Profiler API 检查并优化程序的内存使用》
《在 .NET Framework 2.0 中,没有任何代码能够逃避 Profiling API 的分析》
《用 .NET Framework Profiling API 迅速重写 MSIL 代码》
代码实现及说明
首先,我们需要完成一个基本的COM服务器并实现IcorProfilerCallback2接口,好在这步头疼的工作可以通过修改CLR Profiler for the .NET Framework 2.0源文件来完成。代码实现的主要工作如下:
1. 该源文件的Profiler.cpp完成了一个进程内COM服务器的所有内容,我们只需要改变一下GUID以免和原com冲突就可以了。
2. 去掉ProfilerCallback.h和ProfilerCallback.cpp文件中不必要的部分以提高运行速度。如果你不觉得它太慢了的话,可以什么都不改。而我让它变成了一个实了现IcorProfilerCallback2接口的空壳。
3. 在Initialize方法中设置我们关心的事件掩码,针对源文件现实,则是在由Initialize方法调用的GetEventMask()方法中。我们只关心ASSEMBLY_LOADS和MODULE_LOADS系列事件。所以:
m_dwEventMask = (DWORD) COR_PRF_MONITOR_MODULE_LOADS | (DWORD) COR_PRF_MONITOR_ASSEMBLY_LOADS;
4. 在ModuleLoadFinished方法中完成我们的转贮。
5. 在原C#编写的loder中(Launcher.exe),增加注册和反注册我们的COM的功能。
关键代码说明:
转贮的关键是得到模块加载基址,名称则关系不太大,这两者都可以在ModuleLoadFinished方法中通过IcorProfilerInfo及IMetaDataImport接口完成。
ICorProfilerInfo::GetModuleInfo |
获取有关指定模块的信息。 |
ICorProfilerInfo::GetModuleMetaData |
获取映射到指定模块的元数据接口实例。 |
IMetaDataImport::GetScopeProps |
获取当前元数据范围内的程序集或模块的名称和版本标识符。 |
更详细的说明请参见MSDN帮助文件或Profiler的Doc文档。
具体代码如下:
HRESULT CProfilerCallback::ModuleLoadFinished(ModuleID moduleId, HRESULT hrStatus)
{
HRESULT hr=m_pICorProfilerInfo->GetModuleInfo (
moduleId, (LPCBYTE *)&pBaseLoadAddress,
2048, &size, name,
&assemblyId );
__try {
// let's determine the module name from metadata
hr = m_pICorProfilerInfo->GetModuleMetaData(moduleId, 0, IID_IMetaDataImport, (IUnknown**) &pImport);
if (SUCCEEDED(hr)) {
GUID mvid;
ULONG nameLen = 0;
hr = pImport->GetScopeProps(moduleName, 2048, &nameLen, &mvid);
}
在得到了模块基址及名称的情况下,要做的就是根据PE结构写文件了,代码流程如下:
1. 模块基址指向DOS头,基址+ e_lfanew指向NT头。FileHeader.NumberOfSections是节数也是节表项的数。NT头结构+1指向节表。节数量知道了,也就知道了节表的尾部地址。好从模块起始地址一直到此,先写入文件。
2. 节表尾到第一个节区开始处填充零。
3. 内存中第一个节区的位置起,每次一字节,向文件中写入节数据,每个节数据大小为section->SizeOfRawData
下面为具体代码:
PIMAGE_NT_HEADERS pNTHeader = NULL;
// only dump executable images
__try {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pbImageBase;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
return false;
}
pNTHeader = (PIMAGE_NT_HEADERS)((PBYTE)pDosHeader + pDosHeader->e_lfanew);
if (pNTHeader->Signature != IMAGE_NT_SIGNATURE) {
return false;
}
} __except(EXCEPTION_EXECUTE_HANDLER) {
return false;
}
。。。
int numSections = pNTHeader->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER section = (PIMAGE_SECTION_HEADER)(pNTHeader + 1);
PBYTE pLastSectionEnd = (PBYTE)(section + numSections);
。。。
int headerLen = pLastSectionEnd-pbImageBase;
int numwritten = fwrite( pbImageBase, 1, headerLen, stream );
。。。
char zero = 0;
for (int i=headerLen; i<section->PointerToRawData; i++) {
fwrite( &zero, 1, 1, stream );
}
。。。
if (isMapped)
{
buf = pbImageBase + section->VirtualAddress;
} else {
buf = pbImageBase + section->PointerToRawData; //第一个节区的指针 }
numwritten = fwrite(buf, 1, section->SizeOfRawData, stream);
请参见随文档提供的代码及项目文件。
运行情况
这里仍旧使用上篇文章中《{samartassembly}4.1.39分析(加解密)》提供的样例代码(包括原始无压缩/{sa}程序集打包/{sa}整体压缩)进行测试。被精简了的COM运行速度令人满意,每个可执行模块正确转贮成功。但转贮完成的exe并不能直接运行,经对比发现NT头中的AddressOfEntryPoint所指向的RVA错误,这对DLL并没有影响。
修正:
- 对比原执行程序,修改RVA值。
- 用ILASM/ILDASM对dump出的exe进行重新编译。
结语
利用Profiling API可以完成很多工作,微软的样列、文档及MSDN提供了较丰富的内容。而在.net解密方面这已是一种陈旧的技术,但并不妨碍我们学习和在必要时使用它。如果有时间我们会继续Hook mscoree.dll及Hook mscorjit.dll的旅程。文章中所引用的知识、代码、甚至文档风格全部学习和来源于互联网,在此向所有具有知识共享精神的网友们表示谢意!
(附代码及工程文件)
posted on 2010-05-25 09:07 northstarlight 阅读(5590) 评论(4) 编辑 收藏 举报