1 – 引言
2 - CLR 环境
2.1 – 基本概念
2.1.1 - Metadata表
2.1.2 - Metadata token
2.1.3 - MSIL 字节码
2.2 – 执行环境
3 - JIT 编译器
3.1 – 编译方法
3.2 – Hook编译方法
4 - .NET Instrumentation
4.1 – MSIL注入策略
4.2 - Resolving the method handle
4.3 – 基于call指令实现一个跳板
4.4 – 制作一个动态方法
4.5 – 调用用户自定义代码
4.6 – 修复SHE表
5 – 现实样例
5.1 – Web应用密码盗取
5.2 – 恶意软件检测
6 – 结论
7 – 引用
8 – 源代码
为了找一种在.NET程序运行过程中操作它的新方法,本文我们将深入探索.NET框架内部。 实际上,已经有一些库可以操作.NET程序,它们大多hook某个方法编译后的代码,或者修改汇编指令并将修改结果写回文件。微软也提供了API用于操作某个程序的执行过程。但是必须在程序执行前通过设置一些环境变量使该API可用。
我们的目的是,在不碰二进制汇编代码的情况下,操作运行中的程序。所有功能通过使用一个高级.NET语言实现。正如将看到的,我们通过在目标方法被编译前注入额外的MSIL代码来实现。
在详细描述如何在方法中注入额外的MSIL代码之前,有必要先介绍一些基本概念,如:.NET框架如何工作,它的基本组件有哪些。我们只介绍与我们的目的有关的概念。
.NET二进制代码本质上可以看做是汇编代码(尽管它实际上没有任何汇编代码)。它是个自描述的结构,这意味着在汇编代码中你能找到运行需要的所有信息(更多相关信息见[01])。正如将看到的,这些信息可以通过内存映象访问到。内存映象能够让我们对汇编代码定义了哪些类型和方法有全面的了解。我们同样可以获得传给特定方法的参数的类型和名称。唯一丢失的信息是本地变量的名称,不过这对我们不重要。
注:元数据是描述数据的数据,.NET是基于面向对象的,所以元数据描述的主要目标就是面向对象的基本元素:类、类型、属性、方法、字段、参数、特性等。
上面提到的所有信息存在叫做Metadata tables的表中。下面的列表摘自引用[02],列出了现在所有Metadata表的索引和名称:
00 - Module 01 - TypeRef 02 - TypeDef
04 - Field 06 - MethodDef 08 - Param
09 - InterfaceImpl 10 - MemberRef 11 - Constant
12 - CustomAttribute 13 - FieldMarshal 14 - DeclSecurity
15 - ClassLayout 16 - FieldLayout 17 - StandAloneSig
18 - EventMap 20 - Event 21 - PropertyMap
23 - Property 24 - MethodSemantics 25 - MethodImpl
26 - ModuleRef 27 - TypeSpec 28 - ImplMap
29 - FieldRVA 32 - Assembly 33 - AssemblyProcessor
34 - AssemblyOS 35 - AssemblyRef 36 - AssemblyRefProcessor
37 - AssemblyRefOS 38 - File 39 - ExportedType
40 - ManifestResource 41 - NestedClass 42 - GenericParam
44 - GenericParamConstraint
每个表由一系列行组成。行的大小由表的类型决定,并且可以包含对其它Metadata表的引用。那些表都通过Metadata token来引用,下一节介绍Metadata token的概念。
Metadata token(或短 token)是CLR框架的基本概念。 token准许你引用指定表中指定索引值的项。它是个4字节值,由两部分组成[08]:表索引+RID。表索引是 token的最高字节,指向一个表。RID从 token字节偏移1起,长3字节,是表内的一个记录的指针(其实是个偏移,从2开始每个+1)。作为一个例子,我们来看下面的Metadata token:
(06)00000F
0x06是引用的表的编号,本例中引用的是MethodDef表(参照2.1.1的表)。最后三字节是RID,本例中值是0x0F。
当我们用.NET高级语言写程序,编译器将代码转换成MSIL或ECMA-335[03]CIL中间表示形式,它相当于通常所说的中间语言。安装VS会一起安装一个非常好用的工具ILDasm,利用它可以通过列出MSIL代码和其它有用信息来反汇编一个汇编程序。作为例子,我们编译如下的C#源代码:
JIT中执行一个方法有两种情况。第一,方法已经被编译,这种情况我们可以直接jump到编译后的unmanaged代码。第二,方法还没有被编译,这种情况代码jump到一个存根函数,存根函数call导出的compileMethod方法编译并执行该方法,该函数定义在corjit.h [04]。
再多分析一下这个重要的方法。该方法的函数原型如下:
最有意思的结构是CORINFO_METHOD_INFO,在corinfo.h [05]中定义如下:
要实现我们的目的,最重要的地方是ILCode字节指针成员。它指向包含MSIL字节码的缓存。通过修改这个缓存,我们能够修改方法的执行流。附注一点,该方法在.NET混淆中应用也很广泛。其实,在源码中可以看到如下注释:
注:JIT混淆器依赖遵守__stdcall调用约定的该方法。
混淆器首先对方法的MSIL字节码进行加密,当方法将被执行时,解密字节码并将其值作为字节指针替换原来被加密的(替换CORINFO_METHOD_INFO 结构的ILCode成员)。这也解释了为什么我们在ILDasm或者其它反汇编工具打开它时收到一个错误。那如何知道一个方法将被调用了呢?很简单,负责替换的代码在类型构造器中。这个类型构造器只被调用一次:在类型的新对象被创建前。
compileMethod由Clrjit.dll导出(老版本.NET中由mscorjit.dll导出),我们可以很容易地安装一个hook拦截所有编译请求。下面是该过程
的F#伪代码:
修改MSIL代码时必须注意栈尺寸。我们的框架工作需要一些栈空间,要编译的方法不需要任何局部变量,如果栈尺寸有问题会在运行时收到一个异常。要修复该问题,在写回文件之前修改CORINFO_METHOD_INFO结构的maxStack变量即可。
接下来修改我们选择的方法的MSIL缓存并重定向执行流到我们的代码。正如将看到的,这是一个坎坷的过程,要注意一些方面。
为了调用我们的代码,要遵循的步骤如下:
我们要创建的结构遵循下面图表定义的路径:
| ----------- |
| ----------- | +---------------------+
| Trampoline | ----> | |
| Original MSIL | | Dynamic |
| ------------- | | Method |---------------+
| ------------- | | | |
+----------------------+ v
+-------------------+
| |
| Framework |
| Dispatcher |
| |
+-------------------+
|
+----------------+
|
v
+--------------------+
| |
| User |
| Code Monitor |
| |
+--------------------+
下一段将看到,必须解析一个方法的句柄,该方法被编译用来基于反射获取必要的信息。我找到了一个方法解决这个问题,虽然方法不怎么讲究。下面的F#伪代码显示如何解析给我们提供CorMethodInfo结构的方法的句柄:
这个方法要对所有被加载的汇编文件的每个模块都调用。
现在我们有了一个MethodBase对象,我们可以使用它获得需要的信息,如参数的数量和类型。
首先我们要创建可以调用任意函数的字节码。在所有有效的OpCodes中,我们感兴趣的是calli指令[06](浏览该指令的使用说明,好像它使得我们的代码不可验证)。从MSDN可获得下面内容:
方法的入口指针,指向native代码(目标机器上)的指针,可以被符合调用约定的参数合法调用(每个函数签名一个metadata token)。方法的入口指针可以通过Ldftn或者Ldvirtftn指令创建,或者从native代码获得。
漂亮,calli指令可以任意指定一个指向native代码的指针作为操作数。唯一的困难是,我们需要通过metadata token来获取方法的入口地址,但metadata token不能作为 Ldftn或Ldvirtftn的操作数,并且不能直接在计算栈中压入方法的入口地址。不过,从Ldftn手册中可以看到[07]:
将一个指向实现特定方法的native代码的unmanaged指针(类型为native int)压到evaluation栈。
所以,如果我们有一个unmanaged指针,我们可以通过Ldc_l4指令仿真Ldftn指令的功能(假设在32位环境)[09]。不幸的是还有另一个更大的问题。Calli指令需要一个callSiteDescr。在[08]中我们可以看到:
“<token>-引用到callSiteDescr – 必须是一个有效的StandAloneSig”
StandAloneSig是表编号17。前面说过,我们无法定制这个Metadata token(它或许在表中就不存在)。我进行了一些测试看看calli指令是否可以接受其它表的Metadata token作为参数。最终我发现,它还能接受TypeSpec, Field和MethodDef表作为参数。为了我们的目的,MethodDef表是重要的,现在我们可以通过创建DynamicMethod(稍后详述)欺骗成一个有效的MethodDef token。这样我们就可以利用calli指令跳出困境并修改metadata token来定制一个MethodDef。这里要使用之前步骤中获得的MethodBase对象来确定方法接收多少参数,并在调用前将它们压入栈。
下面的F#伪代码显示如何构建calli指令:
下一步要确保calli调用的方法满足Metadata token引用的数据包含的信息。
传输给calli指令的token方法有一定的调用约定,我们现在要创建符合该方法调用约定的动态方法。从[10]中可知:
方法描述符也是一个metadata token,其中的内容包含:要调用的方法,要放置在堆栈中的参数的数量、类型、顺序,使用的调用约定。
所以,为了创建一个满足token中方法签名的方法,我们要使用一个强大的.NET能力,通过该能力我们可以定义动态方法。通过本流程可实现:
该动态方法会调用另一个方法(一个调度器),该方法有两个参数:一个标识要加载的汇编文件位置的string,一个包含要传给方法的参数的对象数组。创建该方法过程中要多注意创建对象数组,在.NET中不是所有东西都是对象。下面的伪代码创建有正确签名的动态方法:
我们来看看如何使用这个仪器方法来实现一个web应用密码盗取器。在demo中,我们将使用一个非常流行的.NET web服务 Suave[13]。我们以F#语言写该web应用以C#控制台应用写密码盗取器,以这种方法,我们可以在感兴趣的方便被编译前instrument。其它情况需要强制.NET runtime重编译方法来应用我们的配件([14]是一个可能的方法)。Web应用非常简单并且只有一个form;其HTML代码如下:
原文:http://www.phrack.org/papers/dotnet_instrumentation.html
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2019-3-27 19:56
被0sWalker编辑
,原因: