📄 windows进程中的内存结构.txt
字号:
BaseAddress是NtAllocateVirtualMemory返回的地址。Buffer指向我们要写入的字节,BufferLength是我们要写入的字节数。
现在我们来挂钩单个进程。被加载入所有进程的动态链接库只有ntdll.dll。所以我们要检查被导入进程要挂钩的函数是否来自ntdll.dll。但是这些来自其它DLL的函数所在的内存可能已经被分配,这时重写它的代码会在目标进程里导致错误。这就是我们必须去检查我们要挂钩的函数来自的动态链接库是否被目标进程加载的原因。
我们需要通过NtQueryInformationProcess获取目标进程的PEB(进程环境块)。
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
我们把ProcessInformationClass设置为ProcessBasicInformation,然后PROCESS_BASIC_INFORMATION结构会返回到ProcessInformation缓冲区中,大小为给定的ProcessInformationLength。
#define ProcessBasicInformation 0
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
KAFFINITY AffinityMask;
KPRIORITY BasePriority;
ULONG UniqueProcessId;
ULONG InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
PebBaseAddress就是我们要寻找的东西。在PebBaseAddress+0C处是PPEB_LDR_DATA的地址。这些通过调用NtReadVirtualMemory来获得。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
变量和NtWriteVirtualMemory的很相似。
在PPEB_LDR_DATA+01C处是InInitializationOrderModuleList的地址。它是被加载进进程的动态链接库的列表。我们只对这个结构中的一些部分感兴趣。
typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
PVOID Next,
PVOID Prev,
DWORD ImageBase,
DWORD ImageEntry,
DWORD ImageSize,
...
);
Next是指向下一个记录的指针,Prev指向前一个,最后一个记录的会指向第一个。ImageBase是内存中模块的地址,ImageEntry是模快的入口点,ImageSize是它的大小。
对所有我们想要挂钩的库我们需要获得它们的ImageBase(比方调用GetModuleHandle或者LoadLibrary)。然后把这个ImageBase和InInitializationOrderModuleList的ImageBase比较。
现在我们已经为挂钩准备就绪。因为我们是挂钩正在运行的进程,所以可能我们正在改写代码的同时代码被执行,这时就会导致错误。所以首先我们就得停止目标进程里的所有线程。它的所有线程列表可以通过设置了SystemProcessAndThreadInformation的NtQuerySystemInformation来获得。有关这个函数的描述参考第4节。但是还得加入SYSTEM_THREADS结构的描述,用来保存线程的信息。
typedef struct _SYSTEM_THREADS {
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientId;
KPRIORITY Priority;
KPRIORITY BasePriority;
ULONG ContextSwitchCount;
THREAD_STATE State;
KWAIT_REASON WaitReason;
} SYSTEM_THREADS, *PSYSTEM_THREADS;
对每个线程调用NtOpenThread获取它们的句柄,通过使用ClientId。
NTSTATUS NtOpenThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId
);
我们需要的句柄被保存在ThreadHandle。我们需要把DesiredAccess设置为THREAD_SUSPEND_RESUME。
#define THREAD_SUSPEND_RESUME 2
ThreadHandle用来调用NtSuspendThread。
NTSTATUS NtSuspendThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
被挂起的进程就可以被改写了。我们按照"挂钩Windows API"里3.2.2节里描述的方法处理。唯一的不同是使用其它进程的函数。
挂钩完后我们就可以调用NtResumeThread恢复所有线程的运行。
NTSTATUS NtResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
=====[ 7.3 新进程 ]================================================
感染所有正在运行的进程并不能影响将要被运行的进程。我们可以每隔一定时间获取一次进程的列表,然后感染新的列表里的进程。但这种方法很不可靠。
更好的方法是挂钩新进程开始时肯定会调用的函数。因为所有系统中正在运行的进程都已经被挂钩,所以这种方法不会漏掉任何新的进程。我们可以挂钩NtCreateThread,但这不是最简单的方法。我们可以挂钩NtResumeThread,因为它也是每当新进程创建时被调用,它在NtCreateThread之后被调用。
唯一的问题在于,这个函数并不只在新进程被创建时调用。但我们能很容易解决这点。NtQueryInformationThread能给我们指定线程是属于哪个进程的信息。最后我们要做的就是检查进程是否已经被挂钩了。这通过读取我们要挂钩的函数的开始5个字节来完成。
NTSTATUS NtQueryInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ThreadInformationClass是信息分类,在这里它被设置为ThreadBasicInformation。ThreadInformation是保存结果的缓冲区,大小按字节计算为ThreadInformationLength。
#define ThreadBasicInformation 0
对ThreadBasicInformation返回这个结构:
typedef struct _THREAD_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PNT_TIB TebBaseAddress;
CLIENT_ID ClientId;
KAFFINITY AffinityMask;
KPRIORITY Priority;
KPRIORITY BasePriority;
} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;
ClientId是线程所属进程的PID。
现在我们来感染新进程。问题就是新进程的地址空间中只有ntdll.dll,其他的模块在调用NtResumeThread之后被加载。有几种方法可以解决这个问题,比方说我们可以挂钩一个名为LdrInitializeThunk的API函数,它在进程初始化时被调用。
NTSTATUS LdrInitializeThunk(
DWORD Unknown1,
DWORD Unknown2,
DWORD Unknown3
);
首先我们先运行原始的代码,然后挂钩新进程里所有要挂钩的函数。但最好对LdrInitializeThunk解除挂钩,因为这个函数在之后要被调用很多次,我们并不需要重新再挂钩所有的函数。这时在程序执行第一个指令前所有工作已经完成。这就是为什么在我们挂钩它之前它没有机会调用任何一个被挂钩过的函数的原因。
对自己挂钩和动态挂钩正在运行的进程一样,只是这里我们不需要关心正在运行的线程。
=====[ 7.4 DLL ]================================================
系统中每个进程都是一份ntdll.dll拷贝。这意味着我们可以在进程初始化阶段挂钩这个模块里的任意一个函数。但是来自其它模块比如kernel32.dll或advapi32.dll的函数该怎么办呢?还有一些进程只有ntdll.dll,其他模块都是在进程被挂钩之后在运行过程中才被动态加载的。这就是我们还得挂钩加载新模块的函数LdrLoadDll的原因。
NTSTATUS LdrLoadDll(
PWSTR szcwPath,
PDWORD pdwLdrErr,
PUNICODE_STRING pUniModuleName,
PHINSTANCE pResultInstance
);
这里对我们来说最重要的是pUniModuleName,它保存模块名字。当调用成功后pResultInstance保存模块地址。
我们首先调用原始的LdrLoadDll然后挂钩被加载模块里所有函数。
=====[ 8. 内存 ]===========================================
当我们正在挂钩一个函数时我们会修改它开始的字节。通过调用NtReadVirtualMemory任何人都可以检测出函数被挂钩。所以我们还要挂钩NtReadVirtualMemory来防止检测。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
我们修改了我们挂钩的函数开始的字节并且为我们新的代码分配了内存。我们就需要检查时候有人读取了这些代码。如果我们的代码出现在BaseAddress到BaseAddress+BufferLength中我们就需要在缓冲区中改变它的一些字节。
如果有人在我们分配的内存中查询字节我们就返回空的缓冲区和错误STATUS_PARTIAL_COPY。这个值用来表示被请求的字节并没有完全被拷贝到缓冲区中,它也同样被用在当请求了未分配的内存时。这时ReturnLength应该被设为0。
#define STATUS_PARTIAL_COPY 0x8000000D
如果有人查询被挂钩的函数开始的字节我们就调用原始代码并拷贝原始代码里开始的那些字节到缓冲区中。
现在新进程已无法通过读取它的内存来检测是否被挂钩了。同样如果你调试被挂钩的进程调试器也会用问题,它会显示原始代码,但却执行我们的代码。
为了使隐藏更完美,我们还要挂钩NtQueryVirtualMemory。这个函数用来获取虚拟内存的信息。我们挂钩它来防止探测我们分配的虚逆内存。
NTSTATUS NtQueryVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
OUT PVOID MemoryInformation,
IN ULONG MemoryInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
MemoryInformationClass标明了返回数据的类别。我们对开始的2种类型感兴趣。
#define MemoryBasicInformation 0
#define MemoryWorkingSetList 1
对MemoryBasicInformation返回这个结构:
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
ULONG RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
每个区段都有它的大小RegionSize和它的类型Type。空闲内存的类型是MEM_FREE。(区段对象就是文件映射对象,是可被映射到一个进程的虚逆地址空间的对象)
#define MEM_FREE 0x10000
如果我们代码之前一个区段的类型是MEM_FREE我们就在它的RegionSize加上我们代码的区段的大小。如果我们代码之后的区段的类型也是MEM_FREE那么就在之前区段的RegionSize上再加上之后的空闲区段的大小。
如果我们代码之前的区段是其它类型,我们就对我们代码的区段返回MEM_FREE。它的大小根据之后的区段来计算。
对MemoryWorkingSetList返回这个结构:
typedef struct _MEMORY_WORKING_SET_LIST {
ULONG NumberOfPages;
ULONG WorkingSetList[1];
} MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;
NumberOfPages是WorkingSetList中列项的数目。这个数字应该减少一些。我们在WorkingSetList中找到我们代码的区段然后把之后记录前移。WorkingSetList是按DWORD排列的数组,每个元素的高20位标明了区段地址,低12位是标志。
=====[ 9. 句柄 ]=========================================
用类SystemHandleInformation来调用NtQuerySystemInformation会在_SYSTEM_HANDLE_INFORMATION_EX结构中获取所有被打开的句柄的数组。
#define SystemHandleInformation 0x10
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX {
ULONG NumberOfHandles;
SYSTEM_HANDLE_INFORMATION Information[1];
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
ProcessId标明了拥有句柄的进程。ObjectTypeNumber是句柄类型。NumberOfHandles是Information数组中元素的数量。隐藏其中一项是很麻烦的,我们要去掉所有之后的元素并减少NumberOfHandles。去掉之后所有元素是必须的,因为数组中句柄是按ProcessId分组的。这意味着一个来自同一个进程中的所有句柄都在一块儿。对于一个进程变量Handle的数量是不断增加的。
现在回想一下这个函数(NtQuerySystemInformation)使用SystemProcessAndThreadsInformation类来调用时返回的结构_SYSTEM_PROCESSES。这里我们能够看到每个进程都有它自己的句柄的数量在HandleCount中。如果我们想要做得更完美我们就应该修改HandleCount,因为用SystemProcessesAndThreadsInformation类调用这个函数时隐藏了不少句柄。但校正是非常浪费时间的。在系统正常运行的一小段时间里就会有很多句柄正在打开或关上。所以在对这个函数两次紧挨着的调用句柄的数量被更改是很正常的,所以我们根本不需要改变HandleCount。
=====[ 9.1 命名句柄并获取类型 ]===================================
隐藏句柄很麻烦,但找出哪个句柄该被隐藏更困难一些。比方说我们要隐藏一个进程就要隐藏它的所有句柄并隐藏所有和它有联系的句柄。我们比较句柄的ProcessId参数和想要隐藏的进程的PID,如果它们相等就隐藏这个句柄。但是其它进程的句柄在我们能比较任何东西之前不得不先命名。系统中句柄的数量通常很庞大,所以最好在尝试命名之前先比较句柄类型。命名类型可以为我们不感兴趣的句柄省不少时间。
命名句柄和句柄类型通过调用NtQueryObject来完成。
NTSTATUS ZwQueryObject(
IN HANDLE ObjectHandle,
IN OBJECT_INFORMATION_CLASS ObjectInformationClass,
OUT PVOID ObjectInformation,
IN ULONG ObjectInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ObjectHandle是我们想要获取有关信息的句柄,ObjectInformationClass是信息类型,保存在以字节计算长度为ObjectInformationLength的缓冲区ObjectInformation中。
我们对OBJECT_INFORMATION_CLASS使用的类是ObjectNameInformation和ObjectAllTypesInformation。ObjectNameInfromation类在缓冲区中返回OBJECT_NAME_INFORMATION结构,而ObjectAllTypesInformation类返回OBJECT_ALL_TYPES_INFORMATION结构。
#define ObjectNameInformation 1
#define ObjectAllTypesInformation 3
typedef struct _OBJECT_NAME_INFORMATION {
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
Name决定了句柄的名字。
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING Name;
ULONG ObjectCount;
ULONG HandleCount;
ULONG Reserved1[4];
ULONG PeakObjectCount;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -