Based on Android's elf plt/GOT symbol redirection process and ELF Hook implementation, androidelf
The redirection process of elf plt/GOT based on Android and the implementation of ELF Hook -- by low-end code Nong 2014.10.27 Introduction
There are two main reasons for writing this article:
- First, I found that most of the articles on the Internet describing the PLT/GOT symbol redirection process are for x86. For example, Redirecting functions in shared ELF libraries writes very well. Although the process is very similar to that of ARM, commands vary greatly due to different CPU Systems;
- Second, most of the ELF file formats on the Internet are based on the Linking View, And the Link View parses ELF Based on the Section. However, during the loading process of the dynamic link library, linker only pays attention to Segment information in ELF. Therefore, the section information in ELF is completely tampered with or even deleted, and does not affect the linker loading process. This prevents static analysis tools (such as IDA and readelf) from analyzing it, generally, ELF files with shells are processed in this way. For such ELF files, if you want to implement the hook function, you must parse the Symbols Based on the Execution View;
Preparation
Before reading this article, make sure you have a rough understanding of the ELF File Format and ARM Assembly. For more information, see:
- ELF File Format analysis;
- ARM document;
Preparation tools:
- Readelf (NDK inclusion)
- Objdump (NDK included)
- IDA Pro 6.4 or above
- Android real machine or Simulator
Symbol redirection
On ARM, there are three common redirection types:R_ARM_JUMP_SLOT,R_ARM_ABS32AndR_ARM_GLOB_DATTo hook the elf function, you must handle the three types of redirection at the same time.
Example
First look at the sample code
typedef int (*strlen_fun)(const char *);strlen_fun global_strlen1 = (strlen_fun)strlen;strlen_fun global_strlen2 = (strlen_fun)strlen;#define SHOW(x) LOGI("%s is %d", #x, x)extern "C" jint Java_com_example_allhookinone_HookUtils_elfhook(JNIEnv *env, jobject thiz){ const char *str = "helloworld"; strlen_fun local_strlen1 = (strlen_fun)strlen; strlen_fun local_strlen2 = (strlen_fun)strlen; int len0 = global_strlen1(str); int len1 = global_strlen2(str); int len2 = local_strlen1(str); int len3 = local_strlen2(str); int len4 = strlen(str); int len5 = strlen(str); SHOW(len0); SHOW(len1); SHOW(len2); SHOW(len3); SHOW(len4); SHOW(len5); return 0;}
This Code calls strlen in three different ways: global function pointer, local function pointer, and direct call. In this example, we analyze the three call analyses respectively.
First, read the redirection table through readelf, as shown below:
Relocation section '.rel.dyn' at offset 0x2a48 contains 17 entries: Offset Info Type Sym.Value Sym. Name0000ade0 00000017 R_ARM_RELATIVE 0000af00 00000017 R_ARM_RELATIVE 0000af0c 00000017 R_ARM_RELATIVE 0000af10 00000017 R_ARM_RELATIVE 0000af18 00000017 R_ARM_RELATIVE 0000af1c 00000017 R_ARM_RELATIVE 0000af20 00000017 R_ARM_RELATIVE 0000af24 00000017 R_ARM_RELATIVE 0000af28 00000017 R_ARM_RELATIVE 0000af30 00000017 R_ARM_RELATIVE 0000aefc 00003215 R_ARM_GLOB_DAT 00000000 __stack_chk_guard0000af04 00003715 R_ARM_GLOB_DAT 00000000 __page_size0000af08 00004e15 R_ARM_GLOB_DAT 00000000 strlen0000b004 00004e02 R_ARM_ABS32 00000000 strlen0000b008 00004e02 R_ARM_ABS32 00000000 strlen0000af14 00006615 R_ARM_GLOB_DAT 00000000 __gnu_Unwind_Find_exid0000af2c 00007415 R_ARM_GLOB_DAT 00000000 __cxa_call_unexpected......Relocation section '.rel.plt' at offset 0x2ad0 contains 48 entries: Offset Info Type Sym.Value Sym. Name0000af40 00000216 R_ARM_JUMP_SLOT 00000000 __cxa_atexit0000af44 00000116 R_ARM_JUMP_SLOT 00000000 __cxa_finalize0000af48 00001716 R_ARM_JUMP_SLOT 00000000 memcpy...0000afd4 00004c16 R_ARM_JUMP_SLOT 00000000 fgets0000afd8 00004d16 R_ARM_JUMP_SLOT 00000000 fclose0000afdc 00004e16 R_ARM_JUMP_SLOT 00000000 strlen0000afe0 00004f16 R_ARM_JUMP_SLOT 00000000 strncmp......
In the section ". rel. plt" and ". rel. dyn", we found four strlen in total. We first record their key information, which will be useful for later analysis. They are
. Rel. dyn 116af08 R_ARM_GLOB_DAT
. Rel. dyn 116b004 R_ARM_ABS32.rel.dyn 116b008 R_ARM_ABS32.rel.plt 0000 AFDC R_ARM_JUMP_SLOT
In the code, we called strlen six times in total, but why only four times? In addition, how do they correspond to each other? analyze the assembly code with these problems. Drag the compiled so to IDA. We can see the command of the sample code:
.text:000050BC EXPORT Java_com_example_allhookinone_HookUtils_elfhook.text:000050BC Java_com_example_allhookinone_HookUtils_elfhook.text:000050BC.text:000050BC var_40 = -0x40.text:000050BC var_38 = -0x38.text:000050BC var_34 = -0x34.text:000050BC s = -0x2C.text:000050BC var_28 = -0x28.text:000050BC var_24 = -0x24.text:000050BC var_20 = -0x20.text:000050BC var_1C = -0x1C.text:000050BC var_18 = -0x18.text:000050BC var_14 = -0x14.text:000050BC var_10 = -0x10.text:000050BC var_C = -0xC.text:000050BC.text:000050BC PUSH {R4,LR}.text:000050BE SUB SP, SP, #0x38.text:000050C0 STR R0, [SP,#0x40+var_34].text:000050C2 STR R1, [SP,#0x40+var_38].text:000050C4 LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x50CA).text:000050C6 ADD R4, PC ; _GLOBAL_OFFSET_TABLE_.text:000050C8 LDR R3, =(aHelloworld - 0x50CE).text:000050CA ADD R3, PC ; "helloworld".text:000050CC STR R3, [SP,#0x40+s].text:000050CE LDR R3, =(strlen_ptr - 0xAF34).text:000050D0 LDR R3, [R4,R3] ; __imp_strlen.text:000050D2 STR R3, [SP,#0x40+var_28].text:000050D4 LDR R3, =(strlen_ptr - 0xAF34).text:000050D6 LDR R3, [R4,R3] ; __imp_strlen.text:000050D8 STR R3, [SP,#0x40+var_24].text:000050DA LDR R3, =(global_strlen1_ptr - 0xAF34).text:000050DC LDR R3, [R4,R3] ; global_strlen1.text:000050DE LDR R3, [R3].text:000050E0 LDR R2, [SP,#0x40+s].text:000050E2 MOVS R0, R2.text:000050E4 BLX R3.text:000050E6 MOVS R3, R0.text:000050E8 STR R3, [SP,#0x40+var_20].text:000050EA LDR R3, =(global_strlen2_ptr - 0xAF34).text:000050EC LDR R3, [R4,R3] ; global_strlen2.text:000050EE LDR R3, [R3].text:000050F0 LDR R2, [SP,#0x40+s].text:000050F2 MOVS R0, R2.text:000050F4 BLX R3.text:000050F6 MOVS R3, R0.text:000050F8 STR R3, [SP,#0x40+var_1C].text:000050FA LDR R2, [SP,#0x40+s].text:000050FC LDR R3, [SP,#0x40+var_28].text:000050FE MOVS R0, R2.text:00005100 BLX R3.text:00005102 MOVS R3, R0.text:00005104 STR R3, [SP,#0x40+var_18].text:00005106 LDR R2, [SP,#0x40+s].text:00005108 LDR R3, [SP,#0x40+var_24].text:0000510A MOVS R0, R2.text:0000510C BLX R3.text:0000510E MOVS R3, R0.text:00005110 STR R3, [SP,#0x40+var_14].text:00005112 LDR R3, [SP,#0x40+s].text:00005114 MOVS R0, R3 ; s.text:00005116 BLX strlen.text:0000511A MOVS R3, R0.text:0000511C STR R3, [SP,#0x40+var_10].text:0000511E LDR R3, [SP,#0x40+s].text:00005120 MOVS R0, R3 ; s.text:00005122 BLX strlen.text:00005126 MOVS R3, R0 ... ....text:000051CA ADD SP, SP, #0x38.text:000051CC POP {R4,PC}.text:000051CC ; End of function Java_com_example_allhookinone_HookUtils_elfhook
First, find out several important addresses. They are
- GLOBAL_OFFSET_TABLE: 0x0000AF34
- Strlen_ptr: 0x0000AF08
- _ Imp_strlen: 0x0000B0C8
- Global_strlen1_ptr: 0x0000AF0C
- Global_strlen1: 0x0000B004
- Global_strlen2_ptr: 0x0000AF10
- Global_strlen2: 0x0000B008
Global function pointer calls external Function
The call of global_strlen1 and global_strlen2 corresponds to the BLX commands at 0x000050E4 and 0x000050F4. After calculation, the final R3 values are * struct and * global_strlen2 respectively, while the values of global_strlen1 and global_strlen2 exactly correspond. rel. two R_ARM_ABS32 relocation items of dyn, So we come to the conclusion:The global function pointer is used to call an external function. Its relocation type is R_ARM_ABS32 and is located in the. rel. dyn section..
We only analyze the call process of global_strlen1. First, locate global_strlen1_ptr (0x0000AF0C). The address is located in the. got section,GLOBAL_OFFSET_TABLE. Then, use global_strlen1_ptr to locate 0x0000B004 (in the. data Section), and then use 0x0000B004 to locate the final function address,Therefore, the Offset of the R_ARM_ABS32 relocation item points to the address of the final called function address (that is, the pointer to the function pointer)The entire relocation process is first implemented in. got and then located in. date from. got. The following is the hexadecimal representation of the. got segment:
...0000AF0C 04 B0 00 00 08 B0 00 00 DC B0 00 00 B4 87 00 000000AF1C F4 84 00 00 60 5B 00 00 58 5B 00 00 50 5B 00 000000AF2C EC B0 00 00 FC 8C 00 00 00 00 00 00 00 00 00 00...0000B004 C8 B0 00 00 C8 B0 00 00 ?? ?? ?? ?? ?? ?? ?? ??0000B014 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??0000B024 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00...0000B0C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000000B0D8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00...
Finally, it is found that the instruction for 0x0000B0C8 address slices is all 0. When the link is dynamic, the linker will overwrite the value of 0x0000B004 address, pointing to the real address of strlen (instead of the current 0x0000B0C8, a little round ).
Partial function pointer calls external Function
The call of local_strlen1 and local_strlen2 corresponds to the BLX command at 0x00005100 and 0x0000510C. After calculation, the final R3 value is * strlen_prt, that is, 0x0000AF08. rel. r_ARM_GLOB_DAT relocation item in dyn, So we come to the conclusion:Call an external function by using a local function pointer. Its relocation type is R_ARM_GLOB_DAT and is located in the. re. dyn section..
We only analyze the call process of local_strlen1. First, locate strlen_prt (0x0000AF08), which is located in the. got section,GLOBAL_OFFSET_TABLEAnd then use strlen_prt to locate 0x0000B0C8, which is the same as the analysis result above,So R_ARM_GLOB_DAT's reset Offset points to the address of the final called function address (that is, the pointer to the function pointer)The following is the hexadecimal representation of the. got segment:
0000AF08 C8 B0 00 00 04 B0 00 00 08 B0 00 00 DC B0 00 000000AF18 B4 87 00 00 F4 84 00 00 60 5B 00 00 58 5B 00 000000AF28 50 5B 00 00 EC B0 00 00 FC 8C 00 00 00 00 00 00...0000B0C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000000B0D8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00...
Note that the 0x000050D8 command "STR R3, [SP, #0x40 + var_24]" has saved the real address of the function to the stack,Therefore, even if we modify the GOT table, the stack value will not be affected. Therefore, this type of relocation cannot be hook by modifying the address..
Directly call external functions
Finally, let's take a look at the direct call of strlen, which corresponds to the BLX commands at 0x00000000a and 0x00005122. Finally, they all point to the. plt section commands, as shown below:
.plt:00002E38 ADR R12, 0x2E40.plt:00002E3C ADD R12, R12, #0x8000.plt:00002E40 LDR PC, [R12,#(strlen_ptr_0 - 0xAE40)]! ; __imp_strlen...0000AFDC C8 B0 00 00 CC B0 00 00 D0 B0 00 00 D4 B0 00 00 0000AFEC D8 B0 00 00 DC B0 00 00 E0 B0 00 00 E4 B0 00 00 0000AFFC E8 B0 00 00 00 00 00 00 C8 B0 00 00 C8 B0 00 00 ...
Finally, the PC points to * strlen_ptr_0, that is, 0x0000AFDC, the address of strlen_ptr_0, which is located in the. got section, and 0x0000AFDC is exactly 0x0000B0C8. Therefore, the conclusion is as follows,Directly call an external function. Its relocation type is R_ARM_JUMP_SLOT, which is located in the. re. plt section. Its Offset points to the address of the final called function address (that is, the pointer to the function pointer). The whole process is first to. plt, then to. got, and finally to locate the real function address.
The analysis of this part shows that there are some differences between the decompilation results of IDA and objdump. The following is the Assembly command from objdump:
00002e38 <strlen@plt>: 2e38: e28fc600 add ip, pc, #0, 12 2e3c: e28cca08 add ip, ip, #8, 20 ; 0x8000 2e40: e5bcf19c ldr pc, [ip, #412]! ; 0x19c...... afd8: 00002c50 andeq r2, r0, r0, asr ip afdc: 00002c50 andeq r2, r0, r0, asr ip afe0: 00002c50 andeq r2, r0, r0, asr ip afe4: 00002c50 andeq r2, r0, r0, asr ip
See the address at afdc, pointing to 0x00002c50, and 0x00002c50 happens to be PLT [0]. The command is as follows:
00002c50 <__cxa_atexit@plt-0x14>: 2c50: e52de004 push {lr} ; (str lr, [sp, #-4]!) 2c54: e59fe004 ldr lr, [pc, #4] ; 2c60 <__cxa_atexit@plt-0x4> 2c58: e08fe00e add lr, pc, lr 2c5c: e5bef008 ldr pc, [lr, #8]! 2c60: 000082d4 ldrdeq r8, [r0], -r4
After executing the 2c5c command, the pc finally points to 0x0000af3c, Which is exactlyGLOBAL_OFFSET_TABLE+ 8, that is, GOT [2]. We can see at 0x0000af3c:
0000AF3C 00 00 00 00 28 B0 00 00 24 B0 00 00 2C B0 00 000000AF4C 30 B0 00 00 34 B0 00 00 38 B0 00 00 3C B0 00 00
The result shows that the function address pointed to in GOT [2] is actually 0, because the symbol binding on android does not support lazy binding, so when so is loaded, linker will find out all the functions corresponding to GOT [n] (n> = 2) in advance, so the code of GOT [2] will not be executed here, therefore, the complete PLT/GOT link process does not exist in the current Android system. I guess this is mainly due to stability considerations.
Summary
Although the commands decomcompiled by IDA and obudump are somewhat different in the PLT \ GOT process, this difference does not affect Android because lazy binding is not supported on Android. At the same time, we come to a very important conclusion:Although the relocation items of R_ARM_ABS32, R_ARM_GLOB_DAT, and R_ARM_JUMP_SLOT are different in code usage, their offset is a pointer to a function.This is very useful for the following elfhook.
Parsing ELF Based on execution View
The example provided in Redirecting functions in shared ELF libraries is to parse ELF Based on the Link View. Compared with parsing Based on the execution view, the following logic is basically the same. The key is to find it through segment. dynsym ,. dynstr ,. rel. plt and rel. dyn and the number of their items.
For the first time, locate the segment of the PT_DYNAMIC type through the Program Header Table. The content of this segment corresponds to. dynamic, and the content of this segment corresponds to the Elf32_Dyn array. Its structure is as follows:
/* Dynamic structure */typedef struct { Elf32_Sword d_tag; /* controls meaning of d_val */ union { Elf32_Word d_val; /* Multiple meanings - see d_tag */ Elf32_Addr d_ptr; /* program virtual address */ } d_un;} Elf32_Dyn;
By traversing this array, We can find all the required information. I will list their mappings:
- DT_HASH->. hash
- DT_SYMTAB & DT_SYMENT->. dynsym
- DT_STRTAB & DT_STRSZ->. dynstr
- PLTREL (determine whether REL or RELA) & (DT_REL | DT_RELA) & (DT_RELSZ | DT_RELASZ) & (DT_RELENT | DT_RELAENT)->. rel. dyn
- DT_JMPREL & DT_PLTRELSZ & (DT_RELENT | DT_RELAENT)->. rel. plt
- FINI_ARRAY & FINI_ARRAYSZ->. fini_array
- INIT_ARRAY & INIT_ARRAYSZ->. init_array
Here is the code for searching:
void getElfInfoBySegmentView(ElfInfo &info, const ElfHandle *handle){ info.handle = handle; info.elf_base = (uint8_t *) handle->base; info.ehdr = reinterpret_cast<Elf32_Ehdr *>(info.elf_base); // may be wrong info.shdr = reinterpret_cast<Elf32_Shdr *>(info.elf_base + info.ehdr->e_shoff); info.phdr = reinterpret_cast<Elf32_Phdr *>(info.elf_base + info.ehdr->e_phoff); info.shstr = NULL; Elf32_Phdr *dynamic = NULL; Elf32_Word size = 0; getSegmentInfo(info, PT_DYNAMIC, &dynamic, &size, &info.dyn); if(!dynamic){ LOGE("[-] could't find PT_DYNAMIC segment"); exit(-1); } info.dynsz = size / sizeof(Elf32_Dyn); Elf32_Dyn *dyn = info.dyn; for(int i=0; i<info.dynsz; i++, dyn++){ switch(dyn->d_tag){ case DT_SYMTAB: info.sym = reinterpret_cast<Elf32_Sym *>(info.elf_base + dyn->d_un.d_ptr); break; case DT_STRTAB: info.symstr = reinterpret_cast<const char *>(info.elf_base + dyn->d_un.d_ptr); break; case DT_REL: info.reldyn = reinterpret_cast<Elf32_Rel *>(info.elf_base + dyn->d_un.d_ptr); break; case DT_RELSZ: info.reldynsz = dyn->d_un.d_val / sizeof(Elf32_Rel); break; case DT_JMPREL: info.relplt = reinterpret_cast<Elf32_Rel *>(info.elf_base + dyn->d_un.d_ptr); break; case DT_PLTRELSZ: info.relpltsz = dyn->d_un.d_val / sizeof(Elf32_Rel); break; case DT_HASH: uint32_t *rawdata = reinterpret_cast<uint32_t *>(info.elf_base + dyn->d_un.d_ptr); info.nbucket = rawdata[0]; info.nchain = rawdata[1]; info.bucket = rawdata + 2; info.chain = info.bucket + info.nbucket; break; } } //because .dynsym is next to .dynstr, so we can caculate the symsz simply info.symsz = ((uint32_t)info.symstr - (uint32_t)info.sym)/sizeof(Elf32_Sym);}
However, there is a value that I cannot get through the PT_DYNAMIC segment, that is, the number of. dynsym items, and I finally get through a work ing method. Because. dynsym and. the two sections of dynstr are adjacent. Therefore, the two addresses are subtracted to obtain them. the total length of dynsym can be obtained in addition to sizeof (Elf32_Sym. the number of dynsym items. If you have a better method, please let me know.
ELF Hook
With the above introduction, it is very easy to write an ELF Hook. I will post the key code:
#define R_ARM_ABS32 0x02#define R_ARM_GLOB_DAT 0x15#define R_ARM_JUMP_SLOT 0x16int elfHook(const char *soname, const char *symbol, void *replace_func, void **old_func){ assert(old_func); assert(replace_func); assert(symbol); ElfHandle* handle = openElfBySoname(soname); ElfInfo info; getElfInfoBySegmentView(info, handle); Elf32_Sym *sym = NULL; int symidx = 0; findSymByName(info, symbol, &sym, &symidx); if(!sym){ LOGE("[-] Could not find symbol %s", symbol); goto fails; }else{ LOGI("[+] sym %p, symidx %d.", sym, symidx); } for (int i = 0; i < info.relpltsz; i++) { Elf32_Rel& rel = info.relplt[i]; if (ELF32_R_SYM(rel.r_info) == symidx && ELF32_R_TYPE(rel.r_info) == R_ARM_JUMP_SLOT) { void *addr = (void *) (info.elf_base + rel.r_offset); if (replaceFunc(addr, replace_func, old_func)) goto fails; //only once break; } } for (int i = 0; i < info.reldynsz; i++) { Elf32_Rel& rel = info.reldyn[i]; if (ELF32_R_SYM(rel.r_info) == symidx && (ELF32_R_TYPE(rel.r_info) == R_ARM_ABS32 || ELF32_R_TYPE(rel.r_info) == R_ARM_GLOB_DAT)) { void *addr = (void *) (info.elf_base + rel.r_offset); if (replaceFunc(addr, replace_func, old_func)) goto fails; } } fails: closeElfBySoname(handle); return 0;}
Finally, the test code is as follows:
typedef int (*strlen_fun)(const char *);strlen_fun old_strlen = NULL;size_t my_strlen(const char *str){ LOGI("strlen was called."); int len = old_strlen(str); return len * 2;}strlen_fun global_strlen1 = (strlen_fun)strlen;strlen_fun global_strlen2 = (strlen_fun)strlen;#define SHOW(x) LOGI("%s is %d", #x, x)extern "C" jint Java_com_example_allhookinone_HookUtils_elfhook(JNIEnv *env, jobject thiz){ const char *str = "helloworld"; strlen_fun local_strlen1 = (strlen_fun)strlen; strlen_fun local_strlen2 = (strlen_fun)strlen; int len0 = global_strlen1(str); int len1 = global_strlen2(str); int len2 = local_strlen1(str); int len3 = local_strlen2(str); int len4 = strlen(str); int len5 = strlen(str); LOGI("hook before:"); SHOW(len0); SHOW(len1); SHOW(len2); SHOW(len3); SHOW(len4); SHOW(len5); elfHook("libonehook.so", "strlen", (void *)my_strlen, (void **)&old_strlen); len0 = global_strlen1(str); len1 = global_strlen2(str); len2 = local_strlen1(str); len3 = local_strlen2(str); len4 = strlen(str); len5 = strlen(str); LOGI("hook after:"); SHOW(len0); SHOW(len1); SHOW(len2); SHOW(len3); SHOW(len4); SHOW(len5); return 0;}
From the printed results, we can see that local_strlen1 and local_strlen2 are not affected, but if the function is called again, it takes effect and the reason is not resolved. The test results will not be sent. Let's try it.
GitHup address
Complete code, see https://github.com/boyliang/AllHookInOne.git