2016976438 发表于 2023-5-4 13:20

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

# luckfollowmearm学习篇11 - dobby源码探究



回顾之前 分析的 **dobby function inline hook**

它替换我们的方法的前 **12个字节** 用于跳转到 hook方法上

```asm
# 获取 hook方法的地址
adrp         x17, =当前PC内存对齐地址
add         x17, x17, #0xa28
# 绝对跳转 到 hook方法
BR        X17
```

并把原先方法的的12 个字节 转到下面的地方:

```asm
# 原方法的 前 12 个字节
FF 03 01 D1                   SUB             SP, SP, #0x40 ; '@'
E9 23 40 F9                   LDR             X9,
E8 27 40 F9                   LDR             X8,

# 跳回到 原方法的 12 字节后面的地址
51 00 00 58                   LDR             X17, =0x753EDCE894
20 02 1F D6                   BR            X17
```

接下来我们将模拟这个过程。



再次之前,我们得看看 dobby 的部分源码



## dobby 源码分析

我们主要看 **DobbyHook** 实现,它在 **source/InterceptRouting/Routing/FunctionInlineHook/FunctionInlineHook.cc** 下

```c
// 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 路由转发的关键类

它的结构如下:

```cpp
//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_** 中



```cpp
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)** 的指令

```cpp
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(" use ");
} else {
    // 6.否则就生成 ldr br 从内存中获取跳转
    // ldr, br, branch-address
    CodeGen codegen(&turbo_assembler_);
    codegen.LiteralLdrBranch((uint64_t)to);
    DEBUG_LOG(" use ");
}
#undef _

// Bind all labels
turbo_assembler_.RelocBind();
//7. 生成buffer 字节数据
auto result = turbo_assembler_.GetCodeBuffer()->Copy();
return result;
}
```

#### AdrpAdd

在看了下 dobby **adrp** 计算方式,我感觉我之前说错了,所以还是单独看下 dobby 如何通过 adrp 获取目标地址的吧。

```cpp
#define ALIGN ALIGN_FLOOR
// 通过 address & ~(2的幂次方 -1) 计算对齐地址
#define ALIGN_FLOOR(address, range) ((uintptr_t)address & ~((uintptr_t)range - 1))

//rd 是寄存器 from 是 originto 是 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 字节后面的地址

```cpp
//文件位于 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 = 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 _
}
```

最终的样子就是我们之前说的:

```asm
# 原方法的 前 12 个字节
FF 03 01 D1                   SUB             SP, SP, #0x40 ; '@'
E9 23 40 F9                   LDR             X9,
E8 27 40 F9                   LDR             X8,

# 跳回到 原方法的 12 字节后面的地址
51 00 00 58                   LDR             X17, =0x753EDCE894
20 02 1F D6                   BR            X17
```

#### Commit

最终 **commit**方法会吧 buffer 里面的指令影响到我们的内存,看看它是如何做到的。

```cpp
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(" active failed");
    return;
}
DEBUG_LOG(" active");
}
```

#### DobbyCodePatch

最终的 **DobbyCodePatch** 通过 **mprotect** 修改内存页属性,在通过 **memcpy**将修改跳转指令 复制到指定内存。

```cpp
// 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;
}
```

其中两个重要方法:

```cpp
//从 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** 是如何计算的

```cpp
//rd 是寄存器 from 是 originto 是 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 是 寄存器
```

为了您更好得能够理解意思,下面是一段从内存中摘取得指令片段

```asm
0000007B70BA4EE8 E0 00 00 F0                   ADRP            X0, #origin_jump_address@PAGE
0000007B70BA4EEC 00 00 40 F9                   LDR             X0,
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 内存对齐地址**

随后我们加上 **目标地址** 距离 **目标地址内存页**的偏移

```asm
LDR             X0,
```

**origin_jump_address@PAGEOFF** 换算方式 可以使用 ``(uint64_t)to % 0x1000``得到偏移。

### 准备assembler

自己手写指令 转 字节码过于麻烦。我们复制一下 dobby的**assembler** 代码

源码在: ``source/core/assembler/assembler-arm64.h`` 中

我们稍作修改,只用返回 **int32** 数据就行,因为 每个汇编指令是 **4 字节** 正好对应 **int类型**



下面代码实际含义自己研究,或者不研究也行,您只记得返回操作码就行。

```cpp
// 不会重复引用
#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;
    }
        //是adrp 是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;
      //to 到 pc 的 4kb内存对齐地址
      opcodes = adrp(rd,to_PAGE - from_PAGE);
      //to 的内存对齐地址 到 to 的偏移
      opcodes = add(rd,rd,to_PAGEOFF);
      return opcodes;
    }
};
```

下面是测试效果:

```cpp
#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,8,0)
    ,(uint8_t)RightShift(opcodes,8,8)
    ,(uint8_t)RightShift(opcodes,8,16)
    ,(uint8_t)RightShift(opcodes,8,24)
    );
    return 0;
}

```

输出结果也是正确的:

```
adrp_op:E0 00 00 F0
```

Poorwood 发表于 2023-5-16 11:04

我其实想一个方法,自己在app里,实现一个hook。自己hook自己,这样的话,就可以实现。
欸,我已经改掉某某方法了,为什么不起作用。可以起到一个简单的防范措施。问题就来了,怎么做呢?
我用的mmcp方法,直接复制对应的artmethod,然后把新的artmethod放进去。
成了,大部分的机型都能正常运行,但是少部分机型直接崩溃,我研究了半天,没搞懂为什么啊。{:1_909:}
大佬 你有什么思路没?

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

分析的很详细{:1_893:}

中原一点红 发表于 2023-5-6 00:43

虽然没看懂
还是过来支持下
感谢分享
{:17_1077:}

zhouzq709 发表于 2023-5-6 17:04

感谢分享
页: [1] 2
查看完整版本: luckfollowme arm学习篇11 - dobby源码探究