给.net程序打内存补丁-转
标 题: 【原创】给.net程序打内存补丁(1)
作 者: tankaiha
时 间: 2006-08-23,22:37:54
链 接: http://bbs.pediy.com/showthread.php?t=30940
给.net程序打内存补丁(1)
by:tankaiha[NE365]
2006.8.23
any problem: visit http://vxer.cn
内存补丁在破解中的作用不多说了,Win32平台下的内存补丁技术大家也都很熟悉,这里主要讲.Net平台下可执行程序的内存补丁。换一个说法,叫动态地改变正在执行的.Net可执行程序的指令(或数据)。传统的技术在.Net下不能用了吗?也不是,但是给JIT即时编译MSIL代码生成的asm代码打补丁,难度有点大。我们需要的,是直接在MSIL的基础上进行修改。此技术我也在学习中,学一点写一点,有错误请大家指正。
前置知识及参考文献(文献都可在google中搜到,MSDN中还有很多相关文档):
1、MSIL基本知识
2、Rewrite MSIL Code on the Fly with the .NET Framework Profiling API (Aleksandr Mikunov)
3、Modifying IL at runtime (Julien Couvreur's)
4、The .NET Profiling API and the DNProfiler Tool (Matt Pietrek)
5、.Net FrameWork SDK中的相关文档
如果有可能,在阅读本文之前先阅读上面的文献,这样会轻松一些。
一、.Net Profiling API
什么是.Net Profiling API,从字面上看就是这种API可以提供.Net运行情况的概况。而实际情况是它提供的功能远远不只如此,动态修改MSIL代码就是用的这些API。如果你已经看了上面的参考文献,应该已经知道它是干吗的。
通俗的说,Profiling API是.Net平台提供的底层接口之一(还有Debug和Metadata相关的,待续),通过该接口提供的方法,我们可以得到(及控制)以下过程的相关信息:
Application的开始/结束
Assembly的载入/卸载
Methods的开始/结束
Module的载入/卸载
Class的类的载入/卸载
线程
与COM接口的互操作
托管/非托管代码的即时编译
等等……
基本上.Net的核心操作都可以通过Profiling API来得到信息。
二、使用Profiling API
这里结合一个例子说,因为该接口太复杂,不可能一下学完全部的功能。我们结合这篇文章(在 .NET Framework 2.0 中,没有任何代码能够逃避 Profiling API 的分析)中的例子,在其基础上修改,加入我们需要的功能。修改的方法应用了参考文献2的方法。
先去网上搜索上面的文章,把它的附件下来,解压。有四个目录,我们关心的就是Profiler目录中的文件。
Profiling API是通过编译成dll文件,注册为系统的com服务而实现的。(对com我不熟悉,不多说),因此,你见到的profiler的例子通常都是三个文件:ProfilerCallback.h,ProfilerCallback.cpp,Profiler.cpp。前两个是实现Profiler的核心功能代码,最后一个只是实现了dll的基本构架。我们需要的就是在ProfilerCallback.cpp添加代码实现功能。
首先找到CProfilerCallback::JITCompilationStarted,顾名思义,当JIT引擎开始编译MSIL代码的时候,会执行这里的代码。CProfilerCallback是什么?看一下定义:
class CProfilerCallback : public ICorProfilerCallback2
它就是作者实现的类,继承自.Net的核心接口ICorProfilerCallback2。这里的2应该代表是针对.Net 2.0的。(我试过,.net v1.1和v2.0下分别生成的Profiler,对不同版本的执行程序有时存在兼容性问题。)ICorProfilerCallback2接口很复杂,但琐碎的工作都由作者做好了,我们只是使用现成的类。
三、修改的目标
目标程序是一个简单的程序,如图:
代码如下(在VS2005中编译,运行于.Net FrameWork 2.0):
namespace tmp{ public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { if(textBox1.Text!="tankaiha") { MessageBox.Show("Wrong!"); } else { MessageBox.Show("You get it"); } } }}
也就是如果文本框中输入了“tankaiha”,则显示You get it。输入其它则显示错误。用ildasm可以查看这段代码的IL:
IL_0000: ldarg.0
IL_0001: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox tmp.Form1::textBox1
IL_0006: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()
IL_000b: ldstr "tankaiha"
IL_0010: call bool [mscorlib]System.String::op_Inequality(string,
string)
IL_0015: brfalse.s IL_0023
IL_0017: ldstr "Wrong!"
IL_001c: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0021: pop
IL_0022: ret
IL_0023: ldstr "You get it"
IL_0028: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_002d: pop
IL_002e: ret
我们的目标,把IL_0015处的brfalse.s(0x2C)改为brtrue.s(0x2D)。
四、修改ProfilerCallback.cpp
下面要跟着源码看了。
GetFullMethodName():取得即将编译的方法的名称。我们需要调用该方法来比较是不是到了tmp.Form1.button1_Click。
GetFullMethodName (functionId, wszMethod, NAME_BUFFER_SIZE); if (lstrcmpW(wszMethod,wszTarget)!=0) { goto exit; }
假如比较结果OK,我们已经进入了button1_Click,则接着获得该方法的详细信息。
hr = m_pICorProfilerInfo->GetFunctionInfo(functionId, &classId, &moduleId, &tkMethod ); if (FAILED(hr)) { goto exit; } hr = m_pICorProfilerInfo->GetILFunctionBody(moduleId, tkMethod, &pMethodHeader, &iMethodSize); if (FAILED(hr)) { goto exit; }IMAGE_COR_ILMETHOD* pMethod = (IMAGE_COR_ILMETHOD*)pMethodHeader;
.Net中的方法分为fat和tiny,具体区别见参考文献。因此先判断到底是fat还是tiny,然后分别处理。我们是入门,只考虑最简单的情况。
if(IsTinyHeader(pMethod)) { COR_ILMETHOD_TINY* tinyImage = (COR_ILMETHOD_TINY*)&pMethod->Tiny; //Handle Tiny method codeBytes = tinyImage->GetCode(); ULONG codeSize = tinyImage->GetCodeSize(); } else { COR_ILMETHOD_FAT* fatImage = (COR_ILMETHOD_FAT*)&pMethod->Fat; codeBytes = fatImage->GetCode(); ULONG codeSize = fatImage->GetCodeSize(); }
在取得了相应的代码块信息后,开始修改。首先分配新的代码块,将老的复制过去,再修改相应的字节,最后将新的代码块分配给Click方法。
//这里开始修改代码 IMethodMalloc* pIMethodMalloc = NULL; IMAGE_COR_ILMETHOD* pNewMethod = NULL; hr = m_pICorProfilerInfo->GetILFunctionBodyAllocator(moduleId, &pIMethodMalloc); if (FAILED(hr)) { goto exit; } pNewMethod = (IMAGE_COR_ILMETHOD*) pIMethodMalloc->Alloc(iMethodSize); if (pNewMethod == NULL) { goto exit; } memcpy((void*)pNewMethod, (void*)pMethod, iMethodSize); if(IsTinyHeader(pNewMethod)) { COR_ILMETHOD_TINY* newtinyImage = (COR_ILMETHOD_TINY*)&pNewMethod->Tiny; codeBytes = newtinyImage->GetCode(); ULONG codeSize = newtinyImage->GetCodeSize(); codeBytes[21]=0x2D;//就是这里修改 } else { COR_ILMETHOD_FAT* newfatImage = (COR_ILMETHOD_FAT*)&pNewMethod->Fat; codeBytes = newfatImage->GetCode(); ULONG codeSize = newfatImage->GetCodeSize(); codeBytes[21]=0x2D; } hr = m_pICorProfilerInfo->SetILFunctionBody(moduleId, tkMethod, (LPCBYTE) pNewMethod); if (FAILED(hr)) { goto exit; } pIMethodMalloc->Release();
这就是主要添加的代码。源文件中还有很多LogEntry()的代码,这是调试时可以显示调试信息,用DebugTrack就可以看到了。
源代码中还有几处要修改。一处是CProfilerCallback::GetEventMask()。把源代码屏蔽掉,然后m_dwEventMask=COR_PRF_MONITOR_JIT_COMPILATION,这样就可以只接收JIT编译的相关信息了。第二处是有一个sleep及int 3,这是原作者方便调试的,删除之。具体修改后的ProfilerCallback.cpp见附件。
五、实测
编译生成了Profiler.dll后怎么用呢?主要是以下5步(注意,在同一个cmd窗口中操作):
1、regsvr32 Profiler.dll(注册服务)
2、设置环境变量,主要是两个
SET COR_PROFILER={18884ADE-B15B-4af8-BE6C-FE5117BA4B32}
SET COR_ENABLE_PROFILING=1
3、运行tmp.exe(我们的试验程序)。点击check后,你会看到结果。
4、关闭Profiler,set Cor_Enable_Profiling=0x0。
5、regsvr32 /u Profiler.dll。
我打开了所有的调试信息输出,随便输入一些字符串,运行结果如图:
红圈处清楚地看到,2C已经被改为2D了。
你还可以继续作试验,多次点击check,已经不再显示JIT信息了,因为.net已经将这些代码存储在缓冲区中了,而且不管你输入什么,都显示you get it。
六、结语
这篇文章讲得太少了,很多具体的信息都在相应的参考文献中,如果光看这篇文章会摸不着头脑的。本文的主要目的就是告诉你怎么使用现成的代码,修改为自已的Profiler,更深的东东还要继续摸索。
To be continued
给.net程序打内存补丁(2)
tankaiha[NE365][FCG]
2006-8-25
接上文。上次讲了个最简单的动态修改代码的方法,讲得比较简约。今天介绍个复杂点的代码修改,顺便多介绍一些基本概念。
一、修改目标
先看今天修改的目标。这一次tmp.cs的代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace tmp
{
//新增的类
public class userClass1
{
public static void showMsg()
{
MessageBox.Show("You get it", "^_^");
}
};
public partial class Form1 : Form
{
bool bRetVal;
//定义一个变量,表示是否输入正确的字符串
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
string cmpText = textBox1.Text;
if(cmpText=="tankaiha")
{
this.bRetVal = true;
}
else
{
this.bRetVal = false;
}
}
//永远不会执行这个
private void neverUsed()
{
MessageBox.Show("You never get this");
}
}
}
我们在代码新增了一个userClass1,里面有一个静态方法showMsg()。我们要在button1_Click返回前执行这个方法。先对比一下这次的修
改和上次的不同
上次 本次
代码块大小 不变 改变
代码块类型 小型(tiny) 大型(fat)
其实这次难度也不大,更难的在以后介绍。来看一下反汇编代码(部分):
……..
IL_0000: /* 02 | */ ldarg.0
IL_0001: /* 7B | (04)000003 */ ldfld class [System.Windows.Forms/*23000001*/]
System.Windows.Forms.TextBox/*01000006*/ tmp.Form1/*02000002*/::textBox1 /* 04000003 */
IL_0006: /* 6F | (0A)000028 */ callvirt instance string [System.Windows.Forms/*23000001*/]System.Windows.Forms.Control/*0100001B*/::get_Text() /* 0A000028 */
IL_000b: /* 0A | */ stloc.0
IL_000c: /* 06 | */ ldloc.0
IL_000d: /* 72 | (70)00003B */ ldstr "tankaiha" /* 7000003B */
IL_0012: /* 28 | (0A)000029 */ call bool [mscorlib/*23000002*/]System.String/*01000024*/::op_Equality(string,string) /* 0A000029 */
IL_0017: /* 2C | 08 */ brfalse.s IL_0021
IL_0019: /* 02 | */ ldarg.0
IL_001a: /* 17 | */ ldc.i4.1
IL_001b: /* 7D | (04)000004 */ stfld bool tmp.Form1/*02000002*/::bRetVal /* 04000004 */
IL_0020: /* 2A | */ ret
IL_0021: /* 02 | */ ldarg.0
IL_0022: /* 16 | */ ldc.i4.0
IL_0023: /* 7D | (04)000004 */ stfld bool tmp.Form1/*02000002*/::bRetVal /* 04000004 */
IL_0028: /* 2A | */ ret
} // end of method Form1::button1_Click
我们要在最后IL_0028 ret之前插入一个call。
二、相关基本概念
先来看看.net里的call。如果你用ildasm比较多,会比较熟悉。MSIL里的call是0x28,后面接了个(XX)YYYYYY,这是它的操作数,合起来XXYYYYYY就是你要call的方法的token。Token就是一个代码该方法的唯一值。不只是方法,.net中的任何东东都有个token。反汇编后发现userClass1.showMsg()的token是0x06000006。XX是分类(06),YYYYYY(000006)是序号。用工具打开tmp.exe可以看得更清楚些。
从上图中看到,00是module,01是TypeRef,02是TypeDef,当然,还有06代表Method。而Method中,000001是Form1::Dispose(),000002是Form1::InitializeComponent,还有我们要调用的userClass1::showMsg(),排在第6位。这些信息都是存储在#~流中,还有其它的流,如#Strings、#US和#Blog等。这些流分别存储不同的信息,具体见MSDN,与本文关系不大。
.Net中的token还有个方法,就是在一个Module中他是不变的,不管是否在一个类里,都可以直接用token值调用。因此,我们只需要在ret前插入28 06 00 00 06。
第二个要介绍的概念是tiny和fat的区别。下图是方法在内存中的布局。
说白了,方法体就是一块内存。很明显,tiny比fat少了SEH处理块。一般来说,有SEH肯定是fat的,二是代码超过64字节也是fat。我们这次处理的就是不含SEH的fat Method。(下次再说对SEH块的处理)
基本概念先介绍这两个,下面看代码。
三、修改
下面开始修改代码,仍然在JITCompilationStarted中,首先定义我们要插入的代码:
#pragma pack(1)
struct
{
BYTE insertcall;
DWORD method_token;
} InsertCode;
#pragma pack()
InsertCode.insertcall=0x28;
InsertCode.method_token=0x06000006;
这样,我们的代码就比原代码大了5字节,所以在分配空间时要加上:
IMethodMalloc* pIMethodMalloc = NULL;
IMAGE_COR_ILMETHOD* pNewMethod = NULL;
hr = m_pICorProfilerInfo->GetILFunctionBodyAllocator(moduleId, &pIMethodMalloc);
if (FAILED(hr))
{ goto exit; }
pNewMethod = (IMAGE_COR_ILMETHOD*) pIMethodMalloc->Alloc(iMethodSize+sizeof(InsertCode)+1);//注意新空间
的size要改
if (pNewMethod == NULL)
{ goto exit; }
memcpy((void*)pNewMethod, (void*)pMethod, iMethodSize);
下面是对fat方法头的处理和修改
if(IsTinyHeader(pNewMethod))
{
……
}
else
{
COR_ILMETHOD_FAT* newfatImage = (COR_ILMETHOD_FAT*)&pNewMethod->Fat;
codeBytes = newfatImage->GetCode();
ULONG codeSize = newfatImage->GetCodeSize()+sizeof(InsertCode);
//这里更改,注意位置的选择
memcpy(codeBytes+codeSize-sizeof(InsertCode)-1,&InsertCode,sizeof(InsertCode));
codeBytes[codeSize-1]=0x2A;
newfatImage->SetCodeSize(codeSize);
}
最后是将修改过的代码分配给新的方法,并释放空间。
hr = m_pICorProfilerInfo->SetILFunctionBody(moduleId, tkMethod, (LPCBYTE) pNewMethod);
if (FAILED(hr))
{ goto exit; }
pIMethodMalloc->Release();
四、测试
测试方法不变,不过这次给新手做了个动画。下面是前后结果对比,注意看红体字的codeSize前后对比和从第40个字节开始的更改。
1 17:59:04:078 516: tmp funcitonId is a75930
2 17:59:04:078 516: tmp JITCompilationStarted: ::tmp.Form1.button1_Click
3 17:59:04:078 516: tmp target string is: tmp.Form1.button1_Click
4 17:59:04:078 516: tmp enter fat code
5 17:59:04:078 516: tmp Flags: 13
6 17:59:04:093 516: tmp MaxStack: 2
7 17:59:04:093 516: tmp CodeSize: 29
8 17:59:04:093 516: tmp LocalVarSigTok: 11000001
……
44 19:44:59:062 3984: tmp codeBytes[35] = 0x7D;
45 19:44:59:062 3984: tmp codeBytes[36] = 0x04;
46 19:44:59:062 3984: tmp codeBytes[37] = 0x00;
47 19:44:59:062 3984: tmp codeBytes[38] = 0x00;
48 19:44:59:062 3984: tmp codeBytes[39] = 0x04;
49 19:44:59:078 3984: tmp codeBytes[40] = 0x2A;
50 17:59:04:250 516: tmp enter fat code again
51 17:59:04:265 516: tmp Flags: 13
52 17:59:04:265 516: tmp MaxStack: 2
53 17:59:04:281 516: tmp NewCodeSize: 2E
54 17:59:04:281 516: tmp LocalVarSigTok: 11000001
……
90 19:44:59:343 3984: tmp codeBytes[35] = 0x7D;
91 19:44:59:359 3984: tmp codeBytes[36] = 0x04;
92 19:44:59:359 3984: tmp codeBytes[37] = 0x00;
93 19:44:59:375 3984: tmp codeBytes[38] = 0x00;
94 19:44:59:375 3984: tmp codeBytes[39] = 0x04;
95 19:44:59:390 3984: tmp codeBytes[40] = 0x28;
96 19:44:59:390 3984: tmp codeBytes[41] = 0x06;
97 19:44:59:390 3984: tmp codeBytes[42] = 0x00;
98 19:44:59:406 3984: tmp codeBytes[43] = 0x00;
99 19:44:59:406 3984: tmp codeBytes[44] = 0x06;
100 19:44:59:421 3984: tmp codeBytes[45] = 0x2A;
打完收功,下一次会介绍更复杂更有实战性的修改。附件里是测试文件和测试的动画,专为新手准备。
给.net程序打内存补丁(3)
by:tankaiha[NE365][FCG]
2006-9-2附件下载
这算是本系列最后一篇了,因为偶要学习另一个新课题。本系列的文章主要来源是《Modifying IL at runtime》,代码主要来源是MSDN上的《在 .NET Framework 2.0 中,没有任何代码能够逃避 Profiling API 的分析》。虽然是很久前的文章了,但对于像我一样刚接触这方面的人应该还是很有帮助的。废话少说,进入正题。
一、修改目标
先来看看这次修改目标text.exe的代码,有一点接近实战,要求输入用户名和密码,正确和错误均会提示。
主要代码如下,可以看到,程序读取“用户名”框中的用户输入,调用cryptcal函数进行计算,然后和用户输入的注册码比较,判断结果的正误。
private void button1_Click(object sender, EventArgs e)
{
if(textBox1.Text.Length==0)
{
MessageBox.Show("请输入用户名!");
}
else if (textBox2.Text.Length == 0)
{
MessageBox.Show("请输入注册码!");
}
else if(textBox2.Text==cryptcal(textBox1.Text))
{
MessageBox.Show("你怎么猜到的!");
}
else
{
MessageBox.Show("猜错了!");
}
}
private string cryptcal(string textToCal)
{
byte[] encData_byte = new byte[textToCal.Length];
encData_byte = System.Text.Encoding.UTF8.GetBytes(textToCal);
string encodedData = Convert.ToBase64String(encData_byte);
return encodedData;
}
修改的目标,另写一个inject.dll,其中调用MessageBox,动态修改程序让它自己弹出正确的注册码。来看一下inject.dll的源程序,也就是要在test中调用injectClass.injectMsg方法,并把正确的注册码当作参数传递给该方法。
using System;
using System.Windows.Forms;
namespace injectcode
{
public class injectClass
{
public static void injectMsg(string str)
{
MessageBox.Show("正确的注册码是:"+str,"插入代码提示");
}
}
}
inject.dll的编译方法,首先生成一个强命名文件,然后编译,这样inject就有了个PublicKeyToken。没有这个标志时,载入会出错。(原参考文献中并没有,不知道为什么总出错。)全部命令行如下:
sn –k injectkey.snk
csc /target:library /key:injectkey.snk inject.cs
用Reflector可以看到inject的信息,这在下面的代码中要用。
二、修改方法
看一下tmp.exe的反编译代码,来到button1_Click,找一下入手点。
.method /*06000004*/ private hidebysig
instance void button1_Click(object sender,
…..
IL_0049:/*7B | (04)000002*/ ldfld class System.Windows.Forms.TextBox tmp.Form1::textBox1
IL_004e:/*6F | (0A)00002B*/callvirt instance string System.Windows.Forms.Control::get_Text()
IL_0053:/*28 | (06)000005*/call instance string tmp.Form1/*02000002*/::cryptcal(string)
IL_0058:/*28 | (0A)00002E*/call bool System.String::op_Equality(string, string)
IL_005d:/*2C | 0C */ brfalse.s IL_006b
IL_005f:/* 72 | (70)000091*/ ldstr bytearray (60 4F 0E 60 48 4E 1C 73 30 52 84 76 01 FF )
IL_0064:/*28 | (0A)00002D*/ call System.Windows.Forms.MessageBox/*01000027*/::Show(string)
IL_0069: /* 26 | */ pop
IL_006a: /* 2A | */ ret
IL_006b: /*72 | (70)0000A1*/ ldstr bytearray (1C 73 19 95 86 4E 01 FF )
IL_0070: /*28 | (0A)00002D*/ call System.Windows.Forms.MessageBox/*01000027*/::Show(string)
IL_0075: /* 26 | */ pop
IL_0076: /* 2A | */ ret
} // end of method Form1::button1_Click
红色体显示的就是进行字符串比较,从堆栈上取两个参数,其中堆栈顶的就是正确的注册码。我们就把它当作参数,直接传递给injectMsg。由于要平衡堆栈,还应该在调用后执行一个pop。插入代码形如
Call injectClass.injectMsg(string);
Pop;
这两句代码共6个字节,为了保证程序正常运行,我们将从IL_005D行开始的0x0C,到IL_006a处的代码全部nop掉。记住,nop在.net中的代码是00,不是0x90,别搞错了。
三、几个概念
介绍几个基本概念。首先,.net中的程序无论是传入参数还是返回值,都存储在堆栈中,因此修改程序时要注意堆栈的平衡。第二,.net一个进程中的token是唯一的,即使我们要调用的是另一个assembly中的方法,也只需要在本assembly中调用call token既可。Assembly、Class和Method的token结构不同,以Method为例,如下图
我们在代码中的定义就是(具体参考Tool Development Guide中的文档)
// 为injectMsg方法建立token
COR_SIGNATURE Sig_void_String[] = {
0, // IMAGE_CEE_CS_CALLCONV_DEFAULT
0x1, // argument count
ELEMENT_TYPE_VOID, // ret = ELEMENT_TYPE_VOID
ELEMENT_TYPE_STRING// parameter
};
其中
• HASTHIS for IMAGE_CEE_CS_CALLCONV_HASTHIS
• EXPLICITTHIS for IMAGE_CEE_CS_CALLCONV_EXPLICITTHIS
• DEFAULT for IMAGE_CEE_CS_CALLCONV_DEFAULT
• VARARG for IMAGE_CEE_CS_CALLCONV_VARARG
我们用的是第三项,并直接跳过了前两项。
四、Profiler的代码
Profile的代码仍然是在JITCompilationStarted中修改。下面分步讲解。
GetFullMethodName (functionId, wszMethod, NAME_BUFFER_SIZE);
//如果不是我们要找的方法就返回
if (lstrcmpW(wszMethod,wszTarget)!=0)
{
goto exit;
}
//取得函数体
hr = m_pICorProfilerInfo->GetFunctionInfo(functionId, &classId, &moduleId, &tkMethod );
if (FAILED(hr))
{ goto exit; }
hr = m_pICorProfilerInfo->GetILFunctionBody(moduleId, tkMethod, &pMethodHeader, &iMethodSize);
if (FAILED(hr))
{ goto exit; }
//取得Metadata Import
IMetaDataImport* pMetaDataImport = NULL;
hr = m_pICorProfilerInfo->GetModuleMetaData(moduleId, ofRead, IID_IMetaDataImport,(IUnknown** )&pMetaDataImport);
if (FAILED(hr))
{ goto exit; }
//开始修改Metadata
//首先取得必需的接口
IMetaDataEmit* pMetaDataEmit = NULL;
IMetaDataAssemblyEmit* pMetaDataAssemblyEmit = NULL;
mdAssemblyRef tkInsertLib;
hr = m_pICorProfilerInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, IID_IMetaDataEmit,(IUnknown** )&pMetaDataEmit);
if (FAILED(hr)) { goto exit; }
hr = pMetaDataEmit->QueryInterface(IID_IMetaDataAssemblyEmit,(void**)&pMetaDataAssemblyEmit);
if (FAILED(hr)) { goto exit; }
下面的要注意,是关键代码开始。主要是分别为Assembly,Class和Method建立token,还记得原来讲的token是某个东西在.net程序中的唯一标识。
mdTypeDef tkInertClass = 0;
mdMethodDef tkInsertMethod = 0;
// 为inject.dll创立一个token
ASSEMBLYMETADATA amd;
ZeroMemory(&amd, sizeof(amd));
amd.usMajorVersion = 0;
amd.usMinorVersion = 0;
amd.usBuildNumber = 0;
amd.usRevisionNumber = 0;
byte assemblyPublicKeyToken[]={0x1e,0xf0,0xec,0x8e,0x40,0x91,0xde,0x2c};
这里的token就是刚才用Reflector看到的值。代码继续
hr = pMetaDataAssemblyEmit->DefineAssemblyRef(
&assemblyPublicKeyToken, sizeof(assemblyPublicKeyToken),
L"inject",
&amd, NULL, 0, 0,
&tkInsertLib);
if (FAILED(hr)) { goto exit; }
这样就已经为inject这个Assembly建立了token,下面再为class和method建立。
// 为injectClass建立token
hr = pMetaDataEmit->DefineTypeRefByName(tkInsertLib,L"injectcode.injectClass", &tkInertClass);
if (FAILED(hr)) { goto exit; }
// 为injectMsg方法建立token
COR_SIGNATURE Sig_void_String[] = {
0, // IMAGE_CEE_CS_CALLCONV_DEFAULT
0x1, // argument count
ELEMENT_TYPE_VOID, // ret = ELEMENT_TYPE_VOID
ELEMENT_TYPE_STRING// parameter
};
hr = pMetaDataEmit->DefineMemberRef(tkInertClass,
L"injectMsg",Sig_void_String, sizeof(Sig_void_String),
&tkInsertMethod);
if (FAILED(hr)) { goto exit; }
下面开始修改代码了,同前两篇一样,先定义代码,再分配新的方法块并修改。简单起见,我们这里只考虑fat头的修改了,由于是在原代码上修改,因此代码块大小没变。被修改的指令位于第89个字节处。
//这里开始修改代码
//首先定义我们要插入的代码,注意改变默认的对齐方式
#pragma pack(1)
struct
{
BYTE insertcall;
DWORD method_token;
BYTE insertpop;
} InsertCode;
#pragma pack()
InsertCode.insertcall=0x28;//call指令
InsertCode.method_token=tkInsertMethod;//插入方法的token
InsertCode.insertpop=0x26;//pop指令
//下面先取得已有的il
hr = m_pICorProfilerInfo->GetILFunctionBody(moduleId, tkMethod, &pMethodHeader, &iMethodSize);
if (FAILED(hr))
{ goto exit; }
IMAGE_COR_ILMETHOD* pMethod = (IMAGE_COR_ILMETHOD*)pMethodHeader;
if(IsTinyHeader(pMethod)) //小头就不处理了
{
goto exit;
}
//分配新的空间
IMethodMalloc* pIMethodMalloc = NULL;
IMAGE_COR_ILMETHOD* pNewMethod = NULL;
hr = m_pICorProfilerInfo->GetILFunctionBodyAllocator(moduleId, &pIMethodMalloc);
if (FAILED(hr))
{ goto exit; }
pNewMethod = (IMAGE_COR_ILMETHOD*) pIMethodMalloc->Alloc(iMethodSize);//这里的size没变
if (pNewMethod == NULL)
{ goto exit; }
memcpy((void*)pNewMethod, (void*)pMethod, iMethodSize);
COR_ILMETHOD_FAT* newfatImage = (COR_ILMETHOD_FAT*)&pNewMethod->Fat;
LogEntry("enter fat code\n");
//Handle Fat method
LogEntry("Flags: %X\n", newfatImage->Flags);
LogEntry("MaxStack: %X\n", newfatImage->MaxStack);
LogEntry("NewCodeSize: %X\n", newfatImage->CodeSize);
LogEntry("LocalVarSigTok: %X\n", newfatImage->LocalVarSigTok);
codeBytes = newfatImage->GetCode();
ULONG codeSize = newfatImage->GetCodeSize();//方法大小不变
//这里更改
memcpy(codeBytes+88,&InsertCode,sizeof(InsertCode));//从第89个字节开始改
ZeroMemory(codeBytes+94,13);
for(ULONG i = 0; i < codeSize; i++)
{
if(codeBytes[i] > 0x0F)
{
LogEntry("codeBytes[%u] = 0x%X;\n", i, codeBytes[i]);
}
else
{
LogEntry("codeBytes[%u] = 0x0%X;\n", i, codeBytes[i]);
}
}
hr = m_pICorProfilerInfo->SetILFunctionBody(moduleId, tkMethod, (LPCBYTE) pNewMethod);
if (FAILED(hr))
{ goto exit; }
pIMethodMalloc->Release();
LogEntry("modify exit");
五、测试
为方便新手,仍然做了一个动画。最终可以看到程序弹出的MessageBox中显示当用户名是ne365时,正确的注册码为bmUzNjU=。
好了,本系列告一段落。只希望本系列对新手能有些帮助,让更多的人进入.net内核这个有趣的世界。