介绍一种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.
kiiim@ubuntu:~/_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整体加密。

一种简单的ELF加固方法的更多相关文章

  1. GIT将本地项目上传到Github(两种简单、方便的方法)

    GIT将本地项目上传到Github(两种简单.方便的方法) 一.第一种方法: 首先你需要一个github账号,所有还没有的话先去注册吧! https://github.com/ 我们使用git需要先安 ...

  2. Gradle实现的两种简单的多渠道打包方法

    本来计划今天发Android的官方技术文档的翻译--<Gradle插件用户指南>的第五章的,不过由于昨天晚上没译完,还差几段落,所以只好推后了. 今天就说一下使用Gradle进行类似友盟这 ...

  3. iOS几种简单有效的数组排序方法

    第一种,利用数组的sortedArrayUsingComparator调用 NSComparator ,obj1和obj2指的数组中的对象 NSComparator cmptr = ^(id obj1 ...

  4. Git的使用--如何将本地项目上传到Github(三种简单、方便的方法)

    一.第一种方法: 1.首先你需要一个github账号,所以还没有的话先去注册吧! https://github.com/ 我们使用git需要先安装git工具,这里给出下载地址,下载后一路(傻瓜式安装) ...

  5. Git的使用--如何将本地项目上传到Github(两种简单、方便的方法..)

    https://blog.csdn.net/u014135752/article/details/79951802 总结:其实只需要进行下面几步就能把本地项目上传到Github 1.在本地创建一个版本 ...

  6. Honeywords项目——检查密码是否被破解的一种简单方法

    Honeywords项目使用一种简单的方法来改进hash后的密码的安全性——为每个账户维护一个额外的honeywords(假密码).如果有黑客拿到了密码的文件,然后试图用brute froce的方式破 ...

  7. CSharpGL(40)一种极其简单的半透明渲染方法

    CSharpGL(40)一种极其简单的半透明渲染方法 开始 这里介绍一个实现半透明渲染效果的方法.此方法极其简单,不拖累渲染速度,但是不能适用所有的情况. 如下图所示,可以让包围盒显示为半透明效果. ...

  8. 使用strace工具故障排查的5种简单方法

    使用strace工具故障排查的5种简单方法 本文源自5 simple ways to troubleshoot using strace strace 是一个非常简单的工具,用来跟踪可执行程序的系统调 ...

  9. WPF编程 ,TextBlock 显示百分数值的一种简单方法。

    原文:WPF编程 ,TextBlock 显示百分数值的一种简单方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/article/ ...

随机推荐

  1. openssh安装/更新教程(CentOS)

    由于rpm包版本总落后于tar包,对于想安装新版本或由于漏洞需要更新到新版本那只能选择源代方式编译安装. 更新执行和安装一样的步骤就行了. 1.下载 官方网址:http://www.openssh.c ...

  2. 最新jquery+easyui_api培训文档

    目  录 1 Accordion(可折叠标签) 2 1.1 实例 2 1.2 参数 3 2 DateBox(日期框) 4 2.1 实例 4 2.2 参数 6 2.3 事件 6 2.4 方法 6 3 C ...

  3. Qt sprintf_s函数格式化字符串出错

    Qt sprintf_s函数格式化字符串出错 问题的出现: 我在VS上用c C++写的跨平台的函数 移植到Qt 上面 出现sprintf_s 函数格式化出错. 开始以为是编码问题  反复查找Qt乱码问 ...

  4. ELementUI 树形控件tree 获取子节点同时获取半选择状态的父节点ID

    使用element-ui  tree树形控件的时候,在选择一个子节点后,使用getCheckedKeys 后,发现只能返回子节点的ID,但是其父节点ID没有返回. 解决办法有三种: 1.element ...

  5. 在shell脚本里执行sudo 命令

      可以 : echo "yourpasswd" |sudo -S yourcommand 但是不安全,因为密码都显示在shell脚本里面了-_- 引自http://hi.baid ...

  6. facebook视频上传python 返回错误code:100,'type':OAuthException

    首先重新获取访问口令token: https://developers.facebook.com/tools/debug/accesstoken/?q=EAAYDuzyd3eYBAK9lZCErZBl ...

  7. linux下查看运行进程详细信息

    通过ps及top命令查看进程信息时,只能查到相对路径,查不到的进程的详细信息,如绝对路径等.这时,我们需要通过以下的方法来查看进程的详细信息: Linux在启动一个进程时,系统会在/proc下创建一个 ...

  8. react router @4 和 vue路由 详解(三)react如何在路由里面定义一个子路由

    完整版:https://www.cnblogs.com/yangyangxxb/p/10066650.html 5.react如何在路由里面定义一个子路由?   a.引入在需要子路由的页面引入Rout ...

  9. 每天CSS学习之transform

    transform是CSS3的一个属性,其作用是用来进行2D或3D变换. 一.2D变换 1. translate(x-offset , y-offset) translate的作用就是用作位置的移动. ...

  10. Thread.join方法的解析(转)

    原文链接:https://www.cnblogs.com/huangzejun/p/7908898.html 1. join() 的示例和作用 1.1 示例 1 // 父线程 2 public cla ...