目前絕大部分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, &regs);    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, &regs);    }  }  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 = &regs;        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