介绍一种ELF文件函数粒度的加固方法,可以有效防止对程序的静态分析。这是一种有源码加固方式,需要被加固程序中代码配合。加固流程如下:
1)读取ELF文件头,获取e_phoff和e_phnum
2)通过Elf64_Phdr中的p_type字段,找到DYNAMIC
3)遍历.dynamic,找到.dynsym、.dynstr 节区偏移,和.dynstr节区的大小
4)遍历.dynsym,找到函数对应的Elf64_Sym符号后,根据st_value和st_size字段找到函数在ELF的偏移和函数大小
5)根据函数偏移和大小,加密之
加固程序代码如下,在x86_64平台测试通过:
#include <stdio.h> #include <fcntl.h> #include <elf.h> #include <stdlib.h> #include <string.h> typedef struct { Elf64_Addr st_value; Elf64_Word st_size; }func_info; Elf64_Ehdr ehdr; int find_target_section_addr(const int fd, const char *sec_name){ lseek(fd, 0, SEEK_SET); if(read(fd, &ehdr, sizeof(Elf64_Ehdr)) != sizeof(Elf64_Ehdr)){ puts("Read ELF header error"); return (-1); } return (0); } static char get_target_func_info(int fd, const char *func_name, func_info *info){ char flag = -1, *dynstr; int i; Elf64_Sym func_sym; Elf64_Phdr phdr; Elf64_Off dyn_off; Elf64_Word dyn_size, dyn_strsz; Elf64_Dyn dyn; Elf64_Addr dyn_symtab, dyn_strtab; lseek(fd, ehdr.e_phoff, SEEK_SET); for(i = 0; i < ehdr.e_phnum; i++){ if(read(fd, &phdr, sizeof(Elf64_Phdr)) != sizeof(Elf64_Phdr)){ puts("Read segment failed"); return (-1); } if(phdr.p_type == PT_DYNAMIC){ dyn_size = phdr.p_filesz; dyn_off = phdr.p_offset; flag = 0; printf("Find section %s, size = 0x%x, addr = 0x%lx\n", ".dynamic", dyn_size, dyn_off); break; } } if(flag) { puts("Find .dynamic failed"); return (-1); } flag = 0; lseek(fd, dyn_off, SEEK_SET); for(i=0;i < dyn_size / sizeof(Elf64_Dyn); i++){ if(read(fd, &dyn, sizeof(Elf64_Dyn)) != sizeof(Elf64_Dyn)){ puts("Read .dynamic information failed"); return (-1); } if(dyn.d_tag == DT_SYMTAB){ dyn_symtab = dyn.d_un.d_ptr; flag++; printf("Find .dynsym, addr = 0x%lx\n", dyn_symtab); } if(dyn.d_tag == DT_STRTAB){ dyn_strtab = dyn.d_un.d_ptr; flag++; printf("Find .dynstr, addr = 0x%lx\n", dyn_strtab); } if(dyn.d_tag == DT_STRSZ){ dyn_strsz = dyn.d_un.d_val; flag++; printf("Find .dynstr size, size = 0x%x\n", dyn_strsz); } } if(flag != 3){ puts("Find needed .section failed\n"); return (-1); } dynstr = (char*) malloc(dyn_strsz); if(dynstr == NULL){ puts("Malloc .dynstr space failed"); return (-1); } lseek(fd, dyn_strtab, SEEK_SET); if(read(fd, dynstr, dyn_strsz) != dyn_strsz){ puts("Read .dynstr failed"); return (-1); } lseek(fd, dyn_symtab, SEEK_SET); while (1) { if(read(fd, &func_sym, sizeof(Elf64_Sym)) != sizeof(Elf64_Sym)){ puts("Read func_sym failed"); return (-1); } if(strcmp(dynstr + func_sym.st_name, func_name) == 0){ break; } } printf("Find: %s, offset = 0x%lx, size = 0x%lx\n", func_name, func_sym.st_value, func_sym.st_size); info->st_value = func_sym.st_value; info->st_size = func_sym.st_size; ehdr.e_shoff = info->st_value; ehdr.e_shnum = info->st_size; lseek(fd, 0, SEEK_SET); if(write(fd, &ehdr, sizeof(Elf64_Ehdr)) != sizeof(Elf64_Ehdr)){ puts("Write elf header failed"); return (-1); } free(dynstr); return 0; } int main(int argc, char **argv){ char sec_name[] = ".text"; char func_name[] = "say_hello"; /* 被加密函数名 */ char *content = NULL; int fd, i; Elf64_Off secOff; func_info info; if(argc < 2){ puts("Usage: shell libxxx.so .(section) function"); return -1; } fd = open(argv[1], O_RDWR); if(fd < 0){ printf("open %s failed\n", argv[1]); return (-1); } if (find_target_section_addr(fd, sec_name) == -1) { printf("Find section %s failed\n", sec_name); return (-1); } if (get_target_func_info(fd, func_name, &info) == -1) { printf("Find function %s failed\n", func_name); return (-1); } content = (char*) malloc(info.st_size); if(content == NULL){ puts("Malloc space failed"); return (-1); } lseek(fd, info.st_value, SEEK_SET); if(read(fd, content, info.st_size) != info.st_size){ puts("Malloc space failed"); return (-1); } for(i = 0; i < info.st_size; i++){ content[i] = ~content[i]; } lseek(fd, info.st_value, SEEK_SET); if(write(fd, content, info.st_size) != info.st_size){ puts("Write modified content to .so failed"); return (-1); } puts("Complete!"); free(content); close(fd); return 0; }
解密代码放在.init_array节区,使ELF加载时运行解密:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <elf.h> #include <sys/mman.h> #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL << PAGE_SHIFT) typedef struct { Elf64_Addr st_value; Elf64_Word st_size; }func_info; void say_hello() { /* 被加密函数 */ puts("hello elf."); } void __init() __attribute__((constructor)); static unsigned long get_lib_addr(){ unsigned long ret = 0; 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, "libdemo.so")){ temp = strtok(buf, "-"); ret = strtoul(temp, NULL, 16); break; } } _error: fclose(fp); return ret; } void __init(){ /* 解密函数 */ const char target_fun[] = "say_hello"; func_info info; int i; unsigned long npage, base = get_lib_addr(); Elf64_Ehdr *ehdr = (Elf64_Ehdr *)base; info.st_value = ehdr->e_shoff; info.st_size = ehdr->e_shnum; npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1); if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ puts("mem privilege change failed"); } for(i = 0; i < info.st_size; i++){ char *addr = (char*)(base + info.st_value + i); *addr = ~(*addr); } if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), npage, PROT_READ | PROT_EXEC) != 0){ puts("mem privilege change failed"); } }
写了一段测试代码,如下:
#include <stdio.h> #include <dlfcn.h> int main(int argc, char **argv) { void (*say_hello)(); void *h; char *error; h= dlopen("./libdemo.so", RTLD_NOW); if (h == NULL) { error = dlerror(); printf("%s\n", error); return (-1); } say_hello = dlsym(h, "say_hello"); say_hello(); dlclose(h); return (0); }
以上参考了ThomasKing在看雪的贴子,但查找符号位置使用了一种新方法。
原代码使用的DT_HASH,老版本GCC和现在的安卓都在使用这个结构,它比较简单。在Ubuntu 14.04上测试时发现新版GCC并没有用DT_HASH,而是使用的DT_GUN_HASH,它使用BloomFilter算法针对符号不存在的情况做了效率优化。
这个结构比较复杂,如果再按照ELF加载器的流程来做就比较麻烦,所以选择了遍历的方法。但也有个缺点,当查找的符号不存在时程序会崩溃。
运行结果如下:
kiiim@ubuntu :~/_elf/m2$ gcc shell.c
kiiim@ubuntu :~/_elf/m2$ gcc loader.c -o loader -ldl
kiiim@ubuntu :~/_elf/m2$ gcc demo.c -fPIC -shared -o libdemo.so
kiiim@ubuntu :~/_elf/m2$ ./a.out libdemo.so
Find section .dynamic, size = 0x1c0, addr = 0xe18
Find .dynstr, addr = 0x488
Find .dynsym, addr = 0x230
Find .dynstr size, size = 0x10f
Find: say_hello, offset = 0x9f5, size = 0x12
Complete!
kiiim@ubuntu :~/_elf/m2$ ./loader
hello elf.
[email protected]:~/_elf/m2$
原贴中还有另一种加固方法,将要加固函数写入新的节区,如.mytext,然后针对节区整体加密。这种方法实现同样比较简单。但评论里有个问题值得讨论下。
有回复说实现了.text整体加密方案,但我分析了下,觉得不可行。
观察.init_array节,发现在解密函数__init()执行前,还要执行一个frame_dummy()的系统函数:
.init_array:0000000000200DF8 _init_array segment para public ‘DATA‘ use64
.init_array:0000000000200DF8 assume cs:_init_array
.init_array:0000000000200DF8 ;org 200DF8h
.init_array:0000000000200DF8 __frame_dummy_init_array_entry dq offset frame_dummy
.init_array:0000000000200E00 dq offset __init ;解密函数.init_array:0000000000200E00 _init_array ends
而这个函数是在.text中实现的:
.text:00000000000009C0 frame_dummy proc near
.text:00000000000009C0 cmp cs:__JCR_LIST__, 0
.text:00000000000009C8 jz short loc_9F0
.text:00000000000009CA mov rax, cs:_Jv_RegisterClasses_ptr
.text:00000000000009D1 test rax, rax
.text:00000000000009D4 jz short loc_9F0
.text:00000000000009D6 push rbp
.text:00000000000009D7 lea rdi, __JCR_LIST__
.text:00000000000009DE mov rbp, rsp
.text:00000000000009E1 call rax ; _Jv_RegisterClasses
.text:00000000000009E3 pop rbp
.text:00000000000009E4 jmp register_tm_clones
.text:00000000000009E4 ; ---------------------------------------------------------------------------
.text:00000000000009E9 align 10h
.text:00000000000009F0
.text:00000000000009F0 loc_9F0: ; CODE XREF: frame_dummy+8 j
.text:00000000000009F0 ; frame_dummy+14 j
.text:00000000000009F0 jmp register_tm_clones.text:00000000000009F0 frame_dummy endp
也就是说,在解密函数__init()执行之前,frame_dummy()运行就会失败。也就不能对.text整体加密。