基本原理 Linux在执行 动态链接的ELF时候,为了优化性能会使用一个 延迟绑定 的策略。
(延迟绑定:为了解决原本静态编译时要把各种系统API的具体实现代码都编译进ELF文件导致文件巨大臃肿的问题。)
当动态链接的ELF程序调用共享库的函数时,会去查找PLT表中的对应项目,PLT表在跳跃到GOT表中找到执行函数的实际地址,后续再调用的时候会直接去执行GOT表中对应的目标函数。(通过 _dl_runtime_solve()执行),
PLT Hook通过直接修改GOT表,使得在调用对应共享库的函数时跳转到用户自定义的Hook功能代码。
ELF
需要真正了解 PLT Hook的原理,需要从ELF开始,逐步的了解 linker(动态链接器)以及加载ELF文件的过程。
ELF格式
行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object和coew dumps文件。
so库就是ELF格式的文件,了解了ELF结构是PLT Hook的基础知识。
用 readelf 可以查看ELF文件的基本信息。
用 objdump 可以查看ELF文件的反汇编输出。
ELF文件头
固定格式的定长文件头(32位架构为 52字节,64位架构为 64字节)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 > aarch64-linux-android-readelf -h libbytehook.so ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: AArch64 Version: 0x1 Entry point address: 0x55ec Start of program headers: 64 (bytes into file) Start of section headers: 319720 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 34 Section header string table index: 32
固定头字符 7f 45 4c 46 (后三个字符对应 E L F)
ELF被加载到内存时,以segment
为单位,一个segment
包含一个或多个section
。
PHT则是用来记录所有 segment的基本信息。
主要包括如下信息:
segment类型
文件中的偏移量
大小
加载到内存后的虚拟内存相对地址
内存中字节对齐方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 aarch64-linux-android-readelf -l libbytehook.so Elf file type is DYN (Shared object file) Entry point 0x55ec There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000001f8 0x00000000000001f8 R 8 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x000000000000d7a0 0x000000000000d7a0 R E 4000 LOAD 0x000000000000d7a0 0x00000000000117a0 0x00000000000117a0 0x0000000000000638 0x0000000000000638 RW 4000 LOAD 0x000000000000ddd8 0x0000000000015dd8 0x0000000000015dd8 0x0000000000000054 0x00000000000004c8 RW 4000 DYNAMIC 0x000000000000d8c0 0x00000000000118c0 0x00000000000118c0 0x00000000000001e0 0x00000000000001e0 RW 8 GNU_RELRO 0x000000000000d7a0 0x00000000000117a0 0x00000000000117a0 0x0000000000000638 0x0000000000000860 R 1 GNU_EH_FRAME 0x0000000000002d70 0x0000000000002d70 0x0000000000002d70 0x0000000000000884 0x0000000000000884 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0 NOTE 0x0000000000000238 0x0000000000000238 0x0000000000000238 0x00000000000000bc 0x00000000000000bc R 4 Section to Segment mapping: Segment Sections... 00 01 .note.android.ident .note.gnu.build-id .dynsym .gnu.version .gnu.version_r .gnu.hash .hash .dynstr .rela.dyn .rela.plt .rodata .eh_frame_hdr .eh_frame .text .plt 02 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt 03 .data .bss 04 .dynamic 05 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt 06 .eh_frame_hdr 07 08 .note.android.ident .note.gnu.build-id
其中类型(Type)为 LOAD 的segment都会被 linker 通过mmap 映射到内存中。
ELF 以 section 为单位来组织和管理各种信息。使用 SHT 来记录所有section的基本信息。
主要包括:
section类型
文件中的偏移量
大小
加载内存后的虚拟内存相对地址
内存中的字节对齐方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 > aarch64-linux-android-readelf -S libbytehook.so There are 34 section headers, starting at offset 0x4e0e8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.android.ide NOTE 0000000000000238 00000238 0000000000000098 0000000000000000 A 0 0 4 [ 2] .note.gnu.build-i NOTE 00000000000002d0 000002d0 0000000000000024 0000000000000000 A 0 0 4 [ 3] .dynsym DYNSYM 00000000000002f8 000002f8 0000000000000900 0000000000000018 A 8 1 8 [ 4] .gnu.version VERSYM 0000000000000bf8 00000bf8 00000000000000c0 0000000000000002 A 3 0 2 [ 5] .gnu.version_r VERNEED 0000000000000cb8 00000cb8 0000000000000040 0000000000000000 A 8 2 4 [ 6] .gnu.hash GNU_HASH 0000000000000cf8 00000cf8 0000000000000094 0000000000000000 A 3 0 8 [ 7] .hash HASH 0000000000000d8c 00000d8c 0000000000000308 0000000000000004 A 3 0 4 [ 8] .dynstr STRTAB 0000000000001094 00001094 0000000000000598 0000000000000000 A 0 0 1 [ 9] .rela.dyn RELA 0000000000001630 00001630 00000000000004f8 0000000000000018 A 3 0 8 [10] .rela.plt RELA 0000000000001b28 00001b28 0000000000000828 0000000000000018 AI 3 21 8 [11] .rodata PROGBITS 0000000000002350 00002350 0000000000000a20 0000000000000000 AMS 0 0 8 [12] .eh_frame_hdr PROGBITS 0000000000002d70 00002d70 0000000000000884 0000000000000000 A 0 0 4 [13] .eh_frame PROGBITS 00000000000035f8 000035f8 0000000000001ff4 0000000000000000 A 0 0 8 [14] .text PROGBITS 00000000000055ec 000055ec 0000000000007c18 0000000000000000 AX 0 0 4 [15] .plt PROGBITS 000000000000d210 0000d210 0000000000000590 0000000000000000 AX 0 0 16 [16] .data.rel.ro PROGBITS 00000000000117a0 0000d7a0 00000000000000f8 0000000000000000 WA 0 0 8 [17] .fini_array FINI_ARRAY 0000000000011898 0000d898 0000000000000010 0000000000000000 WA 0 0 8 [18] .init_array INIT_ARRAY 00000000000118a8 0000d8a8 0000000000000018 0000000000000000 WA 0 0 8 [19] .dynamic DYNAMIC 00000000000118c0 0000d8c0 00000000000001e0 0000000000000010 WA 8 0 8 [20] .got PROGBITS 0000000000011aa0 0000daa0 0000000000000068 0000000000000000 WA 0 0 8 [21] .got.plt PROGBITS 0000000000011b08 0000db08 00000000000002d0 0000000000000000 WA 0 0 8 [22] .data PROGBITS 0000000000015dd8 0000ddd8 0000000000000054 0000000000000000 WA 0 0 8 [23] .bss NOBITS 0000000000015e30 0000de2c 0000000000000470 0000000000000000 WA 0 0 8 [24] .comment PROGBITS 0000000000000000 0000de2c 0000000000000125 0000000000000001 MS 0 0 1 [25] .debug_loc PROGBITS 0000000000000000 0000df51 000000000000f965 0000000000000000 0 0 1 [26] .debug_abbrev PROGBITS 0000000000000000 0001d8b6 000000000000332c 0000000000000000 0 0 1 [27] .debug_info PROGBITS 0000000000000000 00020be2 0000000000013a92 0000000000000000 0 0 1 [28] .debug_ranges PROGBITS 0000000000000000 00034674 0000000000001d80 0000000000000000 0 0 1 [29] .debug_str PROGBITS 0000000000000000 000363f4 0000000000005705 0000000000000001 MS 0 0 1 [30] .debug_line PROGBITS 0000000000000000 0003baf9 000000000000ac7d 0000000000000000 0 0 1 [31] .symtab SYMTAB 0000000000000000 00046778 0000000000005970 0000000000000018 33 859 8 [32] .shstrtab STRTAB 0000000000000000 0004c0e8 0000000000000157 0000000000000000 0 0 1 [33] .strtab STRTAB 0000000000000000 0004c23f 0000000000001ea8 0000000000000000 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), p (processor specific)
其中 starting at offset 0x4e0e8
对应 header中 Start of section headers: 319720 (bytes into file)
其中与 PLT Hook相关的Section如下:
.dynstr 保存所有字符串常量信息
.dynsym 保存符号(symbol)的信息。
符号的类型
符号起始地址
符号大小
符号名称在 .dynstr
中的索引编号
.text 代码经过编译后生成的二进制机器指令
.data 已初始化的非只读数据
.dynamic 专门为linker
设计的,记录了当前ELF的外部依赖,各个重要的section起始位置。
linker解析和加载ELF会用到的各项数据的索引信息。
.got (Global Offset Table) 用于记录外部调用的入口地址,当linker执行 relocate 之后,表里会记录真实的外部调用绝对地址。
.plt (Procedure Linkage Table) 外部调用的跳板,主要用于支持 Lazy binding 方式的外部调用重定位。(目前只有MIPS架构支持)
.plt 会从 .got
,.data
,.data.rel.ro
中查询符号的绝对地址,然后执行跳转。
连接视图
ELF 未被加载到内存执行前,以section为单位的数据组织形式
*执行视图
ELF 被加载到内存后,以 segment 为单位的数据组织形式
PLT Hook主要关心 执行视图 。
PLT Hook的执行时机是在 linker 使用 mmap 将ELF加载到内存中,再通过执行 relocation 把外部引用的绝对地址填入 GOT表和 DATA中。
* .dynamic section
专门为 linker
设计的,其中包含了 linker解析和加载ELF会用到的各项数据的索引信息。
对应 PHT中 type为 DYNAMIC
的segment,再通过这个 segment 找到 .dynamic session。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 aarch64-linux-android-readelf -d libbytehook.so Dynamic section at offset 0xd8c0 contains 30 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 0x0000000000000001 (NEEDED) Shared library: [libshadowhook.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x000000000000000e (SONAME) Library soname: [libbytehook.so] 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x0000000000000007 (RELA) 0x1630 0x0000000000000008 (RELASZ) 1272 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffff9 (RELACOUNT) 47 0x0000000000000017 (JMPREL) 0x1b28 0x0000000000000002 (PLTRELSZ) 2088 (bytes) 0x0000000000000003 (PLTGOT) 0x11b08 0x0000000000000014 (PLTREL) RELA 0x0000000000000006 (SYMTAB) 0x2f8 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000005 (STRTAB) 0x1094 0x000000000000000a (STRSZ) 1432 (bytes) 0x000000006ffffef5 (GNU_HASH) 0xcf8 0x0000000000000004 (HASH) 0xd8c 0x0000000000000019 (INIT_ARRAY) 0x118a8 0x000000000000001b (INIT_ARRAYSZ) 24 (bytes) 0x000000000000001a (FINI_ARRAY) 0x11898 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 0x000000006ffffff0 (VERSYM) 0xbf8 0x000000006ffffffe (VERNEED) 0xcb8 0x000000006fffffff (VERNEEDNUM) 2 0x0000000000000000 (NULL) 0x0
参考文献 ELF概述
ELF完整结构
linker(动态链接器)
linker源码
大致步骤 动态链接(dlopen) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void * do_dlopen(const char * name, int flags, const android_dlextinfo* extinfo, const void * caller_addr) { ... // load ELF soinfo* si = find_library(ns, translated_name, flags, extinfo, caller); loading_trace.End(); if (si != nullptr ) { void * handle = si->to_handle(); LD_LOG(kLogDlopen, "... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p", si->get_realpath(), si->get_soname(), handle); // 调用构造函数 si->call_constructors(); failure_guard.Disable(); LD_LOG(kLogDlopen, "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p", si->get_realpath(), si->get_soname(), handle); return handle; } return nullptr ; }
加载ELF 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static soinfo* find_library(android_namespace_t* ns, const char * name, int rtld_flags, const android_dlextinfo* extinfo, soinfo* needed_by) { soinfo* si = nullptr ; if (name == nullptr ) { si = solist_get_somain(); } else if (!find_libraries(ns, needed_by, &name, 1, &si, nullptr , 0, rtld_flags, extinfo, false /* add_as_children */)) { if (si != nullptr ) { soinfo_unload(si); } return nullptr ; } si->increment_ref_count(); return si; }bool find_libraries(android_namespace_t* ns, soinfo* start_with, const char * const library_names[], size_t library_names_count, soinfo* soinfos[], std ::vector <soinfo*>* ld_preloads, size_t ld_preloads_count, int rtld_flags, const android_dlextinfo* extinfo, bool add_as_children, std ::vector <android_namespace_t*>* namespaces) { ... }
prelink_image
加载ELF中的 .dynamic section 从里面读取外部以来的ELF列表信息 从PHT读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool soinfo::prelink_image(bool dlext_use_relro) { if (flags_ & FLAG_PRELINKED) return true ; /* Extract dynamic section */ ElfW(Word) dynamic_flags = 0; phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags); }void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count, ElfW(Addr) load_bias, ElfW(Dyn)** dynamic, ElfW(Word)* dynamic_flags) { *dynamic = nullptr ; for (size_t i = 0; i<phdr_count; ++i) { const ElfW(Phdr)& phdr = phdr_table[i]; if (phdr.p_type == PT_DYNAMIC) { *dynamic = reinterpret_cast <ElfW(Dyn)*>(load_bias + phdr.p_vaddr); if (dynamic_flags) { *dynamic_flags = phdr.p_flags; } return ; } } }
link_image
执行 relocation 操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool soinfo::link_image(const SymbolLookupList& lookup_list, soinfo* local_group_root, const android_dlextinfo* extinfo, size_t* relro_fd_offset) { if (this != solist_get_vdso() && !relocate(lookup_list)) { return false ; } ... ++g_module_load_counter; notify_gdb_of_load(this ); set_image_linked(); return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 // bionic/linker/linker_relocate.cppbool soinfo::relocate(const SymbolLookupList& lookup_list) { ... if (relr_ != nullptr && !is_linker()) { LD_DEBUG(reloc, "[ relocating %s relr ]", get_realpath()); const ElfW(Relr)* begin = relr_; const ElfW(Relr)* end = relr_ + relr_count_; if (!relocate_relr(begin, end, load_bias, should_tag_memtag_globals())) { return false ; } } ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // Process relocations in SHT_RELR section (experimental). // Details of the encoding are described in this post: // https://groups.google.com/d/msg/generic-abi/bX460iggiKg/Pi9aSwwABgAJbool relocate_relr(const ElfW(Relr) * begin, const ElfW(Relr) * end, ElfW(Addr) load_bias, bool has_memtag_globals) { constexpr size_t wordsize = sizeof (ElfW(Addr)); ElfW(Addr) base = 0; for (const ElfW(Relr)* current = begin; current < end; ++current) { ElfW(Relr) entry = *current; ElfW(Addr) offset; if ((entry&1) == 0) { // Even entry: encodes the offset for next relocation. offset = static_cast <ElfW(Addr)>(entry); apply_relr_reloc(offset, load_bias, has_memtag_globals); // Set base offset for subsequent bitmap entries. base = offset + wordsize; continue ; } // Odd entry: encodes bitmap for relocations starting at base. offset = base; while (entry != 0) { entry >>= 1; if ((entry&1) != 0) { apply_relr_reloc(offset, load_bias, has_memtag_globals); } offset += wordsize; } // Advance base offset by 63 words for 64-bit platforms, // or 31 words for 32-bit platforms. base += (8*wordsize - 1) * wordsize; } return true ; }
执行重定位操作,这是最关键一步。
目的是为当前加载的ELF的每个导入符号
找到对应的外部符号的绝对地址并写入到对应的位置里。
查询绝对地址
.rela.plt
,.rel.plt
:用于关联.got.plt
和.dynsym
,就是PLT表
.rela.dyn
,.rel.dyn
:关联.data
,.data.rel.ro
,.dynsym
写入位置
.got.plt
:保存外部函数的绝对地址,就是GOT表
.data
,.data.rel.ro
:保存外部数据(函数指针)的绝对地址
.rela.*
只在64位架构做了实现,相比.rel
多了r_addend
字段。
PLT
:Procedure Linkage Table - 过程链接表
GOT
:Global Offset Table - 全局偏移表
Hook原理 通过符号名,先在hash table中找到对应的符号信息(.dynsym
)中,再从PLT表
找到符号对应的信息,再从GOT表
中找到绝对地址信息。通过修改GOT表中的绝对地址值,替换为Hook函数的地址。
那么,当程序通过PLT
调用目标函数时,由于GOT
中的地址被修改为Hook函数,就会跳转到Hook函数执行,从而实现了Hook。
PLT Hook实践 以下是基于 三个自己编译的so进行的处理,代码逻辑相对简单。只为了熟悉基础的hook流程。如果需要更稳定的hook,还是推荐使用 相关三方库里的。
combined.cpp(待Hook类) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // // Created by wxy on 2025/10/5. // #include <jni.h> #include <android/log .h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "PLT_COMBINED", __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "PLT_COMBINED", __VA_ARGS__) // ---------------------------- // target_function(待 hook) // ----------------------------extern "C" __attribute__((noinline)) int target_function(int x) { LOGI("target_function called with %d", x); return x + 1; } // ---------------------------- // callTarget 调用 target_function // ----------------------------extern "C" JNIEXPORT jint JNICALL Java_com_example_plthookdemo_MainActivity_callTarget(JNIEnv* env, jobject thiz, jint v) { LOGI("callTarget: before calling target_function"); int r = target_function((int )v); // 通过 PLT 调用 LOGI("callTarget: after calling target_function, result=%d", r); return r; }
plthook.cpp(PLTHook类) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 #define _GNU_SOURCE #include <dlfcn.h> // dlopen/dlsym/dl_iterate_phdr #include <link.h> // dl_phdr_info, Elf headers #include <elf.h> // ELF 结构定义 #include <pthread.h> // pthread_create #include <unistd.h> // sleep, sysconf #include <sys/mman.h> // mprotect #include <android/log .h> // Android 日志打印 #include <string .h> #include <errno.h> #include <stdint.h> // ----------------------------- // 日志宏定义 // ----------------------------- #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "PLT_HOOK", __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "PLT_HOOK", __VA_ARGS__) // ----------------------------- // 目标库与函数名 // ----------------------------- // 我们的目标是 hook libcombined.so 里的 target_functionstatic const char COMBINED_SO[] = "libcombined.so";static const char TARGET_SYM[] = "target_function"; // ----------------------------- // 原始函数指针 // -----------------------------typedef int (*target_fn_t)(int );static target_fn_t original_target_fn = nullptr ; // ----------------------------- // Hook 替换后的函数实现 // -----------------------------extern "C" int hooked_target_function(int x) { LOGI("[HOOK] hooked_target_function called, arg=%d", x); // 修改输入参数 int new_arg = x + 200; int ret = 0; // 调用原始函数 if (original_target_fn) ret = original_target_fn(new_arg); else LOGE("[HOOK] original_target_fn null!"); LOGI("[HOOK] return %d", ret); return ret; } // ============================================================================ // 下面部分是 Hook 实现逻辑:遍历 ELF 段,定位 GOT 表,修改函数指针 // ============================================================================ // ----------------------------- // 将 GOT 所在内存页设置为可写 // -----------------------------static bool make_writable(void * addr, size_t len) { long page_size = sysconf(_SC_PAGESIZE); // 通常是 4096 bytes // 页对齐:mprotect 只能按页修改权限 uintptr_t start = (uintptr_t)addr & ~(page_size - 1); uintptr_t end = ((uintptr_t)addr + len + page_size - 1) & ~(page_size - 1); size_t size = end - start; // 将 GOT 页改为读写 if (mprotect((void *)start, size, PROT_READ | PROT_WRITE) != 0) { LOGE("mprotect failed: %s addr=%p size=%zu", strerror(errno), addr, size); return false ; } LOGI("mprotect success: addr=%p size=%zu", addr, size); return true ; } // ----------------------------- // 解析 64 位 ELF 动态段信息并修改 GOT // -----------------------------static bool process_relocations_64(uintptr_t base, const Elf64_Phdr* phdr, int phnum) { // 找到 PT_DYNAMIC 段(保存重定位表、符号表等) const Elf64_Dyn* dyn = nullptr ; for (int i = 0; i < phnum; i++) { if (phdr[i].p_type == PT_DYNAMIC) dyn = (const Elf64_Dyn*)(base + phdr[i].p_vaddr); } if (!dyn) return false ; // 动态段中保存的关键表指针 const char * strtab = nullptr ; // 字符串表 const Elf64_Sym* symtab = nullptr ; // 符号表 Elf64_Rela* rela = nullptr ; // PLT 重定位表 size_t rela_size = 0; // 重定位表大小 // 从动态段中提取各个表的地址 for (const Elf64_Dyn* d = dyn; d->d_tag != DT_NULL; d++) { switch (d->d_tag) { case DT_STRTAB: strtab = (const char *)(base + d->d_un.d_ptr); break ; case DT_SYMTAB: symtab = (const Elf64_Sym*)(base + d->d_un.d_ptr); break ; case DT_JMPREL: rela = (Elf64_Rela*)(base + d->d_un.d_ptr); break ; case DT_PLTRELSZ: rela_size = d->d_un.d_val; break ; default : break ; } } // 确保表都存在 if (!strtab || !symtab || !rela || !rela_size) { LOGE("dyn info missing"); return false ; } // 计算重定位项数量 size_t n = rela_size / sizeof (Elf64_Rela); LOGI("Scanning %zu PLT relocations", n); // 遍历所有 .rela.plt 表项 for (size_t i = 0; i < n; i++) { Elf64_Rela* r = &rela[i]; size_t symidx = ELF64_R_SYM(r->r_info); // 符号索引 const char * name = strtab + symtab[symidx].st_name; // 符号名 // 找到我们想 hook 的目标函数 if (name && strcmp (name, TARGET_SYM) == 0) { uintptr_t got_addr = base + r->r_offset; // GOT 条目地址 LOGI("Found symbol %s -> GOT %p", name, (void *)got_addr); // GOT 表项里保存的是原始函数地址 target_fn_t* got_entry = (target_fn_t*)got_addr; original_target_fn = *got_entry; LOGI("Original fn=%p", (void *)original_target_fn); // 修改 GOT 表项前需将该页改为可写 if (!make_writable(got_entry, sizeof (void *))) return false ; // 写入我们自己的函数地址 *got_entry = (target_fn_t)&hooked_target_function; LOGI("Patched GOT to %p", (void *)hooked_target_function); return true ; } } return false ; } // ----------------------------- // dl_iterate_phdr 的回调函数 // 每个已加载的 so 都会调用一次 // -----------------------------static int phdr_cb(struct dl_phdr_info* info, size_t sz, void *) { (void )sz; if (!info->dlpi_name || !info->dlpi_name[0]) return 0; // 提取 so 名称(去掉路径) const char * name = strrchr (info->dlpi_name, '/'); if (name) name++; else name = info->dlpi_name; LOGI("Inspected: %s", name); // 匹配我们目标 so(libcombined.so) if (strcmp (name, COMBINED_SO) == 0) { LOGI("Matched combined: %s", info->dlpi_name); // 找到目标 so 的 ELF 头信息并处理其 PLT 重定位 process_relocations_64( (uintptr_t)info->dlpi_addr, reinterpret_cast <const Elf64_Phdr*>(info->dlpi_phdr), info->dlpi_phnum ); } return 0; // 继续遍历 } // ----------------------------- // 安装 hook(核心流程) // -----------------------------static void install_hook() { LOGI("Installing PLT hook..."); // 遍历进程中已加载的 ELF(so),执行 phdr_cb dl_iterate_phdr(phdr_cb, nullptr ); LOGI("PLT hook installed"); } // ----------------------------- // 异步延迟安装 hook(避免目标 so 尚未加载) // -----------------------------static void * delayed(void *) { sleep(1); // 等待 1 秒,确保 libcombined.so 已加载 install_hook(); return nullptr ; } // ----------------------------- // 构造函数:so 加载时自动执行 // ----------------------------- __attribute__((constructor))static void onload() { // 创建一个后台线程延迟执行 hook pthread_t t; pthread_create(&t, nullptr , delayed, nullptr ); pthread_detach(t); LOGI("plthook constructor executed"); }
dl_iterate_phdr
遍历当前进程中加载的所有ELF模块(.so / 主程序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 // external/cronet/tot/third_party/llvm-libc/src/include/llvm-libc-types/struct_dl_phdr_info.h struct dl_phdr_info { ElfW(Addr) dlpi_addr; // so加载基地址 const char *dlpi_name; // so名称 const ElfW(Phdr) * dlpi_phdr; // 对应ELF的 PHT ElfW(Half) dlpi_phnum; // PH的数量 }; // bionic/libc/bionic/dl_iterate_phdr_static.cppint dl_iterate_phdr(int (*cb)(struct dl_phdr_info* info, size_t size, void * data), void * data) { ElfW(Ehdr)* ehdr = reinterpret_cast <ElfW(Ehdr)*>(&__executable_start); if (memcmp (ehdr->e_ident, ELFMAG, SELFMAG) != 0) { return -1; } // Dynamic binaries get their dl_iterate_phdr from the dynamic linker, but // static binaries get this . We don't have a list of shared objects to // iterate over, since there's really only a single monolithic blob of // code/data, plus optionally a VDSO. struct dl_phdr_info exe_info; exe_info.dlpi_addr = 0; exe_info.dlpi_name = NULL ; exe_info.dlpi_phdr = reinterpret_cast <ElfW(Phdr)*>(reinterpret_cast <uintptr_t>(ehdr) + ehdr->e_phoff); exe_info.dlpi_phnum = ehdr->e_phnum; exe_info.dlpi_adds = 0; exe_info.dlpi_subs = 0; const TlsModules& tls_modules = __libc_shared_globals()->tls_modules; if (tls_modules.module_count == 0) { exe_info.dlpi_tls_modid = 0; exe_info.dlpi_tls_data = nullptr ; } else { const size_t kExeModuleId = 1; const StaticTlsLayout& layout = __libc_shared_globals()->static_tls_layout; const TlsModule& tls_module = tls_modules.module_table[__tls_module_id_to_idx(kExeModuleId)]; char * static_tls = reinterpret_cast <char *>(__get_bionic_tcb()) - layout.offset_bionic_tcb(); exe_info.dlpi_tls_modid = kExeModuleId; exe_info.dlpi_tls_data = static_tls + tls_module.static_offset; } // Try the executable first. int rc = cb(&exe_info, sizeof (exe_info), data); if (rc != 0) { return rc; } // Try the VDSO if that didn't work. ElfW(Ehdr)* ehdr_vdso = reinterpret_cast <ElfW(Ehdr)*>(getauxval(AT_SYSINFO_EHDR)); if (ehdr_vdso == nullptr ) { // There is no VDSO, so there's nowhere left to look. return rc; } struct dl_phdr_info vdso_info; vdso_info.dlpi_addr = 0; vdso_info.dlpi_name = NULL ; vdso_info.dlpi_phdr = reinterpret_cast <ElfW(Phdr)*>(reinterpret_cast <char *>(ehdr_vdso) + ehdr_vdso->e_phoff); vdso_info.dlpi_phnum = ehdr_vdso->e_phnum; vdso_info.dlpi_adds = 0; vdso_info.dlpi_subs = 0; vdso_info.dlpi_tls_modid = 0; vdso_info.dlpi_tls_data = nullptr ; for (size_t i = 0; i < vdso_info.dlpi_phnum; ++i) { if (vdso_info.dlpi_phdr[i].p_type == PT_LOAD) { vdso_info.dlpi_addr = (ElfW(Addr)) ehdr_vdso - vdso_info.dlpi_phdr[i].p_vaddr; break ; } } return cb(&vdso_info, sizeof (vdso_info), data); }
CMakeLists.txt(SO编译配置) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cmake_minimum_required (VERSION 3.10)project ("plthook_combined")set (CMAKE_CXX_STANDARD 17)set (CMAKE_CXX_STANDARD_REQUIRED ON ) # 生成 libcombined.soadd_library (combined SHARED combined.cpp) # hook 库add_library (plthook SHARED plthook.cpp) # 链接库target_link_libraries (combined android log)target_link_libraries (plthook android log dl)
MainActivity(调用SO) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.example.plthookdemoimport android.app.Activityimport android.os.Bundleimport android.util.Logimport androidx.appcompat.app.AppCompatActivityimport com.example.plthookdemo.databinding.ActivityMainBindingclass MainActivity : AppCompatActivity() { // native wrapper that calls into libcaller -> target_function external fun callTarget(v: Int ): Int private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) Log.i("PLT_TEST", "Calling callTarget(5) ...") val r = callTarget(5) Log.i("PLT_TEST", "callTarget returned: " + r) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.sampleText.text = "123" binding.sampleText.setOnClickListener { // testHook() val r = callTarget(5) Log.i("PLT_TEST11", "callTarget returned: " + r) } } companion object { init { // 加载顺序很重要:先加载 target + caller,然后加载 plthook System.loadLibrary("combined"); System.loadLibrary("plthook"); } } }
相关三方库 bytedance/bhook
参考地址 字节跳动开源 Android PLT hook 方案 bhook