内容简介:前言针对一个已经学习了Linux Shellcode开发,并开始在Windows上尝试的研究人员来说,这一过程可能要比想象的更加艰难。Windows内核与Linux完全不同。尽管如此,但Linux内核要比Windows更容易理解,原因在于其开源的特性,并且与Windows相比,Linux只具有相当少的功能。另一方面,Windows在过去几年中进行了重大的改进,由于这一改进,新版本与老版本相比已经发生了许多变化。在本文中,我们将专注于对Windows 10 x86进行分析,但其他旧版本可能与之相比没有太多的不
前言
针对一个已经学习了Linux Shellcode开发,并开始在Windows上尝试的研究人员来说,这一过程可能要比想象的更加艰难。Windows内核与 Linux 完全不同。尽管如此,但Linux内核要比Windows更容易理解,原因在于其开源的特性,并且与Windows相比,Linux只具有相当少的功能。另一方面,Windows在过去几年中进行了重大的改进,由于这一改进,新版本与老版本相比已经发生了许多变化。在本文中,我们将专注于对Windows 10 x86进行分析,但其他旧版本可能与之相比没有太多的不同。目前,已经有很多关于PEB LDR的博客文章,但我们还没有看到有任何文章中展现了完整的逻辑,并阐述其本质原因的。大多数研究人员只是通过WinDBG进行分析,并要求读者具备一定程度的后端基础。我撰写这篇文章的主要原因,是希望从 C语言 转到ASM,并希望我们能共同了解在ASM x86中进行Shellcode开发时,后端的工作原理。
.data段
在我们开始处理Shellcode部分之前,我建议首先应该理解内存是如何工作的,因为我们即将做的一切操作都是在内存中。如果我们已经了解像LPWSTR、LPSTR这样的Windows数据类型,那么无疑是一个好消息,因为我们必须要知道:
标准的C语言并不等同于Windows C编程
接下来,唯一需要重点掌握的,就是基本的Assembly x86。默认情况下,除了系统调用或API调用之外,ASM在Linux或Windows中是相同的。因此,了解寄存器的工作原理就显得非常重要。
最重要的是,我们应该了解如何对二进制文件进行反汇编。我主要使用x32dbg和WinDBG x86。我会同时使用这两个 工具 进行调试,因为有一些我们不能在x32dbg中完成的事情,在WinDBG x86中是可以的,反之亦然。因此,我们将不断切换使用这两个工具。
.text段
在我们开始使用Shellcode之前,理解其在较低级别的工作方式,这一点非常重要。我们首先将从一个非常简单的例子开始,找到系统的当前主机名。我们来看看下面是使用C语言编写的Windows API示例:
在上图中,我创建了两个变量,分别是compName和compNameSize。这些将是提供给函数GetComputerNameA的参数。请记住,GetComputerNameA和GetComputerNameW有两个相似的函数。 W代表宽Unicode字符,而A代表ANSI CHAR字符串。我们将在整个博客系列中使用ANSI。下面是 MSDN 对GetComputerNameA函数的说明:
BOOL GetComputerNameA(LPSTR lpBuffer, LPDWORD nSize);
上面的代码表示,GetComputerNameA接受LPSTR,表示长指针字符串,而LPDWORD则表示长指针双字。一个字的大小是16位,因此DWORD在所有平台上都是32位。现在,如果使用g++编译上述程序,我们将看到如下内容:
现在在这里,在程序的最开始,有#include <windows.h>,这也就意味着Windows库将被引入到代码中,它应该在这里动态链接默认依赖项。但是,我们不能对ASM进行相同的操作。在ASM的场景中,我们需要动态地找到函数GetComputerNameA所在的地址,在堆栈上加载参数,并调用具有函数指针的寄存器。我们要知道的一件重要事情是,Windows的大多数功能,都是通过三个主要DLL访问的:NTDLL.DLL、Kernel32.DLL和Kernelbase.DLL。因此,无论任何时间执行任何二进制文件,这些都是始终要加载的必要DLL。为了加载函数GetComputerNameA,我们必须找到这个函数所在的DLL,并在那里找到它的基址。接下来,我们在x32dbg上尝试加载任何x86二进制文件,看看能得到什么。我将加载我们编译的上述exe文件,但实际上,我们可以加载任何随机的32位可执行文件,因为我们只会浏览上面提到的那些DLL。使用x32dbg打开exe文件,并导航到Log部分,可以看到加载了这三个DLL,以及其特定的地址:
接下来,我们将导航到突出显示的Symbols部分,可以看到加载的不同DLL的名称。在这里,我们可以浏览DLL,并查看它们提供的所有功能。
现在,如果我们在搜索框中搜索函数GetComputerNameA,它将显示Kernel32.DLL加载该函数。此外,还将打印出函数所在的地址0x74F69AC0。在理论和实际测试中,这一点都能够很好地展现。接下来,让我们通过C编程然后通过ASM来完成,我们要执行的步骤如下:
1. 使用函数LoadLibraryA WinAPI在内存中加载Kernel32.dll;
2. 使用GetProcAddress在Kernel32.dll中找到函数GetComputerNameA的地址;
3. 将GetProcAddress返回值类型转换为接受2个参数的WinAPI函数(因为GetComputerNameA接受2个参数);
4. 为ComputerName及其Length创建缓冲区。
将Address作为函数指针来执行。
访问LoadLibraryA的 MSDN 页面,可以发现它返回一个HMODULE,这意味着它将一个句柄返回到一个被加载的模块。因此,我们创建了一个变量hmod_libname。类似地,GetProcAddress返回从DLL加载的函数的地址。我们需要将GetProcAddress返回的地址类型转换为GetComputerNameA函数,以使其能够正常工作。为此,我们创建了一个typedef,它基本上复制了函数GetComputerNameA的结构。在上图中,我们加载库Kernel32.dll,并使用GetProcAddress查找函数GetComputerNameA的基址,将地址存储在GetComputerNameProc中。最后,我们创建两个变量CompName和CompNameSize,并使用(*GetComputerNameProc)作为函数指针,执行存储在GetComputerNameProc中的地址,并为其提供所需的变量。上面的代码中,还打印了函数GetComputerNameA的地址。我们尝试对其进行编译,看看结果如何:
不错!地址0x74F69AC0与上面用x32dbg调试时发现的地址一致。
_start
我们接下来进入到有趣的部分。所有DLL及其函数的地址在重新启动时都会发生变化,并且在每个其他系统中都会有所不同。这就是我们无法对ASM代码中的任何地址进行硬编码的原因。但是,主要问题仍然存在,那就是我们如何找到kernel32.dll自身的地址?
我在一开始说过,每个exe都加载了Kernel32.dll、NTDLL.DLL和Kernelbase.dll。事实上,这些DLL是操作系统中非常重要的一部分,每次在执行任何操作时,都会加载这些DLL。因此,这些DLL到内存中的加载顺序总是相同的。然而,这可能因操作系统而异。这就意味着,在Windows XP与Windows 10之间可能有所不同,但所有Windows 10中的加载顺序将保持不变。
所以,我们在继续下一步之前,需要完成下面的工作:
1. 找到Kernel32.dll的加载顺序;
2. 找到Kernel32.dll的地址;
3. 找到GetComputerNameA的地址;
4. 在栈上加载GetComputerNameA的参数;
5. 调用GetComputerNameA函数指针。
可能听起来很容易?我们来实际尝试一下。
查找kernel32.dll的地址并不简单。当我们执行任何exe时,在操作系统中首先创建的就是 TEB (线程环境块)和 PEB (进程环境块)。
我们的主要关注点在于PEB结构(称为LDR),因为这是与进程相关的所有信息都被加载的地方。从流程参数到流程ID的所有内容都存储在这个位置。在PEB中,有一个名为PEB_LDR_DATA的结构,它包含三个关键部分。这些被称为链接列表(Linked Lists)。
1. InLoadOrderModuleList – 加载模块(exe或dll)的顺序;
2. InMemoryOrderModuleList – 模块(exe或dll)存储在内存中的顺序;
3. InInitializationOrderModuleList – 在进程环境块中初始化模块(exe或dll)的顺序。
在链表中加载模块的顺序是固定的。这意味着,我们如果能够在上面的列表中找到kernel32.dll的顺序,就可以搜索kernel32.dll的地址,并继续进行。现在,我们启动WinDBG x86。如果各位还没有安装WinDBG及其依赖项,你可以在 SLAER 上找到一篇关于WinDBG的 文章 。一旦安装WinDBG之后,就可以像我们之前那样打开任意的exe文件。
在WinDBG中加载exe文件后,会显示一些输出。限制,我们将忽略输出内容,并在下面的命令提示符中输入.cls以清除屏幕并重新开始。现在,我们在命令提示符下输入!peb,看看在这里能够得到什么:
如大家所见,我们得到了LDR(PEB结构)的地址,即779E0C40。这非常重要,因为我们要使用该地址来计算前进的地址。接下来,我们输入命令dt nt!_TEB,以查找PEB结构的偏移量。
如我们所见,_PEB位于偏移量0x030的位置。以类似的方式,我们可以使用dt nt!_PEB查看_PEB结构的内容。
_PEB_LDR_DATA的偏移量为0x00c。接下来,我们尝试查找_PEB_LDR_DATA结构中的内容。我们可以用类似的方式实现这一点:
dt nt!_PEB_LDR_DATA
在这里,我们可以看到InLoadOrderModuleList位于偏移量0x00c处,InMemoryOrderModuleList位于偏移量0x014处,InInitializationOrderModuleList位于偏移量0x01c处。此外,如果要查看每个列表所在的地址,可以使用我们此前找到的地址779E0C40(LDR的地址)以及命令dt nt!_PEB_LDR_DATA 779E0C40。这将向我们显示链接列表的相应起始地址和结束地址,如下所示:
有一个地方,可能会被一些人误解,就是上图中展示出InMemoryOrderModuleList的类型为_LIST_ENTRY,但在 MSDN 上已经另有说明:
因此,MSDN声明它是LDR_DATA_TABLE_ENTRY类型而不是_LIST_ENTRY类型。我们尝试查看结构中加载的模块,并指定该结构的起始地址为0x7041e8,以便可以看到加载的模块的基址。需要注意的是,0x7041e8是此结构的地址,因此第一个条目将比此地址少8个字节。因此,我们的命令是:
dt nt!_LDR_DATA_TABLE_ENTRY 0x7041e8-8
第一个出现的BaseDllName是gethost.exe。这就是我之前执行的exe文件。此外,我们可以看到现在InMemoryOrderLinks的地址是0x7040e0。偏移量0x018处的DllBase中包含BaseDllName的基址。现在,我们下一个加载的模块必须距离0x7040e0有8个字节,也就是0x7040e0-8。
dt nt!_LDR_DATA_TABLE_ENTRY 0x7040e0-8
所以,我们的第二个模块是ntdll.dll,它的地址是0x778c000,下一个模块位于0x704690之后的8个字节。所以,我们的下一个命令是:
dt nt!_LDR_DATA_TABLE_ENTRY 0x704690-8
由此,就得到了第三个模块Kernel32.dll,其地址是0x74f50000,其偏移量时0x018。模块加载的顺序总是固定的,至少这适用于Windows 10、Windows 7、Windows 8(包括8.1)。因此,当我们编写ASM时,我们可以遍历整个PEB LDR结构体,并找到Kernel32.dll的地址,并将其加载到我们的Shellcode中。以类似的方式,我们还可以找到Kernelbase.dll的地址,这是第四个模块。
现在,我们总结一下需要进行的工作:
1. PEB位于距离文件段寄存器偏移量为0x030的位置;
2. LDR位于偏移量为PEB + 0x00C的位置;
3. InMemoryOrderModuleList位于偏移量LDR + 0x014的位置;
4. 第一个模块入口是exe本身;
5. 第二个模块入口是ntdll.dll;
6. 第三个模块入口是kernel32.dll;
7. 第四个模块入口是Kernelbase.dll。
我们现在最感兴趣的,就是Kernel32.dll。每次加载DLL时,地址都将存储在DllBase的偏移量0x018的位置。我们链接列表的起始地址将存储在InMemoryOrderLinks的偏移量中,即0x008。因此,偏移量之间的关系将是 DllBase – InMemoryOrderLinks = 0x018 – 0x008 = 0x10。因此,Kernel32.dll的偏移量将是LDR + 0x10。更详细的理解,可以在下图中看到,这张图是我从 这里 偷过来的。
现在,如果我们在ASM中做同样的工作,将会是如下所示:
我们使用NASM来编译,并在x32dbg中加载它。大家可以从 这里 下载NASM。
实际上,一旦我们的最后一条指令被运行,就应该在EAX寄存器中加载Kernel32.dll的地址。我们来看看它在x32dbg中看起来是否相同。
如我们所见,在最后一条指令之后,加载到EAX中的地址与我们在下面使用lm命令在WinDBG中看到的地址相同,都是74F50000,这是Kernel32.dll的地址。
现在,我们已经有了Kernel32.dll的地址,下一步就是使用LoadLibraryA找到GetComputerNameA的地址,并调用该函数。我们将在下一篇文章中重点讨论这一问题,完善ASM代码,实现获取计算机名称并将其打印在屏幕上,然后打印到Shellcode部分。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员的自我修养
陈逸鹤 / 清华大学出版社 / 2017-5 / 49.00
程序员作为一个职业、也作为一个群体,正逐渐从幕后走向前台,并以他们自己的能力加速改变着世界,也改变着人们生活的方方面面。然而,对于程序员,特别是年轻程序员们来说,如何理解自己的职业与发展,如何看待自己的工作与生活,这些问题往往比那些摆在面前的技术难题更让他们难以解答。 这本书从一个成熟程序员、一名IT管理者的角度,以杂记的形式为大家分享关于国内程序员职业生涯、个人发展、编程中的实践与认知乃至......一起来看看 《程序员的自我修养》 这本书的介绍吧!