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就是通用的了。我们结合实际例子来应用它吧!”