.Net JIT二进制骚操DHVM破解篇

前言

经研究,号称最强.Net加密软件DNGuard HVM(以下简称DHVM),五行代码基本上可以优雅的破解它,本篇看下。友情提示,以下全是二进制汇编骚操,慎入。.

概括

示例:

非常简单的示例

static void ABC(){   Console.WriteLine("Call ABC");}static void DEF(){   Console.WriteLine("Call DEF");}static void Main(string[] args){   Console.WriteLine("Call Main");   ABC();   DEF();   Console.ReadLine();}

修改调用ABC函数的逻辑为调用DEF函数,Main函数的MSIL二进制代码如下:

00 72 25 00 00 70 28 0e 00 00 0a 00 28 06 00 00 06 00 28 07 00 00 06 00 28 0f 00 00 0a 26 2a

这里的MSIL二进制代码可以参考:罕见的技术:MSIL的机器码简析

1.难点
因为Hook JIT,简单的MSIL修改已经不起作用。DHVM的各种反调试,比如VS调试器无法进入某些内存地址。一进入就会报异常。它静态地址在运行的时候动态偏移,它进行了PE的IAT(导入表)的Name字段验证,当IAT的Name不为0的时候,就会报异常等。这些东西叠加在一起,无法调试,无法通过输入表注入DLL等。

2.蛛丝马迹
避开这些反调试手段,魔高一尺道高一丈嘛,蛛丝马迹即是破绽。
当我们通过一些可以调试的地址进入发现一些有趣的东西,比如以下代码:

0000000180497AB2: E9 A1 73 00 00 jmp  0000000180497AB80000000180497AB7: F8             clc0000000180497AB8: 4C 89 5F 10    mov  qword ptr [rdi+10h],r11

这一段汇编代码是关键点,它通过jmp指令跳到地址0000000180497AB8。然后执行指令

mov  qword ptr [rdi+10h],r11

这里的r11寄存器保存的是通过DHVM加密后的托管DLL的真实的MSIL二进制代码。rdi寄存器是DHVM Hook的JIT的函数invokeCompileMethod的参数methodInfo地址,rdi+0x10即是methodInfo的成员变量IL_Code地址。这个IL_Code里面的值会被JIT编译器编译成机器码,然后运行。

那么这段指令的意思很明显,也就是说把DHVM加密后把保存的托管DLL的真实MSIL二进制代码赋值给IL_Code。它这么做的目的就是屏蔽掉原有托管DLL里面的MSIL,而用DHVM自己加密之后保存的MSIL。无论你怎么修改原有的托管DLL,都不会影响JIT的执行。

3.预破
既然探查到了以上蛛丝马迹,下面着手解决掉DHVM。这里的思路是,因为jmp是个跳转指令,所以可以让它跳转到自己的地址。这个自己的地址因为无法通过IAT注入DLL构建,上面说了DHVM会搜寻IAT的Name字段是否为0。因为Win11超强的PatchGuard,所以这里不考虑DLL注入了。直接在HVMRun64.dll内部构建。通过dumpbin,把HVMRun64.dll的汇编代码导出到记事本。HVMRun64.dll的最后的汇编地址如下:

000000018049EE53: 00 74 56 01  add byte ptr [rsi+rdx*2+1],dh000000018049EE57: 00

它这个地址对应的是把HVMRun64二进制的地址如下:

00499253:00 74 56 01 00

从00499258地址开始后面全都是0,类似如下:

00499258:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00          00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00          00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00          00 00 00 00 00 00 00 00 00

可以从这里为零的数据开始构建。在构建数据之前还需要做一件事情,我们上面蛛丝马迹里面jmp指令

0000000180497AB2: E9 A1 01 00 00 jmp  0000000180497AB8

需要让它跳转到00499258这个地址来,然后在这个为零地址里面做自己想要做的事情。如何跳转呢?可以把jmp地址改成如下:

//jmp跳转地址减去jmp所在地址减去5等E9后面的数值,E9是jmp机器码0000000180497AB2: E9 A1 73 00 00  jmp  000000018049EE58

这里的000000018049EE58地址指向的即是00499258所在全部是零的地址处。当它跳转到0区之后如下代码:

首先在00499258地址处写入二进制代码:4D 89 DF,这三个十六进制代表的汇编是mov  r15,r11,上面说了r11保存的是MSIL需要编译的二进制代码,通过跟踪发现如果直接更改r11寄存器,则会导致异常。跟踪也发现r15寄存器为零,所以这里把r11赋值给r15。然后把蛛丝马迹里面的代码
0000000180497AB8: 4C 89 5F 10    mov  qword ptr [rdi+10h],r11也就是这里的r11替换成r15,让它称为最后编译的MSIL
我们需要做的就是在r15里面修改MSIL二进制即可。

如何把r11替换成r15呢?看它的代码:

0000000180497AB8: 4C 89 5F 10  mov  qword ptr [rdi+10h],r11

修改成如下:

0000000180497AB8 4C 89 7F 10   mov  qword ptr [rdi+10h],r15 

把机器码5F改成7F即可。以上所有准备好了,我们开始替换MSIL代码,也即是r15寄存器修改。

4.破解
上面把十六进制的4D 89 DF写入了00499258地址,也即是000000018049EE58所指向的地址。

因为4D89DF

占3个字节,所以下面的地址

0x000000018049EE58+0x3==000000018049EE5B.

000000018049EE5B这个地址写入如下:

0049925B:49 C6 47 0D 07转换成汇编也即是如下:000000018049EE5B 49 C6 47 0D 07  mov ptr byte [r15+D],07

这里是把07这个数值赋值给r15偏移的0xD的位置处。这里修改r15偏移的0xD位置的数值,实际上是把示例里面的调用的ABC函数修改成调用DEF函数,也就是改变函数逻辑。示例的结果是:

Call MainCall ABCCall DEF

我们通过hook DHVM之后的结果是

Call MainCall DEFCall DEF

示例里面的MSIL二进制代码是:

00 72 25 00 00 70 28 0e 00 00 0a 00 28 06 00 00 06 00 28 07 00 00 06

调用ABC函数的MSIL二进制代码是:

28 06 00 00 06

调用DEF函数的二进制代码是:

28 07 00 00 06

可以看到ABC和DEF函数的MSIL二进制代码,只是基本上相同,上面偏移的0x1的位置一个是06,一个是07。如果想要把调用ABC改成调用DEF,这里只需要把06改成07即可,也就是这段汇编代码的意义

000000018049EE5B 49 C6 47 0D 07  mov ptr byte [r15+D],07

这里改了之后,还得跳回去,因为上面的汇编占了五个字节,所以这里下一个地址是:

0x000000018049EE5B+0x5==0x000000018049EE60

在地址0x000000018049EE60里面跳转到原来的jmp需要跳转的地址也即是

00499260:E9 53 8C FF FF000000018049EE60 E9 53 8C FF FF jmp 0000000180497AB8

这样就完成了整个闭环的操作,在Hook DHVM里面这里只是简单的修改了一个字节数值,当然可以修改更多以满足自己的需求。

本篇用的是:.Net JIT的骚操作DNGuard HVM原理简析。里面提到的第二种方法也即是破二。第一种方法也可,而且能够做的更多。但是规模和成本上去了。个人比较喜欢简洁,所以选择了第二种。

5.整体
那么整体的代码是:

DHVM跳转代码和r11替换成r150000000180497AB2: E9 A1 73 00 00  jmp  000000018049EE580000000180497AB8 4C 89 7F 10      mov  qword ptr [rdi+10h],r15
hook代码:000000018049EE58 4D 89 DF        mov   r15,r11000000018049EE5B 49 C6 47 0D 07  mov   ptr byte [r15+D],07000000018049EE60 E9 53 8C FF FF  jmp   0000000180497AB8

可以看到,真正的代码,也就那么几行,甚至也就是修改一个字节。所谓返璞归真,即是这个道理。

以上DHVM的整体过程,仅用于学习用途。