ARM为例,重定位表简介(上)

从小老师就教育我们,从c到exe经历了编译、连接,试卷上可能会出现;这么多年过去了,我也只知道这句话,最近有幸研究了一下 obj 的文件格式,对重定位有了一定的了解,写篇文章造福一下后人。

一、目标文件简介和查看方法

本文使用的是ndk-r19,该版本开始,官方不推荐使用 standard-toolchain, 推荐使用预置好的脚本,例如这些文件。

这里有个非常方便的东西,可以直接编译想要的东西出来,这里随便挑一个吧,反正编出来的也差不多,测试的样例代码是最简单的样例,只是为了教学,将来会有复杂的样例代码来介绍特殊情况。

使用该命令编译目标文件

同样,使用对应的prebuilt工具,可以对其进行解析

ARM 架构是冯诺依曼模型,数据段和代码段是混在一起的,例如上面的例子,在 0x24 的位置放的就是数据,理应当存放指向 HelloWorld 的字符串,但却放了一个奇怪的数字。

重定位表有3项,这里只关注 printf  和 .L.str ,另外一个不用管,因为我看不懂。

二、重定位表简介

通过 readelf -r  可以简单的知道, .rel.text 里有两项我们关注的,表示的是 .text 节的重定位;类似 .rel.ARM.exidx 表示的是 .ARM.exidx 的重定位。猜测 .rel  开头的应该是对应节的重定位表,将来会出现 .rel.data

里面每一项都是一种结构,我使用的是 pyelftools  这个库,抄一下 pwntools 的包装代码,编写了下面的代码,用于理解重定位表。

输出是

表示这两个位置需要修复,前者翻译一下是 BL 0x00 ,也就是原地死循环,后者放着常数。

这里理解起来,建议使用 ida,ida在打开.o文件的时候,会自动帮你把重定位都修好,其实有很多节都是不存在的、由 ida 替你修复的,方便观看而已,例如这个。

对printf原本是

现在是

翻译一下,应该是跳转到 0x14 + 0x8*4 + 8 = 0x3c 的位置,而这个位置对应的是extern 节。

需要注意的是,extern 其实是不存在的、ida 帮你虚拟出来的一个东西,它在偏移0x3c处开始,这个过程就是重定位的过程。

对 HelloWorld原本是

现在是

.L.str 是在 0x30处

因为 ARM 是通过 PC+offset  来拿数据的,将来运行到这里时, [0x24] 存放的是PC到HelloWorld的距离,从而成功获取到 HelloWorld 的绝对地址, PC=base+0x18 , offset=0x18 ,最终寻找到 base+0x30 。

这只是两种情况,将来会遇到更多在情况,下一篇 blog 会讲一下我遇到的所有的情况。

三、从 .o 文件到 .so 文件

接下来看一下ld 帮我们修复的符号表是什么样的

稍微计算一下,实际跳转的地址是 0x3e8 + (0xffffe5*4) + 8 = 0x384

所以 so 里面是让它跳到 plt 节,而 plt  是一段可执行代码,稍微处理一下从 got 节读一个地址,跳过去; got  本身没有内容,是在加载的时候被 dl 填充,这个过程不在本文讨论范围内。

而 HelloWorld 的字符串落在 0x3e8+0x18=0x404 ,没毛病,也是同样的重定位方式。

四、HelloWorld 的修复

上面讲的是编译器、连接器、加载器帮我们干的事情,这里讲一下如何手动去修复 HelloWorld的重定向。

我们的目标是:自己写一段代码,去加载一个 .o 文件,运行它,让寄存器指向这个字符串。

这个 0000000c 其实代表的是:当前地址(base+0x24) 减去 获取地址时的PC(base+0x10+8)得到的差,即

将来它要被写为HelloWorld的地址,也是一个相对偏移。

这里用python简单地算一下字符串在文件中的位置:

算出来hello_world_addr是文件开头起、距离 0x64 的位置。

再算一下取地址时,PC 在文件中的位置:

算出来 .text[0x24] 计算时,PC 实际指向文件开头起、距离为0x4c。

减一下,可以得到在这种设定下的重定位时(注意,这个定位不是标准的so文件的重定位,而是临时的、在.o文件里、测着玩的重定位),是 0x64-0x4c=0x18 。

主要加载思路:

1、将.a文件放到 /data/local/tmp 下, rwx ;

2、 mmap ,创建一块 rwx 的内存(注意,这里会被 SELinux 给挡了);

3、将 .text[0x24] 的地方修改掉,改成 0x18 ;

4、跳过去,看看寄存器是否按照预期被设置

先写一段简单的代码吧!需要关闭SELinux,否则RWX一段内存会报错。

因为我没有修复 BL printf ,所以会有一个一直跳自己的死循环,会白屏,所以下断点看看。

这里确实进入了say里面。

往下走几步,停留到读取到 HelloWorld 地址的地方

ok,这个地方被我们修好了!

五、printf的作用和修复

这里讲一下如何手动去修复 printf的重定向。我们的目标是:自己写一段代码,去加载一个 .o 文件并且调用其中的函数。(这里有个注意的地方,如果使用 BL 的话,printf的绝对地址与内存中的.o文件绝对地址不能差太多,因为 BL 的范围是24bit 的有符号数,再算上2bit 的偏移,也不能覆盖全部内存空间;如果使用 BLX,就没这个限制)。所以需要额外创建一些东西,即plt的内容和got的内容需要我们手写。

思路:

1、因为 mmap  是页对齐的,所以后面有点空间,可以在里面随便写点东西上去

2、文件大小是1164,直接在后面跟一点汇编代码,假装自己是 plt节就行了;

3、 plt里的内容是, B 到真正的 printf 上,但 B  的距离不够远,需要使用 BX

4、这里为了简便, got紧贴着 plt

开搞!贴一下 plt 的汇编内容:

算一下 BL printf  应该跳多远

ok,修改汇编为 0xeb000000 | (0x43c>>2) = 0xeb00010f

来!梭哈一把!

 

没毛病,成功调用!

再打包一个可执行文件出来,不要用 JNI 了。

没毛病,一遍过!

六、整体的理解

这里只修了2种,根据我的是,其实一共需要修复5种不同的情况,本文讲的是两个最简单的情况,其他的会在下一篇文章里讲。

从整体设计上来看,.o文件是可以被重新排序的,里面的每个节的顺序都可以被打乱,因为相互之间没有太大的联系,都是靠 symtab、每个节的位置顺序来联系的,将来多个.o进行合并时,可能进行常量的合并、共同引用外界函数的合并,以及各种各样的优化,设计的还是非常优雅的。

ld 的作用就是把多个.o连接为一个.so或者一个.exe,功能非常强大,可以创建新的节、改变节的顺序等,最后生成常见的elf格式的文件。

这里特别感谢 小花椒、Himyth 的帮助,给我科普了很多elf 格式的基础知识,不然至今都挺迷的,也感谢 Lan 对于plt/got 和 ida 优化的,加深了印象。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code