[一个简单的.NET逆向工程]给没有源代码的.NET程序打补丁

公司为一个web应用程序写了一个注册机,基本原理是用户运行这个软件后,得到一个申请码,然后公司根据这个申请码给出相应注册码,匹配后方可正常使用web软件。在别人机子上没有问题,但是我机子上运行软件后死活就是没有申请码产生,也没报错。开发此程序的人员早不知道是谁了,也没有源码,只好自己分析是什么问题导致的,如果是程序的问题,希望能给程序打个“补丁”,准确的说是采用比较初级的.NET逆向工程来注入需要的补丁代码。以下是思路和主要操作(代码中略去了不需要的代码部分)。

1.

用reflector打开后,发现是.NET程序,且没混淆,这就好办了。因为程序的代码比较少,在reflector中看就那么几个按钮的事件函数,读一下就差不多了。基本思路是,既然申请码那个文本框没有产生申请码,那么就要找到为那个申请框赋值的语句,看看那个语句有没有问题,那么要想找到这个语句,就要先找到这个申请框在程序中的ID号。

2.

从程序中填写完注册码后点击的那个注册按钮入手,那个按钮点击后提示了“注册码错误”,根据这几个字,找到了:

private void registerBut_Click(object sender, EventArgs e)
{
    if (this.sAnswerCode(this.requestCode.Text) == this.answerCode.Text)
    {
        this.setRegStr(this.answerCode.Text);
        MessageBox.Show("注册成功。");
    }
    else
    {
        MessageBox.Show("注册码错误!");
    }
}

 

显然,requestCode就是申请码那个TextBox,answerCode就是注册码那个TextBox,完成找ID号的工作。

3.

然后去找为requestCode赋值的语句:

private static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new SystemSet());
}
public SystemSet()
{
   ………
  this.requestCode.Text = this.CpuID();
   ………
}

private string CpuID()
{
   string text2 = "";
        try
         {
         ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");
         ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get();
            using (ManagementObjectCollection.ManagementObjectEnumerator enumerator = managementObjectCollection.GetEnumerator())
            {
               .....
            }
               .....
          }
        catch
          {
                
          }
        return text2;
}


可以看出来,是CpuID方法用于产生申请码的,应该是根据cpu等硬件信息产生的,这就可以为每台计算机产生不同的申请码。最终经过处理返回text2就是申请码,用try-catch代码块,把所有可能错误都忽略了,难怪没申请码但是也没报错~~~

4.

将CpuID这个方法在我本机的VS中执行了一下(需要引用System.Management),果然报错了,查看catch抛出的异常,是在调用searcher.Get()时抛出的异常,信息如下:
 
{System.Runtime.InteropServices.COMException (0x80070422): 无法启动服务,原因可能是已被禁用或与其相关联的设备没有启动。 (异常来自 HRESULT:0x80070422)
在 System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
在 System.Management.ManagementScope.InitializeGuts(Object o)
在 System.Management.ManagementScope.Initialize()
在 System.Management.ManagementObjectSearcher.Initialize()
在 System.Management.ManagementObjectSearcher.Get()

可能我机子的某个服务没有启动造成的,而Get方法却使用了此服务。Get的智能提示是:

“异步调用 WMI 查询,并绑定到一个观察程序以传递结果。”
这就明白了,去服务里看了一下,我的Windows Management Instrumentation确实没有启动,这是我之前优化系统时关上的,没想到在这里用到了。启动此服务后,程序运行正常了,可以产生申请码。

5.

虽然我知道了是这个问题,不过以后要是再使用这个软件的机子发生同样问题呢?总不能还配一个专门的说明书,所以如果能给打个小小的补丁,那是最好。不过因为没有源码,原则上是修改越少越好,而不是一味追求完美的修补。
为了方便起见,我这里只想在CPUID方法的catch中加一个提示语句:
MessageBox.Show("无法生成申请码,请确认系统WMI服务启动正常!"); 
这肯定是不够严谨的,不会所有的异常都是因为WMI未启动导致的,因为修改IL比较麻烦,这里就为了最小化修改的原则,以后发现别的问题再说。
操作思路如下:
  • 得到程序的IL代码;
  • 将需要的提示信息的IL代码注入;
  • 反编译程序;
  • 测试程序运行是否正常。
(1)
首先,利用ILDASM(.NET的IL反汇编程序,Framework自带的),打开这个程序,然后转储它的IL及其相关文件(这里我转储为programIL,一共生成4个文件,一个IL后缀,两个resources后缀,还一个res后缀)。
1 
(2)
将其中以IL后缀名的文件用文本编辑器打开,并找到CpuID的IL代码段,可以看到如下:
.method private hidebysig instance string 
          CpuID() cil managed
{
......省略
catch [mscorlib]System.Object 
    {
      IL_00b2:  pop
      IL_00b3:  leave.s    IL_00b5 

    }  // end handler
    IL_00b5:  ldloc.1
    IL_00b6:  ret
} // end of method SystemSet::CpuID

 

 
可以看到IL的catch中,确实什么都没干,就直接跳转出去了。我这里需要在里面加上上面说的提示信息,修改完毕的代码是:
method private hidebysig instance string 
          CpuID() cil managed
{
......省略
    catch [mscorlib]System.Object 
    {
      IL_00b2:  pop
      IL_00b3:  nop
	  
      IL_00b4:  ldstr      “无法生成申请码,请确认系统WMI服务启动正常!"   
      IL_00b9:  call       valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
      IL_00be:  pop
      IL_00c0:  nop
      IL_00c1:  nop
      IL_00c2:  nop
	
      IL_00c3:  leave.s    IL_00c5

    }  // end handler
      IL_00c5:  ldloc.1
      IL_00c6:  ret
} // end of method SystemSet::CpuID

 
对加入的IL稍做说明:
  • IL_00b4位置的ldstr,意思就是将后面的字符串的对象引用推送到堆栈上;
  • 然后IL_00b9的call就是调用了MessageBox.Show方法,当然,上面的字符串就会被当参数传入;
  • 最后IL_00be处执行pop,将位于顶部的刚才压入的字符串引用从堆栈中弹出;

经过上面压栈,调用,出栈,我们的自定义代码注入进去了,并且又恢复了修改前栈的状态,因此不影响栈的平衡。这里想强调一下,上面的IL写法不是唯一的,比如我们完全可以将那个字符串“无法生成申请码,请确认系统WMI服务启动正常!”用如下代码来代替:

bytearray (E0 65 D5 6C 1F 75 10 62 33 75 F7 8B 01 78 0C FF F7 8B 6E 78 A4 8B FB 7C DF 7E 57 00 4D 00 49 00 0D 67 A1 52 2F 54 A8 52 63 6B 38 5E 01 FF )

其中bytearray里面的代码为汉字的unicode码。

还需要注意的是:

  • 为了计算代码方便,插入了一些nop命令,它用于在修补操作码的情况下填充空间;
  • 由于插入了一些代码,因此IL的地址需要变化,插入代码后的源IL代码的地址应该后推,也就是之前是到IL_00b3就跳出catch,现在变成到了IL_00c3才退出,相差了16(16进制);
  • 因为从IL_00b3开始到最后所有的代码的地址都变化了,如果在之前有代码跳转到这些代码中的话,需要也相应修改!这个程序就有一个地方,就是在try没有报错情况下,他会跳过catch段而直接到原来的IL_00c5(leave.s IL_00b5),因此我将其改为了IL_00c5即可。
(3)
利用Ilasm((.NET的IL汇编程序,Framework自带的),将修改好的IL汇编成为应用程序。这个只提供了命令行版本的。
以下是命令,将这个il编译到newProgram.exe,还有许多参数,可以参照ilasm的连接;
1 
执行过程略去,以下是部分统计信息:
2 
可以看出,没有出现任何错误,提示编译成功。
 
(4)
运行一下新程序,当WMI服务停止时,确实提示了信息:
3 
经过其他测试,没有影响之前程序的功能,修改就算成功了。
 
总结:
  1. 注册机作为保护别的软件的一种程序,如果自己都保护不好,还谈何保护其他软件呢,起码的反编译措施还是需要做一下的;
  2. try-catch应该是解决软件异常而建立的机制,而不是忽略问题和省事儿的做法,最起码为了用户不至于一头雾水,为了软件发现bug后能够有效反馈和更新,给点儿提示或做个日志也是好的;
  3. 编码和测试都有很大欠缺,尤其是和系统环境相关的代码,一方面编码应该给出一些建设性的建议(比如WMI服务没有启动,请启动服务等),另一方面,测试的环境也不应该仅仅限于开发机器;
  4. 微软的IL中间语言还是很强大的,入门起来比汇编容易,当然深入了,也比较难;
  5. 系统服务不要乱优化~;
  6. 事后诸葛亮,也许我也会犯同样的问题,用别人的错误提高自己吧:)

参考:

MSIL 汇编程序

MSIL 反汇编程序

MSIL OpCodes 字段

推荐:电子工业出版者的《.NET程序的加密与解密》,看雪《加密解密》的姊妹篇,专注.NET~

posted @ 2011-08-22 09:55  DebugLZQ  阅读(2468)  评论(2编辑  收藏  举报