·获取PEB_LDR_DATA
关于获取Ldr的方法,网上有大量的博客作了介绍,但可能因为这些博客写的比较早,所以其中一些类似于“fs[30]指向PEB,PEB+0x0C即为Ldr指针”的观点虽然在当时非常适用,但在如今则显得不尽严谨,因为x86和x64下TEB、PEB、PEB_LDR_DATA等结构体的成员大小是不同的,使用的部分寄存器也有差异,例如x64下通过gs[0x60]获取PEB,而x86下通过fs[0x30]。用WinDbg查看结构体,对照如下:
x64 x86
TEB
PEB
PEB_LDR_DATA
从上面几张图,可以很直观地看出x86和x64环境下的各成员偏移有很大的不同,但是获取PEB_LDR_DATA的思路是完全相同的,即TEB - PEB - PEB.Ldr (指向PEB_LDR_DATA的指针)-> PEB_LDR_DATA。
·获取其他进程的PEB_LDR_DATA
1.读取其他进程的内存信息需要先获取其进程句柄,句柄可以通过OpenProcess函数获得,在此之前还需要获得这个进程的PID,作为OpenProcess的参数。获取PID的方法在此不多赘述。
2.使用未公开的函数NtQueryInformationProcess,使用这个函数首先要包含Winternl.h这个头文件,并且从ntdll.dll中动态获取函数地址。具体步骤如下:
首先按照NtQueryInformationProcess的参数格式创建函数指针My_Query_Information_Process。具体信息参照MSDN:https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms684280(v=vs.85).aspx。
#include
typedef NTSTATUS(WINAPI * MY_QUERY_INFORMATION_PROCESS)
(__in HANDLE ProcessHandle, __in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation, __in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength);
接着通过GetModuleHandle获取ntdll.dll的模块基址,并使用GetProcAddress获得NtQueryInformationProcess的函数地址然后将返回值赋值给My_Query_Information_Process(demo中不考虑返回值为NULL的情况)。
MY_QUERY_INFORMATION_PROCESS NtQueryInformationProcess =
(MY_QUERY_INFORMATION_PROCESS)(GetProcAddress(GetModuleHandle("ntdll"),
"NtQueryInformationProcess"));
定义info和ReturnSize作为函数参数传入,其中info用于保存进程信息。PROCESS_BASIC_INFORMATION结构体的定义可在Winternl.h中查看。
PROCESS_BASIC_INFORMATION info; //保存进程信息
ULONG ReturnSize;
NtQueryInformationProcess(Handle, ProcessBasicInformation, &info,
sizeof(PROCESS_BASIC_INFORMATION), &ReturnSize);
在PROCESS_BASIC_INFORMATION结构体中的指针成员PebBaseAddress指向进程的PEB基址。
获取了PEB,即意味着获得了Ldr指针,获得了Ldr,PEB_LDR_DATA还会远吗?代码如下,首先根据目标程序是x86还是x64来动态赋值offset,接着使用ReadProcessMemory读取进程addrpoint处的值,即PEB_LDR_DATA地址。
//通过进程信息中的pebbaseaddress获取PEB基址,进而获取指定进程内存空间中PEB_LDR_DATA的地址
DWORD offset = IsX86() ? 0xc : 0x18;//根据目标进程是x86还是x64程序,动态赋值
DWORD addrpoint = (DWORD)info.PebBaseAddress + offset; //addrpoint等于Ldr,是指向PEB_LDR_DATA的指针
DWORD ldraddr=0;
ReadProcessMemory(Handle, (LPCVOID)addrpoint, &ldraddr, sizeof(DWORD), NULL);//读取PEB_LDR_DATA的地址
·理解并充分利用PEB_LDR_DATA中最重要的3条双向链表
无论在x86程序中,还是在x64程序中,PEB_LDR_DATA这个结构体的成员顺序是固定的,而其中最重要的则是下图中的3条链表(_LIST_ENTRY就是双向链表,成员为前驱节点FLink和后驱节点BLink)。另外,以左图为例,因为结构体成员对齐,所以虽然UChar的大小为1字节,但Initialized占用4个字节,右图亦然,好吧跑题了。
PEB_LDR_DATA中的3个链表可以理解为是3个连续的_LIST_ENTRY结构体的拷贝,而这3个_LIST_ENTRY本体是另一个结构体LDR_DATA_TABLE_ENTRY的前3个成员。关系较复杂,用图来描述更加直观。如下图。
整体的关系如下:
InLoadOrderModuleList的地址是每一组链表的起始地址,同时也是对应的LDR_DATA_TABLE_ENTRY的起始地址,所以我们可以通过循环遍历双向链表InLoadOrderModuleList来遍历LDR_DATA_TABLE_ENTRY,并从中获取我们需要的模块信息。
既然LDR_DATA_TABLE_ENTRY中的3个核心双向链表均存储于可读可写的内存区域,那么通过修改内存实现反模块枚举是完全可行的。具体方法为循环遍历LDR_DATA_TABLE_ENTRY结构体,直至结构体成员BaseDllName与需要隐藏的Dll模块名相同,然后将前驱节点的BLink指向后驱节点,将后驱节点的FLink指向前驱节点,这样在循环链表中此dll便完成了脱链隐藏。
用代码实现隐藏当前进程中的模块非常容易,隐藏其他进程中模块的代码稍微复杂一些,需要反复地调用ReadProcessMemory和WriteProcessMemory函数。下面是隐藏其他进程中模块的关键代码:
//双向链表结构
PLIST_ENTRY Referpoint = new LIST_ENTRY;//参照点
ReadProcessMemory(Handle, (LPCVOID)(ldraddr + 0xc), Referpoint, sizeof(LIST_ENTRY), NULL);
PLIST_ENTRY temp = Referpoint; // 沿着链表前向遍历直至与参照点重合
//获取ldrdatatable
LDR_DATATABLE_ENTRY* ldrtable = new LDR_DATATABLE_ENTRY;
WCHAR * varname = new WCHAR[strlen(name) + 1];//存储当前遍历到的模块名的前strlen(name)个字符
varname[strlen(name)] = '\0';
for (int i = 0; i < dllnum; i++)
{
ReadProcessMemory(Handle, (LPCVOID)temp->Flink, ldrtable, sizeof(LDR_DATATABLE_ENTRY), NULL);
memset(varname, '\0', strlen(name) + 1);
ReadProcessMemory(Handle, (LPCVOID)ldrtable->BaseDllName.Buffer, varname, sizeof(WCHAR)*strlen(name), NULL);
//若模块名相同
if (strncmp(name, wide2char(varname), strlen(name)) == 0)//对比前strlen(name)个字符
{
//断链
//blink->flink=blink(地址)
char *addr = (char*)&ldrtable->InLoadOrderLinks.Flink; //保存用于写入的数据
WriteProcessMemory(Handle, (LPVOID)ldrtable->InLoadOrderLinks.Blink, addr, 4, NULL);
addr = (char*)&ldrtable->InMemoryOrderLinks.Flink;
WriteProcessMemory(Handle, (LPVOID)ldrtable->InMemoryOrderLinks.Blink, addr, 4, NULL);
addr = (char*)&ldrtable->InInitializationOrderLinks.Flink;
WriteProcessMemory(Handle, (LPVOID)ldrtable->InInitializationOrderLinks.Blink, addr, 4, NULL);
//flink->blink=flink+4
addr = (char*)&ldrtable->InLoadOrderLinks.Blink;
WriteProcessMemory(Handle, (LPVOID)((DWORD)ldrtable->InLoadOrderLinks.Flink + 4), addr, 4, NULL);
addr = (char*)&ldrtable->InMemoryOrderLinks.Blink;
WriteProcessMemory(Handle, (LPVOID)((DWORD)ldrtable->InMemoryOrderLinks.Flink + 4), addr, 4, NULL);
addr = (char*)&ldrtable->InInitializationOrderLinks.Blink;
WriteProcessMemory(Handle, (LPVOID)((DWORD)ldrtable->InInitializationOrderLinks.Flink + 4), addr, 4, NULL);
//内存释放
delete temp;
delete Referpoint;
delete[]varname;
delete[]name;
return;
}
//获取当前地址:先获取后一个节点的地址,再获取后一个节点的前向节点地址,即ldrtable当前指向的地址。
ReadProcessMemory(Handle, (LPCVOID)ldrtable->InLoadOrderLinks.Blink, temp, sizeof(LIST_ENTRY), NULL);
ReadProcessMemory(Handle, (LPCVOID)temp->Flink, temp, sizeof(LIST_ENTRY), NULL);
}
····················································································
隐藏模块对99%的人没有用处,隐藏其他进程的模块这个功能对99.99%的人没任何用处。剩下的1%或0.01%,一定知道这种雕虫小技需要配合其他操作才能真正起到隐藏保护的作用。例如:
1.动态加载待隐藏的dll,至少杜绝通过静态分析查dll宿主文件导入表就能查出dll的情况。
2.重写API,如loadlibrary,防止被hook或API断点跟踪。
3.消除模块内存中的PE特征,如“MZ”标、PE头标,防止被枚举有效模块内存。
4.如何过ZwQueryVirtualMemory和内核层ARK,仍在学习与思考中,有一定知识积累后再回来完善本文。
附上以前写的小程序(管理员模式运行,仅对x86程序有效,x64版本有空会补上)。
效果图:
隐藏前
多次隐藏后
链接:http://pan.baidu.com/s/1cnlg9S 密码:13e8