当前位置:首页 > 公众号精选 > IOT物联网小镇
[导读]作 者:道哥,10年嵌入式开发老兵,专注于:C/C、嵌入式、Linux。关注下方公众号,回复【书籍】,获取Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章(PDF格式)。目录理论与实践开始新的动态库面临的问题怎么做?ELF概述ELF文件头SHT(sectionhe...



目录
  • 理论与实践


  • 开始


    • 新的动态库


    • 面临的问题


    • 怎么做?


  • ELF


    • 概述


    • ELF 文件头


    • SHT(section header table)


    • PHT(program header table)


    • 连接视图和运行视图


    • .dynamic section


    • 动态链接器(liker)


    • 追踪


  • 内存


    • 基地址


    • 内存访问权限


    • 指令缓存


    • 验证


    • 使用 xhook


  • FAQ


    • 可以直接从文件中读取 ELF 信息吗?


    • 计算地址的精确方法是什么?


    • 目标 ELF 使用的编译选项对 hook 有什么影响?


    • hook 时遇到偶发的段错误时什么原因?如何处理?


    • ELF 内部函数之间的调用能 hook 吗?


别人的经验,我们的阶梯!


大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【动态库的内存处理】


在上周的一篇转载文章中,介绍了一种如何把一个动态库中的调用函数进行“掉包”的技术,从而达到一些特殊的目的。


这个技术是爱奇艺开源的 xHook,github地址是:https://github.com/iqiyi/xHook


在官方文档中,作者的描述场景是android系统。因为底层都是基于Linux的,因此这里介绍的hook技术也同样适合其他Linux系统的工作环境。


这篇文章,我们就一起向大神学习一下,如何一步一步找到目标(被调用函数的地址),然后偷换成其他的函数地址。


文章的内容比较长,但是绝对值得花半天的功夫、甚至几天的时间来研究其中的知识点。


也许它不能立竿见影的提高你的编程技术,但是对于内功的修炼、提升,绝对是一等一的好资料!


在学习的过程中,我会在一些重要的地方,用橙色字体加上自己的学习心得,或者说理解。如果理解有误,欢迎指出、一起讨论。


为了便于阅读,我在原文中比较关键的文字上,添加了字体颜色。


理论与实践

关于动态库的相关内容,市面上质量比较好的书籍可能就是:《程序员的自我修养-链接、装载和库》这本书了。


我手里的这一本,是 2019 年 6 月第 29 次印刷,足见这本书的生命力是多么的强悍!


黑客级别的文章:把动态库的内存操作玩出了新花样!如果您读过这本书,可能会有这样的感受:书中的内容理论性太强,即使自己明白了其中的道理,但是应该如何实践呢?或者说,能利用这些知识点来做什么呢?


爱奇艺的xHook,就是对这些理论知识的完美实践!


《程序员的自我修养-链接、装载和库》是一本不可多得的好书,如果您对动态库很感兴趣,建议您入手一本纸质书,支持一下作者!


如果只是想浏览一下,我这里有一个 PDF 版本(忘记从哪里下载的了),已经放在网盘里。


如果您需要的话,在公众号【IOT物联网小镇】的后台留言:1031,即可获取下载链接。


开始

新的动态库

我们有一个新的动态库:libtest.so。


头文件 test.h


#ifndef TEST_H
#define TEST_H 1

#ifdef __cplusplus
extern "C" {
#endif

void say_hello();

#ifdef __cplusplus
}
#endif

#endif
源文件 test.c


#include
#include

void say_hello()
{
char *buf = malloc(1024);
if(NULL != buf)
{
snprintf(buf, 1024, "%s", "hello\n");
printf("%s", buf);
}
}
say_hello的功能是在终端打印出hello\n这6个字符(包括结尾的\n)。


我们需要一个测试程序:main。


源文件 main.c


#include

int main()
{
say_hello();
return 0;
}
编译它们分别生成libtest.so和main。运行一下:


caikelun@debian:~$ adb push ./libtest.so ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
hello
caikelun@debian:~$
太棒了!libtest.so的代码虽然看上去有些愚蠢,但是它居然可以正确的工作,那还有什么可抱怨的呢?


赶紧在新版APP中开始使用它吧!


遗憾的是,正如你可能已经发现的,libtest.so存在严重的内存泄露问题,每调用一次say_hello函数,就会泄露1024字节的内存。


新版APP上线后崩溃率开始上升,各种诡异的崩溃信息和报障信息跌撞而至。


面临的问题

幸运的是,我们修复了libtest.so的问题。可是以后怎么办呢?我们面临2个问题:


  1. 当测试覆盖不足时,如何及时发现和准确定位线上 APP 的此类问题?


  2. 如果 libtest.so 是某些机型的系统库,或者第三方的闭源库,我们如何修复它?如果监控它的行为?


怎么做?

如果我们能对动态库中的函数调用做hook(替换,拦截,窃听,或者你觉得任何正确的描述方式),那就能够做到很多我们想做的事情


比如hook malloc,calloc,realloc和free,我们就能统计出各个动态库分配了多少内存,哪些内存一直被占用没有释放。


这真的能做到吗?答案是:hook我们自己的进程是完全可以的。


hook其他进程需要root权限(对于其他进程,没有root权限就没法修改它的内存空间,也没法注入代码)。


幸运的是,我们只要hook自己就够了。


道哥注解:如果去 hook 不属于自己的进程,那就真的属于病毒了!


进程级别的隔离,一般由操作系统来处理!


ELF

道哥注解:


关于 ELF 的详细介绍,也可以看一下我之前写的一篇文章:Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索


这篇文章的内容非常详细,就像剥洋葱一样,一层一层分析 ELF 文件的结构。


并且以图片的方式,把 ELF 文件中的二进制内容与相关的结构体成员变量一一对应起来,比较直观。


概述

ELF(Executable and Linkable Format)是一种行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object文件和core dumps文件。


使用google NDK对源代码进行编译和链接,生成的动态库或可执行文件都是ELF格式的。


用readelf可以查看ELF文件的基本信息,用objdump可以查看ELF文件的反汇编输出。


ELF格式的概述可以参考这里,完整定义可以参考这里。


其中最重要的部分是:ELF 文件头、SHT(section header table)、PHT(program header table)。


ELF 文件头

ELF文件的起始处,有一个固定格式的定长的文件头(32位架构为52字节,64位架构为64字节)。ELF文件头以magic number 0x7F 0x45 0x4C 0x46开始(其中后3个字节分别对应可见字符E L F)。


libtest.so的ELF文件头信息:


caikelun@debian:~$ arm-linux-androideabi-readelf -h ./libtest.so

ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 52 (bytes into file)
Start of section headers: 12744 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 25
Section header string table index: 24
ELF文件头中包含了SHT和PHT在当前ELF文件中的起始位置和长度。


例如,libtest.so的SHT起始位置为12744,长度40字节;


PHT起始位置52,长度32字节。


SHT(section header table)

ELF以section为单位来组织和管理各种信息。


ELF使用SHT来记录所有section的基本信息。


黑客级别的文章:把动态库的内存操作玩出了新花样!主要包括:section的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。


libtest.so的SHT:


caikelun@debian:~$ arm-linux-androideabi-readelf -S ./libtest.so

There are 25 section headers, starting at offset 0x31c8:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.android.ide NOTE 00000134 000134 000098 00 A 0 0 4
[ 2] .note.gnu.build-i NOTE 000001cc 0001cc 000024 00 A 0 0 4
[ 3] .dynsym DYNSYM 000001f0 0001f0 0003a0 10 A 4 1 4
[ 4] .dynstr STRTAB 00000590 000590 0004b1 00 A 0 0 1
[ 5] .hash HASH 00000a44 000a44 000184 04 A 3 0 4
[ 6] .gnu.version VERSYM 00000bc8 000bc8 000074 02 A 3 0 2
[ 7] .gnu.version_d VERDEF 00000c3c 000c3c 00001c 00 A 4 1 4
[ 8] .gnu.version_r VERNEED 00000c58 000c58 000020 00 A 4 1 4
[ 9] .rel.dyn REL 00000c78 000c78 000040 08 A 3 0 4
[10] .rel.plt REL 00000cb8 000cb8 0000f0 08 AI 3 18 4
[11] .plt PROGBITS 00000da8 000da8 00017c 00 AX 0 0 4
[12] .text PROGBITS 00000f24 000f24 0015a4 00 AX 0 0 4
[13] .ARM.extab PROGBITS 000024c8 0024c8 00003c 00 A 0 0 4
[14] .ARM.exidx ARM_EXIDX 00002504 002504 000100 08 AL 12 0 4
[15] .fini_array FINI_ARRAY 00003e3c 002e3c 000008 04 WA 0 0 4
[16] .init_array INIT_ARRAY 00003e44 002e44 000004 04 WA 0 0 1
[17] .dynamic DYNAMIC 00003e48 002e48 000118 08 WA 4 0 4
[18] .got PROGBITS 00003f60 002f60 0000a0 00 WA 0 0 4
[19] .data PROGBITS 00004000 003000 000004 00 WA 0 0 4
[20] .bss NOBITS 00004004 003004 000000 00 WA 0 0 1
[21] .comment PROGBITS 00000000 003004 000065 01 MS 0 0 1
[22] .note.gnu.gold-ve NOTE 00000000 00306c 00001c 00 0 0 4
[23] .ARM.attributes ARM_ATTRIBUTES 00000000 003088 00003b 00 0 0 1
[24] .shstrtab STRTAB 00000000 0030c3 000102 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (noread), p (processor specific)
比较重要,且和hook关系比较大的几个section是:


dynstr:保存了所有的字符串常量信息。


dynsym:保存了符号(symbol)的信息(符号的类型、起始地址、大小、符号名称在 .dynstr 中的索引编号等)。函数也是一种符号。


text:程序代码经过编译后生成的机器指令。


dynamic:供动态链接器使用的各项信息,记录了当前 ELF 的外部依赖,以及其他各个重要 section 的起始位置等信息。


got:Global Offset Table。用于记录外部调用的入口地址。动态链接器(linker)执行重定位(relocate)操作时,这里会被填入真实的外部调用的绝对地址。


plt:Procedure Linkage Table。外部调用的跳板,主要用于支持 lazy binding 方式的外部调用重定位。(Android 目前只有 MIPS 架构支持 lazy binding)


rel.plt:对外部函数直接调用的重定位信息。


rel.dyn:除 .rel.plt 以外的重定位信息。(比如通过全局函数指针来调用外部函数)


道哥注解:


ELF 文件中,dynamic 这个section是非常重要的!


当一个动态库被加载到内存中时,动态链接器就是读取这个section的内容,比如:


依赖于其他哪些共享对象;


动态链接符号表的位置(.dynsym);


动态链接重定位表的位置;


初始化代码的位置;


...


使用指令:readelf -d xxx.so,即可查看一个动态库中 .dynamic 的内容。


另外,gotplt 这两个 section,主要就是用来处理地址无关的功能。


如果您查询-fPIC的相关内容,一定会讲解这两个知识点。


总的来说就是:Linux 下的动态库,把代码段中地址有关的部分,通过“增加一层”的原理,全部变成“地址无关”的。


这样的话,动态库的代码段在加载到物理内存中之后,就可以被多个不同的进程来共享了,只要把代码段的物理地址,映射到每个进程自己的虚拟地址即可。


而“地址相关”的部分,就放在 got(对变量的引用) 和plt(对函数的引用) 中。


PHT(program header table)

·ELF被加载到内存时,是以segment为单位的。一个segment包含了一个或多个section`。


ELF使用PHT来记录所有segment的基本信息。


主要包括:segment的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。


libtest.so的PHT:


caikelun@debian:~$ arm-linux-androideabi-readelf -l ./libtest.so

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 8 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00100 0x00100 R 0x4
LOAD 0x000000 0x00000000 0x00000000 0x02604 0x02604 R E 0x1000
LOAD 0x002e3c 0x00003e3c 0x00003e3c 0x001c8 0x001c8 RW 0x1000
DYNAMIC 0x002e48 0x00003e48 0x00003e48 0x00118 0x00118 RW 0x4
NOTE 0x000134 0x00000134 0x00000134 0x000bc 0x000bc R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
EXIDX 0x002504 0x00002504 0x00002504 0x00100 0x00100 R 0x4
GNU_RELRO 0x002e3c 0x00003e3c 0x00003e3c 0x001c4 0x001c4 RW 0x4

Section to Segment mapping:
Segment Sections...
00
01 .note.android.ident .note.gnu.build-id .dynsym .dynstr .hash .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .plt .text .ARM.extab .ARM.exidx
02 .fini_array .init_array .dynamic .got .data
03 .dynamic
04 .note.android.ident .note.gnu.build-id
05
06 .ARM.exidx
07 .fini_array .init_array .dynamic .got
所有类型为PT_LOAD的segment都会被动态链接器(linker)映射(mmap)到内存中。


连接视图(Linking View)和执行视图(Execution View)

连接视图:ELF 未被加载到内存执行前,以 section 为单位的数据组织形式。


执行视图:ELF 被加载到内存后,以 segment 为单位的数据组织形式。


黑客级别的文章:把动态库的内存操作玩出了新花样!黑客级别的文章:把动态库的内存操作玩出了新花样!我们关心的hook操作,属于动态形式的内存操作,因此主要关心的是执行视图,即ELF被加载到内存后,ELF中的数据是如何组织和存放的。


.dynamic section

这是一个十分重要和特殊的section,其中包含了ELF中其他各个section的内存位置等信息。


在执行视图中,总是会存在一个类型为PT_DYNAMIC的segment,这个segment就包含了.dynamic section的内容。


无论是执行hook操作时,还是动态链接器执行动态链接时,都需要通过PT_DYNAMIC segment来找到.dynamic section的内存位置,再进一步读取其他各项section的信息。


libtest.so的.dynamic section:


caikelun@debian:~$ arm-linux-androideabi-readelf -d ./libtest.so

Dynamic section at offset 0x2e48 contains 30 entries:
Tag Type Name/Value
0x00000003 (PLTGOT) 0x3f7c
0x00000002 (PLTRELSZ) 240 (bytes)
0x00000017 (JMPREL) 0xcb8
0x00000014 (PLTREL) REL
0x00000011 (REL) 0xc78
0x00000012 (RELSZ) 64 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffa (RELCOUNT) 3
0x00000006 (SYMTAB) 0x1f0
0x0000000b (SYMENT) 16 (bytes)
0x00000005 (STRTAB) 0x590
0x0000000a (STRSZ) 1201 (bytes)
0x00000004 (HASH) 0xa44
0x00000001 (NEEDED) Shared library: [libc.so]
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libstdc .so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x0000000e (SONAME) Library soname: [libtest.so]
0x0000001a (FINI_ARRAY) 0x3e3c
0x0000001c (FINI_ARRAYSZ) 8 (bytes)
0x00000019 (INIT_ARRAY) 0x3e44
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) Flags: NOW
0x6ffffff0 (VERSYM) 0xbc8
0x6ffffffc (VERDEF) 0xc3c
0x6ffffffd (VERDEFNUM) 1
0x6ffffffe (VERNEED) 0xc58
0x6fffffff (VERNEEDNUM) 1
0x00000000 (NULL) 0x0

动态链接器(linker)

安卓中的动态链接器程序是linker。源码在这里。


动态链接(比如执行dlopen)的大致步骤是:


  1. 检查已加载的 ELF 列表。(如果 libtest.so 已经加载,就不再重复加载了,仅把 libtest.so 的引用计数加一,然后直接返回。)


  2. 从 libtest.so 的 .dynamic section 中读取 libtest.so 的外部依赖的 ELF 列表,从此列表中剔除已加载的 ELF,最后得到本次需要加载的 ELF 完整列表(包括 libtest.so 自身)。


  3. 逐个加载列表中的 ELF。加载步骤:


(1) 用 mmap 预留一块足够大的内存,用于后续映射 ELF。(MAP_PRIVATE 方式)


(2) 读 ELF 的 PHT,用 mmap 把所有类型为 PT_LOAD 的 segment 依次映射到内存中。


(3) 从 .dynamic segment 中读取各信息项,主要是各个 section 的虚拟内存相对地址,然后计算并保存各个 section 的虚拟内存绝对地址。


(4) 执行重定位操作(relocate),这是最关键的一步。重定位信息可能存在于下面的一个或多个 secion 中:.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android。动态链接器需要逐个处理这些 .relxxx section 中的重定位诉求。根据已加载的 ELF 的信息,动态链接器查找所需符号的地址(比如 libtest.so 的符号 malloc),找到后,将地址值填入 .relxxx 中指明的目标地址中,这些“目标地址”一般存在于.got 或 .data 中。


(5) ELF 的引用计数加一。


  1. 逐个调用列表中 ELF 的构造函数(constructor),这些构造函数的地址是之前从 .dynamic segment 中读取到的(类型为 DT_INIT 和 DT_INIT_ARRAY)。各 ELF 的构造函数是按照依赖关系逐层调用的,先调用被依赖 ELF 的构造函数,最后调用 libtest.so 自己的构造函数。(ELF 也可以定义自己的析构函数(destructor),在 ELF 被 unload 的时候会被自动调用)
等一下!我们似乎发现了什么!再看一遍重定位操作(relocate)的部分。


难道我们只要从这些.relxxx中获取到“目标地址”,然后在“目标地址”中重新填上一个新的函数地址,这样就完成hook了吗?也许吧。


追踪

静态分析验证一下还是很容易的。以armeabi-v7a架构的libtest.so为例。


先看一下say_hello函数对应的汇编代码吧。


caikelun@debian:~/$ arm-linux-androideabi-readelf -s ./libtest.so

Symbol table '.dynsym' contains 58 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND snprintf@LIBC (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND malloc@LIBC (2)
4: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND printf@LIBC (2)
6: 00000f61 60 FUNC GLOBAL DEFAULT 12 say_hello
...............
...............
找到了!say_hello在地址f61,对应的汇编指令体积为60(10 进制)字节。


用objdump查看say_hello的反汇编输出。


caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00000f60 :
f60: b5b0 push {r4, r5, r7, lr}
f62: af02 add r7, sp, #8
f64: f44f 6080 mov.w r0, #1024 ; 0x400
f68: f7ff ef34 blx dd4
f6c: 4604 mov r4, r0
f6e: b16c cbz r4, f8c
f70: a507 add r5, pc, #28 ; (adr r5, f90 )
f72: a308 add r3, pc, #32 ; (adr r3, f94 )
f74: 4620 mov r0, r4
f76: f44f 6180 mov.w r1, #1024 ; 0x400
f7a: 462a mov r2, r5
f7c: f7ff ef30 blx de0
f80: 4628 mov r0, r5
f82: 4621 mov r1, r4
f84: e8bd 40b0 ldmia.w sp!, {r4, r5, r7, lr}
f88: f001 ba96 b.w 24b8 <_Unwind_GetTextRelBase@@Base 0x8>
f8c: bdb0 pop {r4, r5, r7, pc}
f8e: bf00 nop
f90: 7325 strb r5, [r4, #12]
f92: 0000 movs r0, r0
f94: 6568 str r0, [r5, #84] ; 0x54
f96: 6c6c ldr r4, [r5, #68] ; 0x44
f98: 0a6f lsrs r7, r5, #9
f9a: 0000 movs r0, r0
...............
...............
对malloc函数的调用对应于指令blx dd4。跳转到了地址dd4。


看看这个地址里有什么吧:


caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00000dd4 :
dd4: e28fc600 add ip, pc, #0, 12
dd8: e28cca03 add ip, ip, #12288 ; 0x3000
ddc: e5bcf1b4 ldr pc, [ip, #436]! ; 0x1b4
...............
...............
果然,跳转到了.plt中,经过了几次地址计算,最后跳转到了地址3f90中的值指向的地址处,3f90是个函数指针


稍微解释一下:因为arm处理器使用3级流水线,所以第一条指令取到的pc的值是当前执行的指令地址8。


于是:dd4830001b4=3f90。


地址3f90在哪里呢:


caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00003f60 <.got>:
...
3f70: 00002604 andeq r2, r0, r4, lsl #12
3f74: 00002504 andeq r2, r0, r4, lsl #10
...
3f88: 00000da8 andeq r0, r0, r8, lsr #27
3f8c: 00000da8 andeq r0, r0, r8, lsr #27
3f90: 00000da8 andeq r0, r0, r8, lsr #27
...............
...............
果然,在.got里。


顺便再看一下.rel.plt:


caikelun@debian:~$ arm-linux-androideabi-readelf -r ./libtest.so

Relocation section '.rel.plt' at offset 0xcb8 contains 30 entries:
Offset Info Type Sym.Value Sym. Name
00003f88 00000416 R_ARM_JUMP_SLOT 00000000 __cxa_atexit@LIBC
00003f8c 00000116 R_ARM_JUMP_SLOT 00000000 __cxa_finalize@LIBC
00003f90 00000316 R_ARM_JUMP_SLOT 00000000 malloc@LIBC
...............
...............
malloc的地址居然正好存放在3f90里,这绝对不是巧合啊!


道哥注解:


.rel.plt 这个section中,记录了重定位表的信息,也就是哪些函数地址需要被重定位。


链接器把所有被依赖的共享对象加载到内存中时,会把每个共享对象中的符号给汇总起来,得到全局符号表。


然后再检查每个共享对象中的 .rel.plt,是否需要对一些地址进行重定位。


如果需要的话,就从全局符号表中找到该符号的内存地址,然后填写到 .plt 中对应的位置。


还等什么,赶紧改代码吧。我们的main.c应该改成这样:


#include

void *my_malloc(size_t size)
{
printf("%zu bytes memory are allocated by libtest.so\n", size);
return malloc(size);
}

int main()
{
void **p = (void **)0x3f90;
*p = (void *)my_malloc; // do hook

say_hello();
return 0;
}
编译运行一下:


caikelun@debian:~$ adb push ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
Segmentation fault
caikelun@debian:~$
思路是正确的。但之所以还是失败了,是因为这段代码存在下面的3个问题:


  1. 3f90 是个相对内存地址,需要把它换算成绝对地址。


  2. 3f90 对应的绝对地址很可能没有写入权限,直接对这个地址赋值会引起段错误。


  3. 新的函数地址即使赋值成功了,my_malloc 也不会被执行,因为处理器有指令缓存(instruction cache)。


我们需要解决这些问题。


内存

基地址

在进程的内存空间中,各种ELF的加载地址是随机的,只有在运行时才能拿到加载地址,也就是基地址。


道哥注解:


我们在查看一个动态链接库时,看到的入口地址都是0x0000_0000。


动态库在被加载到内存中时,因为存在加载顺序的问题,所以加载地址不是固定的


还有一种说法:对于某一个进程而言,它在被加载到内存中时,它所依赖的所有动态库的顺序是一定的


因此,每个动态库的加载地址也是固定的,因此,理论上可以在第一次重定位之后,把重定位之后的代码段存储下来。


这样,以后再次启动这个进程时,就不需要重定位了,加快程序的启动速度。


我们需要知道ELF的基地址,才能将相对地址换算成绝对地址


没有错,熟悉Linux开发的聪明的你一定知道,我们可以直接调用dl_iterate_phdr。详细的定义见这里。


道哥注解:


dl_iterate_phdr 这个函数真的很有用,以回调函数的形式可到每一个动态链接库的加载地址等信息。


如果没有这个函数,很多信息就需要从 /proc/xxx/maps 中来获取,执行速度慢,因为要处理很多字符串信息。


嗯,先等等,多年的Android开发被坑经历告诉我们,还是再看一眼NDK里的linker.h头文件吧:


#if defined(__arm__)

#if __ANDROID_API__ >= 21
int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data) __INTRODUCED_IN(21);
#endif /* __ANDROID_API__ >= 21 */

#else
int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data);
#endif
为什么?!ARM架构的Android 5.0以下版本居然不支持dl_iterate_phdr!


我们的APP可是要支持Android 4.0以上的所有版本啊。


特别是ARM,怎么能不支持呢?!这还让不让人写代码啦!


幸运的是,我们想到了,我们还可以解析/proc/self/maps:


root@android:/ # ps | grep main
ps | grep main
shell 7884 7882 2616 1016 hrtimer_na b6e83824 S /data/local/tmp/main

root@android:/ # cat /proc/7884/maps
cat /proc/7884/maps

address perms offset dev inode pathname
---------------------------------------------------------------------
...........
...........
b6e42000-b6eb5000 r-xp 00000000 b3:17 57457 /system/lib/libc.so
b6eb5000-b6eb9000 r--p 00072000 b3:17 57457 /system/lib/libc.so
b6eb9000-b6ebc000 rw-p 00076000 b3:17 57457 /system/lib/libc.so
b6ec6000-b6ec9000 r-xp 00000000 b3:19 753708 /data/local/tmp/libtest.so
b6ec9000-b6eca000 r--p 00002000 b3:19 753708 /data/local/tmp/libtest.so
b6eca000-b6ecb000 rw-p 00003000 b3:19 753708 /data/local/tmp/libtest.so
b6f03000-b6f20000 r-xp 00000000 b3:17 32860 /system/bin/linker
b6f20000-b6f21000 r--p 0001c000 b3:17 32860 /system/bin/linker
b6f21000-b6f23000 rw-p 0001d000 b3:17 32860 /system/bin/linker
b6f25000-b6f26000 r-xp 00000000 b3:19 753707 /data/local/tmp/main
b6f26000-b6f27000 r--p 00000000 b3:19 753707 /data/local/tmp/main
becd5000-becf6000 rw-p 00000000 00:00 0 [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
...........
...........
maps返回的是指定进程的内存空间中mmap的映射信息,包括各种动态库、可执行文件(如:linker),栈空间,堆空间,甚至还包括字体文件。


maps格式的详细说明见这里。


我们的libtest.so在maps中有3行记录。


offset为0的第一行的起始地址b6ec6000在绝大多数情况下就是我们寻找的基地址


内存访问权限

maps返回的信息中已经包含了权限访问信息。


如果要执行hook,就需要写入的权限,可以使用mprotect来完成:


#include

int mprotect(void *addr, size_t len, int prot);
注意修改内存访问权限时,只能以“页”为单位。


mprotect 的详细说明见这里。


指令缓存

注意.got和.data的section类型是PROGBITS,也就是执行代码。处理器可能会对这部分数据做缓存。


修改内存地址后,我们需要清除处理器的指令缓存,让处理器重新从内存中读取这部分指令。


方法是调用__builtin___clear_cache:


void __builtin___clear_cache (char *begin, char *end);
注意清除指令缓存时,也只能以“页”为单位。__builtin___clear_cache的详细说明见这里。


验证

我们把main.c修改为:


#include
#include
#include
#include
#include
#include

#define PAGE_START(addr) ((addr)
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭