在本文开始前,必须要明确的一点是,绕过反Dump不虞味着可以直接Dump下来可以运行和利用的文件!
绕过反Dump的目的是规复出必要的信息让dnSpy、ILSpy等工具可以直接反编译,从而快速剖析这个.NET程序集,而不是规复出和原始的一摸一摸的没有信息丢失的.NET头!
如果想要脱壳,并不能依赖于这个方法!
文中CLR源码来自CoreCLR v1.0
在序言中已经大致先容,通过读取CLR内部工具,可以获取必要的信息来规复.NET头。以是我们须要先理解PE头中的.NET部分和反Dump可以抹除的部分。

首先是Data Directories中的.NET元数据目录。这一项记录了.NET目录(IMAGE_COR20_HEADER)的偏移和大小。一样平常来说偏移是0x2008,也便是.text段的第8个字节,这是由C#和VB.NET编译器决定的。大小是sizeof(IMAGE_COR20_HEADER),也便是固定的0x48。
CFF Explorer中的.NET Directory便是IMAGE_COR20_HEADER。
通过上一步的解析,我们可以得到IMAGE_COR20_HEADER的所在位置。个中IMAGE_COR20_HEADER的定义如下,主要的部分我给了注释。
复制代码 隐蔽代码typedef struct IMAGE_COR20_HEADER{ DWORD cb; // sizeof(IMAGE_COR20_HEADER) WORD MajorRuntimeVersion; WORD MinorRuntimeVersion; IMAGE_DATA_DIRECTORY MetaData; // .NET元数据 DWORD Flags; // 标志位,指示程序集类型,比如是否可实行,是否纯IL union { DWORD EntryPointToken; // Main方法的MDToken DWORD EntryPointRVA; // 入口点的RVA(如果入口是本机代码) } DUMMYUNIONNAME; IMAGE_DATA_DIRECTORY Resources; // .NET资源 IMAGE_DATA_DIRECTORY StrongNameSignature; // .NET强名称 IMAGE_DATA_DIRECTORY CodeManagerTable; IMAGE_DATA_DIRECTORY VTableFixups; IMAGE_DATA_DIRECTORY ExportAddressTableJumps; IMAGE_DATA_DIRECTORY ManagedNativeHeader;} IMAGE_COR20_HEADER, PIMAGE_COR20_HEADER;
此构造中的大部分是可以被打消的(我知道的不能打消的有Resources,由于每次获取资源都要重新读取IMAGE_COR20_HEADER::Resources),但是必要的部分实在只有MetaData这个成员,其它部分如.NET资源只是作为附加项。为了让反编译器可以尽可能显示全部信息,我们规复MetaData,EntryPointToken这三个成员即可。
EntryPointToken和Resources的规复比较大略,只要规复IMAGE_COR20_HEADER中的成员。而MetaData比较繁芜,须要规复MetaData指向的.NET元数据头。.NET元数据头的第一个构造是STORAGESIGNATURE,紧接着的是STORAGEHEADER,然后随着STORAGESTREAM数组。这里给出它们在CFF Explorer中的显示和在CLR中的定义。
CFF Explorer中的MetaData Header便是STORAGESIGNATURE + STORAGEHEADER,MetaData Streams便是紧跟的STORAGESTREAM数组。
复制代码 隐蔽代码struct STORAGESIGNATURE{ ULONG lSignature; // "Magic" signature. USHORT iMajorVer; // Major file version. USHORT iMinorVer; // Minor file version. ULONG iExtraData; // Offset to next structure of information ULONG iVersionString; // Length of version string BYTE pVersion[0]; // Version string};struct STORAGEHEADER{ BYTE fFlags; // STGHDR_xxx flags. BYTE pad; USHORT iStreams; // How many streams are there.};struct STORAGESTREAM{ ULONG iOffset; // Offset in file for this stream. ULONG iSize; // Size of the file. char rcName[MAXSTREAMNAME]; // Start of name, null terminated.};
STORAGESIGNATURE中的iVersionString成员表示pVersion的真实长度,也便是说STORAGESIGNATURE构造体的实际大小是sizeof(STORAGESIGNATURE) + iVersionString。STORAGEHEADER的iStreams成员表示STORAGESTREAM数组的元素数量。一样平常来说iStreams为5,5个STORAGESTREAM构造分别对应了#~、#Strings、#US、#GUID、#Blob这5个元数据流。
在反Dump中,STORAGESIGNATURE的lSignature的成员是一定会被抹除的,它和PE头的"MZ"类似,值恒为0x424A5342,也便是"BSJB"。如果不抹除这个成员,通过搜索特色BSJB可以非常随意马虎地定位到.NET元数据头从而绕过反Dump。和上面提到的构造IMAGE_COR20_HEADER一样,这三个构造的所有成员也都是可以抹除的。规复的时候,我们紧张关注STORAGESTREAM这个构造中的所有三个成员,保存了指向.NET元数据流的信息,和这些元数据流对应的名称。其它两个构造体相对而言没那么主要,可以直接填预设值。
在上面提到的5个元数据流#~、#Strings、#US、#GUID、#Blob中,#~是表流,必须存在的。如果表流是未压缩的,它的名称也可以是#-,元数据构造上和#~是同等的。表流的头部是CMiniMdSchemaBase构造,这里给出它在CFF Explorer中的显示和在CLR中的定义。
复制代码 隐蔽代码class CMiniMdSchemaBase{ ULONG m_ulReserved; // Reserved, must be zero. BYTE m_major; // Version numbers. BYTE m_minor; BYTE m_heaps; // Bits for heap sizes. BYTE m_rid; // log-base-2 of largest rid. unsigned __int64 m_maskvalid; // Bit mask of present table counts. unsigned __int64 m_sorted; // Bit mask of sorted tables.};
在CMiniMdSchemaBase构造后面,紧随着一个UINT32数组,数组长度是m_maskvalid成员bit为1的数量。这个数组的元素按顺序表示了每个存在的表的行数。
CLR加载.NET程序集时,这些成员都会被保存到CLR内部,以是这些成员也都是可以抹除的。规复时,我们紧张关注哪些表是存在的,并且它们的行数分别是多少。通过这些数据我们可以规复出m_maskvalid成员和行数数组。
关键的CLR内部工具有了规复.NET头的思路,我们可以开始理解CLR内部工具了,通过它们来规复.NET头。这部分内容将展开先容关键的CLR内部工具作为铺垫。这些CLR内部工具我会省略掉很多无关的部分,而且不同版本CLR的定义也略有差异,以是列出的成员在构造体中的偏移是不一定的。关于详细如何利用,将会不才一部分详细解释。
ModuleModule类对应mscorlib里System.Reflection.RuntimeModule类的本机工具布局,定义在ceeload.h里。
复制代码 隐蔽代码class Module{ PTR_CUTF8 m_pSimpleName; PTR_PEFile m_file; MethodDesc m_pDllMain; Volatile<DWORD> m_dwTransientFlags; Volatile<DWORD> m_dwPersistedFlags; VASigCookieBlock m_pVASigCookieBlock; PTR_Assembly m_pAssembly; mdFile m_moduleRef;};
m_pSimpleName是模块名,值即是C#代码的assembly.Module.Assembly.GetName().Name,在.NET Framework 4.5.3之前不存在这个成员。m_file是指向PEFile构造的指针,可以用来获取模块基址和大小等信息,非常主要。m_pDllMain是指向DllMain方法的指针,仅对C++/CLI天生的程序集有效。m_pAssembly是指向Assembly构造的指针,这里不须要利用它。PEFile
PEFile类CLR加载器的输入,表示一个抽象的PE文件。它的子类是PEAssembly和PEModule。如果被加载为程序集,那就创建PEAssembly,如果用Assembly.LoadModule方法加载为模块,那么就创建PEModule。在.NET Core里面,多模块程序集特性被移除了。以是.NET Core里面只有PEAssembly,没有PEModule了。
PEFile有多种加载办法:
HMODULE - PEFile是在相应“自发的”系统回调时加载的。只有通过LoadLibrary加载exe主模块和IJW dll,或非托管代码中存在静态导入才会涌现这种情形。Fusion loads - 这是最常见的情形。从Fusion中得到路径,并通过PEImage加载PEFile。Display name loads - 这些是基于元数据的绑定。Path loads - 从完全的绝对路径加载Byte arrays - 由用户代码显式加载。这也是通过PEImage加载的。Dynamic - 此时PEFile不是实际的PE映像,而是基于反射的模块的占位符。 复制代码 隐蔽代码class PEFile{ PTR_PEImage m_identity; PTR_PEImage m_openedILimage; PTR_PEImage m_nativeImage; BOOL m_fCanUseNativeImage; BOOL m_MDImportIsRW_Debugger_Use_Only; Volatile<BOOL> m_bHasPersistentMDImport; IMDInternalImport m_pMDImport; IMetaDataImport2 m_pImporter; IMetaDataEmit m_pEmitter;};
m_identity是作为标识符的指向PEImage构造的指针。一样平常情形下不用这个成员,而是利用m_openedILimage。在PEFile::GetILimage函数里,如果m_openedILimage为空,m_identity的值会赋给m_openedILimage。m_openedILimage是作为供应元数据的指向PEImage构造的指针。我们规复.NET头会利用这个成员获取信息。m_nativeImage是用于NGEN等情形的指向PEImage构造的指针。比如mscorlib.ni.dll这种NGEN创建的预编译的模块,就会加载并保存到m_nativeImage成员。m_pMDImport是指向IMDInternalImport接口的指针,我们可以用这个接口读取一些元数据信息。
PEFile的子类PEAssembly和PEModule我们不须要过于关心,里面没有什么可用的信息。通过不雅观察PEFile的成员,我们可以大概认为PEFile是对PEImage的包装,封装了各种情形下.NET程序集加载的结果。CLR只须要利用抽象的IMDInternalImport接口来获取元数据即可,不须要关心PE映像的详细细节。
PEImagePEImage是由CLR的“仿照LoadLibrary”机制加载的PE文件。PEImage可以加载为FLAT(与磁盘上的文件布局相同)或MAPPED(PE区段映射到虚拟地址)。
复制代码 隐蔽代码class PEImage{ SString m_path; LONG m_refCount; SString m_sModuleFileNameHintUsedByDac; BOOL m_bIsTrustedNativeImage; BOOL m_bIsNativeImageInstall; BOOL m_bPassiveDomainOnly; SimpleRWLock m_pLayoutLock; PTR_PEImageLayout m_pLayouts[IMAGE_COUNT]; BOOL m_bInHashMap; IMDInternalImport m_pMDImport; IMDInternalImport m_pNativeMDImport;};
m_path是PE映像的路径。如果PEImage是通过文件加载的,那么m_path便是这个文件的路径。如果PEImage是通过内存加载的,也便是利用了Assembly.Load(byte[])等方法加载,那么m_path便是空。m_pLayouts保存了PEImageLayout指针的数组。PEImageLayout供应了详细的PE映像的布局信息,包括模块基址和模块大小。以是m_pLayouts是一个很主要的成员。m_pMDImport是指向IMDInternalImport接口的指针,我们可以用这个接口读取一些元数据信息。这个成员和PEFile的m_pMDImport可以认为是一样的。PEImageLayout
PEImageLayout是指详细的PE映像布局,有MappedImageLayout、LoadedImageLayout、FlatImageLayout等子类。子类的成员不须要关心,主要的部分都在基类PEImageLayout中。
复制代码 隐蔽代码class PEDecoder{ TADDR m_base; COUNT_T m_size; ULONG m_flags; PTR_IMAGE_NT_HEADERS m_pNTHeaders; PTR_IMAGE_COR20_HEADER m_pCorHeader; PTR_CORCOMPILE_HEADER m_pNativeHeader; PTR_READYTORUN_HEADER m_pReadyToRunHeader;};class PEImageLayout : public PEDecoder{ Volatile<LONG> m_refCount; PEImage m_pOwner; DWORD m_Layout;};
m_base是模块基址。m_size是模块大小。m_pCorHeader是指向IMAGE_COR20_HEADER构造的指针。被反Dump保护抹除的偏移就可以利用这个成员规复。m_Layout表示当前布局是什么类型,比如FLAT、MAPPED、LOADED。MDInternalRO && MDInternalRW
这两个类是CLR内部元数据接口IMDInternalImport的实现类。获取了IMDInternalImport接口的指针意味着拿到了这两个类的实例。通过这两个类,我们可以获取关于元数据表流和堆流的所有信息。
复制代码 隐蔽代码class MDInternalRO : public IMDInternalImport, IMDCommon{ CLiteWeightStgdb<CMiniMd> m_LiteWeightStgdb; CMethodSemanticsMap m_pMethodSemanticsMap; // Possible array of method semantics pointers, ordered by method token. mdTypeDef m_tdModule; // <Module> typedef value. LONG m_cRefs; // Ref count.};
m_LiteWeightStgdb是保存了元数据信息的成员,通过它可以读取元数据信息从而规复.NET头。
复制代码 隐蔽代码class MDInternalRW : public IMDInternalImportENC, public IMDCommon{ CLiteWeightStgdbRW m_pStgdb; mdTypeDef m_tdModule; // <Module> typedef value. LONG m_cRefs; // Ref count. bool m_fOwnStgdb; IUnknown m_pUnk; IUnknown m_pUserUnk; // Release at shutdown. IMetaDataHelper m_pIMetaDataHelper;// pointer to cached public interface UTSemReadWrite m_pSemReadWrite; // read write lock for multi-threading. bool m_fOwnSem; // Does MDInternalRW own this read write lock object?};
m_pStgdb和上面的MDInternalRO::m_LiteWeightStgdb一样,是保存了元数据信息的成员,通过它可以读取元数据信息从而规复.NET头。CLiteWeightStgdb && CLiteWeightStgdbRW
这两个是对CMiniMd和CMiniMdRW的包装。CLiteWeightStgdbRW这个类不是非常主要,没有规复.NET头须要的信息。实际上我们只须要CLiteWeightStgdb这个类。它们的定义如下。
复制代码 隐蔽代码template <class MiniMd>class CLiteWeightStgdb{ MiniMd m_MiniMd; // embedded compress meta data schemas definition const void m_pvMd; // Pointer to meta data. ULONG m_cbMd; // Size of the meta data.}class CLiteWeightStgdbRW : public CLiteWeightStgdb<CMiniMdRW>{ UINT32 m_cbSaveSize; // Size of the saved streams. int m_bSaveCompressed; // If true, save as compressed stream (#-, not #~) VOID m_pImage; // Set in OpenForRead, NULL for anything but PE files DWORD m_dwImageSize; // On-disk size of image DWORD m_dwPEKind; // The kind of PE - 0: not a PE. DWORD m_dwMachine; // Machine as defined in NT header. STORAGESTREAMLST m_pStreamList; CLiteWeightStgdbRW m_pNextStgdb; FILETYPE m_eFileType; WCHAR m_wszFileName; // Database file name (NULL or non-empty string) DWORD m_dwDatabaseLFT; // Low bytes of the database file's last write time DWORD m_dwDatabaseLFS; // Low bytes of the database file's size StgIO m_pStgIO; // For file i/o.}
m_MiniMd是CMiniMd和CMiniMdRW,下一小节会提到这两个类。m_pvMd是指向元数据的指针,对应CFF Explorer中.NET Directory的MetaData RVA。m_cbMd是元数据的大小,对应CFF Explorer中.NET Directory的MetaData Size。值得把稳的一点是对付CMiniMdRW,也便是未压缩的表流,m_cbMd是无效的,我们须要自己打算元数据总大小。CMiniMd & CMiniMdRW
CMiniMd是CLR内部的元数据Provider实现,与其类似的还有一个CMiniMdRW。两者不同之处是,CMiniMd是用于#~这种已压缩的表流,而CMiniMdRW是用于#-这种未压缩的表流。
从构造上来说它们有一个共同的基类CMiniMdBase。
复制代码 隐蔽代码class CMiniMdBase{ CMiniMdSchema m_Schema; // data header. ULONG m_TblCount; // Tables in this database. BOOL m_fVerifiedByTrustedSource; // whether the data was verified by a trusted source CMiniTableDef m_TableDefs[TBL_COUNT]; ULONG m_iStringsMask; ULONG m_iGuidsMask; ULONG m_iBlobsMask;};
m_Schema是上面提到的CMiniMdSchemaBase构造的子类,是用来规复表流头部的关键之一。
CLR会为压缩的表流利用CMiniMd,由于它不可扩充,构造体积更小,运行速率也更快。
复制代码 隐蔽代码class CMiniMd : public CMiniMdBase{ MetaData::TableRO m_Tables[TBL_COUNT]; struct MetaData::HotTablesDirectory m_pHotTablesDirectory; MetaData::StringHeapRO m_StringHeap; MetaData::BlobHeapRO m_BlobHeap; MetaData::BlobHeapRO m_UserStringHeap; MetaData::GuidHeapRO m_GuidHeap;};
m_Tables是保存了每一个元数据表的数组。数组元素类型TableRO内部保存了指向每一个元数据表起始地址的指针。用来规复#~。m_StringHeap是字符串流,保存了方法名、类名等元数据字符串。类型StringHeapRO的终极基类是StgPoolSeg,下文会先容。用来规复#Strings。m_BlobHeap是二进制工具流。类型BlobHeapRO的终极基类是StgPoolSeg,下文会先容。用来规复#Blob。m_UserStringHeap是用户字符串流,保存了用户定义的字符串,如'string s = "Hello World"'。类型BlobHeapRO的终极基类是StgPoolSeg,下文会先容。用来规复#US。m_GuidHeap是GUID流。类型GuidHeapRO的终极基类是StgPoolSeg,下文会先容。用来规复#GUID。
对付未压缩的表流#-,CLR会利用CMiniMdRW。它是可以扩充追加数据的。下面列出的只是一部分成员,还有很多没列出的。总之便是比CMiniMd大而且繁芜了不少。
复制代码 隐蔽代码class CMiniMdRW : public CMiniMdBase{ CMemberRefHash m_pMemberRefHash; CMemberDefHash m_pMemberDefHash; CLookUpHash m_pLookUpHashs[TBL_COUNT]; MapSHash<UINT32, UINT32> m_StringPoolOffsetHash; CMetaDataHashBase m_pNamedItemHash; ULONG m_maxRid; // Highest RID so far allocated. ULONG m_limRid; // Limit on RID before growing. ULONG m_maxIx; // Highest pool index so far. ULONG m_limIx; // Limit on pool index before growing. enum {eg_ok, eg_grow, eg_grown} m_eGrow; // Is a grow required? done? MetaData::TableRW m_Tables[TBL_COUNT]; VirtualSort m_pVS[TBL_COUNT]; // Virtual sorters, one per table, but sparse. MetaData::StringHeapRW m_StringHeap; MetaData::BlobHeapRW m_BlobHeap; MetaData::BlobHeapRW m_UserStringHeap; MetaData::GuidHeapRW m_GuidHeap; IMapToken m_pHandler; // Remap handler. ULONG m_cbSaveSize; // Estimate of save size.};
m_Tables是保存了每一个元数据表的数组。数组元素类型TableRW内部是一个StgPoolSeg的子类。用来规复#~。m_StringHeap是字符串流,保存了方法名、类名等元数据字符串。类型StringHeapRW的终极基类是StgPoolSeg,下文会先容。用来规复#Strings。m_BlobHeap是二进制工具流。类型BlobHeapRW的终极基类是StgPoolSeg,下文会先容。用来规复#Blob。m_UserStringHeap是用户字符串流,保存了用户定义的字符串,如'string s = "Hello World"'。类型BlobHeapRW的终极基类是StgPoolSeg,下文会先容。用来规复#US。m_GuidHeap是GUID流。类型GuidHeapRW的终极基类是StgPoolSeg,下文会先容。用来规复#GUID。
这里RW和RO的差异便是,RW是可写的,可以在数据段后再追加数据段,而RO是只读的,初始化之后就不能变动了。
CMiniTableDefCMiniTableDef是表示元数据表定义的构造,里面保存了表的字段、大小、行数,个中行数是我们用来规复.NET头的。
复制代码 隐蔽代码struct CMiniColDef{ BYTE m_Type; // Type of the column. BYTE m_oColumn; // Offset of the column. BYTE m_cbColumn; // Size of the column.};struct CMiniTableDef{ CMiniColDef m_pColDefs; // Array of field defs. BYTE m_cCols; // Count of columns in the table. BYTE m_iKey; // Column which is the key, if any. USHORT m_cbRec; // Size of the records.};
m_pColDefs是表示表内有哪些字段的数组。m_cCols是表内字段数量,也便是m_pColDefs数组的长度。m_cbRec是表的行数,这个是用来规复.NET头中表流头部的关键之一。StgPoolSeg
上面提到的StringHeapRO、BlobHeapRO、GuidHeapRO、StringHeapRW、BlobHeapRW、GuidHeapRW都是终极继续自StgPoolSeg的子类。关键的保存数据位置和大小的成员就在基类StgPoolSeg中。以是理解StgPoolSeg的机构即可。
复制代码 隐蔽代码class StgPoolSeg{ BYTE m_pSegData; // Pointer to the data. StgPoolSeg m_pNextSeg; // Pointer to next segment, or NULL. // Size of the segment buffer. If this is last segment (code:m_pNextSeg is NULL), then it's the // allocation size. If this is not the last segment, then this is shrinked to segment data size // (code:m_cbSegNext). ULONG m_cbSegSize; ULONG m_cbSegNext; // Offset of next available byte in segment. Segment relative.};
通过CLR内部工具规复.NET头
在大致理解反Dump保护可能抹除的数据和CLR内部工具后,我们就可以通过代码定位CLR内部工具,然后规复.NET头了。这里我们做最极度的假设,反Dump技能抹除了所有可能的数据,我们要依赖CLR内部工具规复它们。我们按顺序,从外向内地一层一层规复。
以下提到的代码在文末都有完全地实现。
定位IMAGE_COR20_HEADER对付Data Directories的.NET MetaData Directory。
我们可以利用反射API得到System.Reflection.RuntimeModule。然后利用反射API获取它的私有字段m_pData。这个字段的值是指向CLR内部工具Module的指针。
获取Module工具后,我们利用Module::m_file,得到PEFile工具,这个PEFile是PEAssembly和PEModule,但是实际上只须要利用基类PEFile的内容。
然后我们找到PEFile::m_openedILimage,用来拿到作为PEFile后真个PEImage。
末了我们从PEImage中,获取PEImageLayout即可拿到IMAGE_COR20_HEADER,也便是Data Directories的.NET MetaData Directory。但是PEImage中有好几个PEImageLayout,我们须要的布局是LOADED。LOADED指用来供应IL代码的那一个,并不是一个详细的布局如FLAT、MAPPED,而是一个抽象的。CLR会从已有的布局里面选取一个已经打开的,作为LOADED布局。
大略地用C#代码表示便是:
复制代码 隐蔽代码var module = assembly.Module.m_pData;// Get native Module objectvar pCorHeader = module->m_file->m_openedILimage.m_pLayouts[IMAGE_LOADED]->m_pCorHeader;// Get IMAGE_COR20_HEADER
然后搜索是这些成员偏移的关键代码:
复制代码 隐蔽代码static Pointer ScanLoadedImageLayoutPointer(out bool isMappedLayoutExisting) { const bool InMemory = true; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object uint m_file_Offset; if (RuntimeEnvironment.Version >= RuntimeVersion.Fx453) m_file_Offset = (uint)((nuint)(&Module_453.Dummy->m_file) - (nuint)Module_453.Dummy); else m_file_Offset = (uint)((nuint)(&Module_20.Dummy->m_file) - (nuint)Module_20.Dummy); nuint m_file = (nuint)(module + m_file_Offset); Utils.Check((PEFile)m_file); // Module.m_file uint m_openedILimage_Offset = (uint)((nuint)(&PEFile.Dummy->m_openedILimage) - (nuint)PEFile.Dummy); nuint m_openedILimage = (nuint)(m_file + m_openedILimage_Offset); Utils.Check((PEImage)m_openedILimage, InMemory); // PEFile.m_openedILimage nuint m_pMDImport = MetadataImport.Create(assembly.Module).This; uint m_pMDImport_Offset; bool found = false; for (m_pMDImport_Offset = 0x40; m_pMDImport_Offset < 0xD0; m_pMDImport_Offset += 4) { if ((nuint)(m_openedILimage + m_pMDImport_Offset) != m_pMDImport) continue; found = true; break; } Utils.Check(found); // PEFile.m_pMDImport (not use, just for locating previous member 'm_pLayouts') isMappedLayoutExisting = false; uint m_pLayouts_Loaded_Offset = m_pMDImport_Offset - 4 - (uint)sizeof(nuint); uint m_pLayouts_Offset_Min = m_pLayouts_Loaded_Offset - (4 (uint)sizeof(nuint)); nuint actualModuleBase = ReflectionHelpers.GetNativeModuleHandle(assembly.Module); found = false; for (; m_pLayouts_Loaded_Offset >= m_pLayouts_Offset_Min; m_pLayouts_Loaded_Offset -= 4) { var m_pLayout = (RuntimeDefinitions.PEImageLayout)(m_openedILimage + m_pLayouts_Loaded_Offset); if (!Memory.TryReadUIntPtr((nuint)m_pLayout, out _)) continue; if (!Memory.TryReadUIntPtr(m_pLayout->__vfptr, out _)) continue; if (actualModuleBase != m_pLayout->__base.m_base) continue; Debug2.Assert(InMemory); var m_pLayout_prev1 = (RuntimeDefinitions.PEImageLayout)(m_openedILimage + m_pLayouts_Loaded_Offset - (uint)sizeof(nuint)); var m_pLayout_prev2 = (RuntimeDefinitions.PEImageLayout)(m_openedILimage + m_pLayouts_Loaded_Offset - (2 (uint)sizeof(nuint))); if (m_pLayout_prev2 == m_pLayout) isMappedLayoutExisting = true; else if (m_pLayout_prev1 == m_pLayout) isMappedLayoutExisting = false; // latest .NET, TODO: update comment when .NET 7.0 released found = true; break; } Utils.Check(found); nuint m_pLayouts_Loaded = (nuint)(m_openedILimage + m_pLayouts_Loaded_Offset); Utils.Check((RuntimeDefinitions.PEImageLayout)m_pLayouts_Loaded, InMemory); // PEImage.m_pLayouts[IMAGE_LOADED] uint m_pCorHeader_Offset = (uint)((nuint)(&RuntimeDefinitions.PEImageLayout.Dummy->__base.m_pCorHeader) - (nuint)RuntimeDefinitions.PEImageLayout.Dummy); nuint m_pCorHeader = (nuint)(m_pLayouts_Loaded + m_pCorHeader_Offset); Utils.Check((IMAGE_COR20_HEADER)m_pCorHeader); // PEImageLayout.m_pCorHeader var pointer = new Pointer(new[] { m_file_Offset, m_openedILimage_Offset, m_pLayouts_Loaded_Offset }); Utils.Check(Utils.Verify(pointer, null, p => Memory.TryReadUIntPtr(p + (uint)sizeof(nuint), out nuint @base) && (ushort)home.php?mod=space&uid=1282447 == 0)); Utils.Check(Utils.Verify(Utils.WithOffset(pointer, m_pCorHeader_Offset), null, p => Memory.TryReadUInt32(p, out uint cb) && cb == 0x48)); return pointer;}
定位CLiteWeightStgdb
在定位MetaData之前,我们获取元数据干系信息须要先定位到CLiteWeightStgdb构造。
大略表示:
复制代码 隐蔽代码var pMDImport = GetMetadataImport(assembly.Module);// Get IMDInternalImportvar pStgdb = null;if (table_stream_is_compressed) pStgdb = &(((MDInternalRO)pMDImport)->m_LiteWeightStgdb);else pStgdb = ((MDInternalRW)pMDImport->m_pStgdb;// Get CLiteWeightStgdb
关键代码:
复制代码 隐蔽代码static Pointer ScanLiteWeightStgdbPointer(bool uncompressed, out nuint vfptr) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object uint m_file_Offset; if (RuntimeEnvironment.Version >= RuntimeVersion.Fx453) m_file_Offset = (uint)((nuint)(&Module_453.Dummy->m_file) - (nuint)Module_453.Dummy); else m_file_Offset = (uint)((nuint)(&Module_20.Dummy->m_file) - (nuint)Module_20.Dummy); nuint m_file = (nuint)(module + m_file_Offset); Utils.Check((PEFile)m_file); // Module.m_file var metadataImport = MetadataImport.Create(assembly.Module); vfptr = metadataImport.Vfptr; nuint m_pMDImport = metadataImport.This; uint m_pMDImport_Offset; bool found = false; for (m_pMDImport_Offset = 0; m_pMDImport_Offset < 8 (uint)sizeof(nuint); m_pMDImport_Offset += 4) { if ((nuint)(m_file + m_pMDImport_Offset) != m_pMDImport) continue; found = true; break; } Utils.Check(found); // PEFile.m_pMDImport uint m_pStgdb_Offset = 0; if (uncompressed) { if (RuntimeEnvironment.Version >= RuntimeVersion.Fx45) m_pStgdb_Offset = (uint)((nuint)(&MDInternalRW_45.Dummy->m_pStgdb) - (nuint)MDInternalRW_45.Dummy); else m_pStgdb_Offset = (uint)((nuint)(&MDInternalRW_20.Dummy->m_pStgdb) - (nuint)MDInternalRW_20.Dummy); } // MDInternalRW.m_pStgdb var pointer = new Pointer(new[] { m_file_Offset, m_pMDImport_Offset }); if (m_pStgdb_Offset != 0) pointer.Add(m_pStgdb_Offset); Utils.Check(Utils.Verify(pointer, uncompressed, p => Memory.TryReadUInt32(p, out _))); return pointer;}
定位元数据
在定位了IMAGE_COR20_HEADER之后,里面有一个最关键的成员MetaData须要定位。
大略表示:
复制代码 隐蔽代码var pMDImport = GetMetadataImport(assembly.Module);// Get IMDInternalImportvar m_pvMd = null;if (table_stream_is_compressed) m_pvMd = ((MDInternalRO)pMDImport)->m_LiteWeightStgdb.m_pvMd;else m_pvMd = ((MDInternalRW)pMDImport->m_pStgdb->m_pvMd;// Get metadata address
关键代码:
复制代码 隐蔽代码static void ScanMetadataOffsets(Pointer stgdbPointer, bool uncompressed, out uint metadataAddressOffset, out uint metadataSizeOffset) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object nuint pStgdb = Utils.ReadUIntPtr(stgdbPointer, module); var peInfo = PEInfo.Create(assembly.Module); var imageLayout = peInfo.MappedLayout.IsInvalid ? peInfo.LoadedLayout : peInfo.MappedLayout; var m_pCorHeader = (IMAGE_COR20_HEADER)imageLayout.CorHeaderAddress; nuint m_pvMd = imageLayout.ImageBase + m_pCorHeader->MetaData.VirtualAddress; uint m_cbMd = uncompressed ? 0x1c : m_pCorHeader->MetaData.Size; // pcb = sizeof(STORAGESIGNATURE) + pStorage->GetVersionStringLength(); // TODO: we should calculate actual metadata size for uncompressed metadata uint start = uncompressed ? (sizeof(nuint) == 4 ? 0x1000u : 0x19A0) : (sizeof(nuint) == 4 ? 0x350u : 0x5B0); uint end = uncompressed ? (sizeof(nuint) == 4 ? 0x1200u : 0x1BA0) : (sizeof(nuint) == 4 ? 0x39Cu : 0x5FC); uint m_pvMd_Offset = 0; for (uint offset = start; offset <= end; offset += 4) { if ((nuint)(pStgdb + offset) != m_pvMd) continue; if ((uint)(pStgdb + offset + (uint)sizeof(nuint)) != m_cbMd) continue; m_pvMd_Offset = offset; break; } Utils.Check(m_pvMd_Offset != 0); Utils.Check(Utils.Verify(Utils.WithOffset(stgdbPointer, m_pvMd_Offset), uncompressed, p => Memory.TryReadUInt32(p, out uint signature) && signature == 0x424A5342)); metadataAddressOffset = m_pvMd_Offset; metadataSizeOffset = m_pvMd_Offset + (uint)sizeof(nuint);}
定位元数据表流头部
表流相对来说麻烦一些,有更多的数据要填。首先是获取表流Schema。
大略表示:
复制代码 隐蔽代码var pMDImport = GetMetadataImport(assembly.Module);// Get IMDInternalImportvar pMiniMd = null;if (table_stream_is_compressed) pMiniMd = &(((MDInternalRO)pMDImport)->m_LiteWeightStgdb.m_MiniMd);else pMiniMd = &(((MDInternalRW)pMDImport->m_pStgdb->m_MiniMd);// Get CMiniMdvar m_Schema = pMiniMd->m_Schema;// Get metadata schema
关键代码:
复制代码 隐蔽代码static void ScanSchemaOffset(Pointer stgdbPointer, MiniMetadataInfo info, bool uncompressed, out uint schemaOffset) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object nuint pStgdb = Utils.ReadUIntPtr(stgdbPointer, module); for (schemaOffset = 0; schemaOffset < 0x30; schemaOffset += 4) { if ((ulong)(pStgdb + schemaOffset) != info.Header1) continue; if ((ulong)(pStgdb + schemaOffset + 0x08) != info.ValidMask) continue; if ((ulong)(pStgdb + schemaOffset + 0x10) != info.SortedMask) continue; break; } Utils.Check(schemaOffset != 0x30); // CMiniMdBase.m_Schema}
在获取Schema后,我们还要获取目标模块存在哪些元数据表,行数分别是多少。由于CLR内部没有保存行数,而是直接保存了指向每个元数据表的指针,以是我们须要获取每个元数据表的地址,然后打算通过表的大小除以每行大小,打算求出每个元数据表的行数。
大略表示:
复制代码 隐蔽代码var pMDImport = GetMetadataImport(assembly.Module);// Get IMDInternalImportvar pMiniMd = null;if (table_stream_is_compressed) pMiniMd = &(((MDInternalRO)pMDImport)->m_LiteWeightStgdb.m_MiniMd);else pMiniMd = &(((MDInternalRW)pMDImport->m_pStgdb->m_MiniMd);// Get CMiniMdvar m_TableDefs = pMiniMd->m_TableDefs;// Get metadata table definitions (to get row size)var m_Tables = pMiniMd->m_Tables;// Get metadata tables (to get table address)
关键代码:
复制代码 隐蔽代码static void ScanTableDefsOffsets(Pointer stgdbPointer, bool uncompressed, uint schemaOffset, out uint tableCountOffset, out uint tableDefsOffset) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object nuint pSchema = Utils.ReadPointer(Utils.WithOffset(stgdbPointer, schemaOffset), module); nuint p = pSchema + (uint)sizeof(CMiniMdSchema); uint m_TblCount = (uint)p; tableCountOffset = schemaOffset + (uint)(p - pSchema); Utils.Check(m_TblCount == TBL_COUNT_V1 || m_TblCount == TBL_COUNT_V2); // CMiniMdBase.m_TblCount if (RuntimeEnvironment.Version >= RuntimeVersion.Fx40) p += (uint)((nuint)(&CMiniMdBase_40.Dummy->m_TableDefs) - (nuint)(&CMiniMdBase_40.Dummy->m_TblCount)); else p += (uint)((nuint)(&CMiniMdBase_20.Dummy->m_TableDefs) - (nuint)(&CMiniMdBase_20.Dummy->m_TblCount)); tableDefsOffset = schemaOffset + (uint)(p - pSchema); var m_TableDefs = (CMiniTableDef)p; for (int i = 0; i < TBL_COUNT; i++) Utils.Check(Memory.TryReadUInt32((nuint)m_TableDefs[i].m_pColDefs, out _)); // CMiniMdBase.m_TableDefs}static void ScanTableOffset(Pointer stgdbPointer, MiniMetadataInfo info, bool uncompressed, out uint tableAddressOffset, out uint nextTableOffset) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object tableAddressOffset = 0; nextTableOffset = 0; nuint pStgdb = Utils.ReadUIntPtr(stgdbPointer, module); uint start = uncompressed ? (sizeof(nuint) == 4 ? 0x2A0u : 0x500) : (sizeof(nuint) == 4 ? 0x200u : 0x350); uint end = uncompressed ? (sizeof(nuint) == 4 ? 0x4A0u : 0x800) : (sizeof(nuint) == 4 ? 0x300u : 0x450); for (uint offset = start; offset < end; offset += 4) { nuint pFirst = pStgdb + offset; if ((nuint)pFirst != info.TableAddress[0]) continue; uint start2 = 4; uint end2 = uncompressed ? 0x100u : 0x20; uint offset2 = start2; for (; offset2 < end2; offset2 += 4) { if ((nuint)(pFirst + offset2) != info.TableAddress[1]) continue; if ((nuint)(pFirst + (2 offset2)) != info.TableAddress[2]) continue; break; } if (offset2 == end2) continue; tableAddressOffset = offset; nextTableOffset = offset2; break; } Utils.Check(tableAddressOffset != 0); Utils.Check(nextTableOffset != 0); // CMiniMd.m_Tables}
定位元数据堆流
末了我们定位元数据堆流,也便是#Strings、#US、#GUID、#Blob这四个堆流。还原的时候比较大略,我们只须要把这4个堆流的偏移、大小和名字写入到.NET头。
大略表示:
复制代码 隐蔽代码var pMDImport = GetMetadataImport(assembly.Module);// Get IMDInternalImportvar pMiniMd = null;if (table_stream_is_compressed) pMiniMd = &(((MDInternalRO)pMDImport)->m_LiteWeightStgdb.m_MiniMd);else pMiniMd = &(((MDInternalRW)pMDImport->m_pStgdb->m_MiniMd);// Get CMiniMdvar m_StringHeap = pMiniMd->m_StringHeap;// Get #Stringsvar m_BlobHeap = pMiniMd->m_BlobHeap;// Get #Blobvar m_UserStringHeap = pMiniMd->m_UserStringHeap;// Get #USvar m_GuidHeap = pMiniMd->m_GuidHeap;// Get #GUID
关键代码:
复制代码 隐蔽代码static void ScanHeapOffsets(Pointer stgdbPointer, MiniMetadataInfo info, bool uncompressed, out uint[] heapAddressOffsets, out uint[] heapSizeOffsets) { const bool InMemory = false; var assemblyFlags = InMemory ? TestAssemblyFlags.InMemory : 0; if (uncompressed) assemblyFlags |= TestAssemblyFlags.Uncompressed; var assembly = TestAssemblyManager.GetAssembly(assemblyFlags); nuint module = assembly.ModuleHandle; Utils.Check((Module)module, assembly.Module.Assembly.GetName().Name); // Get native Module object nuint pStgdb = Utils.ReadUIntPtr(stgdbPointer, module); uint start = uncompressed ? (sizeof(nuint) == 4 ? 0xD00u : 0x1500) : (sizeof(nuint) == 4 ? 0x2A0u : 0x500); uint end = uncompressed ? (sizeof(nuint) == 4 ? 0x1000u : 0x1900) : (sizeof(nuint) == 4 ? 0x3A0u : 0x600); heapAddressOffsets = new uint[4]; heapSizeOffsets = new uint[heapAddressOffsets.Length]; int found = 0; for (uint offset = start; offset < end; offset += 4) { nuint address = (nuint)(pStgdb + offset); uint size = (uint)(pStgdb + offset + (2 (uint)sizeof(nuint))); if (address == info.StringHeapAddress) { Utils.Check(info.StringHeapSize - 8 < size && size <= info.StringHeapSize); Utils.Check(heapAddressOffsets[0] == 0); heapAddressOffsets[StringHeapIndex] = offset; heapSizeOffsets[StringHeapIndex] = offset + (2 (uint)sizeof(nuint)); found++; } else if (address == info.UserStringHeapAddress) { Utils.Check(info.UserStringHeapSize - 8 < size && size <= info.UserStringHeapSize); Utils.Check(heapAddressOffsets[1] == 0); heapAddressOffsets[UserStringsHeapIndex] = offset; heapSizeOffsets[UserStringsHeapIndex] = offset + (2 (uint)sizeof(nuint)); found++; } else if (address == info.GuidHeapAddress) { Utils.Check(info.GuidHeapSize - 8 < size && size <= info.GuidHeapSize); Utils.Check(heapAddressOffsets[2] == 0); heapAddressOffsets[GuidHeapIndex] = offset; heapSizeOffsets[GuidHeapIndex] = offset + (2 (uint)sizeof(nuint)); found++; } else if (address == info.BlobHeapAddress) { Utils.Check(info.BlobHeapSize - 8 < size && size <= info.BlobHeapSize); Utils.Check(heapAddressOffsets[3] == 0); heapAddressOffsets[BlobHeapIndex] = offset; heapSizeOffsets[BlobHeapIndex] = offset + (2 (uint)sizeof(nuint)); found++; } } Utils.Check(found == 4); // Find heeap info offsets for (int i = 0; i < heapAddressOffsets.Length; i++) Utils.Check(Utils.Verify(Utils.WithOffset(stgdbPointer, heapAddressOffsets[i]), uncompressed, p => Memory.TryReadUInt32(p, out _)));}
规复.NET头
在探求完须要的CLR内部工具的成员偏移后,我们就可以通过这些信息来还原.NET头了。
复制代码 隐蔽代码static unsafe void FixDotNetHeaders(byte[] data, MetadataInfo metadataInfo, PEImageLayout imageLayout) { fixed (byte p = data) { var pNETDirectory = (IMAGE_DATA_DIRECTORY)(p + GetDotNetDirectoryRVA(data)); pNETDirectory->VirtualAddress = (uint)imageLayout.CorHeaderAddress; pNETDirectory->Size = (uint)sizeof(IMAGE_COR20_HEADER); // Set Data Directories var pCor20Header = (IMAGE_COR20_HEADER)(p + (uint)imageLayout.CorHeaderAddress); pCor20Header->cb = (uint)sizeof(IMAGE_COR20_HEADER); pCor20Header->MajorRuntimeVersion = 0x2; pCor20Header->MinorRuntimeVersion = 0x5; pCor20Header->MetaData.VirtualAddress = (uint)metadataInfo.MetadataAddress; pCor20Header->MetaData.Size = GetMetadataSize(metadataInfo); // Set .NET Directory var pStorageSignature = (STORAGESIGNATURE)(p + (uint)metadataInfo.MetadataAddress); pStorageSignature->lSignature = 0x424A5342; pStorageSignature->iMajorVer = 0x1; pStorageSignature->iMinorVer = 0x1; pStorageSignature->iExtraData = 0x0; pStorageSignature->iVersionString = 0xC; var versionString = Encoding.ASCII.GetBytes("v4.0.30319"); for (int i = 0; i < versionString.Length; i++) pStorageSignature->pVersion[i] = versionString[i]; // versionString仅仅占位用,程序集详细运行时版本用dnlib获取 // Set StorageSignature var pStorageHeader = (STORAGEHEADER)((byte)pStorageSignature + 0x10 + pStorageSignature->iVersionString); pStorageHeader->fFlags = 0x0; pStorageHeader->pad = 0x0; pStorageHeader->iStreams = 0x5; // Set StorageHeader var pStreamHeader = (uint)((byte)pStorageHeader + sizeof(STORAGEHEADER)); var tableStream = metadataInfo.TableStream; if (!tableStream.IsInvalid) { pStreamHeader = (uint)tableStream.Address; pStreamHeader -= (uint)metadataInfo.MetadataAddress; pStreamHeader++; pStreamHeader = tableStream.Length; pStreamHeader++; pStreamHeader = tableStream.IsCompressed ? 0x00007E23u : 0x000002D23; pStreamHeader++; } // Set #~ or #- var stringHeap = metadataInfo.StringHeap; if (!stringHeap.IsInvalid) { pStreamHeader = (uint)stringHeap.Address; pStreamHeader -= (uint)metadataInfo.MetadataAddress; pStreamHeader++; pStreamHeader = stringHeap.Length; pStreamHeader++; pStreamHeader = 0x72745323; pStreamHeader++; pStreamHeader = 0x73676E69; pStreamHeader++; pStreamHeader = 0x00000000; pStreamHeader++; } // Set #Strings var userStringHeap = metadataInfo.UserStringHeap; if (!userStringHeap.IsInvalid) { pStreamHeader = (uint)userStringHeap.Address; pStreamHeader -= (uint)metadataInfo.MetadataAddress; pStreamHeader++; pStreamHeader = userStringHeap.Length; pStreamHeader++; pStreamHeader = 0x00535523; pStreamHeader++; } // Set #US var guidHeap = metadataInfo.GuidHeap; if (!guidHeap.IsInvalid) { pStreamHeader = (uint)guidHeap.Address; pStreamHeader -= (uint)metadataInfo.MetadataAddress; pStreamHeader++; pStreamHeader = guidHeap.Length; pStreamHeader++; pStreamHeader = 0x49554723; pStreamHeader++; pStreamHeader = 0x00000044; pStreamHeader++; } // Set #GUID var blobHeap = metadataInfo.BlobHeap; if (!blobHeap.IsInvalid) { pStreamHeader = (uint)blobHeap.Address; pStreamHeader -= (uint)metadataInfo.MetadataAddress; pStreamHeader++; pStreamHeader = blobHeap.Length; pStreamHeader++; pStreamHeader = 0x6F6C4223; pStreamHeader++; pStreamHeader = 0x00000062; pStreamHeader++; } // Set #GUID switch (GetCorLibVersion(data).Major) { case 2: versionString = Encoding.ASCII.GetBytes("v2.0.50727"); break; case 4: versionString = Encoding.ASCII.GetBytes("v4.0.30319"); break; default: throw new NotSupportedException(); } for (int i = 0; i < versionString.Length; i++) pStorageSignature->pVersion[i] = versionString[i]; // Re set Version }}
源码与成品下载
这个方法已经在我最新的ExtremeDumper里面实现了,可以实现对.NET程序集反Dump保护绕过。
元数据定位的代码:https://github.com/wwh1004/MetadataLocator
通过CLR内部工具还原.NET头:https://github.com/wwh1004/ExtremeDumper/tree/master/ExtremeDumper.AntiAntiDump