android ebpf之uprobe原理和检测方法

发布时间 2023-11-01 20:52:24作者: 怎么可以吃突突

uprobe通过内核层对用户层进程的指定地址的原指令copy到其他位置,然后写入指定类型中断指令,然后内核中设置对应的中断处理程序,中断处理程序中执行uprobe设置的回调过滤函数,然后设置单步执行copy的原指令后恢复寄存器状态继续执行。ida查看被uprobehook的函数头部,指令被修改为了中断指令BRK #5

uprobe原理源码分析

uprobe注册

分为两种情况:一种是对已经创建的进程,一种是对新创建的进程。对于已经创建的进程而言,当通过ebpf附加回调函数到uprobe时,内核最终会调用__uprobe_register。此函数调用alloc_uprobe申请一个struct uprobe结构体保存hook的目标函数偏移以及设置的uprobe回调函数等信息,接着传给register_for_each_vma

register_for_each_vma中先调用find_vmam_struct mm进程内存描述符中找到需要hook的地址对应的线性内存描述符vm_area_struct *vma,之后valid_vma验证线性内存描述符的权限,含有WRITE可写权限的text段将验证失败。如果验证成功则会调用install_breakpoint将目标hook地址的指令先保存,然后向对应的线性地址(虚拟地址)写入断点指令(AARCH64_BREAK_MON | (UPROBES_BRK_IMM << 5)) = 0xd4200000 | ( 5 << 5)= 0xd42000A0

第二种情况是新创建的进程,当进程的text段被map到进程空间时,会依次调用mmap_region-->vma_merge-->__vma_adjust-->uprobe_mmapuprobe_mmap同样会调用valid_vma验证线性内存描述符的权限,验证成功后调用install_breakpoint将目标hook地址的指令先保存,然后向对应的线性地址(虚拟地址)写入断点指令0xd42000A0(arm64平台)。

uprobe设置中断处理程序

uprobe在内核初始化的时候调用register_user_break_hook注册一个.imm = UPROBES_BRK_IMM = 5 的中断处理程序uprobe_breakpoint_handler和一个单步异常处理函数uprobe_single_step_handler

register_user_break_hook 调用register_debug_hookUPROBES_BRK_IMM中断处理程序加入到user_break_hook链表中

当用户层进程执行到BRK #5指令是会触发中断异常,最后内核中会去执行断点异常处理程序brk_handlerbrk_handler会调用call_break_hookcall_break_hook回去判断是来自用户层还是内核层的异常,如果是用户层的就会获取user_break_hook链表中指定imm对应的中断处理程序,当imm = 5时对应的中断处理程序就是之前设置的uprobe_breakpoint_handler

imm = 5的中断处理程序uprobe_breakpoint_handler会调用uprobe_pre_sstep_notifier,后者会给当前线程设置TIF_UPROBE标志

uprobe触发回调函数

BRK #5中断处理程序执行完之后会调用ret_to_user从内核层返回用户层,此函数进一步调用do_notify_resume

do_notify_resume会检测线程的标志,如果发现TIF_UPROBE的话会去调用uprobe_notify_resume

uprobe_notify_resume第一次调用的时候utask是空的,所以回去调用handle_swbp

handle_swbp先调用handler_chain,此函数会去执行uprobe中保存的回调函数,也就是执行在ebpf附加uprobe时设置的回调函数。执行完回调函数后会调用pre_ssout

pre_ssout会先去调用xol_get_insn_slotarch_uprobe_pre_xol,下面分别看一下这两个函数。

经过如下调用链xol_get_insn_slot-->get_xol_area-->__create_xol_area,查看__create_xol_area函数。

  1. 调用alloc_page(GFP_HIGHUSER)申请4kb物理页
  2. uprobe中保存的hook之前用户层的原始指令copy到这个物理页中
  3. 调用xol_add_vma为此物理页创建名称为[uprobes]用户层map,并将此用户层map的线性地址(虚拟地址)加入到进程内存描述符m_struct mm的线性地址描述符二叉树中(VAD树)。

xol_add_vma 会尝试在较高的用户地址空间创建这个名称为[uprobes]map,然后设置其属性为 VM_EXEC|VM_MAYEXEC|VM_DONTCOPY|VM_IO,这里注意具有VM_IO属性的内存,通过ptrace是无法访问的,因为ptrace内部有权限判断,如果目标内存具有VM_IO属性操作会被拒绝。

pre_ssout调用完xol_get_insn_slot之后会调用arch_uprobe_pre_xol

  1. arch_uprobe_pre_xol函数会将保存的寄存器环境中的ip设置为[uprobes]中保存原始指令的地址,同时设置单步执行
  2. arch_uprobe_pre_xol函数调用完后,设置uprobe_taskactive_uprobestate

这样在执行完原始指令后会返回到内核中调用单步异常处理函数uprobe_single_step_handleruprobe_single_step_handler会调用uprobe_post_sstep_notifier设置线程标志TIF_UPROBE

因为设置了线程标志TIF_UPROBE,所以在内核返回到用户层之前还会调用ret_to_user-->do_notify_resume-->uprobe_notify_resume,不同的是这次active_uprobeutask都是在设置单步异常之前调用handle_swbp创建好的,因此这次是执行handle_singlestep函数而不是handle_swbp

handle_singlestep调用arch_uprobe_post_xol将单步异常的返回地址设置为之前执行BRK #5指令的返回地址,即hook地址的下一条指令对应的地址。接着进行一些资源的清理并将active_uprobe置空,最后会返回到用户层继续执行BRK #5后面剩余的指令

uprobe检测

检测断点指令

uprobe会将hook地址的指令修改为BRK #5,对应的字节码为A0 00 20 D4,可以对关键的函数进行字节码扫描,或者crc校验。

检测[uprobes]

uprobe会将hook点原始指令放在名称为[uprobes]名称的map中,通过扫描map表查看是否有名称为[uprobes]的虚拟内存。

为text段增加可写属性

因为在注册uprobe的时候其会调用valid_vma判断目标地址所在的代码段是否具有VM_WRITE可写属性,如果有的话就拒绝注册。通过给so的代码段增加可写属性可以阻止uprobe设置hook。

因为在android平台从android 8.0开始就不支持加载具有可写属性的代码段,其是通过linker程序对此增加的限制,在linker调用ElfReader::LoadSegments加载so文件的各个段时,如果判断一个段同时具有PROT_EXEC | PROT_WRITE属性就会拒绝加载。

ElfReader::LoadSegments如果判断段的属性没问题会通过调用mmap64将段加载到内存,可以通过前驱so 去 hook libcmmap64并在加载受保护so加载的时候增加可写属性。

方法二和三都是可以修改内核规避的,修改map名称,去除对WRITE属性的判断。方法一如果crc检测逻辑和扫描逻辑被定位到也是可以绕过,所以应该尽量做的隐蔽点。

以上均为个人观点,仅供参考。
参考:
https://blog.csdn.net/sinat_32960911/article/details/133807486
https://blog.csdn.net/feelabclihu/article/details/105872886