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 整体流程

  1. 确定自定义节的名称
  2. 加密流程实现
  3. 解密代码编写
  4. 集成到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 解密原理

  1. 从加密后SO文件头的e_entry右移16位获取加密节的长度
  2. 从文件头的e_shoff获取加密节的内存偏移
  3. 使用mprotect修改节属性为可写
  4. 逐个字符解密
  5. 恢复节的原始权限

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 关键点解析

  1. __attribute__((constructor)):

    • 使函数在SO加载时自动执行
    • 类似于Java中的静态初始化块
  2. __attribute__((section(".mytext"))):

    • 将函数放入自定义节中
    • 确保需要保护的代码集中在特定节
  3. mprotect使用:

    • 修改内存页属性为可写(PROT_WRITE)
    • 解密完成后恢复为只读可执行(PROT_READ | PROT_EXEC)
  4. ELF头信息隐藏:

    • 利用e_entry存储加密节长度
    • 利用e_shoff存储加密节偏移

五、集成与使用

5.1 使用步骤

  1. 编写Native代码,将核心函数放入自定义节
  2. 编译生成SO文件
  3. 使用加密工具加密指定节
  4. 将解密代码集成到项目中
  5. 打包发布APK

5.2 加密工具使用

# 基础加密
./encrypt_tool libnative-lib.so .mytext

# 增强版加密
./enhanced_encrypt_tool libnative-lib.so .mytext

六、防护与对抗

6.1 防护效果

  • IDA等静态分析工具无法直接查看加密节内容
  • 动态调试需要定位解密点才能获取明文代码

6.2 可能的绕过方式

  1. .init_array节下断点,拦截解密过程
  2. 解密完成后dump内存中的SO文件
  3. 修改内存中的解密逻辑

6.3 增强防护建议

  1. 结合反调试技术
  2. 增加代码完整性校验
  3. 使用多节交叉加密
  4. 采用更复杂的加密算法

七、总结

SO文件的Section加密技术是一种有效的Native层保护方案,通过将关键代码集中到自定义节并加密,配合运行时解密机制,可以在不影响功能的前提下显著增加逆向分析的难度。实际应用中可以根据安全需求选择不同强度的加密方案,并结合其他保护技术形成多层防御体系。

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 基础加密实现 3.2 增强版加密实现 在基础版基础上,增加对ELF头的修改,进一步增加反汇编难度: 修改原理 : 对于动态链接库, e_entry 入口地址无实际意义 将加密节的信息隐藏在ELF头中,增加逆向难度 解密时可以从这些字段中恢复出必要信息 四、解密实现 4.1 解密原理 从加密后SO文件头的 e_entry 右移16位获取加密节的长度 从文件头的 e_shoff 获取加密节的内存偏移 使用 mprotect 修改节属性为可写 逐个字符解密 恢复节的原始权限 4.2 完整解密代码 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 加密工具使用 六、防护与对抗 6.1 防护效果 IDA等静态分析工具无法直接查看加密节内容 动态调试需要定位解密点才能获取明文代码 6.2 可能的绕过方式 在 .init_array 节下断点,拦截解密过程 解密完成后dump内存中的SO文件 修改内存中的解密逻辑 6.3 增强防护建议 结合反调试技术 增加代码完整性校验 使用多节交叉加密 采用更复杂的加密算法 七、总结 SO文件的Section加密技术是一种有效的Native层保护方案,通过将关键代码集中到自定义节并加密,配合运行时解密机制,可以在不影响功能的前提下显著增加逆向分析的难度。实际应用中可以根据安全需求选择不同强度的加密方案,并结合其他保护技术形成多层防御体系。