##0x01 ELF文件介绍
APK里的.so文件即ELF文件(Executable and Linking Format)最初是由 UNIX 系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。工具接口标准(Tool Interface Standards,TIS)委员会将还在发展的 ELF 标准选作为一种可移植的目标文件格式,可以在 32 位 Intel 体系结构上的很多操作系统中使用。
目标文件有三种类型:
- 可重定位文件(Relocatable File) (*.o) 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File) (*.exe) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。
- 共享目标文件(Shared Object File) (*.so)包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,
生成另外一个目标文件(比如:编译器和连接器 把*.o和*.so一起装配成一个*.exe文件)。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像(比如:动态加载器把exe程序和*.so加载进内存执行)。
目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
##0x02ELF文件头介绍
ELF头的各个字段如下:
1. #define EI_NIDENT 16
2. typedef struct{
3. unsigned char e_ident[EI_NIDENT]; //目标文件标识信息
4. Elf32_Half e_type; //目标文件类型
5. Elf32_Half e_machine; //目标体系结构类型
6. Elf32_Word e_version; //目标文件版本
7. Elf32_Addr e_entry; //程序入口的虚拟地址,若没有,可为0
8. Elf32_Off e_phoff; //程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0
9. Elf32_Off e_shoff; //节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0
10. Elf32_Word e_flags; //保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。
11. Elf32_Half e_ehsize; //ELF 头部的大小(以字节计算)。
12. Elf32_Half e_phentsize; //程序头部表格的表项大小(按字节计算)。
13. Elf32_Half e_phnum; //程序头部表格的表项数目。可以为 0。
14. Elf32_Half e_shentsize; //节区头部表格的表项大小(按字节计算)。
15. Elf32_Half e_shnum; //节区头部表格的表项数目。可以为 0。
16. Elf32_Half e_shstrndx; //节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。
17. }Elf32_Ehdr;
关于头部我们要记住的有这几点,就可以根据其中部分条件找另外的值了:
e_phoff = sizeof(e_ehsize);
整个ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1
通常情况下:e_shstrndx = e_shnum – 1
e_shstrndx字段的值跟strip有关。Strip之前:.shstrtab 并不是最后一个section.则 e_shstrndx = e_shnum – 1 – 2;
而经过strip之后,动态链接库末尾的.symtab和.strtab这两个section会被去掉. 则e_shstrndx = e_shnum – 1。
####使用ndk生成在\libs\ armeabi\下的.so文件是经过strip的,也是被打包到apk中的。
tips:但是如果e_shoff和e_shnum都改成任意值,那么修正起来比较麻烦。
但貌似e_shoff、e_shnum等与section相关的信息任意修改,对.so文件的使用毫无影响。
能找到的一句如下:
1.elf如何装载
2.linker如何链接
基于上面的结论,再来分析下ELF头的字段。
- e_ident[EI_NIDENT] 字段包含魔数、字节序、字长和版本,后面填充0。对于安卓的linker,通过verify_elf_object函数检验魔数,判定是否为.so文件。那么,我们可以向位置写入数据,至少可以向后面的0填充位置写入数据。遗憾的是,我在fedora 14下测试,是不能向0填充位置写数据,链接器报非0填充错误。
- 对于安卓的linker,对e_type、e_machine、e_version和e_flags字段并不关心,是可以修改成其他数据的(仅分析,没有实测)
- 对于动态链接库,e_entry 入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
- so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。
- 既然so装载与装载视图紧密相关,自然e_phoff、e_phentsize和e_phnum这些字段是不能动的。
##0x03ELF文件加固
###3.1有源码加固 基于特定section的加解密实现
基于section的加解密,是指将so文件的特定section进行加密,so文件被加载时解密。下面给出实例。
假设有一个shelldemo应用,调用一个native方法返回一个字符串供UI显示。在native方法中,又调用getString方法返回一个字符串供native方法返回。我需要将getString方法加密。这里,将getString方法存放在.mytext中(指定__attribute__((section (".mytext")))?,即是需要对.mytext进行加密。
加密流程:
- 从so文件头读取section偏移shoff、shnum和shstrtab
- 读取shstrtab中的字符串,存放在str空间中
- 从shoff位置开始读取section header, 存放在shdr
- 通过shdr -> sh_name 在str字符串中索引,与.mytext进行字符串比较,如果不匹配,继续读取
- 通过shdr -> sh_offset 和 shdr -> sh_size字段,将.mytext内容读取并保存在content中。
- 为了便于理解,不使用复杂的加密算法。这里,只将content的所有内容取反,即 *content = ~(*content);
- 将content内容写回so文件中
- 为了验证第二节中关于section 字段可以任意修改的结论,这里,将shdr -> addr 写入ELF头e_shoff,将shdr -> sh_size 和 addr 所在内存块写入e_entry中,即ehdr.e_entry = (length << 16) + nsize。当然,这样同时也简化了解密流程,还有一个好处是:如果将so文件头修正放回去,程序是不能运行的。
#include <stdio.h>
#include <fcntl.h>
#include <elf.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv){
char target_section[] = ".mytext";
char *shstr = NULL;
char *content = NULL;
Elf32_Ehdr ehdr;
Elf32_Shdr shdr;
int i;
unsigned int base, length;
unsigned short nblock;
unsigned short nsize;
unsigned char block_size = 16;
int fd;
if(argc < 2){
puts("Input .so file");
return -1;
}
fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("open %s failed\n", argv[1]);
goto _error;
}
//从so文件头读取section偏移shoff、shnum和shstrtab
if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Read ELF header error");
goto _error;
}
//off_t lseek(int handle, off_t offset, int fromwhere);
lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);
// ELF section string table
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Read ELF section string table error");
goto _error;
}
//分配str空间中
if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
puts("Malloc space for section string table failed");
goto _error;
}
//Read string table
lseek(fd, shdr.sh_offset, SEEK_SET);
if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
puts("Read string table failed");
goto _error;
}
//通过shdr -> sh_name 在str字符串中索引,与.mytext进行字符串比较,如果不匹配,继续读取
lseek(fd, ehdr.e_shoff, SEEK_SET);
for(i = 0; i < ehdr.e_shnum; i++){
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Find section .text procedure failed");
goto _error;
}
if(strcmp(shstr + shdr.sh_name, target_section) == 0){
base = shdr.sh_offset;
length = shdr.sh_size;
printf("Find section %s\n", target_section);
break;
}
}
//通过shdr -> sh_offset 和 shdr -> sh_size字段,将.mytext内容读取并保存在content中
lseek(fd, base, SEEK_SET);
content = (char*) malloc(length);
if(content == NULL){
puts("Malloc space for content failed");
goto _error;
}
if(read(fd, content, length) != length){
puts("Read section .text failed");
goto _error;
}
nblock = length / block_size;
nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);
printf("base = %d, length = %d\n", base, length);
printf("nblock = %d, nsize = %d\n", nblock, nsize);
ehdr.e_entry = (length << 16) + nsize;
ehdr.e_shoff = base;
//将content的所有内容取反
for(i=0;i<length;i++){
content[i] = ~content[i];
}
lseek(fd, 0, SEEK_SET);
if(write(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Write ELFhead to .so failed");
goto _error;
}
//将content内容写回so文件中
lseek(fd, base, SEEK_SET);
if(write(fd, content, length) != length){
puts("Write modified content to .so failed");
goto _error;
}
puts("Completed");
_error:
free(content);
free(shstr);
close(fd);
return 0;
}
####解密对应的加密操作
解密时,需要保证解密函数在so加载时被调用,那函数声明为:init_getString attribute((constructor))。(也可以使用c++构造器实现, 其本质也是用attribute实现)
解密流程:
- 动态链接器通过call_array调用init_getString
- Init_getString首先调用getLibAddr方法,得到so文件在内存中的起始地址
- 读取前52字节,即ELF头。通过e_shoff获得.mytext内存加载地址,ehdr.e_entry获取.mytext大小和所在内存块
- 修改.mytext所在内存块的读写权限
- 将[e_shoff, e_shoff + size]内存区域数据解密,即取反操作:*content = ~(*content);
- 修改回内存区域的读写权限
(这里是对代码段的数据进行解密,需要写权限。如果对数据段的数据解密,是不需要更改权限直接操作的)
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
jstring getString(JNIEnv*) __attribute__((section (".mytext")));
jstring getString(JNIEnv* env){
return (*env)->NewStringUTF(env, "Native method return!");
};
void init_getString() __attribute__((constructor));
unsigned long getLibAddr();
void init_getString(){
char name[15];
unsigned int nblock;
unsigned int nsize;
unsigned long base;
unsigned long text_addr;
unsigned int i;
Elf32_Ehdr *ehdr;
Elf32_Shdr *shdr;
base = getLibAddr();
ehdr = (Elf32_Ehdr *)base;
text_addr = ehdr->e_shoff + base;
nblock = ehdr->e_entry >> 16;
nsize = ehdr->e_entry & 0xffff;
printf("nblock = %d\n", nblock);
if(mprotect((void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
}
for(i=0;i< nblock; i++){
char *addr = (char*)(text_addr + i);
*addr = ~(*addr);
}
if(mprotect((void *) base, 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
puts("mem privilege change failed");
}
puts("Decrypt success");
}
unsigned long getLibAddr(){
unsigned long ret = 0;
char name[] = "libdemo.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
puts("open failed");
goto _error;
}
while(fgets(buf, sizeof(buf), fp)){
if(strstr(buf, name)){
temp = strtok(buf, "-");
ret = strtoul(temp, NULL, 16);
break;
}
}
_error:
fclose(fp);
return ret;
}
JNIEXPORT jstring JNICALL
Java_com_example_shelldemo_MainActivity_getString( JNIEnv* env,
jobject thiz )
{
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
#if defined(__ARM_NEON__)
#define ABI "armeabi-v7a/NEON"
#else
#define ABI "armeabi-v7a"
#endif
#else
#define ABI "armeabi"
#endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__mips__)
#define ABI "mips"
#else
#define ABI "unknown"
#endif
return getString(env);
}
注意:并不是所有的section都能全加,有些数据是不能加密的。比如直接对.text直接加密,会把与crt有关代码也加密,只能选择性的加密。
###3.2无源码加固
把要加固的so插入到一个load的尾部,然后用load将尾部的那个so正常加载运行起来。在外部逆向时只看到那个load的代码,完成加固的操作。
加密流程
$ ./merge libelf_loader.so libnative-lib.mo
fd1 is 3
fd2 is 4
ehdr is 0x6022c0
phdr is 0x6022f4
loader size = 0x4004
so size = 0x348c
base is 0x6608d000
write file libnative-lib.so sucess
3.3 elf保护
https://bbs.pediy.com/thread-203611.htm
-
序言
-
ELF文件格式分析
-
ELF常见HOOK方案及应用
a) Inline hook
b) GOT hook
c) PRELOAD hook
d) Linker重定位hook
-
ELF若干种保护方案
a) UPX壳及分析
b) Shellcode保护方案
c) 链接器及加载器
d) VMP
-
ELF混淆方案
a) 花指令
b) 指令乱序,使用B衔接
c) 指令替换,替换为B指令
d) 指令索引并且乱序,使用索引表来跳转
e) LLVM方式混淆
-
ELF反调试
a) 捕捉信号
b) 检测tracerPID
c) 检测调试进程
d) 处理ELF格式,阻止IDA加载
e) 多进程守护
f) CRC校验
g) 调试中断指令检测(类似x86 0xCC)
-
ELF函数加密
a) ELF入口加解密
b) 函数动态加解密,指令级加解密
c) 基于加载器的函数加解密
4.init.array的执行流程
https://bbs.pediy.com/thread-191092.htm
参考:http://bbs.pediy.com/thread-191649.htm
类似的文章:https://paper.seebug.org/89/
https://github.com/dzx1994/ELFShield/blob/master/README.md 无源码加固