
目前絕大部分app都會頻繁的使用syscall去獲取設備指紋和做一些反調試,使用常規方式過反調試已經非常困難了,使用內存搜索svc指令已經不能滿足需求了,開始學習了一下通過ptrace/ptrace配合seccomp來解決svc反調試難定位難繞過等問題。
seccomp
Linux 2.6.12中的導入了第一個版本的seccomp,通過向/proc/PID/seccomp接口中寫入“1”來啟動通過濾器只支持幾個函數。
read(),write(),_exit(),sigreturn()
使用其他系統調用就會收到信號(SIGKILL)退出。測試代碼如下:
#include <fcntl.h>#include <stdio.h>#include <unistd.h>#include <sys/prctl.h>#include <linux/seccomp.h> void configure_seccomp() { printf("Configuring seccomp\n"); prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);}int main(int argc, char* argv[]) { int infd, outfd; if (argc < 3) { printf("Usage:\n\t%s <input path> <output_path>\n", argv[0]); return -1; } printf("Starting test seccomp Y/N?"); char c = getchar(); if (c == 'y' || c == 'Y') configure_seccomp(); printf("Opening '%s' for reading\n", argv[1]); if ((infd = open(argv[1], O_RDONLY)) > 0) { ssize_t read_bytes; char buffer[1024]; printf("Opening '%s' for writing\n", argv[2]); if ((outfd = open(argv[2], O_WRONLY | O_CREAT, 0644)) > 0) { while ((read_bytes = read(infd, &buffer, 1024)) > 0) write(outfd, &buffer, (ssize_t)read_bytes); } close(infd); close(outfd); } printf("End!\n"); return 0;}
可以看到執行到22行就結束了沒執行到 Eed.

seccomp-bpf
Seccomp-BPF(Berkeley Packet Filter)是Linux內核中的一種安全機制,用于限制進程對系統調用的訪問權限。它主要用于防止惡意軟件對系統的攻擊,提高系統的安全性。
Seccomp-BPF使用BPF(Berkeley Packet Filter)技術來實現系統調用過濾,可以使用BPF程序指定哪些系統調用可以被進程訪問,哪些不能。BPF程序由一組BPF指令組成,可以在系統調用執行之前對其進行檢查,以決定是否允許執行該系統調用。
Seccomp-BPF提供了兩種模式:白名單模式和黑名單模式。白名單模式允許所有系統調用,除非明確指定不允許的系統調用。黑名單模式禁止所有系統調用,除非明確指定允許的系統調用。這兩種模式的選擇取決于您的實際需求。
Seccomp-BPF提供了一個鉤子函數,在系統調用執行之前會進入到這個函數,對系統調用進行檢查,如果BPF程序允許執行該系統調用,則進程可以繼續執行,否則會拋出一個異常。
1.BPF確定了一個可以在內核內部實現的虛擬機,該虛擬機具有以下特性:
簡單指令集 小型指令集 所有的命令大小相一致 實現過程簡單、快速只有分支向前指令 程序是有向無環圖(DAGs),沒有循環易于驗證程序的有效性/安全性 簡單的指令集?可以驗證操作碼和參數 可以檢測死代碼 程序必須以 Return 結束 BPF過濾器程序僅限于4096條指令
2.Seccomp-BPF 使用的也只是BPF的子集功能:
Conditional JMP(條件判斷跳轉) 當匹配條件為真,跳轉到true指定位置 當 匹配條件為假,跳轉到false指定位置 跳轉偏移量最大255JMP(直接跳轉) 跳轉目標是指令偏移量 跳轉 偏移量最大255Load(數據讀取) 讀取程序參數 讀取指定的16位內存地址Store(數據存儲) 保存數據到指定的16位內存地址中支持的運算 + - * / & | ^ >> << !返回值 SECCOMP_RET_ALLOW - 允許繼續使用系統調用 SECCOMP_RET_KILL - 終止系統調用 SECCOMP_RET_ERRNO - 返回設置的errno值 SECCOMP_RET_TRACE - 通知附加的ptrace(如果存在) SECCOMP_RET_TRAP - 往進程發送 SIGSYS信號最多只能有4096條命令不能出現循環
Seccomp-BPF程序 接收以下結構作為輸入參數:
/** * struct seccomp_data - the format the BPF program executes over. * @nr: the system call number * @arch: indicates system call convention as an AUDIT_ARCH_* value * as defined in <linux/audit.h>. * @instruction_pointer: at the time of the system call. * @args: up to 6 system call arguments always stored as 64-bit values * regardless of the architecture. */struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 args[6];};
使用示例:
在這種情況下,seccomp-BPF 程序將允許使用 O_RDONLY 參數打開第一個調用 , 但是在使用 O_WRONLY | O_CREAT 參數調用 open 時終止程序。
#include <stdio.h>#include <fcntl.h>#include <unistd.h>#include <stddef.h>#include <sys/prctl.h>#include <linux/seccomp.h>#include <linux/filter.h>#include <linux/unistd.h> void configure_seccomp() { struct sock_filter filter [] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[1]))), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, O_RDONLY, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL) }; struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter) / sizeof (filter[0])), .filter = filter, }; printf("Configuring seccomp\n"); prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);} int main(int argc, char* argv[]) { int infd, outfd; ssize_t read_bytes; char buffer[1024]; if (argc < 3) { printf("Usage:\n\tdup_file <input path> <output_path>\n"); return -1; } printf("Ducplicating file '%s' to '%s'\n", argv[1], argv[2]); configure_seccomp(); //配置seccomp printf("Opening '%s' for reading\n", argv[1]); if ((infd = open(argv[1], O_RDONLY)) > 0) { printf("Opening '%s' for writing\n", argv[2]); if ((outfd = open(argv[2], O_WRONLY | O_CREAT, 0644)) > 0) { while((read_bytes = read(infd, &buffer, 1024)) > 0) write(outfd, &buffer, (ssize_t)read_bytes); } } close(infd); close(outfd); return 0;}

使用ptrace修改系統調用
將getpid()的實現改為mkdir()的實現。主要是通過ptrace函數來跟蹤子進程,獲取其寄存器中的信息,然后根據需求替換對應的系統調用。
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <unistd.h>#include <ctype.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/user.h>#include <sys/signal.h>#include <sys/wait.h>#include <sys/ptrace.h>#include <sys/fcntl.h>#include <syscall.h> void die (const char *msg){ perror(msg); exit(errno);} void attack(){ int rc; syscall(SYS_getpid, SYS_mkdir, "dir", 0777);} int main(){ int pid; struct user_regs_struct regs; switch( (pid = fork()) ) { case -1: die("Failed fork"); case 0: ptrace(PTRACE_TRACEME, 0, NULL, NULL); kill(getpid(), SIGSTOP); attack(); return 0; } waitpid(pid, 0, 0); while(1) { int st; ptrace(PTRACE_SYSCALL, pid, NULL, NULL); if (waitpid(pid, &st, __WALL) == -1) { break; } if (!(WIFSTOPPED(st) && WSTOPSIG(st) == SIGTRAP)) { break; } ptrace(PTRACE_GETREGS, pid, NULL, ®s); printf("orig_rax = %lld\n", regs.orig_rax); if (regs.rax != -ENOSYS) { continue; } if (regs.orig_rax == SYS_getpid) { regs.orig_rax = regs.rdi; regs.rdi = regs.rsi; regs.rsi = regs.rdx; regs.rdx = regs.r10; regs.r10 = regs.r8; regs.r8 = regs.r9; regs.r9 = 0; ptrace(PTRACE_SETREGS, pid, NULL, ®s); } } return 0;}
使用seccomp-bpf+ptrace加ptrace修改系統調用
看一下main函數這里設置了跟蹤openat系統調用子進程請求父進程附加 父進程開啟ptrace+seccomp。
1.main
int main(){ pid_t pid; int status; if ((pid = fork()) == 0) { /* 目前是跟蹤open系統調用 */ struct sock_filter filter[] = { BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 1), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_TRACE), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog = { .filter = filter, .len = (unsigned short) (sizeof(filter)/sizeof(filter[0])), }; //告訴父進程允許子進程跟蹤 ptrace(PTRACE_TRACEME, 0, 0, 0); /* 避免需要 CAP_SYS_ADMIN */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { perror("prctl(PR_SET_NO_NEW_PRIVS)"); return 1; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { perror("when setting seccomp filter"); return 1; } kill(getpid(), SIGSTOP); ssize_t count; char buf[256]; int fd; fd = syscall(__NR_openat,fd,"/data/local/tmp/tuzi.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/asdss.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/asda.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/TsdsaWO.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/sadas.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/sad.txt", O_RDONLY); syscall(__NR_openat,fd,"/data/local/tmp/asda.txt", O_RDONLY); //printf("fd : %d \n" ,fd); if (fd == -1) { perror("open"); return 1; } while((count = syscall(__NR_read, fd, buf, sizeof(buf))) > 0) { syscall(__NR_write, STDOUT_FILENO, buf, count); } syscall(__NR_close, fd); } else { waitpid(pid, &status, 0); //嘗試開啟ptrace+seccomp ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESECCOMP); process_signals(pid); return 0; }}
2.bpf結構
下面來解釋一下bpf結構,BPF 被定義為一種虛擬機 (VM),它具有一個數據寄存器或累加器、一個索引寄存器和一個隱式程序計數器 (PC)。它的“匯編”指令被定義為具有以下格式的結構:
struct sock_filter { u_short code; u_char jt; u_char jf; u_long k;};
有累加器,跳轉等待碼(操作碼),jt和jf是跳轉指令中使用的程序計數器的增量,而k是一個輔助值,其用法取決于代碼編號。
BPFs有一個可尋址空間,其中的數據在網絡情況下是一個數據包數據報,對于seccomp有一下結構:
struct seccomp_data { int nr; /* System call number */ __u32 arch; /* AUDIT_ARCH_* value (see <linux/audit.h>) */ __u64 instruction_pointer; /* CPU instruction pointer */ __u64 args[6]; /* Up to 6 system call arguments */};
所以bpfs在seccomp中做的是對這些數據進行操作并返回一個值告訴內核下一步做什么,比如:
允許進程執行調用(SECCOMP_RET_ALLOW)終止(SECCOMP_RET_KILL)
詳細見文檔:seccomp文檔(https://manpages.ubuntu.com/manpages/xenial/man2/seccomp.2.html)
現在我們可以根據系統調用號和參數進行過濾,bpf過濾器被定義為一個sock_filter結構,其中每條都是一個bpf指令。
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)),BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 1),BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_TRACE),BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT和BPF_JUMP是兩個填充sock_filter結構的簡單紅。他在參數上有所不同。其中包括BPF_JUMP中的跳躍偏移量。在兩種情況下。第一個參數都是操作碼,作為助記符幫助:例如,第一個參數是使用絕對尋址(BPF_ABS) 將一個字 (BPF_W) 加載到累加器 (BPF_LD) 中。
第一條指令是要求VM將呼叫號碼加載nr到累加器。第二條將與openat的系統調用號進行比較。如果他們相等(pc + o),則要求vm不修改計數器。因此運行第三條指令,否則跳轉到PC+1,這是第四條指令(當執行到這條指令時,pc已經指向第三條指令)。因此如果這是一個開放的系統調用,我們將返回SECCOMP_RET_TRACE,這會調用跟蹤器否則返回SECCOMP_RET_ALLOW,這將會讓沒有被跟蹤的系統調用直接執行。
然后是第一次調用 prctl 設置PR_SET_NO_NEW_RPIVS,這會阻止子進程擁有比父進程更多的權限。他使用PR_SET_SECCOMP選擇設置seccomp過濾器,不是root用戶也可以使用,之后使用openat系統調用進行打開文件等操作。
父進程我設置了PTRACE_O_TRACESECCOMP 選項,當過濾器返回 SECCOMP_RET_TRACE 并將事件信號發送給跟蹤器時,跟蹤器將停止。此函數的另一個變化是我們不再需要設置 PTRACE_O_TRACESYSGOOD,因為我們被 seccomp 中斷,而不是因為系統調用。
3.最終功能
static void process_signals(pid_t child){ char file_to_redirect[256] = "/data/local/tmp/tuzi1.txt"; char file_to_avoid[256] = "/data/local/tmp/tuzi.txt"; int status; while(1) { char orig_file[PATH_MAX]; struct user_pt_regs regs; struct iovec io; io.iov_base = ®s; io.iov_len = sizeof(regs); ptrace(PTRACE_CONT, child, 0, 0); waitpid(child, &status, 0); ptrace(PTRACE_GETREGSET, child, (void*)NT_PRSTATUS, &io); if (status >> 8 == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8)) ){ switch (regs.regs[8]) { case __NR_openat: read_file(child, orig_file,regs); if (strcmp(file_to_avoid, orig_file) == 0){ putdata(child,regs.regs[1],file_to_redirect,strlen(file_to_avoid)+1); } } if (WIFEXITED(status)){ break; } }}
這里就很簡單了獲取到svc的信號后讀取x8寄存器判斷是否為openat的系統調用號,這里只對file_to_avoid進行了替換,看一下最終效果:

可以看到不僅只對openat進行了監控也成功的將了第一次打開的文件
/data/local/tmp/tuzi.txt修改為了/data/local/tmp/tuzi1.txt。
結束
demo地址 github(https://github.com/xiaotujinbnb/ptrace-seccomp-demo)
007bug
安全俠
cayman
007bug
上官雨寶
合天網安實驗室
雷石安全實驗室
黑客技術與網絡安全
一顆小胡椒
一顆小胡椒