📄 lion-petut-c07.htm
字号:
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=gb_2312-80">
<meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<title>Iczelion的PE教程7: Export Table(引出表)</title>
</head>
<body bgcolor="#000066" text="#FFFFFF" link="#FFFFCC"
vlink="#FFCCCC" alink="#CCFFCC">
<h1 align="center"><font color="#FFFFCC">PE教程7: Export Table(引出表)</font></h1>
<p><font size="2" face="MS Sans Serif">上一课我们已经学习了动态联接中关于引入表那部分知识,现在继续另外一部分,那就是引出表。</font></p>
<p><font size="2">下载 </font><a href="files/pe-tut07.zip"
style="text-decoration:none"><font size="2"><b>范例</b></font></a><font
size="2">。</font></p>
<h3>理论<font face="Arial, Helvetica, sans-serif">:</font></h3>
<p><font size="2" face="MS Sans Serif">当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的引出函数。</font></p>
<p><font size="2" face="MS Sans Serif">DLL/EXE要引出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名引出或者仅仅通过序数引出。比如某个DLL要引出名为"GetSysConfig"的函数,如果它以函数名引出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是GetSysConfig。另外一个办法就是通过序数引出。什么是序数呢?
序数是唯一指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数引出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。</font></p>
<p><font size="2" face="MS Sans Serif">我们不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级/修改,程序员无法改变函数的序数,否则调用该DLL的其他程序都将无法工作。</font></p>
<p><font size="2" face="MS Sans Serif">现在我们开始学习引出结构。象引出表一样,可以通过数据目录找到引出表的位置。这儿,引出表是数据目录的第一个成员,又可称为IMAGE_EXPORT_DIRECTORY。该结构中共有11
个成员,常用的列于下表。</font></p>
<table border="1" cellpadding="2">
<tr>
<th bgcolor="#006666"><font size="2" face="MS Sans Serif"><b>Field
Name</b></font></th>
<th bgcolor="#006666"><font size="2" face="MS Sans Serif">Meaning</font></th>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>nName</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>nBase</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">基数,加上序数就是函数地址数组的索引值了。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>NumberOfFunctions</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">模块引出的函数/符号总数。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>NumberOfNames</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">通过名字引出的函数/符号数目。该值</font><font
color="#CC9900" size="2" face="MS Sans Serif"><b>不是</b></font><font size="2"
face="MS Sans Serif">模块引出的函数/符号总数</font>,这是由上面的<font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>NumberOfFunctions</b></font><font
size="2" face="MS Sans Serif">给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中引出表的RVA为0。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>AddressOfFunctions</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>AddressOfNames</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。</font></td>
</tr>
<tr>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font></td>
<td align="center" bgcolor="#006666"><font size="2"
face="MS Sans Serif">RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。</font></td>
</tr>
</table>
<p><font size="2" face="MS Sans Serif">上面也许无法让您完全理解引出表,下面的简述将助您一臂之力。</font></p>
<p><font size="2" face="MS Sans Serif">引出表的设计是为了方便PE装载器工作。首先,模块必须保存所有引出函数的地址以供PE装载器查询。模块将这些信息保存在</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>AddressOfFunctions</b></font><font
size="2" face="MS Sans Serif">域指向的数组中,而数组元素数目存放在</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>NumberOfFunctions</b></font><font
size="2" face="MS Sans Serif">域中。 因此,如果模块引出40个函数,则</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfFunctions</b></font><font size="2" face="MS Sans Serif">指向的数组必定有40个元素,而</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>NumberOfFunctions</b></font><font
size="2" face="MS Sans Serif">值为40。现在如果有一些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些 名字的RVAs存放在一数组中以供PE装载器查询。该数组由</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNames</b></font><font
size="2" face="MS Sans Serif">指向,</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>NumberOfNames</b></font><font
size="2" face="MS Sans Serif">包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个模块:
名字数组和地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取了</font><font color="#999900" size="2"
face="MS Sans Serif"><b> 指向地址表中对应元素的索引</b></font><font
size="2" face="MS Sans Serif">。 而这些索引保存在由</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font><font
size="2" face="MS Sans Serif">域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定</font><font
size="2" face="MS Sans Serif">: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,以此类推。
</font></p>
<table border="0" cellpadding="2">
<tr>
<th bgcolor="#006666"><font size="2" face="MS Sans Serif">AddressOfNames</font></th>
<th> </th>
<th bgcolor="#006666"><font size="2" face="MS Sans Serif">AddressOfNameOrdinals</font></th>
</tr>
<tr>
<td align="center"><font size="2" face="MS Sans Serif">|</font>
</td>
<td align="center"> </td>
<td align="center"><font size="2" face="MS Sans Serif">|</font>
</td>
</tr>
<tr>
<td align="center"><table border="1" cellpadding="2">
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">RVA of Name 1</font></td>
</tr>
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">RVA of Name 2</font></td>
</tr>
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">RVA of Name 3</font></td>
</tr>
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">RVA of Name 4</font></td>
</tr>
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">...</font> </td>
</tr>
<tr>
<td align="center" bgcolor="#660066"><font
size="2" face="MS Sans Serif">RVA of Name N</font></td>
</tr>
</table>
</td>
<td align="center"><table border="0" cellpadding="2">
<tr>
<td align="center"><font size="2"
face="MS Sans Serif"><--></font></td>
</tr>
<tr>
<td align="center"><font size="2"
face="MS Sans Serif"><--></font></td>
</tr>
<tr>
<td align="center"><font size="2"
face="MS Sans Serif"><--></font></td>
</tr>
<tr>
<td align="center"><font size="2"
face="MS Sans Serif"><--></font></td>
</tr>
<tr>
<td align="center"><font size="2"
face="MS Sans Serif">...</font> </td>
</tr>
<tr>
<td align="center"><font size="2"
face="MS Sans Serif"><--></font></td>
</tr>
</table>
</td>
<td align="center"><table border="1" cellpadding="2">
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">Index of Name 1</font></td>
</tr>
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">Index of Name 2</font></td>
</tr>
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">Index of Name 3</font></td>
</tr>
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">Index of Name 4</font></td>
</tr>
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">...</font> </td>
</tr>
<tr>
<td align="center" bgcolor="#003300"><font
size="2" face="MS Sans Serif">Index of Name N</font></td>
</tr>
</table>
</td>
</tr>
</table>
<p><font size="2" face="MS Sans Serif">下面举一两个例子说明问题。如果我们有了引出函数名并想以此获取地址,可以这么做:</font></p>
<ol>
<li><font size="2" face="MS Sans Serif">定位到PE header。</font></li>
<li><font size="2" face="MS Sans Serif">从数据目录读取引出表的虚拟地址。</font></li>
<li><font size="2" face="MS Sans Serif">定位引出表获取名字数目(</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>NumberOfNames</b></font><font
size="2" face="MS Sans Serif">)。</font></li>
<li><font size="2" face="MS Sans Serif">并行遍历</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>AddressOfNames</b></font><font
size="2" face="MS Sans Serif">和</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font><font size="2" face="MS Sans Serif">指向的数组匹配名字。如果在</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNames</b></font><font
size="2" face="MS Sans Serif"> 指向的数组中找到匹配名字,从</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font><font
size="2" face="MS Sans Serif"> 指向的数组中提取索引值。例如,若发现匹配名字的RVA存放在</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNames</b></font><font
size="2" face="MS Sans Serif"> 数组的第77个元素,那就提取</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font><font
size="2" face="MS Sans Serif">数组的第77个元素</font><font
size="2" face="MS Sans Serif">作为索引值。如果遍历完</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>NumberOfNames</b></font><font
size="2" face="MS Sans Serif"> 个元素,说明当前模块没有所要的名字。</font></li>
<li><font size="2" face="MS Sans Serif">从</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>AddressOfNameOrdinals</b></font><font
size="2" face="MS Sans Serif"> 数组提取的数值作为</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>AddressOfFunctions</b></font><font
size="2" face="MS Sans Serif"> 数组的索引。也就是说,如果值是5,就必须读取</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>AddressOfFunctions</b></font><font
size="2" face="MS Sans Serif"> 数组的第5个元素,此值就是所要函数的RVA。</font></li>
</ol>
<p><font size="2" face="MS Sans Serif">现在我们在把注意力转向</font><font color="#FFFFCC"
size="2" face="MS Sans Serif"><b>IMAGE_EXPORT_DIRECTORY</b></font><font
size="2" face="MS Sans Serif"> 结构的</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>nBase</b>成员。您已经知道</font><font color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfFunctions</b></font><font
size="2" face="MS Sans Serif"> 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时,让我们设想这样一种情况,如果程序员在.def文件中设定起始序数号为200,这意味着</font><font
color="#FFFFCC" size="2" face="MS Sans Serif"><b>AddressOfFunctions</b></font><font
size="2" face="MS Sans Serif"> 数组至少有200个元素,甚至这前面200个元素并没使用,但它们必须存在,因为PE装载器这样才能索引到正确的地址。这种方法很不好,所以又设计了</font><font color="#FFFFCC" size="2"
face="MS Sans Serif"><b>nBase</b></font><font size="2"
face="MS Sans Serif"> 域解决这个问题。如果程序员指定起始序数号为200,</font><font
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -