首页
社区
课程
招聘
[原创]seccomp-bpf+ptrace实现修改系统调用原理(附demo)
发表于: 2022-12-13 11:43 46239

[原创]seccomp-bpf+ptrace实现修改系统调用原理(附demo)

2022-12-13 11:43
46239

前言

目前绝大部分app都会频繁的使用syscall去获取设备指纹和做一些反调试,使用常规方式过反调试已经非常困难了,使用内存搜索svc指令已经不能满足需求了,开始学习了一下通过ptrace/ptrace配合seccomp来解决svc反调试难定位难绕过等问题。

seccomp

Linux 2.6.12中的导入了第一个版本的seccomp,通过向/proc/PID/seccomp接口中写入“1”来启动通过滤器只支持几个函数。

1
read(),write(),_exit(),sigreturn()

使用其他系统调用就会收到信号(SIGKILL)退出。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include
#include
#include
#include
#include
 
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 \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确定了一个可以在内核内部实现的虚拟机,该虚拟机具有以下特性:

1
2
3
4
5
6
7
8
9
10
11
简单指令集
    小型指令集
    所有的命令大小相一致
    实现过程简单、快速
只有分支向前指令
    程序是有向无环图(DAGs),没有循环
易于验证程序的有效性/安全性
    简单的指令集⇒可以验证操作码和参数
    可以检测死代码
    程序必须以 Return 结束
    BPF过滤器程序仅限于4096条指令

2.Seccomp-BPF 使用的也只是BPF的子集功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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程序 接收以下结构作为输入参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 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 /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 时终止程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include
#include
#include
#include
#include
#include
#include
#include
 
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 \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函数来跟踪子进程,获取其寄存器中的信息,然后根据需求替换对应的系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
 
 
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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)。它的“汇编”指令被定义为具有以下格式的结构:


[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

最后于 2023-1-11 14:54 被王麻子本人编辑 ,原因:
收藏
免费 35
支持
分享
打赏 + 20.00雪花
打赏次数 2 雪花 + 20.00
 
赞赏  万里星河   +10.00 2023/01/08 瑕不掩瑜 感谢分享
赞赏  守哥哥   +10.00 2022/12/13 我也想要源码
最新回复 (91)
雪    币: 107
活跃值: (563)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
2022-12-13 12:58
0
雪    币: 519
活跃值: (1837)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
tql
2022-12-13 15:33
0
雪    币: 217
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
tql 能发一下全部源码吗
2022-12-13 16:03
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
mb_ozmrnffd tql 能发一下全部源码吗
发了
2022-12-13 16:19
0
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
牛逼
2022-12-13 16:41
0
雪    币: 217
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
编译报arm_seccomp.h:3:47: error: unknown type name 'user_pt_regs'
2022-12-13 16:47
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
8
mb_ozmrnffd 编译报arm_seccomp.h:3:47: error: unknown type name 'user_pt_regs'

user_pt_regs结构体在asm/ptrace应该没问题的吧。

最后于 2022-12-13 16:52 被王麻子本人编辑 ,原因:
2022-12-13 16:51
0
雪    币: 2603
活跃值: (11160)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
窝草看不懂啊
2022-12-13 17:04
0
雪    币: 986
活跃值: (6207)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
10
我那个项目太简陋了,直接去看proot吧,ptrace相关api都封装好了,重定向的参数在栈上动态分配,少很多限制
2022-12-13 19:52
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
Ssssone 我那个项目太简陋了,直接去看proot吧,ptrace相关api都封装好了,重定向的参数在栈上动态分配,少很多限制

我是先看的你的项目然后结合seccopm打的基础在去弄的proot,目前这个是放了个简单的demo出来给大家一个思路,你的那个项目还是给了我很大启发的。

最后于 2022-12-13 21:23 被王麻子本人编辑 ,原因:
2022-12-13 21:23
0
雪    币: 5
活跃值: (1125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
dwmo源码好像有问题,替换不成功,putdata后open error错误,源文件跟重定向文件都是存在的,路径也没错。另外while死循环,执行不到WIFEXITED(status),这个break语句始终没进来
2022-12-14 06:36
0
雪    币: 5
活跃值: (1125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
demo源码好像有问题,替换不成功,putdata后open error错误,源文件跟重定向文件都是存在的,路径也没错。另外while死循环,执行不到WIFEXITED(status),这个break语句始终没进来
2022-12-14 06:37
0
雪    币: 5
活跃值: (1125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
用打包注入方法测试的
2022-12-14 06:38
0
雪    币: 1825
活跃值: (2038)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
执行第二个prctl函数返回结果错误, 报: when setting seccomp filter: Invalid argument, 
请问这个怎么解决?
2022-12-14 09:52
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
16
mb_czxkzsmv 执行第二个prctl函数返回结果错误, 报: when setting seccomp filter: Invalid argument, 请问这个怎么解决?

我重新上传了一下发的时候发错了好像,BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 2),改成BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 1)即可

最后于 2022-12-14 10:15 被王麻子本人编辑 ,原因:
2022-12-14 10:11
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
17
恋一世的爱 demo源码好像有问题,替换不成功,putdata后open error错误,源文件跟重定向文件都是存在的,路径也没错。另外while死循环,执行不到WIFEXITED(status),这个break ...
啊?WIFEXITED?
2022-12-14 10:13
0
雪    币: 5
活跃值: (1125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
王麻子本人 啊?WIFEXITED?
是的,会有这2个问题,我昨天也改成了BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 1),也还是有我说的问题
2022-12-14 11:42
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
19
恋一世的爱 是的,会有这2个问题,我昨天也改成了BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 1),也还是有我说的问题
我发新的了我这边是没问题的方便截图看一下吗
2022-12-14 12:13
0
雪    币: 5
活跃值: (1125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20

比如我要将1.txt重定向为2.txt


2022-12-14 13:35
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
21
恋一世的爱 比如我要将1.txt重定向为2.txt

你这个拦截是拦截成功了,修改参数也没啥问题,我demo写的非常简陋你可以试试换个目录,替换字符串你可以看到这个demo也是十分简陋看ptrace文档可以知道PTRACE_POKEDATA是

将字数据复制到被跟踪者的地址addr

这个在字符串长度相等和一些情况是可以正常运行的但是往往不能正常运行的,可以参考proot把重定向的参数在栈上动态分配

WIFEXITED的话就是的具体处理我这个demo没做只是子程序结束就结束循环而已。

最后于 2022-12-14 15:31 被王麻子本人编辑 ,原因:
2022-12-14 15:17
0
雪    币: 1825
活跃值: (2038)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
大佬, 请教一下, 为什么循环里面使用 
ptrace(PTRACE_CONT, child, 0, 0); 
而不是
ptrace(PTRACE_SYSCALL, child, 0, 0); 呢? 
2022-12-14 16:04
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
23
mb_czxkzsmv 大佬, 请教一下, 为什么循环里面使用 ptrace(PTRACE_CONT, child, 0, 0); 而不是 ptrace(PTRACE_SYSCALL, child, 0, 0); ...
PTRACE_CONT是重新启动子进程让子进程继续运行下去。PTRACE_SYSCALL也是启动跟踪的子进程但是它只允许子进程执行一条系统调用后暂停。如果单纯使用ptrace去处理svc才需要使用PTRACE_SYSCALL,我们使用的是seccomp+ptrace,系统调用由seccomp进行过滤。
2022-12-14 16:16
0
雪    币: 1825
活跃值: (2038)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
王麻子本人 PTRACE_CONT是重新启动子进程让子进程继续运行下去。PTRACE_SYSCALL也是启动跟踪的子进程但是它只允许子进程执行一条系统调用后暂停。如果单纯使用ptrace去处理svc才需要使用PT ...
感谢解答, 豁然开朗!

如果要同时拦截多个系统调用 bpf 规则咋写啊, 我尝试添加了 __NR_write, __NR_openat , 虽然可以正常运行, 但是只拦截到了其中一个调用
2022-12-14 17:24
0
雪    币: 944
活跃值: (4590)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
25
mb_czxkzsmv 感谢解答, 豁然开朗! 如果要同时拦截多个系统调用 bpf 规则咋写啊, 我尝试添加了 __NR_write, __NR_openat , 虽然可以正常运行, 但是只拦截到了其中一个调用
比较呆的写法是这样:添加一条jump指令
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 2),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_read, 0, 1),
如果你需要过滤的系统调用比较多可以把每个调用的返回值放在一个数组里面
2022-12-14 18:18
0
游客
登录 | 注册 方可回帖
返回