给.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内核这个有趣的世界。
录像在二楼
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: