6.1.3 方法二、PEB获取GetProcAddrees函数地址

“我们还是来‘独览大略’吧!”老师说道,“斗转星移,随着时间的发展,有了更简单、优雅的搜索方法。”

“哦,是谁最先提出来的呢?”

“这个……我也不知道,估计最早提出来的时候,我连电脑都不知为何物呢!呵呵!”老师说,“我看国内倒是jneo和scz都有过详细的分析。这种方法还是分为两部分,第一部分是获得kerner32.dll的基址;第二部分是动态获得函数的地址。”

“哦!yange获得kerner32.dll基址的方法是暴力搜索吧?”古风问道。

“对!而jneo和scz则使用了改进的方法,利用PEB结构来获得kerner32.dll的基址。”

“PEB,进程环境块?好像以前介绍过。”大家隐约有点印象。

“是啊,在堆溢出利用时,我们介绍过覆盖PEB里面的RtlEnterCriticalSection函数指针,大家可翻翻以前的笔记。而这里,利用PEB获得kernel32.dll的原理如下:”

老师在黑板上写了下来。

1.fs寄存器指向TEB结构

2.在TEB+0x30地方指向PEB结构

3.在PEB+0x0C地方指向PEB_LDR_DATA结构

4.在PEB_LDR_DATA+0x1C地方就是一些动态连接库地址了,如第一个指向ntdll.dll,第二个就是kernel32.dll的地址。

“其结构示意图如图6-2。”

“老师,为什么给出的偏移量正好指向想要的结构呢?”玉波问道。

“因为……比尔.盖茨当初就是这么设计来着。”

“那比尔.盖茨为什么要这样设计呢?”玉波又问。

“那可能是由他生活的环境和个人性格造就的吧!”

“哦?是什么环境和性格造就了他这么设计呢?”玉波要打破沙锅问到底了。

“噢!那是美国的诞生和文化造就了那样的环境和性格呀!好了,要完全解决这个问题,我们就只有使用回溯法,回溯到亿万年前,宇宙大爆炸的时候,可能某个尘埃的偏移加速度的值,导致了今天有这么一位比尔.盖茨;可能那个尘埃的某次碰撞变向,导致了比尔.盖茨采用这样的结构设计。”

“……”大家无语了。

“呵呵,人类一思考,上帝就发笑。有很多事情,我们是无法探其根源的,只能接受!就如同我们不知道,也不用去知道,在大爆炸前的宇宙是什么样的一样,我们只能认为时间是从那一刻才开始的。”

“我们还是看看更关心的东西吧!利用PEB查找kernerl32地址的汇编实现吧!以下是汇编实现。”

mov eax, fs:0x30 ;PEB的地址
mov eax, [eax + 0x0c] ;Ldr的地址
mov esi, [eax + 0x1c] ;Flink地址
lodsd 
mov eax, [eax + 0x08] ;eax就是kernel32.dll的地址

“很优雅吧?呵呵!”老师问道。

“哇!和暴力搜索相比,简直是天壤之别啊!”

“呵呵!程序设计真的是门艺术啊!顺便说一下,在刚刚结束的29届ACM国际大学生程序设计竞赛亚洲赛区预赛北京赛的比赛中,上海交大以五道的成绩获得冠军!我们学校队过了三道题,获得铜奖。”

老师有点可惜的说:“这次很有希望啊,就差最后一点的突破了。大家多努力啊!你们是八九点钟的太阳!以后的希望就在你们的身上了。”

“如果能够代表学校去北京参加亚洲预赛的话,太精彩了!”大家的气势被带动了起来。

宇强心想,自己在大学生涯中一定要不断努力,努力,再努力!大学生活充满阳光,自己也需要充满激情和挑战;希望在自己毕业时,能对这四年青葱岁月无悔,还能像现在这样,对未来充满好奇和梦想!

“好,我们回到程序中,来测试一下。”老师的话把宇强拉了回来,“在VC中嵌入这几句汇编,然后调试执行,当执行完 mov eax, [eax + 0x08] 时,可以看到eax中保存了我们正确的kernel32.dll的地址——XP系统SP0下为0x77E40000。如图6-3。”

“哇!EAX真的是77E40000啊!实在太方便了。”

“嗯,我们继续,”老师一口气接着说道,“第二部分就是要动态获得函数地址了。”

“也是与系统结构相关吗?”

“当然,完全是Windows系统的结构让我们可以使用这种方法,如果拿到Linux下,就完全行不通了。”

“动态获得函数地址的部分和yuange使用的方法是一样的。”老师说道,“就是利用Kernel32.dll中的引出表!”

“每个dll都有引出表,这样,外部程序可以调用dll里实现的函数,而不必关心实现的细节。”老师接着说,“而GetProcAddress是在kernel32.dll里实现的,所以我们可通过查找kernel32.dll的引出表来找到GetProcAddress函数。”

“查找引出表?什么意思?”

“嗯,这就要解释一下引出表和我们找地址的过程了。有点麻烦,大家可要集中点精神听啊!”

“首先,引出表的结构如下:”

Typedef struct _IMAGE_EXPORT_DIRECTORY
{
    Characteristics; 4
    TimeDateStamp 4
    MajorVersion 2
  MinorVersion 2
  Name 4 模块名字
  Base 4 基数,加上序数就是函数地址数组的索引值
  NumberOfFunctions 4
    NumberOfNames 4
    AddressOfFunctions 4 指向函数地址数组
  AddressOfNames 4 函数名字的指针地址
  AddressOfNameOrdinal 4 指向输出序列号数组
}

每个字段的解释如表1。

“不用去记它,记下来也没用。我们只关心最后几个字段,如图6-4。”

  

“给大家解释一下图上的某些字段涵义:”

图6-4中的字段涵义:

NumberOfFunctions字段:为AddressOfFunctions指向的函数地址数组的个数,此例中,这里是3;

NumberOfName字段:为AddressOfNames指向的函数名称数组的个数,这里是2;

AddressOfFunctions字段:指向模块中所有函数地址的数组;

AddressOfNames字段:指向模块中所有函数名称的数组;

AddressOfNameOrdinals字段:指向AddressOfNames数组中函数对应序数的数组。

“我们查找函数地址时,先在函数名称数组中找到需要的函数名;然后在函数序号数组中得到对应的函数序号;最后根据这个序号,在函数地址数组中得到对应的地址值。”

“好抽象啊!头晕啊!老师,给个例子吧!”玉波嚷道。

“好,比如我们在那个引出表中查找MyFunc3函数的地址。”

“先从AddressOfName开始,依次在函数名数组中查找与MyFunc3相同的项,从而得到MyFunc3在函数名数组中是第几个函数。在图6-4的例子中,MyFunc3是第二个函数。”

“然后,我们从AddressOfNameOrdinals开始,在函数序号数组中查找MyFunc3函数对应的序号。在函数序号数组中,第二个函数序号数组的序号值是3。”

“最后,根据序号3,从AddressOfFunctions开始的函数地址数组中查找MyFunc3函数的地址。在函数地址数组中,第3项的值是0x400197。这样,我们就得到了MyFunc3函数的地址——0x400197。”

“哦,是这样啊!”

“但……输出表在哪儿呢?”一直没说话的宇强问道。

“呵呵,知道了kerner32.dll的基地址后,其PE头部偏移在kerner32.dll基址+0x3C的地方;而输出表的位置在kerner32.dll基地址+PE头部地址+0x78的地方。”

“而kerner32.dll的基地址我们刚刚学会了:从PEB中获得。什么预备工作都完成了,我们来看看搜索函数地址流程吧!”老师说道。

a. 通过TEB/PEB获取kernel32.dll基址

b. 在(基址+0x3c)处获取e_lfanewc就是PE标志。

c. 在(基址+e_lfanew+0x78)处获取引出表地址(后面为描述方便简称export)

d. 在(基址+export+0x1c)处获取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse。

e. 搜索AddressOfNames,确定“GetProcAddress”所对应的index

f. index = AddressOfNameOrdinalse [ index ];

g. 函数地址 = AddressOfFunctions [ index ];

“比如,我们想查找GetProcAddress的地址,就在函数名称数组中,搜索GetProcAddress的名称;找到后根据编号,在序号数组中,得到它对应的序号值;最后根据序号值,在地址数组中,提取出它的地址。其汇编代码如下,并给出了详细的解释。”

 __asm
{
    mov ebp, 0x77E40000 ;kernel32.dll 基址
    mov eax, [ebp+3Ch] ;eax = PE首部
    mov edx,[ebp+eax+78h]
    add edx,ebp ;edx = 引出表地址
    mov ecx , [edx+18h] ;ecx = 输出函数的个数
    mov ebx,[edx+20h] 
    add ebx, ebp ;ebx =函数名地址,AddressOfName 
    search:
    dec ecx
    mov esi,[ebx+ecx*4] 
    add esi,ebp ;依次找每个函数名称
    ;GetProcAddress
    mov eax,0x50746547
    cmp [esi], eax; 'PteG'
    jne search
    mov eax,0x41636f72
    cmp [esi+4],eax; 'Acor'
    jne search 
    ;如果是GetProcA,表示找到了 
    mov ebx,[edx+24h]
    add ebx,ebp ;ebx = 序号数组地址,AddressOf
    mov cx,[ebx+ecx*2] ;ecx = 计算出的序号值
    mov ebx,[edx+1Ch]
    add ebx,ebp ;ebx=函数地址的起始位置,AddressOfFunction
    mov eax,[ebx+ecx*4] 
    add eax,ebp ;利用序号值,得到出GetProcAddress的地址
}

“我们来测试一下吧!在VC中嵌入汇编,并单步调试,执行到最后一句时,可以发现EAX是0x77E5A5FD,即得到了我们系统XP SP0的GetProcAddress函数的地址:0x77E5A5FD,如图6-5。”

  

“我们直接获得地址看看!如图6-6,EAX果然也是一样,XP SP0的GetProcAddress函数地址就是0x77E5A5FD!”

“哇!太妙了!”大家说道。

“嗯,我们把获得Kernel32.dll地址部分和获得函数地址部分合起来,就可动态得到GetProcAddress函数的地址,而且与系统版本是无关的!”

“在动态得到了GetProcAddress函数的地址后,我们再使用它来动态获得其他函数的地址,这样生成的ShellCode就是通用的了。我们结合实际例子来应用它吧!”