很久以前我写过三篇关于如何编写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 。
为了继续,需要理解大会中的多个差异。在这里,我们将讨论与我们将要做的事情相关的x86和x64之间最重要的变化。
请注意,本文仅用于教育目的。它必须简单,这意味着,当然,可以对结果shellcode进行大量优化,使其更小更快。
首先,寄存器现在如下:
另一个重要的区别是调用函数的方式,即调用约定。
以下是我们需要了解的最重要的事情:
有关在Windows上调用约定的更多详细信息,请参见此处。
让我们举一个简单的例子来理解这些事情。下面是一个简单添加的函数,它从main调用。
#include “stdafx.h”int add(long x,int y){ int z = x + y ;
返回z;}int main(){
加(3,4);
返回0; }
删除所有优化和安全功能后,这是一个可能的输出。
主功能:
sub rsp,28
mov edx,4
mov ecx,3
致电<consolex64.Add>
xor eax,eax
添加rsp,28
RET
我们可以看到以下内容:
添加功能:
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
让我们看看这个函数是如何工作的:
在Windows x64上有多种方法可以编写汇编程序。我将使用NASM和Microsoft Visual Studio社区提供的链接器。
我将使用x64.asm文件编写汇编代码,NASM将输出x64.obj,链接器将创建x64.exe。为了简化这个过程,我创建了一个简单的Windows Batch脚本:
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之前,我们可以从以下开始:
比特64
SECTION .text
全球主要
主要:
sub RSP,0x28; 40个字节的阴影空间
和RSP,0FFFFFFFFFFFFFFF0h; 将堆栈与16个字节的倍数对齐
这将指定64位代码,在“.text”(代码)部分中使用“main”函数。代码还将分配一些堆栈空间并将堆栈对齐到16个字节的倍数。
我们知道,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寄存器中:
; 解析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函数的地址,唯一的区别是导出表的偏移量是0x88而不是0x78。
步骤是一样的:
下面的代码可以帮助我们找到GetProcAddress的地址:
; 解析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的寄存器。
由于我们有GetProcAddress的地址和kernel32.dll的基地址,我们可以使用它们来调用GetProcAddress(kernel32.dll,“LoadLibraryA”)并找到LoadLibraryA函数的地址。
但是,我们需要注意一些重要的事情:我们将使用堆栈来放置我们的字符串(例如“LoadLibraryA”),这可能会破坏堆栈对齐,因此我们需要确保它是16字节的对齐。此外,我们不能忘记我们需要为函数调用分配的堆栈空间,因为我们调用的函数可能会使用它。因此,我们需要将我们的字符串放在堆栈上,然后在此之后为我们调用的函数分配空间(例如GetProcAddress)。
查找LoadLibraryA的地址非常简单:
; 使用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函数的地址,因此调用LoadLibraryA(“user32.dll”)来加载user32.dll并找出它将由LoadLibraryA返回的基址非常简单。
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寄存器中。
我们有GetProcAddress的地址,user32.dll的基地址,我们知道该函数被称为“SwapMouseButton”。所以我们只需要调用GetProcAddress(user32.dll,“SwapMouseButton”);
请注意,当我们在函数调用的堆栈上分配空间时,我们不再分配0x30(48)字节,我们只分配0x28(40)字节。这是因为要将我们的字符串(“SwapMouseButton”)放在堆栈上,我们使用3个PUSH指令,因此我们得到0x18(24)字节的数据,这不是16的倍数。因此我们使用0x28而不是0x30来对齐堆栈到16个字节。
; 调用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寄存器中。
好吧,我们有它的地址,它应该很容易调用它。我们之前清理过没有任何问题,我们不需要在此函数调用中更改堆栈。所以我们只需将RCX寄存器设置为1(表示真)并调用它。
; 调用SwapMouseButton(true)
mov rcx,1; 真正
拨打r15; SwapMouseButton(真)
正如我们之前所做的那样,我们使用GetProcAddress来查找kernel32.dll导出的ExitProcess函数的地址。我们在RBX中仍然有kernel32.dll基地址(这是一个非易失性寄存器,这就是使用它的原因)所以它很简单:
; 调用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(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/