给.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内核这个有趣的世界。