基于seccomp的Android通用svc hook方案

写在前面

众所周知,目前各大APP的安全模块几乎都会使用自实现的libc函数,如open,read等函数,通过自实现svc方式来实现系统调用。因此我们如果想要hook系统调用,只能通过扫描厂商自实现的代码段,定位svc指令所在地址,再通过inline hook方式来进行hook操作,但是这种方式需要涉及内存修改,很容易被检测到内存篡改行为。

作者:风铃Cipher
链接:https://juejin.cn/post/7242815848087945272

本文将利用seccomp方式来监听系统调用,以达到劫持svc调用的目的。

什么是seccomp

Seccomp是一个Linux内核安全模块,它可以使进程限制可以进行的系统调用数量,从而提高进程的安全性和可靠性。Seccomp提供了一种轻量级的进程隔离方式,可以在限制进程的能力的同时,不会影响操作系统的整体功能,是现代容器和虚拟化技术中广泛使用的安全保障机制之一。

Seccomp的主要工作流程是通过在进程中使用prctl()系统调用来指定一个过滤规则集,该规则集称为“过滤器”,它定义了该进程允许使用的系统调用类型和参数。当进程调用系统调用时,过滤器会拦截该调用并进行验证,以判断其是否符合规则集中指定的条件。如果系统调用不符合规则,Seccomp将拒绝该操作,并终止进程。

Seccomp的过滤器有两种类型:全局过滤器和线程过滤器。全局过滤器是在prctl()系统调用时设置的,它会对整个进程使用的所有线程都生效。而线程过滤器是在线程创建时设置的,只会对该线程生效。通常情况下,Seccomp的使用者只需要使用全局过滤器即可,因为它可以在进程创建时就应用到所有的线程中。

过滤器的具体设置可以使用在C语言中定义的BPF程序宏来完成,也可以使用SECCOMP_MODE_FILTER模式下的seccomp()库函数来设置。BPF程序宏和seccomp()函数都需要将过滤器规则集以二进制方式指定,并将其传递给prctl()系统调用来启用过滤器。下面是一个简单的示例,演示如何使用BPF程序宏来在全局范围内应用过滤器:

#include <stddef.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>


#define FILTER_SYSCALLS \
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)), \
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_getpid, 0, 1), \
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW), \
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRACE)


int main(int argc, char *argv[]) {
    struct sock_fprog filter = {
        .len = sizeof(FILTER_SYSCALLS) / sizeof(FILTER_SYSCALLS[0]),
        .filter = FILTER_SYSCALLS,
    };
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter) < 0) {
        perror("prctl");
        return 1;
    }


    return 0;
}

此例中,过滤器仅允许进程调用getpid()系统调用。如果进程尝试调用其他系统调用,Seccomp将拒绝该操作并终止进程。这里使用BPF程序宏来定义过滤器,当然也可以使用seccomp()库函数来编写等效的过滤器规则,例如通过添加seccomp_rule_add()库函数,来定义更复杂的规则集匹配条件等。

通用svc hook

当前网上能搜到的基于seccomp实现svc hook的文章大致有两类:

  1. 利用fork + ptrace方式,来捕获seccomp的SECCOMP_RET_TRACE信号,从而达到劫持svc调用的目的。这种方案在可执行文件下,是可行的,能够跑的通。但是在app环境下,ptrace将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。

  2. 利用frida + seccomp方式,通过Process.setExceptionHandler来捕获SECCOMP_RET_TRAP信号,并且为了避免hook时死循环递归(hook函数中调用svc又再次被seccomp过滤发出SECCOMP_RET_TRAP信号),该项目通过创建新线程的方式来规避,但这种方式在处理多线程或者多进程任务时处理起来很麻烦。

本文的通用hook svc方法与第二种类似,主要是捕获SECCOMP_RET_TRAP信号,来实现的hook操作。

syscall信号拦截

seccomp的filter中发出SECCOMP_RET_TRAP信号来触发中断,代码如下。

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), //判断是否等于__NR_openat
        BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRAP), //若是,则触发SECCOMP_RET_TRAP信号
        BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW), //若否,则通过。
    };


    struct sock_fprog prog;
    prog.filter = filter2;
    prog.len = (unsigned short) (sizeof(filter) / sizeof(filter[0]));


    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;
    }

seccomp_data是一个结构体类型,它在使用Seccomp系统调用过滤机制时作为参数传递给BPF程序来指定系统调用。它的定义包含在linux/seccomp.h头文件中,通常定义如下:

struct seccomp_data {
  int nr;
  __u32 arch;
  __u64 instruction_pointer;
  __u64 args[6];
};

其中,各字段表示的含义如下:

  • nr:系统调用号。当前正在调用的系统调用的编号,一般为0到MAX_SYSCALL之间的值之一。

  • arch:处理器架构。指向当前处理器架构类型的指针。

  • instruction_pointer:指令指针,指向正在执行的指令的地址。

  • args:系统调用参数。系统调用的参数,0到5条参数的值分别储存在这个字段中。

使用Seccomp过滤器功能时,可以使用seccomp_data结构体的这些成员来访问和操作该进程的系统调用,对系统调用进行过滤或修改,来完成对特定系统调用的限制。

信号处理

seccomp发出SECCOMP_RET_TRAP信号时,会引发程序阻塞机制,此时系统会产生一个SIGSYS信号,并使原程序处于临时阻塞状态。因此我们可以使用sigaction来注册一个对SIGSYS信号进行处理的handler,再将处理完的结果返回出去,从而达到hook效果。代码如下

struct sigaction sa;
sigset_t sigset;
sigfillset(&sigset);


sa.sa_sigaction = sig_handler;
sa.sa_mask = sigset;
sa.sa_flags = SA_SIGINFO;


if (sigaction(SIGSYS, &sa, NULL) == -1) {
    LOGE("sigaction init failed.\n");
    return 1;
}
void sig_handler(int signo, siginfo_t *info, void *data) {
    int my_signo = info->si_signo;
    printf("sig_handler2, signo: %x\n", my_signo);  
    unsigned long sysno = ((ucontext_t *) data)->uc_mcontext.arm_r7;
    unsigned long arg0 = ((ucontext_t *) data)->uc_mcontext.arm_r0;
    unsigned long arg1 = ((ucontext_t *) data)->uc_mcontext.arm_r1;
    unsigned long arg2 = ((ucontext_t *) data)->uc_mcontext.arm_r2;
    unsigned long arg3 = ((ucontext_t *) data)->uc_mcontext.arm_r3;
    unsigned long arg4 = ((ucontext_t *) data)->uc_mcontext.arm_r4;
}

避免死循环

上面例子中我们想要对openat的svc进行hook操作,那么我们就需要再sig_handler函数里面对修改过的参数再调用一次openat函数,再将返回值给到主进程,让进程继续执行。

但是这样就会有个问题,就是我们的sig_handler里面再调用openat函数会再次经过seccomp的filter,并且会再次触发一次SECCOMP_RET_TRAP信号,从而又一次进入sig_handler。。。造成死循环问题。

因此我们需要优化filter内容,让其能够知道哪些svc调用是从sig_handler调用过来,哪些svc调用是主进程自身调用。有多种不同的思路,包括:

  1. seccomp_data中的instruction_pointer表示svc调用后的返回地址,我们可以在sig_handler中使用我们自实现的svc,filter中判断返回地址是否是在我们自实现的svc函数即可。

  2. 比较简单的就是我们sig_handler进行svc调用时,多传递一个MAGIC参数进去,filter中判断多余的参数是否等于MAGIC,若是则启用SECCOMP_RET_ALLOW信号,否则触发SECCOMP_RET_TRAP。filter如下

#define SECMAGIC 0xdeadbeef
    struct sock_filter filter2[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close, 0, 2),
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SECMAGIC, 0, 1),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP)
    };

sig_handle中处理逻辑如下:

#define SECMAGIC 0xdeadbeef


void sig_handler(int signo, siginfo_t *info, void *data) {
    int my_signo = info->si_signo;
    LOGD("sig_handler2, signo: %x\n", my_signo);
    printRegs(data);
    unsigned long sysno = ((ucontext_t *) data)->uc_mcontext.arm_r7;
    unsigned long arg0 = ((ucontext_t *) data)->uc_mcontext.arm_r0;
    unsigned long arg1 = ((ucontext_t *) data)->uc_mcontext.arm_r1;
    unsigned long arg2 = ((ucontext_t *) data)->uc_mcontext.arm_r2;
    unsigned long arg3 = ((ucontext_t *) data)->uc_mcontext.arm_r3;
    unsigned long arg4 = ((ucontext_t *) data)->uc_mcontext.arm_r4;
    switch (sysno) {
        case __NR_close:
            LOGD("[close]fd: %ld\n", arg0);
            int close_fd;
            close_fd = syscall(__NR_close, arg0, 0, SECMAGIC);
            LOGD("[close]close ret: %d\n", close_fd);
            ((ucontext_t *) data)->uc_mcontext.arm_r0 = close_fd;
            break;
        default:
            break;
    }
}

关注我获取更多知识或者投稿

5cae907b89bb3c225357122af10a4384.jpeg

46436cad4bc2b9c50d1aae0f306dadfc.jpeg

猜你喜欢

转载自blog.csdn.net/c6E5UlI1N/article/details/131179270