前言
“某数字壳又双叒叕更新了”
众所周知,插件党想要开心,那安全厂就不开心,于是插件党就得想办法开心——
然而国内这内卷的现状,许多旧的过签方式一出来就被修掉了,于是又得开始开发新的魔法。常见的过签方法两种,要么欺骗目标拿到的包文件是“正确的”,要么日掉系统调用狸猫换太子。前者过于依赖壳内部实现,后者很容易被检测。因此亟需发明一种新的魔法。
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 转发到一个自由线程处理,但是这么做存在一些不足:
- 部分线程相关调用,如
pthread_sigprocmask
、getpid
无法或者很难在 worker 线程中处理 - 线程转发可能存在一些很难发现的异步问题,并且某些情况下
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;
}
...
}
6 条评论
old_action应该用k_sigaction结构体,不能用sigaction
不在预期内应该call到原handler不然有拦截信号的痕迹,call了原handler调用堆栈又算一个痕迹
啥时候更新啊大佬
似乎有办法检测
请问有相关的demo代码吗?按照你这个尝试了一下,发现还是会bad system call。。。
牛蛙