luckfollowme arm学习篇11 - dobby源码探究

查看 51|回复 4
作者:2016976438   
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 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 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(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 的实现


01.png (51.74 KB, 下载次数: 0)
下载附件
2023-5-4 13:19 上传

ADRP
由于 adrp 计算目标地址会导致字节码的变化,所以我们并不能使用固定的字节码来完成这个功能。
所以我们需要知道 adrp 字节码如何计算的
下图是 arm architecture reference manual 的 arm 架构参考手册中对 ADRP 的介绍:


02.png (85.86 KB, 下载次数: 0)
下载附件
2023-5-4 13:19 上传

大致意思是说
生成基于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);
}
在看看这张图:


03.png (12.75 KB, 下载次数: 0)
下载附件
2023-5-4 13:19 上传

其中 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
我们将 immhiimmlo组合起来
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
// 左移 右移
#define LeftShift(a, b, c) ((a & ((1 > c) & ((1 > (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 > 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
#include
#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

地址, 字节

466640010   

感谢大佬,学习了
hushxh   

看不懂,想学都不知道怎么学
aFeng1188   

luckfollowme ar
hehengfa   

感谢分享
您需要登录后才可以回帖 登录 | 立即注册

返回顶部