前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >无可执行权限加载 ShellCode 技术原理

无可执行权限加载 ShellCode 技术原理

作者头像
红队蓝军
发布2024-06-17 18:57:27
1040
发布2024-06-17 18:57:27
举报
文章被收录于专栏:红队蓝军

1. 介绍

无需解密,无需 X 内存,直接加载运行 R 内存中的 ShellCode 密文。

x64 项目: https://github.com/HackerCalico/No_X_Memory_ShellCode_Loader

2. 常规 ShellCode 加载器

在大家刚开始学习 ShellCode 的时候,通常不明白 ShellCode 本身是什么,而是仅仅学习了以下加载器的写法:

代码语言:javascript
复制
unsigned char buf[] = "ShellCode 密文";

void* p = VirtualAlloc(NULL, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, buf, sizeof buf);

// ShellCode 解密

((void(*)())p)();

上述加载器直接将 ShellCode 密文写入 RWX (可读可写可执行) 内存解密,进而调用。此时进程内存中出现了少见且敏感的 RWX 内存空间,容易被查杀。

为了避免使用 RWX 内存属性,大家开始先将 ShellCode 密文写入 RW 内存解密,再将内存属性改为 RX 运行。如果 Hook CS 直接生成的后门程序,就会发现在执行一些敏感功能时,后门采取了这种来回修改内存属性的操作,容易被行为查杀。

于是我开始思考是否存在完全规避以上问题的方法。

3. ShellCode 作用原理

为了找到新的 ShellCode 加载方式,我决定深入了解 ShellCode。

ShellCode 是一段地址无关机器码。机器码就是代码对应的汇编指令的硬编码,通常存在于程序文件的 .text 段中,比如以下 MyMessageBoxA_Not 函数:

该函数的硬编码与汇编指令:

代码语言:javascript
复制
48 83 EC 38       ------> SUB RSP, 0X38
C6 44 24 20 00    ------> MOV BYTE PTR [RSP + 0X20], 0
41 B9 40 00 00 00 ------> MOV R9D, 0X40
4C 8D 44 24 20    ------> LEA R8, [RSP + 0X20]
48 8D 54 24 20    ------> LEA RDX, [RSP + 0X20]
33 C9             ------> XOR ECX, ECX
FF 15 2F 12 00 00 ------> CALL QWORD PTR [RIP + 0X122F]
48 83 C4 38       ------> ADD RSP, 0X38
C3                ------> RET

可以看到通过 Call 指令调用 MessageBoxA 这个 Windows API,但是很明显 MessageBoxA 的地址存储在其他位置,所以如果单独运行这段机器码会运行失败。

ShellCode 地址无关,意味着不直接使用这种外部的地址。实现的方法是,在写代码的过程中不直接调用 Windows API,而是主动获取 Windows API 的地址进行调用,比如以下 MyMessageBoxA 函数:

代码语言:javascript
复制
typedef int(WINAPI* pMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);

#pragma code_seg(".shell")

void MyMessageBoxA(pMessageBoxA funcMessageBoxA) {
    char text[] = { '\0' };
    funcMessageBoxA(0, text, text, MB_ICONINFORMATION);
}

#pragma code_seg(".text")

int main() {
    MyMessageBoxA(MessageBoxA);
}

该函数使用的 MessageBoxA 的地址通过参数从外部传入,所以该函数的机器码可以作为 ShellCode 直接运行。

4. 新型加载器的实现分析

通过对 ShellCode 深入了解,可以知道 ShellCode 其实就是按照地址无关标准编写的代码对应的汇编指令的硬编码,而汇编指令与硬编码是相对应的。

所以可以说,运行 ShellCode 就是运行其汇编指令,只要实现了其汇编指令的等效功能,就是实现了 ShellCode 的等效运行。

于是当前的研究转化为其汇编指令实现了什么功能。

通过学习汇编语言,可以知道这些汇编指令简单来说就是不断修改寄存器、栈、内存的值,通过不断的修改构造好调用 Windows API 所需的参数,进而成功调用 Windows API。

函数参数的构造过程可以通过上文的 MyMessageBoxA 来简单解释,该函数通过以下代码调用:

代码语言:javascript
复制
MyMessageBoxA(MessageBoxA)

该行代码实际上就构造好了函数的参数,其汇编指令:

代码语言:javascript
复制
mov rcx,qword ptr [__imp_MessageBoxA]
call MyMessageBoxA

汇编指令将 MessageBoxA 的地址放入了 RCX 寄存器,这就是一个简单的构造过程。复杂的过程比如要对字符串循环解密等,可以统一认为是构造函数参数的过程。

于是当前的研究转化为如何用其他办法构建好 Windows API 的参数来调用。

我想到的办法是实现汇编指令的解释器。解释器是一种逐行对代码进行词法、语法、语义等分析进行运行的程序。

只要我传入汇编指令的文本,解释器逐条指令解析实现对应的功能即可。这里涉及到几个问题。比如解释到 mov rsp, 0x00,此时不应该将真实 RSP 寄存器的值改为 0x00,这样会导致解释器本身错误。解决办法是实现虚拟寄存器和虚拟栈,将虚拟的 vtRSP 改为 0x00。在解释 Windows API 的调用指令时,先将虚拟寄存器的值覆盖真实寄存器,此时 Windows API 的参数为构造完整的状态,之后直接调用 Windows API 即可成功。

下面以 MyMessageBoxA 为例演示解释过程:

该函数的汇编指令:

代码语言:javascript
复制
MOV QWORD PTR [RSP + 8], RCX
SUB RSP, 0X38
MOV BYTE PTR [RSP + 0X20], 0
MOV R9D, 0X40
LEA R8, [RSP + 0X20]
LEA RDX, [RSP + 0X20]
XOR ECX, ECX
CALL QWORD PTR [RSP + 0X40]
ADD RSP, 0X38
RET

模拟解释器:

以下代码忽略了汇编指令的解析过程,直接模拟每条指令对虚拟值修改进而构造好 Windows API 的参数,将虚拟值覆盖真实值后成功调用 Windows API。

注:需要配置 Clang 环境以支持 x64 内联汇编。

Visual Studio Installer ------> 单个组件 ------> LLVM (clang-cl) + Clang ------> 安装

Visual Studio ------> 项目属性 ------> 常规 ------> 平台工具集 (LLVM (clang-cl))

代码语言:javascript
复制
// 虚拟栈
PVOID vtStack = malloc(0x10000);
// 虚拟栈顶
DWORD64 vtRSP = (DWORD64)vtStack + 0x9000;

// mov rcx,qword ptr [__imp_MessageBoxA]
DWORD64 vtRCX = (DWORD64)MessageBoxA;
// call MyMessageBoxA
vtRSP -= 8;

// MOV QWORD PTR [RSP + 8], RCX
*(PDWORD64)(vtRSP + 8) = vtRCX;
// SUB RSP, 0x38
vtRSP -= 0x38;
// MOV BYTE PTR [RSP + 0x20], 0
*(PBYTE)(vtRSP + 0x20) = 0;
// MOV R9D, 0x40
DWORD64 vtR9 = 0x40;
// LEA R8, [RSP + 0x20]
DWORD64 vtR8 = vtRSP + 0x20;
// LEA RDX, [RSP + 0x20]
DWORD64 vtRDX = vtRSP + 0x20;
// XOR ECX, ECX
vtRCX ^= vtRCX;

// 虚拟寄存器 覆盖 真实寄存器
__asm {
    mov rcx, vtRCX
    mov rdx, vtRDX
    mov r8, vtR8
    mov r9, vtR9
    mov rsp, vtRSP
}

// CALL QWORD PTR [RSP + 0x40]
__asm {
    call qword ptr[rsp + 0x40]
}

// ADD RSP, 0x38
vtRSP += 0x38;

// RET
vtRSP += 8;
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 红队蓝军 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 介绍
  • 2. 常规 ShellCode 加载器
  • 3. ShellCode 作用原理
  • 4. 新型加载器的实现分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档