近年来,我们发现恶意使用直接系统调用来逃避安全产品挂钩的情况有所增加。这些挂钩用于监视可能存在恶意活动的 API 调用。
什么是API hook
API 挂钩是防病毒和 EDR 解决方案使用的一种技术,旨在实时监控进程和代码行为。无论何时产生一个新进程,EDR 负责将自己注入进程的内存,加载挂钩 DLL,以确定活动是否是恶意的。通常,EDR 解决方案将挂钩 NTDLL.dll 中的 Windows API,因为 NTDLL.dll 库中的 API 是进行系统调用之前调用的最后一个 API,系统调用会将执行上下文切换到内核。EDR 解决方案通常会针对已知恶意软件开发人员使用的 API,比如NtMapViewOfSection。
下图展示一个没有挂钩的 API 的示例:
将一个值从 rcx 移动到 r10寄存器中,然后将28(syscall 编号)移动到 eax 中,然后执行 syscall 命令。
被挂钩的API示例:
在这个被钩住的 API 中,指令被 jmp < address > 指令覆盖。如果我们遵循这个 jmp 指令,我们将被引导到以下位置:
在这里,一些值在 RCX 和 RSP 寄存器中被混合。然后我们调用一个位于7FFEB06F04F3的函数。如果我们调用这个函数,我们将跳转到00007FF4B0E31000的位置。经过进一步检查,我们发现所有寄存器的值都被保存到堆栈中,并可能在以后用于检查。如果任何东西被发现是恶意的,那么反病毒产品将终止程序的执行。
恶意软件如何规避的呢
直接系统调用规避方法是从ntdll.dll中读取系统调用号,将相应的系统调用号放入eax寄存器中,将函数参数放入堆栈中,然后使用syscall或int 0x2e命令直接进入内核。这样,就不会调用 ntdll.dll 中的函数,该钩子对于检测恶意活动毫无用处。这张照片显示了大多数恶意软件如何执行直接系统调用:
直接系统调用通常用于悄悄地将恶意代码注入其他进程。在32位系统中,可以通过hook SSDT来监控内核中的系统调用。但是在 Windows Vista 及更高版本中(仅限 64 位)。由于补丁保护机制,这是不可能的。
简要介绍恶意活动的痕迹
由于缺少钩子,我们无法跟踪从 ntdll.dll 调用的函数。但我们能追踪到什么呢?我们能得到什么?让我们收集并检查证据,然后得出如何减轻这些恶意软件的结论。
我们记得,对于系统调用方法上下文中的代码注入,我们需要执行两项任务:
-
读取ntdll.dll中的系统调用号
-
执行功能(导致远程线程/排队 APC/进程创建)
下面举例三种常见的方式读取ntdll.dll,并且分析利弊、特征:
-
双重加载——使用包含 ntdll.dll 的新拷贝的部分从磁盘调用 NtMapViewOfSection。
优点:看起来 ntdll.dll 是由 Windows 加载程序以一种典型的方式加载的。
缺点:可以在加载模块中找到。
特征:可以通过 NtMapViewOfSection 上的钩子进行跟踪。
NTSTATUS Status;
LARGE_INTEGER SectionOffset;
SIZE_T ViewSize;
PVOID ViewBase;
HANDLE SectionHandle;
OBJECT_ATTRIBUTES ObjectAttributes;
UNICODE_STRING KnownDllsNtDllName;
FARPROC Function;
INIT_UNICODE_STRING(
KnownDllsNtDllName,
L"\KnownDlls\ntdll.dll"
);
InitializeObjectAttributes(
&ObjectAttributes,
&KnownDllsNtDllName,
OBJ_CASE_INSENSITIVE,
0,
NULL
);
Status = NtOpenSection(
&SectionHandle,
SECTION_MAP_EXECUTE | SECTION_MAP_READ | SECTION_QUERY,
&ObjectAttributes
);
if(!NT_SUCCESS(Status)) {
SET_LAST_NT_ERROR(Status);
printf("Unable to open section %ldn", GetLastError());
goto cleanup;
}
//
// Set the offset to start mapping from.
//
SectionOffset.LowPart = 0;
SectionOffset.HighPart = 0;
//
// Set the desired base address and number of bytes to map.
//
ViewSize = 0;
ViewBase = NULL;
Status = NtMapViewOfSection(
SectionHandle,
NtCurrentProcess(),
&ViewBase,
0, // ZeroBits
0, // CommitSize
&SectionOffset,
&ViewSize,
ViewShare,
0,
PAGE_EXECUTE_READ
);
if(!NT_SUCCESS(Status)) {
SET_LAST_NT_ERROR(Status);
printf("Unable to map section %ldn", GetLastError());
goto cleanup;
}
Function = (FARPROC)GetProcAddressFromEAT(ViewBase, "NtOpenProcess");
printf("NtOpenProcess : %p, %ldn", Function, GetLastError());
cleanup:
if(ViewBase != NULL) {
NtUnmapViewOfSection(
NtCurrentProcess(),
ViewBase
);
}
if(SectionHandle != NULL) {
NtClose(SectionHandle);
}
-
从磁盘读取-使用 NtReadFile, NtOpenFile,NtCreateFile 并手动从磁盘映射 ntdll.dll 的新副本。
优点:映射是手动完成的,因此我们无法使用调试器在已加载的模块中找到 ntdll.dll。
缺点:从磁盘读取 ntdll.dll 是可疑的,因为每个进程都已经加载了它。
特征:可以通过 NtReadFile、NtOpenFile、NtCreateFile 上的钩子跟踪。
NTSTATUS Status;
LARGE_INTEGER SectionOffset;
SIZE_T ViewSize;
PVOID ViewBase=NULL;
HANDLE FileHandle=NULL, SectionHandle=NULL;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK StatusBlock;
UNICODE_STRING FileName;
FARPROC Function;
//
// Try open ntdll.dll on disk for reading.
//
INIT_UNICODE_STRING(
FileName,
L"\??\C:\Windows\System32\ntdll.dll"
);
InitializeObjectAttributes(
&ObjectAttributes,
&FileName,
OBJ_CASE_INSENSITIVE,
0,
NULL
);
Status = NtOpenFile(
&FileHandle,
FILE_READ_DATA,
&ObjectAttributes,
&StatusBlock,
FILE_SHARE_READ,
NULL
);
if(!NT_SUCCESS(Status)) {
SET_LAST_NT_ERROR(Status);
printf("NtOpenFile failed %ldn", GetLastError());
goto cleanup;
}
//
// Create section
//
Status = NtCreateSection(
&SectionHandle,
SECTION_ALL_ACCESS,
NULL,
NULL,
PAGE_READONLY,
SEC_IMAGE,
FileHandle
);
if(!NT_SUCCESS(Status)) {
SET_LAST_NT_ERROR(Status);
printf("NtCreateSection failed %ldn", GetLastError());
goto cleanup;
}
//
// Set the offset to start mapping from.
//
SectionOffset.LowPart = 0;
SectionOffset.HighPart = 0;
//
// Set the desired base address and number of bytes to map.
//
ViewSize = 0;
ViewBase = NULL;
Status = NtMapViewOfSection(
SectionHandle,
NtCurrentProcess(),
&ViewBase,
0, // ZeroBits
0, // CommitSize
&SectionOffset,
&ViewSize,
ViewShare,
0,
PAGE_EXECUTE_READ
);
if(!NT_SUCCESS(Status)) {
SET_LAST_NT_ERROR(Status);
printf("Unable to map section %ldn", GetLastError());
goto cleanup;
}
Function = (FARPROC)GetProcAddressFromEAT(ViewBase, "NtOpenProcess");
printf("NtOpenProcess : %p, %ldn", Function, GetLastError());
cleanup:
if(ViewBase != NULL) {
NtUnmapViewOfSection(
NtCurrentProcess(),
ViewBase
);
}
if(SectionHandle != NULL) {
NtClose(SectionHandle);
}
if(FileHandle != NULL) {
NtClose(FileHandle);
}
-
读取已经加载到进程中的现有 ntdll.dll
优点:没有被钩子发现,没有留下任何证据。
缺点:如果函数被安全产品挂钩会被发现。
特征:具体情况具体分析。
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
EXTERN_C NTSTATUS NtProtectVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN OUT PSIZE_T RegionSize,
IN ULONG NewProtect,
OUT PULONG OldProtect);
EXTERN_C NTSTATUS NtCreateThreadEx(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
EXTERN_C NTSTATUS NtWaitForSingleObject(
IN HANDLE Handle,
IN BOOLEAN Alertable,
);
EXTERN_C NTSTATUS NtOpenSection(
OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
using MyNtMapViewOfSection = NTSTATUS(NTAPI*)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);
LPVOID getNtdll() {
LPVOID pntdll = NULL;
//Create our suspended process
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
CreateProcessA("C:\Windows\System32\notepad.exe", NULL, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
if (!pi.hProcess)
{
printf("[-] Error creating processrn");
return NULL;
}
//Get base address of NTDLL
HANDLE process = GetCurrentProcess();
MODULEINFO mi;
HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");
GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
pntdll = HeapAlloc(GetProcessHeap(), 0, mi.SizeOfImage);
SIZE_T dwRead;
BOOL bSuccess = ReadProcessMemory(pi.hProcess, (LPCVOID)mi.lpBaseOfDll, pntdll, mi.SizeOfImage, &dwRead);
if (!bSuccess) {
printf("Failed in reading ntdll (%u)n", GetLastError());
return NULL;
}
TerminateProcess(pi.hProcess, 0);
return pntdll;
}
还有其他方式。
如何找到恶意活动的痕迹
我们知道恶意软件需要获取系统调用号。在 ntdll.dll 中,在相应函数的
mov eax,SYSCALL _ NUMBER
命令处存在系统调用编号。这些数字会从一个操作系统版本转换到另一个(包括服务包) 因此不太容易在恶意软件中硬编码。这个 dll 加载到系统中的每个进程。因此,加载另一个副本可能是可疑的。
使用最初加载的 ntdll.dll 并不是一个好主意,因为如果这些函数被安全产品挂钩,函数的起始位置已经更改,“ mov”命令不再位于相同的位置。它将不能轻易找到系统调用号。
从 Windows732位(系统调用编号0xa8)和 Windows1064位(系统调用编号0x28)的 NtMapViewOfSection API 调用片段显示,不同操作系统的系统调用编号不同。
NtMapViewOfSection in ntdll.dl, Windows 7 32-bit
NtMapViewOfSection in ntdll.dll, Windows 10 64-bit
下面来分析介绍的第一种方法:双重加载
这个方法是使用一个包含 ntdll.dll 的新副本的节调用 NtMapViewOfSection。此节对象是使用 NtCreateSection 创建的,并使用 ntdll.dll 的文件句柄,可以使用 NtCreateFile 获取该句柄。
这样,我们将在已加载的模块列表中看到另一个已加载的 ntdll.dll 模块。
使用内存映射文件:
在 MapViewOfFile 内部,有一个对 NtMapViewOfSection 的调用。
第二种方法:使用ReadFile、NtReadFile从磁盘读取ntdll
借助微软的SysInternals Process Monitor,我们可以很容易地发现这个活动:
第三种方法:读取已经加载到进程中的现有 ntdll.dll
查找的函数之一是 NtCreateSection。在这个函数上,我们没有钩子,所以它会成功地将它的 syscall 号(不仅如此,还有该函数的下一个指令)复制到另一个内存区域:
查找NtCreateSection
新内存区域中的 NtCreateSection 函数
这就是被钩住的 UnmapViewOfSection 的样子:
复制失败时调用API,会被反病毒产品发现。
原文始发于微信公众号(疯猫网络):恶意软件攻击技术大揭秘——直接系统调用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论