从小老师就教育我们,从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 mmapimport structfrom elftools.elf.elffile import ELFFilefrom elftools.elf.descriptions import _DESCR_RELOC_TYPE_ARMfile = 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 r_info = rel.entry.r_info r_info_sym = rel.entry.r_info_sym r_info_type = rel.entry.r_info_type 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+0x18
,offset=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)得到的差,即
将来它要被写为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 JNICALLJava_com_leadroyal_hellonative_MainActivity_stringFromJNI ( JNIEnv *env, jobject ) { 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 ; 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 优化的,加深了印象。