我接触 Windows 最开始看的两本书是《PE 权威指南》和《Windows 核心编程》,学这两本书的目的也很简单:实现内存加载。我们知道,要实现内存加载,最重要的是处理 PE 中的三个表:导入表,iat 和重定位表。然而跟 pe 装载有关系的表却不仅仅只有这三个,那么剩下的表都有着怎样的内容?在 PE 的装在过程中发挥了什么样的作用?
毫无疑问,仅仅处理导入表和重定位表的内存加载是不完美的,只能实现部分 pe 的加载。
这个系列目的在于提供 pe 装载部分细节的索引,希望读者能通过这些索引去更深入的学习,以实现完美的内存加载技术。当然,不会有现成的代码,甚至不会有太多细节。
对 pe 这些表的了解过程一定程度上也代表了我的二进制学习历程。
PEB_LDR_DATA
尽管 PEB_LDR_DATA 并非是 pe 中的一个表,但是它记录了当前进程中到底有哪些模块被装载,如果要实现完美的内存加载,它是少不了的,因为 GetModuleHandle 是依赖于 LdrpCheckForLoadedDll ,而 LdrpCheckForLoadedDll 最终就是检查 PEB_LDR_DATA。
另外,如果要将内存加载的模块设置为主模块,需要修改 ` (HMODULE)(PVOID)NtCurrentPeb()->ImageBaseAddress`
//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
};
//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
};
TLS 表
Windows TLS (Thread Local Storage) 机制意在为每个线程提供的独立的存储空间,分为动态 TLS 和静态 TLS,动态 TLS 自然不必多说,通过 Windows Api 实现,而静态 TLS 则关乎 PE 的 TLS 表。
typedef struct _IMAGE_TLS_DIRECTORY64 {
ULONGLONG StartAddressOfRawData;
ULONGLONG EndAddressOfRawData;
ULONGLONG AddressOfIndex; // PDWORD
ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
DWORD SizeOfZeroFill;
union {
DWORD Characteristics;
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} IMAGE_TLS_DIRECTORY64;
在装载 pe 的时候,ntdll 使用 LdrpAllocateTlsEntry 为每个 image 分配 tls 表项,具体来说就是找到 image 的 tls 表,然后在内存中分配一个 buffer 将表中的数据拷贝到 buffer 中,并调用 LdrpAcquireTlsIndex 为这个 tls entry 分配一个 index(也就是 tls index),最后将这块 buffer 加入一个双向链表 LdrpTlsList。
在分配完 index 后,对于 LdrpTlsList 中的每个 tls entry,ntdll 将其包含的静态 tls data 写入 teb 中的 ThreadLocalStoragePointer 指向的数组中。
如果反汇编一段读取静态 tls 数据的代码,我们就可以看到,程序通过 tlsindex 在 ThreadLocalStoragePointer 中读取了数据。
mov eax,108
mov eax,eax
mov ecx,dword ptr ds:[<_tls_index>]
mov rdx,qword ptr gs:[58]
add rax,qword ptr ds:[rdx+rcx*8]
mov r9d,1
xor r8d,r8d
mov rdx,rax
xor ecx,ecx
call qword ptr ds:[<&MessageBoxA>]
最近几个月一直没有写博客,这篇文章写得也很简略,一方面因为脑子里确实没什么东西,另一方面也因为工作换了,精力少了很多,我也不确定这个系列是否能写完。虽然本文技术上的内容没有写多少,但是还有其他话想说正。如我去年所写的,“事情的发展总是凡人难以预料的,通过渗透入门安全的时候我无论如何也不会想到三年后已经早已不再接触渗透”,如今这似乎像预言一样的东西确实兑现了。尽管现在做的东西已经不属于安全行业了,但是我仍然认为它跟安全有着联系,我也仍然认为我是一个搞安全的。
最后仿写一段本人刚刚接触二进制时看到的一篇大佬的文章末尾写的话,我觉得此时此刻恰如彼时彼刻:
写这篇文章时笔者不禁想起了几年前刚成为黑客只是想绕过 360 做免杀的自己,如今几年过去了以笔者的能力自认为做到完美的免杀变成易如反掌的事情了,但是笔者却成为了一个送外卖的外卖小哥. 安全路漫漫, 要学的东西还有很多。