ARM连接时重定位简介(上)

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

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

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

1
2
3
4
5
6
7
8
9
10
11
➜  ndk-bundle lsa toolchains/llvm/prebuilt/darwin-x86_64/bin  grep armv7a  head
-rwxr-xr-x 1 leadroyal staff 190B 1 17 11:01 armv7a-linux-androideabi16-clang
-rwxr-xr-x 1 leadroyal staff 209B 1 17 11:01 armv7a-linux-androideabi16-clang++
-rwxr-xr-x 1 leadroyal staff 190B 1 17 11:01 armv7a-linux-androideabi17-clang
-rwxr-xr-x 1 leadroyal staff 209B 1 17 11:01 armv7a-linux-androideabi17-clang++
-rwxr-xr-x 1 leadroyal staff 190B 1 17 11:01 armv7a-linux-androideabi18-clang
-rwxr-xr-x 1 leadroyal staff 209B 1 17 11:01 armv7a-linux-androideabi18-clang++
-rwxr-xr-x 1 leadroyal staff 190B 1 17 11:01 armv7a-linux-androideabi19-clang
-rwxr-xr-x 1 leadroyal staff 209B 1 17 11:01 armv7a-linux-androideabi19-clang++
-rwxr-xr-x 1 leadroyal staff 190B 1 17 11:01 armv7a-linux-androideabi21-clang
-rwxr-xr-x 1 leadroyal staff 209B 1 17 11:01 armv7a-linux-androideabi21-clang++

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

1
2
3
4
#include<stdio.h>
void say(){
printf("HelloWorld\n");
}

使用该命令编译目标文件

1
2
3
➜ /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi17-clang -c test.c
➜ /tmp file test.o
test.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
➜  /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/arm-linux-androideabi/bin/readelf -r test.o

Relocation section '.rel.text' at offset 0x234 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000014 0000061c R_ARM_CALL 00000000 printf
00000024 00000403 R_ARM_REL32 00000000 .L.str

Relocation section '.rel.ARM.exidx' at offset 0x244 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
00000000 0000052a R_ARM_PREL31 00000000 .text

➜ /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/arm-linux-androideabi/bin/objdump -Ss test.o

test.o: file format elf32-littlearm

Contents of section .text:
0000 00482de9 0db0a0e1 08d04de2 10009fe5 .H-.......M.....
0010 00008fe0 feffffeb 04008de5 0bd0a0e1 ................
0020 0088bde8 0c000000 ........
......省略......
Contents of section .rodata.str1.1:
0000 48656c6c 6f576f72 6c640a00 HelloWorld..

Disassembly of section .text:

00000000 <say>:
0:e92d4800 push{fp, lr}
4:e1a0b00d movfp, sp
8:e24dd008 subsp, sp, #8
c:e59f0010 ldrr0, [pc, #16]; 24 <say+0x24>
10:e08f0000 addr0, pc, r0
14:ebfffffe bl0 <printf>
18:e58d0004 strr0, [sp, #4]
1c:e1a0d00b movsp, fp
20:e8bd8800 pop{fp, pc}
24:0000000c .word0x0000000c

ARM 架构是冯诺依曼模型,数据段和代码段是混在一起的,例如上面的例子,在0x24 的位置放的就是数据,理应当存放指向 HelloWorld 的字符串,但却放了一个奇怪的数字。 重定位表有3项,这里只关注 printf 和 .L.str ,另外一个不用管,因为我看不懂。

二、重定位表简介

通过 readelf -r 可以简单的知道,.rel.text 里有两项我们关注的,表示的是.text 节的重定位;类似.rel.ARM.exidx 表示的是.ARM.exidx 的重定位。猜测 .rel 开头的应该是对应节的重定位表,将来会出现 .rel.data 里面每一项都是一种结构,我使用的是 pyelftools 这个库,抄一下pwntools 的包装代码,编写了下面的代码,用于理解重定位表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import mmap
import struct

from elftools.elf.elffile import ELFFile
from elftools.elf.descriptions import _DESCR_RELOC_TYPE_ARM

file = open('/tmp/test.o', 'rb')
data = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_COPY)
elf = ELFFile(data)

rel_text_setcion = elf.get_section_by_name('.rel.text')
for rel in rel_text_setcion.iter_relocations():
r_offset = rel.entry.r_offset # 表示 .text[r_offset]处需要进行修复
r_info = rel.entry.r_info # 用来存放 r_info_sym 和 r_info_type
r_info_sym = rel.entry.r_info_sym # 表示该重定向为符号表中的第 N 项
r_info_type = rel.entry.r_info_type # 表示该重定向的类型,对应枚举值 ENUM_RELOC_TYPE_ARM
sym = elf.get_section_by_name('.symtab').get_symbol(r_info_sym)
print(".text[0x%x] should be fixed. Type is %s. Symbol is %s" % (r_offset, _DESCR_RELOC_TYPE_ARM[r_info_type], sym.name))
print("symbol %s %s %s" % (sym.name, sym.entry.st_info.bind, sym.entry.st_info.type))

输出是

1
2
3
4
.text[0x14] should be fixed. Type is R_ARM_CALL. Symbol is printf
symbol printf STB_GLOBAL STT_NOTYPE
.text[0x24] should be fixed. Type is R_ARM_REL32. Symbol is .L.str
symbol .L.str STB_LOCAL STT_OBJECT

表示这两个位置需要修复,前者翻译一下是 BL 0x00 ,也就是原地死循环,后者放着常数。 这里理解起来,建议使用 ida,ida在打开.o文件的时候,会自动帮你把重定位都修好,其实有很多节都是不存在的、由 ida 替你修复的,方便观看而已,例如这个。

对printf原本是

1
14:ebfffffe bl0 <printf>

现在是

1
14: 08 00 00 EB                 BL              printf

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

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

1
24:0000000c .word0x0000000c

现在是

1
24: 18 00 00 00 off_24          DCD .L.str - 0x18

而 .L.str 是在 0x30处

1
.rodata.str1.1:00000030 .L.str          DCB "HelloWorld",0xA,0

因为 ARM 是通过 PC+offset 来拿数据的,将来运行到这里时,[0x24] 存放的是PC到HelloWorld的距离,从而成功获取到 HelloWorld 的绝对地址,PC=base+0x18offset=0x18 ,最终寻找到base+0x30 。 这只是两种情况,将来会遇到更多在情况,下一篇 blog 会讲一下我遇到的所有的情况。

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

1
2
3
➜  /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi17-clang test.o -shared -o test.so
➜ /tmp file test.so
test.so: ELF 32-bit LSB pie executable ARM, EABI5 version 1 (SYSV), dynamically linked, not stripped

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

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

所以 so 里面是让它跳到plt 节,而 plt 是一段可执行代码,稍微处理一下从got 节读一个地址,跳过去;got 本身没有内容,是在加载的时候被 dl 填充,这个过程不在本文讨论范围内。 而 HelloWorld 的字符串落在0x3e8+0x18=0x404 ,没毛病,也是同样的重定位方式。

四、HelloWorld 的修复

上面讲的是编译器、连接器、加载器帮我们干的事情,这里讲一下如何手动去修复 HelloWorld的重定向。 我们的目标是:自己写一段代码,去加载一个 .o 文件,运行它,让寄存器指向这个字符串。

1
.text[0x24] should be fixed. Type is R_ARM_REL32. Symbol is .L.str
1
24:0000000c .word0x0000000c
1
.text:00000010    ADD    R0, PC, R0 ; "HelloWorld\n" #这个R0就是[0x24]

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

1
0x24-(0x10+0x8) = 0xC

将来它要被写为HelloWorld的地址,也是一个相对偏移。 这里用python简单地算一下字符串在文件中的位置:

1
2
3
rodata_offset = elf.get_section_by_name('.rodata.str1.1').header.sh_offset
hello_world_offset = elf.get_section_by_name('.symtab').get_symbol_by_name('.L.str')[0].entry.st_value
hello_world_addr = rodata_offset + hello_world_offset

算出来hello_world_addr是文件开头起、距离 0x64 的位置。 再算一下取地址时,PC 在文件中的位置:

1
2
3
text_offset = elf.get_section_by_name('.text').header.sh_offset
inst_offset = 0x10
inst_addr = text_offset + inst_offset + 8

算出来 .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一段内存会报错。

1
2
3
root@HWPLK:/ # setenforce 0
root@HWPLK:/ # getenforce
Permissive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern "C" JNIEXPORT jstring JNICALL
Java_com_leadroyal_hellonative_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
int fd = open("/data/local/tmp/test.o", O_RDWR O_EXCL);
__android_log_print(ANDROID_LOG_ERROR, "Native", "fd = %d", fd);
unsigned char *data = static_cast<unsigned char *>(mmap(nullptr, 0x1000, PROT_READ PROT_WRITE PROT_EXEC, MAP_PRIVATE, fd, 0));
*(int *) (data + 0x34 + 0x24) = 0x18;
unsigned int entry = 52; // say函数的入口
void (*say)() = (void (*)()) (data + entry);
say();
return env->NewStringUTF(hello.c_str());
}

因为我没有修复 BL printf ,所以会有一个一直跳自己的死循环,会白屏,所以下断点看看。 这里确实进入了say里面。

1
2
3
4
5
6
7
8
9
(lldb) dis
-> 0xf4a3d034: push {r11, lr}
0xf4a3d038: mov r11, sp
0xf4a3d03c: sub sp, sp, #8
0xf4a3d040: ldr r0, [pc, #0x10]
0xf4a3d044: add r0, pc, r0
0xf4a3d048: bl 0xf4a3d048
0xf4a3d04c: str r0, [sp, #0x4]
0xf4a3d050: mov sp, r11

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

1
2
3
4
5
6
(lldb) register read r0
r0 = 0xf4a3d064
(lldb) x/s $r0
0xf4a3d064: "HelloWorld\n"
(lldb) dis
-> 0xf4a3d048: bl 0xf4a3d048

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 的汇编内容:

1
2
3
.plt [1164] ldr r12, [pc]  # e59fc000
.plt [1168] bx r12 # e12fff1c
.got [1172] .word addr_printf

算一下 BL printf 应该跳多远

1
1164 - (0x34+0x14+0x8)  = 0x43c

ok,修改汇编为 0xeb000000 (0x43c>>2) = 0xeb00010f 来!梭哈一把!

1
2
3
4
5
6
7
8
9
10
11
12
13
int fd = open("/data/local/tmp/test.o", O_RDWR  O_EXCL);
__android_log_print(ANDROID_LOG_ERROR, "Native", "fd = %d", fd);
unsigned char *data = static_cast<unsigned char *>(mmap(nullptr, 0x1000, PROT_READ PROT_WRITE PROT_EXEC, MAP_PRIVATE, fd, 0));
*(unsigned int *) (data + 0x34 + 0x14) = 0xeb000000 (0x43c >> 2); // 修printf
*(unsigned int *) (data + 0x34 + 0x24) = 0x18; // 修HelloWorld
unsigned int entry = 52; // say函数的入口
*(unsigned int *) (data + 1164) = 0xe59fc000; // ldr r12, [pc]
*(unsigned int *) (data + 1168) = 0xe12fff1c; // bx r12
*(unsigned int *) (data + 1172) = (unsigned int) printf; // add_printf

void (*say)() = (void (*)()) (data + entry);
say();
return env->NewStringUTF(hello.c_str());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(lldb) dis
-> 0xf4a3d048: bl 0xf4a3d48c
0xf4a3d04c: str r0, [sp, #0x4]
0xf4a3d050: mov sp, r11
0xf4a3d054: pop {r11, pc}
0xf4a3d058: andeq r0, r0, r8, lsl r0
0xf4a3d05c: andeq r0, r0, r0
0xf4a3d060: andeq r0, r0, r1
0xf4a3d064: .long 0x6c6c6548 ; unknown opcode
(lldb) x/3i 0xf4a3d48c
0xf4a3d48c: 0xe59fc000 ldr r12, [pc]
0xf4a3d490: 0xe12fff1c bx r12
0xf4a3d494: 0xf74aaa3d .long 0xf74aaa3d ; unknown opcode

(lldb) si
(lldb) dis
-> 0xf4a3d490: bx r12
0xf4a3d494: .long 0xf74aaa3d ; unknown opcode
0xf4a3d498: andeq r0, r0, r0
0xf4a3d49c: andeq r0, r0, r0
0xf4a3d4a0: andeq r0, r0, r0
0xf4a3d4a4: andeq r0, r0, r0
0xf4a3d4a8: andeq r0, r0, r0
0xf4a3d4ac: andeq r0, r0, r0
(lldb) si
(lldb) dis
libc.so`printf:
-> 0xf74aaa3c <+0>: push {r0, r1, r2, r3}
0xf74aaa3e <+2>: ldr r3, [pc, #0x24]
0xf74aaa40 <+4>: push {r0, r1, r2, lr}

没毛病,成功调用! 再打包一个可执行文件出来,不要用 JNI 了。

1
2
3
4
5
6
7
8
9
10
11
12
➜  HelloNative cd app/build/intermediates/cmake/debug/obj/armeabi-v7a 
➜ armeabi-v7a lsa
total 3408
drwxr-xr-x 4 leadroyal staff 128B Feb 3 18:28 .
drwxr-xr-x 6 leadroyal staff 192B Feb 3 15:25 ..
-rwxr-xr-x 1 leadroyal staff 875K Feb 3 18:27 libnative-lib.so
-rw-r--r-- 1 leadroyal staff 827K Feb 3 18:28 native-exec
➜ armeabi-v7a adb push native-exec /data/local/tmp
native-exec: 1 file pushed. 5.5 MB/s (846504 bytes in 0.146s)
➜ armeabi-v7a adb shell chmod 777 /data/local/tmp/native-exec
➜ armeabi-v7a adb shell /data/local/tmp/native-exec
HelloWorld

没毛病,一遍过!

六、整体的理解

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

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

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

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