首页
社区
课程
招聘
[原创]基于seccomp+sigaction的Android通用svc hook方案
2023-6-9 22:10 20936

[原创]基于seccomp+sigaction的Android通用svc hook方案

2023-6-9 22:10
20936

前叙

众所周知,目前各大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的文章大致有两类:

  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[2])),
        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;
    }
}

有问题欢迎大家私聊或者下方评论。


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2023-6-13 10:51 被一只流浪狗编辑 ,原因: 事例代码更正。
收藏
点赞14
打赏
分享
最新回复 (20)
雪    币: 224
活跃值: (962)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
樊辉 2023-6-10 16:15
2
0
有很多不错的svc hook开源项目
雪    币: 5
活跃值: (985)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
恋一世的爱 2023-6-10 21:29
3
0
能否开源提供完整代码给小白学习下
雪    币: 19803
活跃值: (29410)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-6-10 22:05
4
0
感谢分享
雪    币: 1773
活跃值: (8930)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
你瞒我瞒 2023-6-12 10:16
5
0
恋一世的爱 能否开源提供完整代码给小白学习下
我也是想问有没有一个完整的demo来跟着复习下
雪    币: 349
活跃值: (562)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
一只流浪狗 2023-6-12 11:47
6
0
你瞒我瞒 我也是想问有没有一个完整的demo来跟着复习下[em_85]
等过两天我再整理好发上来。
雪    币: 739
活跃值: (185)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_痛 2023-6-12 13:47
7
0
要什么完整demo,论坛里看完相关文章真的写不出来吗.想要源码白嫖还是学习目的
雪    币: 19
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jilay 2023-6-13 10:40
8
0
你好,代码用在安卓上switch case命中就卡主了。。是什么情况啊
雪    币: 349
活跃值: (562)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
一只流浪狗 2023-6-13 10:44
9
0
jilay 你好,代码用在安卓上switch case命中就卡主了。。是什么情况啊
“BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1]))”改成“BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2]))”,试试。
雪    币: 19
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jilay 2023-6-13 12:23
10
0
一只流浪狗 “BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1]))”改成“BPF_STMT(BPF_LD | BPF ...
可以了。。
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
SharkFall 2023-6-25 15:05
11
0
在miui上跑过嘛,大佬。我在demo上跑正常hook,注入以后就出问题了
雪    币: 5
活跃值: (985)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
恋一世的爱 2023-7-1 01:54
12
0

测试__NR_kill  __NR_openat都正常,__NR_exit会卡死

最后于 2023-7-2 13:49 被恋一世的爱编辑 ,原因:
雪    币: 5
活跃值: (985)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
恋一世的爱 2023-7-2 13:49
13
0
测试__NR_kill  __NR_openat都正常,__NR_exit会卡死
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_kgsesxqm 2023-7-14 23:59
14
0
大佬,按照你的教程来,写好demo,拦截的是  __NR_openat  注入游戏(游戏没检测)会崩溃,报错:Process 8911 exited due to signal 31 (Bad system call),可以帮忙看下吗,游戏链接可以在apkpure或者APKCombo搜索Animal Revolt Battle Simulator(版本3.1.1_arm64)https://apkcombo.com/zh/animal-revolt-battle-simulator/com.vdimensionltd.animalrevoltbattlesimulator/download/apk
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_kgsesxqm 2023-7-15 00:01
15
0
SharkFall 在miui上跑过嘛,大佬。我在demo上跑正常hook,注入以后就出问题了
兄弟,你是拦截哪个NR,出现的问题是什么呢
雪    币: 62
活跃值: (572)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-9-5 21:15
16
0
一只流浪狗 等过两天我再整理好发上来。

大佬 64位的信号处理怎么写呀

最后于 2023-9-5 21:16 被万里星河编辑 ,原因:
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
SharkFall 2024-2-8 11:15
17
0
mb_kgsesxqm 兄弟,你是拦截哪个NR,出现的问题是什么呢
忘了,哈哈哈,不过解决了,现在是32位跑不进hook
雪    币: 20
活跃值: (643)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
肉蚌葱鸡 2024-2-9 09:56
18
0

请问有尝试hook __NR_clone 的吗 会出现无限重启的情况

最后于 2024-2-11 13:20 被肉蚌葱鸡编辑 ,原因: 回答错误
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_gipfprwu 2024-2-12 09:14
19
0
尝试hook openat注入后有些为什么跑不进hook
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_gipfprwu 2024-3-1 08:01
20
0
mb_gipfprwu 尝试hook openat注入后有些为什么跑不进hook

其他线程的似乎没被应用到

最后于 2024-3-1 08:02 被mb_gipfprwu编辑 ,原因:
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
半只青蛙 2024-4-26 15:57
21
0
请教大神__NR_openat 的实现该怎么写呢,我写的始终不对。
游客
登录 | 注册 方可回帖
返回