Windows shellcode执行技术入门指南

xiaohui Web安全 2019年9月10日发布
Favorite收藏

导语:本文旨在介绍如何在进程的内存空间中执行shellcode的基本技术,之所以要专门拿出一篇文章来单独讨论这个技术,是因为每天都会出现实现隐身代码执行的新技术。不过要将这些新概念完全理解则没有那么简单。

微信截图_20190817161916.png

本文旨在介绍如何在进程的内存空间中执行shellcode的基本技术,之所以要专门拿出一篇文章来单独讨论这个技术,是因为每天都会出现实现隐身代码执行的新技术。不过要将这些新概念完全理解则没有那么简单。

本文将介绍以下四种执行技术:

1.动态内存分配;

2.函数指针的执行

3..TEXT-Segment执行;

4.RWX-Hunter执行;

其中前两种技术是众所周知的,大多数人应该熟悉这些,然而,后两种技术可能比较新,大多数人并不是很熟悉。

这些技术都描述了在不同内存部分中执行代码的方法,因此有必要先说一说进程内存布局。

进程内存布局

首先需要理解的概念是,整个虚拟内存空间分为两个相关部分:为用户进程预留的虚拟内存空间(用户空间)和为系统进程预留的虚拟内存空间(内核空间),如下图所示:

1.png

具体的可视化过程,请阅读Microsoft的详细描述

首先,每个进程都有自己的私有虚拟地址空间,其中“内核空间”是一种“共享环境”,这意味着每个内核进程可以在任何它想要的地方读写虚拟内存。请注意,“共享环境”只适用于没有基于虚拟化的安全性(VBS)的环境,但这是另一个话题了,不属于本文的讨论范围。

上表示显示了全局虚拟地址空间的样式,现在让我们把该样式转化成一个进程。

2.png

可以看出,单个进程的虚拟内存空间由多个部分组成,这些部分通过地址空间布局随机化(ASLR)放置在可用空间边界内的某个位置。这些部分行内的人应该很熟悉了,但为了让每个人都处于同一理解水平线上,我列出了这些部分的概要,方便你了解:

.text段是程序代码段:这是可执行进程映像所在的位置,在这个区域中,你将找到可执行文件的主条目,即执行流开始的地方。

.data段:.data部分包含全局初始化或静态变量。任何没有绑定到特定函数的变量都存储在这里。

.bss段:与.data段类似,此段包含任何未初始化的全局变量或静态变量。

HEAP(堆):这是存储所有动态局部变量的地方,每次创建在运行时确定所需空间的对象时,都会在HEAP中动态分配所需的地址空间(通常使用alloc()或类似的系统调用)。

STACK(栈):栈是分配给每个静态局部变量的位置,如果在函数中局部初始化一个变量,该变量将被放在STACK上。

动态分配内存

了解了基本知识后,让我们看看在进程内存空间中执行shellcode需要什么。为了执行你的shellcode,你需要完成以下三个检查:

1.是否需要标记为可执行的虚拟地址空间,否则DEP将抛出异常;

2.是否需要将shellcode放入该地址空间;

3.是否需要将代码流定向到该内存区域;

完成这三个步骤的方法是使用WinAPI调用动态分配可读、可写和可执行(RWX)内存,并启动一个指向新分配内存区域的线程。用C语言编写这个代码应该是以下这样的:

#include <windows.h>
int main()
{
char shellcode[] = "\xcc\xcc\xcc\xcc\x41\x41\x41\x41";
// Alloc memory
LPVOID addressPointer = VirtualAlloc(NULL, sizeof(shellcode), 0x3000, 0x40);
// Copy shellcode
RtlMoveMemory(addressPointer, shellcode, sizeof(shellcode));
// Create thread pointing to shellcode address
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addressPointer, NULL, 0, 0);
// Sleep for a second to wait for the thread
Sleep(1000);
return 0;
}

正如以下屏幕截图所示,在编译和执行上述代码时,shellcode将从堆中执行,默认情况下受到Windows XP中引入的系统范围数据执行保护(DEP)策略的保护,详细过程,请点此阅读。对于启用DEP的进程,这将防止在此内存区域执行代码。为了克服这个障碍,我们要求系统将所需的内存区域标记为RWX。这是通过将VirtualAlloc的最后一个参数指定为0x40来实现的,这就相当于PAGE_EXECUTE_READWRITE。至于具体原因,请参考此文

到目前为止一切顺利,但是这些代码在内存中的表现如何呢?为了分析这一点,我们将使用WinDbg。Windows 调试器 (WinDbg) 可用于调试内核模式和用户模式代码,来分析故障转储,并检查代码时 CPU 寄存器执行,详情请点此了解,如果你以前从未设置过WinDbg,请参考下面的截图,了解如何将WinDbg指向源代码,列出所有已加载的模块,设置断点并运行程序。

4.jpg

在WinDbg命令行中输入“g”后,程序将进入可执行文件的主函数。如果在调用RtlMoveMemory之后逐步执行代码,就会在WinDbg中遇到如下情况。

5.jpg

如紫线标识的地方,我们当前正处于调用RtlMoveMemory之后。如果我们参考上面的代码,RtlMoveMemory将从VirtualAlloc获取一个指针,将shellcode写入给定的位置。由于从VirtualAlloc返回的指针是RtlMoveMemory的第一个参数,因为函数参数是按相反的顺序被推送到栈上的,所以在调用函数之前,它将最后被推送到栈上(在寄存器ecx中)。如果我们在调用RtlMoveMemory之前就停止了,则ecx寄存器将显示地址位置为‘0x420000’,在上面的屏幕截图中,该地址位置在WinAPI调用之后被放置到eax寄存器中。

检查上面屏幕截图中地址0x420000处的内存位置,显示我们的shellcode已放置在此地址。此外,请注意栈基址(ebp)显示为0x5afa34,栈指针(esp -栈的顶部地址)指向0x5af938,跨越此范围内的地址栈。由于shellcode的内存位置不在栈范围内,我们可以得出结论,它已被放置在堆上。

小结

WinAPI系统调用用于在堆中动态分配RWX内存,将shellcode移动到新分配的内存区域,并启动一个新的执行线程。

优点

1.使用WinAPI调用是执行代码的标准方法,非常可靠。

2.分配的内存区域不仅是可执行的,而且是可写和可读的,这允许在这个内存区域内修改shellcode,并对shellcode编码或加密。

缺点

WinAPI调用的使用很容易被成熟的AV/EDR系统检测到。

函数指针的执行

与上面的普通方法不同,在内存中执行shellcode的另一种技术是使用函数指针,如下面的代码片段所示:

#include <windows.h>
int main()
{
char buf[] = "\xcc\xcc\xcc\xcc";
// One way to do it
int (*func)();
func = (int (*)()) (void*)buf;
(int)(*func)();
              // Shortcut way to do it
// (*(int(*)()) buf)();
// sleep for a second
Sleep(1000);
              return 0;
}

此代码的工作方式如下:

1.声明了一个函数的指针,在上面的代码片段中,函数指针被命名为'func';

2.声明的函数指针被分配了要执行的代码的地址(因为任何变量将被赋值,所以func指针也被赋予地址);

3.最后调用函数指针,这意味着执行流程已经被定向到指定的地址。

应用与上面相同的步骤,我们可以使用WinDbg在内存中对此进行分析,这将执行以下操作:

7.jpg

在这种情况下,导致代码执行的关键步骤如下:

1.包含在局部变量中的shellcode在初始化期间进入栈(比较接近ebp,因为这是main-method中首先发生的事情之一);

2.shellcode从栈加载到eax中,如地址0x00fd1753所示;

3.shellcode通过调用eax来执行,如地址0x00fd1758所示;

返回参考上面所示的单个过程的虚拟存储器布局,声明栈仅标记为关于DEP的RW存储器部分。同样的问题发生在堆内存的动态分配中,在这种情况下,使用WinAPI函数(VirtualAlloc)将内存部分标记为可执行文件。在本文的示例中,我们没有使用任何WinAPI函数,但幸运的是,我们可以通过设置/NXCOMPAT:NO标志来简单地为已编译的可执行文件禁用DEP(对于VisualStudio,可以在高级链接器选项中设置)。这样,shellcode就会被顺利的执行。

小结

函数指针用于调用shellcode,在栈上作为局部变量分配。

优点

1.没有使用WinAPI调用,这可以用来避免AV/EDR检测。

2.栈是可写和可读的,这允许在这个内存区域内修改shellcode,从而对shellcode编码或加密。

缺点

默认情况下,DEP会阻止栈中的代码执行,这就需要在没有DEP支持的情况下编译代码。另外,系统范围的DEP强制执行将阻止代码执行。

.TEXT段执行

到目前为止,我们已经在堆和栈中实现了代码执行。由于这些代码在默认情况下都不可执行,因此我们需要使用WinAPI函数并分别禁用DEP来克服这个问题。

我们可以避免在已经标记为可执行的内存区域中执行代码时使用这种方法,对上面内存布局的快速引用显示. text段就是这样一个内存区域。

.TEXT段需要是可执行的,因为这是包含可执行代码的部分,例如主函数。

从理论上来说,这是一个执行shellcode的合适位置,但是我们如何在.TEXT段中放置和执行shellcode呢?很明显,我们不能使用WinAPI函数简单地将shellcode移到这里,因为. text段是不可写的,而且我们不能还使用函数指针,因为这里没有指向的引用。

因此,解决方案只能是内联汇编(inline assembly)中。

@MrUn1k0d3r在这里展示了这种技术的实现过程,下面是一个精简的代码段:

#include <Windows.h>
int main() {
asm(".byte 0xde,0xad,0xbe,0xef,0x00\n\t"
"ret\n\t");
return 0;
}

要编译此代码,由于使用了“.byte”指令,因此需要GCC编译器。幸运的是,MinGW项目中包含一个GCC编译器,MinGW,是Minimalist GNUfor Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。MinGW 是一组包含文件和端口库,其功能是允许控制台模式的程序使用微软的标准C运行时(C Runtime)库(MSVCRT.DLL),该库在所有的 NT OS 上有效,在所有的 Windows 95发行版以上的 Windows OS 有效,使用基本运行时,你可以使用 GCC 写控制台模式的符合美国标准化组织(ANSI)程序,可以使用微软提供的 C 运行时(C Runtime)扩展,与基本运行时相结合,就可以有充分的权利既使用 CRT(C Runtime)又使用 WindowsAPI功能。我们可以按如下方式轻松编译:

mingw32-gcc.exe -c Main.c -o Main.o
mingw32-g++.exe -o Main.exe Main.o

在IDA中查看此内容后,我们发现shellcode已嵌入到.TEXT段中:

9.jpg

被定义的shellcode'0xdeadbeef'在调用__main之后就被置于汇编代码中,__ main用作初始化例程。一旦__main函数完成初始化,我们就立即执行shellcode。

小结

内联程序集用于将shellcode嵌入可执行程序的. text段中。

优点

没有使用WinAPI调用,这可以用来避免AV/EDR检测。

缺点

.TEXT段不可写,因此不能使用任何shellcode编码器或加密器。

因此,如果不进行定义,AV/EDR很容易检测到这种恶意shellcode。

RWX-Hunter执行

最后,在使用默认的可执行.TEXT段进行代码执行并使用WinAPI函数创建非默认的可执行内存部分并禁用DEP之后,还有最后一条路径,即搜索已被标记为读(R),写(W)和可执行(X)的内存部分。详细的思路,请阅读这个帖子

10.jpg

RWX-Hunter的基本思路是遍历进程的虚拟内存空间,搜索标记为RWX的内存部分。

细心的读者可能会注意到,这只完成了我最初设置的代码执行的步骤的1/3,即:查找可执行内存。至于如何将shellcode放入这个内存区域,以及如何将代码流程定向到该内存区域,第一步并没有提到。

需要回答的第一个问题是搜索RWX内存部分的范围,让我们再次回到进程私有虚拟内存空间的初始描述,这里声明进程内存空间的范围是从0x00000000到0x7FFFFFFFF,所以这应该是搜索范围。

long MaxAddress = 0x7fffffff;
long address = 0;
do
{
MEMORY_BASIC_INFORMATION m;
int result = VirtualQueryEx(process, (LPVOID)address, &m, sizeof(MEMORY_BASIC_INFORMATION));
if (m.AllocationProtect == PAGE_EXECUTE_READWRITE)
{
printf("YAAY - RWX found at 0x%x\n", m.BaseAddress);
return m.BaseAddress;
}
if (address == (long)m.BaseAddress + (long)m.RegionSize)
break;
address = (long)m.BaseAddress + (long)m.RegionSize;
} while (address <= MaxAddress);

这个方法非常直接地实现了我们想要实现的目标。搜索处理专用虚拟存储器空间(用户区域虚拟存储器空间)以寻找用PAGE_EXECUTE_READWRITE标记的存储器部分,其再次映射到0x40,如先前示例中所示。如果找到该空间,则返回该空间,否则将下一个搜索地址设置为下一个存储区域(基地址+内存区域)。

为了完成代码执行,你需要将shellcode移动到找到的内存区域并执行。如第一种技术所示,一种简单的方法是返回到WinAPI调用,但是这种方法的缺点你也要考虑进来。在这篇文章的最后,我将分享一些有用的PoC,以参考如何实现这一步。

小结

在进程内存空间中搜索可读、可写和可执行(RWX)内存部分,以避免动态创建此类内存。

优点

避免了对VirtuallAlloc/VirtuallAllocEx的调用,并且利用过程不会动态创建RWX内存。

缺点

需要很强的网络安全知识来避免WinAPI调用将shellcode和代码流重定向到放置的shellcode。

PoC

完整PoC,请点此

本文翻译自:https://www.contextis.com/en/blog/a-beginners-guide-to-windows-shellcode-execution-techniques如若转载,请注明原文地址: https://www.4hou.com/web/19758.html
点赞 3
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论