很久以前我写过三篇关于如何编写Windows shellcode(x86 - 32位)的详细博客文章。文章初学者友好,包含很多细节。第一部分解释什么是shellcode,哪些是它的局限性,第二部分解释了PEB(进程环境块),PE(可移植可执行文件)文件格式和ASM(汇编程序)的基础知识,第三部分说明了Windows shellcode如何实际实现。这篇博客文
很久以前我写过三篇关于如何编写windows Shellcode(x86 - 32位)的详细博客文章。文章初学者友好,包含很多细节。第一部分解释什么是shellcode,哪些是它的局限性,第二部分解释了PEB(进程环境块),PE(可移植可执行文件)文件格式和ASM(汇编程序)的基础知识,第三部分说明了Windows shellcode如何实际实现。
这篇博客文章是以前关于Windows 64位(x64)的文章的端口,它不会涵盖以前博客文章中解释的所有细节,所以谁不熟悉Windows上shellcode开发的所有概念必须先看到它们走得更远。
当然,这里将介绍Windows上x86和x64 shellcode开发之间的差异,包括ASM。但是,由于我已经在x64(Windows)博客文章的基于堆栈的缓冲区溢出上写了一些关于Windows 64位的详细信息,我将在这里复制并粘贴它们。
与之前的博客文章一样,我们将创建一个简单的shellcode,使用user32.dll导出的SwapMouseButton函数交换鼠标按钮,并使用kernel32.dll导出的ExitProcess函数grecefully关闭proccess 。
ASM for x64
为了继续,需要理解大会中的多个差异。在这里,我们将讨论与我们将要做的事情相关的x86和x64之间最重要的变化。
请注意,本文仅用于教育目的。它必须简单,这意味着,当然,可以对结果shellcode进行大量优化,使其更小更快。
首先,寄存器现在如下:
- 通用寄存器如下:RAX,RBX,RCX,RDX,RSI,RDI,RBP和RSP。它们现在是64位(8字节)而不是32位(4字节)。
- EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP代表前面提到的寄存器的最后4个字节。它们保存32位数据。
- 有几个新的寄存器:R8,R9,R10,R11,R12,R13,R14,R15,也保持64位。
- 可以使用R8d,R9d等来访问最后4个字节,因为您可以使用EAX,EBX等。
- 在堆栈上推送和弹出数据将使用64位而不是32位
召集会议
另一个重要的区别是调用函数的方式,即调用约定。
以下是我们需要了解的最重要的事情:
- 前4个参数未放在堆栈上。前4个参数在RCX,RDX,R8和R9寄存器中指定。
- 如果有超过4个参数,则其他参数将从左到右放置在堆栈中。
- 与x86类似,返回值将在RAX寄存器中可用。
- 函数调用者将为寄存器中使用的参数(称为“阴影空间”或“家庭空间”)分配堆栈空间。即使在调用函数时,参数也放在寄存器中,如果被调用函数需要修改寄存器,则需要一些空间来存储它们,这个空间就是堆栈。函数调用者必须在函数调用之前分配这个空间,并在函数调用之后释放它。函数调用者应该至少分配32个字节(对于4个寄存器),即使它们并未全部使用。
- 在任何调用指令之前,堆栈必须是16字节对齐的。为此,一些函数可能在堆栈上分配40个(0x28)字节(4个寄存器为32个字节,8个字节用于将堆栈与先前的用法对齐 - 堆栈上返回的Rip地址)。你可以在这里找到更多细节。
- 有些寄存器是易失性的,而另一些是非易失性的。这意味着如果我们将一些值设置到寄存器并调用某些函数(例如Windows API),则易失性寄存器可能会改变,而非易失性寄存器将保留它们的值。
有关在Windows上调用约定的更多详细信息,请参见此处。
函数调用示例
让我们举一个简单的例子来理解这些事情。下面是一个简单添加的函数,它从main调用。
代码语言:javascript复制#include “stdafx.h”int add(long x,int y){ int z = x y ;
返回z;}int main(){
加(3,4);
返回0; }
删除所有优化和安全功能后,这是一个可能的输出。
主功能:
代码语言:javascript复制sub rsp,28
mov edx,4
mov ecx,3
致电<consolex64.Add>
xor eax,eax
添加rsp,28
RET
我们可以看到以下内容:
- sub rsp,28 - 这将在堆栈上分配0x28(40)字节,正如我们所讨论的:寄存器参数为32个字节,对齐为8个字节。
- mov edx,4 - 这将在EDX寄存器中放置第二个参数。由于数量很少,不需要使用RDX,结果是一样的。
- mov ecx,3 - 第一个参数的值放在ECX寄存器中。
- call <consolex64.Add> - 调用“添加”功能。
- xor eax,eax - 将EAX(或RAX)设置为0,因为它将是main的返回值。
- add rsp,28 - 清除分配的堆栈空间。
- ret - 从主要归来。
添加功能:
代码语言:javascript复制mov dword ptr ss:[rsp 10],edx
mov dword ptr ss:[rsp 8],ecx
sub rsp,18
mov eax,dword ptr ss:[rsp 28]
mov ecx,dword ptr ss:[rsp 20]
添加ecx,eax
mov eax,ecx
mov dword ptr ss:[rsp],eax
mov eax,dword ptr ss:[rsp]
添加rsp,18
RET
让我们看看这个函数是如何工作的:
- mov dword ptr ss:[rsp 10],edx - 我们知道,参数在ECX和EDX寄存器中传递。但是如果函数需要使用那些寄存器怎么办(但请注意,某些寄存器必须通过函数调用保存,这些寄存器如下:RBX,RBP,RDI,RSI,R12,R13,R14和R15)?在这种情况下,该函数将使用函数调用者分配的“阴影空间”(“home space”)。通过该指令,该函数在EDX寄存器中将第二个参数(值4)保存在阴影空间中。
- mov dword ptr ss:[rsp 8],ecx - 与前一条指令类似,这一条将从堆栈中保存ECX寄存器中的第一个参数(值3)
- sub rsp,18 - 在堆栈上分配0x18(或24)字节。此函数不调用其他函数,因此不需要分配至少32个字节。此外,由于它不调用其他函数,因此不需要将堆栈对齐到16个字节。我不确定为什么它分配24个字节,看起来堆栈上的“局部变量区域”必须对齐到16个字节,其他8个字节可能用于堆栈对齐(如前所述)。
- mov eax,dword ptr ss:[rsp 28] - 将在EAX寄存器中放置第二个参数的值(值4)。
- mov ecx,dword ptr ss:[rsp 20] - 将在ECX寄存器中放置第一个参数的值(值3)。
- 添加ecx,eax - 将ECX添加到EAX寄存器的值,因此ECX将变为7。
- mov eax,ecx - 将相同的值(总和)保存到EAX寄存器中。
- mov dword ptr ss:[rsp],eax和 mov eax,dword ptr ss:[rsp] 看起来它们是删除优化的一些效果,它们没有做任何有用的事情。
- add rsp,18 - 清理分配的堆栈空间。
- ret - 从函数返回
在Windows x64上编写ASM
在Windows x64上有多种方法可以编写汇编程序。我将使用NASM和Microsoft Visual Studio社区提供的链接器。
我将使用x64.asm文件编写汇编代码,NASM将输出x64.obj,链接器将创建x64.exe。为了简化这个过程,我创建了一个简单的Windows Batch脚本:
代码语言:javascript复制del x64.obj
del x64.exe
nasm -f win64 x64.asm -o x64.obj
链接/输入:main / MACHINE:X64 / NODEFAULTLIB / SUBSYSTEM:CONSOLE x64.obj
您可以使用“x20 Native Tools Command Prompt for VS 2019”运行它,其中“link”可直接使用。不要忘记将NASM二进制文件目录添加到PATH环境变量中。
要测试shellcode,我在x64bdg中打开生成的二进制文件,然后逐步完成代码。这样,我们可以确定一切正常。
在开始实际的shellcode之前,我们可以从以下开始:
代码语言:javascript复制比特64
SECTION .text
全球主要
主要:
sub RSP,0x28; 40个字节的阴影空间
和RSP,0FFFFFFFFFFFFFFF0h; 将堆栈与16个字节的倍数对齐
这将指定64位代码,在“.text”(代码)部分中使用“main”函数。代码还将分配一些堆栈空间并将堆栈对齐到16个字节的倍数。
找到kernel32.dll的基地址
我们知道,Windows的shellcode开发过程的第一步是找到kernel32.dll的基地址,它是加载它的内存地址。这将帮助我们找到有用的导出函数:GetProcAddress和LoadLibraryA,我们可以使用它来实现我们的目标。
我们将开始找到TEB(线程环境块),在usermode中包含线程信息的结构,我们可以使用GS寄存器找到它,例如:[0x00]。该结构还包含指向偏移0x60处的PEB(处理环境块)的指针。
PEB包含偏移量为0x18的“ 加载器 ”(Ldr),其中包含偏移量为0x20的“ InMemoryOrder ”模块列表。正如我们为x86所做的那样,第一个模块将是可执行文件,第二个模块是ntdll.dll,第三个是我们想要查找的kernel32.dll。这意味着我们将通过一个链表(LIST_ENTRY结构包含LIST_ENTRY *指针,Flink和Blink,x64各占8个字节)。
在我们找到第三个模块kernel32.dll之后,我们只需要去偏移0x20来获取它的基地址,我们就可以开始做我们的事了。
下面是我们如何使用PEB获取kernel32.dll的基址并将其存储在RBX寄存器中:
代码语言:javascript复制; 解析PEB并找到kernel32
xor rcx,rcx; RCX = 0
mov rax,[gs:rcx 0x60]; RAX = PEB
mov rax,[rax 0x18]; RAX = PEB-> Ldr
mov rsi,[rax 0x20]; RSI = PEB-> Ldr.InMemOrder
lodsq; RAX =第二个模块
xchg rax,rsi; RAX = RSI,RSI = RAX
lodsq; RAX =第三(kernel32)
mov rbx,[rax 0x20]; RBX =基地址
找到GetProcAddress函数的地址
它实际上类似于找到GetProcAddress函数的地址,唯一的区别是导出表的偏移量是0x88而不是0x78。
步骤是一样的:
- 转到PE头(偏移量0x3c)
- 转到导出表(偏移量0x88)
- 转到名称表(偏移量0x20)
- 获取函数名称
- 检查它是否以“GetProcA”开头
- 转到序数表(偏移量0x24)
- 获取功能号码
- 转到地址表(偏移量0x1c)
- 获取功能地址
下面的代码可以帮助我们找到GetProcAddress的地址:
代码语言:javascript复制; 解析kernel32 PE
xor r8,r8; 清除r8
mov r8d,[rbx 0x3c]; R8D = DOS-> e_lfanew偏移量
mov rdx,r8; RDX = DOS-> e_lfanew
添加rdx,rbx; RDX = PE标头
mov r8d,[rdx 0x88]; R8D =偏移导出表
添加r8,rbx; R8 =导出表
xor rsi,rsi; 清除RSI
mov esi,[r8 0x20]; RSI =偏移名称
添加rsi,rbx; RSI =名称表
xor rcx,rcx; RCX = 0
mov r9,0x41636f7250746547; GetProcA
; 循环导出函数并找到GetProcAddress
Get_Function:
inc rcx; 增加序数
xor rax,rax; RAX = 0
mov eax,[rsi rcx * 4]; 获取名称偏移量
添加rax,rbx; 获取功能名称
cmp QWORD [rax],r9; GetProcA?
jnz Get_Function
xor rsi,rsi; RSI = 0
mov esi,[r8 0x24]; ESI =偏移序数
添加rsi,rbx; RSI =普通表
mov cx,[rsi rcx * 2]; 功能数量
xor rsi,rsi; RSI = 0
mov esi,[r8 0x1c]; 偏移地址表
添加rsi,rbx; ESI =地址表
xor rdx,rdx; RDX = 0
mov edx,[rsi rcx * 4]; EDX =指针(偏移)
添加rdx,rbx; RDX = GetProcAddress
mov rdi,rdx; 在RDI中保存GetProcAddress
请注意,这必须小心。PE文件中的一些结构不是8个字节,而我们最终需要8个字节的指针。这就是为什么在上面的代码中使用了诸如ESI或CX的寄存器。
找到LoadLibraryA的地址
由于我们有GetProcAddress的地址和kernel32.dll的基地址,我们可以使用它们来调用GetProcAddress(kernel32.dll,“LoadLibraryA”)并找到LoadLibraryA函数的地址。
但是,我们需要注意一些重要的事情:我们将使用堆栈来放置我们的字符串(例如“LoadLibraryA”),这可能会破坏堆栈对齐,因此我们需要确保它是16字节的对齐。此外,我们不能忘记我们需要为函数调用分配的堆栈空间,因为我们调用的函数可能会使用它。因此,我们需要将我们的字符串放在堆栈上,然后在此之后为我们调用的函数分配空间(例如GetProcAddress)。
查找LoadLibraryA的地址非常简单:
代码语言:javascript复制; 使用GetProcAddress查找LoadLibrary的地址
mov rcx,0x41797261; 阿里亚
推rcx; 推上堆栈
mov rcx,0x7262694c64616f4c; LoadLibr
推rcx; 推上堆栈
mov rdx,rsp; LoadLibraryA
mov rcx,rbx; kernel32.dll的基地址
sub rsp,0x30; 为函数调用分配堆栈空间
打电话给rdi; 调用GetProcAddress
添加rsp,0x30; 清理分配的堆栈空间
添加rsp,0x10; 为LoadLibrary字符串清理空间
mov rsi,rax; LoadLibrary保存在RSI中
我们将“LoadLibraryA”字符串放在堆栈上,设置RCX和RDX寄存器,在堆栈上为函数调用分配空间,调用GetProcAddress并清理堆栈。因此,我们将LoadLibraryA地址存储在RSI寄存器中。
使用LoadLibraryA加载user32.dll
由于我们有LoadLibraryA函数的地址,因此调用LoadLibraryA(“user32.dll”)来加载user32.dll并找出它将由LoadLibraryA返回的基址非常简单。
代码语言:javascript复制mov rcx,0x6c6c; 二
推rcx; 推上堆栈
mov rcx,0x642e323372657375; user32.d
推rcx; 推上堆栈
mov rcx,rsp; user32.dll中
sub rsp,0x30; 为函数调用分配堆栈空间
打电话给rsi; 调用LoadLibraryA
添加rsp,0x30; 清理分配的堆栈空间
添加rsp,0x10; 清理user32.dll字符串的空间
mov r15,rax; R15中user32.dll的基址
该函数将user32.dll模块的基地址返回到RAX,我们将其保存在R15寄存器中。
找到SwapMouseButton功能的地址
我们有GetProcAddress的地址,user32.dll的基地址,我们知道该函数被称为“SwapMouseButton”。所以我们只需要调用GetProcAddress(user32.dll,“SwapMouseButton”);
请注意,当我们在函数调用的堆栈上分配空间时,我们不再分配0x30(48)字节,我们只分配0x28(40)字节。这是因为要将我们的字符串(“SwapMouseButton”)放在堆栈上,我们使用3个PUSH指令,因此我们得到0x18(24)字节的数据,这不是16的倍数。因此我们使用0x28而不是0x30来对齐堆栈到16个字节。
代码语言:javascript复制; 调用GetProcAddress(user32.dll,“SwapMouseButton”)
xor rcx,rcx; RCX = 0
推rcx; 在堆栈上按0
mov rcx,0x6e6f7474754265; eButton
推rcx; 推上堆栈
mov rcx,0x73756f4d70617753; SwapMous
推rcx; 推上堆栈
mov rdx,rsp; SwapMouseButton
mov rcx,r15; User32.dll基地址
sub rsp,0x28; 为函数调用分配堆栈空间
打电话给rdi; 调用GetProcAddress
添加rsp,0x28; 清理分配的堆栈空间
添加rsp,0x18; 清理SwapMouseButton字符串的空间
mov r15,rax; R15中的SwapMouseButton
GetProcAddress将在RAX中返回SwapMouseButton函数的地址,我们将其保存到R15寄存器中。
调用SwapMouseButton
好吧,我们有它的地址,它应该很容易调用它。我们之前清理过没有任何问题,我们不需要在此函数调用中更改堆栈。所以我们只需将RCX寄存器设置为1(表示真)并调用它。
代码语言:javascript复制; 调用SwapMouseButton(true)
mov rcx,1; 真正
拨打r15; SwapMouseButton(真)
找到ExitProcess函数的地址
正如我们之前所做的那样,我们使用GetProcAddress来查找kernel32.dll导出的ExitProcess函数的地址。我们在RBX中仍然有kernel32.dll基地址(这是一个非易失性寄存器,这就是使用它的原因)所以它很简单:
代码语言:javascript复制; 调用GetProcAddress(kernel32.dll,“ExitProcess”)
xor rcx,rcx; RCX = 0
mov rcx,0x737365; ESS
推rcx; 推上堆栈
mov rcx,0x636f725074697845; ExitProc
推rcx; 推上堆栈
mov rdx,rsp; ExitProcess的
mov rcx,rbx; Kernel32.dll基地址
sub rsp,0x30; 为函数调用分配堆栈空间
打电话给rdi; 调用GetProcAddress
添加rsp,0x30; 清理分配的堆栈空间
添加rsp,0x10; 为ExitProcess字符串清理空间
mov r15,rax; R15中的ExitProcess
我们在R15寄存器中保存ExitProcess函数的地址。
ExitProcess的
由于我们不想让进程崩溃,我们可以通过调用ExitProcess函数“优雅地”退出。我们有地址,堆栈是对齐的,我们只需要调用它。
代码语言:javascript复制; 调用ExitProcess(0)
mov rcx,0; 退出代码0
拨打r15; ExitProcess的(0)
结论
有很多关于x64上的Windows shellcode开发的文章,比如这个或者这个,但我只想按照以前写过的文章讲述我的方式。
shellcode远离优化,它还包含NULL字节。但是,这些限制都可以得到改善。
Shellcode开发很有趣,需要从x86到x64的转换,因为x86将来不会用得太多。
或者,我将在Shellcode Compiler中添加对Windows x64的支持。
原文标题《Writing shellcodes for Windows x64》
黑白网小编翻译自:https://nytrosecurity.com/2019/06/30/writing-shellcodes-for-windows-x64/