[原创]seccomp-bpf+ptrace实现修改系统调用原理(附demo)
2022-12-13 11:43:0 Author: bbs.pediy.com(查看原文) 阅读量:17 收藏

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

2022-12-13 11:43 7671

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

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

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

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程序允许执行该系统调用,则进程可以继续执行,否则会抛出一个异常。

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 <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 时终止程序。

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

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函数来跟踪子进程,获取其寄存器中的信息,然后根据需求替换对应的系统调用。

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

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;

}

看一下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)。它的“汇编”指令被定义为具有以下格式的结构:

1

2

3

4

5

6

struct sock_filter {

    u_short code;

    u_char  jt;

    u_char  jf;

    u_long k;

};

有累加器,跳转等待码(操作码),jt和jf是跳转指令中使用的程序计数器的增量,而k是一个辅助值,其用法取决于代码编号。

BPFs有一个可寻址空间,其中的数据在网络情况下是一个数据包数据报,对于seccomp有一下结构:

1

2

3

4

5

6

7

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中做的是对这些数据进行操作并返回一个值告诉内核下一步做什么,比如:

1

2

允许进程执行调用(SECCOMP_RET_ALLOW)

终止(SECCOMP_RET_KILL)

详细见文档:seccomp文档
现在我们可以根据系统调用号和参数进行过滤,bpf过滤器被定义为一个sock_filter结构,其中每条都是一个bpf指令。

1

2

3

4

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.最终功能

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

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
完结撒花!

proot

基于ptrace的Android系统调用跟踪&hook工具

SVC的TraceHook沙箱的实现&无痕Hook实现思路

ptrace を使用して seccomp による制限を回避してみる

ptrace(2) — Linux manual page

Seccomp and Seccomp-BPF

深入浅出 eBPF

看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2022-12-13 21:25 被王麻子本人编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-275511.htm
如有侵权请联系:admin#unsafe.sh