type
status
date
slug
summary
tags
category
icon
password
 

0x00 引言

一切都因WMCTF2023一道Android 游戏题BabyAnti-2而起,预期解为拦截mincore调用(int mincore(void *start, size_t length, unsigned char *vec);监测指定大小的页面是否处于物理内存中。一般用于内存扫描的检查,一旦扫描行为发生,有些并不在物理内存的页面被调入。vec 是一个字节数组,用于存储结果。每个字节对应 addr 和 length 指定的内存区域中的一个页面。如果相应的页面驻留在内存中,那么相应的字节的最低位会被设置为 1,否则会被设置为 0。),当时非预期了题目,即直接CheatEngine附加游戏题,扫描内存时游戏虽然会监测到并弹窗,但是游戏正常运行,直接能修改分数并且拿到flag
由于题目中mincore 不止存在直接libc调用,而且存在svc指令的调用,这种svc指令相当于是直接的系统调用,不能被一般的钩子挂住,从而无法监视和修改调用参数返回值。而且题目设计使用申请的内存空间修改为可执行后放置svc指令,一直循环监测。除此之外,题目还是flutter写的,逆向逻辑难上加难。经过大量逆向和调试工作后,利用frida的内存搜索功能匹配svc指令,最终能够拦截并且不被检测到,但是鉴于太复杂,想着有没有什么通用办法,不需要逆程序逻辑就直接拦截svc调用的方法。
于是有了这篇文章,文章总结了看雪大佬提出的三种方案(ptrace-seccomp,frida-seccomp以及sigaction-seccomp)并进行了测试。(如有侵权或其问题他请联系本人删帖)

0x01 Seccomp

 

1. 介绍

Seccomp(安全计算模式)是 Linux 内核中的一种功能,它可以用来限制进程可以执行的系统调用。这是一种沙盒机制,用于限制应用程序可以访问的系统资源,从而提高系统的安全性。Seccomp 最初在 2005 年引入 Linux 内核,主要用于限制进程只能执行退出(exit)和等待(sigreturn)系统调用。这种模式称为严格模式(strict mode),主要用于一些不需要进行系统调用的安全敏感程序。后来,Seccomp 在 Linux 3.5 版本中引入了过滤模式(filter mode)。在过滤模式中,开发者可以为每个进程定义一个系统调用的白名单,只有白名单上的系统调用被允许执行。这使得 Seccomp 变得更加灵活和实用。Seccomp 通常与其他沙盒技术(如 chroot、namespaces、cgroups 等)一起使用,以提供更强大的隔离和安全机制。例如,Docker 容器默认启用了 Seccomp,以限制在容器中运行的进程可以执行的系统调用。使用 Seccomp 可以有效地防止一些安全漏洞,例如,如果一个进程被恶意代码入侵,那么即使恶意代码尝试执行一些危险的系统调用,由于 Seccomp 的限制,这些调用也会被拦截,从而保护了系统的安全
BPF (Berkeley Packet Filter):最初是为了高效处理网络数据包而设计的。它允许用户在内核级别定义一些规则,这些规则可以决定哪些数据包应该被接收,哪些应该被丢弃。在 Linux 3.18 版本中,BPF 被扩展为eBPF (Extended Berkeley Packet Filter),它不仅可以处理网络数据包,还可以用来观察和控制系统的各种行为,包括文件访问、系统调用等。eBPF 的规则是用一种专门的字节码语言编写的,这种语言可以被 eBPF 虚拟机在内核中执行。在 Linux 3.5 版本之后,Seccomp 开始支持 BPF,这意味着可以用 BPF 来编写 Seccomp的过滤规则。

2. BPF filter数据结构

3. BPF程序基本指令

BPF_STMTBPF_JUMP:这两个宏用于生成BPF指令。BPF_STMT用于生成一条简单的BPF语句,而BPF_JUMP用于生成一条带有跳转的BPF语句。
在上述指令中BPF_LD: 建一个 BPF 加载操作 ;BPF_W:操作数大小是一个字,BPF_ABS: 使用绝对偏移,即使用指令中的值作为数据区的偏移量,该值是体系结构字段与数据区域的偏移量 。offsetof()生成数据区域中期望字段的偏移量。
BPF_JMP 即进行跳转操作;BPF_JEQ | BPF_K判断BPF_K是否与O_RDONLY相等,0, 1 代表if(true){ jmp next } else {jmp next + 1}
这两条指令提取系统调用的第三个参数,并与O_RDONLY 进行比较,如果是O_RDONLY 则继续执行下一条指令,否则跳过一条指令。

4. Seccomp返回值

5. 配置Seccomp过滤器

上面的configure_seccomp 函数配置了一个 Seccomp 过滤器,该过滤器只允许 openat 系统调用在读取文件(O_RDONLY)的情况下被执行。任何其他的 openat 调用或其他系统调用都将导致进程被杀死。
  • 首先,用 BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))) 读取系统调用的编号,将其加载到 BPF 的寄存器中。
  • 然后,用 BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 3) 检查系统调用编号是否等于 openat 的编号。如果不是,跳转到最后一条语句,返回 SECCOMP_RET_KILL,杀死进程。如果是,继续执行下一条语句。
  • 接下来,用 BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[2]))) 读取 openat 调用的第三个参数(即打开模式),将其加载到 BPF 的寄存器中。
  • 然后,用 BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, O_RDONLY, 0, 1) 检查打开模式是否为 O_RDONLY。如果不是,跳转到最后一条语句,返回 SECCOMP_RET_KILL,杀死进程。如果是,继续执行下一条语句。
  • 最后,返回 SECCOMP_RET_ALLOW,允许 openat 系统调用执行。
然后,这个函数创建了一个 sock_fprog 结构体,其中包含了过滤器的长度和指向过滤器的指针。
最后,函数调用 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) 确保新的子进程不能获得新的权限,然后调用 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) 启动 Seccomp 并设置过滤器。

6. 拦截__NR_openat并根据打开模式决定是否放行

💡
这里在具体实现的过程中必须以实际出发,比如通过strace 看看实际触发的是哪个系统调用,然后编写过滤器代码,踩坑:未经实验以为open底层为__NR_open系统调用,但是通过strace发现实际是__NR_openat
write 信息出错,即返回了SECCOMP_RET_KILL ,即seccomp规则阻止了write系统调用,而允许openat系统调用。
然后仅改变为BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 2) ,代表对除openat外的系统调用直接返回SECCOMP_RET_ALLOW ,否则进行进一步参数判断。如果第三个参数不为O_RDONLY 则阻止,否则允许。

0x02 Ptrace-Seccomp

1. 原理

ptrace 是一个Unix和类Unix系统(如Linux)提供的系统调用,可以让一个进程观察和控制另一个进程的执行,还可以改变被跟踪进程的寄存器和内存。这使得它在实现诸如断点、单步执行和系统调用跟踪等调试功能时非常有用。我们的目标是监测或者修改系统调用的参数和返回值,即被监测的进程叫做tracee ,用于监测的进程叫做tracer ,为了能够实现监测,那么就可以使用ptrace 来简单的实现。
例如,strace 工具就是使用 ptrace 来跟踪进程执行的系统调用和接收的信号。另一个例子是 gdb,它使用 ptrace 来实现其调试功能。proot 也使用 ptrace 来拦截和修改系统调用,以在用户空间实现根文件系统的更改。
以 strace 使用 ptrace 来监视系统调用为例:
  1. 开始跟踪:当使用 strace 命令跟踪一个程序时,strace 会首先使用 fork 或 clone 系统调用创建一个新的进程。在新的子进程中,strace 使用 ptrace 系统调用与父进程建立跟踪关系,然后执行 execve 系统调用来加载并运行指定的程序。在父进程(也就是 strace 自身)中,strace 会进入一个循环,等待子进程的状态改变。
  1. 拦截系统调用:当子进程执行一个系统调用时,它会被暂停,父进程会收到一个信号。此时,strace 可以使用 ptrace 系统调用获取子进程的寄存器值,从而知道子进程试图执行的系统调用及其参数。strace 会将这些信息格式化并输出。
  1. 继续执行:获取完信息后,strace 使用 ptrace 系统调用告诉子进程继续执行,直到下一个系统调用,或者直到子进程退出。
  1. 处理信号:如果子进程接收到一个信号,strace 也可以看到这个信号,并将其记录下来。strace 可以选择将这个信号传递给子进程,或者阻止它。

2. 修改系统调用参数

以下为Ubuntu20.04 X86_64环境下的示例源码,这将处理syscall(SYS_getpid, SYS_mkdir, "dir", 0777);这样一个看似错误的系统调用,从而使进程实际执行syscall(SYS_mkdir, "dir", 0777); 具体而言就是通过ptracePTRACE_SYSCALL功能使被ATTACH的进程在进行系统调用前或系统调用结束时停下来,然后使用PTRACE_GETREGS PTRACE_SETREGS 两个功能获取系统调用时的寄存器并且重新设置。
notion image
其中3983分别为getpidmkdir的调用号,也就是在调用前后都停下来并且输出。
 

3. 监控_NR_openat调用并修改文件名使其读取其他文件内容

代码来源:
ptrace-seccomp-demo
xiaotujinbnbUpdated May 30, 2024
,该DEMO为ARM架构下的示例,在这里将其进行修改使其可以运行在Linux X86架构上并且修复小问题。
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESECCOMP); 表示要跟踪seccomp事件。当被跟踪的进程触发一个seccomp事件时,它会暂停执行,等待跟踪它的进程做出响应。
ptrace(PTRACE_SYSCALL, child, 0, 0);是让子进程继续执行,直到它到达下一个系统调用。在这个调用之后,子进程会继续执行,直到它发出一个系统调用,然后它会暂停执行,等待父进程的响应。waitpid(child, &status, 0);则是让父进程等待,直到子进程的状态发生变化。在这个调用之后,父进程会暂停执行,等待子进程的状态发生变化。当子进程发出一个系统调用并暂停执行时,waitpid函数会返回,然后父进程可以检查子进程的状态,获取系统调用的参数,然后根据需要修改这些参数。
💡
在修改数据时必须通过val = ptrace(PTRACE_PEEKTEXT, pid, addr + i * 8, NULL); val = *(long *)(s + i * 8) | val; 读出数据并进行合并,否则会造成子进程内存空间出现错误,如果不加这部分内容直接覆盖,会导致/home/lleaves/code/seccomp/b 开头三个字节为0。
输出,文件a中的内容为123456,c中为HappyHack!,在这里输出了c的文件内容,说明替换是成功的。
 

4. ptrace一个APP的主进程

如果使用父进程ptrace子进程则可以使用seccomp 拦截到系统调用,也可以做参数修改等操作,但是由于要处理子进程的信号,所以会阻塞在process_signal,因此一般都是由一个无关紧要的进程来循环处理信号监视系统调用。所以就fork一个子进程循环处理信号,从而实现无阻塞监听系统调用。
将上述源码编译为so,在合适的时机注入到APP即可
notion image
这种方案在可执行文件下,是可行的,能够跑的通。但是在app环境下,ptrace将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。(https://bbs.kanxue.com/thread-277544.htm)。
确实如上述所述,直接注入so然后fork出子进程后通过ptrace监测APP主进程会遇到很多阻碍,对于简单的APP或许没什么问题,但是一般会在ptrace稍微复杂一点的APP就会出现问题,而且注入的时机也需要考虑,还需要考虑对应APP的对抗手段。
 

5. syscall调用前后的监测

💡
PTRACE_SYSCALLPTRACE_CONT有着相同的处理,都是让子进程继续运行,其区别PTRACE_SYSCALL设置了进程标志PF_TRACESYS这样可以使进程在下一次系统调用开始或结束时中止运行。继续执行要保证清除单步执行标志。用户参数data为用户提供的信号,希望子进程继续处理此信号。如果为0则不处理,如果不为0则在唤醒子进程后向子进程发送此信号(在do_signal()和syscall_trace()函数中完成)。
💡
除此之外,判断信号的条件也十分重要 if (status >> 8 == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) 不会在调用结束时触发,当 seccomp 过滤器匹配到一个系统调用并且该过滤器的行为被设置为 SECCOMP_RET_TRACE 时,这个条件就会触发。具体来说,这个条件会在系统调用开始之前触发,这是因为 seccomp 过滤器在系统调用开始之前就会检查系统调用,
但是这个条件不会在系统调用结束时触发。if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) 会在系统调用前和结束都触发。
这段代码监视mincore的系统调用执行,int mincore(void *start, size_t length, unsigned char *vec);监测指定大小的页面是否处于物理内存中,一般用于内存扫描的检查,这里监测到mincore调用后将mincore第三个参数vec指向内存位置的值改为original & -256 ,实际上是将原始值的最低8位清零(这里实际不太严谨,但是因为只监测了一页,所以低8位就可以表示是否存在于物理内存),表示该内存并未被调入内存以绕过APP对内存扫描的检查,通过adb logcat日志输出可以看到在进行指定页面内存操作后,原本mincore第三个参数指向内存的值的低8位为1,在这里将其修改为0,APP输出页面并未调入内存,Hook成功。
使用frida将这个so注入
notion image

0x03 Frida-Seccomp

思路和代码来源:https://bbs.kanxue.com/thread-271815.htm 作者使用Frida的CModule编写Seccomp过滤规则,当遇到指定的系统调用时触发异常,然后通过Frida的Process.setExceptionHandler即可捕获异常并在自己写的回调中进行数据处理。作者在这里指出了几个容易出现问题的坑,在这里就不赘述,直接看原文即可。
编写下面的APP进行测试,下面APP用三种打开文件的方式打开文件并且读取内容,其中后两种写法不会被一般的钩子挂住,从而避免被Hook。通过Frida-Seccomp 的方案对系统调用和svc指令进行拦截。
使用对应项目拦截openat调用,能够输出文件名并且能够修改参数和返回值。
notion image

0x04 Sigaction-seccomp

参考这位大佬的代码,在ARM64设备进行实验 https://bbs.kanxue.com/thread-277544.htm
  1. 利用fork + ptrace方式,来捕获seccomp的SECCOMP_RET_TRACE信号,从而达到劫持svc调用的目的。这种方案在可执行文件下,是可行的,能够跑的通。但是在app环境下,ptrace将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。
  1. 利用frida + seccomp方式通过Process.setExceptionHandler来捕获SECCOMP_RET_TRAP信号,并且为了避免hook时死循环递归(hook函数中调用svc又再次被seccomp过滤发出SECCOMP_RET_TRAP信号),该项目通过创建新线程的方式来规避,但这种方式在处理多线程或者多进程任务时处理起来很麻烦。
💡
前面两种方案的缺点显而易见,ptrace-seccomp不易实现兼容性太差,frida-seccomp 效率极低。
sigaction() 是一个 Unix 系统调用,用于检查和更改信号的行为。信号是 Unix 和 Linux 系统中的一种软件中断,可以用来告诉进程发生了某种情况。允许进程改变系统对给定信号的反应。它可以用来设置一个函数(称为信号处理器),当特定信号被接收时,这个函数就会被调用。sigaction 结构如下:
sa_handler:是一个指向信号处理函数的指针。 sa_sigaction:是一个指向信号处理函数的指针,该函数接受三个参数,可以提供关于信号的更多信息。 sa_mask:定义了在处理该信号时需要阻塞的其他信号。 sa_flags:修改信号处理的其他方面。 sa_restorer:这是一个过时的选项,不应在新的代码中使用。
对 __NR_openat 系统调用进行监控和处理。当 __NR_openat 系统调用被调用时,如果它的第四个参数不等于 SECMAGIC,则会触发 SIGSYS 信号,然后由 sig_handler() 函数处理这个信号。在 sig_handler() 函数中,它将 __NR_openat 系统调用的参数打印出来,并重新执行 __NR_openat 系统调用,但是这次将第四个参数设置为 SECMAGIC,以避免无限循环。

1. 编译so并注入

notion image
mincore实现监测并修改第三个参数指向内存的后4字节为0,表示对应内存不处于物理内存中。
PS: 下面的27纯属脑抽写错了(代码已经调整好了),本意为232
notion image
 

2. 利用frida的CMoudle省去注入的步骤

在实战时根据自己需求修改下面的define,其中target_nr 是目标系统调用号。
在拦截到系统调用后会再次进行系统调用,防止再次被拦截,就需要一个寄存器来放一个标识符SECMAGIC ,避免反复调用crashSECMAGIC_POS 即为对应系统调用所不需要的第一个寄存器,比如openat需要三个参数,那么SECMAGIC_POS 填3即可,因为寄存器从x0开始,args[3]即为第四个寄存器。
然后就是在sig_handler 中写劫持逻辑。如果想拦截更多的系统调用,就需要重写seccomp filter
监控文件的打开
notion image
劫持mincore svc调用使其无法检测页面是否被调入物理内存。
notion image
修改游戏中检测内存扫描的大量mincore svc调用
notion image

0x05 再会WMCTF2023 BabyAnti2

1. 最初的解法

需要处理大量的mprotectmincore,并且这部分内容必须通过逆向和反复测试来验证,工作量极大(mprotect开出的内存中存在mincoresvc指令调用,通过搜索内存中svc指令的方式较为困难)。除此之外,由于设备的不确定性,过早或者过晚对mincore进行Hook都会使程序Crash,差点做奔溃了。
不过可以看到在下面的脚本中有这么一段,这算是很优雅的解法了,属于非预期。但是还是需要克服flutter逆向的困难,Transform2D::y__equals__ 符号来之不易。题目采用了较新版本的flutterSDK,导致现有工具无法直接获取符号(reflutter就不行),重新编译了flutter对应版本的libflutter.so ,在其处理libapp.so快照时拿到符号,但是仅能恢复部分符号,具体过程可以参见前面的文章(并未尝试Blutter ,有兴趣可以尝试)。
搜索内存svc指令,并且反复调整脚本,最终hook住全部的mincore,工作量极大。脚本太长太丑陋,不放了。

2. 再战!

这次利用了Frida-seccomp对svc的mincore调用进行拦截(为什么不用ptrace呢,因为不知道出现了什么问题,ptrace方案会失败),这次就简单了很多,不需要管mprotect开出了哪些内存空间,而又在哪些内存中存在mincore的svc调用。只需要seccomp规则写明拦截mincore 调用,然后在遇到mincore后抛出异常TRAP ,由frida 捕获异常并修改mincore调用的第二个参数指向位置的值为0,即不处于物理内存。
然后使用GG修改器就可以愉快的搜索内存而不报错了,但是代价是什么呢,代价是游戏运行极其缓慢,mincore的大量调用导致游戏不停抛出异常并由frida进行处理。游戏肉眼可见的卡(极其的卡,对比图可以看下面能否再快一点!)。
notion image
 

3. 能否再快一点!

由于太慢了,所以能不能再快一点,解决方案就是将Frida的异常处理转到APP内部去,那么这个时候就可以注入so来做sigaction信号处理。
将上面的c++代码编译为so,通过frida直接注入,但是最好在android_dlopen_ext 最初被调用时注入,这时不容易出现问题,并且有时候需要多重试几次,直到logcat输出大量mincore日志。
速度对比,左侧使用Frida-seccomp方案,右侧使用Sigaction-seccomp
notion image

0x06 总结

通过这篇文章的撰写,基本上掌握了这三种方法,但是对于Ptrace方案还有更多骚操作,但是鉴于篇幅和时间不在这里赘述,参考项目PROOT即可。经过实验可以发现:
  • Ptrace-seccomp方案兼容性不太好,容易出现各种问题,而且需要处理进程间的问题,包括防止被附加APP自己开进程自己附加自己等问题。
  • Frida-seccomp方案,代码是成型的,每次只需要改一小部分即可。不需要注入so,不需要fork进程,直接起Frida即可,缺点是速度较慢。
  • Sigaction-seccomp方案在速度方面和代码复杂度方面以及兼容性方面都有着优势。

0x07 参考

  1. https://blog.csdn.net/x646602196/article/details/130137830
  1. https://bbs.kanxue.com/thread-275511-1.htm
  1. https://bbs.kanxue.com/thread-273160.htm
  1. https://bbs.kanxue.com/thread-271921.htm
  1. https://bbs.pediy.com/thread-271815.htm
  1. https://manpages.ubuntu.com/manpages/xenial/man2/seccomp.2.html
  1. https://github.com/proot-me/proot
  1. https://bbs.kanxue.com/thread-277544.htm
  1. https://blog.ssrf.in/post/bypass-seccomp-with-ptrace/
 
Android grantUriPermission与StartAnyWhere野火IM-APP端聊天数据库分析
  • Twikoo