应用安全 --- 安卓逆向 之 dobby框架
https://blog.csdn.net/qq_60933960/article/details/151935127
dobby框架本质就是so层的hook工具,通过编写c++代码替换原有的c++函数,与firda不同的是不用root并且是内部编译进入apk中的持久化的
分为两步,第一步编译库为so ,第二步 将so 拷贝进入你目标安卓项目
注意原仓库的无法编译为安卓so文件,我用第三方仓库编译so,在kali linux中编译通过
https://gitee.com/null_465_7266/dobby
第一步
第二步
我创建一个demo
https://gitee.com/null_465_7266/dobbyandroidproject
我这里也有将两步合并为一步的代码
https://gitee.com/null_465_7266/dobbydemo
这是上面代码精简只保留arm64的代码,方便分析原理
https://gitee.com/null_465_7266/dobbyandroid

Dobby Hook框架实现原理深度分析
一、整体架构概览
┌─────────────────────────────────────────────────────┐
│ Public API Layer │
│ DobbyHook / DobbyInstrument / DobbyDestroy │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ Interceptor │
│ (Hook Entry 管理中心) │
│ 双向链表维护所有HookEntry │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ InterceptRouting │
│ FunctionInlineReplace / DynamicBinaryInstrument │
│ (路由调度核心) │
└──────┬───────────────┬──────────────────────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────────────────────────────┐
│ Trampoline │ │ ClosureTrampolineBridge │
│ 跳板生成 │ │ 闭包跳板+寄存器上下文保存/恢复 │
└──────┬──────┘ └──────┬──────────────────────────────┘
│ │
┌──────▼───────────────▼──────────────────────────────┐
│ InstructionRelocation │
│ 指令重定位引擎 │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ Assembler / CodeGen / CodeBuffer / MemoryArena │
│ 底层汇编/内存管理基础设施 │
└─────────────────────────────────────────────────────┘
二、核心数据结构
2.1 HookEntry —— Hook的基本信息单元
typedef struct {
int id;
int type; // kFunctionWrapper / kFunctionInlineHook / kDynamicBinaryInstrument
union {
void *target_address;
void *function_address;
void *instruction_address;
};
void *route; // 指向对应的InterceptRouting子类实例
union {
void *relocated_origin_instructions; // 重定位后的原始指令地址
void *relocated_origin_function;
};
AssemblyCodeChunkBuffer origin_chunk_; // 保存被覆盖的原始字节
} HookEntry;
关键设计:
origin_chunk_保存原始指令字节,用于DobbyDestroy时恢复relocated_origin_instructions指向重定位后的代码,供Hook返回时执行原始逻辑
2.2 Interceptor —— 单例Hook管理器
// 使用Linux内核风格的双向循环链表管理所有HookEntry
struct list_head hook_entry_list_; // 链表头
// 核心操作
FindHookEntry(void *address) // 按地址查找
AddHookEntry(HookEntry *entry) // 插入链表头部
RemoveHookEntry(void *address) // 从链表删除
三、DobbyHook核心流程
以DobbyHook(address, replace_call, origin_call)为例,完整执行流程:
DobbyHook()
│
├─1─► 创建 HookEntry
│ type = kFunctionInlineHook
│ function_address = address
│
├─2─► 创建 FunctionInlineReplaceRouting
│
├─3─► route->Prepare() [空实现,留给子类扩展]
│
├─4─► route->DispatchRouting()
│ │
│ ├─► BuildReplaceRouting()
│ │ SetTrampolineTarget(replace_call)
│ │ GenerateTrampolineBuffer(target, replace_call)
│ │ │
│ │ └─► 生成跳转到replace_call的机器码
│ │ (arm64: adrp+add+br 或 ldr+br)
│ │ (x64: jmp [rip+offset])
│ │
│ └─► GenerateRelocatedCode(tramp_size)
│ │
│ ├─► 读取target处原始指令(tramp_size字节)
│ ├─► 调用 GenRelocateCodeAndBranch()
│ │ 重定位PC相关指令 + 末尾追加跳回原函数的branch
│ └─► 保存原始字节到 origin_chunk_
│
├─5─► Interceptor::AddHookEntry(entry)
│
├─6─► *origin_call = entry->relocated_origin_function
│
└─7─► route->Commit() => route->Active()
CodePatch(target_address, trampoline_buffer, size)
[实际写入跳转指令,劫持控制流]
四、指令重定位引擎(以ARM64为例)
这是整个框架最精密的部分。ARM64使用PC相对寻址,搬移指令时必须修正。
4.1 需要重定位的指令类型
┌──────────────────────┬────────────────────────────────────┐
│ 指令类型 │ 重定位策略 │
├──────────────────────┼────────────────────────────────────┤
│ LDR (literal) │ Mov(TMP, abs_addr) + LDR [TMP, 0] │
│ ADR / ADRP │ Mov(Xrd, runtime_address) │
│ B / BL │ Ldr(TMP, label) + BR/BLR TMP │
│ TBZ / TBNZ │ 反转条件 + 跳过 + Ldr + BR │
│ CBZ / CBNZ │ 反转条件 + 跳过 + Ldr + BR │
│ B.cond │ 反转条件 + 跳过 + Ldr + BR │
│ 其他指令 │ 原样复制 │
└──────────────────────┴────────────────────────────────────┘
4.2 条件跳转重定位原理
以CBZ X0, #far_target为例,原始范围±1MB,重定位后需跳到任意地址:
; 原始代码(在原地址)
CBZ X0, #far_target ; 如果X0==0,跳到far_target
; 重定位后代码(在新地址)
CBNZ X0, #skip ; 条件取反:如果X0!=0,跳过branch块
LDR X17, =far_target ; 加载绝对地址到临时寄存器
BR X17 ; 无条件跳转
skip:
; ... 继续执行下一条重定位指令
关键细节:条件取反(op ^ 1),固定偏移为4 * 3 = 12字节(branch_instr + ldr + br)
4.3 重定位代码末尾追加回跳
// 所有需要重定位的指令处理完后
CodeGen codegen(&turbo_assembler_);
codegen.LiteralLdrBranch(curr_orig_pc); // 跳回原函数剩余部分
五、跳板(Trampoline)生成机制
5.1 ARM64普通跳板
根据源地址和目标地址的距离选择策略:
距离 < 128MB (adrp范围):
ADRP X17, to_PAGE - from_PAGE ; 4字节
ADD X17, X17, to_PAGEOFF ; 4字节
BR X17 ; 4字节
共12字节
距离 >= 128MB:
LDR X17, #8 ; 4字节,从后面的label加载
BR X17 ; 4字节
.quad target_addr ; 8字节
共16字节
5.2 ARM64近跳板(NearBranchTrampoline插件)
当启用dobby_enable_near_branch_trampoline()时,只需4字节:
B #offset ; 单条B指令,范围±128MB
如果目标超出范围,则在±128MB内分配一个快速转发跳板(fast forward trampoline),形成二级跳转:
原函数 --[B #near]--> 快速转发跳板 --[Adrp+Add+Br]--> 目标地址
5.3 x64跳板
使用RIP相对间接跳转(6字节):
FF 25 XX XX XX XX ; JMP [RIP + offset]
; offset指向存有绝对目标地址的内存位置
六、ClosureTrampolineBridge(闭包跳板桥)
这是DobbyInstrument(DBI模式)的核心,用于在不知道具体目标函数的情况下,将任意Hook点的上下文打包传递给用户回调。
6.1 整体结构
ClosureTrampolineEntry {
void *address; // 跳板代码地址
void *carry_data; // 携带的数据(HookEntry*)
void *carry_handler; // 处理函数(instrument_routing_dispatch)
}
6.2 ARM64完整执行流程
原函数入口
│
▼
[ClosureTrampoline 代码]
│ SUB SP, SP, #16 ; 分配栈空间
│ STR X30, [SP, #8] ; 保存LR
│ LDR X17, =entry ; 加载ClosureTrampolineEntry*
│ STR X17, [SP, #0] ; 压栈(作为closure_bridge的参数)
│ LDR X17, =closure_bridge ; 加载closure_bridge地址
│ BLR X17 ; 调用closure_bridge(执行后X17=next_hop)
│ LDR X30, [SP, #8] ; 恢复LR
│ ADD SP, SP, #16 ; 释放栈
│ BR X17 ; 跳转到next_hop(重定位后的原始指令)
▼
[closure_bridge 代码](动态生成)
│ ; 保存完整寄存器上下文
│ SUB SP, SP, #(8*16) ; 为q0-q7分配空间
│ STP Q6, Q7, [SP, #96]
│ ... (保存所有浮点寄存器)
│ SUB SP, SP, #(30*8) ; 为x1-x30分配空间
│ STP X29, X30, [SP, #224]
│ ... (保存所有通用寄存器)
│ SUB SP, SP, #16
│ STR X0, [SP, #8] ; 保存X0
│ ; 计算原始SP
│ ADD X17, SP, #(2*8 + 2*8 + 30*8 + 8*16)
│ SUB SP, SP, #16
│ STR X17, [SP, #8] ; 保存原始SP
│
│ MOV X0, SP ; arg1 = RegisterContext*
│ LDR X1, [SP, #REGISTER_CONTEXT_SIZE] ; arg2 = ClosureTrampolineEntry*
│ CALL intercept_routing_common_bridge_handler
│
│ ; 恢复寄存器
│ ADD SP, SP, #16 ; 释放SP占位
│ LDR X0, [SP, #8]
│ ADD SP, SP, #16 ; 释放X0占位
│ LDP X1, X2, [SP], #16 ; 恢复x1-x30
│ ... (恢复所有寄存器)
│ LDP Q0, Q1, [SP], #32 ; 恢复浮点寄存器
│ ...
│ RET ; 返回ClosureTrampoline(X17已是next_hop)
6.3 RegisterContext内存布局(ARM64)
高地址
┌──────────────────┐ ← 原始SP
│ 原始SP(8字节) │
│ dummy(8字节) │
├──────────────────┤
│ x0(8字节) │
│ dummy(8字节) │
├──────────────────┤
│ x1...x28(29*8)│
├──────────────────┤
│ fp=x29(8字节) │
│ lr=x30(8字节) │
├──────────────────┤
│ q0...q7(8*16) │
低地址(当前SP)
对应dobby.h中的RegisterContext结构体,用户回调可直接读写寄存器。
七、DBI路由处理链
instrument_routing_dispatch(ctx, closure_trampoline_entry)
│
├─► entry = closure_trampoline_entry->carry_data (HookEntry*)
│
└─► instrument_call_forward_handler(ctx, entry)
│
├─► route = entry->route (DynamicBinaryInstrumentRouting*)
│
├─► 调用用户的 handler(ctx, &entry_info)
│ 用户可在此读写所有寄存器
│
└─► set_routing_bridge_next_hop(ctx, entry->relocated_origin_instructions)
// 设置 ctx->general.x[17] = relocated_addr
// closure_bridge结束后BR X17跳到重定位后的原始指令
八、内存管理子系统
8.1 MemoryArena(普通内存池)
page_chunks (LiteMutableArray)
│
├─► PageChunk_1 (kReadExecute, 4KB)
│ page_cursor → 当前分配位置
│ chunks[] → [CodeChunk1, CodeChunk2, ...]
│
└─► PageChunk_2 (kReadWrite, 4KB)
...
分配流程:
- 遍历已有页面,找到同权限且有剩余空间的页
- 没有则
mmap分配新页(4KB) - 从页面的
page_cursor处划分内存块
8.2 NearMemoryArena(近端内存池)
在pos ± alloc_range范围内寻找可用内存,两种策略:
策略1:寻找空白页面(search_near_blank_page)
遍历进程内存布局 → 找相邻region之间的空洞 → mmap分配
策略2:寻找代码洞(search_near_blank_memory_chunk)
在已有可执行页面中 → 用memmem搜索连续零字节 → 直接写入
(适用于无法分配新页的限制环境)
8.3 CodeBuffer层次结构
LiteMutableBuffer(动态扩容缓冲区)
└─► CodeBufferBase(添加Emit8/16/32/64)
├─► CodeBuffer(ARM) : +EmitARMInst/EmitThumb1/2Inst
├─► CodeBuffer(ARM64) : +EmitInst/Emit64/FixBindLabel
├─► CodeBuffer(x86) : +Emit32/FixBindLabel
└─► CodeBuffer(x64) : +Emit32/Emit64/FixBindLabel
九、代码修补(CodePatch)多平台实现
9.1 Linux/Android
mprotect(page, size, PROT_READ|PROT_WRITE|PROT_EXEC) // 改权限
memcpy(target, buffer, size) // 写入
mprotect(page, size, PROT_READ|PROT_EXEC) // 恢复权限
ClearCache(start, end) // 清指令缓存
9.2 macOS/iOS(关键)
iOS不允许直接修改可执行页,使用mach_vm_remap技巧:
1. mmap 分配新匿名页(dummy_page)
2. memcpy 原始页内容到 dummy_page
3. 在 dummy_page 上 patch 目标字节
4. mprotect dummy_page 为 READ|EXEC
5. mach_vm_remap(self_port, &target_page, ..., dummy_page, ...)
→ 将 dummy_page 重映射覆盖到 target_page 的虚拟地址
→ 绕过 W^X 限制
6. munmap dummy_page
7. ClearCache
9.3 ARM64缓存清除关键步骤
// 1. 清D-cache(数据缓存到统一点)
for (addr = start; addr < end; addr += dcache_line_size)
"dc cvau, addr"
// 2. 数据同步屏障
"dsb ish"
// 3. 清I-cache(指令缓存)
for (addr = start; addr < end; addr += icache_line_size)
"ic ivau, addr"
// 4. 指令同步屏障
"isb sy"
十、汇编器Label系统
Dobby实现了一个两阶段label绑定系统:
Label状态机:
unused (pos_=0)
│ link_to(pos)
▼
linked (pos_>0) ←─── link_to(pos) 形成链表
│ bind_to(pos)
▼
bound (pos_<0)
PseudoLabel扩展了Label,记录所有引用该label的指令位置:
// 前向引用场景:
Ldr(X17, &label) // 此时label未绑定,记录当前位置到instructions_[]
...
PseudoBind(&label) // 绑定label
EmitInt64(data) // 发射实际数据
│
└─► link_confused_instructions()
// 回填之前所有LDR指令的偏移量
encoded = inst32 & 0xFF00001F
encoded |= LeftShift((offset >> 2), 19, 5)
FixBindLabel(position, encoded)
十一、多架构跳板对比
| 架构 | 普通跳板大小 | 指令序列 | 近跳板大小 |
|---|---|---|---|
| ARM64 | 12B(近) / 16B(远) | ADRP+ADD+BR / LDR+BR+data | 4B (B #off) |
| ARM | 8B | LDR PC, [PC,-4] + data | 4B (B #off) |
| x86 | 5B | JMP rel32 | - |
| x64 | 6B+8B | JMP [RIP+0] + abs_addr | - |
十二、DobbyDestroy恢复机制
PUBLIC int DobbyDestroy(void *address) {
HookEntry *entry = Interceptor::SharedInstance()->FindHookEntry(address);
if (entry) {
uint8_t *buffer = entry->origin_chunk_.chunk_buffer; // 原始字节
uint32_t size = entry->origin_chunk_.chunk.length; // 原始大小
// ARM需要去掉Thumb标记位(最低bit)
#if TARGET_ARCH_ARM
address = (void*)((addr_t)address - 1);
#endif
CodePatch(address, buffer, size); // 恢复原始指令
Interceptor::SharedInstance()->RemoveHookEntry(address);
return RT_SUCCESS;
}
return RT_FAILED;
}
十三、关键设计决策总结
| 设计点 | 实现方案 | 原因 |
|---|---|---|
| Hook管理 | 单例+侵入式双向链表 | 轻量,无STL依赖 |
| 寄存器保存 | 动态汇编生成 | 灵活,支持运行时决策 |
| 指令重定位 | 逐条解码+重写 | 处理所有PC相关指令 |
| iOS写保护绕过 | mach_vm_remap | 唯一合法绕过W^X的方式 |
| 近端内存 | 进程布局扫描+代码洞 | 最小化跳板大小 |
| 前向label | 两阶段绑定+回填 | 支持不知道目标地址时先emit |
| 跨平台 | 编译期宏隔离 | 零运行时开销 |
浙公网安备 33010602011771号