首页 > 软件开发 > Java > 关于 CSharp 中调用非托管代码的方法

关于 CSharp 中调用非托管代码的方法

本文并非从专业开发的角度去阐述托管/非托管的概念及托管代码如何调用非托管代码,而是从日常的工具编写中及使用中遇到的一些问题,带着解决问题的态度出发,去看待这么一个过程。

整个过程并非专业解析,而只是助于我们理解罢了。

0x00 前言

托管/非托管是微软的 .NET Framework 中特有的概念,其中,非托管代码也叫本地(Native)代码。与 Java 中的机制类似,也是先将源代码编译成中间代码(MSIL,Microsoft Intermediate Language),然后再由 .NET 中的 CLR 将中间代码编译成机器代码。

在 Csharp 中,托管代码引用非托管代码的方式一般有两种:

  • P/Invoke(平台调用)
  • Delegate(委托)-> 后续转换为 D/Invoke(动态调用)

而个人在日常工具编写的过程中,经常用到的调用方式是 P/Invoke。这种方式普遍应用于各大工具开发中,对于这种方式,从攻击角度来看,存在一些缺陷:

  • 通过 P/Invoke 进行的任何 Windows API 引用都将在 .NET 程序集的 “导入表” 中产生一个相应的条目;
  • 在存在任何可监视 API 调用(如通过 API Hooking)的安全产品,都会在 P/Invoke 调用任何 API 上看到警告/阻止,这个 Hook 方式称之为 IAT hooking

而动态调用的目的是提供一种访问(调用)这些 Windows API 的替代方案,而不会留下这些特定的指标(并不是说动态调用没有自身的指标)。

但是关于 Delegate 的使用,我们在大多数利用工具的开发中,很少人会去用到。但是如果去搜索这东西,会发现很早就有人使用它来写了东西,因此我们可以很快的找到资料进行学习。Delegate 主要用于解决 Csharp 和 DLL 之间的数据传送问题

若传递的是函数指针,有两种方法:

  • 由于 Csharp 中没有函数指针的概念,因此采用委托(Delegate)的方式,使用 Intptr 存储指针,并使用 ref 获得地址(&);

  • 另一种是在 Csharp 中编写非托管的代码,用 unsafe 声明

因此本文会对 P/InvokeDelegate 两种调用方式进行一些说明,并说明动态调用为什么可以绕过 IAT hooking

0x01 IAT hooking

Hook 的概念就不累述了

即使是基于 CS 的 execute-assembly 等内存执行方法,EDR 通过 Hook 进程,也能捕获到进攻性行为。针对这种情况,@CCob 巨佬在他的文章中也给了一个非常奈斯的例子,证明的这个 POC,以及如何绕过这种 Hooking。一个高效的 EDR,会尽可能的 Hooking 底层函数,如 NT 级别的 Win32 API。下图是一个很好的例子,充分阐明了 EDR 的工作原理(其中 ntdll.dll 负责向 Windows 内核进行系统调用):

EDR 的 Hook 方式主要有两种:

  • IAT hooking:IAT 是 Import Address Table 的缩写。每个可执行程序都拥有该 IAT 区域,程序运行时,PE 装载器会将 Win32 API 的函数地址记录到 IAT 区域,在 EDR 的 hook.dll 注入到程序后,当程序调用到记录在内的函数时,则跳转至 hook.dll(至于是什么函数才跳转,由 EDR 决定)。
  • Inline hooking:是一种通过修改机器码的方式来实现 Hook 的技术。

我们这就讲讲 IAT hooking 就好。

在此示例中,就是简单的调用一个 MessageBoxA 的程序,该程序将会在 Import Address Table 中查找 MessageBoxA 的地址,以便它能够顺利的运行。

我们不知道的是, EDR 参与了其中,其实 EDR 替换了程序中的 IAT 区域内容。在程序调用 MessageBoxA 时,实际上该调用已经被 EDR 强制跳转到它自身 dll 的地址,因此最后是由 EDR 判断传递的数据是否是恶意的,还是正常的。如果是恶意代码,则拦截执行,反之。

在 .NET 中,这个调用过程被称为 Platform Invoking,简称 P/invoke 。该机制允许 .NET 应用程序方位非托管库(DLL)中的数据和 API。通过使用 P/invoke,Csharp 开发人员可以轻松地调用标准 Win32 API。

该过程主要是利用 System.Runtime.InteropServices 命名空间来完成,且由 CLR 管理。下图显示了 P/invoke 中非托管代码与托管代码之间的联系及过程:

P/invoke 调用非托管函数时,它将执行以下操作序列:

1,找到包含函数的 DLL;

2,将 DLL 加载到内存中;

3,在内存中找到该函数的地址,并将其参数压入堆栈,根据需要封送数据;

4,将控制权转移到非托管功能。

P/invoke 会将由非托管函数生成的异常抛出给托管调用方。

但是,利用 .NET 也存在操作上的缺点(第一小节中已经说明)。由于是 CLR 负责将 .NET 翻译成机器代码(语言),而可执行文件并没有直接翻译成这种代码。因此,可执行文件将整个代码库存储它的汇编代码中,因此稍微逆向该可执行文件,就可以看到全部信息。比如以下的一些信息:

0x03 Delegate

现在好多的工具都开始以动态调用/执行的方式进行编写,这是非常有趣的一点,也是非常值得我们去学习。D/invoke 允许我们调用 P/invoke 所使用的 API,但它不是静态导入,而是动态导入。这样子就不会将 Win32 API 地址写入 Import Address Table 中,这就意味着我们完全的绕过了 IAT hooking。所以如果程序是使用了动态调用,我们是无法查看程序的导出表的。

那么,我们怎么实现动态调用呢?与其使用 P/Invoke 导入我们想调用的 API,不如将 DLL 手动加载到内存中。此后,我们会得到一个指向该 DLL 中的一个函数的指针,后续可以在传参的同时从指针中调用该函数。

那么说到指针,不得不说 C# 中的 Delegate(委托)了,该部分内容在第一小节中有讲到。因此我们直接看看具体是怎么实现的。

我们的目的是在内存中调用非托管代码

可以通过 Delegate 的来实现这一点。.NET 包含了 Delegate API,作为在类中包装方法/函数的一种方式。如果你们曾经使用反射来枚举类中的方法,那么你可以自己观察一下,实际上就是一种 Delegate 的形式。

Delegate API 有很多奇妙的功能,比如可以从一个函数的指针实例化一个 Delegate,并在传递参数的同时动态调用该函数。这里主要用的函数是:GetDelegateForFunctionPointer

需要两个参数,分别为:

  • IntPtr ptr:要转换的非托管函数指针。
  • Type t:要返回的委托人的类型,也就是要传入的非托管代码的函数原型。

当看到 Type t 这个类型参数时,可能会不理解。这其实就是操作者传入要调用的非托管代码的函数原型的地方。这可以让 Delegate 知道当它调用函数时如何设置 CPU 寄存器和栈。

如果你记得在 P/Invoke 中,肯定用过类似这样的方式来设置函数

定义一个委托的方式与此类似,可以像定义变量一样定义一个委托。同时还要指定由委托人封装的函数时使用的调用约定(C++的标准调用约定是 StdCall),此处的调用约定务必一致,要不然会出现堆栈被破坏的情况。

一个函数原型就定义完成。

接下来就看看怎么获取函数的指针了。

如果了解一些 PE 结构,可以知道由于所有的程序在初始化运行时,本身都会加载一些模块(库),这些模块是保证程序能正常运行的基本要素。因此可直接在当前进程中查找所需模块,即可获取到基址。实现如下:

在获取模块基址之后,通过遍历模块导出表来解析函数的地址,具体实现,可以在 4.2 章节看到。这里还有一个要注意的问题,那就是如果程序在初始化时,所调用的库并没有在预加载的模块里面,那么上面的代码就不会返回结果。这种情况就需要从磁盘中查找所需 DLL。

基础条件已经满足,因此直接套用即可,这部分内容,TheWover 已经写好了库。

TheWover 写了一篇关于为什么使用 D/invoke 而非 P/invlke 的原因的文章,强烈推荐。并且他还发布了一个 NuGet 包,该库其实就是一个 Delegate 库和函数包装器,目前基本满足平时的工具开发需求,它实现了定义结构及功能,只需要引用即可。

0x04 示例

上面说了 TheWover 发布了他的 DInvoke 项目,这个项目主要是帮我们定义处理对应的结构及功能,否则需要自己去定义对应的结构。如果不想使用这个项目,那么自己手动构造即可,这个后续会说到。

4.1、使用 DInvoke

这里直接使用官方的示例。下面的例子演示了如何使用 DInvoke 动态查找和调用一个 DLL 的函数地址:

  • 获取 ntdll.dll 的基址。当它被初始化时,它被加载到每个 Windows 进程中,所以我们知道它已经被加载了。因此,我们可以搜索 PEB 的加载模块列- 表来找到它的引用。一旦我们从 PEB 中找到了它的基址,我们就输出地址;

  • 给定一个函数名称,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 给定一个函数的序号,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 给定一个函数的 HMACMD5 值,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 在获取 ntdll.dll 的前提下,使用 GetExportAddress 在内存中按给定的函数名查找地址。

再看看官方的另一个例子。在下面的示例中,我们先使用 P/Invoke 调用 OpenProcess。然后,我们再使用 D/Invoke 方式调用它(多种),并证明任何一种动态调用的机制都成功执行了非托管代码且绕过了 API hooking。

为了更好的说明实验结果,我们使用 API Monitor v2 比作 EDR,并钩住 kernel32.dll!OpenProcess,然后通过 API Monitor 运行该示例程序。接下来仔细观察那些用 PROCESS_ALL_ACCESS Flag 的调用,然后根据基址进行校对。结果如下图所示:

结果很明显,P/Invoke 的方式可以成功捕获,但使用 D/Invoke、手动映射和模块重载映射时,未成功捕获。

4.2、手动构造

为了更好的理解动态调用,我们可以尝试手动进行构造,这里我们使用 【知识回顾】进程注入-第一部分 中的代码注入代码,P/Invoke 的方式转由 D/Invoke 调用

编译代码后,同上使用 API Monitor v2 比做 EDR,并且钩完所涉及的 API,它们分别是:

  • kernel32.dll!OpenProcess
  • kernel32.dll!VirtualAllocEx
  • kernel32.dll!WriteProcessMemory
  • kernel32.dll!CreateRemoteThread
  • kernel32.dll!CloseHandle

完全没有被钩住。这就完全绕过了 IAT hooking。这里仅是绕过了 IAT hooking,在实战中,还需要处理 shellcode。

查看导出表情况

后续随着 DInvoke 的完善,直接应用该库即可。这样无需自己定义函数,省时省力。

猜你喜欢

Shiro框架的基本使用及问题解决

Shiro框架的基本使用及问题解决

Java常用API

Java常用API

Java工具类整理

Java工具类整理

脚本分享-打印Spring Mvc容器中的所有接口

脚本分享-打印Spring Mvc容器中的所有接口

简单实现MyBatis

简单实现MyBatis

0 条评论

img 登陆后才能评论哦~