zoukankan      html  css  js  c++  java
  • 黑客级别的文章:把动态库的内存操作玩出了新花样!

    作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux

    关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。

    目录

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

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

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

    这个技术是爱奇艺开源的 xHookgithub 地址是: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 <stdlib.h>
    #include <stdio.h>
    
    void say_hello()
    {
        char *buf = malloc(1024);
        if(NULL != buf)
        {
            snprintf(buf, 1024, "%s", "hello
    ");
            printf("%s", buf);
        }
    }
    

    say_hello 的功能是在终端打印出hello 这6个字符(包括结尾的 )。

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

    源文件 main.c

    #include <test.h>
    
    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 malloccallocrealloc 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 文件头中包含了SHTPHT在当前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_LOADsegment都会被动态链接器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 <say_hello@@base>:
         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 <malloc@plt>
         f6c:   4604        mov r4, r0
         f6e:   b16c        cbz r4, f8c <say_hello@@base+0x2c>
         f70:   a507        add r5, pc, #28 ; (adr r5, f90 <say_hello@@base+0x30>)
         f72:   a308        add r3, pc, #32 ; (adr r3, f94 <say_hello@@base+0x34>)
         f74:   4620        mov r0, r4
         f76:   f44f 6180   mov.w   r1, #1024   ; 0x400
         f7a:   462a        mov r2, r5
         f7c:   f7ff ef30   blx de0 <snprintf@plt>
         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 <malloc@plt>:
     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

    于是:dd4 + 8 + 3000 + 1b4 = 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 <test.h>
    
    void *my_malloc(size_t size)
    {
        printf("%zu bytes memory are allocated by libtest.so
    ", 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.somaps中有3行记录。

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

    内存访问权限

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

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

    #include <sys mman.h="">
    
    int mprotect(void *addr, size_t len, int prot);
    

    注意修改内存访问权限时,只能以“页”为单位。

    mprotect 的详细说明见这里

    指令缓存

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

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

    方法是调用 __builtin___clear_cache

    void __builtin___clear_cache (char *begin, char *end);
    

    注意清除指令缓存时,也只能以“页”为单位。__builtin___clear_cache 的详细说明见这里

    验证

    我们把main.c修改为:

    #include <inttypes.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys mman.h="">
    #include <test.h>
    
    #define PAGE_START(addr) ((addr) & PAGE_MASK)
    #define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
    
    void *my_malloc(size_t size)
    {
        printf("%zu bytes memory are allocated by libtest.so
    ", size);
        return malloc(size);
    }
    
    void hook()
    {
        char       line[512];
        FILE      *fp;
        uintptr_t  base_addr = 0;
        uintptr_t  addr;
    
        //find base address of libtest.so
        if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
        while(fgets(line, sizeof(line), fp))
        {
            if(NULL != strstr(line, "libtest.so") &&
               sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
                break;
        }
        fclose(fp);
        if(0 == base_addr) return;
    
        //the absolute address
        addr = base_addr + 0x3f90;
        
        //add write permission
        mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
    
        //replace the function address
        *(void **)addr = my_malloc;
    
        //clear instruction cache
        __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
    }
    
    int main()
    {
        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"
    1024 bytes memory are allocated by libtest.so
    hello
    caikelun@debian:~$
    

    是的,成功了!

    我们并没有修改libtest.so的代码,甚至没有重新编译它。我们仅仅修改了main程序。

    libtest.so main的源码放在github上,可以从这里获取到。

    (根据你使用的编译器不同,或者编译器的版本不同,生成的libtest.so中,也许malloc对应的地址不再是 0x3f90,这时你需要先用readelf确认,然后再到main.c中修改。)

    使用 xhook

    当然,我们已经开源了一个叫xhook的工具库。

    使用 xhook,你可以更优雅的完成对libtest.sohook操作,也不必担心硬编码0x3f90导致的兼容性问题。

    #include <stdlib.h>
    #include <stdio.h>
    #include <test.h>
    #include <xhook.h>
    
    void *my_malloc(size_t size)
    {
        printf("%zu bytes memory are allocated by libtest.so
    ", size);
        return malloc(size);
    }
    
    int main()
    {
        xhook_register(".*/libtest\.so$", "malloc", my_malloc, NULL);
        xhook_refresh(0);
        
        say_hello();
        return 0;
    }
    

    xhook 支持 armeabi, armeabi-v7a arm64-v8a

    支持Android 4.0 (含)以上版本 (API level >= 14)。

    经过了产品级的稳定性和兼容性验证。可以在这里 获取 xhook

    总结一下xhook中执行PLT hook的流程:

    1. 读 maps,获取 ELF 的内存首地址(start address)。

    2. 验证 ELF 头信息。

    3. 从 PHT 中找到类型为 PT_LOAD 且 offset 为 0 的 segment。计算 ELF 基地址。

    4. 从 PHT 中找到类型为 PT_DYNAMIC 的 segment,从中获取到 .dynamic section,从 .dynamic section中获取其他各项 section 对应的内存地址。

    5. 在 .dynstr section 中找到需要 hook 的 symbol 对应的 index 值。

    6. 遍历所有的 .relxxx section(重定位 section),查找 symbol index 和 symbol type 都匹配的项,对于这项重定位项,执行 hook 操作。hook 流程如下:

    (1) 读 maps,确认当前 hook 地址的内存访问权限。

    (2) 如果权限不是可读也可写,则用 mprotect 修改访问权限为可读也可写。

    (3) 如果调用方需要,就保留 hook 地址当前的值,用于返回。

    (4) 将 hook 地址的值替换为新的值。(执行 hook)

    (5) 如果之前用 mprotect 修改过内存访问权限,现在还原到之前的权限。

    (6) 清除 hook 地址所在内存页的处理器指令缓存。

    FAQ

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

    可以。

    而且对于格式解析来说,读文件是最稳妥的方式,因为ELF在运行时,原理上有很多section不需要一直保留在内存中,可以在加载完之后就从内存中丢弃,这样可以节省少量的内存。

    但是从实践的角度出发,各种平台的动态链接器和加载器,都不会这么做,可能它们认为增加的复杂度得不偿失

    所以我们从内存中读取各种ELF信息就可以了,读文件反而增加了性能损耗。

    另外,某些系统库ELF文件,APP 也不一定有访问权限。

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

    正如你已经注意到的,前面介绍libtest.so基地址获取时,为了简化概念和编码方便,用了“绝大多数情况下”这种不应该出现的描述方式。

    对于hook来说,精确的基地址计算流程是:

    1. 在 maps 中找到找到 offset 为 0,且 pathname 为目标 ELF 的行。保存该行的 start address 为 p0。

    2. 找出 ELF 的 PHT 中第一个类型为 PT_LOAD 且 offset 为 0 的 segment,保存该 segment 的虚拟内存相对地址(p_vaddr)为 p1。

    3. p0 - p1 即为该 ELF 当前的基地址。

    绝大多数的ELF第一个PT_LOAD segmentp_vaddr都是 0

    另外,之所以要在maps里找offset0的行,是因为我们在执行hook之前,希望对内存中的ELF文件头进行校验,确保当前操作的是一个有效的 ELF,而这种 ELF 文件头只能出现在offset0mmap区域。

    可以在Android linker的源码中搜索“load_bias”,可以找到很多详细的注释说明,也可以参考linker对 load_bias_ 变量的赋值程序逻辑。

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

    会有一些影响。

    对于外部函数的调用,可以分为3种情况:

    1. 直接调用。无论编译选项如何,都可以被 hook 到。外部函数地址始终保存在 .got 中。

    2. 通过全局函数指针调用。无论编译选项如何,都可以被 hook 到。外部函数地址始终保存在 .data 中。

    3. 通过局部函数指针调用。如果编译选项为 -O2(默认值),调用将被优化为直接调用(同情况 1)。如果编译选项为 -O0,则在执行 hook 前已经被赋值到临时变量中的外部函数的指针,通过 PLT 方式无法 hook;对于执行 hook 之后才被赋值的,可以通过 PLT 方式 hook。

    一般情况下,产品级的ELF很少会使用-O0进行编译,所以也不必太纠结。

    但是如果你希望你的ELF尽量不被别人 PLT hook,那可以试试使用-O0来编译,然后尽量早的将外部函数的指针赋值给局部函数指针变量,之后一直使用这些局部函数指针来访问外部函数。

    总之,查看C/C++的源代码对这个问题的理解没有意义,需要查看使用不同的编译选项后,生成的ELF的反汇编输出,比较它们的区别,才能知道哪些情况由于什么原因导致无法被 PLT hook

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

    我们有时会遇到这样的问题:

    1. 读取 /proc/self/maps 后发现某个内存区域的访问权限为可读,当我们读取该区域的内容做 ELF 文件头校验时,发生了段错误(sig: SIGSEGV, code: SEGV_ACCERR)。

    2. 已经用 mprotect() 修改了某个内存区域的访问权限为可写,mprotect() 返回修改成功,然后再次读取 /proc/self/maps 确认对应内存区域的访问权限确实为可写,执行写入操作(替换函数指针,执行 hook)时发生段错误(sig: SIGSEGV, code: SEGV_ACCERR)。

    3. 读取和验证 ELF 文件头成功了,根据 ELF 头中的相对地址值,进一步读取 PHT 或者 .dynamic section 时发生段错误(sig: SIGSEGV, code: SEGV_ACCERR 或 SEGV_MAPERR)。

    可能的原因是

    1. 进程的内存空间是多线程共享的,我们在执行 hook 时,其他线程(甚至 linker)可能正在执行 dlclose(),或者正在用 mprotect() 修改这块内存区域的访问权限。

    2. 不同厂家、机型、版本的 Android ROM 可能有未公开的行为,比如在某些情况下对某些内存区域存在写保护或者读保护机制,而这些保护机制并不反应在 /proc/self/maps 的内容中。

    问题分析

    1. 读内存时发生段错误其实是无害的。

    2. 我在 hook 执行的流程中,需要直接通过计算内存地址的方式来写入数据的地方只有一处:即替换函数指针的最关键的那一行。只要其他地方的逻辑没有错误,这里就算写入失败了,也不会对其他内存区域造成破坏。

    3. 加载运行安卓平台的 APP 进程时,加载器已经向我们注入了 signal handler 的注册逻辑,以便 APP 崩溃时与系统的 debuggerd 守护进程通讯,debuggerd 使用 ptrace 调试崩溃进程,获取需要的崩溃现场信息,记录到 tombstone 文件中,然后 APP 自杀。

    4. 系统会精确的把段错误信号发送给“发生段错误的线程”。

    5. 我们希望能有一种隐秘的,且可控的方式来避免段错误引起 APP 崩溃。

    先明确一个观点:

    不要只从应用层程序开发的角度来看待段错误,段错误不是洪水猛兽,它只是内核与用户进程的一种正常的交流方式。

    当用户进程访问了无权限或未mmap的虚拟内存地址时,内核向用户进程发送SIGSEGV信号,来通知用户进程,仅此而已。

    只要段错误的发生位置是可控的,我们就可以在用户进程中处理它。

    解决方案

    1. 当 hook 逻辑进入我们认为的危险区域(直接计算内存地址进行读写)之前,通过一个全局 flag 来进行标记,离开危险区域后将 flag 复位。

    2. 注册我们自己的 signal handler,只捕获段错误。在 signal handler 中,通过判断 flag 的值,来判断当前线程逻辑是否在危险区域中。如果是,就用 siglongjmp 跳出 signal handler,直接跳到我们预先设置好的“危险区域以外的下一行代码处”;如果不是,就恢复之前加载器向我们注入的 signal handler,然后直接返回,这时系统会再次向我们的线程发送段错误信号,由于已经恢复了之前的 signal handler,这时会进入默认的系统 signal handler 中走正常逻辑。

    3. 我们把这种机制简称为:SFP (segmentation fault protection,段错误保护)

    4. 注意:SFP需要一个开关,让我们随时能够开启和关闭它。在 APP 开发调试阶段,SFP 应该始终被关闭,这样就不会错过由于编码失误导致的段错误,这些错误是应该被修复的;在正式上线后 SFP 应该被开启,这样能保证 APP 不会崩溃。(当然,以采样的形式部分关闭 SFP,用以观察和分析 hook 机制本身导致的崩溃,也是可以考虑的)

    具体代码可以参考xhook中的实现,在源码中搜索siglongjmpsigsetjmp

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

    我们这里介绍的hook方式为 PLT hook,不能做ELF内部函数之间调用的 hook

    道哥注解:

    外部函数是被记录到.plt这个section中的,因此可以在这个section中一步一步找到它的重定位地址,然后进行修改

    对于内部函数来说,比如一个使用static关键字修饰的函数,编译器在编译时,可能就直接把函数的地址“硬编码”在引用它的地方了。

    这也是为什么:如果一个函数只在文件内部使用,最好加上 static 关键字

    一个原因是安全,防止与其他文件中的符号重名,还有一个原因是加快启动速度,因为不需要重定位啊!

    inline hook 可以做到,你需要先知道想要hook的内部函数符号名(symbol name)或者地址,然后可以 hook

    有很多开源和非开源的inline hook实现,比如:

    substrate:http://www.cydiasubstrate.com/

    frida:https://www.frida.re/

    inline hook 方案强大的同时可能带来以下的问题

    1. 由于需要直接解析和修改 ELF 中的机器指令(汇编码),对于不同架构的处理器、处理器指令集、编译器优化选项、操作系统版本可能存在不同的兼容性和稳定性问题。

    2. 发生问题后可能难以分析和定位,一些知名的 inline hook 方案是闭源的。

    3. 实现起来相对复杂,难度也较大。

    4. 未知的坑相对较多,这个可以自行 google。

    建议如果PLT hook够用的话,就不必尝试inline hook了。


    ------ End ------

    文章来源于:https://my.oschina.net/nomagic/blog/1806011。

    我已经私信作者转载这篇文章,但是一直没有得到回复。鉴于文章写的太好了,先分享给大家。

    如果侵权,请私信我删文,谢谢!

    许可证
    Copyright (c) 2018, 爱奇艺, Inc. All rights reserved.

    本文使用 Creative Commons 许可证 授权。

    推荐阅读

    【1】《Linux 从头学》系列文章

    【2】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻

    【3】原来gdb的底层调试原理这么简单

    【4】内联汇编很可怕吗?看完这篇文章,终结它!

    其他系列专辑:精选文章应用程序设计物联网C语言

    星标公众号,第一时间看文章!

    </xhook.h></test.h></stdio.h></stdlib.h></test.h></stdio.h></stdlib.h></unistd.h></inttypes.h></test.h>/malloc@plt/snprintf@plt</say_hello@@base+0x34></say_hello@@base+0x30></say_hello@@base+0x2c>/malloc@plt</say_hello@@base></test.h></stdio.h></stdlib.h>

  • 相关阅读:
    ArcGIS Pro获得一个要素图层一种方法
    ArcGIS Pro layout clone
    ActiveMapViewChanged和选择变化
    ArcGIS Pro 改变栅格的数据源
    ArcGIS Pro自定义图标
    Windows Server 2016 路由和远程访问
    IIS应用程序池_缓存回收
    asp.net RSA密钥之C#格式与Java格式转换(PEM格式)
    MD5和Hash
    C# list与数组的转换
  • 原文地址:https://www.cnblogs.com/sewain/p/15497155.html
Copyright © 2011-2022 走看看