luckfollowme arm学习篇11 - dobby源码探究
回顾之前 分析的 dobby function inline hook
它替换我们的方法的前 12个字节 用于跳转到 hook方法上
# 获取 hook方法的地址
adrp x17, =当前PC内存对齐地址
add x17, x17, #0xa28
# 绝对跳转 到 hook方法
BR X17
并把原先方法的的12 个字节 转到下面的地方:
# 原方法的 前 12 个字节
FF 03 01 D1 SUB SP, SP, #0x40 ; '@'
E9 23 40 F9 LDR X9, [SP,#0x40]
E8 27 40 F9 LDR X8, [SP,#0x48]
# 跳回到 原方法的 12 字节后面的地址
51 00 00 58 LDR X17, =0x753EDCE894
20 02 1F D6 BR X17
接下来我们将模拟这个过程。
再次之前,我们得看看 dobby 的部分源码
dobby 源码分析
我们主要看 DobbyHook 实现,它在 source/InterceptRouting/Routing/FunctionInlineHook/FunctionInlineHook.cc 下
// address 被hook的方法地址 replace_func hook方法地址 origin_func 储存开辟用于跳到原方法的指针
PUBLIC int DobbyHook(void *address, dobby_dummy_func_t replace_func, dobby_dummy_func_t *origin_func) {
//1.查找是否已经被 hook 过了
auto entry = Interceptor::SharedInstance()->find((addr_t)address);
if (entry) {
ERROR_LOG("%p already been hooked.", address);
return -1;
}
//2。创建拦截实体
entry = new InterceptEntry(kFunctionInlineHook, (addr_t)address);
//6.创建inline hook routing
auto *routing = new FunctionInlineHookRouting(entry, replace_func);
//准备hook
routing->Prepare();
//进行转发 在这里获取 patched 修补指令 的字节码
routing->DispatchRouting();
//7.在这里提交 修改内存属性 并修改内存
routing->Commit();
//8.添加注册信息
Interceptor::SharedInstance()->add(entry);
return 0;
}
//3. 拦截实体构造器 需要1个hook 的类型 被hook 的地址
InterceptEntry::InterceptEntry(InterceptEntryType type, addr_t address) {
//4.这个类型一般是 function hook
//当然还有 instruction 任意地址hook
this->type = type;
//5.patched addr属性 到时候修改跳转的 12 个字节的开始地址
//此时这个属性就是我们被hook方法的开始地址
this->patched_addr = address;
this->id = Interceptor::SharedInstance()->count();
}
在这里可以看到,关键获取 pathced 字节码 和 提交修改内存 都在 FunctionInlineHookRouting 这个里面。
我们稍微分析下这个类即可.
FunctionInlineHookRouting
FunctionInlineHookRouting 是 dobby 用户 对 function 进行 内联 hook 路由转发的关键类
它的结构如下:
//1. 这各是 拦截路由基础类
class InterceptRouting {
public:
explicit InterceptRouting(InterceptEntry *entry) : entry_(entry) {
// 拦截实体信息
entry->routing = this;
// 原地址
origin_ = nullptr;
// 迁移后的地址
relocated_ = nullptr;
trampoline_ = nullptr;
//patched 数据 也就是修改的12个字节
trampoline_buffer_ = nullptr;
//跳到hook方法的地址
trampoline_target_ = 0;
}
protected:
InterceptEntry *entry_;
CodeMemBlock *origin_;
CodeMemBlock *relocated_;
CodeMemBlock *trampoline_;
CodeBufferBase *trampoline_buffer_;
addr_t trampoline_target_;
....
}
// 2. FunctionInlineHookRouting 继承 InterceptRouting
class FunctionInlineHookRouting : public InterceptRouting {
public:
FunctionInlineHookRouting(InterceptEntry *entry, dobby_dummy_func_t replace_func) : InterceptRouting(entry) {
//3. 在原有的 interceptrouting 上 扩展了 replace_func 我们 hook的方法
this->replace_func = replace_func;
}
void DispatchRouting() override;
private:
void BuildRouting();
private:
dobby_dummy_func_t replace_func;
};
DispatchRouting
转发路由。
通过 replace_func 替换方法地址,生成 12个字节 指令用于跳转到 replace_func
并将原来的 12个字节 储存在 origin_ 中
void FunctionInlineHookRouting::BuildRouting() {
//2. 设置 trampolineTarget 跳转地址 用于跳到 替换方法
SetTrampolineTarget((addr_t)replace_func);
//3. 从 patched_addr 跳转到 tranpolineTarget
addr_t from = entry_->patched_addr;
addr_t to = GetTrampolineTarget();
//4. 生成跳转字节码 储存在 trampoline buffer 中
GenerateTrampolineBuffer(from, to);
}
void FunctionInlineHookRouting::DispatchRouting() {
//1.构建转发路由
BuildRouting();
//2.生成迁移数据,也就是将原来的 12 个字节换个地方 并加跳转
GenerateRelocatedCode();
}
GenerateTrampolineBuffer
GenerateTrampolineBuffer 用于生成跳转 trampoline_target(repalce_func) 的指令
bool InterceptRouting::GenerateTrampolineBuffer(addr_t src, addr_t dst) {
。。。。。。
if (GetTrampolineBuffer() == nullptr) {
// 1.生成 跳转指令 储存到 trampline_buffer 中
auto tramp_buffer = GenerateNormalTrampolineBuffer(src, dst);
SetTrampolineBuffer(tramp_buffer);
}
return true;
}
CodeBufferBase *GenerateNormalTrampolineBuffer(addr_t from, addr_t to) {
TurboAssembler turbo_assembler_((void *)from);
//2. llabs 获取 uint64 的绝对值
// 这明显是获取 from - to 也就是 origin - trampline_target 地址
uint64_t distance = llabs((int64_t)(from - to));
// adrp 只能获取 1 << 32 的地址寻址
uint64_t adrp_range = ((uint64_t)1 << (2 + 19 + 12 - 1));
//3. 如果没有超过 adrp 范围
if (distance < adrp_range) {
//5.则生成 adrp, add, br 这 3 条指令
_ AdrpAdd(TMP_REG_0, from, to);
_ br(TMP_REG_0);
DEBUG_LOG("[trampoline] use [adrp, add, br]");
} else {
// 6.否则就生成 ldr br 从内存中获取跳转
// ldr, br, branch-address
CodeGen codegen(&turbo_assembler_);
codegen.LiteralLdrBranch((uint64_t)to);
DEBUG_LOG("[trampoline] use [ldr, br, #label]");
}
#undef _
// Bind all labels
turbo_assembler_.RelocBind();
//7. 生成buffer 字节数据
auto result = turbo_assembler_.GetCodeBuffer()->Copy();
return result;
}
AdrpAdd
在看了下 dobby adrp 计算方式,我感觉我之前说错了,所以还是单独看下 dobby 如何通过 adrp 获取目标地址的吧。
#define ALIGN ALIGN_FLOOR
// 通过 address & ~(2的幂次方 -1) 计算对齐地址
#define ALIGN_FLOOR(address, range) ((uintptr_t)address & ~((uintptr_t)range - 1))
//rd 是寄存器 from 是 origin to 是 trampline_target
void AdrpAdd(Register rd, uint64_t from, uint64_t to) {
//获取 from 对齐的内存页
uint64_t from_PAGE = ALIGN(from, 0x1000);
// 获取 to 对齐的内存页
uint64_t to_PAGE = ALIGN(to, 0x1000);
// 获取 to 基于对齐内存页的偏移
uint64_t to_PAGEOFF = (uint64_t)to % 0x1000;
// rd = to_PAGE - from_PAGE
adrp(rd, to_PAGE - from_PAGE);
// rd = rd + to_PAGEOFF
add(rd, rd, to_PAGEOFF);
}
可以看到 adrp 是pc 的4kb对齐地址 到 目标地址的4kb对齐地址 的偏移。
最终会定位到 目标地址的 4kb 内存对齐的位置
随后通过 add 指令 加上 目标地址距离 目标地址4kb内存对齐的偏移。最终定位到目标地址上。
为什么要这么麻烦?因为 每个汇编指令都是 4字节 不可能寻址到所有地址范围。
虽然 adrp 也有范围限制,但通过基于 pc 偏移的数据量,可以满足大部分地址寻址。
随后通过 to % 0x1000 计算清零的后 12位的偏移加回去
GenerateRelocatedCode
关于生成 relocated 迁移代码,代码不好细说,但是我可以简要说一下.
大致原理就是 : 原来的 12 字节 + ldr + br 指令 跳转到之前 12 字节后面的地址
//文件位于 source/InstructionRelocation/arm64/InstructionRelocationARM64.cc
// branch 表示跳到原来的地方
int relo_relocate(relo_ctx_t *ctx, bool branch) {
//用于写 assembly
TurboAssembler turbo_assembler_(0);
// 使用 "_" 替换 "turbo_assembler_."
#define _ turbo_assembler_.
//遍历 原有 12 字节的每条指令
while (ctx->buffer_cursor < ctx->buffer + ctx->buffer_size) {
// 原有的指令的位置
uint32_t orig_off = ctx->buffer_cursor - ctx->buffer;
// 迁移的位置
uint32_t relocated_off = relocated_buffer->GetBufferSize();
ctx->relocated_offset_map[orig_off] = relocated_off;
// 原有指令
arm64_inst_t inst = *(arm64_inst_t *)ctx->buffer_cursor;
if(...){
...
}else if (){
....
}
else {
//原有指令插入到迁移的地方
_ Emit(inst);
}
}
#undef _
。。。。
if (branch) {
// 迁移过来的原有的 12 字节
CodeGen codegen(&turbo_assembler_);
// 在加上 ldr + br 指令 跳回去
codegen.LiteralLdrBranch(ctx->origin->addr + ctx->origin->size);
}
。。。。
}
void CodeGen::LiteralLdrBranch(uint64_t address) {
auto turbo_assembler_ = reinterpret_cast<TurboAssembler *>(this->assembler_);
#define _ turbo_assembler_->
// 生成标签 指向地址
auto label = RelocLabel::withData(address);
turbo_assembler_->AppendRelocLabel(label);
// 获取标签偏移基于 随后 br跳转
_ Ldr(TMP_REG_0, label);
_ br(TMP_REG_0);
#undef _
}
最终的样子就是我们之前说的:
# 原方法的 前 12 个字节
FF 03 01 D1 SUB SP, SP, #0x40 ; '@'
E9 23 40 F9 LDR X9, [SP,#0x40]
E8 27 40 F9 LDR X8, [SP,#0x48]
# 跳回到 原方法的 12 字节后面的地址
51 00 00 58 LDR X17, =0x753EDCE894
20 02 1F D6 BR X17
Commit
最终 commit方法会吧 buffer 里面的指令影响到我们的内存,看看它是如何做到的。
void InterceptRouting::Commit() {
//1. 调用激活方法
this->Active();
}
void InterceptRouting::Active() {
//2. 通过 DobbyCodePatch 传入 patched 地址 trampoline_buffer 修改的buffer 进行修改内存
auto ret = DobbyCodePatch((void *)entry_->patched_addr, trampoline_buffer_->GetBuffer(),
trampoline_buffer_->GetBufferSize());
if (ret == -1) {
ERROR_LOG("[intercept routing] active failed");
return;
}
DEBUG_LOG("[intercept routing] active");
}
DobbyCodePatch
最终的 DobbyCodePatch 通过 mprotect 修改内存页属性,在通过 memcpy将修改跳转指令 复制到指定内存。
// address patched 地址 也就是 我们被hook方法的地址
// buffer* 里面存放的 adrp add br 的 12 字节指令
PUBLIC int DobbyCodePatch(void *address, uint8_t *buffer, uint32_t buffer_size) {
#if defined(__ANDROID__) || defined(__linux__)
// 1. 获取页大小
int page_size = (int)sysconf(_SC_PAGESIZE);
// 2. 获取 address 基于页大小 对齐的地址
uintptr_t patch_page = ALIGN_FLOOR(address, page_size);
....
// 修改页的权限是 rwc 可读 可写 可执行
mprotect((void *)patch_page, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);
...
// 把 buffer 中的指令复制到内存中
memcpy(address, buffer, buffer_size);
....
....
return 0;
}
其中两个重要方法:
//从 src 复制 n 大小的数据到 dest 里面
void *memcpy(void *restrict dest, const void *restrict src, size_t n);
// 设置 addr 到 addr + len 长度的页内存属性
// 其中 r 代表可读 w 可写 x 可执行
//对应着 PROT_READ | PROT_WRITE | PROT_EXEC
int mprotect(void *addr, size_t len, int prot);
手写 inline hook 准备工作
由于dobby 是通过 计算地址偏移 和 插入字节码完成的,这种方式目前对于我来说过于麻烦。
所以为了方便,我只对动态计算地址的汇编通过计算字节码外,其余全部通过 gas 汇编文件
后续我将通过如下流程图进行我们的 inline hook 的实现
</P>
</p>
ADRP
由于 adrp 计算目标地址会导致字节码的变化,所以我们并不能使用固定的字节码来完成这个功能。
所以我们需要知道 adrp 字节码如何计算的
下图是 arm architecture reference manual 的 arm 架构参考手册中对 ADRP 的介绍:
</p>
</p>
大致意思是说
生成基于PC的4KB内存页对齐的相对地址。
什么意思呢?
就是您输入的立即数(就是目标地址) 抹掉 12位 的 4kb内存页,相对于 PC 的 4kb 内存页的地址。
不懂的在看看 dobby 是如何计算的
//rd 是寄存器 from 是 origin to 是 trampline_target
void AdrpAdd(Register rd, uint64_t from, uint64_t to) {
//获取 from 对齐的内存页
uint64_t from_PAGE = ALIGN(from, 0x1000);
// 获取 to 对齐的内存页
uint64_t to_PAGE = ALIGN(to, 0x1000);
// 获取 to 基于对齐内存页的偏移
uint64_t to_PAGEOFF = (uint64_t)to % 0x1000;
// rd = to_PAGE - from_PAGE
adrp(rd, to_PAGE - from_PAGE);
// rd = rd + to_PAGEOFF
add(rd, rd, to_PAGEOFF);
}
在看看这张图:
</p>
</p>
其中 imm 表示立即数,也就是我们输入的目标地址。
immlo 表示 低位
immhi 表示 高位
RD 表示 寄存器
换算的话就是:
31 是 1
29-30 是 立即数的最后两位
24-28 是 10000 固定的5位
5-23 是 立即数的剩余高位的数据
0-4 是 寄存器
为了您更好得能够理解意思,下面是一段从内存中摘取得指令片段
0000007B70BA4EE8 E0 00 00 F0 ADRP X0, #origin_jump_address@PAGE
0000007B70BA4EEC 00 00 40 F9 LDR X0, [X0,#origin_jump_address@PAGEOFF]
0000007B70BA4EF0 00 00 1F D6 BR X0
-------------------------------
0000007B70BC3000 00 00 00 00 00 00 00 00 origin_jump_address DCQ
其中 ADRP X0, #origin_jump_address@PAGE
得字节码是 E0 00 00 F0 由于是小端排序,所以我们通过 F0 00 00 E0转换二进制就是
b1111 0000 0000 0000 0000 0000 1110 0000
op = b1
immlo = b11
adrp = b10000
immhi = b0000 0000 0000 0000 111
rd = b0 0000
我们将 immhi 和 immlo组合起来
immhi:immlo = b0000 0000 0000 0000 11111 = 0x1F
可以看到这个立即数是 0x1f
如何得到的呢?
注意下面的两个地址
0000007B70BA4EE8 ADRP X0, #origin_jump_address@PAGE
0000007B70BC3000 origin_jump_address DCQ
看看是如何换算的:
0000007B70BA4EE8 & (~ (0x1000-1) ) = 7B 70BA 4000
0000007B70BC3000 & (~ (0x1000-1) ) = 7B 70BC 3000
7B 70BC 3000 - 7B 70BA 4000 = 1 F000
1 F000 >> 12 = 1F
也就说 ADRP x0,imm 中的 imm 需要的是 目标地址和PC 地址的 4kb 内存对齐的 偏移。 偏移结果抹掉 12 位的
最终 x0 实际就是 目标地址的 4kb 内存对齐地址
随后我们加上 目标地址 距离 目标地址内存页的偏移
LDR X0, [X0,#origin_jump_address@PAGEOFF]
origin_jump_address@PAGEOFF 换算方式 可以使用 (uint64_t)to % 0x1000
得到偏移。
准备assembler
自己手写指令 转 字节码过于麻烦。我们复制一下 dobby的assembler 代码
源码在: source/core/assembler/assembler-arm64.h
中
我们稍作修改,只用返回 int32 数据就行,因为 每个汇编指令是 4 字节 正好对应 int类型
下面代码实际含义自己研究,或者不研究也行,您只记得返回操作码就行。
// 不会重复引用
#pragma once
#include <stdint.h>
// 左移 右移
#define LeftShift(a, b, c) ((a & ((1 << b) - 1)) << c)
#define RightShift(a, b, c) ((a >> c) & ((1 << b) - 1))
// 用于截取二进制
#define submask(x) ((1L << ((x) + 1)) - 1)
#define bits(obj, st, fn) (((obj) >> (st)) & submask((fn) - (st)))
// RN RD 位置不同 下面是定义属于移动的位数
enum InstructionFields
{
// Registers.
kRdShift = 0,
kRdBits = 5,
kRnShift = 5,
kRnBits = 5,
kRaShift = 10,
kRaBits = 5,
kRmShift = 16,
kRmBits = 5,
kRtShift = 0,
kRtBits = 5,
kRt2Shift = 10,
kRt2Bits = 5,
kRsShift = 16,
kRsBits = 5,
};
#define Rd(rd) (rd << kRdShift)
#define Rt(rt) (rt << kRtShift)
#define Rt2(rt) (rt << kRt2Shift)
#define Rn(rn) (rn << kRnShift)
#define Rm(rm) (rm << kRmShift)
#define OPT_X(op, attribute) op##_x_##attribute
enum AddSubImmediateOp
{
AddSubImmediateFixed = 0x11000000,
#define AddSubImmediateOpSub(sf, op, S) \
AddSubImmediateFixed | LeftShift(sf, 1, 31) | LeftShift(op, 1, 30) | LeftShift(S, 1, 29)
OPT_X(ADD, imm) = AddSubImmediateOpSub(1, 0, 0),
};
enum PCRelAddressingOp {
PCRelAddressingFixed = 0x10000000,
ADRP = PCRelAddressingFixed | 0x80000000
};
// 得到操作码指令
class AssemblerBase
{
public:
// adrp
uint32_t adrp(int32_t rd, int64_t imm)
{
//右移12 到4kb内存页位置
//bits 宏定义方法用于截取二进制位数 求得 immlo immhi
//LeftShift 左移指定位数
uint32_t immlo = LeftShift(bits(imm >> 12, 0, 1), 2, 29);
uint32_t immhi = LeftShift(bits(imm >> 12, 2, 20), 19, 5);
uint32_t opcode = ADRP | Rd(rd) | immlo | immhi;
return opcode;
}
// 立即数 add
uint32_t add(int32_t rd, const int32_t rn, int64_t imm)
{
int32_t imm12 = LeftShift(imm, 12, 10);
uint32_t op = OPT_X(ADD, imm);
uint32_t opcode = op | Rd(rd) | Rn(rn) | imm12;
return opcode;
}
//[0]是adrp [1]是add指令
uint32_t* AdrpAdd(int32_t rd,uint64_t from, uint64_t to){
uint64_t from_PAGE = from & ~(0x1000 - 1);
uint64_t to_PAGE = to & ~(0x1000 - 1);
uint64_t to_PAGEOFF = (uint64_t)to & (0x1000 - 1);
uint32_t* opcodes = new uint32_t[2];
//to 到 pc 的 4kb内存对齐地址
opcodes[0] = adrp(rd,to_PAGE - from_PAGE);
//to 的内存对齐地址 到 to 的偏移
opcodes[1] = add(rd,rd,to_PAGEOFF);
return opcodes;
}
};
下面是测试效果:
#include <stdio.h>
#include <stdint.h>
#include "Assembler.h"
int main(int argc, char const *argv[])
{
AssemblerBase assembler;
uint64_t from = 0x0000007B70BA4EE8;
uint64_t to = 0x0000007B70BC3000;
uint32_t* opcodes = assembler.AdrpAdd(0,from,to);
printf("adrp_op:%02X %02X %02X %02X\n"
,(uint8_t)RightShift(opcodes[0],8,0)
,(uint8_t)RightShift(opcodes[0],8,8)
,(uint8_t)RightShift(opcodes[0],8,16)
,(uint8_t)RightShift(opcodes[0],8,24)
);
return 0;
}
输出结果也是正确的:
adrp_op:E0 00 00 F0