无可执行权限加载 ShellCode 技术原理
Last Update:
介绍
无需解密,无需 X 内存,直接加载运行 R 内存中的 ShellCode 密文。
x64 项目: https://github.com/HackerCalico/No_X_Memory_ShellCode_Loader
规避了以下特征:
(1) 申请 RWX 属性的内存。
(2) 来回修改 W 和 X 的内存属性。
(3) 内存中出现 ShellCode 特征码。
常规 ShellCode 加载器
在大家刚开始学习 ShellCode 的时候,通常不明白 ShellCode 本身是什么,而是仅仅学习了以下加载器的写法:
1 |
|
上述加载器直接将 ShellCode 密文写入 RWX (可读可写可执行) 内存解密,进而调用。此时进程内存中出现了少见且敏感的 RWX 内存空间,容易被查杀。
为了避免使用 RWX 内存属性,大家开始先将 ShellCode 密文写入 RW 内存解密,再将内存属性改为 RX 运行。如果 Hook CS 直接生成的后门程序,就会发现在执行一些敏感功能时,后门采取了这种来回修改内存属性的操作,容易被行为查杀。
于是我开始思考是否存在完全规避以上问题的方法。
ShellCode 作用原理
为了找到新的 ShellCode 加载方式,我决定深入了解 ShellCode。
ShellCode 是一段地址无关机器码。机器码就是代码对应的汇编指令的硬编码,通常存在于程序文件的 .text 段中,比如以下 MyMessageBoxA_Not 函数:
该函数的硬编码与汇编指令:
1 |
|
可以看到通过 Call 指令调用 MessageBoxA 这个 Windows API,但是很明显 MessageBoxA 的地址存储在其他位置,所以如果单独运行这段机器码会运行失败。
ShellCode 地址无关,意味着不直接使用这种外部的地址。实现的方法是,在写代码的过程中不直接调用 Windows API,而是主动获取 Windows API 的地址进行调用,比如以下 MyMessageBoxA 函数:
1 |
|
该函数使用的 MessageBoxA 的地址通过参数从外部传入,所以该函数的机器码可以作为 ShellCode 直接运行。
新型加载器的实现分析
通过对 ShellCode 深入了解,可以知道 ShellCode 其实就是按照地址无关标准编写的代码对应的汇编指令的硬编码,而汇编指令与硬编码是相对应的。
所以可以说,运行 ShellCode 就是运行其汇编指令,只要实现了其汇编指令的等效功能,就是实现了 ShellCode 的等效运行。
于是当前的研究转化为其汇编指令实现了什么功能。
通过学习汇编语言,可以知道这些汇编指令简单来说就是不断修改寄存器、栈、内存的值,通过不断的修改构造好调用 Windows API 所需的参数,进而成功调用 Windows API。
函数参数的构造过程可以通过上文的 MyMessageBoxA 来简单解释,该函数通过以下代码调用:
1 |
|
该行代码实际上就构造好了函数的参数,其汇编指令:
1 |
|
汇编指令将 MessageBoxA 的地址放入了 RCX 寄存器,这就是一个简单的构造过程。复杂的过程比如要对字符串循环解密等,可以统一认为是构造函数参数的过程。
于是当前的研究转化为如何用其他办法构建好 Windows API 的参数来调用。
我想到的办法是实现汇编指令的解释器。解释器是一种逐行对代码进行词法、语法、语义等分析进行运行的程序。
只要我传入汇编指令的文本,解释器逐条指令解析实现对应的功能即可。这里涉及到几个问题。比如解释到 mov rsp, 0x00,此时不应该将真实 RSP 寄存器的值改为 0x00,这样会导致解释器本身错误。解决办法是实现虚拟寄存器和虚拟栈,将虚拟的 vtRSP 改为 0x00。在解释 Windows API 的调用指令时,先将虚拟寄存器的值覆盖真实寄存器,此时 Windows API 的参数为构造完整的状态,之后直接调用 Windows API 即可成功。
下面以 MyMessageBoxA 为例演示解释过程:
该函数的汇编指令:
1 |
|
模拟解释器:
以下代码忽略了汇编指令的解析过程,直接模拟每条指令对虚拟值修改进而构造好 Windows API 的参数,将虚拟值覆盖真实值后成功调用 Windows API。
注:需要配置 Clang 环境以支持 x64 内联汇编。
Visual Studio Installer ——> 单个组件 ——> LLVM (clang-cl) + Clang ——> 安装
Visual Studio ——> 项目属性 ——> 常规 ——> 平台工具集 (LLVM (clang-cl))
1 |
|