由于软件安全课程要考PE文件,只能强制记忆PE结构体和字段偏移量。虽然实际中不会真的这么做,但是强制记忆还是有点好处,以合理的方式记忆之后就很难忘记。(以下仅代表个人的看法和认知)
(1) 记忆的一些原则
可以参照申一帆的记忆法,结合自己的一些假设和想象即可。从某种层面来讲,一般的问题不涉及太高深的数学(除非自己有进行深入的思考),大多数的人是可以比较轻松的解决的,即使有点困难也可以想办法来解决的。
在DLL注入_修改导入表(1)中,主要是为了熟悉PE结构
而逐个结构体来分析,有一连串的过程,显然是很难记住的。过程分析就像是编写程序的思想,要把所有的关系找出来。这个过程可以看成是:假设我要和远方进行通信而使用完整的一根线。实际上,远距离通信,中间要经过很多节点,记忆也应该是这样的。
其实可以把这样的记忆过程看成一个数学问题,即带方向的图形一笔画问题,但是有点要求:点与点之间的顺序是固定的(偏序关系)。举个例子:(a1 a2 a3)
代表某种开展顺序,我们可以将(a1 a2 a3)
拆分成 (a1 a3)
(a2 a3)
(a1 a2)
,这样就可以将一连串的过程拆分成较小的片段而最终能恢复序列,从而使记忆更轻松。较小的片段可以看成是离散的点集,通过曲线连接这些点而使事物形象化,片段与片段之间则是事物之间的关系(如果没有的话,自己假设)。这个就像是天上的星星,通过画星宿图把不相干的事物联系在一起了。
记忆难点一:抽象的概念
对于抽象的概念多读熟一点、画一画图、记(构造)一些特殊实例就可以较好的理解概念,如离散数学中的树和图的部分。
记住核心的语句。如函数的栈帧 push ebp;mov ebp,esp;
可以看成esp指向下一个函数的ebp。两个函数可以看成两个队伍,大端或小段标记法看成是两个队伍追逐的方向,最终的目的是使一个队伍永远追逐不上另一个队伍,这就是bp栈帧的作用。
适当的想象。如carry out
和perform
都有执行的意思。执行由人的手去执行,手拿东西呈现出一个c
,out
有远离的意思,可以想象每次过安检的时候,把行李箱放在传送带上而离我们远去即carry out
。perform
的对象是人person
。执行的英语首字母是c
或p
,就看你平时更经常使用哪一个(权重的高低)。
适当的假设。由于我们所接触的是前人创造出来的东西,因此只要按照正常的思维,可以找到事物一些合理的解释。如我们的汉字与国外的英文,都有象形的印记,也有抽象的地方。如下面一句话:
This manual uses specific notation for data-structure formats, for symbolic representation of instructions, and for hexadecimal and binary numbers. This notation is described below.
也许使用英文的人认为for data-structure formats
这个修饰比较次要放在了后面,这样就能更好的接受新的概念。
又如汇编指令mov eax, dword ptr [ebp-10]
,也许当初设计汇编的人认为mov
操作指令最重要,放在第一个位置(实际上是地址编码的问题),而目标寄存器相对重要,放在第二个位置,这样就不会觉得很奇怪了。
记忆难点二:过于离散或连续的过程
过于离散转化成连续,过于连续化成离散。
(2) 具体过程
如果不太了解PE结构,可以阅读加密与解密第四版
的第11章,相对讲的通俗易懂(https://pan.baidu[.]com/s/1ZOlGamc-v2V_0ClT_GEVPQ 提取码:y17d),加载到内存中的过程可以阅读逆向工程核心原理
的第13章p108和p114(https://github.com/sv4us/ebook/tree/master/%E9%80%86%E5%90%91%E5%B7%A5%E7%A8%8B%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86)
记住IMAGE_DATA_DIRECTORY
这个关键的中间节点,然后向前和向后回忆相关的结构体、字段和偏移量。(不要用写程序的思维来记忆,因为写程序是从头开始一个个分析)
向前
IMAGE_DATA_DIRECTORY
是IMAGE_OPTIONAL_HEADER
的最后一个字段,距IMAGE_NT_HEADER
(即PE头) +78H
处。而PE头是PE00
,为IMAGE_NT_HEADER
的第一个字段Signature
(类型是DWORD双字),而PE头
是由DOS头(MZ 文件的起始)
的e_lfanew
字段指出 距DOS头
+3CH
处(类型是WORD)。(DOS的结构不重要,直接忽略,只需记住文件头偏移量+3CH)
向后
IMAGE_DATA_DIRECTORY
是一个结构数组(共16项)
第一项是IMAGE_DIRECTORY_EXPORT
导出表 距PE头+78H
处,
第二项是IMAGE_DIRECTORY_IMPORT
导入表 距PE头+80H
处。(也许为别的程序服务比较重要,因此把导出表放在第一项)
IMAGE_DATA_DIRECTORY
结构的第一个字段VirtualAddress
指出了数据起始地址RVA
,第二个字段size
指出了数据块长度
,类型都是DWORD双字
。(储存数据起始地址RVA和数据大小来确定分配的空间)
由IMAGE_DATA_DIRECTORY
的第二项导入表 PE头+80H
处的VirtualAddress
得到了IMAGE_IMPORT_DESCRIPTOR
(IID)的结构数组。每个IID
结构长度5个双字20字节,最后一个单元是NULL,可以算出数组项数。
IID
可以看成是操作导入表
的核心,不管是导入表还是导出表,都是要将名称表写入到地址表中,记住这个原则(显然对于我们而言,看一个名称比看一串数字地址轻松,而机器认识的是地址)
IMAGE_IMPORT_DESCRIPTOR
共5个字段(双字)
第1个字段是OriginalFirstThunk
,指向INT(输入名称表 IMPORT_NAME_TABLE
)
第4个字段是Name
,指向DLL名称,RVA
第5个字段是FirstThunk
,指向IAT(输入地址表 IMPORT_ADDRESS_TABLE
)
可执行文件从user32.dll输入API
;
PE装载器把导入函数
输入到IAT
:
通过第2张图片的执行过程与第一张图对应。整理一下:
由IID的第4个字段Name
得到库函数名称,由IID的第1个字段OriginalFirstThunk
得到INT结构数组的首地址,由IID的第5个字段FirstThunk
得到IAT输入地址表地址
。中间通过2个api调用(LoadLibrary GetProcAdderss
)。调用IID
字段的顺序为(+4 +1 +5),可以看成垂直排列的3个点,可以用逆时针的螺旋线将点连接起来。
OriginalFirstThun
k和FirstThunk
都指向IMAGE_THUNK_DATA
结构。
IMAGE_THUNK_DATA
最高位为1
时,函数以序号
导出,Oridinal低31位为输出函数在其DLL的导出序号,最高位为0
时,函数以字符串
导出,AddressOfData
指向用来导入函数名称的IMAGE_IMPORT_BY_NAME
的数据结构RVA
typedef struct IMAGE_IMPORT_BY_NAME {
HINT WORD ; //函数序号,非必须
NAME BYTE; //导入函数的名称,大小可变,以0结尾的ascii字符串
}
IMAGE_DATA_DIRECTORY
第一项是IMAGE_DIRECTORY_EXPORT
导出表 距PE头+78H
,由VirtualAddress
得到了IMAGE_EXPORT_DIRECTORY
(IED)的结构数组,结构如下。
典型的输出表
加载