.NET 内部和原生编译






4.94/5 (71投票s)
一篇关于 .NET 内部和原生编译的文章。
引言
本文是关于 .NET Framework 内部以及 .NET 程序集可用保护措施的两篇系列文章中的第二篇。本文更深入地分析了 .NET 内部。因此,读者应该熟悉前一篇文章,否则本文的某些段落可能会显得晦涩。由于 JIT 的内部工作原理尚未进行分析,.NET 的保护措施目前相当幼稚。一旦逆向工程社区将注意力集中在这项技术上,这种情况将迅速改变。这两篇文章旨在提高人们对 .NET 保护措施当前状态以及可能实现但尚未实现的功能的认识。特别是,前一篇文章关于 .NET 代码注入的文章,可以说是代表了现在,而当前关于 .NET 原生编译的文章则代表了未来。我在写作时介绍的这两篇文章的内容都是新的,但我预计它们将在一年内过时。当然,这是显而易见的,因为我正在走出当前 .NET 保护措施的范围,朝着更好的保护措施方向迈出第一步。但本文并非真正关于保护措施:探索 .NET Framework 内部可以用于多种目的。因此,谈论保护措施只是达到目的的手段。
什么是原生编译?
严格来说,它意味着将 .NET 程序集的 MSIL 代码转换为原生机器代码,然后从该程序集中删除 MSIL 代码,使其无法以直接的方式进行反编译。唯一可用于原生编译 .NET 程序集的工具是 Salamander.NET 链接器,它依赖原生映像来完成其工作。“原生映像”(在本文中我称之为“原生框架部署”)技术与 .NET 内部相距甚远:实现它不需要对 .NET 内部有深入的了解。但是,由于这个主题可能非常流行,我将向读者展示如何编写自己的原生框架部署工具(如果他们愿意)。然而,本文将超越这一点,介绍原生注入,它只不过是取代 JIT 的位置。尽管这对商业保护(或其他任何东西)没有用处,但它是玩转 JIT 内部的好方法。我还会介绍原生反编译,这是对 .NET 内部理解的结果。我还试图解决另一个主题:.NET 虚拟机保护。
原生映像
原生映像的内部格式尚未文档化。而且,由于它不断变化,文档化起来会相当困难。例如,它在 .NET Framework 的第一个版本和第二个版本之间完全改变了。而且,由于几天前发布了新的 Framework 3.5 SP1,它又改变了一次。我不确定它在最新版本中改变的程度,但有一个变化可以立即注意到。原始元数据现在可以直接访问,而无需更改 .NET 目录中的条目到 Native Header 中的 MetaData RVA。如果您执行此操作,您将得到原生映像的元数据,这并没有多大意义。此外,在早期的原生映像中(3.5 SP1 框架之前),要获取方法的原始 MSIL 代码,必须将 MethodDef 表中的 RVA 添加到原生头中的 Original MSIL Code RVA 条目。现在不再需要这样做,因为 MethodDef RVA 条目现在直接指向方法的 MSIL 代码。
这很重要,因为像 Salamander Linker 这样的保护措施在部署原生映像之前需要删除原始 MSIL 代码。否则,整个保护措施将毫无用处,因为元数据和 MSIL 代码是重建一个完全可反编译的 .NET 程序集所需的一切。在“旧”格式中,剥离 MSIL 代码更容易,因为只需要 Original MSIL Code RVA 和 Size 条目就知道需要使用简单的 memset 将原生映像的哪个部分擦除。
为了编写原生框架部署工具,我们只需要了解原生映像格式即可。即使是 Salamander Linker 也需要时间来适应新的原生映像格式,以便与框架 3.5 SP1 一起工作。而且,目前没有针对 3.5 SP1 原生映像的保护措施,因此本文的内容仅针对早期映像进行了测试。
另一个难以文档化原生映像的原因是 Rotor 项目中缺少处理它们的代码。微软故意选择将框架的这一部分排除在 Rotor 项目之外。
原生框架部署
我给这种保护措施起的名字可能有点奇怪,但一旦我解释了它是如何工作的,它就会变得很明显。如前所述,除了 Salamander Linker 之外,没有其他保护系统会删除 MSIL 并仅打包原生机器代码。而且,为了做到这一点,Salamander Linker 依赖 ngen 生成的原生映像。Salamander Linker 在其主页上提供了一个可下载的演示版,我们将看一下,当然,不会分析其代码,因为我无意侵犯它可能包含的任何许可条款。在这一段中,我将展示编写一个原生框架部署工具在技术上是多么简单,但我怀疑读者在阅读本文后会想编写一个。不要误解我,Salamander Linker 绝对信守其承诺,并且确实从应用程序中删除了 MSIL 代码,但所使用的方法面临许多问题,在我看来并非真正的解决方案。
Salamander Linker 的演示版名为 scribble,它是一个简单的 MDI 应用程序。我们来看看应用程序的主目录
v2.0.50727 目录对应于位于 C:\Windows\Microsoft.NET\ 的框架目录,尽管它只包含少量文件
我稍后会解释为什么一些重要的程序集,如 System 或 System.Windows.Forms 缺失。与此同时,C 目录会导向一系列其他目录。它产生的主要路径看起来像这样:C\WINDOWS\assembly\。在此路径的最后一个目录中包含另外两个目录。一个目录称为 GAC_32,包含 mscorlib 程序集。另一个目录称为 NativeImages_v2.0.50727_32,是存储原生映像的目录。此目录只包含两个原生映像:mscorlib 的映像和 scribble 的映像。scribble 原生映像非常庞大,因为在 ngening scribble 之前,它与依赖项 System、System.Windows.Forms 等合并了。唯一无法合并到其他程序集的依赖项是 mscorlib。原因有很多。读者可以想象其中一个,如果他读过前一篇文章:mscorlib 是一个与框架紧密相关的低级程序集,它执行诸如提供内部调用实现之类的操作。如果一个非系统程序集尝试调用一个内部函数,它只会导致框架显示一个权限错误。
Salamander Linker 部署了框架的一个子集。因此,我给这项技术起名为原生框架部署。原生映像以一种相当复杂的方式绑定到框架。实际上,原生映像高度依赖于框架。但我们暂时只关注本地系统上程序集与其原生映像之间的关系。一个人可以随意修改程序集,但只要保留其 #GUID 流和元数据表中的一些数据不变,就会加载相同的原生映像。这意味着一个人甚至可以将一个完全不同的程序集绑定到一个原生映像。这很容易实现:首先,让我们 ngen 一个随机的程序集。程序集通过注册表绑定到它们的原生映像。注册表项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\NativeImagesIndex\v2.0.50727_32 是程序集和原生映像之间发生绑定的地方。
此项有两个子项:“IL”和“NI”。“IL”项包含一系列子项,这些子项代表 ngened 程序集以及将它们绑定到其原生映像所需的信息。
请记住 DisplayName,因为 SIG 值包含程序集的 GUID 及其 SHA1 哈希。
所选字节代表 SHA1 哈希。具有讽刺意味的是,此哈希并不用于将实际程序集绑定到其原生映像。但这种行为将来可能会改变,所以值得一提。
“NI”项的子项告诉框架在何处查找给定程序集的原生映像。
MVID 值指定了原生映像的路径。在这种情况下,它将是:C:\Windows\assembly\NativeImages_v2.0.50727_32\rebtest\0f12d8560d3b72df51b3471002c911a0。另外,应注意“511072a1”子项引用了相应的“IL”子项。
因此,为了将另一个程序集绑定到此程序集的原生映像,有必要更改其 GUID 和程序集元数据表。
程序集元数据表中的 Name 应更改为显示名称(在本例中为“rebtest”)。同时,相应地更改 MajorVersion、MinorVersion、BuildNumber 和 RevisionNumber。我在这里显示了 Module Table 的图像只是因为更改它也很合乎逻辑,但框架并不关心它。因此,我们也不关心。
这就是绑定本地映像所需的一切,并且它也适用于框架 3.5 SP1。当然,在另一台计算机上绑定原生映像并不容易,因为原生映像依赖于框架/系统。而且也不能保证成功,因为如前所述,原生映像会随着新版本的框架而改变。可以通过打包整个框架以及原生映像来“解决”这个问题。
我们回到 Salamander Linker 演示版的主目录。Scribble.exe 是一个原生 exe,它加载 Scribble.rsm。Scribble.rsm 是一个空的程序集,用于加载原生映像。该空程序集与原生映像之间的绑定方式如上所述。通过打包其自身的框架版本,Salamander Linker 只需担心本地绑定。当然,仅仅将框架文件放在文件夹中不足以进行部署。还需要提供虚拟化。mdepoy.registry 是一个包含要虚拟化的注册表项的文本文件。它看起来像这样
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\NativeImagesIndex\v2.0.50727_32\IL]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\NativeImagesIndex\v2.0.50727_32\IL\
23ca0da0]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\NativeImagesIndex\v2.0.50727_32\
IL\23ca0da0\2bbf7a73]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\NativeImagesIndex\v2.0.50727_32\IL\
23ca0da0\2bbf7a73\8]
"DisplayName"="Scribble,0.0.0.0,,"
"SIG"=hex:af,ab,74,2d,d3,3a,1c,43,be,55,fc,b4,11,39,af,45,b7,ce,d1,a1,22,41,42,\
18,11,62,fb,d2,01,d5,41,f6,24,46,e2,15
"Status"=dword:00000000
"LastModTime"=hex:00,00,00,00,00,00,00,00
实际文件要大得多(31 KB)。rsdeploy.dll 是 Salamander Linker 中执行大部分工作的组件:它挂钩所有它需要虚拟化框架的 API。这可以很容易地通过不分析其代码来验证。它需要挂钩的 API 中当然包括 LoadLibrary,以及所有注册表函数。它还需要挂钩一些其他函数,我将在下一段中讨论。
虚拟化应用程序时,不仅要考虑文件系统和注册表。还需要考虑环境变量。如果我们使用 Russinovich 的 Process Explorer 查看 Scribble 进程的环境,我们会注意到一些东西
Salamander Linker 将 COMPLUS_InstallRoot 变量设置为其自己的主目录。由于此变量未使用,并且即使没有它框架也能加载,所以我的猜测是,它是框架 1.0 的一个已弃用变量。
这就是开发自己的原生框架部署工具所需了解的所有内容。有人可能会问合并部分在哪里。实际上,合并并不是真正必要的。它只是让事情更容易,而且,由于打包了整个框架,它加快了性能。我可以轻松地调整 Rebel.NET 的代码来编写一个程序集合并器(这将是两周的工作),但我对通过合并程序集可以实现的任何东西都不感兴趣:例如,编写这样的保护措施。作为替代方案,人们可以考虑使用 ILMerge,这是一个也可以用于商业应用程序的 Microsoft 工具。唯一的缺点是它速度极慢(它是一个 .NET 程序集),而且我曾遇到过它不起作用的情况,但这可能会随着时间而改善。在接下来的子段中,我将讨论原生框架部署服务可能开发的某些方面。
原生加载器
让我们看看一个可能的原生框架部署服务加载器可能是什么样子。以下只是加载器的第一个草稿:我还没有介绍完整的加载器,因为我正在逐步进行。
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
//
// set COMPLUS_InstallRoot environment variable
// (useless on framework 2.0 and later)
//
/*
TCHAR CurPath[MAX_PATH];
GetModuleFileName(NULL, CurPath, MAX_PATH);
TCHAR *pSlash = _tcsrchr(CurPath, '\\');
if (pSlash) *pSlash = 0;
SetEnvironmentVariable(_T("COMPLUS_InstallRoot"), CurPath);
*/
//////////////////////////////////////////////////////////////////////////
// TODO: hook registry APIs, LoadLibrary and ...
//////////////////////////////////////////////////////////////////////////
HMODULE hMainAsm = LoadLibrary(ASSEMBLY_TO_LOAD);
if (hMainAsm == NULL) return 0;
IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *) hMainAsm;
IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew +
(ULONG_PTR) pDosHeader);
if (pNtHeaders->OptionalHeader.ImageBase != (ULONG_PTR) pDosHeader)
FixReloc(pDosHeader, pNtHeaders);
FixIAT(pDosHeader, pNtHeaders);
// retrieve entry point
VOID *pEntryPoint = (VOID *) (pNtHeaders->OptionalHeader.AddressOfEntryPoint +
(ULONG_PTR) pDosHeader);
__asm jmp pEntryPoint;
return 0;
}
关于这段代码有几件事要说。首先,读者可能不明白我为什么修复 IAT 和重定位。通常,LoadLibrary
(我正在使用它来加载程序集)会执行此任务,但在安装了 .NET Framework 的系统上,它不会为 .NET 程序集执行此任务。修复 PE 后,我跳转到程序集的入口点(它只是跳转到 mscoree 中的 _CorExeMain
)。实际上,我可以直接调用 _CorExeMain
而无需跳转到原始入口点。因此,修复 IAT 和重定位的代码是不必要的。我这样做是为了避免将来的任何不兼容性。加载程序集的关键在于理解 _CorExeMain 将如何在当前地址空间中检索主程序集的基地址。_CorExeMain
的代码在执行一些检查以加载正确的 .NET 运行时后,会调用 mscorwks 中的同一个函数。这是 mscorwks 中的代码
.text:79F05ECA ; int __stdcall _CorExeMain()
.text:79F05ECA public __CorExeMain@0
.text:79F05ECA __CorExeMain@0 proc near
.text:79F05ECA
.text:79F05ECA var_2C = byte ptr -2Ch
.text:79F05ECA var_28 = dword ptr -28h
.text:79F05ECA var_1C = byte ptr -1Ch
.text:79F05ECA var_18 = dword ptr -18h
.text:79F05ECA var_14 = dword ptr -14h
.text:79F05ECA var_4 = dword ptr -4
.text:79F05ECA
.text:79F05ECA ; FUNCTION CHUNK AT .text:79FBF47D SIZE 0000005A BYTES
.text:79F05ECA ; FUNCTION CHUNK AT .text:79FBF4FC SIZE 00000042 BYTES
.text:79F05ECA
.text:79F05ECA push 20h
.text:79F05ECC mov eax, offset loc_7A2EE124
.text:79F05ED1 call __EH_prolog3_catch
.text:79F05ED6 xor edi, edi
.text:79F05ED8 push edi ; lpModuleName
.text:79F05ED9 call ?WszGetModuleHandle@@YGPAUHINSTANCE__@@PBG@Z ; WszGetModuleHandle(ushort const *)
mscorwks 中的 _CorExeMain
函数通过调用 WszGetModuleHandle
内部的 GetModuleHandleA/W(NULL)
来检索主程序集。不仅如此:在 GetModuleHandle
之前,GetModuleFileName
会在 mscoree 中调用。此 API 接受与 GetModuleHandle
相同的 NULL 语法,以获取有关当前地址空间中主模块的信息。因此,告诉框架主程序集是哪个的最简单方法是挂钩 GetModuleHandleA/W
和 GetModuleFileNameA/W
。我决定使用 Microsoft 的 Detour 来实现挂钩,因为它对研究项目是免费许可的,并且保证在所有 Windows 平台上都能正常工作。这是实际加载器的代码
#include "stdafx.h"
#include "fxloader.h"
#include "detours.h"
#define ASSEMBLY_TO_LOAD _T("rebtest.exe")
#define ASSEMBLY_TO_LOAD_A "rebtest.exe"
#define ASSEMBLY_TO_LOAD_W L"rebtest.exe"
#define IS_FLAG(Value, Flag) ((Value & Flag) == Flag)
typedef ULONG_PTR THUNK;
VOID FixIAT(VOID *pBase, IMAGE_NT_HEADERS *pNtHeaders);
VOID FixReloc(VOID *pBase, IMAGE_NT_HEADERS *pNtHeaders);
HMODULE pMainBaseAddr = NULL;
CHAR MainAsmNameA[MAX_PATH];
WCHAR MainAsmNameW[MAX_PATH];
HMODULE (WINAPI *pGetModuleHandleA)(LPCSTR lpModuleName) = GetModuleHandleA;
HMODULE (WINAPI *pGetModuleHandleW)(LPCWSTR lpModuleName) = GetModuleHandleW;
DWORD (WINAPI *pGetModuleFileNameA)(HMODULE hModule, LPCH lpFilename,
DWORD nSize) = GetModuleFileNameA;
DWORD (WINAPI *pGetModuleFileNameW)(HMODULE hModule, LPWCH lpFilename,
DWORD nSize) = GetModuleFileNameW;
HMODULE WINAPI MyGetModuleHandleA(LPCSTR lpModuleName);
HMODULE WINAPI MyGetModuleHandleW(LPCWSTR lpModuleName);
DWORD WINAPI MyGetModuleFileNameA(HMODULE hModule, LPCH lpFilename, DWORD nSize);
DWORD WINAPI MyGetModuleFileNameW(HMODULE hModule, LPWCH lpFilename, DWORD nSize);
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
//////////////////////////////////////////////////////////////////////////
// TODO: hook registry and load library
//////////////////////////////////////////////////////////////////////////
HMODULE hMainAsm = LoadLibrary(ASSEMBLY_TO_LOAD);
if (hMainAsm == NULL) return 0;
pMainBaseAddr = hMainAsm;
GetModuleFileNameA(NULL, MainAsmNameA, MAX_PATH);
CHAR *cSlash = strrchr(MainAsmNameA, '\\') + 1;
strcpy(cSlash, ASSEMBLY_TO_LOAD_A);
GetModuleFileNameW(NULL, MainAsmNameW, MAX_PATH);
WCHAR *wSlash = wcsrchr(MainAsmNameW, '\\') + 1;
wcscpy(wSlash, ASSEMBLY_TO_LOAD_W);
//
// Hook GetModuleXXXX APIs
//
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)pGetModuleFileNameA, MyGetModuleFileNameA);
DetourAttach(&(PVOID&)pGetModuleFileNameW, MyGetModuleFileNameW);
DetourAttach(&(PVOID&)pGetModuleHandleA, MyGetModuleHandleA);
DetourAttach(&(PVOID&)pGetModuleHandleW, MyGetModuleHandleW);
LONG err = DetourTransactionCommit();
if (err != NO_ERROR) return 0;
//
IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *) hMainAsm;
IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *) (pDosHeader->e_lfanew +
(ULONG_PTR) pDosHeader);
if (pNtHeaders->OptionalHeader.ImageBase != (ULONG_PTR) pDosHeader)
FixReloc(pDosHeader, pNtHeaders);
FixIAT(pDosHeader, pNtHeaders);
// retrieve entry point
VOID *pEntryPoint = (VOID *) (pNtHeaders->OptionalHeader.AddressOfEntryPoint +
(ULONG_PTR) pDosHeader);
__asm
{
jmp pEntryPoint
}
return 0;
}
HMODULE WINAPI MyGetModuleHandleW(LPCWSTR lpModuleName)
{
if (lpModuleName == NULL)
return pMainBaseAddr;
return pGetModuleHandleW(lpModuleName);
}
HMODULE WINAPI MyGetModuleHandleA(LPCSTR lpModuleName)
{
if (lpModuleName == NULL)
return pMainBaseAddr;
return pGetModuleHandleA(lpModuleName);
}
DWORD WINAPI MyGetModuleFileNameA(HMODULE hModule, LPCH lpFilename, DWORD nSize)
{
if (hModule == NULL)
{
strcpy_s(lpFilename, nSize, MainAsmNameA);
return (DWORD) strlen(lpFilename);
}
return pGetModuleFileNameA(hModule, lpFilename, nSize);
}
DWORD WINAPI MyGetModuleFileNameW(HMODULE hModule, LPWCH lpFilename, DWORD nSize)
{
if (hModule == NULL)
{
wcscpy_s(lpFilename, nSize, MainAsmNameW);
return (DWORD) wcslen(lpFilename);
}
return pGetModuleFileNameW(hModule, lpFilename, nSize);
}
// x64 compatible
VOID FixIAT(VOID *pBase, IMAGE_NT_HEADERS *pNtHeaders)
{
if (pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
return;
IMAGE_IMPORT_DESCRIPTOR *pImpDescr = (IMAGE_IMPORT_DESCRIPTOR *)
(pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress +
(ULONG_PTR) pBase);
DWORD dwOldIATProtect;
VOID *pIAT = NULL;
if (pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress != 0)
{
VOID *pIAT = (VOID *) (pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress +
(ULONG_PTR) pBase);
VirtualProtect(pIAT,
pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IAT].Size,
PAGE_EXECUTE_READWRITE,
&dwOldIATProtect);
}
while (pImpDescr->Name != 0)
{
char *DllName = (char *) (pImpDescr->Name +
(ULONG_PTR) pBase);
HMODULE hImpDll = LoadLibraryA(DllName);
if (hImpDll == NULL) continue;
THUNK *pThunk;
if (pImpDescr->OriginalFirstThunk)
pThunk = (THUNK *)(pImpDescr->OriginalFirstThunk +
(ULONG_PTR) pBase);
else
pThunk = (THUNK *)(pImpDescr->FirstThunk +
(ULONG_PTR) pBase);
THUNK *pIATThunk = (THUNK *) (pImpDescr->FirstThunk +
(ULONG_PTR) pBase);
while (*pThunk)
{
if (IS_FLAG(*pThunk, IMAGE_ORDINAL_FLAG))
{
*pIATThunk = (THUNK) GetProcAddress(hImpDll,
(LPCSTR) (*pThunk ^ IMAGE_ORDINAL_FLAG));
}
else
{
char *pImpFunc = (char *) (sizeof (WORD) + ((ULONG_PTR) *pThunk) +
((ULONG_PTR) pBase));
*pIATThunk = (THUNK) GetProcAddress(hImpDll, pImpFunc);
}
pThunk++;
pIATThunk++;
}
pImpDescr++;
}
if (pIAT)
{
VirtualProtect(pIAT,
pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IAT].Size,
dwOldIATProtect,
&dwOldIATProtect);
}
}
// x86 recycled code from an older article
VOID FixReloc(VOID *pBase, IMAGE_NT_HEADERS *pNtHeaders)
{
//
// Set first section to writeable in order to fix
// the relocations in the code
//
IMAGE_SECTION_HEADER *pCodeSect = (IMAGE_SECTION_HEADER *)
IMAGE_FIRST_SECTION(pNtHeaders);
VOID *pCode = (VOID *) (pCodeSect->VirtualAddress + (ULONG_PTR) pBase);
DWORD dwOldCodeProtect;
VirtualProtect(pCode,
pCodeSect->Misc.VirtualSize,
PAGE_READWRITE,
&dwOldCodeProtect);
//
// Relocate
//
DWORD Delta = (DWORD)(((ULONG_PTR) pBase) -
pNtHeaders->OptionalHeader.ImageBase);
DWORD RelocRva;
if (!(RelocRva = pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress))
return;
IMAGE_BASE_RELOCATION *ImgBaseReloc =
(IMAGE_BASE_RELOCATION *) (RelocRva + (ULONG_PTR) pBase);
WORD *wData;
do
{
if (!ImgBaseReloc->SizeOfBlock)
break;
UINT nItems = (ImgBaseReloc->SizeOfBlock -
IMAGE_SIZEOF_BASE_RELOCATION) / sizeof (WORD);
wData = (WORD *)(IMAGE_SIZEOF_BASE_RELOCATION +
(ULONG_PTR) ImgBaseReloc);
for (UINT i = 0; i < nItems; i++)
{
DWORD Offset = (*wData & 0xFFF) + ImgBaseReloc->VirtualAddress;
DWORD Type = *wData >> 12;
if (Type != IMAGE_REL_BASED_ABSOLUTE)
{
DWORD *pBlock = (DWORD *)(Offset + (ULONG_PTR) pBase);
*pBlock += Delta;
}
wData++;
}
ImgBaseReloc = (PIMAGE_BASE_RELOCATION) wData;
} while (*(DWORD *) wData);
//
// Restore memory settings
//
VirtualProtect(pCode,
pCodeSect->Misc.VirtualSize,
dwOldCodeProtect,
&dwOldCodeProtect);
}
完整的源代码和二进制文件可以从这里下载
- 下载原生加载器
此代码仅加载 .NET 程序集。为了实现 .NET 框架的部署,还需要挂钩注册表 API 和文件系统 API,例如 LoadLibrary
。在下一段中,我将讨论注册表虚拟化,这将使我们向前迈进了一步。
注册表虚拟化
如果我没有已经拥有的将要介绍的资料,我不会写这一段。我的一篇未完成的文章(由于时间不足)与虚拟化有关。很多年前我写了一个 注册表虚拟化工具。
此工具的主窗体(VirtualReg Manager)提供了创建虚拟注册表的可视化界面。如我们稍后将看到的,也可以通过命令行来实现。一个人可以决定是否要将一个项及其子项一起虚拟化。
虚拟注册表是一个 XML 数据库。此 XML 文件的格式如下
<?xml version="1.0"
encoding="utf-8"?>
<VIRTUALREG>
<KEY Name="HKEY_LOCAL_MACHINE">
<SUBKEYS>
<KEY Name="SOFTWARE">
<SUBKEYS>
<KEY Name="Microsoft">
<SUBKEYS>
<KEY Name="Fusion">
<VALUES>
<VALUE Name="ZapQuotaInKB" Type="REG_DWORD">F4240</VALUE>
<VALUE Name="DisableCacheViewer"
Type="REG_BINARY">AQAQAA==</VALUE>
<VALUE Name="ForceLog" Type="REG_DWORD">1</VALUE>
<VALUE Name="LogPath" Type="REG_SZ">YwA6AFwAAAA=</VALUE>
</VALUES>
<SUBKEYS>
<KEY Name="GACChangeNotification">
<SUBKEYS>
<KEY Name="Default">
<VALUES>
<VALUE
Name="Accessibility,1.0.5000.0,,b03f5f7f11d50a3a"
Type="REG_BINARY">yEWDMkwyxgE=</VALUE>
<VALUE Name="cscompmgd,7.0.5000.0,,b03f5f7f11d50a3a"
Type="REG_BINARY">ROfXLkwyxgE=</VALUE>
<VALUE
Name="CustomMarshalers,1.0.5000.0,,b03f5f7f11d50a3a"
Type="REG_BINARY">yEWDMkwyxgE=</VALUE>
数字以十六进制格式存储,而所有其他数据都以 base64 编码。虚拟注册表文件可以使用 VirtualReg Editor (vregedit) 进行编辑,它的界面与 regedit 相同,非常用户友好。
从 GUI 创建虚拟注册表对于手动任务来说还可以,但工具可以使用程序的命令行来生成虚拟注册表。要做到这一点,必须将一个“.tovreg”文件作为命令行参数传递给程序。tovreg 文件具有此语法
[OPTIONS]
output="c:\....\fusion.vreg"
[HKEY_CLASSES_ROOT\CLSID]
[HKEY_LOCAL_MACHINE\Software\Microsoft\Fusion]
subkeys=true
正如所见,它是一个简单的 ini 文件。如果“subkeys”参数丢失,则不虚拟化子项。
由于这是未完成文章的一部分,我还没有编写监视器来检索要虚拟化的项。但是,编写一个监视器相当容易,或者,如果您非常懒惰,使用 Russinovich 的 Process Monitor 生成的日志也是一个选择。捕获的项应不带子项进行虚拟化,因为这有时可能会导致虚拟注册表过大且包含不必要的项。
欢迎将此工具包含在您的免费软件中。
问题与结论
由于原生映像的代码生成是平台特定的,它可能还包含在其他 CPU 上无法工作的优化。例如,可能使用了 SSE 指令的一个特定版本,而该版本并非在所有体系结构上都可用。可以通过让 ngen 相信它运行在较旧(或不同)的 CPU 上来“解决”这个问题,但这只是一个混乱。
我不提倡在技术文章中加入个人观点,但有必要对此说些什么,因为有人可能会问我为什么不自己编写一个原生框架部署服务。利用本文提供的信息,提供一个商业产品只需要一个月的时间。我不这样做是因为我认为它是不专业的,而且在技术上是一个混乱。它也可能总是有效,但没有人会疯到将每个 .NET 程序集都打包成 .NET 框架的一个子集。为一个简单的程序集打包 40MB 或更多的数据并不是真正的解决方案。事实上,它根本不是解决方案。
我曾想为本文编写一个完整的此类保护措施演示(当然,不包括合并部分),这只需要几天时间,但它有一些缺点。由于我对开发围绕此概念的商业解决方案不感兴趣,其他人可能会简单地重新利用代码。即使现在也没有太多工作要做,但至少人们需要花一些时间来处理它,然后才能获得一些可以赚钱的东西。然而,我非常支持逆向工程师编写一个演示版,仅仅是为了好玩并免费赠送。是的,它应该是免费的。它在技术上并不复杂,而且根本不应该商业化。
原生注入
在这一段中,我将展示如何通过取代 JIT 来完成原生映像在加载时所做的工作。原生映像中的代码需要修复:许多引用需要在运行时解决,例如外部调用。我没有展示一种实际原生编译 .NET 程序集的方法,因为取代 JIT 的位置不仅复杂,而且在 .NET Framework 的未来版本中不太可能有效。实际上,我写的内容在 .NET Framework 2 和 3 上是有效的,但似乎新的 Framework 3.5 SP1 改变了很多东西,而且我已经注意到我在 Vista x64 上安装的版本不起作用。这并不重要,我也不想深入解决问题,因为我在这里所做的只是一个技巧,以更好地理解 JIT 的工作原理,这将在接下来的段落中有所帮助。它还将证明我关于 .NET 原生编译的最终结论。
这一段中使用的测试程序集是 rebtest.exe:一个我已经在测试 Rebel.NET 时使用过的程序集。该应用程序非常简单,只是一个带有文本框和按钮的窗体。当用户单击按钮时,它会检查文本框中输入的密码是否正确。如果不正确,它会显示消息框:“密码错误!”。这是按钮单击事件的 MSIL 代码
.method private hidebysig instance void button1_Click(object sender,
class [mscorlib]System.EventArgs e) cil managed
{
// Code size 43 (0x2b)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.0
IL_0002: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox
rebtest.Form1::textBox1
IL_0007: callvirt instance string [
System.Windows.Forms]System.Windows.Forms.Control::get_Text()
IL_000c: call instance bool rebtest.Form1::CheckPassword(string)
IL_0011: brfalse.s IL_001f
IL_0013: ldstr "Right password!"
IL_0018: call valuetype System.Windows.Forms.DialogResult
System.Windows.Forms.MessageBox::Show(string)
IL_001d: pop
IL_001e: ret
IL_001f: ldstr "Wrong password!"
IL_0024: call valuetype System.Windows.Forms.DialogResult
System.Windows.Forms.MessageBox::Show(string)
IL_0029: pop
IL_002a: ret
} // end of method Form1::button1_Click
让我们来看看在两台不同的计算机上从这段 MSIL 代码生成的原生代码的差异
代码 A | 代码 B |
00000000 push esi 00000001 mov esi, ecx 00000003 mov ecx, [esi+0x140] 00000009 mov eax, [ecx] 0000000B call [eax+0x164] 00000011 mov edx, [0x238b9bc] 00000017 mov ecx, eax 00000019 call 0x7426edd0 0000001E and eax, 0xff 00000023 jz 0x2c 00000025 mov eax, 0x1 0000002A jmp 0x2e 0000002C xor eax, eax 0000002E test eax, eax 00000030 jz 0x42 00000032 mov ecx, [0x238b9c0] 00000038 call [0x5102544] 0000003E pop esi 0000003F ret 0x4 00000042 mov ecx, [0x238b9c4] 00000048 call [0x5102544] 0000004E pop esi 0000004F ret 0x4 |
00000000 push esi 00000001 mov esi, ecx 00000003 mov ecx, [esi+0x140] 00000009 mov eax, [ecx] 0000000B call [eax+0x164] 00000011 mov edx, [0x385b9bc] 00000017 mov ecx, eax 00000019 call 0x742ff5b0 0000001E and eax, 0xff 00000023 jz 0x2c 00000025 mov eax, 0x1 0000002A jmp 0x2e 0000002C xor eax, eax 0000002E test eax, eax 00000030 jz 0x42 00000032 mov ecx, [0x385b9c0] 00000038 call [0x5053524] 0000003E pop esi 0000003F ret 0x4 00000042 mov ecx, [0x385b9c4] 00000048 call [0x5053524] 0000004E pop esi 0000004F ret 0x4 |
即使在这个小方法中,也有许多事情是在运行时解决的。在这种特定情况下,我们有一个 ldfld,一个 callvirt,一个 ldstr 和一个 call。应该注意的是,此汇编代码使用 fastcall,将第一个参数存储在 ecx 中,第二个参数存储在 edx 中。
为了理解如何解决这些引用,有必要理解 JIT 的内部工作原理。在第一篇文章中,我介绍了 compileMethod
函数,但我只关注了它的前两个参数:ICorJitInfo
和 CORINFO_METHOD_INFO
。我还没有讨论的是它的最后两个参数:nativeEntry
和 nativeSizeOfCode
。这是两个用于检索原生代码地址和大小的指针。当然,一个人可以挂钩 compileMethod
以在调用原始 compileMethod
函数(这并没有太大用处)后检索原生代码,或者他可以实际使用这两个参数来注入自己的原生代码。这正是我要做的。但我不是注入任何代码。不,我将通过解决内部引用来注入原生的 .NET 代码。
让我们从 compileMethod
函数开始
/*****************************************************************************
* The main JIT function
*/
//Note: this assumes that the code produced by fjit is fully relocatable, i.e.
//requires no fixups after it is generated when it is moved. In particular it
//places restrictions on the code sequences used for static and non virtual
//calls and for helper calls among other things,i.e. that pc relative
//instructions are not used for references to things outside of the
//jitted method, and that pc relative instructions are used for all
//references to things within the jitted method. To accomplish this,
//the fjitted code is always reached via a level of indirection.
CorJitResult __stdcall FJitCompiler::compileMethod (
ICorJitInfo* compHnd, /* IN */
CORINFO_METHOD_INFO* info, /* IN */
unsigned flags, /* IN */
BYTE ** entryAddress, /* OUT */
ULONG * nativeSizeOfCode /* OUT */
)
{
#if defined(_DEBUG) || defined(LOGGING)
// make a copy of the ICorJitInfo vtable so that I can log mesages later
// this was made non-static due to a VC7 bug
static void* ijitInfoVtable;
ijitInfoVtable = *((void**) compHnd);
logCallback = (ICorJitInfo*) &ijitInfoVtable;
#endif
if(!FJitCompiler::GetJitHelpers(compHnd))
return CORJIT_INTERNALERROR;
// NOTE: should the properties of the FJIT change such that it
// would have to pay attention to specific IL sequence points or
// local variable liveness ranges for debugging purposes, we would
// query the Runtime and Debugger for such information here,
FJit* fjitData=NULL;
CorJitResult ret = CORJIT_INTERNALERROR;
unsigned char* savedCodeBuffer = NULL;
unsigned savedCodeBufferCommittedSize = 0;
unsigned int codeSize = 0;
unsigned actualCodeSize;
#if defined(_DEBUG) || defined(LOGGING)
const char *szDebugMethodName = NULL;
const char *szDebugClassName = NULL;
szDebugMethodName = compHnd->getMethodName(info->ftn, &szDebugClassName );
#endif
#ifdef _DEBUG
static ConfigMethodSet fJitBreak;
fJitBreak.ensureInit(L"JitBreak");
if (fJitBreak.contains(szDebugMethodName, szDebugClassName,
PCCOR_SIGNATURE(info->args.sig)))
_ASSERTE(!"JITBreak");
// Check if need to print the trace
static ConfigDWORD fJitTrace;
if ( fJitTrace.val(L"JitTrace") )
printf( "Method %s Class %s \n",szDebugMethodName, szDebugClassName );
#endif
PAL_TRY // for PAL_FINALLY
PAL_TRY // for PAL_EXCEPT
{
fjitData = FJit::GetContext(compHnd, info, flags);
_ASSERTE(fjitData); // if GetContext fails for any reason it throws an exception
_ASSERTE(fjitData->opStack_len == 0); // stack must be balanced at beginning
// of method
codeSize = ROUND_TO_PAGE(info->ILCodeSize * CODE_EXPANSION_RATIO);
#ifdef LOGGING
static ConfigMethodSet fJitCodeLog;
fJitCodeLog.ensureInit(L"JitCodeLog");
fjitData->codeLog = fJitCodeLog.contains(szDebugMethodName,
szDebugClassName, PCCOR_SIGNATURE(info->args.sig));
if (fjitData->codeLog)
codeSize = ROUND_TO_PAGE(info->ILCodeSize * 64);
#endif
BOOL jitRetry = FALSE; // this is set to false unless we get an exception
// because of underestimation of code buffer size
do { // the following loop is expected to execute only once,
// except when we underestimate the size of the code buffer,
// in which case, we try again with a larger codeSize
if (codeSize < MIN_CODE_BUFFER_RESERVED_SIZE)
{
if (codeSize > fjitData->codeBufferCommittedSize)
{
if (fjitData->codeBufferCommittedSize > 0)
{
unsigned AdditionalMemorySize =
codeSize - fjitData->codeBufferCommittedSize;
if (AdditionalMemorySize > PAGE_SIZE) {
unsigned char* additionalMemory = (unsigned char*)
VirtualAlloc(fjitData->codeBuffer+fjitData->codeBufferCommittedSize+
PAGE_SIZE,
AdditionalMemorySize-PAGE_SIZE,
MEM_COMMIT,
PAGE_READWRITE);
if (additionalMemory == NULL)
{
ret = CORJIT_OUTOFMEM;
goto Done;
}
_ASSERTE(additionalMemory == fjitData->codeBuffer+
fjitData->codeBufferCommittedSize+PAGE_SIZE);
}
// recommit the guard page
VirtualAlloc(fjitData->codeBuffer +
fjitData->codeBufferCommittedSize,
PAGE_SIZE,
MEM_COMMIT,
PAGE_READWRITE);
fjitData->codeBufferCommittedSize = codeSize;
}
else { /* first time codeBuffer being initialized */
savedCodeBuffer = fjitData->codeBuffer;
fjitData->codeBuffer = (unsigned char*)VirtualAlloc(
fjitData->codeBuffer,
codeSize,
MEM_COMMIT,
PAGE_READWRITE);
if (fjitData->codeBuffer == NULL)
{
fjitData->codeBuffer = savedCodeBuffer;
ret = CORJIT_OUTOFMEM;
goto Done;
}
fjitData->codeBufferCommittedSize = codeSize;
}
_ASSERTE(codeSize == fjitData->codeBufferCommittedSize);
unsigned char* guardPage = (unsigned char*)VirtualAlloc(
fjitData->codeBuffer +
codeSize,
PAGE_SIZE,
MEM_COMMIT,
PAGE_READONLY);
if (guardPage == NULL)
{
ret = CORJIT_OUTOFMEM;
goto Done;
}
}
}
else
{ // handle larger than MIN_CODE_BUFFER_RESERVED_SIZE methods
savedCodeBuffer = fjitData->codeBuffer;
savedCodeBufferCommittedSize = fjitData->codeBufferCommittedSize;
fjitData->codeBuffer = (unsigned char*)VirtualAlloc(NULL,
codeSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
if (fjitData->codeBuffer == NULL)
{
// Make sure that the saved buffer is freed in the destructor
fjitData->codeBuffer = savedCodeBuffer;
ret = CORJIT_OUTOFMEM;
goto Done;
}
fjitData->codeBufferCommittedSize = codeSize;
}
unsigned char* entryPoint;
actualCodeSize = codeSize;
PAL_TRY
{
FJitResult FJitRet;
jitRetry = false;
FJitRet = fjitData->jitCompile(&entryPoint,&actualCodeSize);
if (FJitRet == FJIT_VERIFICATIONFAILED)
{
if (!(flags & CORJIT_FLG_IMPORT_ONLY))
// If we get a verification failed error, just map it to OK as
// it's already been dealt with.
ret = CORJIT_OK;
else
// if we are in "Import only" mode, we are actually verifying
// generic code. It's important that we don't return CORJIT_OK,
// because we want to skip the code generation phase.
ret = CORJIT_BADCODE;
}
else if (FJitRet == FJIT_JITAGAIN)
{
jitRetry = true;
ret = CORJIT_INTERNALERROR;
}
else // Otherwise cast it to a CorJitResult
ret = (CorJitResult)FJitRet;
if ( ret == CORJIT_OK )
ret = fjitData->fixupTable->resolve(fjitData->mapping,
fjitData->codeBuffer, jitRetry );
if ( jitRetry )
{
fjitData->ReleaseContext();
fjitData = FJit::GetContext(compHnd, info, flags);
fjitData->mapInfo.savedIP = true;
}
}
该函数实际上要大得多,但我只粘贴了对我们来说有趣的部分。在我粘贴的最后几行代码中,您可以看到 compileMethod
调用了 jitCompile
函数。这是 JIT 的主函数。它是一个非常大的函数,因为它包含处理每个 MSIL 操作码的开关。我将在此处粘贴该函数的一个“小”部分,以让您了解其规模。
/************************************************************************************/
/* jit the method. if successful, return number of bytes jitted, else return 0 */
FJitResult FJit::jitCompile(
BYTE ** ReturnAddress,
unsigned * ReturncodeSize
)
{
/*****************************************************************************
* The following macro reads a value from the IL stream. It checks that the size
* of the object doesn't exceed the length of the stream. It also checks that
* the data has not been previously read and marks it as read, unless the "reread"
* variable is set to true.
*****************************************************************************/
#define GET(val, type, reread) \
{ \
unsigned int size_operand; \
VALIDITY_CHECK( inPtr + sizeof(type) <= inBuffEnd ); \
for ( size_operand = 0; size_operand < sizeof(type) && !reread;
size_operand++ ) \
VALIDITY_CHECK(!state[inPtr- inBuff+size_operand].isJitted) \
switch(sizeof(type)) { \
case 1: val = (type)*inPtr; break; \
case 2: val = (type)GET_UNALIGNED_VAL16(inPtr); break; \
case 4: val = (type)GET_UNALIGNED_VAL32(inPtr); break; \
case 8: val = (type)GET_UNALIGNED_VAL64(inPtr); break; \
default: val = (type)0; _ASSERTE(!"Invalid size"); break; \
} \
inPtr += sizeof(type); \
for ( size_operand = 1; size_operand <= sizeof(type) &&
!reread; size_operand++ ) \
state[inPtr-inBuff-size_operand].isJitted = true; \
}
#define LEAVE_CRIT \
if (methodInfo->args.hasThis()) { \
emit_WIN32(emit_LDVAR_I4(offsetOfRegister(0))) \
emit_WIN64(emit_LDVAR_I8(offsetOfRegister(0))); \
emit_EXIT_CRIT(); \
} \
else { \
void* syncHandle; \
syncHandle = jitInfo->getMethodSync(methodInfo->ftn); \
emit_EXIT_CRIT_STATIC(syncHandle); \
}
#define ENTER_CRIT \
if (methodInfo->args.hasThis()) { \
emit_WIN32(emit_LDVAR_I4(offsetOfRegister(0))) \
emit_WIN64(emit_LDVAR_I8(offsetOfRegister(0))); \
emit_ENTER_CRIT(); \
} \
else { \
void* syncHandle; \
syncHandle = jitInfo->getMethodSync(methodInfo->ftn); \
emit_ENTER_CRIT_STATIC(syncHandle); \
}
#define CURRENT_INDEX (inPtr - inBuff)
TailCallForbidden = !!((methodInfo->args.callConv & CORINFO_CALLCONV_MASK) ==
CORINFO_CALLCONV_VARARG);
// if set, no tailcalls allowed. Initialized to FALSE.
// When a security test changes it to TRUE, it remains
// TRUE for the duration of the jitting of the function
outBuff = codeBuffer;
CORINFO_METHOD_HANDLE methodHandle= methodInfo->ftn;
unsigned int len = methodInfo->ILCodeSize; // IL size
inBuff = methodInfo->ILCode; // IL bytes
inBuffEnd = &inBuff[len]; // end of IL
entryAddress = ReturnAddress;
codeSize = ReturncodeSize;
// Information about arguments and locals
offsetVarArgToken = sizeof(prolog_frame);
// Local variables declared for convenience and flags
unsigned offset;
unsigned address;
signed int i4;
int merge_state;
FJitResult JitResult = FJIT_OK;
unsigned char opcode_val;
InstStart = 0;
DelegateStart = 0;
DelegateMethodRef = 0;
UnalignedOffset = (unsigned)-1;
JitAgain:
MadeTailCall = false; // if a tailcall has been made and subsequently
// TailCallForbidden is set to TRUE,
// we will rejit the code, disallowing tailcalls.
inRegTOS = false; // flag indicating if the top of the stack is in a
// register
controlContinue = true; // does control we fall thru to next il instr
inPtr = inBuff; // Set the current IL offset to the start of the IL buffer
outPtr = outBuff; // Set the current output buffer position to the start
// of the buffer
codeGenState = FJIT_OK; // Reset the global error flag
JitResult = FJIT_OK; // Reset the result flag for simple operations that
// don't set it
UnalignedAccess = false; // Reset the unaligned access flag
#ifdef _DEBUG
didLocalAlloc = false;
#endif
// Can not jit a native method
VALIDITY_CHECK(!(methodAttributes & (CORINFO_FLG_NATIVE)));
// Zero sized methods are not allowed
VALIDITY_CHECK(methodInfo->ILCodeSize > 0);
// Can not jit methods with shared bodies
VALIDITY_CHECK(!(methodAttributes & CORINFO_FLG_SHAREDINST) );
*(entryAddress) = outPtr;
#if defined(_DEBUG)
static ConfigMethodSet fJitHalt;
fJitHalt.ensureInit(L"JitHalt");
if (fJitHalt.contains(szDebugMethodName, szDebugClassName,
PCCOR_SIGNATURE(methodInfo->args.sig))) {
emit_break();
}
#endif
//Skip verification if possible
JitVerify = !(flags & CORJIT_FLG_SKIP_VERIFICATION);
IsVerifiableCode = true; // assume the code is verifiable unless proven otherwise
// load any constraints for verification, detecting and rejecting cycles
if (JitVerify)
{
BOOL hasCircularClassConstraints = FALSE;
BOOL hasCircularMethodConstraints = FALSE;
jitInfo->initConstraintsForVerification(methodHandle,&hasCircularClassConstraints,
&hasCircularMethodConstraints);
VERIFICATION_CHECK(!hasCircularClassConstraints);
VERIFICATION_CHECK(!hasCircularMethodConstraints);
}
#if defined(_SPARC_) || defined(_PPC_)
// Check if the offset of the vararg token has been computed correctly
offsetVarArgToken += ( methodInfo->args.hasThis() ? sizeof( void * ) : 0 ) +
( methodInfo->args.hasRetBuffArg() &&
EnregReturnBuffer ? sizeof( void * ) : 0 );
#endif
// it may be worth optimizing the following to only initialize locals so as
// to cover all refs.
unsigned int localWords = (localsFrameSize+sizeof(void*)-1)/ sizeof(void*);
emit_prolog(localWords);
if (flags & CORJIT_FLG_PROF_ENTERLEAVE)
{
BOOL bHookFunction;
void *eeHandle;
void *profilerHandle;
BOOL bIndirected;
jitInfo->GetProfilingHandle(methodHandle,
&bHookFunction,
&eeHandle,
&profilerHandle,
&bIndirected);
if (bHookFunction)
{
_ASSERTE(!bIndirected); // FJIT does not handle NGEN case
_ASSERTE(!inRegTOS);
ULONG func = (ULONG) jitInfo->getHelperFtn(CORINFO_HELP_PROF_FCN_ENTER);
_ASSERTE(func != NULL);
emit_callhelper_prof4(func,
(CorJitFlag) CORINFO_HELP_PROF_FCN_ENTER,
eeHandle,
profilerHandle,
NULL, // FRAME_INFO (see definition of
// FunctionEnter2 in corprof.idl)
NULL); // ARG_INFO (see definition of FunctionEnter2
// in corprof.idl)
}
}
// Do we need to insert a "JustMyCode" callback?
if (flags & CORJIT_FLG_DEBUG_CODE)
{
CORINFO_JUST_MY_CODE_HANDLE *pDbgHandle;
CORINFO_JUST_MY_CODE_HANDLE dbgHandle = jitInfo->getJustMyCodeHandle(
methodHandle, &pDbgHandle);
_ASSERTE(!dbgHandle || !pDbgHandle);
if (dbgHandle || pDbgHandle)
emit_justmycode_callback( dbgHandle, pDbgHandle );
}
#ifdef LOGGING
if (codeLog) {
emit_log_entry(szDebugClassName, szDebugMethodName);
}
#endif
// Get sequence points
unsigned nextSequencePoint = 0;
if (flags & CORJIT_FLG_DEBUG_INFO) {
getSequencePoints(jitInfo,methodHandle,&cSequencePoints,
&sequencePointOffsets,&offsetsImplicit);
}
else {
cSequencePoints = 0;
offsetsImplicit = ICorDebugInfo::NO_BOUNDARIES;
}
mapInfo.prologSize = outPtr-outBuff;
// note: entering of the critical section is not part of the prolog
mapping->add(CURRENT_INDEX,(unsigned)(outPtr - outBuff));
if (methodAttributes & CORINFO_FLG_SYNCH) {
ENTER_CRIT;
}
// Verify the exception handlers' table
int ver_exceptions = verifyHandlers();
VALIDITY_CHECK( ver_exceptions != FAILED_VALIDATION );
VERIFICATION_CHECK( ver_exceptions != FAILED_VERIFICATION );
// Initialize the state map with the exception handling information
initializeExceptionHandling();
bool First = true;
popSplitStack = false; // Start jitting at the next offset on the split stack
UncondBranch = false; // Executing an unconditional branch
LeavingTryBlock = false; // Executing a "leave" from a try block
LeavingCatchBlock = false; // Executing a "leave" from a catch block
FinishedJitting = false; // Finished jitting the IL stream
makeClauseEmpty(¤tClause);
_ASSERTE(!inRegTOS);
while (!FinishedJitting)
{
//INDEBUG( printf("IL offset: %x PopStack: %d StackEmpty: %d\n", CURRENT_INDEX,
// popSplitStack, SplitOffsets.isEmpty() );)
START_LOOP:
// If we jitted the last statement or an uncondtional branch with jitted target
// we need to restart at the next split offset
if ( inPtr >= inBuffEnd || popSplitStack )
{
// Remove the IL offsets that's already been jitted
while ( !SplitOffsets.isEmpty() && state[SplitOffsets.top()].isJitted )
(void)SplitOffsets.popOffset();
//INDEBUG(SplitOffsets.dumpStack();)
// We reached the end of the IL opcode stream, but not all code has been jitted
// Pop the offset from the split offsets stack
if (!SplitOffsets.isEmpty())
{
inPtr = (unsigned char *)&inBuff[SplitOffsets.popOffset()];
//INDEBUG(printf("Starting jitting at %d \n", inPtr-inBuff );)
// Treat a split as a forward jump
controlContinue = false;
// Reset flag
popSplitStack = false;
}
else
{
// Check for a fall through at the end of the function
VALIDITY_CHECK( popSplitStack || inBuff[InstStart] == CEE_THROW );
goto END_JIT_LOOP;
}
}
// Check if max stack value has been exceded
VERIFICATION_CHECK( methodInfo->maxStack >= opStack_len );
//INDEBUG(if (JitVerify) printf("IL offset is %x\n", CURRENT_INDEX );)
// Guard against a fall through into/from a catch/finally/filter
VALIDITY_CHECK(!(state[CURRENT_INDEX].isHandler) &&
!(state[CURRENT_INDEX].isFilter) &&
!(state[CURRENT_INDEX].isEndBlock) || !controlContinue ||
UncondBranch );
UncondBranch = false; // This flag is only used to check for fall through
if (controlContinue) {
if (state[CURRENT_INDEX].isJmpTarget && inRegTOS
!= state[CURRENT_INDEX].isTOSInReg) {
if (inRegTOS) {
deregisterTOS;
}
else {
enregisterTOS;
}
}
}
else { // controlContinue == false
unsigned int label = ver_stacks.findLabel(CURRENT_INDEX);
if (label == LABEL_NOT_FOUND) {
CHECK_POP_STACK(opStack_len);
inRegTOS = false;
}
else {
opStack_len = ver_stacks.setStackFromLabel(label, opStack, opStack_size);
inRegTOS = state[CURRENT_INDEX].isTOSInReg;
}
controlContinue = true;
}
//Check if this IL offset has already been jitted. Note, that to see if
//an offset has been jitted we need to check that it is not in skipped code
//intervals and that an offset equal to or above it has been jitted
if ( state[inPtr-inBuff].isJitted )
{
//INDEBUG( printf("Detected jitted code: IL offset is %x\n",CURRENT_INDEX );)
// The skipped code interval must just have ended
// If verification is enabled we need to compare the current
// state of the stack with the saved one
merge_state = verifyStacks(CURRENT_INDEX, 0);
VERIFICATION_CHECK( merge_state );
if ( JitVerify && merge_state == MERGE_STATE_REJIT )
{ resetState(false); goto JitAgain; }
// Emit a jump to the jitted code
ilrel = CURRENT_INDEX;
if (state[inPtr-inBuff].isTOSInReg)
{ enregisterTOS; }
else
{ deregisterTOS; }
address = mapping->pcFromIL(inPtr-inBuff);
VALIDITY_CHECK(address > 0 );
emit_jmp_abs_address(CEE_CondAlways, address + (unsigned)outBuff, true);
// INDEBUG(printf("Emitted a jump to %d\n", outPtr+address-outBuff);)
// Remove the IL offsets that's already been jitted
while ( !SplitOffsets.isEmpty() && state[SplitOffsets.top()].isJitted )
(void)SplitOffsets.popOffset();
// Pop the offset from the split offsets stack
if (!SplitOffsets.isEmpty())
{
inPtr = (unsigned char *)&inBuff[SplitOffsets.popOffset()];
//INDEBUG(printf("Starting jitting at %d \n", inPtr-inBuff );)
// Treat a split as a forward jump
controlContinue = false;
//INDEBUG(SplitOffsets.dumpStack();)
goto START_LOOP;
}
else
goto END_JIT_LOOP;
}
// If the current offset is a beginning of a try block, it is necessary to
// push the addresses of associated handlers onto the split offsets stack
// in the correct order
if (state[CURRENT_INDEX].isTry)
{
//INDEBUG(printf("Pushed Handlers at %x\n", CURRENT_INDEX );)
// The stack has to be empty on an entry to a try block
VALIDITY_CHECK(isOpStackEmpty());
// Push the starting offset of the try block onto the split offsets stack
SplitOffsets.pushOffset(CURRENT_INDEX);
// Push the starting addresses of all the handlers onto the split
// offsets stack
pushHandlerOffsets(CURRENT_INDEX);
// Emit a jump to the start of the try block
fixupTable->insert((void**) outPtr);
emit_jmp_abs_address(CEE_CondAlways, CURRENT_INDEX, false);
//INDEBUG(SplitOffsets.dumpStack();)
state[CURRENT_INDEX].isTry = 0; // Reset the flag once the handlers have
// been pushed onto the stack
// Start jitting the first handler
popSplitStack = true;
controlContinue = false;
First = false;
continue;
}
// This IL opcode will be jitted
if (!First)
mapping->add(CURRENT_INDEX,(unsigned)(outPtr - outBuff));
First = false;
if (state[CURRENT_INDEX].isHandler) {
if ( (offsetsImplicit & ICorDebugInfo::CALL_SITE_BOUNDARIES) != 0 )
emit_sequence_point_marker();
unsigned int nestingLevel = Compute_EH_NestingLevel(inPtr-inBuff);
emit_storeTOS_in_JitGenerated_local(nestingLevel,
state[CURRENT_INDEX].isFilter);
}
state[CURRENT_INDEX].isTOSInReg = inRegTOS;
// Check if we are currently at a sequence point
emitSequencePointPre( CURRENT_INDEX, nextSequencePoint );
// If verification is enabled we need to store the current state of the stack
merge_state = verifyStacks(CURRENT_INDEX, 1);
VERIFICATION_CHECK( merge_state );
if ( JitVerify && merge_state == MERGE_STATE_REJIT )
{ resetState(false); goto JitAgain; }
InstStart = CURRENT_INDEX;
if ( InstStart == UnalignedOffset ) UnalignedAccess = true;
#ifdef LOGGING
ilrel = inPtr - inBuff;
#endif
GET(opcode_val, unsigned char, false );
OPCODE opcode = OPCODE(opcode_val);
DECODE_OPCODE:
#ifdef LOGGING
if (codeLog && opcode != CEE_PREFIXREF && (opcode < CEE_PREFIX7 ||
opcode > CEE_PREFIX1)) {
bool oldstate = inRegTOS;
emit_log_opcode(ilrel, opcode, oldstate);
inRegTOS = oldstate;
}
#endif
switch (opcode)
{
case CEE_PREFIX1:
GET(opcode_val, unsigned char, false);
opcode = OPCODE(opcode_val + 256);
goto DECODE_OPCODE;
case CEE_LDARG_0:
case CEE_LDARG_1:
case CEE_LDARG_2:
case CEE_LDARG_3:
offset = (opcode - CEE_LDARG_0);
// Make sure that the offset is legal (with respect to the IL encoding)
VERIFICATION_CHECK(offset < 4);
JitResult = compileDO_LDARG( opcode, offset);
break;
仅在最后几行代码中,我们就遇到了我所说的开关。开关位于一个循环(自然)中,该循环会一直进行,直到最后一个操作码被 jit 编译。正如人们所注意到的,开关并不直接出现在 jit 编译循环的开头。这是因为在处理每个指令之前,JIT 会执行许多检查。例如,它检查最大堆栈大小是否已超出,或者当前偏移量是否是 try 块的开始。但是,我们不必关心所有这些事情,因为我们不必执行有效性检查或实现异常处理程序。
注意:GET 宏应该简要讨论以便更好地理解。此宏从当前 MSIL 操作码流指针读取一个值类型并将其放入一个变量(第一个参数),然后它会递增流指针。
我要做的是注入显示“密码正确!”的 .NET 消息框。因此,我们必须分析 JIT 如何处理 ldstr 和 call 操作码。这是一个很好的进展方式,因为 ldstr 操作码非常简单,并且能给读者时间来适应 JIT 的逻辑。所以,让我们看看开关中的 ldstr 情况
case CEE_LDSTR:
JitResult = compileCEE_LDSTR();
break;
这是处理操作码的常用语法:调用 compileCEE_OpcodeName。让我们看看这个函数
FJitResult FJit::compileCEE_LDSTR()
{
unsigned int token;
InfoAccessType iat;
CORINFO_MODULE_HANDLE tokenScope = methodInfo->scope;
GET(token, unsigned int, false); VERIFICATION_CHECK(jitInfo->isValidToken(
tokenScope, token));
void* literalHnd = NULL;
iat = jitInfo->constructStringLiteral(tokenScope,token, &literalHnd);
// the code only ever supported the equivalent of IAT_PVALUE, this is now asserted
VALIDITY_CHECK(iat == IAT_PVALUE);
// Check if the string was constructed successfully
VALIDITY_CHECK(literalHnd != 0);
emit_WIN32(emit_LDC_I4(literalHnd)) emit_WIN64(emit_LDC_I8(literalHnd)) ;
emit_LDIND_PTR(false);
// Get the type handle for strings
CORINFO_CLASS_HANDLE s_StringClass = jitInfo->getBuiltinClass(CLASSID_STRING);
VALIDITY_CHECK( s_StringClass != NULL );
pushOp(OpType(typeRef, s_StringClass ));
return FJIT_OK;
}
查看此函数时,有必要定义我们为了获得字符串引用而需要的内容。我们已经熟悉 GET 宏及其用法。我们已经有了字符串令牌和作用域。我们不需要进行任何验证。所以,一切都归结为 constructStringLiteral
函数,该函数在 dynamicmethod.cpp 中声明。
InfoAccessType CEEDynamicCodeInfo::constructStringLiteral(
CORINFO_MODULE_HANDLE moduleHnd,
mdToken metaTok,
void **ppInfo)
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_COOPERATIVE;
PRECONDITION(IsDynamicScope(moduleHnd));
}
CONTRACTL_END;
_ASSERTE(ppInfo != NULL);
*ppInfo = NULL;
DynamicResolver* pResolver = GetDynamicResolver(moduleHnd);
OBJECTHANDLE string = NULL;
STRINGREF strRef = ObjectToSTRINGREF(pResolver->GetStringLiteral(metaTok));
GCPROTECT_BEGIN(strRef);
if (strRef != NULL)
{
MethodDesc* pMD = pResolver->GetDynamicMethod();
string =
(OBJECTHANDLE)pMD->GetModule()->GetAssembly()->Parent()->GetOrInternString(&strRef);
}
GCPROTECT_END();
*ppInfo = (LPVOID)string;
return IAT_PVALUE;
}
我粘贴此函数只是为了展示字符串引用是如何在内部检索的。这对于演示来说并非必需,但我认为它很有趣,因为它涉及到 GetDynamicResolver
和模块句柄。我在上一篇文章中已经介绍了 CORINFO 句柄,展示了它们如何只是类指针。实际上,GetDynamicResolver
基本上只是一个类型转换。
inline DynamicResolver* GetDynamicResolver(CORINFO_MODULE_HANDLE module)
{
WRAPPER_CONTRACT;
CONSISTENCY_CHECK(IsDynamicScope(module));
return (DynamicResolver*)(
((size_t)module) & ~((size_t)CORINFO_MODULE_HANDLE_TYPE_MASK));
}
为了完成对 compileCEE_LDSTR
的分析,“emit_”宏用于生成平台特定的原生代码,而 pushOp
函数是一系列用于处理 JIT 编译为原生代码所需的 MSIL 堆栈的函数的一部分。我稍后将讨论 MSIL 堆栈。
这是 call 操作码的处理程序
case CEE_CALL:
JitResult = compileCEE_CALL();
break;
compileCEE_CALL
在内部调用另一个函数。所以我将粘贴两者
FJitResult FJit::compileCEE_CALL()
{
unsigned int token;
CORINFO_METHOD_HANDLE targetMethod;
CORINFO_MODULE_HANDLE tokenScope = methodInfo->scope;
GET(token, unsigned int, false);
VERIFICATION_CHECK(jitInfo->isValidToken(tokenScope, token));
CORINFO_CALL_INFO callInfo;
// Call this because the CLR "misuses" this method to activate
// the target assembly (if needed). So if we would not call it
// later in the game the compiled code could try to call into
// the assembly which was not activated yet.
// On the other hand we don't actually need any information
// provided by this call.
jitInfo->getCallInfo(methodInfo->ftn,
tokenScope,
token,
0, // constraintToken -
methodInfo->ftn,
CORINFO_CALLINFO_KINDONLY,
&callInfo);
targetMethod = jitInfo->findMethod(tokenScope, token, methodInfo->ftn);
VALIDITY_CHECK(targetMethod);
return this->compileHelperCEE_CALL(token, targetMethod, false /*readonly*/);
}
FJitResult FJit::compileHelperCEE_CALL(unsigned int token,
CORINFO_METHOD_HANDLE targetMethod,
bool isReadOnly /* = false */)
{
unsigned int argBytes, stackPadorRetBase = 0;
unsigned int parentToken;
CORINFO_CLASS_HANDLE targetClass, parentClass = NULL;
CORINFO_SIG_INFO targetSigInfo;
CORINFO_METHOD_HANDLE tokenContext= methodInfo->ftn;
CORINFO_MODULE_HANDLE tokenScope = methodInfo->scope;
// Get attributes for the method being called
DWORD methodAttribs;
methodAttribs = jitInfo->getMethodAttribs(targetMethod,methodInfo->ftn);
// Get the class of the method being called
targetClass = jitInfo->getMethodClass (targetMethod);
// get the exact parent of the method
parentToken = jitInfo->getMemberParent(tokenScope, token);
parentClass = jitInfo->findClass(tokenScope,
parentToken,
methodInfo->ftn);
// Get the attributes of the class of the method being called
DWORD classAttribs;
classAttribs = jitInfo->getClassAttribs(targetClass, methodInfo->ftn);
// Verify that the method has an implementation i.e. it is not abstract
VERIFICATION_CHECK(!(methodAttribs & CORINFO_FLG_ABSTRACT ));
if (methodAttribs & CORINFO_FLG_SECURITYCHECK)
{
TailCallForbidden = TRUE;
if (MadeTailCall)
{ // we have already made a tailcall, so cleanup and jit this method again
if(cSequencePoints > 0)
cleanupSequencePoints(jitInfo,sequencePointOffsets);
resetContextState();
return FJIT_JITAGAIN;
}
}
jitInfo->getMethodSig(targetMethod, &targetSigInfo);
if (targetSigInfo.isVarArg())
jitInfo->findCallSiteSig(tokenScope,token,tokenContext,&targetSigInfo);
// Verify that the arguments on the stack match the method signature
int result_arg_ver = ( JitVerify ? verifyArguments( targetSigInfo, 0, false) :
SUCCESS_VERIFICATION );
VALIDITY_CHECK( result_arg_ver != FAILED_VALIDATION );
VERIFICATION_CHECK( result_arg_ver != FAILED_VERIFICATION );
// Verify the this argument for non-static methods( it is not part of
// the method signature )
CORINFO_CLASS_HANDLE instanceClassHnd = jitInfo->getMethodClass(methodInfo->ftn);
if (!( methodAttribs& CORINFO_FLG_STATIC) )
{
// For arrays we don't have the correct class handle
if ( classAttribs & CORINFO_FLG_ARRAY)
targetClass = jitInfo->findMethodClass( tokenScope, token, tokenContext );
int result_this_ver = ( JitVerify
? verifyThisPtr(instanceClassHnd, targetClass,
targetSigInfo.numArgs, false )
: SUCCESS_VERIFICATION );
VERIFICATION_CHECK( result_this_ver != FAILED_VERIFICATION );
}
// Verify the constraints on the target method (including its parent)
VERIFICATION_CHECK( jitInfo->satisfiesClassConstraints(parentClass));
VERIFICATION_CHECK( jitInfo->satisfiesMethodConstraints(parentClass, targetMethod));
// Verify that the method is accessible from the call site
VERIFICATION_CHECK(jitInfo->canAccessMethod(methodInfo->ftn, parentClass,
targetMethod, instanceClassHnd ));
if (targetSigInfo.hasTypeArg())
{
CORINFO_CLASS_HANDLE tokenType;
// Instantiated generic method
if(isReadOnly)
{
// when the call is readonly the Array Stub expects the type arg to
// be zero
emit_LDC_I(0);
}
else
{
TokenToHandle(parentToken, tokenType);
}
}
argBytes = buildCall(&targetSigInfo, CALL_NONE, stackPadorRetBase, false );
CORINFO_CONST_LOOKUP addrInfo;
jitInfo->getFunctionEntryPoint(targetMethod, IAT_VALUE, &addrInfo);
VALIDITY_CHECK(addrInfo.addr);
VALIDITY_CHECK(addrInfo.accessType == IAT_VALUE || addrInfo.accessType == IAT_PVALUE);
emit_callnonvirt((unsigned)addrInfo.addr,
(targetSigInfo.hasRetBuffArg() ? typeSizeInBytes(jitInfo,
targetSigInfo.retTypeClass) : 0),
addrInfo.accessType == IAT_PVALUE);
return compileDO_PUSH_CALL_RESULT(argBytes, stackPadorRetBase, token,
targetSigInfo, targetClass);
}
如前所述,ldstr 是一个非常容易处理的操作码。call 指令稍微复杂一些,但不要被它吓到,它很简单。代码的大小主要是因为进行了许多有效性检查。compileCEE_CALL
首先调用 getCallInfo
,它似乎被误用为激活包含代码的程序集。然后调用 findMethod
来检索正在调用的方法的句柄。之后,调用 compileHelperCEE_CALL
函数。此函数执行大量检查:我们可以跳过这些检查,专注于后面的部分。在最后的调用中,可以看到 getFunctionEntryPoint
函数,这正是我们所寻找的。buildCall
、emit_callnonvirt
和 compileDO_PUSH_CALL_RESULT
只构建原生代码调用语法并发出原生操作码。
getFunctionEntryPoint
的唯一描述可以在 corinfo.h 中找到
// return a callable address of the function (native code). This function
// may return a different value (depending on whether the method has
// been JITed or not. pAccessType is an in-out parameter. The JIT
// specifies what level of indirection it desires, and the EE sets it
// to what it can provide (which may not be the same).
virtual void __stdcall getFunctionEntryPoint(
CORINFO_METHOD_HANDLE ftn, /* IN */
InfoAccessType requestedAccessType, /* IN */
CORINFO_CONST_LOOKUP * pResult, /* OUT */
CORINFO_ACCESS_FLAGS accessFlags =
CORINFO_ACCESS_ANY) = 0;
基本上,此函数检索目标函数的可用原生代码。在调用 getFunctionEntryPoint
之前,有必要检索目标方法的句柄。这可以通过 findMethod
来实现。
现在可以编写一个小演示。与上一篇文章一样,我使用 .NET 加载器在加载受害者程序集之前挂钩 JIT。nvcoree.dll 挂钩 compileMethod
并注入显示带有文本“密码正确!”的 .NET 消息框的原生代码。这是 nvcoree.dll 的代码
#include "stdafx.h"
#include <CorHdr.h>
#include "corinfo.h"
#include "corjit.h"
#include <tchar.h>
extern "C" __declspec(dllexport) void HookJIT();
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD dwReason,
LPVOID lpReserved
)
{
HookJIT();
return TRUE;
}
BOOL bHooked = FALSE;
ULONG_PTR *(__stdcall *p_getJit)();
typedef int (__stdcall *compileMethod_def)(ULONG_PTR classthis, ICorJitInfo *comp,
CORINFO_METHOD_INFO *info, unsigned flags,
BYTE **nativeEntry, ULONG *nativeSizeOfCode);
struct JIT
{
compileMethod_def compileMethod;
};
compileMethod_def compileMethod;
//
// native code to inject
//
#define CODE_SIZE 15
BYTE Code[CODE_SIZE] =
{
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [addr]
0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [msgbox]
0xC2, 0x04, 0x00 // ret 4
};
int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
CORINFO_METHOD_INFO *info,
unsigned flags, BYTE **nativeEntry, ULONG *nativeSizeOfCode)
{
//
// Very lazy way to identify the method to inject
//
const char *szMethodName = NULL;
const char *szClassName = NULL;
szMethodName = comp->getMethodName(info->ftn, &szClassName);
if (strcmp(szMethodName, "button1_Click") == 0)
{
//
// Retrieve string
//
unsigned int strToken = 0x70000063; // "Right password!"
void* literalHnd = NULL;
comp->constructStringLiteral(info->scope, strToken, &literalHnd);
//
// Retrieve method
//
/*
* misused to activate the method's assembly
* (we don't care about that)
*
CORINFO_CALL_INFO callInfo;
comp->getCallInfo(info->ftn,
info->scope,
0x0A00001E,
0, // constraintToken
info->ftn,
CORINFO_CALLINFO_KINDONLY,
&callInfo);
*/
CORINFO_METHOD_HANDLE targetMethod = comp->findMethod(info->scope,
0x0A00001E, info->ftn);
CORINFO_CONST_LOOKUP addrInfo;
comp->getFunctionEntryPoint(targetMethod, IAT_VALUE, &addrInfo);
//
// Set up native code
//
/*
* This is basically what we're doing
*
__asm
{
mov ecx, [literalHnd]
call [addrInfo.addr]
}
*/
BYTE *pCode = Code;
pCode += 2;
*((ULONG_PTR *) pCode) = (ULONG_PTR) literalHnd;
pCode += 6;
*((ULONG_PTR *) pCode) = (ULONG_PTR) addrInfo.addr;
DWORD dwOldProtect;
VirtualProtect(Code, CODE_SIZE, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*nativeEntry = Code;
*nativeSizeOfCode = CODE_SIZE;
return CORJIT_OK; // it's 0 as usual
}
int nRet = compileMethod(classthis, comp, info, flags, nativeEntry, nativeSizeOfCode);
return nRet;
}
//
// Hooks compileMethod
//
extern "C" __declspec(dllexport)
void HookJIT()
{
if (bHooked) return;
LoadLibrary(_T("mscoree.dll"));
HMODULE hJitMod = LoadLibrary(_T("mscorjit.dll"));
if (!hJitMod)
return;
p_getJit = (ULONG_PTR *(__stdcall *)()) GetProcAddress(hJitMod, "getJit");
if (p_getJit)
{
JIT *pJit = (JIT *) *((ULONG_PTR *) p_getJit());
if (pJit)
{
DWORD OldProtect;
VirtualProtect(pJit, sizeof (ULONG_PTR), PAGE_READWRITE, &OldProtect);
compileMethod = pJit->compileMethod;
pJit->compileMethod = &my_compileMethod;
VirtualProtect(pJit, sizeof (ULONG_PTR), OldProtect, &OldProtect);
bHooked = TRUE;
}
}
}
每次用户单击按钮时,注入的代码将始终被调用,而不是实际的密码检查。
- 下载原生注入演示
我处理的两个指令相对简单。其他操作码,如 ldfld 和 callvirt,稍微复杂一些,因为它们也使用 MSIL 堆栈,我之前提到了这一点。ldfld 从堆栈中弹出一个值,该值是它将引用的字段所属的对象。这是 jit ldfld 的一小部分代码
FJitResult FJit::compileCEE_LDFLD( OPCODE opcode)
{
unsigned address = 0;
unsigned int token, parentToken;
DWORD fieldAttributes;
CorInfoType jitType;
CORINFO_CLASS_HANDLE targetClass = NULL, parentClass = NULL;
bool fieldIsStatic;
CORINFO_MODULE_HANDLE tokenScope = methodInfo->scope;
CORINFO_METHOD_HANDLE tokenContext = methodInfo->ftn;
CORINFO_FIELD_HANDLE targetField;
// Get MemberRef token for object field
GET(token, unsigned int, false);
VERIFICATION_CHECK(jitInfo->isValidToken(tokenScope, token));
targetField = jitInfo->findField (tokenScope, token,tokenContext);
VALIDITY_CHECK(targetField);
fieldAttributes = jitInfo->getFieldAttribs(targetField,methodInfo->ftn);
fieldIsStatic = (fieldAttributes & CORINFO_FLG_STATIC) ? true : false;
targetClass = jitInfo->findClass(tokenScope, jitInfo->getMemberParent(
tokenScope, token), tokenContext);
VALIDITY_CHECK(targetClass);
// targetClass is the enclosing class
CORINFO_CLASS_HANDLE valClass;
jitType = jitInfo->getFieldType(targetField, &valClass, targetClass);
if (fieldIsStatic)
{
emit_initclass(targetClass);
}
OpType fieldType = createOpType(jitType, valClass );
OpType type;
#if !defined(FJIT_NO_VALIDATION)
// Initialize the type correctly getting additional information for
// managed pointers and objects
if ( fieldType.enum_() == typeByRef )
{
_ASSERTE(valClass != NULL);
CORINFO_CLASS_HANDLE childClassHandle;
CorInfoType childType = jitInfo->getChildType(valClass, &childClassHandle);
fieldType.setTarget(OpType(childType).enum_(),childClassHandle);
}
else if ( fieldType.enum_() == typeRef )
VALIDITY_CHECK( valClass != NULL );
// Verify that the correct type of the instruction is used
VALIDITY_CHECK( fieldIsStatic || (opcode == CEE_LDFLD) );
CORINFO_CLASS_HANDLE instanceClassHnd = jitInfo->getMethodClass(methodInfo->ftn);
//INDEBUG(printf( "Field Type [%d, %d] %d \n",fieldType.enum_(),fieldType.cls(),
//valClass );)
#endif
if (opcode == CEE_LDFLD)
{
// There must be an object on the stack
CHECK_STACK(1);
type = topOp();
if (type.type_enum == typeR4 || type.type_enum == typeR8) {
return FJIT_OK;
}
// The object on the stack can be managed pointer, object, native int,
// instance of object
VALIDITY_CHECK( type.isPtr() || type.enum_() == typeValClass );
// Verification doesn't allow native int to be used
VERIFICATION_CHECK( type.enum_() != typeI || (type.cls() &&
isPrimitiveValueType(type.cls())) );
// Store the object reference for the access check
instanceClassHnd = type.cls();
OpType targetType = createOpType(type.enum_(), targetClass );
// Check that the object on the stack encloses the field
VERIFICATION_CHECK( canAssign( jitInfo, methodInfo->ftn, type, targetType));
// Remove the instance object of the IL stack
POP_STACK(1);
if (fieldIsStatic) {
// we don't need this pointer
if (type.isValClass())
{
unsigned sizeValClass = typeSizeInSlots(jitInfo, type.cls()) *
sizeof(void*);
emit_drop(BYTE_ALIGNED(sizeValClass));
}
else
{
emit_POP_PTR();
}
}
else
{
//INDEBUG(printf( "Object Type [%d, %d] \n",type.enum_(),type.cls() );)
if (type.isValClass() || (type.enum_() == typeI && type.cls() &&
isPrimitiveValueType(type.cls())) )
{ // the object itself is a value class
pushOp(type); // we are going to leave it on the stack
emit_getSP(STACK_BUFFER); // push pointer to object
}
}
如您所见,该函数使用了许多处理 MSIL 堆栈(内部称为操作数堆栈)的 Op 方法。以下是一些内联方法
inline OpType& FJit::topOp(unsigned back) {
_ASSERTE (opStack_len > back);
if ( opStack_len <= back )
RaiseException(SEH_JIT_REFUSED,EXCEPTION_NONCONTINUABLE,0,NULL);
return(opStack[opStack_len-back-1]);
}
inline void FJit::popOp(unsigned cnt) {
_ASSERTE (opStack_len >= cnt);
opStack_len -= cnt;
#ifdef _DEBUG
opStack[opStack_len] = OpType(typeError);
#endif
}
inline void FJit::pushOp(OpType type) {
_ASSERTE (opStack_len < opStack_size);
_ASSERTE (type.isValClass() || (type.enum_() >= typeI4 || type.enum_() < typeU1));
_ASSERTE (type.enum_() != 0 );
opStack[opStack_len++] = type;
#ifdef _DEBUG
opStack[opStack_len] = OpType(typeError);
#endif
}
inline void FJit::resetOpStack() {
opStack_len = 0;
#ifdef _DEBUG
opStack[opStack_len] = OpType(typeError);
#endif
}
inline bool FJit::isOpStackEmpty() {
return (opStack_len == 0);
}
opStack 仅仅是指向 OpType 类数组的指针。以下是 OpType 类及其可以表示的类型的声明
enum OpTypeEnum {
typeError = 0,
typeByRef = 1,
typeRef = 2,
typeU1 = 3,
typeU2 = 4,
typeI1 = 5,
typeI2 = 6,
typeI4 = 7,
typeI8 = 8,
typeR4 = 9,
typeR8 = 10,
typeRefAny = 11,
typeValClass = 12,
typeMethod = 13,
typeCount = 14,
typeI = typeI4,
};
struct OpType {
OpType();
OpType(OpTypeEnum opEnum);
explicit OpType(CORINFO_CLASS_HANDLE valClassHandle);
explicit OpType(CORINFO_METHOD_HANDLE mHandle);
explicit OpType(OpTypeEnum opEnum,
CORINFO_CLASS_HANDLE valClassHandle,
bool setClassHandle = false,
bool isReadOnly = false);
explicit OpType(OpTypeEnum opEnum, OpTypeEnum childEnum);
explicit OpType(CorInfoType jitType, CORINFO_CLASS_HANDLE valClassHandle,
bool setClassHandle = false);
explicit OpType(CorInfoType jitType);
static const char toOpStackType[];
/* OPERATORS */
int operator==(const OpType& opType) {
return( type_handle == opType.type_handle &&
type_enum == opType.type_enum &&
readonly == opType.readonly ); }
int operator!=(const OpType& opType) { return(!(*this == opType)); }
/* ACCESSORS */
bool isPtr() { return(type_enum == typeRef || type_enum == typeByRef ||
type_enum == typeI ); }
bool isPrimitive()
{ return((unsigned) type_enum <= (unsigned) typeRefAny); } // refany is a primitive
bool isValClass()
{ return((unsigned) type_enum >= (unsigned) typeRefAny); } // refany is a valclass too
bool isTargetPrimitive() { return((unsigned) child_type <= (unsigned) typeRefAny); }
inline bool isNull() { return (child_type == typeRef && type_enum == typeRef); }
inline bool isRef() { return (type_enum == typeRef); }
inline bool isRefAny() { return (type_enum == typeRefAny); }
inline bool isByRef() { return (type_enum == typeByRef); }
inline bool isReadOnly() { return (readonly == 1); }
inline bool isMethod() { return (type_enum == typeMethod); }
inline OpTypeEnum enum_() { return ( type_enum ); }
inline CORINFO_CLASS_HANDLE cls() { return ( type_handle ); }
inline CORINFO_METHOD_HANDLE getMethod() { return ( method_handle ); }
inline OpTypeEnum targetAsEnum() { return child_type; }
OpType getTarget()
{ return ( isTargetPrimitive() ? OpType( child_type ) : OpType( type_handle )); }
bool matchTarget( OpType other )
{ _ASSERTE( type_enum == typeByRef ); return isTargetPrimitive() ?
other.enum_() == targetAsEnum() : other.cls() == cls(); }
/* MUTATORS */
// unsafe, please limit use
void fromInt(unsigned i){ type_handle = (CORINFO_CLASS_HANDLE)(size_t)i; }
void setHandle(CORINFO_CLASS_HANDLE h) { type_handle = h; }
void setTarget( OpTypeEnum opEnum, CORINFO_CLASS_HANDLE h )
{ if ( h == NULL ) child_type = opEnum; else type_handle = h;
_ASSERTE( (child_type != typeByRef && child_type != typeRef) || isNull() );}
void setTarget( CorInfoType jitType, CORINFO_CLASS_HANDLE h )
{ if ( h == NULL ) child_type = OpType(jitType).enum_(); else type_handle = h;
_ASSERTE( (child_type != typeByRef && child_type != typeRef) || isNull() );}
void setReadOnly(bool isReadOnly) { readonly = (unsigned) isReadOnly; }
void init(OpTypeEnum opEnum, CORINFO_CLASS_HANDLE valClassHandle,
bool isReadOnly = false )
{ type_enum = opEnum; type_handle = valClassHandle; readonly =
(unsigned) isReadOnly; }
void init(CorInfoType jitType, CORINFO_CLASS_HANDLE valClassHandle )
{ type_enum = OpType(jitType).enum_(); type_handle = valClassHandle; }
static const OpTypeEnum Signed[];
void toSigned() {
if (type_enum < typeI1)
type_enum = Signed[type_enum];
}
static const OpTypeEnum Normalize[];
void toNormalizedType() {
if (type_enum < typeI4)
type_enum = Normalize[type_enum];
}
static const OpTypeEnum FPNormalize[];
void toFPNormalizedType() {
if ( type_enum < typeR8)
type_enum = FPNormalize[type_enum];
}
// Data structure
unsigned readonly : 1;
OpTypeEnum type_enum : 31;
union {
// Valid only for STRUCT or REF or BYREF
CORINFO_CLASS_HANDLE type_handle;
// Valid only for type METHOD
CORINFO_METHOD_HANDLE method_handle;
// Valid for BYREF to primitives only
OpTypeEnum child_type;
};
};
此类的实际数据适合 qword。此类的主要值是 type 成员。在某些情况下(取决于类型),还需要附加信息,例如句柄。例如,如果类型是 typeMethod
,还需要 CORINFO_METHOD_HANDLE
。我粘贴这段代码的原因是理解 MSIL 堆栈可能对下面的两段有用。
原生反编译
这个主题在 .NET 上从未被讨论过。我所说的原生反编译并不是从机器代码到 C#(举个例子),而是从机器代码到 MSIL。MSIL 随后可以反编译成 C#。将机器代码转换为 MSIL 不仅更容易,而且是唯一符合逻辑的反编译方法。这个过程很困难:我只讨论可能性。最重要的事情是堆栈解释。让我们以“原生注入”段中看到的部分代码为例
00000011 mov edx, [0x238b9bc]
00000017 mov ecx, eax
00000019 call 0x7426edd0
0000001E and eax, 0xff
00000023 jz 0x2c
00000025 mov eax, 0x1
0000002A jmp 0x2e
0000002C xor eax, eax
0000002E test eax, eax
00000030 jz 0x42
00000032 mov ecx, [0x238b9c0]
00000038 call [0x5102544]
0000003E pop esi
0000003F ret 0x4
00000042 mov ecx, [0x238b9c4]
00000048 call [0x5102544]
0000004E pop esi
0000004F ret 0x4
由于我知道偏移量为 38h 的调用会像 MessageBox.Show(String)
一样被调用,因此我也知道堆栈上的第一个参数,或者在这种情况下,由于它是 fastcall,ecx 中的数据代表一个 String
类。然而,这相当正常,因为 MessageBox
是一个公共 API。在原生 C++ 应用程序中,公共 API 可以以相同的方式进行解析。当考虑代码中调用的 CheckPassword(String)
方法时,可以注意到区别。CheckPassword
是一个私有方法,但仍然可以检索其参数、返回类型,如果它没有被混淆,甚至可以检索其名称。因此,我完全知道移动到 ecx 中的数据代表一个实例,因为 CheckPassword
是一个非静态类成员,而移动到 edx 中的数据代表一个 String
类。我也知道这个调用返回一个布尔值,并且可以相应地解释下面的指令。
我必须与原生 C++ 应用程序进行一个小比较,因为许多人轻视 MSIL 代码可以被反编译的事实,因为他们说 C/C++ 代码也可以被反编译。这是一个完全不正确的说法,因为它是在比较苹果和橙子。谈到 C/C++ 应用程序,有时可以获得粗略的反编译 C 代码。在某些情况下,反编译器甚至无法生成任何 C 代码。即使它能够生成,在许多情况下,反编译的代码是错误的。即使在反编译的 C 代码实际上是正确的(即它正确地表示机器代码正在做什么)的情况下,它也不一定比机器代码更容易被读者理解,因为反编译的 C 代码通常是一团糟。最后但同样重要的是,C 反编译器不知道如何解释数据。例如,当我引用结构中的成员时,生成的反编译 C 代码只会生成一个指向指针 + N 的引用,其中 N 是到所引用成员的偏移量。这意味着“info.bValue = TRUE
”在 C 代码中会生成类似“*((int *) (ptr + N)) = 1;
”的内容。对于方法的参数、返回值、调用等也是如此。尽管反编译的 C 代码有时可以重新编译,但它对知识产权完全没有威胁。至少,不像分析机器代码那样。
在讨论保护 .NET 应用程序时,问题的根源在于元数据。元数据对于许多目的都很有用,但我从逆向工程的角度来分析它。元数据无所不包,使其无法隐藏任何内容。
尽管 .NET 原生反编译目前还不是一个重要问题,但评估其可能性很有趣,因为它会使像原生框架部署服务这样的尝试变得无用。原生映像本身必须包含足够的信息,以便执行引擎能够解决原生代码中的引用。这些信息可以被逆向工程师用来反编译。即使信息丢失,例如在手动注入原生代码的情况下,仍然(尽管不容易)可以与 JIT 通信以解决引用。
理论上,机器代码也可以被混淆以进一步复杂化反编译,但仍然可以解决代码中的引用,使其比其 C/C++ 等价物更容易理解。
.NET 虚拟机
虚拟机在原生代码领域取得了巨大的成功。将这个概念引入 .NET 代码只是时间问题。我不知道有多少保护措施依赖于这项技术,但我可以说微软自己也投入了这项技术,推出了其 SLP(软件许可与保护)服务。我无法分析其产品代码,因为这在某种程度上会违反其许可条款,但我可以讨论它。
SLP 提供每个方法的保护。这意味着用户可以选择要保护哪些方法。受保护的方法在反汇编时看起来是这样的
private bool CheckPassword(string strPass)
{
object[] args = new object[] { strPass };
return (bool) SLMRuntime.SVMExecMethod(this, "28d981d5a74646a9bed4c66fdcbd82d8", args);
}
该方法只做一件事:通过传递类实例、方法的参数和一个代表被调用方法的字符串来调用虚拟机。
保护的运行时由三个 .NET 程序集组成。运行时在 .NET Framework 之上创建自己的虚拟机。.NET 虚拟机使用反射来解析外部引用。如果我在当前类中引用一个私有变量,虚拟机将执行以下操作
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
namespace reflection
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private int MyPrivateVariable = 0;
private void ChangePrivateVar(object obj)
{
Type t = obj.GetType();
// get the field, no matter how the field is declared
FieldInfo f = t.GetField("MyPrivateVariable", BindingFlags.Public |
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance);
f.SetValue(obj, (int) 1);
}
private void button1_Click(object sender, EventArgs e)
{
// displays 0
MessageBox.Show(MyPrivateVariable.ToString());
// changes the value given the current object
ChangePrivateVar(this);
// displays 1
MessageBox.Show(MyPrivateVariable.ToString());
}
}
}
如您所见,元数据结合反射确实非常有用。然而,我让读者想象一下基于反射技术构建的 .NET 虚拟机在执行时间上会有多慢。这就是为什么即使 SLP 指南也警告用户
在之前关于从食谱烘烤蛋糕的比喻中,假设您必须保护整个食谱。当然,蛋糕食谱有很多相似之处,保护整个食谱是不必要的,只需要保护那些使其独特的部分。这不会显著降低食谱的安全性,但会使其阅读速度更快——只需要解密那些秘密配料。
同样,由于 SVM 需要解释 SVML 代码,并且运行在 CLR 之上,因此存在一个性能要素需要解决。您不想保护整个代码库,因为它会减慢整个应用程序的速度并对整体安全性几乎没有贡献。相反,您只想保护必要的部分:秘密配料。
在这段文字中,他们似乎认为只保护少量方法是好事,但这并不现实。鉴于 .NET 虚拟机方法相当不错,并且比原生框架部署服务更专业,它存在一些重大缺陷。这种方法可能是许可 .NET 应用程序的最佳方法,但它确实对保护知识产权没有什么帮助。如果一个人的整个应用程序依赖于一堆非执行时间关键的方法,那么它隐藏的东西真的不是什么大秘密。虚拟化方法也存在一些限制
具有以下构造的方法无法转换为代码保护程序。
- 泛型类中的方法。
- 包含泛型类型显式实例化的方法。
- 具有泛型参数的方法。
- 结构体的非静态方法。
- 具有“out”或“ref”参数的方法。
- 调用具有“out”或“ref”参数的其他方法的那些方法。
- 修改任何方法参数的方法,即使参数定义为“按值”传递。
- 参数数量可变的方法(例如,在 C# 中使用“params”关键字)。
- 具有过多局部变量或参数(> 254)的方法。
- 调用 Reflection.Assembly.GetExecutingAssembly()、Reflection.MethodInfo.GetCurrentMethod() 或 Reflection.Assembly.GetCallingAssembly() 的方法。
- 仅限 CLR 1.1 Framework:创建具有可变数量参数的构造函数的对象的那些方法。当调用非构造函数方法时,不存在此限制。
- 隐式和显式转换运算符无法转换为安全虚拟机(SVM)。
- 不安全代码——例如,在 C# 中,包含 unsafe 关键字的方法通常无法转换。
此列表对于那些可能考虑自己编写 .NET 虚拟机的人来说也很有趣。我已经给出了我对这种保护技术我的看法,但让我们看看如何克服它。
如果有人真的对受保护的方法做了什么感兴趣,那么有必要分析虚拟机代码。我想到的第一个方法是使用 .NET 分析 API 注入日志代码,以检索虚拟机内部调用的方法。这将提供一个执行流程日志,可用于分析特定方法执行的虚拟机代码。
第二种克服此类保护的技术基于替换。如果一个人不关心代码的作用,因为他知道它或者知道代码应该做什么,那么他可以替换自己的代码。这可以通过 Sebastien Lebreton 的 Reflexil 轻松完成。这种方法针对破解,而不是逆向工程。但由于 SLP 也是一个许可系统,因此必须将其考虑在内。假设方法 F 设置了应用程序的初始化设置。此方法通过 SLP 进行保护,除非拥有程序的有效许可证,否则 SLP 不会执行它。一个人可以重新实现 F 方法,并完全分离受保护程序集的 SLP 运行时。在某些情况下这可能很困难,但这正是逆向工程的全部内容。然而,SLP 非常慢,保护许多方法会导致不可接受的性能损失。通过在安装过程中自动生成原生映像,可以显著提高性能问题。
有时,虚拟机保护与代码混淆结合使用,为所有未被虚拟化的方法提供安全。在这种情况下,如果一个人对反编译 MSIL 代码感兴趣,第一步是删除代码混淆。这只能通过分析混淆算法并理解如何反转它来完成。通过 Rebel.NET 可以轻松地重建去混淆的程序集。
结论
我从未读过关于 CLR 基础结构的书籍或文章,本文介绍的是从逆向工程师的角度来看的 .NET 内部。这是关于 .NET 内部和保护措施的两篇系列文章中的第二篇。我希望我让读者对 .NET 保护系统周围的问题有了一些了解。由于 .NET 技术仍然非常年轻,它可能会发生重大变化。我不知道知识产权是否会在框架的未来版本中得到考虑。我也希望在开发新框架时会考虑到这些问题。由于 .NET Framework 一直是逆向工程的新游乐场,我只能猜测在开发初期许多问题并不明显(尽管 Java 的经验应该是一个教训)。.NET 框架的一个可能演变是提供原生编译作为 MSIL 的替代方案,并大幅减少元数据信息,仅保留公共类型/成员的元数据。
也许我完全错了,我们很快就会看到大多数大型应用程序以 MSIL 程序集的形式部署。我强烈怀疑这一点。