在Windows上通过系统调用Syscall实现Shellcode注入

在Windows上通过系统调用Syscall实现Shellcode注入

在通过Sektor7 Malware Development Essentials课程学习了如何用C编写shellcode注入器之后,我想学习如何在C#中执行相同的操作。使用P / Invoke来运行类似的Win32 API调用,编写一个与Sektor7相似的简单注入器非常容易。我注意到的最大区别是,没有直接等效的方法来混淆API调用。在对BloodHound Slack频道进行了一些研究和一些问题之后(感谢@TheWover@NotoriousRebel!),我发现我可以研究两个主要选项。一种是使用本机Windows系统调用(即AKA syscalls),或使用动态调用。每个人都有其优点和缺点,在这种情况下,系统调用的最大优点是杰克·哈隆(这里这里)和badBounty解释和演示它们的出色工作。这篇文章和POC的大部分内容来自他们在该主题上的出色工作。我知道TheWover和Ruben Boonen正在D / Invoke 方面做一些工作,我计划在下一个方面进行研究。

我想提一下,这篇文章的主要目的是作为证明这一概念的文档,并阐明我自己的理解。因此,尽管我已尽最大努力确保此处的信息准确无误,但并不能保证一定是100%。但是,至少代码可以正常工作。

所说的工作代码在这里可用

本机API和Win32 API

首先,我想介绍为什么我们首先要使用syscalls。答案是由AV / EDR产品执行的API挂钩。这是一种防御性产品,用于在执行Win32 API调用之前对其进行检查,确定它们是否可疑/恶意以及阻止或允许调用继续进行,以使用该技术。这是通过稍微更改常用滥用API调用的程序集来完成的, 以跳转到AV / EDR控制的代码,然后在此处进行检查,并假设允许该调用,跳回到原始API调用的代码。例如,和CreateThreadCreateRemoteThread将Shellcode注入本地或远程进程时,通常使用Win32 API。实际上,我CreateThread很快将在使用严格Win32 API的注入演示中使用。这些API在Windows DLL文件中定义,在这种情况下, MSDN在中告诉我们Kernel32.dll。这些是用户模式的DLL,这意味着它们可以被正在运行的用户应用程序访问,并且它们实际上并不直接与操作系统或CPU交互。Win32 API本质上是Windows 本机API之上的抽象层。这个API被认为是内核模式,因为这些API更接近于操作系统和底层硬件。实际上,有比实际执行内核模式功能低的级别,但是这些级别不会直接公开。本机API是仍可由用户应用程序公开和访问的最低级别,它充当用户代码和操作系统之间的一种桥梁或胶合层。这是一个很好的外观图:

您可以看到Kernell32.dll,尽管名称具有误导性,但它位于比更高的级别上ntdll.dll,后者位于用户模式和内核模式之间的边界。

那么为什么Win32 API存在呢?存在的一个重要原因是调用本地API。当您调用Win32 API时,它将依次调用本机API函数,该函数将边界越过进入内核模式。用户模式代码从不直接接触硬件或操作系统。因此,它能够访问较低级别功能的方法是通过本机PI。但是,如果本机API仍必须调用较低级别的API,为什么不直接使用本机API并减少额外的步骤呢?一种答案是,Microsoft可以在不影响用户模式应用程序代码的情况下更改本机API。实际上,本机API中的特定功能 通常Windows版本之间确实会进行更改,但是更改不会影响用户模式代码,因为Win32 API保持不变。

那么,如果我们只想注入一些shellcode,为什么所有这些层,级别和API对我们都很重要?Win32 API与本机API之间的主要区别是AV / EDR产品可以挂接Win32调用,但不能挂接本机调用。这是因为本机调用被视为内核模式,并且用户代码无法对其进行更改。有一些例外情况,例如驱动程序,但不适用于本帖子。最大的收获是,防御者无法挂接本地API调用,而我们仍然可以自己调用它们。这样,我们就可以通过防御性产品在没有相同可见性的情况下实现相同的功能。这是系统调用的基本价值。

系统调用

本机API调用的另一个名称是系统调用。与Linux类似,每个系统调用都有一个代表它的特定数字。该数字表示系统服务调度表(SSDT)中的一个条目,该表是内核中的一个表,其中包含对各种内核级功能的各种引用。每个命名的本机API都有一个匹配的syscall编号,该编号具有相应的SSDT条目。为了利用系统调用,仅知道API的名称是不够的,例如NtCreateThread。我们也必须知道它的系统调用号。我们还需要知道我们的代码将在哪个版本的Windows上运行,因为系统调用号可能并且很可能会在两个版本之间发生变化。有两种找到这些数字的方法,一种简单,一种涉及可怕的调试器。

第一种简便的方法是使用Mateusz“ j00ru” Jurczyk创建的便捷的Windows系统调用表。假设您已经知道要查找的API,这将使查找所需的syscall编号变得非常简单(稍后会详细介绍)。

WinDbg

查找系统调用号的第二种方法是直接在源中查找它们ntdll.dll。我们需要对喷油器进行的第一个系统调用是NtAllocateVirtualMemory。因此,我们可以启动WinDbg并在中查找NtAllocateVirtualMemory功能ntdll.dll。这比听起来容易得多。首先,我打开一个目标进程进行调试。哪个进程都没有关系,因为基本上所有进程都将映射ntdll.dll。在这种情况下,我使用了不错的旧记事本。

我们附加到记事本过程,然后在命令提示符下输入x ntdll!NtAllocateVirtualMemory。这使我们可以检查DLL中的NtAllocateVirtualMemory功能。它返回该函数的内存位置,我们可以使用以下命令检查或反汇编该函数:ntdll.dll u

NtAllocateVirtualMemory未汇编

现在我们可以看到有关调用的确切汇编语言说明NtAllocateVirtualMemory。在汇编中调用syscall倾向于遵循一种模式,即在堆栈上设置一些自变量,如mov r10,rcx语句所示,然后将syscall号移到eax寄存器中,如所示mov eax,18heaxsyscall指令用于每个系统调用的寄存器。因此,现在我们知道syscall的NtAllocateVirtualMemory十六进制数字为18,恰好与Mateusz表格中列出的值相同!到目前为止,一切都很好。我们再重复两次,一次为NtCreateThreadEx,一次为NtWaitForSingleObject

查找NtCreateThreadEx的系统调用号
查找NtWaitForSingleObject的系统调用号

您从哪里获得这些本机功能?

到目前为止,查找本机API调用的syscall编号的过程非常简单。但是到目前为止,我遗漏了一个关键信息:如何知道我需要哪些系统调用?我这样做的方法是在C#中使用Win32 API调用(在此帖子的Github存储库中包含的名为Win32Injector)中使用一个基本运行的Shellcode注入器,并为每个Win32 API调用找到相应的syscall 。这是Win32Injector的代码:

这是一个准系统的shellcode注入器,它执行一些shellcode来显示一个弹出框:

从代码中可以看到,通过P / Invoke使用的三个主要Win32 API调用分别是VirtualAllocCreateThreadWaitForSingleObject,它们为我们的shellcode分配内存,创建一个指向我们的shellcode的线程,并分别启动该线程。由于它们是正常的Win32 API,因此它们各自具有有关MSDN的全面文档。但是由于本机API被认为是未记录的,因此我们可能不得不寻找其他地方。我找不到API文档的真实来源,但是通过一些搜索,我能够找到我需要的一切。

在这种情况下VirtualAlloc,一些简单的搜索显示底层的本机API是NtAllocateVirtualMemory,实际上已在MSDN上进行了记录。一下来,二去。

不幸的是,没有MSDN文档NtCreateThreadEx,这是本机API的CreateThread。幸运的是,badBounty的directInjectorPOC具有可用的函数定义,并且已经在C#中。这个项目提供了巨大的帮助,因此对badBounty表示敬意!

最后,我需要查找的文档NtWaitForSingleObject,您可能会猜到它是由调用的本机APIWaitForSingleObject。您会注意到一个主题,其中许多本机API调用以“ Nt”开头,这使得从Win32调用映射它们变得更加容易。您可能还会看到前缀“ Zw”,这也是一个本机API调用,但通常是从内核调用的。这些有时是相同的,如果你这样做,你会看到x ntdll!ZwWaitForSingleObjectx ntdll!NtWaitForSingleObject在WinDbg中。同样,由于ZwWaitForSingleObject已在MSDN上进行了记录,因此我们对使用此API感到幸运。

我想指出一些其他有用的信息源,用于将Win32映射到本机API调用。首先是ReactOS的源代码,它是Windows的开源重新实现。他们代码库的Github镜像有很多您可以搜索的syscall。接下来是 jthuraisamy的SysWhispers。这是一个旨在帮助您查找和实现syscall的项目。这里真的很好。最后,工具API Monitor。您可以运行一个过程,并观察什么叫API,它们的参数以及更多其他内容。我并没有大量使用它,因为我只需要3个syscall,而且查找现有文档的速度更快,但是我可以看到此工具在较大的项目中将多么有用。我相信Sysinternals的ProcMon具有类似的功能,但是我并没有对其进行太多测试。

好的,所以我们将Win32 API映射到了系统调用。让我们写一些C#!

但是这些文档都是针对C / C ++的!那不是那个集会吗…

等等,这些文档都具有C / C ++实现。我们如何将它们转换为C#?答案是封送。这是P / Invoke的本质。封送处理是一种利用非托管代码(例如C / C ++)并在托管上下文(即C#)中使用的方法。对于Win32 API,可以通过P / Invoke轻松完成。只需导入DLL,并在pinvoke.net的帮助下指定函数定义,就可以开始比赛了。您可以在Win32Injector的演示代码中看到这一点。但是,由于syscall没有记录,因此Microsoft不提供与之交互的简便方法。但是通过代理人的魔力,确实有可能。杰克·哈隆(Jack Halon)在这里这里都很好地代表了代表,因此在这篇文章中我不会太深入。我建议阅读这些文章以更好地理解它们,以及通常使用syscall的过程。但是为了完整起见,委托实际上是函数指针,它使我们可以将函数作为参数传递给其他函数。我们在这里使用它们的方式是定义一个委托,其返回类型和函数签名与我们要使用的syscall相匹配。我们使用封送处理来确保C / C ++数据类型与C#兼容,定义一个实现syscall的函数,包括其所有参数和返回类型,就在那里!

不完全的。我们实际上不能调用本地API,因为我们唯一的实现是在汇编中!我们知道它的函数定义和参数,但实际上不能像使用Win32 API一样直接调用它。组装对我们来说很好。再说一次,用C / C ++执行汇编相当简单,但是C#有点难。幸运的是,我们有办法做到这一点,而且我们已经有了WinDbg冒险中的程序集。不用担心,您真的不需要了解汇编程序就可以使用syscalls。这是NtAllocateVirtualMemorysyscall 的程序集:

从注释中可以看出,我们正在堆栈上设置一些参数,将系统调用号移入eax寄存器,并使用magic syscall运算符。在足够低的级别上,这只是一个函数调用。还记得委派如何只是函数指针吗?希望这开始变得有意义。为了调用本机API,我们需要获得一个指向该程序集的函数指针以及一些C / C ++兼容格式的参数。

放在一起

因此,我们现在差不多完成了。我们有系统调用,它们的编号,调用它们的程序集以及在委托中调用它们的方法。让我们看看它在C#中的实际外观:

从顶部开始,我们可以看到的C / C ++定义NtAllocateVirtualMemory以及syscall本身的程序集。从第38行开始,我们有了的C#定义NtAllocateVirtualMemory。请注意,要使C#中的每种类型与非托管类型相匹配,可能需要花一些试验和错误。我们在一个unsafe块内创建一个指向程序集的指针。这使我们能够在C#中执行操作,例如对原始内存执行操作,这些操作通常在托管代码中是不安全的。我们还使用fixed关键字,以确保C#垃圾收集器不会无意间移动内存并更改指针。一旦有了指向shellcode内存位置的原始指针,就需要将其内存保护更改为可执行文件,以便可以直接运行它,因为它将是函数指针,而不仅仅是数据。请注意,我正在使用Win32 API VirtualProtectEx更改内存保护。我不知道通过syscall进行此操作的方法,因为它有点像鸡和蛋的问题,需要获取内存可执行文件才能运行syscall。如果有人知道如何在C#中执行此操作,请联系!这里要注意的另一件事是,将内存设置为RWX通常有点可疑,但这是一个POC,目前我还不太担心。我们现在关心的是钩子,而不是内存扫描!

现在来了魔术。这是我们的代表被声明的结构:

请注意,委托定义只是函数签名和返回类型。只要实现与委托定义匹配,就由我们决定,这是我们在C#NtAllocateVirtualMemory函数中实现的。在上面的第65行,我们创建了一个名为的委托assembledFunction,该委托利用了特殊的封送处理功能Marshal.GetDelegateForFunctionPointer。这种方法使我们可以从函数指针获取委托。在这种情况下,我们的函数指针是指向的系统调用程序集的指针memoryAddressassembledFunction现在是指向汇编语言函数的函数指针,这意味着我们现在可以执行系统调用!我们可以assembledFunction像调用任何普通函数一样使用完整的参数来调用 委托,然后将获得NtAllocateVirtualMemorysyscall 的结果。因此,在我们的退货声明中,我们称assembledFunction带有传入的参数并返回结果。让我们看一下在以下位置实际调用此函数的位置Program.cs

在这里您可以看到我们进行了调用,NtAllocateMemory而不是Win32Injector使用的Win32 API VirtualAlloc。我们使用所有需要的参数(第43-48行)设置函数调用,然后调用NtAllocateMemory。这将为我们的shellcode返回一块内存,就像那样VirtualAlloc

其余步骤类似:

我们将shellcode复制到新分配的内存中,然后在当前进程中通过另一个syscall NtCreateThreadEx代替该指向该内存创建线程CreateThread。最后,我们通过调用syscall NtWaitForSingleObject而不是来启动线程WaitForSingleObject。这是最终结果:

你好,世界通过syscall!假设这是在启用了API挂钩的系统上运行的某种有效负载,我们将绕过它并成功运行我们的有效负载。

关于本机代码的注释

我尚未提到的这个难题的一些关键部分是syscall正常运行所需的所有本机结构,枚举和定义。如果查看上面的屏幕截图,您将看到在C#中没有实现的NTSTATUS类型,例如所有syscall 的返回类型或AllocationTypeand位ACCESS_MASK掩码。这些类型通常在各种Windows标头和DLL中声明,但是要使用syscall,我们需要自己实现它们。我发现它们的过程是查找任何非简单类型,然后尝试为其找到定义。Pinvoke.net对此任务非常有帮助。在它和其他资源(例如MSDN)之间以及ReactOS源代码,我能够找到并添加所需的一切。您可以在此处Native.cs的解决方案类中找到该代码。

包起来

Syscall很有趣!并非每天都可以在一个小程序中结合使用3种不同的语言,托管和非托管代码以及多个级别的Windows API。也就是说,系统调用存在明显的困难。他们需要使用一些样板代码,并且样板散布在各处,以供您查找,就像一些没有证件的寻宝一样。在托管和非托管代码之间进行转换时,调试也可能很棘手。最后,系统调用号码经常更改,必须针对您所针对的平台进行自定义。D / Invoke似乎可以很好地处理其中的几个问题,因此,我很高兴能尽快深入探讨这些问题。

from https://www.solomonsklash.io/syscalls-for-shellcode-injection.html

POC:

GitHub https://github.com/SolomonSklash/SyscallPOC