给.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