當(dāng)前位置:首頁 > 公眾號精選 > IOT物聯(lián)網(wǎng)小鎮(zhèn)
[導(dǎo)讀]作?者:道哥,10年嵌入式開發(fā)老兵,專注于:C/C、嵌入式、Linux。關(guān)注下方公眾號,回復(fù)【書籍】,獲取Linux、嵌入式領(lǐng)域經(jīng)典書籍;回復(fù)【PDF】,獲取所有原創(chuàng)文章(PDF格式)。目錄理論與實踐開始新的動態(tài)庫面臨的問題怎么做?ELF概述ELF文件頭SHT(sectionhe...



目錄
  • 理論與實踐


  • 開始


    • 新的動態(tài)庫


    • 面臨的問題


    • 怎么做?


  • ELF


    • 概述


    • ELF 文件頭


    • SHT(section header table)


    • PHT(program header table)


    • 連接視圖和運行視圖


    • .dynamic section


    • 動態(tài)鏈接器(liker)


    • 追蹤


  • 內(nèi)存


    • 基地址


    • 內(nèi)存訪問權(quán)限


    • 指令緩存


    • 驗證


    • 使用 xhook


  • FAQ


    • 可以直接從文件中讀取 ELF 信息嗎?


    • 計算地址的精確方法是什么?


    • 目標 ELF 使用的編譯選項對 hook 有什么影響?


    • hook 時遇到偶發(fā)的段錯誤時什么原因?如何處理?


    • ELF 內(nèi)部函數(shù)之間的調(diào)用能 hook 嗎?


別人的經(jīng)驗,我們的階梯!


大家好,我是道哥,今天我為大伙兒解說的技術(shù)知識點是:【動態(tài)庫的內(nèi)存處理】


在上周的一篇轉(zhuǎn)載文章中,介紹了一種如何把一個動態(tài)庫中的調(diào)用函數(shù)進行“掉包”的技術(shù),從而達到一些特殊的目的。


這個技術(shù)是愛奇藝開源的 xHook,github地址是:https://github.com/iqiyi/xHook。


在官方文檔中,作者的描述場景是android系統(tǒng)。因為底層都是基于Linux的,因此這里介紹的hook技術(shù)也同樣適合其他Linux系統(tǒng)的工作環(huán)境。


這篇文章,我們就一起向大神學(xué)習(xí)一下,如何一步一步找到目標(被調(diào)用函數(shù)的地址),然后偷換成其他的函數(shù)地址。


文章的內(nèi)容比較長,但是絕對值得花半天的功夫、甚至幾天的時間來研究其中的知識點。


也許它不能立竿見影的提高你的編程技術(shù),但是對于內(nèi)功的修煉、提升,絕對是一等一的好資料!


在學(xué)習(xí)的過程中,我會在一些重要的地方,用橙色字體加上自己的學(xué)習(xí)心得,或者說理解。如果理解有誤,歡迎指出、一起討論。


為了便于閱讀,我在原文中比較關(guān)鍵的文字上,添加了字體顏色。


理論與實踐

關(guān)于動態(tài)庫的相關(guān)內(nèi)容,市面上質(zhì)量比較好的書籍可能就是:《程序員的自我修養(yǎng)-鏈接、裝載和庫》這本書了。


我手里的這一本,是 2019 年 6 月第 29 次印刷,足見這本書的生命力是多么的強悍!


黑客級別的文章:把動態(tài)庫的內(nèi)存操作玩出了新花樣!如果您讀過這本書,可能會有這樣的感受:書中的內(nèi)容理論性太強,即使自己明白了其中的道理,但是應(yīng)該如何實踐呢?或者說,能利用這些知識點來做什么呢?


愛奇藝的xHook,就是對這些理論知識的完美實踐!


《程序員的自我修養(yǎng)-鏈接、裝載和庫》是一本不可多得的好書,如果您對動態(tài)庫很感興趣,建議您入手一本紙質(zhì)書,支持一下作者!


如果只是想瀏覽一下,我這里有一個 PDF 版本(忘記從哪里下載的了),已經(jīng)放在網(wǎng)盤里。


如果您需要的話,在公眾號【IOT物聯(lián)網(wǎng)小鎮(zhèn)】的后臺留言:1031,即可獲取下載鏈接。


開始

新的動態(tài)庫

我們有一個新的動態(tài)庫: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個字符(包括結(jié)尾的\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中開始使用它吧!


遺憾的是,正如你可能已經(jīng)發(fā)現(xiàn)的,libtest.so存在嚴重的內(nèi)存泄露問題,每調(diào)用一次say_hello函數(shù),就會泄露1024字節(jié)的內(nèi)存。


新版APP上線后崩潰率開始上升,各種詭異的崩潰信息和報障信息跌撞而至。


面臨的問題

幸運的是,我們修復(fù)了libtest.so的問題??墒且院笤趺崔k呢?我們面臨2個問題:


  1. 當(dāng)測試覆蓋不足時,如何及時發(fā)現(xiàn)和準確定位線上 APP 的此類問題?


  2. 如果 libtest.so 是某些機型的系統(tǒng)庫,或者第三方的閉源庫,我們?nèi)绾涡迯?fù)它?如果監(jiān)控它的行為?


怎么做?

如果我們能對動態(tài)庫中的函數(shù)調(diào)用做hook(替換,攔截,竊聽,或者你覺得任何正確的描述方式),那就能夠做到很多我們想做的事情。


比如hook malloc,calloc,realloc和free,我們就能統(tǒng)計出各個動態(tài)庫分配了多少內(nèi)存,哪些內(nèi)存一直被占用沒有釋放。


這真的能做到嗎?答案是:hook我們自己的進程是完全可以的。


hook其他進程需要root權(quán)限(對于其他進程,沒有root權(quán)限就沒法修改它的內(nèi)存空間,也沒法注入代碼)。


幸運的是,我們只要hook自己就夠了。


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


進程級別的隔離,一般由操作系統(tǒng)來處理!


ELF

道哥注解:


關(guān)于 ELF 的詳細介紹,也可以看一下我之前寫的一篇文章:Linux系統(tǒng)中編譯、鏈接的基石-ELF文件:扒開它的層層外衣,從字節(jié)碼的粒度來探索。


這篇文章的內(nèi)容非常詳細,就像剝洋蔥一樣,一層一層分析 ELF 文件的結(jié)構(gòu)。


并且以圖片的方式,把 ELF 文件中的二進制內(nèi)容與相關(guān)的結(jié)構(gòu)體成員變量一一對應(yīng)起來,比較直觀。


概述

ELF(Executable and Linkable Format)是一種行業(yè)標準的二進制數(shù)據(jù)封裝格式,主要用于封裝可執(zhí)行文件、動態(tài)庫、object文件和core dumps文件。


使用google NDK對源代碼進行編譯和鏈接,生成的動態(tài)庫或可執(zhí)行文件都是ELF格式的。


用readelf可以查看ELF文件的基本信息,用objdump可以查看ELF文件的反匯編輸出。


ELF格式的概述可以參考這里,完整定義可以參考這里。


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


ELF 文件頭

ELF文件的起始處,有一個固定格式的定長的文件頭(32位架構(gòu)為52字節(jié),64位架構(gòu)為64字節(jié))。ELF文件頭以magic number 0x7F 0x45 0x4C 0x46開始(其中后3個字節(jié)分別對應(yīng)可見字符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在當(dāng)前ELF文件中的起始位置和長度。


例如,libtest.so的SHT起始位置為12744,長度40字節(jié);


PHT起始位置52,長度32字節(jié)。


SHT(section header table)

ELF以section為單位來組織和管理各種信息。


ELF使用SHT來記錄所有section的基本信息。


黑客級別的文章:把動態(tài)庫的內(nèi)存操作玩出了新花樣!主要包括:section的類型、在文件中的偏移量、大小、加載到內(nèi)存后的虛擬內(nèi)存相對地址、內(nèi)存中字節(jié)的對齊方式等。


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關(guān)系比較大的幾個section是:


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


dynsym:保存了符號(symbol)的信息(符號的類型、起始地址、大小、符號名稱在 .dynstr 中的索引編號等)。函數(shù)也是一種符號。


text:程序代碼經(jīng)過編譯后生成的機器指令。


dynamic:供動態(tài)鏈接器使用的各項信息,記錄了當(dāng)前 ELF 的外部依賴,以及其他各個重要 section 的起始位置等信息。


got:Global Offset Table。用于記錄外部調(diào)用的入口地址。動態(tài)鏈接器(linker)執(zhí)行重定位(relocate)操作時,這里會被填入真實的外部調(diào)用的絕對地址。


plt:Procedure Linkage Table。外部調(diào)用的跳板,主要用于支持 lazy binding 方式的外部調(diào)用重定位。(Android 目前只有 MIPS 架構(gòu)支持 lazy binding)


rel.plt:對外部函數(shù)直接調(diào)用的重定位信息。


rel.dyn:除 .rel.plt 以外的重定位信息。(比如通過全局函數(shù)指針來調(diào)用外部函數(shù))


道哥注解:


ELF 文件中,dynamic 這個section是非常重要的!


當(dāng)一個動態(tài)庫被加載到內(nèi)存中時,動態(tài)鏈接器就是讀取這個section的內(nèi)容,比如:


依賴于其他哪些共享對象;


動態(tài)鏈接符號表的位置(.dynsym);


動態(tài)鏈接重定位表的位置;


初始化代碼的位置;


...


使用指令:readelf -d xxx.so,即可查看一個動態(tài)庫中 .dynamic 的內(nèi)容。


另外,gotplt 這兩個 section,主要就是用來處理地址無關(guān)的功能。


如果您查詢-fPIC的相關(guān)內(nèi)容,一定會講解這兩個知識點。


總的來說就是:Linux 下的動態(tài)庫,把代碼段中地址有關(guān)的部分,通過“增加一層”的原理,全部變成“地址無關(guān)”的。


這樣的話,動態(tài)庫的代碼段在加載到物理內(nèi)存中之后,就可以被多個不同的進程來共享了,只要把代碼段的物理地址,映射到每個進程自己的虛擬地址即可。


而“地址相關(guān)”的部分,就放在 got(對變量的引用) 和plt(對函數(shù)的引用) 中。


PHT(program header table)

·ELF被加載到內(nèi)存時,是以segment為單位的。一個segment包含了一個或多個section`。


ELF使用PHT來記錄所有segment的基本信息。


主要包括:segment的類型、在文件中的偏移量、大小、加載到內(nèi)存后的虛擬內(nèi)存相對地址、內(nèi)存中字節(jié)的對齊方式等。


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都會被動態(tài)鏈接器(linker)映射(mmap)到內(nèi)存中。


連接視圖(Linking View)和執(zhí)行視圖(Execution View)

連接視圖:ELF 未被加載到內(nèi)存執(zhí)行前,以 section 為單位的數(shù)據(jù)組織形式。


執(zhí)行視圖:ELF 被加載到內(nèi)存后,以 segment 為單位的數(shù)據(jù)組織形式。


黑客級別的文章:把動態(tài)庫的內(nèi)存操作玩出了新花樣!黑客級別的文章:把動態(tài)庫的內(nèi)存操作玩出了新花樣!我們關(guān)心的hook操作,屬于動態(tài)形式的內(nèi)存操作,因此主要關(guān)心的是執(zhí)行視圖,即ELF被加載到內(nèi)存后,ELF中的數(shù)據(jù)是如何組織和存放的。


.dynamic section

這是一個十分重要和特殊的section,其中包含了ELF中其他各個section的內(nèi)存位置等信息。


在執(zhí)行視圖中,總是會存在一個類型為PT_DYNAMIC的segment,這個segment就包含了.dynamic section的內(nèi)容。


無論是執(zhí)行hook操作時,還是動態(tài)鏈接器執(zhí)行動態(tài)鏈接時,都需要通過PT_DYNAMIC segment來找到.dynamic section的內(nèi)存位置,再進一步讀取其他各項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

動態(tài)鏈接器(linker)

安卓中的動態(tài)鏈接器程序是linker。源碼在這里。


動態(tài)鏈接(比如執(zhí)行dlopen)的大致步驟是:


  1. 檢查已加載的 ELF 列表。(如果 libtest.so 已經(jīng)加載,就不再重復(fù)加載了,僅把 libtest.so 的引用計數(shù)加一,然后直接返回。)


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


  3. 逐個加載列表中的 ELF。加載步驟:


(1) 用 mmap 預(yù)留一塊足夠大的內(nèi)存,用于后續(xù)映射 ELF。(MAP_PRIVATE 方式)


(2) 讀 ELF 的 PHT,用 mmap 把所有類型為 PT_LOAD 的 segment 依次映射到內(nèi)存中。


(3) 從 .dynamic segment 中讀取各信息項,主要是各個 section 的虛擬內(nèi)存相對地址,然后計算并保存各個 section 的虛擬內(nèi)存絕對地址。


(4) 執(zhí)行重定位操作(relocate),這是最關(guān)鍵的一步。重定位信息可能存在于下面的一個或多個 secion 中:.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android。動態(tài)鏈接器需要逐個處理這些 .relxxx section 中的重定位訴求。根據(jù)已加載的 ELF 的信息,動態(tài)鏈接器查找所需符號的地址(比如 libtest.so 的符號 malloc),找到后,將地址值填入 .relxxx 中指明的目標地址中,這些“目標地址”一般存在于.got 或 .data 中。


(5) ELF 的引用計數(shù)加一。


  1. 逐個調(diào)用列表中 ELF 的構(gòu)造函數(shù)(constructor),這些構(gòu)造函數(shù)的地址是之前從 .dynamic segment 中讀取到的(類型為 DT_INIT 和 DT_INIT_ARRAY)。各 ELF 的構(gòu)造函數(shù)是按照依賴關(guān)系逐層調(diào)用的,先調(diào)用被依賴 ELF 的構(gòu)造函數(shù),最后調(diào)用 libtest.so 自己的構(gòu)造函數(shù)。(ELF 也可以定義自己的析構(gòu)函數(shù)(destructor),在 ELF 被 unload 的時候會被自動調(diào)用)
等一下!我們似乎發(fā)現(xiàn)了什么!再看一遍重定位操作(relocate)的部分。


難道我們只要從這些.relxxx中獲取到“目標地址”,然后在“目標地址”中重新填上一個新的函數(shù)地址,這樣就完成hook了嗎?也許吧。


追蹤

靜態(tài)分析驗證一下還是很容易的。以armeabi-v7a架構(gòu)的libtest.so為例。


先看一下say_hello函數(shù)對應(yīng)的匯編代碼吧。


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,對應(yīng)的匯編指令體積為60(10 進制)字節(jié)。


用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函數(shù)的調(diào)用對應(yīng)于指令blx dd4。跳轉(zhuǎn)到了地址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
...............
...............
果然,跳轉(zhuǎn)到了.plt中,經(jīng)過了幾次地址計算,最后跳轉(zhuǎn)到了地址3f90中的值指向的地址處,3f90是個函數(shù)指針。


稍微解釋一下:因為arm處理器使用3級流水線,所以第一條指令取到的pc的值是當(dāng)前執(zhí)行的指令地址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中,記錄了重定位表的信息,也就是哪些函數(shù)地址需要被重定位。


鏈接器把所有被依賴的共享對象加載到內(nèi)存中時,會把每個共享對象中的符號給匯總起來,得到全局符號表。


然后再檢查每個共享對象中的 .rel.plt,是否需要對一些地址進行重定位。


如果需要的話,就從全局符號表中找到該符號的內(nèi)存地址,然后填寫到 .plt 中對應(yīng)的位置。


還等什么,趕緊改代碼吧。我們的main.c應(yīng)該改成這樣:


#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 是個相對內(nèi)存地址,需要把它換算成絕對地址。


  2. 3f90 對應(yīng)的絕對地址很可能沒有寫入權(quán)限,直接對這個地址賦值會引起段錯誤。


  3. 新的函數(shù)地址即使賦值成功了,my_malloc 也不會被執(zhí)行,因為處理器有指令緩存(instruction cache)。


我們需要解決這些問題。


內(nèi)存

基地址

在進程的內(nèi)存空間中,各種ELF的加載地址是隨機的,只有在運行時才能拿到加載地址,也就是基地址。


道哥注解:


我們在查看一個動態(tài)鏈接庫時,看到的入口地址都是0x0000_0000。


動態(tài)庫在被加載到內(nèi)存中時,因為存在加載順序的問題,所以加載地址不是固定的


還有一種說法:對于某一個進程而言,它在被加載到內(nèi)存中時,它所依賴的所有動態(tài)庫的順序是一定的。


因此,每個動態(tài)庫的加載地址也是固定的,因此,理論上可以在第一次重定位之后,把重定位之后的代碼段存儲下來。


這樣,以后再次啟動這個進程時,就不需要重定位了,加快程序的啟動速度。


我們需要知道ELF的基地址,才能將相對地址換算成絕對地址


沒有錯,熟悉Linux開發(fā)的聰明的你一定知道,我們可以直接調(diào)用dl_iterate_phdr。詳細的定義見這里。


道哥注解:


dl_iterate_phdr 這個函數(shù)真的很有用,以回調(diào)函數(shù)的形式可到每一個動態(tài)鏈接庫的加載地址等信息。


如果沒有這個函數(shù),很多信息就需要從 /proc/xxx/maps 中來獲取,執(zhí)行速度慢,因為要處理很多字符串信息。


嗯,先等等,多年的Android開發(fā)被坑經(jīng)歷告訴我們,還是再看一眼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架構(gòu)的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返回的是指定進程的內(nèi)存空間中mmap的映射信息,包括各種動態(tài)庫、可執(zhí)行文件(如:linker),??臻g,堆空間,甚至還包括字體文件。


maps格式的詳細說明見這里。


我們的libtest.so在maps中有3行記錄。


offset為0的第一行的起始地址b6ec6000在絕大多數(shù)情況下就是我們尋找的基地址。


內(nèi)存訪問權(quán)限

maps返回的信息中已經(jīng)包含了權(quán)限訪問信息。


如果要執(zhí)行hook,就需要寫入的權(quán)限,可以使用mprotect來完成:


#include

int mprotect(void *addr, size_t len, int prot);
注意修改內(nèi)存訪問權(quán)限時,只能以“頁”為單位。


mprotect 的詳細說明見這里。


指令緩存

注意.got和.data的section類型是PROGBITS,也就是執(zhí)行代碼。處理器可能會對這部分數(shù)據(jù)做緩存。


修改內(nèi)存地址后,我們需要清除處理器的指令緩存,讓處理器重新從內(nèi)存中讀取這部分指令。


方法是調(diào)用__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)
本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉