这两天学习了PE结构后,做了一个简单的内存加载demo。在完成这个demo期间也是遇到了很多问题,大部分的问题最后都得到了解决,但还是有一些问题依然困扰着我,我在最后将会提到。不过总的来说也算是顺利完成了这个demo。对于一个java程序员来说,理解内存和指针确实有些困难 。因此这次用C语言编写demo对我来说是个不小的挑战。不过同样也让我对底层有了更深入的了解。因此在这里做一个工作总结,加深一下印象,同时也希望有朋友能解答我的一些疑惑。
win7内存加载
windows系统在启动一个程序前首先会对exe进行合法性检查等操作,紧接着就是划分4G的内存空间,创建进程和线程并把PE文件映射到2G的用户空间中,接着装载动态库并填写IAT表。然后按照重定位表进行重定位。最后启动线程执行程序入口点。我要做的是直接在内存中加载并运行test22.exe这个程序,因此我的程序只需要为test22.exe完成空间的申请、PE文件的内存映射、装载动态库填写IAT表、改写重定位表以及跳转到入口点就可以正常的运行test22.exe了。至于进程和线程的创建以及堆栈等资源直接由本程序初始化时的资源代替即可,我不需要考虑这些。接下来我将从以上的四个方面叙述我的工作过程。
节的映射
首先PE文件是由文件头部和各个节组成的。头部主要负责记载该PE文件的结构信息,其中对我们最有用的结构信息是选项表和数据目录以及节表。
在对节进行映射前我们需要根据选项表中的SizeOfImage项为程序申请内存空间,空间大小为SizeOfImage,紧接着就是根具节表中记录的节的信息把每一个节按照内存对齐的方式映射到内存中去。大概如下图所示:
节表的结构如下图所示。
内存申请代码:
在这里有个坑,在申请内存空间时一定要记得给执行权限,不然当程序跳转到这个内存空间的代码运行时就会直接报错。
节表加载代码:
修改重定位
节表加载完成后,由于我们申请的内存空间的起始地址NImageBase一般和PE文件的ImageBase是不一样的。而PE文件在编译生成时一些静态全局变量的地址是按照ImageBase+偏移生成的,因此代码中调用这些变量也是直接访问ImageBase+偏移这个地址。这样就会出错。因为ImageBase+偏移对我们来说可能是一个没用的地址。真正的变量是存放在NImageBase+偏移中的。所以我们需要把代码中访问ImageBase+偏移的代码改成访问NImageBase+偏移。
那么如何知道代码中哪些地方需要修改了?很幸运这些需要修改的地方都被记录在了重定位表中,我们只需要在数据目录中找到重定位表的位置信息就可以定位到重定位表,进而找到需要更改的代码。
重定位表按照PE文件大小分为很多项,每一项负责记录内存中长度为0x1000的数据块中需要重定位的地址信息。重定位表中的一项数据分别由前4个字节的重定位地址和四个字节的数据项长度以及后面每两个字节的地址信息组成。
我在读取到两个字节的地址信息后需要处理下(去掉最高位然后加上前4个字节的重定位地址)就可以得到需要更改的代码的内存地址。然后把ImageBase+偏移的代码改成NImageBase+偏移就可以了。
重定位表其中一项的图片如下:
RVA是需要修改的代码的内存地址。
重定位代码如下:
值得注意的是我高亮了的代码。这也是一个坑,exe和dll的重定位表有一个细微的差距,就是exe的重定位表会比dll多一个0项,如果不处理,那么就会改变不应该改变的代码,从而导致在运行过程中出现崩溃。
装载动态库
在处理完重定位后,还面临最后一个问题,加载动态库。每个程序都会或多或少引用一些动态库如user32.dll和kernel.dll等,PE文件中的IAT表负责存放动态库中被引用到的函数的地址,提供给程序访问调用。因此我们也需要填写IAT表,否则程序就无法正常运行。
要填写IAT表我们首先需要从数据目录中读取导入表的地址,然后根据导入表获得程序需要加载的动态库的名称以及调用的函数名称和对应的IAT表的位置。
导入表的每一项的长度为20字节,第1个4字节记录着函数名称地址数组的地址,倒数第1个4字节记录该库的IAT表地址,倒数第二个四字节则记录着动态库名称的地址。
导入表如下图:
函数名称地址数组:
值得注意的是地址数组每一项长度为4字节代表函数名称的地址,最后一个值为0的四字节表示数组结尾。同时在读取函数名称时需要跳过前2个字节
修复导入表代码:
值得注意的是在填写IAT表时,如果遇到这一项的IAT=0那么就可以不填写。同时填写IAT表的顺序和地址表是一致的
修改入口点并跳转
在完成上诉工作后,最后我们只需要修改选项头中的程序入口点(EntryPoint)为当前内存中的程序真实入口点的值,在写入头部到内存中就可以跳转到test22.exe程序执行了。
代码如下:
就成功啦
虽然最后做出来了,但还是有些问题不能理解,我看网上有些人说exe不能直接这样启动,而是需要启动一个进程然后把程序写入进程空间中在更改PEB中EAX为程序入口 地址才能执行。虽然这种方法确实可以,但我不太明白为什么我这样也成功了,是因为系统版本问题吗。