type
status
date
slug
summary
tags
category
icon
password
 
 

0x00 probe

 

1. 概述

Uprobes(用户空间探针)是Linux内核的一个特性,它允许在任何用户空间程序的任何指令上设置钩子(hook)。当这些钩子被触发时,会创建一个事件,并将被探测程序的上下文(例如,CPU寄存器的值)提供给处理程序,这些处理程序可以是eBPF(扩展的Berkeley Packet Filter)程序。通过这种方式,你可以记录程序执行时的详细信息,或者执行特定的eBPF程序来分析或修改程序行为。
例如,Quarkslab开发的peetch工具集就利用了uprobes和eBPF技术。它通过在OpenSSL库的SSL_read()SSL_write()函数上设置uprobes钩子,来全系统地记录TLS(传输层安全协议)消息。当这些函数被调用时,uprobes机制会触发,并允许eBPF程序访问这些函数的上下文,包括可以读取或修改CPU寄存器的值。这样,peetch能够在TLS加密之前或解密之后捕获数据,实现对TLS消息的明文访问。
这种技术的应用场景包括但不限于安全研究、性能分析、调试以及监控和审计系统行为。uprobes提供了一种强大的机制来深入理解和控制运行中的程序,而eBPF则扩展了这种能力,允许开发者编写高效且灵活的程序来处理uprobe生成的事件。
可以通过向/sys/kernel/debug/tracing/uprobe_events 中添加内容进行挂钩
例如假设要监听/bin/bash的readline调用,执行下面的命令即可
通过读取/sys/kernel/tracing/error_log查看错误日志 
同样也可以通过bcc等脚手架使用uprobe挂钩各种调用
 

2. 实现

在设置 uprobes 时,内核会调用 probes_write() 和 trace_uprobe_create(),它们又调用 __trace_uprobe_create()。(在5.10版本的源码中没有 __trace_uprobe_create 的调用,而是直接调用 trace_uprobe_create,其实现是一致的)最后一个函数以 uprobe_events 中的一行作为参数,并调用 kern_path() 获取与所设置二进制文件的路径相对应的文件的 inode。随后,register_trace_uprobe()_add_event_to_tracers() 和其他函数创建了伪目录 /sys/kernel/tracing/events/uprobes/<EVENT>/,以及一些文件(enableid 等)。
当启用 uprobes 时,会发生以下嵌套调用:trace_uprobe_register() => probe_event_enable() => trace_uprobe_enable() => uprobe_register()
当探针被注册时,除了访问引用计数(access refcount)外,__uprobe_register 还会通过 alloc_uprobe 增加一个创建引用计数(creation refcount),但这只在探针被插入红黑树(即该 @inode:@offset 对的第一个消费者)时发生。创建引用计数防止在注册操作完成之前 uprobe_unregister 函数释放 @uprobe
alloc_uprobe()  :这个函数创建一个 struct uprobe 实例。struct uprobe 是一个结构体,用于存储探针的相关信息,包括要放置探针的文件(inode)、在文件中的偏移量(offset),以及被替换的指令。创建的 struct uprobe 实例包含了探针需要的所有信息,以便将来使用。
insert_uprobe() :这个函数将创建的 struct uprobe 实例添加到一个红黑树(rb_tree)中。这个红黑树用于存储所有的探针实例,以便高效地查找和管理。
register_for_each_vma() :这个函数遍历所有现有的虚拟内存区域(VMA),寻找与某个探针的 inode对应的 VMA。对于每个找到的对应 VMA,valid_vma函数会检查它是否是一个有效的 VMA。
install_breakpoint() :在确认 VMA 有效后,这个函数会在 arch.insn 结构体中复制完整的被探测指令。arch.insn 是一个依赖于当前架构的结构体,用于存储指令信息。然后,它会将原指令替换为一个断点ret = set_swbp(&uprobe->arch, mm, vaddr);set_swbp - store breakpoint at a given address.。这样,当程序执行到这个位置时,执行会暂停。
然后需要了解当一个新的ELF(Executable and Linkable Format)程序被执行时,其内存是如何通过mmap系统调用被映射的过程,以及uprobes(用户空间探针)是如何被添加到新的程序实例中的。
mmap系统调用的入口点,用于请求内核将文件或设备的某个部分映射到内存中。
__vma_adjust()是一个辅助函数,用于在添加或修改VMA时调整VMA的属性。如果VMA是基于文件的(即代码段或数据段映射自一个文件),这个函数会调用uprobe_mmap(),这个函数专门处理与uprobes相关的VMA。如果VMA有效,build_probe_list()函数会查找与VMA关联的文件(inode)匹配的所有uprobes,并将它们组织成一个列表。对于列表中的每个uprobe,install_breakpoint会在程序的代码中安装一个断点。当程序执行到这个断点时,会触发一个事件,允许调试器或其他工具介入程序的执行。在ARM 64中,断点会被设置为A0 00 20 D4 BRK #5
notion image
综上,其实uprobe实现挂钩的基本原理还是指令替换,以实现断点。
当程序执行到设置了断点的地方时,会触发一个int3异常。内核通过do_int3()函数处理这个异常,并调用notify_die(DIE_INT3, …),随后调用atomic_notifier_call_chain(&die_chain, …)die_chain包含了通过register_die_notifier()函数注册的所有通知器。atomic_notifier_call_chain会调用notifier_call_chain(),通过它们的notifier_call属性通知链中注册的通知器发生了一个事件。对于我们的uprobe,这个事件是arch_uprobe_exception_notify(),这个函数在uprobe_init()时设置。它调用uprobe_pre_sstep_notifier()设置TIF_UPROBE标志。当线程返回用户空间时,会注意到TIF_UPROBE标志,并调用uprobe_notify_resume(struct pt_regs * regs),这个函数接着调用handle_swbp(regs)
handle_swbp这个函数主要做两件事:
  1. handler_chain(find_active_uprobe())执行这个uprobe的处理程序。例如,被eBPF程序使用的perf_event
  1. pre_ssout()准备单步执行被探测的指令。因为原始指令已经被uprobe的断点操作码替换,所以这个指令不能在程序内存中执行。内核开发者最初尝试临时移除断点,但遇到了一些问题,因此他们选择在一个新的内存区域(也称为xol,即out of line)执行这个指令。首先调用xol_get_insn_slot获取xol的虚拟地址,这个函数使用get_xol_area()来设置[uprobes]特殊的虚拟内存区域,如果它还没有被创建的话,通过xol_add_vma() => install_special_mapping()。这个vma是原始指令将要被out of line执行的地方。接着,pre_ssout()使用arch_uprobe_pre_xol()调用regs_set_return_ip(regs, current->utask->xol_vaddr)user_enable_single_step()。此时current->utask->xol_vaddr指向之前创建的XOL槽。因此,这个函数将程序计数器设置到原始指令的副本所在的位置,并激活单步模式。然后,这个指令将被执行,并且程序将再次被停止。
当单步执行结束后,从uprobe_notify_resume调用arch_uprobe_post_xol。这个函数准备在单步执行后恢复执行,并调用post_xol处理程序。在单步执行之后的RIP寄存器是相对于复制的指令地址的,指向的是在XOL区域中地址,而不是原本要指向的地址。接下来,内核需要调整RIP寄存器的值,使其指回原始指令之后的位置。如果原始指令使用了RIP寄存器(例如,用于计算相对地址的指令),内核可能需要使用另一个寄存器来替换RIP,以保持地址计算的正确性。以便程序可以正常继续执行。

0x01 对抗

 

1.断点检测

程序可以读取待检测函数的前几个字节,以检测某个函数是否被挂钩,但是这样显然是不现实的,因为uprobe可以实现任意位置的挂钩,不仅局限于某个函数的开头。除此之外,检测函数需要不间断的检测,其开销十分巨大。同样的也可以去强制覆写函数的开头取消断点以实现uprobe监控逃逸。
 

2. 内存映射检测

另一种方法是在触发 uprobe 后检测它。我们知道在创建指令副本用于执行原本的指令时,创建了一个特殊的内存映射,称为 [uprobes]。因此可以读取 /proc/self/maps,并搜索这些内容。
notion image

3. 监控逃逸

在进行uprobe注册时会寻找所有与inode关联的vma,并且通过valid_vma 检查是否是有效的vma,其实现如下,可以发现,如果is_register的标志为true ,则flags |= VM_WRITE 。在 uprobe 注册期间,is_register值为true,就会执行flags |= VM_WRITE,而如果代码段是可写的VM_WRITE, 在之后的返回语句中(vma->vm_flags & flags) == VM_MAYEXEC 就会不成立,从而无法继续添加断点。所以可以通过将代码段的属性添加VM_WRITE 就可以使uprobe无法正常添加断点,从而实现监控逃逸。
对于一般的进程可以设置自己的代码段为可写的,但是无法设置共享的动态链接库的属性,也就是说对共享的动态链接库(除非拥有root权限),比如libc等无法实现监控逃逸。但是可以自实现ELF加载器,在这些库加载到程序内存前设置权限,为每个库添加写属性。当然在这么操作存在很多问题,包括破坏程序原本的安全性等等。
 

4. 返回虚假信息

这个部分就不再赘述,一个方法是条件竞争,创建一个线程发送虚假数据。另一个方法就是使用ptrace实现对自身的调试,相当于实现了一个调试器,在关键位置修改上下文以实现返回虚假数据。
 

参考

  1. https://blog.quarkslab.com/defeating-ebpf-uprobe-monitoring.html
  1. https://www.cnxct.com/defeating-ebpf-uprobe-monitoring/
  1. https://zhuanlan.zhihu.com/p/466319667
 
相关文章
基于eBPF实现一个简单的隐蔽脱壳工具-eBPFDexDumper
Lazy loaded image
Frida Interceptor Hook实现原理图
Lazy loaded image
SystemUI As EvilPiP
Lazy loaded image
Android 悬浮窗覆盖攻击
Lazy loaded image
Magisk Eop本地提权漏洞
Lazy loaded image
CVE-2024-31317 Zygote命令注入提权system分析
Lazy loaded image
CVE-2024-31317 Zygote命令注入提权system分析Android Data Encryption-从百草园Patch到三味书屋
Loading...
LLeaves
LLeaves
Happy Hacking
最新发布
基于eBPF实现一个简单的隐蔽脱壳工具-eBPFDexDumper
2025-1-9
LakeCTF At your Service 题解
2024-12-13
PendingIntent-security
2024-12-1
Android grantUriPermission与StartAnyWhere
2024-11-30
eBPF实践之修改bpf_probe_write_user以对抗某加固Frida检测
2024-11-10
CVE-2024-31317 Zygote命令注入提权system分析
2024-11-10
公告