android so加固之section加密
字数 1280 2025-08-22 22:47:30
Android SO加固之Section加密技术详解
一、技术背景与原理
1.1 为什么需要SO加固
在Android开发中,Native层的核心代码通常编译为SO(共享库)文件。与Java层代码相比,SO文件更容易被逆向分析,因此需要对SO文件中的核心代码进行保护。
1.2 Section加密原理
通过将核心代码写入自定义节(Section)中,并对该节进行加密。在SO文件加载执行时,利用__attribute__((constructor))属性,使解密函数先于main函数执行,完成解密工作。
1.3 技术优势
- 保护核心算法不被直接逆向
- 增加动态分析的难度
- 不影响SO文件的正常加载流程
二、实现流程
2.1 整体流程
- 确定自定义节的名称
- 加密流程实现
- 解密代码编写
- 集成到Android项目中
三、加密工具实现
3.1 基础加密实现
#include <stdio.h>
#include <elf.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd;
Elf32_Ehdr ehdr;
Elf32_Shdr shdr;
char *section_name_table;
int i;
unsigned int base, length;
char *content;
// 参数验证
if (argc != 3) {
printf("Encrypt section of elf file\n\nUsage:\n\t%s <elf_file> <section_name>\n", *argv);
goto _error;
}
// 打开ELF文件
if ((fd = open(argv[1], O_RDWR, 0777)) == -1) {
perror("open");
goto _error;
}
// 读取ELF头
if (read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)) {
perror("read elf header");
goto _error;
}
// 读取节头字符串表
printf("[+] Begining find section %s\n", argv[2]);
lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);
if (read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)) {
perror("read elf section header which contain string table");
goto _error;
}
// 分配内存并读取节名称表
if ((section_name_table = (char *)malloc(shdr.sh_size)) == NULL) {
perror("malloc for SHT_STRTAB");
goto _error;
}
lseek(fd, shdr.sh_offset, SEEK_SET);
if (read(fd, section_name_table, shdr.sh_size) != shdr.sh_size) {
perror("read string table");
goto _error;
}
// 遍历节头查找目标节
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)) {
perror("read section");
goto _error;
}
if (strcmp(section_name_table + shdr.sh_name, argv[2]) == 0) {
base = shdr.sh_offset;
length = shdr.sh_size;
printf("[+] Find section %s\n", argv[2]);
printf("[+] %s section offset is %X\n", argv[2], base);
printf("[+] %s section size is %d\n", argv[2], length);
break;
}
}
// 读取节内容
lseek(fd, base, SEEK_SET);
content = (char *)malloc(length);
if (content == NULL) {
perror("malloc space for section");
goto _error;
}
if (read(fd, content, length) != length) {
perror("read section in encrpt");
goto _error;
}
// 取反加密
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)) {
perror("write ELF header to file");
goto _error;
}
lseek(fd, base, SEEK_SET);
if (write(fd, content, length) != length) {
perror("write encrypted section to file");
goto _error;
}
printf("[+] Encrypt section %s completed!\n", argv[2]);
_error:
free(section_name_table);
free(content);
close(fd);
return 0;
}
3.2 增强版加密实现
在基础版基础上,增加对ELF头的修改,进一步增加反汇编难度:
// 在基础版代码中添加以下修改:
// 将该节具体长度值替换到入口点去
// 将该节的偏移地址替换到文件头中的节头表偏移值中去
nsize = length / 4096 + (length % 4096 == 0 ? 0 : 1);
ehdr.e_entry = (length << 16) + nsize;
ehdr.e_shoff = base;
printf("[+] %s section use %d memory page!\n", argv[2], nsize);
修改原理:
- 对于动态链接库,
e_entry入口地址无实际意义 - 将加密节的信息隐藏在ELF头中,增加逆向难度
- 解密时可以从这些字段中恢复出必要信息
四、解密实现
4.1 解密原理
- 从加密后SO文件头的
e_entry右移16位获取加密节的长度 - 从文件头的
e_shoff获取加密节的内存偏移 - 使用
mprotect修改节属性为可写 - 逐个字符解密
- 恢复节的原始权限
4.2 完整解密代码
#include <jni.h>
#include <string>
#include <asm/fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sstream>
#include <fcntl.h>
#include <android/log.h>
#include <elf.h>
#include <sys/mman.h>
#define PAGE_SIZE 4096
#define LOG_TAG "JNITag"
// 声明需要加密的函数放在自定义节中
jstring getString(JNIEnv*) __attribute__((section(".mytext")));
// 声明为构造函数,在.init_array节执行
void decryte_section() __attribute__((constructor));
// 获取当前SO的加载基址
unsigned long getLibAddr();
void decryte_section() {
unsigned long base;
Elf32_Ehdr *ehdr;
unsigned long my_text_addr;
unsigned int nblock;
unsigned int nsize;
unsigned int i;
base = getLibAddr();
ehdr = (Elf32_Ehdr *)base;
// 从ELF头中获取加密节信息
my_text_addr = base + ehdr->e_shoff;
nblock = ehdr->e_entry >> 16;
nsize = (nblock / PAGE_SIZE) + (nblock % PAGE_SIZE == 0 ? 0 : 1);
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "size of encrypted section is %d", nblock);
// 修改内存页属性为可写
if (mprotect((void *)(my_text_addr / PAGE_SIZE * PAGE_SIZE),
nsize * PAGE_SIZE,
PROT_READ | PROT_EXEC | PROT_WRITE) == -1) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Memory privilege change failed before encrypt");
}
// 执行解密(取反)
for (i = 0; i < nblock; i++) {
char *addr = (char *)(my_text_addr + i);
*addr = ~(*addr);
}
// 恢复内存页属性
if (mprotect((void *)(my_text_addr / PAGE_SIZE * PAGE_SIZE),
nsize * PAGE_SIZE,
PROT_READ | PROT_EXEC) == -1) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Memory privilege change failed after encrypt");
}
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Decrypt completed!");
}
/**
* 获取当前进程内部指定共享库文件的内存映射地址
*/
unsigned long getLibAddr() {
int pid;
char buffer[4096];
FILE *fd;
char *tmp;
unsigned long ret = 0;
char so_name[] = "libnative-lib.so";
pid = getpid();
sprintf(buffer, "/proc/%d/maps", pid);
if ((fd = fopen(buffer, "r")) == NULL) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "open /proc/%d/maps failed!", pid);
goto _error;
}
while (fgets(buffer, sizeof(buffer), fd)) {
if (strstr(buffer, so_name)) {
tmp = strtok(buffer, "-");
ret = strtoul(tmp, 0, 16);
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Find file %s", so_name);
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "The memory address of %s is 0x%X", so_name, ret);
break;
}
}
_error:
fclose(fd);
return ret;
}
// JNI导出函数
extern "C" JNIEXPORT jstring JNICALL
Java_com_testjni_MainActivity_isTraceMe(JNIEnv *env, jobject instance) {
return getString(env);
}
// 需要加密的核心代码(存放在自定义节中)
jstring getString(JNIEnv *env) {
return env->NewStringUTF("Text from JNI");
}
4.3 关键点解析
-
__attribute__((constructor)):- 使函数在SO加载时自动执行
- 类似于Java中的静态初始化块
-
__attribute__((section(".mytext"))):- 将函数放入自定义节中
- 确保需要保护的代码集中在特定节
-
mprotect使用:- 修改内存页属性为可写(PROT_WRITE)
- 解密完成后恢复为只读可执行(PROT_READ | PROT_EXEC)
-
ELF头信息隐藏:
- 利用
e_entry存储加密节长度 - 利用
e_shoff存储加密节偏移
- 利用
五、集成与使用
5.1 使用步骤
- 编写Native代码,将核心函数放入自定义节
- 编译生成SO文件
- 使用加密工具加密指定节
- 将解密代码集成到项目中
- 打包发布APK
5.2 加密工具使用
# 基础加密
./encrypt_tool libnative-lib.so .mytext
# 增强版加密
./enhanced_encrypt_tool libnative-lib.so .mytext
六、防护与对抗
6.1 防护效果
- IDA等静态分析工具无法直接查看加密节内容
- 动态调试需要定位解密点才能获取明文代码
6.2 可能的绕过方式
- 在
.init_array节下断点,拦截解密过程 - 解密完成后dump内存中的SO文件
- 修改内存中的解密逻辑
6.3 增强防护建议
- 结合反调试技术
- 增加代码完整性校验
- 使用多节交叉加密
- 采用更复杂的加密算法
七、总结
SO文件的Section加密技术是一种有效的Native层保护方案,通过将关键代码集中到自定义节并加密,配合运行时解密机制,可以在不影响功能的前提下显著增加逆向分析的难度。实际应用中可以根据安全需求选择不同强度的加密方案,并结合其他保护技术形成多层防御体系。