.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方法中通过IcorProfilerInfoIMetaDataImport接口完成。

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并没有影响。

 

 

    

 

 

 修正:

  

  1. 对比原执行程序,修改RVA值。
  2. 用ILASM/ILDASM对dump出的exe进行重新编译。

结语

利用Profiling API可以完成很多工作,微软的样列、文档及MSDN提供了较丰富的内容。而在.net解密方面这已是一种陈旧的技术,但并不妨碍我们学习和在必要时使用它。如果有时间我们会继续Hook mscoree.dllHook mscorjit.dll的旅程。文章中所引用的知识、代码、甚至文档风格全部学习和来源于互联网,在此向所有具有知识共享精神的网友们表示谢意!

附代码及工程文件

posted on 2010-05-25 09:07  northstarlight  阅读(5590)  评论(4编辑  收藏  举报

导航