x64 返回地址欺骗

admin 2025年1月20日13:37:56评论5 views字数 8439阅读28分7秒阅读模式

【翻译】x64 Return Address Spoofing 

引言

杀毒软件或 EDR 解决方案基于行为阈值来判断活动是否恶意。如果进程的活动超过这个阈值,它就会被归类为可疑或直接判定为恶意,并可能被终止。然而,这个阈值对于不同的进程是不同的。受信任的进程(如系统服务)具有较高的阈值,而未见过或未签名的可执行文件进程则阈值较低。有多种活动会被监控以识别可疑行为。通常,其中一种活动是从未备份的内存区域执行可疑的 API。例如,如果加载到内存中的 shellcode 开始执行诸如"InternetConnectA"、"HttpOpenRequestA"或"HttpSendRequestA"等 WinAPI,该进程就可以被确定为恶意的。

要确定 WinAPI 被调用的内存位置,只需查看其返回地址即可。这是程序从 WinAPI 返回后将继续执行的地址。通过修改返回地址,我们可以使其看起来像是从不同位置调用该函数。这种技术被称为"返回地址欺骗"。虽然这种技术并不新颖或完美,而且由于破坏了调用栈链而可能被检测到,但它是一个可以继续发展的基础技术。关于这种技术有很多博客和优秀的资源,阅读这些内容启发我开发了一个适用于所有 WinAPI 的概念验证。

本项目的代码可以在我的GitHub上找到。

让我们开始

首先,让我们看看当未备份内存中的 shellcode 执行 MessageBox 时,调用栈会是什么样子。

#include<Windows.h>#include<stdio.h>unsigned char shellcode[] = { 0x57,0x48,0x89,0xe7,0x48,0x83,0xe4,0xf0,0x48,0x83,0xec,0x20,0xe8,0x0f,0x01,0x00,0x00,0x48,0x89,0xfc,0x5f,0xc3,0x66,0x2e,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00,0x65,0x48,0x8b,0x04,0x25,0x60,0x00,0x00,0x00,0x48,0x8b,0x40,0x18,0x41,0x89,0xca,0x4c,0x8b,0x58,0x20,0x4d,0x89,0xd9,0x66,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00,0x49,0x8b,0x41,0x50,0x31,0xc9,0x4c,0x8d,0x40,0x02,0x0f,0xb7,0x00,0x66,0x85,0xc0,0x74,0x20,0x66,0x0f,0x1f,0x44,0x00,0x00,0x89,0xca,0x0f,0xb7,0xc0,0x49,0x83,0xc0,0x02,0xc1,0xe2,0x04,0x01,0xd0,0x01,0xc1,0x41,0x0f,0xb7,0x40,0xfe,0x66,0x85,0xc0,0x75,0xe6,0x41,0x39,0xca,0x74,0x09,0x4d,0x8b,0x09,0x4d,0x39,0xcb,0x75,0xc1,0xc3,0x49,0x8b,0x41,0x20,0xc3,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x57,0x56,0x53,0x48,0x63,0x41,0x3c,0x8b,0xbc,0x01,0x88,0x00,0x00,0x00,0x48,0x01,0xcf,0x44,0x8b,0x4f,0x20,0x8b,0x5f,0x14,0x49,0x01,0xc9,0x85,0xdb,0x74,0x51,0x49,0x89,0xcb,0x89,0xd6,0x45,0x31,0xd2,0x66,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00,0x41,0x8b,0x01,0x31,0xc9,0x4c,0x01,0xd8,0x4c,0x8d,0x40,0x01,0x0f,0xbe,0x00,0x84,0xc0,0x74,0x1c,0x0f,0x1f,0x44,0x00,0x00,0x89,0xca,0xc1,0xe2,0x04,0x01,0xd0,0x01,0xc1,0x4c,0x89,0xc0,0x49,0x83,0xc0,0x01,0x0f,0xbe,0x00,0x84,0xc0,0x75,0xe9,0x39,0xce,0x74,0x11,0x49,0x83,0xc2,0x01,0x49,0x83,0xc1,0x04,0x4c,0x39,0xd3,0x75,0xc0,0x5b,0x5e,0x5f,0xc3,0x8b,0x57,0x24,0x4b,0x8d,0x0c,0x53,0x8b,0x47,0x1c,0x5b,0x5e,0x0f,0xb7,0x14,0x11,0x5f,0x49,0x8d,0x14,0x93,0x8b,0x04,0x02,0x4c,0x01,0xd8,0xc3,0x48,0xb8,0x75,0x73,0x65,0x72,0x33,0x32,0x2e,0x64,0x48,0x83,0xec,0x38,0x48,0x89,0x44,0x24,0x25,0xb8,0x6c,0x6c,0x00,0x00,0x66,0x89,0x44,0x24,0x2d,0xc6,0x44,0x24,0x2f,0x00,0xc7,0x44,0x24,0x20,0x74,0x65,0x73,0x74,0xc6,0x44,0x24,0x24,0x00,0x65,0x48,0x8b,0x04,0x25,0x60,0x00,0x00,0x00,0x48,0x8b,0x40,0x18,0x4c,0x8b,0x50,0x20,0x4d,0x89,0xd1,0x0f,0x1f,0x44,0x00,0x00,0x49,0x8b,0x41,0x50,0x4c,0x8d,0x40,0x02,0x0f,0xb7,0x00,0x66,0x85,0xc0,0x74,0x2a,0x31,0xc9,0x66,0x0f,0x1f,0x44,0x00,0x00,0x89,0xca,0x0f,0xb7,0xc0,0x49,0x83,0xc0,0x02,0xc1,0xe2,0x04,0x01,0xd0,0x01,0xc1,0x41,0x0f,0xb7,0x40,0xfe,0x66,0x85,0xc0,0x75,0xe6,0x81,0xf9,0x00,0x27,0x9b,0x77,0x74,0x3f,0x4d,0x8b,0x09,0x4d,0x39,0xca,0x75,0xbe,0x4c,0x89,0xd9,0xba,0x86,0x45,0x6a,0xef,0xe8,0xd9,0xfe,0xff,0xff,0x48,0x8d,0x4c,0x24,0x25,0xff,0xd0,0xba,0x7f,0x30,0x7b,0xb4,0x48,0x89,0xc1,0xe8,0xc5,0xfe,0xff,0xff,0x48,0x8d,0x54,0x24,0x20,0x45,0x31,0xc9,0x31,0xc9,0x49,0x89,0xd0,0xff,0xd0,0x31,0xc0,0x48,0x83,0xc4,0x38,0xc3,0x4d,0x8b,0x59,0x20,0xeb,0xc3,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };int main() { UINT64 pAddr; HANDLE hThread; pAddr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(pAddr, shellcode, sizeof(shellcode)); hThread = CreateThread(NULL, 0x00, pAddr, NULL, 0x00, NULL); WaitForSingleObject(hThread, INFINITE);}
x64 返回地址欺骗
System Informer - 线程信息

如我们所见,发起调用的位置来自一个未备份的内存区域。

让我们看看下面的代码在调试器中是什么样子。

#include<Windows.h>#include<stdio.h>int main() { MessageBoxA(NULL, "TEST""TEST", MB_OK);}
x64 返回地址欺骗
x64dbg - MessageBoxA

断点设置在MessageBoxA的第一条指令处。由于这个 API 是在"main()"函数内调用的,我们可以在栈顶看到它的返回地址。我们的目标是将这个值修改为一个受信任 DLL(如 KERNEL32.DLL) 中的地址。这样就会让它看起来像是从 KERNEL32.DLL 中调用的 MessageBoxA。当 MessageBoxA API 返回时,执行流会被定向到 KERNEL32.DLL 中的地址,并执行该地址处的指令。

返回地址的选择必须确保相应的指令能将执行流重定向回我们的代码,否则程序会崩溃。这类指令通常被称为 gadgets(小工具)。在我们的程序中将使用以下 gadget:jmp QWORD PTR [rbx]

我们选择一个合适的地址并将其指针放入"rbx"寄存器。当 KERNEL32.DLL 中的 gadget 被执行时,控制流会被重定向回存储在"rbx"中的地址,从而让我们重新获得控制权。

你也可以使用任何包含这些指令的 DLL。这里有一个简单的 Python 脚本来检查 DLL 是否可用。

withopen("C:\Windows\System32\kernel32.dll"'rb'as binfile:    bindata = binfile.read()for i inrange(0len(bindata)-1):if (bindata[i] == 0xffand bindata[i+1] == 0x23):print("[#] This DLL Works")break

实现

现在我们已经了解了要做什么,让我们开始实现。

C 程序

以下函数从已加载的 DLL 中检索我们的 gadget 的内存位置。

x64 返回地址欺骗
查找 Gadget 的函数

我们将所有必需的参数存储在一个结构体中,定义如下

x64 返回地址欺骗
  • "pRopGadget" 将保存我们的 gadget 的地址

  • "pTarget" 将包含需要调用的函数的地址。例如 MessageBoxA、VirtualAlloc 等

  • "dwNumberOfArgs" 用于指定目标函数需要的参数数量。例如 MessageBoxA 需要 4 个参数

  • "pEbx" 用于在执行 gadget 时保存我们想要的地址

  • "pArgs" 指向目标函数的参数

我们将使用一个函数来初始化这个结构体和目标函数所需的参数。

x64 返回地址欺骗
初始化 STACK_CONFIG 结构体的函数

该函数将最小参数数量配置为四个,如果参数数量为奇数,则加一。这样做是为了避免在汇编代码中进行复杂的步骤来确保栈对齐。

在进入汇编部分之前,让我们先了解 Windows x64 调用约定和栈对齐要求。

Windows x64 使用 fastcall 调用约定。在 fastcall 调用约定中,函数的前四个参数按以下顺序存储在寄存器中:rcxrdxr8 和 r9。任何额外的参数都从右到左压入栈中。即第 5 个参数将位于栈顶,最后一个参数将位于栈底。如果参数数量为奇数,则会调整额外的 8 字节填充以保持栈对齐。

此外,在调用函数之前必须在栈上保留 32 位的空间,称为影子空间。被调用者使用这个空间来存储来自寄存器的前四个参数。Windows 还要求在调用函数之前栈必须 16 字节对齐。基本上,rsp 中的值必须能被 16 整除。

我们将为我们的 Spoof 函数定义一个原型,该函数将用汇编语言编写。这个函数将接收指向我们配置结构的指针作为参数,并负责欺骗返回地址。

x64 返回地址欺骗

Spoof 汇编代码

这是我们直接使用汇编的地方。这部分是必要的,因为我们想要对栈和寄存器值进行精细控制。

要开始编写汇编代码,请查看 g3tsyst3m 的 "x64 Assembly" 系列。

汇编代码

首先,我们将在 "main" 函数中的返回地址存储在 rdi 寄存器中。我们希望在完成我们的操作后从这个地址继续执行。一旦执行了 pop 指令,我们的栈将处于对齐状态,我们需要保持这种状态。下一组指令在寄存器中配置前四个参数。如果我们的目标函数有超过四个参数,要放在栈上的参数数量将存储在 r12 寄存器中。

x64 返回地址欺骗
汇编代码

现在我们必须将额外的参数存储在栈上。这将通过使用循环来完成。首先,通过减少 rsp 值来在栈上创建空间。由于我们已确保参数数量为偶数,栈仍将保持对齐。

x64 返回地址欺骗
汇编代码

现在我们的栈已经准备好了,我们必须执行以下步骤:

  • 分配影子空间

  • 将 gadget 的地址压入栈中,它将作为返回地址

  • 配置 rbx

  • 跳转到目标函数

x64 返回地址欺骗
汇编代码

最后一部分是 cleanup,它将把栈恢复到原始状态并返回到我们的 "main" 函数。

x64 返回地址欺骗

示例

让我们看一个使用这种技术执行 shellcode 的示例。

x64 返回地址欺骗
通过欺骗返回地址执行 Shellcode 的代码
x64 返回地址欺骗
弹出消息框的 Shellcode

我们可以看到我们的 Shellcode 已成功执行。让我们在调试器中分析这个过程。

**VirtualAlloc**处的断点

x64 返回地址欺骗
VirtualAlloc 处的返回地址

**CreateThread**处的断点

x64 返回地址欺骗
CreateThread 处的返回地址

**WaitForSingleObject**处的断点

x64 返回地址欺骗
WaitForSingleObject 处的返回地址

参考资料

  • https://sabotagesec.com/the-stack-series-return-address-spoofing-on-x64/

  • https://g3tsyst3m.github.io/shellcoding/assembly/debugging/x64-Assembly-&-Shellcoding-101/

原文始发于微信公众号(securitainment):x64 返回地址欺骗

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月20日13:37:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   x64 返回地址欺骗https://cn-sec.com/archives/3645395.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息