Android逆向之Native层分析
字数 1061 2025-08-22 18:37:22

Android Native层逆向分析技术详解

一、Native层分析概述

Android应用的核心保护逻辑和算法通常会被放在Native层(so库)中,主要原因包括:

  1. 增加逆向分析难度
  2. 保护关键业务逻辑
  3. 隐藏native方法注册过程
  4. 实现壳保护机制

本文以分析libshella_3.0.0.0.so文件为例,重点讲解以下技术点:

  • init_array节的解密操作
  • JNI_OnLoad函数的保护机制
  • 通过/proc/self/maps获取so基址的技术
  • 动态解密和内存操作技术

二、init_array节解密分析

1. 函数初始化

STMFD SP!, {R11,LR}      ; 保存上一个栈帧的帧地址和返回地址
ADD R11, SP, #4         ; 设置当前栈帧
SUB SP, SP, #0x48       ; 开辟0x48字节的栈空间
STR R0, [R11,#var_48]   ; 参数入栈
STR R1, [R11,#var_4C]   ; 参数入栈

2. 获取so基址

通过ELF头特征0x464C457C(即"\x7FELF")来定位so基址:

LDR R3, =(init_array - 0x763FA964)
ADD R3, PC, R3          ; 获取init_array函数地址
BIC R3, R3, #0xFF0      ; 对齐到0x1000边界
BIC R3, R3, #0xF        ; 得到so基址
STR R3, [R11,#var_8]   ; 存储基址到局部变量

loc_763FA97C:
LDR R3, [R11,#var_8]   ; 加载基址
LDR R2, [R3]           ; 读取ELF头
LDR R3, =unk_464C457F  ; 加载ELF魔数
CMP R2, R3             ; 比较是否为ELF文件
BNE loc_763FA970       ; 不是则继续查找

loc_763FA970:
LDR R3, [R11,#var_8]   ; 加载当前基址
SUB R3, R3, #0x1000    ; 向上移动一个内存页
STR R3, [R11,#var_8]   ; 存储新基址
B loc_763FA97C         ; 继续检查

3. 解析程序头表

LDR R3, [R11,#var_8]    ; so基址
LDR R2, [R3,#0x1C]      ; 读取程序头表偏移
ADD R3, R2, R3          ; 计算程序头表地址
STR R3, [R11,#var_C]    ; 存储程序头表地址

4. 遍历程序头表

while (*(unkown *)(var_28 + 0x2c) > var_10) {
    if (*(unkown *)var_C == 1 && *(unkown *)(var_C + 0x18) == 5) {
        // 可加载且可读可执行段
        var_1C = (*(unkown *)(var_C + 0x8) + *(unkown *)(var_C + 0x10) + 0xFFF) & 0xFFFFF000;
    } else if (*(unkown *)var_C == 1 && *(unkown *)(var_C + 0x18) == 6) {
        // 可加载且可读可写段
        var_20 = *(DWORD *)(var_C + 8) & 0xFFFFF000;
        var_24 = (*(DWORD *)(var_C + 8) + *(DWORD *)(var_C + 0x10) + 0xFFF) & 0xFFFFF000;
        break;
    }
    var_10++;
    var_C += 0x20;
}

5. 内存属性修改与解密

// 修改内存属性为可读写
mprotect(*(unkown *)var_30 + 0x1000, ((*(unkown *)var_30 / 0x1000) + 1) * 0x1000, 3);

// 解密循环
for (var_18 = 0x1000; var_18 <= var_3C; var_18++) {
    var_31 = *(BYTE *)(var_30 + var_18);
    var_2A = (unsigned __int8)(((var_12 - var_13) ^ var_18 + var_14) ^ var_11);
    *(BYTE *)(var_30 + var_18) ^= (char)var_2A;
    *(BYTE *)(var_30 + var_18) += (unsigned __int8)((var_13 & var_14) ^ var_12);
    var_11 += (unsigned __int8)(var_12 + var_13 - var_14) & var_31 & var_18;
    var_12 += (unsigned __int8)(var_18 + var_11) ^ var_31;
    var_13 ^= (unsigned __int8)(var_31 - var11) ^ var_18;
    var_14 += (unsigned __int8)(var_18) - (var_31 + var_11);
}

// 恢复内存属性为可读可执行
mprotect(var_38 + var_30, (var_40 / 0x1000 + 1) * 0x1000, 5);
sub_B39E47E8(var_38 + var_30, var_40);  // 缓存更新

三、/proc/self/maps遍历技术

1. 初始化操作

// 打开/proc/self/maps文件
LDR R3, =(aProcSelfMaps - 0xA54EF5D0)
ADD R3, PC, R3          ; "/proc/self/maps"
MOV R0, R3
LDR R3, =(unk_A54EFA5C - 0xA54EF5DC)
ADD R3, PC, R3          ; "r"
MOV R1, R3
BL fopen
STR R0, [R11,#var_8]    ; 存储文件指针

// 初始化缓冲区
memset(&var_40C, 0, 0x400);
memset(&var_80C, 0, 0x400);

2. 读取和解析maps文件

while (feof(var_8) == 0) {
    fgets(&var_40C, 0x400, var_8);
    sscanf(var_40C, "%lx-%lx %s %s %s %s %s", 
           &var_810, &var_814, var_834, var_830, var_82C, var_828, var_824);
    
    // 检查是否找到目标so区域
    if (var_810 > var_C || var_814 <= var_C) {
        fclose(var_8);
        return 0;
    }
}

四、动态解密与加载技术

1. 内存映射与初始化

// 映射内存区域
var_C = mmap(var_2C, var_F4, 0, 0x22, 0xFFFFFFFF, 0);

// 计算加载偏移
var_30 = var_C - var_2C;
var_210 = var_C;
var_180 = var_30;

2. 动态解密流程

// 初始化解压结构
while (deflateInit(&var_22AC, 0xFFFFFFF1, "1.2.3", 0x38) != 0);

// 解密循环
while (*(unkown *)(var_10 + 0xC) > var_8) {
    // 计算读取长度
    if (var_8 + 0x1000 <= *(unkown *)(var_10 + 0xC))
        var_4C = 0x1000;
    else
        var_4C = *(unkown *)(var_10 + 0xC) - var_8;
    
    // 读取加密数据
    pread(var_28, &var_229C, var_4C, var_8 + var_22DC + *(unkown *)(var_10 + 8));
    
    // 解密数据
    decrypt("Tx:12345Tx:12345", &var_229C, var_50, 0x10);
    
    // 解压数据
    var_54 = inflate_0(&var_229C, 0);
    
    // 更新指针
    var_8 += var_4C;
}

// 刷新缓存
for (var_1C = var_38; var_18 + var_38 > var_1C; var_1C += 0x400)
    cacheflush(var_1C, 0x400);

五、关键技术与总结

1. 汇编分析技巧

  • 控制流分析:重点查看CMP、CMN等条件判断指令,确定程序分支
  • 函数调用:分析BL/BLX指令,向上查找参数传递过程
  • 栈帧分析:识别局部变量和参数在栈中的布局
  • 循环识别
    • for循环:条件判断在底部,修改循环变量的代码在循环体之前
    • while循环:条件判断成功后直接跳转到循环体

2. 常用ARM指令模式

  • 内存对齐计算ADD Rd,0xFFF + BIC Rd,0xFFF = 对齐到0x1000边界
  • 内存属性修改:使用mprotect系统调用切换内存区域的读写执行属性
  • 缓存更新:使用cacheflush或类似机制确保解密后的代码生效

3. 反调试与保护技术

  • 动态解密:运行时解密关键代码段,增加静态分析难度
  • 自修改代码:修改内存中的代码段,对抗调试器断点
  • 多重映射:通过/proc/self/maps检测内存布局,防止内存dump
  • 环境检测:检查ro.build.version.sdk等属性,识别模拟器或调试环境

4. 逆向分析建议

  1. 优先分析init_array和JNI_OnLoad函数
  2. 跟踪内存属性修改操作(mprotect调用)
  3. 监控/proc/self/maps的读取行为
  4. 关注异常的内存读写模式
  5. 使用动态调试结合静态分析,特别关注解密循环

通过掌握这些Native层分析技术,可以有效应对Android应用中的各种保护机制,为安全分析和漏洞挖掘奠定基础。

Android Native层逆向分析技术详解 一、Native层分析概述 Android应用的核心保护逻辑和算法通常会被放在Native层(so库)中,主要原因包括: 增加逆向分析难度 保护关键业务逻辑 隐藏native方法注册过程 实现壳保护机制 本文以分析 libshella_3.0.0.0.so 文件为例,重点讲解以下技术点: init_ array节的解密操作 JNI_ OnLoad函数的保护机制 通过/proc/self/maps获取so基址的技术 动态解密和内存操作技术 二、init_ array节解密分析 1. 函数初始化 2. 获取so基址 通过ELF头特征 0x464C457C (即"\x7FELF")来定位so基址: 3. 解析程序头表 4. 遍历程序头表 5. 内存属性修改与解密 三、/proc/self/maps遍历技术 1. 初始化操作 2. 读取和解析maps文件 四、动态解密与加载技术 1. 内存映射与初始化 2. 动态解密流程 五、关键技术与总结 1. 汇编分析技巧 控制流分析 :重点查看CMP、CMN等条件判断指令,确定程序分支 函数调用 :分析BL/BLX指令,向上查找参数传递过程 栈帧分析 :识别局部变量和参数在栈中的布局 循环识别 : for循环:条件判断在底部,修改循环变量的代码在循环体之前 while循环:条件判断成功后直接跳转到循环体 2. 常用ARM指令模式 内存对齐计算 : ADD Rd,0xFFF + BIC Rd,0xFFF = 对齐到0x1000边界 内存属性修改 :使用mprotect系统调用切换内存区域的读写执行属性 缓存更新 :使用cacheflush或类似机制确保解密后的代码生效 3. 反调试与保护技术 动态解密 :运行时解密关键代码段,增加静态分析难度 自修改代码 :修改内存中的代码段,对抗调试器断点 多重映射 :通过/proc/self/maps检测内存布局,防止内存dump 环境检测 :检查ro.build.version.sdk等属性,识别模拟器或调试环境 4. 逆向分析建议 优先分析init_ array和JNI_ OnLoad函数 跟踪内存属性修改操作(mprotect调用) 监控/proc/self/maps的读取行为 关注异常的内存读写模式 使用动态调试结合静态分析,特别关注解密循环 通过掌握这些Native层分析技术,可以有效应对Android应用中的各种保护机制,为安全分析和漏洞挖掘奠定基础。