栈溢出笔记1.6 地址问题(1)

前面的Shellcode中,我使用的都是自己XP机器上的硬编码地址。任何时候在Shellcode中使用硬编码地址都不是个好主意,这一点与动态库的重定位类似,一旦系统环境和程序编译设置发生变化,Shellcode几乎肯定会失效。因此,我们要找到更好一点的方法。

前面的Shellcode中,我用到了如下几个硬编码地址,它们的含义如下:
栈溢出笔记1.6 地址问题(1)_第1张图片
其中,LoadLibraryA的作用比较特殊,我们用它来加载user32.dll库。

现在我们要换掉这些硬编码地址。那么,如何得到这些API函数的地址呢?在动态链接库中获取函数地址有一个专门的函数——GetProcAddress,这个函数的原型如下:

/*****************************************************************************/
FARPROC WINAPI GetProcAddress(
  _In_ HMODULE hModule,
  _In_ LPCSTR  lpProcName
);
/*****************************************************************************/

这个函数第一个参数为模块的句柄,可以调用LoadLibraryA获得,第二个参数为函数名称。这样的话,MessageBoxA和ExitProcess都可以使用这种方式来获取。但是这依赖于两个函数:LoadLibraryA和GetProcAddress,那这两个函数的地址又怎么得到呢?

这两个函数都位于kernel32.dll,kernel32.dll肯定会加载,不需要我们自己加载。因此,现在的问题就是如何从kernel32.dll中找到LoadLibraryA和GetProcAddress的地址?

接下来就需要一点Windows内核和PE文件格式的知识了。我们知道,kernel32.dll为PE格式的动态链接库,要导出的函数放在PE文件的导出表中,因此,为了获取LoadLibraryA和GetProcAddress的地址,我们需要手动解析kernel32.dll的导出表。但在这之前,我们需要知道kernel32.dll在内存中的位置,也就是kernel32.dll的基地址。因此,问题总结为以下:
(1)如何获取kernel32.dll的基地址?
(2)如何在kernel32.dll的导出表中找到LoadLibraryA和GetProcAddress的地址?

先来解决第一个问题。我们知道,一个进程运行的时候,除了加载exe文件外,所依赖的.dll也会映射到进程的虚拟地址空间中,那么,这些加载的dll也是进程的财产,因此,进程会保存它们的信息。进程的信息都保存在PEB结构中,其中的Ldr(偏移为0xc0)指向一个PEB_LDR_DATA结构,这个结构保存的进程已加载模块的信息。这个结构如下:

/*****************************************************************************/
typedef struct _PEB_LDR_DATA
{
 ULONG Length;          // +0x00
 BOOLEAN Initialized;   // +0x04
 PVOID SsHandle;            // +0x08
 LIST_ENTRY InLoadOrderModuleList;      // +0x0c
 LIST_ENTRY InMemoryOrderModuleList;    // +0x14
 LIST_ENTRY InInitializationOrderModuleList;    // +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA;      // +0x24
/*****************************************************************************/

Windows并未完全公开(文档化)此结构。这是网上的版本,我们也可以通过Windbg来得到,这是我XP SP3机器上的该结构:
栈溢出笔记1.6 地址问题(1)_第2张图片
图37

这个结构的重点在于后面三个链表:InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList。从名称上看,这三个队列都是模块链表,第一个是按加载的先后顺序,第二个是按在虚拟空间中的位置,第三个是按初始化的顺序。第二个容易理解,但是第一个和第三个有什么区别呢?加载是先于初始化的,加载就是完成虚拟空间的映射和与exe的链接,加载完成后的DLL会挂入InInitializationOrderModuleList,进行初始化,初始化就是调用其DLLMain。可以看到的一点是InLoadOrderModuleList中有exe本身的模块,而InInitializationOrderModuleList中只有exe依赖的DLL。(当然,这随着系统版本在发生变化)

我们要使用的是InInitializationOrderModuleList,挂入该链表中的为LDR_DATA_TABLE_ENTRY,也就是说,每个加载的模块对应于一个LDR_DATA_TABLE_ENTRY,该结构如下:

/*****************************************************************************/
typedef struct _LDR_DATA_TABLE_ENTRY  
{  
 LIST_ENTRY InLoadOrderLinks; 
 LIST_ENTRY InMemoryOrderLinks; 
 LIST_ENTRY InInitializationOrderLinks; 
 PVOID DllBase; 
 PVOID EntryPoint; 
 DWORD SizeOfImage; 
 UNICODE_STRING FullDllName; 
 UNICODE_STRING BaseDllName; 
 DWORD Flags; 
 WORD LoadCount; 
 WORD TlsIndex; 
 LIST_ENTRY HashLinks; 
 PVOID SectionPointer; 
 DWORD CheckSum; 
 DWORD TimeDateStamp; 
 PVOID LoadedImports; 
 PVOID EntryPointActivationContext; 
 PVOID PatchInformation; 
}LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;  
/*****************************************************************************/

同样,在我的机器上为:

前面三个链表对应于该结构挂入的三个链表,重点在于第四个成员DllBase,这是载入模块的基地址。因此,我们只需要顺着InInitializationOrderModuleList链表找到kernel32.dll的PLDR_DATA_TABLE_ENTRY,然后通过其DllBase成员,就知道了kernel32.dll载入的地址。那么InInitializationOrderModuleList链表中哪一个kernel32.dll呢?最保险的方法是解析FullDllName成员,这样代码会比较复杂。实际上在特定版本的系统中,动态库初始化的顺序是一定的,第一个为ntdll.dll,第二个就是kernel32.dll。Vista 以后第二个是kernelbase.dll,第三个是kernel32.dll。因此,可以避免解析FullDllName成员。

现在,我们要找到进程的PEB结构地址,PEB结构保存于线程的TEB结构中的peb成员,而Windows系统中,寄存器fs总是指向当前线程的TEB。因此,获取kernel32.dll基地址的整个流程如下:

栈溢出笔记1.6 地址问题(1)_第3张图片
图38

写代码之前,先通过调试器顺着该顺序看一看,我用windbg加载的是example_1,输入:
d fs:[0x30]查看当前peb的地址:

栈溢出笔记1.6 地址问题(1)_第4张图片
图39

地址为0x7ffda000,输入:!peb,验证一下:

这里写图片描述
图40

是相同的。输入: d 7ffda000+0x0c,查看PEB_LDR_DATA结构的地址:


图41

输入: d 00251ea0+0x1c,查看InInitializationOrderModuleList链表:

栈溢出笔记1.6 地址问题(1)_第5张图片
图42

先列一下LIST_ENTRY结构,Flink 指向下一个节点:

/*****************************************************************************/
typedef struct _LIST_ENTRY 
{
 struct _LIST_ENTRY *Flink; 
 struct _LIST_ENTRY *Blink; 
} LIST_ENTRY, *PLIST_ENTRY;
/*****************************************************************************/

因此,0x00251f58是第一个元素。输入:d 0x00251f58查看以下其内容:

栈溢出笔记1.6 地址问题(1)_第6张图片
基地址为0x7c920000,但是右边显示为kernel32.dll???第一个元素不是ntdll.dll吗?

来看看下一个元素:

基地址为0x7c800000,显示为uer32.dll,看来是出了一些问题,输入!peb来看看:

栈溢出笔记1.6 地址问题(1)_第7张图片
这里的基地址和名称对应才是对的。

下面来写获取kernel32.dll基地址的代码:

/*****************************************************************************/
// example_8 获取kernel32.dll的基地址
#include <stdio.h>

// 获取kernel32.dll的基地址
int get_kernel32_base()
{
 __asm
 {
 mov eax, fs:[0x30] // PEB
 mov eax, [eax+0x0c] // PEB->Ldr
 mov eax, [eax+0x1c] // PEB->Ldr.InInitializationOrderModuleList.Flink(指向第一个元素)
 mov eax, [eax] // 指向第二个元素
 mov eax, [eax+0x08] // kernel32.dll基地址
 }
}

int main()
{
 printf("0x%x\n", get_kernel32_base());

 return 0;
}
/*****************************************************************************/

结果如下:

图43

这里是一种方法,其它方法(包括Windows 7)请看:

http://blog.harmonysecurity.com/2009_06_01_archive.html

你可能感兴趣的:(windows,栈溢出,shellcode)