美文网首页
hook原理小结

hook原理小结

作者: M_天河 | 来源:发表于2020-09-29 17:52 被阅读0次

常用的hook方式主要有导入表hook、导出表hook和inline hook三种。

一,导入表hook

首先需要了解延时重定位的过程,下面这张图很好的解释了整个过程



即程序调用目标函数时,函数地址才会被运行时函数解析出来写到got表中。
导入表hook就是基于修改got实现对目标函数hook的,同时导入表hook也是基于ptrace注入的,前面在总结注入的时候有个案例,将包含hello函数的so注入到target进程中,这里需要将包含修改got表的函数注入到target中;

got表的修改,原理很简单,主要是got表定位的过程有很多方法,可以根据自己喜好,通过dlopen函数打开so文件,返回的是solist信息,这里可以直接解析出so文件的section中的字符串表的索引,然后通过对比section的名字去确定got位置,因为我们注入的时机已经加载过目标函数了,所以目标函数的地址是已经重定位过的,可以直接调用目标函数从而获取目标函数在内存中的地址,然后去got表中对比,命中后直接修改即可。

这里有个想法没有实际验证过,就是如果我们注入的实际更早一些,got表没有进行重定位,我们可以通过动态加载段去找到动态字符串和hash表,然后计算出目标函数在got表中的地址,然后直接修改该地址应该也是可以的。

这样的hook原理实际上是有很大局限性的:

1,比如,目标函数不在目标进程中的时候,目标进程通过dlopen去打开其他so,然后dlsym调用目标函数的时候,我们对目标进程的got表修改是无效的;
2,对于so内部自定义的函数也是无法修改的;
3,修改got表后影响的是整个目标进程,无法精确到hook某次调用;

二,导出表hook

符号表中每个符号的结构如下:

typedef struct elf32_sym{
    Elf32_Word    st_name;
    Elf32_Addr    st_value;
    Elf32_Word    st_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half    st_shndx;
} Elf32_Sym;

其中的st_info中包含type字段,用STB_GLOBAL、STB_LOCAL和STB_WEAK等字段来标识是全局符号还是本地符号。


然后来看下linker加载so的过程,http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp
源码中可以看到,linker将so加载到内存之后,会符号进行重定位,通过检测符号中的st_info字段判断外部符号还是本地符号,如果是外部符号就会去解析NEEDED获取外部符号的地址,
if (reloc == sym_addr) {
                Elf32_Sym *src = soinfo_do_lookup(NULL, sym_name, &lsi, needed);

返回Elf32_Sym,从这个Elf32_Sym中的st_value字段得到函数的虚地址。

typedef struct {
 Elf32_Word  st_name;    /* Symbol name (.strtab index) */
 Elf32_Word  st_value;   /* value of symbol */
 Elf32_Word  st_size;    /* size of symbol */
 Elf_Byte    st_info;    /* type / binding attrs */
 Elf_Byte    st_other;   /* unused */
 Elf32_Half  st_shndx;   /* section index of symbol */
} Elf32_Sym;

那么修改NEEDED 中的这个符号 st_value 字段,即可实现导出表 HOOK。

流程如下:以 libc.so 中的 unlink 函数为例

  1. 注入 zygote 进程;
  2. dlopen libc.so,找到unlink符号;
  3. 解析此符号,得到其st_value地址;
  4. 修改此地址的值为:NewFunc – BaseAddr(libc.so 加载的基地址)

第四步中之所以要改成偏移地址而不是绝对地址是是因为st_value保存的本身就是是偏移地址;
导出表 HOOK的局限性就在于只能HOOK导出的符号。

三,inline hook

inline hook的原理是在在汇编指令层做修改,下面这两张图很好的解释了具体操作,其中第二张更详细一些:



一,汇编指令构造:

说到跳转指令我们首先想到的是B指令,但是在32位arm中,地址占4字节,B指令跳转范围是有限的,因此我们还可以使用LDR指令将立即数赋值给pc寄存器的方式:

原指令
修改后指令

其中修改后的第二条指令是目标函数的地址,这是是默认识别成了指令,实际并没有这条汇编指令,

修改后的第一条指令ldr是其实原指令应该是ldr pc,[pc,#-4]这样就看出了ldr指令寻址其实是通过第一个寄存器参数的偏移来寻址的,并不是直接跳转到某地址,这就解释了为什么mov、b、bl、ldr等指令寻址是有范围的了,这里为什么是#-4而不是```#4``,这与汇编指令的三级流水线相关,执行第一条指令的时候实际上pc指向的第三条指令,所以是减4而不是加4。

在构造汇编指令时可以使用https://armconverter.com/将汇编代码转化成16进制,https://onlinedisassembler.com/odaweb/将16进制转换成汇编代码。
实现代码为:

// 设置bit[0]的值为1
#define SET_BIT0(addr)      (addr | 1)
// 设置bit[0]的值为0
#define CLEAR_BIT0(addr)    (addr & 0xFFFFFFFE)
// 测试bit[0]的值,若为1则返回真,若为0则返回假
#define TEST_BIT0(addr)     (addr & 1)
if (TEST_BIT0(item->target_addr)) {
    int i;
    i = 0;
    if (CLEAR_BIT0(item->target_addr) % 4 != 0) {
        ((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xBF00;  // NOP
    }
    ((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF8DF;
    ((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF000; // LDR.W PC, [PC]
    ((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr & 0xFFFF;
    ((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr >> 16;
}
二,inst1指令修正

上面再构造跳转指令的时候覆盖了两条指令,所以在目标函数运行结束后还是需要恢复的,但是,如果要恢复的这两条指令是包含PC寄存器的,那么需要注意,此时的pc寄存器的值与原环境中的pc寄存器的值是不一样的,需要针对不同的指令做不同的修复。

首先,对涉及pc寄存器操作的指令进行分类:

static int getTypeInArm(uint32_t instruction)
{
    if ((instruction & 0xFE000000) == 0xFA000000) {
        return BLX_ARM;
    }
    if ((instruction & 0xF000000) == 0xB000000) {
        return BL_ARM;
    }
    if ((instruction & 0xF000000) == 0xA000000) {
        return B_ARM;
    }
    if ((instruction & 0xFF000FF) == 0x120001F) {
        return BX_ARM;
    }
    if ((instruction & 0xFEF0010) == 0x8F0000) {
        return ADD_ARM;
    }
    if ((instruction & 0xFFF0000) == 0x28F0000) {
        return ADR1_ARM;
    }
    if ((instruction & 0xFFF0000) == 0x24F0000) {
        return ADR2_ARM;        
    }
    if ((instruction & 0xE5F0000) == 0x41F0000) {
        return LDR_ARM;
    }
    if ((instruction & 0xFE00FFF) == 0x1A0000F) {
        return MOV_ARM;
    }
    return UNDEFINE;
}

1, 若是B系列指令,包括b、bx、bl、blx等指令,


if (type == BLX_ARM || type == BL_ARM || type == B_ARM || type == BX_ARM) {
            uint32_t x;
            int top_bit;
            uint32_t imm32;
            uint32_t value;

            if (type == BLX_ARM || type == BL_ARM) {
                trampoline_instructions[trampoline_pos++] = 0xE28FE004; // ADD LR, PC, #4
            }
            trampoline_instructions[trampoline_pos++] = 0xE51FF004;     // LDR PC, [PC, #-4]
            if (type == BLX_ARM) {
                x = ((instruction & 0xFFFFFF) << 2) | ((instruction & 0x1000000) >> 23);
            }
            else if (type == BL_ARM || type == B_ARM) {
                x = (instruction & 0xFFFFFF) << 2;
            }
            else {
                x = 0;
            }
            
            top_bit = x >> 25;
            imm32 = top_bit ? (x | (0xFFFFFFFF << 26)) : x;
            if (type == BLX_ARM) {
                value = pc + imm32 + 1;
            }
            else {
                value = pc + imm32;
            }
            trampoline_instructions[trampoline_pos++] = value;
            
        }

2,若是LDR、ADR、MOV等指令,同样首先解析指令,得到value,然后用于构造修复指令,代码如下:

else if (type == ADD_ARM) {
            int rd;
            int rm;
            int r;
            
            rd = (instruction & 0xF000) >> 12;
            rm = instruction & 0xF;
            
            for (r = 12; ; --r) {
                if (r != rd && r != rm) {
                    break;
                }
            }
            
            trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12); // PUSH {Rr}
            trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8]
            trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);
            trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12); // POP {Rr}
            trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
            trampoline_instructions[trampoline_pos++] = pc;
        }
        else if (type == ADR1_ARM || type == ADR2_ARM || type == LDR_ARM || type == MOV_ARM) {
            int r;
            uint32_t value;
            
            r = (instruction & 0xF000) >> 12;
            
            if (type == ADR1_ARM || type == ADR2_ARM || type == LDR_ARM) {
                uint32_t imm32;
                
                imm32 = instruction & 0xFFF;
                if (type == ADR1_ARM) {
                    value = pc + imm32;
                }
                else if (type == ADR2_ARM) {
                    value = pc - imm32;
                }
                else if (type == LDR_ARM) {
                    int is_add;
                    
                    is_add = (instruction & 0x800000) >> 23;
                    if (is_add) {
                        value = ((uint32_t *) (pc + imm32))[0];
                    }
                    else {
                        value = ((uint32_t *) (pc - imm32))[0];
                    }
                }
            }
            else {
                value = pc;
            }
                
            trampoline_instructions[trampoline_pos++] = 0xE51F0000 | (r << 12); // LDR Rr, [PC]
            trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
            trampoline_instructions[trampoline_pos++] = value;
        }

3,理论上其他指令也可能用到pc寄存器操作,但实际中没有,这类我们不做考虑,故默认其他的指令都不涉及pc寄存器操作,直接将原指令写入即可,但是如果遇到bug分析的时候不要忘了这一点。

4,上面讲的是ARM指令环境下的hook,但是系统中还会遇到thumb指令,对于thumb指令的构造和修复,原理与arm相同,只是有些地方需要注意:

  1. Thumb 模式并没有能表跳转任意地址的指令,只能切换到 ARM 状态再进行跳转。
  2. 为了尽可能的 减少替换的指令数,状态切换应尽快,这里采用BX PC
  3. ARM 指令是 4 字节对齐,BX PC状态切换时,必须保证跳转到的地址为4字节对齐。
  4. 由于 T2 指令占 4 字节,如果被替换指令的最后一条为T2指令,且T2指令的前2字节处于被替换指令中,而后2字节未处于其中时,也是需要将后2字节归入被替换的指令中作为一个整体。

有了上面的分析,指令的替换流程如下:

  1. 判定其实地址是否 4 字节对齐,如果不为 4 字节对齐,则 BX PC 之前构造 NOP 指令
  2. 由于预取 2 条指令,BX PC之后2字节填充 NOP
  3. 构造ARM LDR指令,占4字节。其后4字节存放跳转绝对地址
  4. 判定被替换指令最后2字节是否为T2指令的前2两字节,如果是,则还需把之后2字节加入替换指令中,后2字节用Thumb NOP填充。

参考:
项目源码:Ele7enxxh大神的源码,github收藏地址
TK大神的《SO Hook 技术汇总》
游戏安全实验室的Android平台inline hook实现

相关文章

  • hook原理小结

    常用的hook方式主要有导入表hook、导出表hook和inline hook三种。 一,导入表hook 首先需要...

  • iOS逆向 ---- Hook方法及原理OC篇

    iOS逆向 ---- Hook方法及原理OC篇[iOS逆向 ---- Hook方法及原理OC篇](阅读原文

  • 005——HOOK原理

    HOOK概述 HOOK(钩子)其实就是改变程序执行流程的一种技术的统称!HOOK原理 IOS中HOOK技术的几种方...

  • Android Hook 系列教程(一) Xposed Hook

    章节内容 一. Android Hook 系列教程(一) Xposed Hook 原理分析二. Android H...

  • Go语言一个轻便的实时日志类似slack收集应用

    wslog原理 利用github.com上无数的slack hook 日志工具sdk 遵循 slack hook ...

  • iOS逆向之HOOK原理

    iOS逆向之HOOK原理 HOOK概述 HOOK(钩子) 其实就是改变程序执行流程的一种技术的统称!image.p...

  • iOS逆向学习笔记 - HOOK 原理

    一、HOOK概述 HOOK(钩子) 其实就是改变程序执行流程的一种技术的统称! 009--HOOK原理 一、HOO...

  • HOOK原理

    009--HOOK原理 一、HOOK概述 HOOK(钩子) 其实就是改变程序执行流程的一种技术的统称! iOS中H...

  • HOOK原理

    一、HOOK概述 HOOK(钩子)其实就是改变程序执行流程的一种技术的统称! 二、iOS中HOOK技术的集中方式 ...

  • HOOK原理

    hook(钩子)处理特殊的消息机制 iOS中HOOK技术的几种方式 1、Method Swizzle利用OC的Ru...

网友评论

      本文标题:hook原理小结

      本文链接:https://www.haomeiwen.com/subject/hqwwjktx.html