一种简单的ELF加固方法
介绍一种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加固方法的更多相关文章
- GIT将本地项目上传到Github(两种简单、方便的方法)
GIT将本地项目上传到Github(两种简单.方便的方法) 一.第一种方法: 首先你需要一个github账号,所有还没有的话先去注册吧! https://github.com/ 我们使用git需要先安 ...
- Gradle实现的两种简单的多渠道打包方法
本来计划今天发Android的官方技术文档的翻译--<Gradle插件用户指南>的第五章的,不过由于昨天晚上没译完,还差几段落,所以只好推后了. 今天就说一下使用Gradle进行类似友盟这 ...
- iOS几种简单有效的数组排序方法
第一种,利用数组的sortedArrayUsingComparator调用 NSComparator ,obj1和obj2指的数组中的对象 NSComparator cmptr = ^(id obj1 ...
- Git的使用--如何将本地项目上传到Github(三种简单、方便的方法)
一.第一种方法: 1.首先你需要一个github账号,所以还没有的话先去注册吧! https://github.com/ 我们使用git需要先安装git工具,这里给出下载地址,下载后一路(傻瓜式安装) ...
- Git的使用--如何将本地项目上传到Github(两种简单、方便的方法..)
https://blog.csdn.net/u014135752/article/details/79951802 总结:其实只需要进行下面几步就能把本地项目上传到Github 1.在本地创建一个版本 ...
- Honeywords项目——检查密码是否被破解的一种简单方法
Honeywords项目使用一种简单的方法来改进hash后的密码的安全性——为每个账户维护一个额外的honeywords(假密码).如果有黑客拿到了密码的文件,然后试图用brute froce的方式破 ...
- CSharpGL(40)一种极其简单的半透明渲染方法
CSharpGL(40)一种极其简单的半透明渲染方法 开始 这里介绍一个实现半透明渲染效果的方法.此方法极其简单,不拖累渲染速度,但是不能适用所有的情况. 如下图所示,可以让包围盒显示为半透明效果. ...
- 使用strace工具故障排查的5种简单方法
使用strace工具故障排查的5种简单方法 本文源自5 simple ways to troubleshoot using strace strace 是一个非常简单的工具,用来跟踪可执行程序的系统调 ...
- WPF编程 ,TextBlock 显示百分数值的一种简单方法。
原文:WPF编程 ,TextBlock 显示百分数值的一种简单方法. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/article/ ...
随机推荐
- servlet-api-2.4.jar not loaded(转)
信息: validateJarFile(D:/xj/workspace/webworktest/webapp/WEB-INF/lib/servlet-api-2.4.jar) - jar not lo ...
- python中Flask模块的使用
1.简介 在服务器上运行Flask接口,就能使用requests模块获取该接口的值. 先运行接口文件,再运行requests文件,即可获取值. 2.示例 2.1一个简单的flask接口 import ...
- Django框架(一)
一.web应用 web应用 Web应用程序是一种可以通过Web访问的应用程序,程序的最大好处是用户很容易访问应用程序,用户只需要有浏览器即可,不需要再安装其他软件.应用程序有两种模式C/S.B/S.C ...
- IO多路复用和协程
1.IO多路复用 作用:检测多个socket是否已经发生变化(是否连接成功/是否已经获取数据) 什么是进程.线程.协程以及它们的区别? 进程是资源分配的最小单元,其作用是进行数据隔离, 线程是cpu调 ...
- 第2天【OS Linux发行版介绍、Linux系统基础使用入门、Linux命令帮助、Linux基础命令】
Logout 退出系统 Gedit 文本编辑器工具 Uname –r 查看内核版本信息,uname –a 比较详细 Cat /proc/cpuinfo 查看CPU Cat /p ...
- WebAPi获取请求头中对应键值
/// <summary> /// 依据键获取请求头中值数据 /// </summary> /// <param name="request"> ...
- python图片识别
python 图像处理模块1. 安装 pytesseract模块是会自动安装Pillow模块.pillow 为标准图像处理库 手册地址 http://pillow-cn.readthedocs.io/ ...
- jQuery获取select值
jQuery操作select标签 即控制select的option属性 <select id="sid" > <option value="-1&quo ...
- rnnlib依赖ubuntu环境配置
rnnlib help http://sourceforge.net/apps/mediawiki/rnnl/index.php?title=Main_Page boost: http://blog. ...
- shell shell基本概述
SHELL的概念 SHELL是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序, 用户可以用shell来启动,挂起,停止甚至是编写一些程序. Shell还是 ...