前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编写Windows x64的shellcode

编写Windows x64的shellcode

作者头像
C4rpeDime
发布2022-04-26 08:21:41
1.4K0
发布2022-04-26 08:21:41
举报
文章被收录于专栏:黑白安全

很久以前我写过三篇关于如何编写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

我们可以看到以下内容:

  1. sub rsp,28 - 这将在堆栈上分配0x28(40)字节,正如我们所讨论的:寄存器参数为32个字节,对齐为8个字节。
  2. mov edx,4 - 这将在EDX寄存器中放置第二个参数。由于数量很少,不需要使用RDX,结果是一样的。
  3. mov ecx,3 - 第一个参数的值放在ECX寄存器中。
  4. call <consolex64.Add> - 调用“添加”功能。
  5. xor eax,eax - 将EAX(或RAX)设置为0,因为它将是main的返回值。
  6. add rsp,28 - 清除分配的堆栈空间。
  7. 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

让我们看看这个函数是如何工作的:

  1. mov dword ptr ss:[rsp + 10],edx - 我们知道,参数在ECX和EDX寄存器中传递。但是如果函数需要使用那些寄存器怎么办(但请注意,某些寄存器必须通过函数调用保存,这些寄存器如下:RBX,RBP,RDI,RSI,R12,R13,R14和R15)?在这种情况下,该函数将使用函数调用者分配的“阴影空间”(“home space”)。通过该指令,该函数在EDX寄存器中将第二个参数(值4)保存在阴影空间中。
  2. mov dword ptr ss:[rsp + 8],ecx - 与前一条指令类似,这一条将从堆栈中保存ECX寄存器中的第一个参数(值3)
  3. sub rsp,18 - 在堆栈上分配0x18(或24)字节。此函数不调用其他函数,因此不需要分配至少32个字节。此外,由于它不调用其他函数,因此不需要将堆栈对齐到16个字节。我不确定为什么它分配24个字节,看起来堆栈上的“局部变量区域”必须对齐到16个字节,其他8个字节可能用于堆栈对齐(如前所述)。
  4. mov eax,dword ptr ss:[rsp + 28] - 将在EAX寄存器中放置第二个参数的值(值4)。
  5. mov ecx,dword ptr ss:[rsp + 20] - 将在ECX寄存器中放置第一个参数的值(值3)。
  6. 添加ecx,eax - 将ECX添加到EAX寄存器的值,因此ECX将变为7。
  7. mov eax,ecx - 将相同的值(总和)保存到EAX寄存器中。
  8. mov dword ptr ss:[rsp],eax和  mov eax,dword ptr ss:[rsp] 看起来它们是删除优化的一些效果,它们没有做任何有用的事情。
  9. add rsp,18 - 清理分配的堆栈空间。
  10. ret - 从函数返回

在Windows x64上编写ASM

在Windows x64上有多种方法可以编写汇编程序。我将使用NASMMicrosoft 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。

步骤是一样的:

  1. 转到PE头(偏移量0x3c)
  2. 转到导出表(偏移量0x88)
  3. 转到名称表(偏移量0x20)
  4. 获取函数名称
  5. 检查它是否以“GetProcA”开头
  6. 转到序数表(偏移量0x24)
  7. 获取功能号码
  8. 转到地址表(偏移量0x1c)
  9. 获取功能地址

下面的代码可以帮助我们找到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/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-07-02),如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ASM for x64
    • 召集会议
      • 函数调用示例
      • 在Windows x64上编写ASM
      • 找到kernel32.dll的基地址
      • 找到GetProcAddress函数的地址
      • 找到LoadLibraryA的地址
      • 使用LoadLibraryA加载user32.dll
      • 找到SwapMouseButton功能的地址
      • 调用SwapMouseButton
      • 找到ExitProcess函数的地址
      • ExitProcess的
      • 结论
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档