uprobe
通过内核层对用户层进程的指定地址的原指令copy到其他位置,然后写入指定类型中断指令,然后内核中设置对应的中断处理程序,中断处理程序中执行uprobe
设置的回调过滤函数,然后设置单步执行copy的原指令后恢复寄存器状态继续执行。ida
查看被uprobe
hook的函数头部,指令被修改为了中断指令BRK #5
uprobe原理源码分析
uprobe注册
分为两种情况:一种是对已经创建的进程,一种是对新创建的进程。对于已经创建的进程而言,当通过ebpf
附加回调函数到uprobe
时,内核最终会调用__uprobe_register
。此函数调用alloc_uprobe
申请一个struct uprobe
结构体保存hook的目标函数偏移以及设置的uprobe
回调函数等信息,接着传给register_for_each_vma
register_for_each_vma
中先调用find_vma
从m_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_mmap
。uprobe_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_hook
将UPROBES_BRK_IMM
中断处理程序加入到user_break_hook
链表中
当用户层进程执行到BRK #5
指令是会触发中断异常,最后内核中会去执行断点异常处理程序brk_handler
。brk_handler
会调用call_break_hook
,call_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_slot
和arch_uprobe_pre_xol
,下面分别看一下这两个函数。
经过如下调用链xol_get_insn_slot-->get_xol_area-->__create_xol_area
,查看__create_xol_area
函数。
- 调用
alloc_page(GFP_HIGHUSER)
申请4kb物理页 - 将
uprobe
中保存的hook之前用户层的原始指令copy到这个物理页中 - 调用
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
。
arch_uprobe_pre_xol
函数会将保存的寄存器环境中的ip
设置为[uprobes]
中保存原始指令的地址,同时设置单步执行
。arch_uprobe_pre_xol
函数调用完后,设置uprobe_task
的active_uprobe
和state
这样在执行完原始指令后会返回到内核中调用单步异常处理函数uprobe_single_step_handler
,uprobe_single_step_handler
会调用uprobe_post_sstep_notifier
设置线程标志TIF_UPROBE
因为设置了线程标志TIF_UPROBE
,所以在内核返回到用户层之前还会调用ret_to_user-->do_notify_resume-->uprobe_notify_resume
,不同的是这次active_uprobe
和utask
都是在设置单步异常之前调用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 libc
的mmap64
并在加载受保护so加载的时候增加可写属性。
方法二和三都是可以修改内核规避的,修改map名称,去除对WRITE
属性的判断。方法一如果crc检测逻辑和扫描逻辑被定位到也是可以绕过,所以应该尽量做的隐蔽点。
以上均为个人观点,仅供参考。
参考:
https://blog.csdn.net/sinat_32960911/article/details/133807486
https://blog.csdn.net/feelabclihu/article/details/105872886