怎样为程序打补丁(二)―――提高篇
声明:
一.本文实用于初学者,需要具备一定的汇编和系统底层的知识。
二.本文只是为了让广大网友共同提高一些基础知识,本人决无卖弄之意,只供需要这方面知识的读者阅读,如果你是高手,或者不需要这方面知识,请跳过。
三.本文的实例均为普通程序,如有雷同,敬请谅解。
四.本文欢迎传抄转载,如果是商业用途,请联系本人。http://www.zoudan.com zd_dan@263.com
不知道各位把“基础篇”中的内容搞懂没有,我现在要带领大家提高啦,请各位用心听讲!
这次要教大家的是如何为程序添加功能。大家都知道,在可执行文件当中只有少量的空间和位置可以放置我们的补丁程序,这就注定了要实现比较复杂的功能是非常有局限的。想象一下,如果在人家程序中添加大量的汇编代码,且只能是汇编代码(不要问我为什么),并且调试运行。因为是在人家的运行环境中运行,必须维护大量的寄存器,地址空间,堆栈等等,那将是何其困难和痛苦的一件事情。汇编能作很多事情,但是也不是万能,要用汇编实现很多复杂的功能简直就是变态的行为。我为大家制作的FolkQQ补丁程序,其实汇编代码也就几十行,功能也很简单,就几十个字节。但这已经使我够痛苦的了,如果再多些,常人是吃不消的(调试运行这些代码很费力,如果一个地方错了至少得重敲,而且死机重起是常事)。
看到这里,可能有同学会问,像你的SE补丁系列,实现了IP地址到具体物理地址的转换,难道也是你用汇编写的吗(用汇编写这个会死人的)?当然不是,大家注意到有个IPsearcher.dll,这是一个动态链接库,看名字就知道这是关于IP地址转换成实际地址功能的东东。对,所有的转换工作就是在其中完成。他是我用VC写的,程序很简单,但是如果用汇编写那就复杂了。我想我自己都没有毅力把它用汇编写完,然后一个一个字节一个一个指令的敲到人家的程序中去。
呵呵,今天要教大家的就是如何在补丁程序中实现复杂的功能,具体问题在上面已经描述了,方法就是为程序附带一个动态链接库,把所有复杂的功能都留在DLL里面,让高级语言去作低级语言难于实现的功能,留出接口,供被打补丁的使用即可。
关于什么是DLL,我想不用我再多说了。简单的理解就是一个函数库,别人调用它,它具体完成,就这么简单。大家可以用软件看看我写的那个IPsearcher.dll(用tdump,或者VC中的工具depends.exe),可以看到其中只有一个函数接口,叫“_GetAddress”。给它一个IP地址的作为参数,它就能返回那个IP地址的实际地址。具体它的实现不在我们今天的讨论范围当中。
问题的关键就是如何在要被打补丁的程序中调用到这个动态链接库,让它为我们添加的补丁程序服务。
再往下看,就需要各位同学具备一定的Win32 PE格式的可执行文件的基础了(可以参考我以前写的“关于Win95下的可执行文件的加密研究”和“基础篇”)。在PE文件中,指定了该程序要使用到的所有动态链接库,还有要调用的哪个函数接口。这些信息都写在一个叫Import Table的数据结构当中。它描述了某个动态链接库的某个函数接口的调用地址,这个是为了使系统能够为该程序装入动态链接库并且重定位接口地址信息的。
可能大家有点晕菜了,没关系。让我用一个实例来解释一下Import Table。我们用tdump 打开一个PE格式的可执行文件:
C:\>tdump example.exe |more
Turbo Dump Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation
........
Name RVA Size
------------------ -------- --------
Exports 00000000 00000000
Imports 0019CFA0 0000017C
Resources 00287000 00077F18
........
# Name VirtSize RVA PhysSize Phys off Flags
-- -------- -------- -------- -------- -------- --------
01 .text 00157744 00001000 00158000 00001000 60000020 [CER]
02 .rdata 0004723A 00159000 00048000 00159000 40000040 [IR]
03 .data 000E5C28 001A1000 0002A000 001A1000 C0000040 [IRW]
.......
Section: Import
ImportLookUpTblRVA:0019D938
Time Stamp: 00000000
Forwarder Chain: 00000000 (index of first forwarder reference)
Imports from WINMM.dll
(hint = 0071) mixerOpen
(hint = 0065) mixerClose
(hint = 006B) mixerGetLineControlsA
先说说第一个出现红字的地方,这是这个程序的Import Table的RVA地址(什么是RVA自己查)和长度;第二个红字是1A1000,它大于19CFA0,说明什么呢,Import Table的地址在.rdata段当中(好像知道了也没什么用,呵呵,写都写了,也不管这么多了);第三个红字的地方就是tdump解析出的Import Table具体的内容,比如有个叫WINMM.DLL的动态链接库,需要调用它的序列号是0ach的waveOutClose 函数。诸如此类的后面还有很多很多,通常一个程序需要调用至少是几个的动态链接库(kernel32.dll,user32.dll等等)。
那Import Table在文件中具体的样子是什么呢,同学们可以用UltraEdit打开偏移为19CFA0的地方看看。然后结合我给出的数据结构和tdump解析出的结果,具体分析和理解一下。
以下摘抄于“关于Win95下的可执行文件的加密研究”
.idata块以一个IMAGE_IMPORT_DESCRIPTOR数组开始。每一个被PE文件隐式连结进来的DLL都有一个IMAGE_IMPORT_DESCRIPTOR。在这个数组中,没有字段指出该结构数组的项数,但它的最后一个单元是NULL,可以由此计数算出该数组的项数。IMAGE_IMPORT_DESCRIPTOR的格式如下:
Dword Characteristics
该字段是一个指针数组的RVA偏移。其中每一个指针都指向一个IMAGE_IMPORT_BY_NAME结构
Dword TimeDateStamp
时间及日期标志,可以忽略。
Dword ForwarderChain
正向链结索引。我们的资料中没有函数正向链结的格式,也没有这一样的例子。
Dword Name
以NULL结尾的ASCII字符的RVA地址,该字符串包含输入的DLL名,比如“Kernel32.dll”或“USER32.DLL”。
PIMAGE_THUNK_DATA FirstThunk
该字段是在Image_thunk_data联合结构中的RVA偏移。大多数情况下,Image_thunk_data是指IMAGE_IMPORT_BY_NAME结构的指针。如果不是一个指针的话,那它就是该功能在DLL中的序号。
IMAGE_IMPORT_DESCRIPTOR重要的部分有输入的DLL名字及两个IMAGE_IMPORT_BY_NAME指针数组。在执行文件中,这两个指针数组彼此平行,末尾都是以Null表示数组的结束。下图给出了这种关系的图形描述。
为什么由两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构呢?第一个Characteristics是单独的一项,而且不可改写,它有时被称为提示名表(Hint Name Table)。第二个数组(FirstThunk所指)是由PE装入器重写的。装载程序迭代搜索数组中的每一个指针,找到每一个IMAGE_IMPORT_BY_NAME结构所指的输入函数的地址,然后装载器找到程序的地址改写IMAGE_IMPORT_BY_NAME指针。Jmp dword ptr [xxxxxxxx]中的[xxxxxxxx]是指First Thunk数组中的一个入口。因为它被称为输入地址表(Import Address Table)。
好了,不知道大家看明白了没有,对Import Table不解的地方可以仔细查看我的文章,那里面有很详细的阐述。
我们的目标已经很明确了,把我们自己的DLL描述添加到被打补丁程序的Import table当中,让系统在装入该程序的同时,装入我们自己的DLL,并且做好重定位,为我们的补丁程序调用DLL中的接口做好准备。
我们可以直接修改Import Table,如果空间足够,我们可以这么干。但是Import Table往往是个很小一块数据,可以为一个段,也可以塞在程序的任何地方。所以Import Table的往往前后都是其他数据,我们要加一项都是很困难的事情(其实一项也就20个字节)。那如果前后都是有数据的怎么半,没关系,直接全部挪到其他空闲的地方去(什么地方空闲,请看“基础篇”)。但一定要记得的是,必须修改PE部首的字段(第一个红字所描述的地方),改到你自己的Import Table所在的地方,当然,长度也要修改(通常一个DLL就是一项,一项就是20个字节)。
最重要最关键的就是自己构造一个Import Table的表项了。(喝口水,稍等)
关于表项的具体数据结构和描述已经在上面讲述了。用UltraEdit改二进制文件就像是给病人开刀,要格外小心谨慎。要记住,每个数据的每一位都像是病人身上的肉肉,动刀前先要有信心,要有把握,谨慎,仔细,把稳。。。。
算了,说得我自己都紧张了,还是我一手一手示范讲解吧。(上个WC,不好意思)
回来了,我接着和大家聊。让我们先来看看一个实际PE可之行文件的16进制映像吧。就拿那个程序为例,我们已经知道它的Import Table的起始RVA地址(0019CFA0),用UltraEdit打开该程序,按那个Goto按钮,接着输入地址“0x19CFA0”回车。这样你就会在编辑其中看到该文件Import Table的样子。(如下图)
大家可以看到这个可执行文件的从19cfa0到19d11b共17C个字节的Import Table,其中最后20个字节是一个空表项,它表示这个Import Table的结束。可以看到,在Import Table结束之后仍然有大量的未知数据,这就给我们添加表项带来了困难。该怎么办呢,前面已经提到,挪到其他地方去。怎么挪?
1。先找空地方,我们找到001A0250,它在.rdata和.data中间,大家可以自己用UltraEdit找各个段的接逢处,往往有空余的空间。(怎么找空隙请参阅“基础篇”)
2。把UltraEdit的光标移动到地址19cfa0到19d11f(0x180h个字节),然后按Ctrl-c拷贝。然后移动光标到1A0250,选住空白的0x180h个字节好(如果不选住再粘贴就是插入了),按Ctrl-V。这样我们就把以前的Import Table拷贝到了新的地方(以前Import Table 19cfa0的数据不用删除)。
3。拷贝完了之后还需要把PE文件头Dir Table中的Import Table的首址RVA指向到新的地方。具体做法是,在文件头部查找16进制数A0 CF 19 00(也就是19cfa0), 把它改成50 02 1A 00 (1a0250),然后紧接着后面两个自己是Import Table的长度,以前是17Ch,现在改成17Ch+14h=190h。(14h=20,这是一个表项的长度)
挪完了,让我们用tdump 检查一下文件:
Name RVA Size
------------------ -------- --------
Exports 00000000 00000000
Imports 001A0250 00000190
.......
Section: Import
ImportLookUpTblRVA:0019D938
Time Stamp: 00000000
Forwarder Chain: 00000000 (index of first forwarder reference)
Imports from WINMM.dll
(hint = 0071) mixerOpen
(hint = 0065) mixerClose
(hint = 006B) mixerGetLineControlsA
第一个红字的地方说明我们已经把Import Table的首址改到了1a0250长度是190了。第二个红字的地方说明新的Import Table的表项数据是可靠的。
好了,最关键的最激动人心的时候到了,让我们真正为这个新的Import Table添加表项,添加表项必须是要建立在你已经完全对Import Table结构了解的基础上,输入的过程就是输入一些16进制数和字符串。下面是输入好后的屏幕切图,我将结合该图为大家讲解。
好了,大家已经看到了,这是添加好表项以后的Import Table,其中1a0250到1a03b8和以前的Import Table一模一样。以前的Import Table在1a03b8以后是20个字节的全零结束符。1a03b9到1a03cc是我们添加的表项,后面的20个字节是新的结束符。然后空了16个字节,接着是DLL信息。
让我把新表项的每一个字段都给大家讲解一下。
1a03b9开始,第一个DWORD是1a0410。地址1a0410是指向的1a03f0,1a03f0这是一个DLL接口的描述(具体结构请自行查阅资料)。可以看到01 00(0001)是_GetAddress函数在DLL中的序列号(IPsearcher.dll中只有一个函数接口,序号是1),然后是一个以0结尾的字符串“_GetAddress”这是函数的名称。
表项的第二个第三个DWORD是timestamp和向后的指针链,不管(只有一个函数,所以指针链是0,表示结束)。
第四个DWORD是1a03e0,它指向DLL文件的文件名,图例中是“ipsearcher.dll”,当然也需要以0结尾。
第五个DWORD是1a0418,这个地址是在图例中有数值,其实可以不管。因为这个地址是系统在装入DLL之后填写的该函数接口的具体地址。我们的补丁程序就是调用这个地址中的地址来调用_GetAddress这个函数。具体怎么访问呢?程序装入地址是40000h,所以该地址在装入之后就是40000h+1a0418h=5a0418h。我们的补丁程序使用call dword ptr [5a0418]就可以调用_GetAddress了。
呵呵,修改Import Table的工作已经结束。我使劲在讲,不知道大家明白了没有,可不要说我没照顾大家的感受!
对了,还有一个很重要的事情。由于在系统装入DLL的时候需要在1a0418中填写函数入口地址,所以,找个地址所在的段必须可写。关于怎么修改段表,使某一个段可写,请参阅我以前的文章。
最后当然要用我们的tdump查看一下我们修改PE文件的结果:
Imports from WININET.dll
(hint = 0075) InternetQueryOptionA
(hint = 0052) InternetCanonicalizeUrlA
(hint = 0056) InternetCloseHandle
(hint = 007E) InternetSetFilePointer
(hint = 0077) InternetReadFile
(hint = 0074) InternetQueryDataAvailable
(hint = 0088) InternetWriteFile
(hint = 0069) InternetGetLastResponseInfoA
(hint = 005C) InternetCrackUrlA
(hint = 0071) InternetOpenUrlA
(hint = 006F) InternetOpenA
(hint = 0083) InternetSetStatusCallback
Imports from ipsearcher.dll
(hint = 0001) _GetAddress
看红字,呵呵,我们已经成功了。这样,被打补丁的程序在装入的时候会自动装入ipsearcher.dll,并且把_GetAddress函数的入口地址填写到1a0418处。
以后该怎么办不是我们今天要探讨的内容,总之我们在被打补丁的程序中已经能够调用新的动态链接库中的函数接口了,这无疑为在补丁中添加实现复杂的功能提供了可能。
呵呵,很有用吧。我表达能力不太好,希望大家看懂了。不懂的自己先努力查资料自己专研,实在不懂的再问我吧。
辛苦大家看这么多,下次再见吧!
本篇总结:
1. 本系列文章讨论的主要话题和范围。
2. PE文件是Win32平台的可执行文件。
3. 关于为程序添加动态连接库的问题。
4. 其他一些相关知识的介绍。
5. 需要参考我以前的文章
邹丹 于 2001年8月18日