前叙
众所周知,目前各大APP的安全模块几乎都会使用自实现的libc函数,如open,read等函数,通过自实现svc方式来实现系统调用。因此我们如果想要hook系统调用,只能通过扫描厂商自实现的代码段,定位svc指令所在地址,再通过inline hook方式来进行hook操作,但是这种方式需要涉及内存修改,很容易被检测到内存篡改行为。
本文将利用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的文章大致有两类:
- ,来捕获seccomp的
SECCOMP_RET_TRACE
信号,从而达到劫持svc调用的目的。这种方案在可执行文件下,是可行的,能够跑的通。但是在app环境下,ptrace将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。
- ,通过
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调用是主进程自身调用。有多种不同的思路,包括:
- seccomp_data中的instruction_pointer表示svc调用后的返回地址,我们可以在
sig_handler
中使用我们自实现的svc,filter
中判断返回地址是否是在我们自实现的svc函数即可。
- 比较简单的就是我们
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, MAGIC, 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;
}
}
有问题欢迎大家私聊或者下方评论。