本文为看雪论坛优秀文章
看雪论坛作者ID:王麻子本人
目前绝大部分app都会频繁的使用syscall去获取设备指纹和做一些反调试,使用常规方式过反调试已经非常困难了,使用内存搜索svc指令已经不能满足需求了,开始学习了一下通过ptrace/ptrace配合seccomp来解决svc反调试难定位难绕过等问题。
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(Berkeley Packet Filter)是Linux内核中的一种安全机制,用于限制进程对系统调用的访问权限。它主要用于防止恶意软件对系统的攻击,提高系统的安全性。
Seccomp-BPF使用BPF(Berkeley Packet Filter)技术来实现系统调用过滤,可以使用BPF程序指定哪些系统调用可以被进程访问,哪些不能。BPF程序由一组BPF指令组成,可以在系统调用执行之前对其进行检查,以决定是否允许执行该系统调用。
Seccomp-BPF提供了两种模式:白名单模式和黑名单模式。白名单模式允许所有系统调用,除非明确指定不允许的系统调用。黑名单模式禁止所有系统调用,除非明确指定允许的系统调用。这两种模式的选择取决于您的实际需求。
Seccomp-BPF提供了一个钩子函数,在系统调用执行之前会进入到这个函数,对系统调用进行检查,如果BPF程序允许执行该系统调用,则进程可以继续执行,否则会抛出一个异常。
简单指令集
小型指令集
所有的命令大小相一致
实现过程简单、快速
只有分支向前指令
程序是有向无环图(DAGs),没有循环
易于验证程序的有效性/安全性
简单的指令集⇒可以验证操作码和参数
可以检测死代码
程序必须以 Return 结束
BPF过滤器程序仅限于4096条指令
Conditional JMP(条件判断跳转)
当匹配条件为真,跳转到true指定位置
当 匹配条件为假,跳转到false指定位置
跳转偏移量最大255
JMP(直接跳转)
跳转目标是指令偏移量
跳转 偏移量最大255
Load(数据读取)
读取程序参数
读取指定的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;
}
将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。
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;
}
}
下面来解释一下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 中断,而不是因为系统调用。
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;
}
}
}
可以看到不仅只对openat进行了监控也成功的将了第一次打开的文件
/data/local/tmp/tuzi.txt修改为了/data/local/tmp/tuzi1.txt。
结束
demo地址 github(https://github.com/xiaotujinbnb/ptrace-seccomp-demo)
proot
(https://github.com/proot-me/proot)
基于ptrace的Android系统调用跟踪&hook工具
(https://bbs.pediy.com/thread-271921.htm)
SVC的TraceHook沙箱的实现&无痕Hook实现思路
(https://bbs.pediy.com/thread-273160.htm#msg_header_h2_5)
ptrace を使用して seccomp による制限を回避してみる
(https://blog.ssrf.in/post/bypass-seccomp-with-ptrace/)
ptrace(2) — Linux manual page
(https://man7.org/linux/man-pages/man2/ptrace.2.html)
Seccomp and Seccomp-BPF
(https://ajxchapman.github.io/linux/2016/08/31/seccomp-and-seccomp-bpf.html)
深入浅出 eBPF
(https://www.ebpf.top/post/kernel_btf/)
看雪ID:王麻子本人
https://bbs.kanxue.com/user-home-928079.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!