接问题2.20后的第21个问题
问:3.21
上回说到loader 为了使程序正常运行,偷偷的把.rdata 节中的某些数据给改了,能再讲清楚一些吗?
答:3.21
加载器未改之前,我们用hiew 看文件有如下数据
.00402000: 76 20 00 00-00 00 00 00-5C 20 00 00-00 00 00 00
加载器改动之后,我们可以用ollydbg 看一下,这是加载器完成修改后的结果
具体操作是用ollydbg 加载hello.exe,点击数据窗口,按ctrl-G,输入地址402000
然后看到如下数据
00402000 >DA CD 81 7C 00 00 00 00 8A 05 D5 77 00 00 00 00 谕亅....?誻....
这就说明,加载器把2076 改成了7c81cdda, 把205c 改成了 77d5d58a
问:3.22
呦,还藏着这等玄机呢! 不过,加载器也是一段程序,它总不能乱改吧?咱就以后面
的205c 为例,它凭什么要把205c 改成77D5058A呢?这个可是我们要call 的MessageBox的地址呢。
答:3.22
连接器知道你要CALL MessageBox, 但MessageBox 是USer32.dll 的函数,连接器不知道
MessageBox 在哪里,只有操作系统才知道USer32.dll 在哪里。loader 也是通过调用系统
函数才知道的。由于link 的时候文件还没有运行,所以它不知道MessagBox 的具体地址。
好,这个问题讲清了,连接器不知道DLL及其函数的具体地址。
但连接器也会尽其所能,告诉加载器一些信息,他对加载器说,运行前你要帮我把USer32.dll
的MessageBox 地址给填好!拜托了!
用计算机来描述是这样的。
他往402008处填了一个205c, 这个205c 是什么,是一个RVA, 它指向一个数据结构,该结构
实现linker 向 loader 的信息传递, loader 来完成linker 未完成的使命。
如果你要想看看linker 向 loader 说了什么,咱还得先看看这个数据结构的地址。
问:3.23
就已hello.exe 为例吧,看看205c 怎么找到那个数据结构地址的。
答:3.23
loader 拿到了数据205c, 知道这是一个RVA, 加上影像基地址0x400000,或者说叫module 地址吧。
得到了一个虚地址0x40205c,你从ollydbg 的数据窗口中看0x40205c 地址。是如下内容:
0040205C 9D 01 4D 65 73 73 61 67 65 42 6F 78 41 00 75 73 ?MessageBoxA.us
0040206C 65 72 33 32 2E 64 6C 6C 00 00 80 00 45 78 69 74 er32.dll..€.Exit
0040207C 50 72 6F 63 65 73 73 00 6B 65 72 6E 65 6C 33 32 Process.kernel32
0040208C 2E 64 6C 6C 00 00 00 00 00 00 00 00 00 00 00 00 .dll............
它指向一个WORD 数据019D, 后跟MessageBoxA 0字终结字符串,其后再跟USER32.dll。
这个结构有一个学名,叫 IMPORT_BY_NAME,如下定义:
//
// Import Format
//
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint, 就是那个019d 了,BYTE Name[1], 这个变量定义看起来很奇怪,是吗?
代学生: 是的,我很少见到这种定义。
通常我们会定义 char buffer[256], BYTE data[8]; 等类型。
BYTE Name[1], 只包含一个元素的数组,它也装不下后面的"MessageBoxA"字符串啊。
代老师:这种定义是一种指针的变通用法。
如果你真要定义成数组来包含后面的字符串,你定义成多大呢?定义成100,短字符串浪费,
长字符串可能就真能碰到一个101个字符的名字,你定义的还是占不下。
所以说,这个Name[1], 不是要你往里面装东西的,C 语言里,你可以借助这个Name 变量访问到它对应的地址。
这种用法通常是很少用的。因为它毛病很多,例如结构后面不能再定义其它变量了,必须是最后一个,定义了
数组又不用它装东西,也不符合数组的初衷. 所以你只有明白这个道理就可以了。
代学生:既然它那么不好用,为什么还那样定义呢。
代老师:还是那句话,是变通。
你看,它简洁,它完成了使命。否则你就要把结构变一变,例如按常规估计应该是这样子。
WORD Hint; BYTE *pName; 然后你要求微软说,Hint 后面不要跟字符串,要跟一个地址。这样C语言好写。
好比说大部分人沿着盘山路往山上走,也有人愿意盘着荆棘往山上爬,后者绕了近路,但风险也大。
代学生:讲了这么多,其实我看一个word 后面跟着一个0字终结符字符串,还是很好理解的吗。
代老师:C 语言以其简洁,高效,使我们受益良多。但在某些特殊的情况下,它也会力不从心。有时刻当你看着一堆堆
结构套结构,一堆堆宏套宏令你头晕时,而看看它最终的list 表或二进制输出反而能令你豁然开朗。
哦,有点扯远了。 我还是最喜欢C的。
问: 3.24
找到了这个 IMPORT BY Name 结构, loader 是怎样根据这些信息修改地址的呢?
答:3.24
loader 在加载时,是要先收集信息的,不过这部分还没有讲,收集好后,以MessageBoxA为例
loader询问系统,USER32.dll 加载了吗?没有我要先加载它啦。哦,加载了,告诉我MessageBoxA
函数地址是多少?系统返回一个地址 77d5d58a, loader 就把这个地址覆盖了原来存储的 0x205c 。
这样就把MessageBoxA 的内存地址给拧上了。
注意了,这个 77d5d58a 是我机器上的MessageBoxA 内存地址, 到了你的机器上,它就变成别的啦。
这正是DLL 存在的妙处。
意思表达很明确,不是吗?
问:3.25
对,它肯定能表达明确。不过目前我还有很多问题要问,别嫌麻烦呦。我问得可是很细致的。
答:3.25
难得你精神可嘉,咱们也是互相促进的。能走到这里,也可以说是渐入佳境了。
问:3.26
弱弱得问一下,3.23 提到的那个module 地址0x400000, 是固定死的吗?
答:3.26
module 加载地址,是loader 将应用程序加载到内存时的起始地址,loader 是从Option header
结构中的Image Base 项得到这个地址的,由于exe 文件不存在地址冲突问题,所以loader 总能
把程序加载到Option header 中Image Base 指定的位置,这个位置通常都是0x400000.
问:3.27
刚才没顾上问,MessageBoxA 前面那个WORD 019d, 就是hint, 是干什么的。
答:3.27
那个东东是loader 向系统询问函数地址的另外一种方式,它可以向系统询问user32.dll 第019d 个
导出函数是多少 ? 系统回答 77d5d58a 。 这种导入方式叫ordinal. DLL 中有名称的导出函数都可以
用名称访问,也可以用ordinal 访问,而有的导出函数没有名,只能用ordinal方式导入。
问:3.28
刚才你是用ollydbg 来讲解的,我们不是一直用ultraedit 打开这个文件,直接分析它的二进制
结构的吗。能用ultraedit 再讲一讲linker 向 loader 说了什么吗?
答:3.28
这主要是因为动态链接的数据是一个RVA, 所以用ollydbg 讲的方便。同时由于在ollydbg中已经完
成了动态连接,跟utraedit 静态分析正好有个对照。现在我们再来看从ultraedit 中怎样找到 ???结构
loader 拿到了数据205c, 哦, 不对,是我们拿到了205A,知道这是一个RVA.需要把它转成文件偏移
好看看它到底对我们说了什么。
从RVA 到 OFFSET, 我们好像还没有讲呢,就在这里补上吧。
从RVA 到 OFFSET 没有一个简单的公式,唯一的办法就是查表。
查什么表,查section header 表。
已hello.exe 为例,我们查表,看到205c 落入.rdata 节,该节的虚拟地址(就是内存地址)0x2000
对应文件偏移0600,那么205c, 则对应文件偏移的065c. 用ultraedit 观看,如下图示。跟ollydbg看到的内容一样。
0000650: 0000 0000 5c20 0000 0000 0000 9d01 4d65 ....\ ........Me
0000660: 7373 6167 6542 6f78 4100 7573 6572 3332 ssageBoxA.user32
0000670: 2e64 6c6c 0000 8000 4578 6974 5072 6f63 .dll....ExitProc
0000680: 6573 7300 6b65 726e 656c 3332 2e64 6c6c ess.kernel32.dll
代学生:顺便问一下,那些查虚拟地址到文件偏移转换的工具是这样查的吗?
代老师:是的。
问3.29
.rdata 节中大部分数据都明白了,但从610-65d 那一段数据是干什么的?
答3.29
这段数据,当然也是动态加载用的。
前面讲的link 与 loader 对话,确实如上所说,但那只是问题的一半,还有一半就是,当loader 把程序
加载到内存,它要修改数据,它怎样找到修改数据的地址,也就是说,那个存储着RVA 205c 的地址
00402008 loader 是怎样得到的?还有,它怎么知道MessageBoxA在user32.dll 里面?
问3.30
平时都是我问,现在忽然被反问。翻翻前面讲的 。。。
啊! 是2.18 时提出来的,程序要call 41c, 41c 要向402008 地址所装的内容处跳。
哦,这是我们的读法。loader 是不会这么读的,loader 是死的,loader 是一段程序,它只会干机械的事。
它怎么找到402008的,它怎么知道MessageBoxA在user32.dll 里面? 还是听你讲吧。
答3.30
讲清这个问题,我们还要再看option header. 坚持住,动态加载导入部分也就差这一点点了。
在1.2中提到option header 的数据结构,在他的底部有一个成员。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
看着眼晕,还不如写简单点,它的数组就是16个元素
IMAGE_DATA_DIRECTORY DataDirectory[16];
前面那个结构叫数据目录,如下定义
//
// Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD RelativeVirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
挺简单的,其实就是8个字节。 16个目录吗,就是16*8 = 108 字节。占8整行
别担心,大部分没有用,微软在这里搞捉迷藏。我们只关心几个就行了。
这里首先介绍的一个叫导入表(import table)。
0000130: 0000 0000 0000 0000 ................
0000140: 1020 0000 3c00 0000 0040 0000 a003 0000 . ..<....@......
0000150: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000160: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000170: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000180: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000190: 0000 0000 0000 0000 0020 0000 1000 0000 ......... ......
00001a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001b0: 0000 0000 0000 0000 2e74 6578 7400 0000 .........text...
为什么还留下那个.text, 一看就知道,.text 代表的是section header table 的开始地址
其上的8个整行就是16 个目录了。真要让你像计算机一样从上到下数偏移,我们还真不行,
找ascii 字,我们还在行。
第一个目录叫导出表,这里为空
第二个目录叫导入表,这里虚拟地址是0x2010, 大小是3c.
2010 RVA 对应的文件偏移是610 (算法我想你已经掌握了,看3.28)
第三个目录叫资源表。RVA=0x4000, size=0x3a0 (暂时先不讨论)
第十三个目录叫IAT 表,英文全称为:Import Address Table. RVA=0x2000,size=0x10
其它的目录都是空的,没用,不理它们。
代老师:哦,是不是讲的有点多了?
代学生: 还行,反正讲得挺清楚的。想不到这个小小的hello.exe 还有这么多事情。
不过再看看hello.exe文件,大部分内容都讲了,我想也不会有太多东西了。
代老师:正是,坚持一下就是胜利。