吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3832|回复: 11
收起左侧

[Android 原创] luckfollowme arm学习篇11 - dobby源码探究

  [复制链接]
2016976438 发表于 2023-5-4 13:20

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>

01.png

</p>

ADRP

由于 adrp 计算目标地址会导致字节码的变化,所以我们并不能使用固定的字节码来完成这个功能。

所以我们需要知道 adrp 字节码如何计算的

下图是 arm architecture reference manual 的 arm 架构参考手册中对 ADRP 的介绍:

</p>

02.png

</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>

03.png

</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

我们将 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 <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

免费评分

参与人数 4威望 +1 吾爱币 +24 热心值 +4 收起 理由
junjia215 + 1 + 1 用心讨论,共获提升!
云天逵 + 1 + 1 用心讨论,共获提升!
debug_cat + 2 + 1 用心讨论,共获提升!
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Poorwood 发表于 2023-5-16 11:04
我其实想一个方法,自己在app里,实现一个hook。自己hook自己,这样的话,就可以实现。
欸,我已经改掉某某方法了,为什么不起作用。可以起到一个简单的防范措施。问题就来了,怎么做呢?
我用的mmcp方法,直接复制对应的artmethod,然后把新的artmethod放进去。
成了,大部分的机型都能正常运行,但是少部分机型直接崩溃,我研究了半天,没搞懂为什么啊。
大佬 你有什么思路没?
debug_cat 发表于 2023-12-11 17:01
如果是这样,能不能读取本地so中对应函数符号的位置前面几个字节码,然后和内存加载的so对应函数符号字节码进行对比就可以发现有没有被hook?
466640010 发表于 2023-5-4 13:57
hushxh 发表于 2023-5-4 14:06
看不懂,想学都不知道怎么学
aFeng1188 发表于 2023-5-4 14:12
luckfollowme ar
hehengfa 发表于 2023-5-4 14:58
感谢分享
sunnyli1024 发表于 2023-5-4 20:35

感谢分享
myloverycpp 发表于 2023-5-5 11:50
分析的很详细
中原一点红 发表于 2023-5-6 00:43
虽然没看懂
还是过来支持下
感谢分享
zhouzq709 发表于 2023-5-6 17:04
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 09:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表