☆ 通过TEB/PEB枚举当前进程空间中用户模块列表
http://scz.617.cn/windows/200311231555.txt
实在没精力找出最早介绍该项技术的文章,尽管很想知道答案。29A杂志中大量使用 该技术([8]),并辅以文字说明,推荐阅读。下面操作是在windbg中进行的,提前在 注册表中设置好AeDebug,执行vulnerable_0.exe,进入windbg调试状态。也可通过 其它办法进入windbg调试状态。
段选择子FS所对应的段即当前线程TEB(Thread Environment Block),即FS:0指向TEB。
dg @fs Selector Base Limit Type DPL Size Gran Pres
0038 7ffde000 00000fff Data RW Ac 3 Big Byte P
r $teb $teb=7ffde000
TEB[0x30]是一个指向当前进程PEB(Process Environment Block)的指针。大家都这 么说,可他们如何知道这个偏移的,你该上哪去找TEB的数据结构定义,不同系统上 该结构定义一致吗。有这么多疑问,可绝大多数文章没有告诉你捞鱼的办法,只给你 鱼,你一定有种意尤未尽的郁闷。呵,了解,下面就是捞鱼的办法,windbg的dt命令:
dt ntdll!teb (列出匹配通配符的结构名) ntdll!_TEB ... ... dt -v -r ntdll!_TEB struct _TEB, 64 elements, 0xfb4 bytes +0x000 NtTib : struct _NT_TIB, 8 elements, 0x1c bytes +0x01c EnvironmentPointer : Ptr32 to Void +0x020 ClientId : struct _CLIENT_ID, 2 elements, 0x8 bytes +0x028 ActiveRpcHandle : Ptr32 to Void +0x02c ThreadLocalStoragePointer : Ptr32 to Void +0x030 ProcessEnvironmentBlock : Ptr32 to struct _PEB, 66 elements, 0x210 bytes ... ...
偏移、名称、类型、大小等等一应俱全地列举在此。万事不求人是不对的,指望别人 老来求自己更是不对的,藏着掖着是要一起挨米国鬼子打的。无论你用什么系统,去 下载相应版本的windbg,然后用上述办法自己核实偏移。MS一贯擅长保持向后兼容性, 这是它巨大成功的基础,类似0x30这样的偏移,被改动的可能性并不大。
号称新版Platform SDK的ntpsapi.h文件中直接给出了TEB的数据结构,我的不够新, 没找着这个文件。反正dt命令够我用了,懒得深究。
顺便提个事。肯定有人见过这样的代码,之后eax将指向PEB:
mov eax,fs:[18h]
mov eax,[eax+30h]
看完本文"新"的介绍,你肯定在疑惑中,这样的代码居然也能成功获取PEB地址,惟 一的解释就是fs:[18h]与fs:0h指向同一处(注意方括号的使用)。TEB结构第一成员是 NT_TIB结构,后者的Self成员指向自身这个NT_TIB结构,"碰巧"这个NT_TIB结构是 TEB结构第一成员,于是可以认为Self指向TEB。Self的偏移是0x18,就这么简单。
dt -v _NT_TIB $teb struct _NT_TIB, 8 elements, 0x1c bytes +0x000 ExceptionList : 0x0012ffb0 struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes +0x004 StackBase : 0x00130000 +0x008 StackLimit : 0x0012b000 +0x00c SubSystemTib : (null) +0x010 FiberData : 0x00001e00 +0x010 Version : 0x1e00 +0x014 ArbitraryUserPointer : (null) +0x018 Self : 0x7ffde000 struct _NT_TIB, 8 elements, 0x1c bytes
换句话说,ds:7ffde000h与fs:0h这两个不同的逻辑地址对应同一个线性地址。参看 <<IA-32 Software Developer's Manual. Volume 3>>([12])的3.1小节,下面是 Intel的标准定义:
逻辑地址
段选择子(16-bits):段内偏移(32-bits)
也叫远指针。程序中寻址时只能使用逻辑地址。没有办法禁用段机制,但有办法
禁用分页机制。
线性地址
逻辑地址经GDT、LDT转换后得到线性地址。
只需一条代码即可获取PEB地址:
mov eax,fs:[30h]
dd @fs:30 L1 0038:00000030 7ffdf000 dt ntdll!_TEB 7ffde000 +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : (null) +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : (null) +0x02c ThreadLocalStoragePointer : (null) +0x030 ProcessEnvironmentBlock : 0x7ffdf000 ... ... !teb TEB at 7ffde000 ExceptionList: 0012ffb0 ... ... PEB Address: 7ffdf000 ... ... r $peb $peb=7ffdf000 dd $teb+30 L1 (当前基是16进制) 7ffde030 7ffdf000
无数种办法可以得到指向PEB的指针0x7ffdf000。
Inside 2K([9])<<Table 7-6 Windows 2000 User Process Address Space Layout>> 中直接将TEB、PEB定位在0x7FFDE000、0x7FFDF000。一般来说是这样的,可是不建议 依赖硬编码地址。这就看当前最紧要的是广泛兼容性还是压缩代码空间。注意,讨论 前提是编写exploit、shellcode、virus,最终可能要汇编化的,而非常规编程。
PEB[0x0c]指向PEB_LDR_DATA结构,该结构有三个成员均可用于枚举当前进程空间中 的模块列表,区别在于加载顺序、内存顺序、初始化顺序。这是三个双向循环链表, 遍历时留神。
dt -v -r ntdll!_PEB struct _PEB, 66 elements, 0x210 bytes +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 SpareBool : UChar +0x004 Mutant : Ptr32 to Void +0x008 ImageBaseAddress : Ptr32 to Void +0x00c Ldr : Ptr32 to struct _PEB_LDR_DATA, 7 elements, 0x28 bytes ... ... dt -v -r ntdll!_PEB_LDR_DATA struct _PEB_LDR_DATA, 7 elements, 0x28 bytes +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr32 to Void +0x00c InLoadOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes +0x014 InMemoryOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes +0x01c InInitializationOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes +0x024 EntryInProgress : Ptr32 to Void dt -v -r ntdll!_LIST_ENTRY struct _LIST_ENTRY, 2 elements, 0x8 bytes +0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes +0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes dd $peb+c L1 7ffdf00c 00241e90 dt ntdll!_PEB_LDR_DATA 00241e90 +0x000 Length : 0x28 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x241ec0 - 0x2429d8 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x241ec8 - 0x2429e0 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x241f28 - 0x2429e8 ] +0x024 EntryInProgress : (null) dl 241f28 ffff 2 (注意Flink形成循环链表) 00241f28 00241fd0 00241eac 00241fd0 00242118 00241f28 00242118 00242258 00241fd0 00242258 002423a0 00242118 002423a0 00242300 00242258 00242300 002424e0 002423a0 002424e0 00242440 00242300 00242440 00242770 002424e0 00242770 002428a8 00242440 002428a8 00242808 00242770 00242808 002429e8 002428a8 002429e8 00241eac 00242808 00241eac 00241f28 002429e8 dlb 2429e8 ffff 2 (注意Blink形成循环链表) 002429e8 00241eac 00242808 00242808 002429e8 002428a8 002428a8 00242808 00242770 00242770 002428a8 00242440 00242440 00242770 002424e0 002424e0 00242440 00242300 00242300 002424e0 002423a0 002423a0 00242300 00242258 00242258 002423a0 00242118 00242118 00242258 00241fd0 00241fd0 00242118 00241f28 00241f28 00241fd0 00241eac 00241eac 00241f28 002429e8
单从windbg所给出的信息,只能看出三个双向循环链表的存在,每个链表结点上只有 前向指针、后向指针,如何获取最终的模块信息呢。其实这是很关键的问题,遗憾的 是某些文章不知出于什么动机刻意回避该问题。
Reactos与Tomasz Nowak提供了未公开的LDR_MODULE数据结构,我把偏移标出来了, 方便计算:
typedef struct _LDR_MODULE { LIST_ENTRY InLoadOrderModuleList; // +0x000 LIST_ENTRY InMemoryOrderModuleList; // +0x008 LIST_ENTRY InInitializationOrderModuleList; // +0x010 PVOID BaseAddress; // +0x018 PVOID EntryPoint; // +0x01C ULONG SizeOfImage; // +0x020 UNICODE_STRING FullDllName; // +0x024 UNICODE_STRING BaseDllName; // +0x02C ULONG Flags; // +0x034 SHORT LoadCount; // +0x038 SHORT TlsIndex; // +0x03A LIST_ENTRY HashTableEntry; // +0x03C ULONG TimeDateStamp; // +0x044 // +0x048 } LDR_MODULE, *PLDR_MODULE;
郑重推荐<<The Undocumented Functions For Microsoft Windows NT/2000>>,逆反 心理促使我一定要让大家都知道这个数据结构引自何处([10])。
说点题外话,Google是你的朋友。Windows底层编程在中国不知因为什么而显得资料 罕见,结果以前给我一个错觉,Windows底层编程举步维艰。后来因工作原因Google 时向Windows做了点偏移,发现国外此类编程资料并非如想像的那般罕见,有些人共 享出完整源代码后只是说如果感觉有帮助能否在网上订份小礼物给他,我是没境外信 用卡,否则一定订份小礼物给他。我还发现一件有意思的事,有人注明"原创"的时候 居然没有任何参考资源,不巧的是我看过的英文文章实在有点烂多,当创意、技巧、 代码片段重合得太多时,就有种无奈的感概。有鉴于此,己所不欲、勿施与人,写此 类型文章时一概不敢宣称是原创、翻译,撑死了是笔记,还生怕漏了引自何处。也可 能是我水平低,才如此。水平一高,就不便引他人之文了,谁知道呢。呵,很小的时 候听爹说过一句话,吃饭的永远不要责怪做饭的,确实如此,所以就不多评价了。
建议大家写文章时认真负责地给参考资源,有URL的都注明URL,不费什么事。
扯远了,回到LDR_MODULE结构上来,三个双向循环链表的结点正是该结构。
按加载顺序遍历:
!list -t _LIST_ENTRY.Flink -x "!ustr" -a "+24" 241ec0 !list -t _LIST_ENTRY.Flink -x "!ustr" -a "+2c" 241ec0
按内存顺序遍历:
!list -t _LIST_ENTRY.Flink -x "!ustr" -a "+1c" 241ec8 !list -t _LIST_ENTRY.Flink -x "!ustr" -a "+24" 241ec8
按初始化顺序遍历:
!list -t _LIST_ENTRY.Flink -x "!ustr" -a "+14" 241f28 String(58,520) at 00241f3c: D:\WINDOWS\System32\ntdll.dll String(64,66) at 00241fe4: D:\WINDOWS\system32\kernel32.dll ... ... !list -t _LIST_ENTRY.Flink -x "!ustr" -a "+1c" 241f28 String(18,20) at 00241f44: ntdll.dll String(24,26) at 00241fec: kernel32.dll ... ...
一般"按初始化顺序"前向遍历链表时,第一个节点对应ntdll.dll,第二个结点对应 kernel32.dll,我们不太关心其它模块。如果按加载顺序前向遍历,第一个节点对应 EXE文件本身,第二个节点才对应ntdll.dll。
编程枚举用户模块列表时,重点显示BaseAddress、FullDllName成员。总结一下全过 程:
a. 从fs:[30h]获取PEB地址
b. 从PEB[0x0c]获取Ldr地址
c. 从Ldr[0x0c]获取InLoadOrderModuleList.Flink
d. 从InLoadOrderModuleList.Flink开始前向遍历循环链表
e. 显示LDR_MODULE结构的BaseAddress、FullDllName成员。
下面是完整的C语言演示程序,汇编化留到编写完整shellcode时进行。
/*
-
- Compile : For x86/EWindows XP SP1 & VC 7
-
: cl EnumModule.c /nologo /Os /G6 /W3 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /link /RELEASE
-
:
- Create : 2003-08-12 11:36
- Modify :
-
*/
/*
- 按加载顺序遍历双向循环链表 */
#include <stdio.h> #include <stdlib.h>
#pragma comment( linker, "/INCREMENTAL:NO" ) #pragma comment( linker, "/subsystem:console" )
int __cdecl main ( int argc, char * argv[] ) { void *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL, *BaseAddress = NULL, *FullDllName = NULL;
__asm
{
mov eax,fs:[0x30]
mov PEB,eax
}
printf( "PEB = 0x%08X\n", PEB );
Ldr = *( ( void ** )( ( unsigned char * )PEB + 0x0c ) );
printf( "Ldr = 0x%08X\n", Ldr );
Flink = *( ( void ** )( ( unsigned char * )Ldr + 0x0c ) );
printf( "Flink = 0x%08X\n", Flink );
p = Flink;
do
{
BaseAddress = *( ( void ** )( ( unsigned char * )p + 0x18 ) );
FullDllName = *( ( void ** )( ( unsigned char * )p + 0x28 ) );
printf( "p = 0x%08X 0x%08X ", p, BaseAddress );
wprintf( L"%s\n", FullDllName );
p = *( ( void ** )p );
}
while ( Flink != p );
return( EXIT_SUCCESS );
执行效果如下。可以看出,尽管是循环链表,也可通过判断BaseAddress是否为NULL 来结束遍历。
EnumModule PEB = 0x7FFDF000 Ldr = 0x00241E90 Flink = 0x00241EC0 p = 0x00241EC0 0x00400000 X:\EnumModule.exe p = 0x00241F18 0x77F50000 X:\XP\System32\ntdll.dll p = 0x00241FC0 0x77E60000 X:\XP\system32\kernel32.dll p = 0x00241E9C 0x00000000
☆ 参考资源
[ 8] http://vx.netlux.org/vx.php?id=z001 (29A杂志)
Virus Writing Guide 1.00 for Win32 - Billy Belceb
29A-4.202
A guide to the latest methods to retrieve API's in a Win32 environment - LethalMind
29A-4.227
Gaining important datas from PEB under NT boxes - Ratter
29A-6.024
[ 9] <<Inside Microsoft Windows 2000 Third Edition>> - David A. Solomon, Mark E. Russinovich
[10] The Undocumented Functions For Microsoft Windows NT/2000 http://undocumented.ntinternals.net/ntundoc.chm
[12] Intel Architecture Software Developer's Manual. Volume 1 ftp://download.intel.com/design/PentiumII/manuals/24319002.pdf
Intel Architecture Software Developer's Manual. Volume 2
ftp://download.intel.com/design/PentiumII/manuals/24319102.pdf
Intel Architecture Software Developer's Manual. Volume 3
ftp://download.intel.com/design/PentiumII/manuals/24319202.pdf