当前位置: 首页 > news >正文

应用安全 --- 安卓加固 之 PIE 与 ASLR

应用安全 --- 安卓加固 之 PIE 与 ASLR

 

一、先理解没有 PIE/ASLR 时的情况

text
没有地址随机化的世界(固定基址):

每次运行程序,内存布局完全一样:

第1次运行                    第2次运行                    第3次运行
┌──────────┐ 0xFFFFFFFF    ┌──────────┐ 0xFFFFFFFF    ┌──────────┐ 0xFFFFFFFF
│   栈     │               │   栈     │               │   栈     │
│          │ 0x7FFE0000    │          │ 0x7FFE0000    │          │ 0x7FFE0000
├──────────┤               ├──────────┤               ├──────────┤
│          │               │          │               │          │
│   堆     │ 0x08100000    │   堆     │ 0x08100000    │   堆     │ 0x08100000
├──────────┤               ├──────────┤               ├──────────┤
│  .data   │ 0x08050000    │  .data   │ 0x08050000    │  .data   │ 0x08050000
├──────────┤               ├──────────┤               ├──────────┤
│  .text   │ 0x08048000    │  .text   │ 0x08048000    │  .text   │ 0x08048000
│ (代码段)  │               │ (代码段)  │               │ (代码段)  │
└──────────┘ 0x00000000    └──────────┘ 0x00000000    └──────────┘ 0x00000000

         ↑                          ↑                          ↑
    main在0x08048500           main在0x08048500           main在0x08048500
    
    每次都一样!攻击者可以直接写死地址进行攻击

问题:攻击者知道关键函数永远在固定地址,可以精确跳转执行恶意代码


二、ASLR 是什么

text
ASLR = Address Space Layout Randomization(地址空间布局随机化)

这是 操作系统内核 提供的安全机制
每次加载程序时,内核给各个内存区域分配一个随机的基地址

┌─────────────────────────────────────────────────────────────┐
│  ASLR 是操作系统层面的开关                                     │
│                                                             │
│  Linux:   /proc/sys/kernel/randomize_va_space               │
│           0 = 关闭                                          │
│           1 = 栈和共享库随机化                                │
│           2 = 栈、堆、共享库全部随机化(默认值)                 │
│                                                             │
│  Android: 默认开启 ASLR(从Android 4.1开始)                  │
│  Windows: 从Vista开始默认支持                                 │
└─────────────────────────────────────────────────────────────┘

三、PIE 是什么

text
PIE = Position Independent Executable(位置无关可执行文件)

这是 编译器 在编译时生成的一种代码格式
代码中不使用绝对地址,而是使用相对偏移

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  非PIE代码(绝对地址):                                      │
│  ┌─────────────────────────────┐                            │
│  │  call 0x08048500            │  ← 写死地址                │
│  │  mov eax, [0x0804A000]      │  ← 写死地址                │
│  │  jmp 0x08048600             │  ← 写死地址                │
│  └─────────────────────────────┘                            │
│  无论加载到哪里,代码里写死了要跳转到0x08048500                 │
│  如果基址变了,这些指令全部崩溃                                 │
│                                                             │
│  PIE代码(相对地址):                                        │
│  ┌─────────────────────────────┐                            │
│  │  call [PC + 0x200]          │  ← 相对当前位置偏移         │
│  │  mov eax, [PC + 0x1A00]     │  ← 相对当前位置偏移         │
│  │  jmp [PC + 0x300]           │  ← 相对当前位置偏移         │
│  └─────────────────────────────┘                            │
│  不管加载到内存哪个位置,都能正确运行                           │
│  因为用的是"距离我当前位置多远"而不是"去哪个固定地址"            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

四、PIE + ASLR 配合工作

text
关键关系:

  ASLR(操作系统):我要把程序放到随机地址
  PIE(编译器):  没问题,我的代码放哪儿都能跑

  ┌──────────────────────────────────────────────────────┐
  │  两者缺一不可:                                        │
  │                                                      │
  │  有ASLR + 无PIE = 主程序地址固定,只有库地址随机        │
  │  无ASLR + 有PIE = 代码支持随机但系统不随机,等于没用     │
  │  有ASLR + 有PIE = 全部随机化,安全性最高 ✅             │
  └──────────────────────────────────────────────────────┘

实际运行效果

text
有 PIE + ASLR 的世界(随机基址):

第1次运行                    第2次运行                    第3次运行
┌──────────┐               ┌──────────┐               ┌──────────┐
│   栈     │ 0x7FFD3000    │   栈     │ 0x7FFC1000    │   栈     │ 0x7FFE8000
├──────────┤               ├──────────┤               ├──────────┤
│  共享库   │ 0x76A40000    │  共享库   │ 0x71B20000    │  共享库   │ 0x74C60000
├──────────┤               ├──────────┤               ├──────────┤
│   堆     │ 0x55900000    │   堆     │ 0x56300000    │   堆     │ 0x55B00000
├──────────┤               ├──────────┤               ├──────────┤
│  .data   │ 0x55555000    │  .data   │ 0x55688000    │  .data   │ 0x55432000
├──────────┤               ├──────────┤               ├──────────┤
│  .text   │ 0x55554000    │  .text   │ 0x55687000    │  .text   │ 0x55431000
└──────────┘               └──────────┘               └──────────┘

    main在0x55554500           main在0x55687500           main在0x55431500
         ↑                          ↑                          ↑
                        每次都不一样!

五、对逆向分析的具体影响

影响1:IDA静态分析的地址 ≠ 运行时地址

text
IDA中看到的(静态分析):
┌─────────────────────────────────────┐
│  IDA默认基址:0x00000000            │
│                                     │
│  sub_56BD8 位于 0x00056BD8          │
│  encrypt_key 位于 0x000A1000        │
│  init_array 位于 0x00098000         │
└─────────────────────────────────────┘
        │  实际运行时(ASLR生效)
┌─────────────────────────────────────┐
│  实际基址:0x742B0000(随机的)       │
│                                     │
│  sub_56BD8 位于 0x74306BD8          │
│  │                                  │
│  计算方式:                          │
│  0x742B0000(随机基址)              │
│  + 0x56BD8(IDA中的偏移)            │
│  = 0x74306BD8(实际内存地址)         │
│                                     │
│  下次运行基址变成 0x71F20000         │
│  sub_56BD8 就在 0x71F76BD8          │
└─────────────────────────────────────┘

影响2:断点设置

text
调试时设置断点的流程:

  ❌ 错误做法:直接写死地址
     b *0x74306BD8
     → 下次运行这个地址就不对了

  ✅ 正确做法:先获取基址再计算
     ┌──────────────────────────────────────────────┐
     │  1. 查看模块加载地址                           │
     │     cat /proc/<pid>/maps | grep libxxx.so     │
     │     输出: 742B0000-742F0000 r-xp libxxx.so    │
     │                                               │
     │  2. 基址 = 0x742B0000                         │
     │                                               │
     │  3. 断点地址 = 基址 + IDA偏移                  │
     │     = 0x742B0000 + 0x56BD8                    │
     │     = 0x74306BD8                              │
     │                                               │
     │  Frida中的写法:                               │
     │  var base = Module.getBaseAddress("libxxx.so") │
     │  var target = base.add(0x56BD8)               │
     │  → 自动处理基址随机化                           │
     └──────────────────────────────────────────────┘

影响3:Frida Hook 示例

JavaScript
// Frida脚本 — 正确处理PIE/ASLR

// 方法1:通过模块名自动获取基址
var module = Process.getModuleByName("libxxx.so");
console.log("基址: " + module.base);            // 每次运行都不同
console.log("大小: " + module.size);

// IDA中的偏移是固定的,加上随机基址得到真实地址
var func_addr = module.base.add(0x56BD8);       // 0x56BD8是IDA中的偏移
console.log("函数真实地址: " + func_addr);       // 每次运行都不同

Interceptor.attach(func_addr, {
    onEnter: function(args) {
        console.log("进入 sub_56BD8");
    }
});

// 方法2:通过导出函数名(不受ASLR影响)
var jni_onload = Module.getExportByName("libxxx.so", "JNI_OnLoad");
// 导出函数有名字,系统自动解析地址,不用手动计算

六、通俗比喻

text
╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║  没有ASLR的程序 = 你家地址固定且公开                                ║
║  ├─ 小偷知道你家在 "人民路100号3楼"                                ║
║  ├─ 知道保险箱在卧室衣柜后面                                       ║
║  └─ 直接去就行                                                    ║
║                                                                   ║
║  有ASLR的程序 = 每天醒来你家随机搬到城市的不同位置                    ║
║  ├─ 今天在人民路100号,明天在建设路50号,后天在解放大道200号          ║
║  ├─ 但房子内部布局不变:保险箱还是在"进门后走10步右转第2个房间"       ║
║  │                      ↑ 这就是偏移量(IDA中的地址)               ║
║  └─ 小偷每次都要先找到房子在哪(获取基址),再用内部路线找保险箱       ║
║                                                                   ║
║  PIE = 房子的设计方式                                               ║
║  ├─ 房子里所有指示牌写的都是 "向前走10步" 而不是 "去人民路100号"      ║
║  └─ 这样房子搬到任何地方,内部导航都能正常工作                        ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

七、逆向时的实际应对

text
应对PIE/ASLR的方法:

┌─ 方法1:计算偏移(最常用)
│   真实地址 = 模块基址 + IDA偏移
│   模块基址通过 /proc/pid/maps 或 Frida 获取
├─ 方法2:关闭ASLR(调试时临时关闭)
│   echo 0 > /proc/sys/kernel/randomize_va_space
│   → 地址变固定,方便反复调试
│   → 仅限有root权限的调试环境
├─ 方法3:用导出函数名定位(不受影响)
│   导出函数有符号名,动态链接器自动解析
│   如 JNI_OnLoad、Java_com_xxx_nativeFunc
└─ 方法4:IDA远程调试自动rebase
    IDA附加进程后会自动将数据库rebase到实际基址
    此时IDA显示的地址 = 内存中的真实地址