前言

“某数字壳又双叒叕更新了”

众所周知,插件党想要开心,那安全厂就不开心,于是插件党就得想办法开心——

然而国内这内卷的现状,许多旧的过签方式一出来就被修掉了,于是又得开始开发新的魔法。常见的过签方法两种,要么欺骗目标拿到的包文件是“正确的”,要么日掉系统调用狸猫换太子。前者过于依赖壳内部实现,后者很容易被检测。因此亟需发明一种新的魔法。

Magic Time

背景

各大厂商都在积极绞杀重打包方案,一堆用户大叫某易云、某安和某乎在 LSPatched 之后打开闪退,太极等框架已经放弃签名对抗。

一开始我想使用 fork + ptrace 监听 syscall,但由于 fork 后 ART 尸体之类的问题,ptrace 会让某些 app 一直等待 gc 而卡死(说的就是你,X易云)。在无意之中看到了这篇文章,于是产生了用 seccomp 日 syscall 的想法,鉴于著名的 vmos 就是采用的 seccomp,应该具有不错的兼容性。

施工

启用 seccomp

参考文章

syscall 拦截与恢复

要达到沙盒的效果,除了拦截系统调用外,还需要对其进行监控和修改。而 sighandler 本身仍然受 seccomp 约束,因此如果在 handler 里直接进行系统调用则会再次触发 SIGSYS,导致程序 abort。

我们希望实现的是,在 handler 内能够自由进行 syscall,而拦截 handler 外的调用。一种方法如看雪那篇文章,将我们的 syscall 转发到一个自由线程处理,但是这么做存在一些不足:

  1. 部分线程相关调用,如 pthread_sigprocmaskgetpid 无法或者很难在 worker 线程中处理
  2. 线程转发可能存在一些很难发现的异步问题,并且某些情况下 errno 会被强制设置成 EPERM,原因未知

因此,我们需要自己的事情自己做,通过修改规则放行属于自己的 syscall。幸运的是,BPF Filter 具有这样的能力。

借鉴谷歌 linux-syscall-support 的处理方案,内嵌汇编设置 Trampoline,并在 BPF 规则中放行,实现 syscall wrapper。(问题来了,为什么谷歌只做了 x86 的......)

Wrapper

extern "C" void Trampoline();

__attribute__((naked, noinline))
long DoSyscall(long nr, ...) {
#if defined(__i386__)
    asm(
    "pushl %ebp\n\t"
    "pushl %edi\n\t"
    "pushl %esi\n\t"
    "pushl %ebx\n\t"
    "movl 44(%esp), %ebp\n\t"
    "movl 40(%esp), %edi\n\t"
    "movl 36(%esp), %esi\n\t"
    "movl 32(%esp), %edx\n\t"
    "movl 28(%esp), %ecx\n\t"
    "movl 24(%esp), %ebx\n\t"
    "movl 20(%esp), %eax\n\t"
    "int $0x80\n\t"
    "Trampoline:\n\t"
    "popl %ebx\n\t"
    "popl %esi\n\t"
    "popl %edi\n\t"
    "popl %ebp\n\t"
    "ret\n\t"
    );
#elif defined(__x86_64__)
    ...
}

BPF Filter

auto trampoline = (uintptr_t) Trampoline;
struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
#if defined(__i386__) || defined(__arm__)
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 2),
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, trampoline, 0, 1),
#else
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 4),
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, static_cast<uint32_t>(trampoline), 0, 3),
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer) + sizeof(uint32_t)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, static_cast<uint32_t>(trampoline >> 32), 0, 1),
#endif
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP)
};

由此,我们便可以在 handler 中自由使用 syscall,不再被自己的 seccomp 困住。

信号处理

等等,这就完了?当然没那么简单。真正的应用极有可能设置自己的各种 sighandler,而这无意间就会把我们的 handler 给覆盖,然后一旦 app 的 handler 使用了 syscall,Fatal error: 31 (Bad system call) 就会炸掉整个 app。

因此,我们还需要特殊处理各种涉及信号的系统调用。

SIGSYS

启用 seccomp 后,在发生系统调用时,如果满足 BPF 规则则会触发 SIGSYS,从而转入自己设置的 handler 中。

常规情况下,由于隐式阻塞机制,在 sighandler 中再次触发相同信号会被阻塞,直到处理结束后再次进入 handler;但 seccomp 是一个特例:在 seccomp handler 中再次触发 SIGSYS,会导致程序直接 abort。因此,必须避免 handler 中出现 SIGSYS

seccomp(2) - Linux manual page

The process terminates as though killed by a SIGSYS signal. Even if a signal handler has been registered for SIGSYS, the handler will be ignored in this case and the process always terminates. To a parent process that is waiting on this process (using waitpid(2) or similar), the returned wstatus will indicate that its child was terminated as though by a SIGSYS signal.

SIGSEGV

bionic(?) 默认的 SIGSEGV 处理器会遮蔽 SIGSYS 信号,从而使 seccomp handler 失效导致 abort。因此,我们需要代理系统的 handler。(虽然发生 SIGSEGV 本身也会导致程序死掉,但我们需要栈展开信息和 tombstone 以定位问题所在)

sigemptyset64(&sa.sa_mask);
sigaction64(SIGSYS, &sa, nullptr);
syscall(SYS_rt_sigaction, SIGSEGV, nullptr, &sa, 8);
if (sigismember64(&sa.sa_mask, SIGSYS)) {
    LOGD("Sigsys is masked");
    sigdelset64(&sa.sa_mask, SIGSYS);
} else {
    LOGD("Sigsys is not masked");
}
syscall(SYS_rt_sigaction, SIGSEGV, &sa, nullptr, 8);

sigaction

用户程序使用 rt_sigaction 系统调用设置 sighandler。我们需要拦截涉及到 SIGSYS 的行为,防止 seccomp handler 失效。

thread_local struct sigaction kSavedAction;

inline void RegisterSigactionInterceptor() {
    RegisterSyscallHandler(SYS_rt_sigaction, [](long* regs) {
        LOGD("SigactionInterceptor");
        auto* new_action = (const struct sigaction*) regs[REG_ARG1];
        auto* old_action = (struct sigaction*) regs[REG_ARG2];
        if (regs[REG_ARG0] != SIGSYS) {
            regs[REG_RET] = DoSyscall(SYS_rt_sigaction, SIGSYS, new_action, old_action, regs[REG_ARG3]);
            return;
        }
        LOGI("Intercept SIGSYS action");
        if (old_action != nullptr) {
            *old_action = kSavedAction;
        }
        if (new_action != nullptr) {
            kSavedAction = *new_action;
        }
    });
}

sigprocmask

用户程序使用 rt_sigaction 系统调用设置 signal mask。同样,我们也需要阻止 SIGSYS 被 mask,同时还不能让用户程序发现不对劲的地方。

thread_local bool kSigsysMasked;

inline void RegisterSigprocmaskInterceptor() {
    RegisterSyscallHandler(SYS_rt_sigprocmask, [](long* regs) {
        LOGD("SigprocmaskInterceptor");
        auto* new_set = (sigset64_t*) regs[REG_ARG1];
        auto* old_set = (sigset64_t*) regs[REG_ARG2];

        auto newSigsysMasked = kSigsysMasked;
        if (new_set && sigismember64(new_set, SIGSYS)) {
            newSigsysMasked = regs[REG_ARG0] != SIG_UNBLOCK;
            sigdelset64(new_set, SIGSYS);
            regs[REG_RET] = DoSyscall(SYS_rt_sigprocmask, regs[REG_ARG0], new_set, old_set, regs[REG_ARG3]);
            sigaddset64(new_set, SIGSYS);
        } else {
            regs[REG_RET] = DoSyscall(SYS_rt_sigprocmask, regs[REG_ARG0], new_set, old_set, regs[REG_ARG3]);
        }
        if (kSigsysMasked) {
            sigaddset64(old_set, SIGSYS);
        }
        kSigsysMasked = newSigsysMasked;
    });
}

seccomp handler

当然,我们的 handler 中也需要特殊处理。

void SyscallHandler(int /*unused*/, siginfo_t* info, void* ucontext) {
    ...
    if (info->si_code != 1) {
        LOGD("SIGSYS: si_code = %d saved_handler = %p", info->si_code, kSavedAction.sa_handler);
        if (kSigsysMasked) return;
        if (kSavedAction.sa_handler == nullptr) exit(1);
        if (kSavedAction.sa_flags == SA_SIGINFO) {
            kSavedAction.sa_sigaction(info->si_signo, info, ucontext);
        } else {
            kSavedAction.sa_handler(info->si_signo);
        }
        LOGD("Exit SyscallHandler %d", info->si_syscall);
        return;
    }
    ...
}

未完待续——

最后修改:2022 年 04 月 27 日
如果觉得我的文章对你有用,请随意赞赏