前言
经研究,号称最强.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 0000000180497AB8
0000000180497AB7: F8 clc
0000000180497AB8: 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],dh
000000018049EE57: 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 Main
Call ABC
Call DEF
我们通过hook DHVM之后的结果是
Call Main
Call DEF
Call 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 FF
000000018049EE60 E9 53 8C FF FF jmp 0000000180497AB8
这样就完成了整个闭环的操作,在Hook DHVM里面这里只是简单的修改了一个字节数值,当然可以修改更多以满足自己的需求。
本篇用的是:.Net JIT的骚操作DNGuard HVM原理简析。里面提到的第二种方法也即是破二。第一种方法也可,而且能够做的更多。但是规模和成本上去了。个人比较喜欢简洁,所以选择了第二种。
5.整体
那么整体的代码是:
DHVM跳转代码和r11替换成r15
0000000180497AB2: E9 A1 73 00 00 jmp 000000018049EE58
0000000180497AB8 4C 89 7F 10 mov qword ptr [rdi+10h],r15
hook代码:
000000018049EE58 4D 89 DF mov r15,r11
000000018049EE5B 49 C6 47 0D 07 mov ptr byte [r15+D],07
000000018049EE60 E9 53 8C FF FF jmp 0000000180497AB8
可以看到,真正的代码,也就那么几行,甚至也就是修改一个字节。所谓返璞归真,即是这个道理。
以上DHVM的整体过程,仅用于学习用途。
评论 (0)