##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 体系结构上的很多操作系统中使用。
目标文件有三种类型:
生成另外一个目标文件(比如:编译器和连接器 把*.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头的字段。
##0x03ELF文件加固
###3.1有源码加固 基于特定section的加解密实现
基于section的加解密,是指将so文件的特定section进行加密,so文件被加载时解密。下面给出实例。
假设有一个shelldemo应用,调用一个native方法返回一个字符串供UI显示。在native方法中,又调用getString方法返回一个字符串供native方法返回。我需要将getString方法加密。这里,将getString方法存放在.mytext中(指定__attribute__((section (".mytext")))?,即是需要对.mytext进行加密。
加密流程:
#include
#include
#include
#include
#include
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
####解密对应的加密操作
解密时,需要保证解密函数在so加载时被调用,那函数声明为:init_getString attribute((constructor))。(也可以使用c++构造器实现, 其本质也是用attribute实现)
解密流程:
#include
#include
#include
#include
#include
#include
#include
#include
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
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) 基于加载器的函数加解密
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 无源码加固