type
status
date
slug
summary
tags
category
icon
password
0x00 引言0x01 Seccomp1. 介绍2. BPF filter数据结构3. BPF程序基本指令4. Seccomp返回值5. 配置Seccomp过滤器6. 拦截__NR_openat并根据打开模式决定是否放行0x02 Ptrace-Seccomp1. 原理2. 修改系统调用参数3. 监控_NR_openat调用并修改文件名使其读取其他文件内容4. ptrace一个APP的主进程5. syscall调用前后的监测0x03 Frida-Seccomp0x04 Sigaction-seccomp1. 编译so并注入2. 利用frida的CMoudle省去注入的步骤0x05 再会WMCTF2023 BabyAnti21. 最初的解法2. 再战!3. 能否再快一点!0x06 总结0x07 参考
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_STMT
和BPF_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
来监视系统调用为例:- 开始跟踪:当使用
strace
命令跟踪一个程序时,strace
会首先使用fork
或clone
系统调用创建一个新的进程。在新的子进程中,strace
使用ptrace
系统调用与父进程建立跟踪关系,然后执行execve
系统调用来加载并运行指定的程序。在父进程(也就是strace
自身)中,strace
会进入一个循环,等待子进程的状态改变。
- 拦截系统调用:当子进程执行一个系统调用时,它会被暂停,父进程会收到一个信号。此时,
strace
可以使用ptrace
系统调用获取子进程的寄存器值,从而知道子进程试图执行的系统调用及其参数。strace
会将这些信息格式化并输出。
- 继续执行:获取完信息后,
strace
使用ptrace
系统调用告诉子进程继续执行,直到下一个系统调用,或者直到子进程退出。
- 处理信号:如果子进程接收到一个信号,
strace
也可以看到这个信号,并将其记录下来。strace
可以选择将这个信号传递给子进程,或者阻止它。
2. 修改系统调用参数
以下为
Ubuntu20.04 X86_64
环境下的示例源码,这将处理syscall(SYS_getpid, SYS_mkdir, "dir", 0777);
这样一个看似错误的系统调用,从而使进程实际执行syscall(SYS_mkdir, "dir", 0777);
具体而言就是通过ptrace
的PTRACE_SYSCALL
功能使被ATTACH
的进程在进行系统调用前或系统调用结束时停下来,然后使用PTRACE_GETREGS
和PTRACE_SETREGS
两个功能获取系统调用时的寄存器并且重新设置。 其中
39
和83
分别为getpid
和mkdir
的调用号,也就是在调用前后都停下来并且输出。3. 监控_NR_openat调用并修改文件名使其读取其他文件内容
代码来源:,该DEMO为ARM架构下的示例,在这里将其进行修改使其可以运行在Linux X86架构上并且修复小问题。
ptrace-seccomp-demo
xiaotujinbnb • Updated Jul 6, 2024
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即可
这种方案在可执行文件下,是可行的,能够跑的通。但是在app
环境下,ptrace
将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。(https://bbs.kanxue.com/thread-277544.htm)。
确实如上述所述,直接注入so然后
fork
出子进程后通过ptrace
监测APP主进程会遇到很多阻碍,对于简单的APP或许没什么问题,但是一般会在ptrace稍微复杂一点的APP就会出现问题,而且注入的时机也需要考虑,还需要考虑对应APP的对抗手段。5. syscall调用前后的监测
PTRACE_SYSCALL
和PTRACE_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
注入0x03 Frida-Seccomp
思路和代码来源:https://bbs.kanxue.com/thread-271815.htm 作者使用Frida的CModule
编写Seccomp
过滤规则,当遇到指定的系统调用时触发异常,然后通过Frida的Process.setExceptionHandler
即可捕获异常并在自己写的回调中进行数据处理。作者在这里指出了几个容易出现问题的坑,在这里就不赘述,直接看原文即可。
编写下面的APP进行测试,下面APP用三种打开文件的方式打开文件并且读取内容,其中后两种写法不会被一般的钩子挂住,从而避免被Hook。通过
Frida-Seccomp
的方案对系统调用和svc指令进行拦截。使用对应项目拦截
openat
调用,能够输出文件名并且能够修改参数和返回值。0x04 Sigaction-seccomp
参考这位大佬的代码,在ARM64
设备进行实验 https://bbs.kanxue.com/thread-277544.htm
- 利用fork + ptrace方式,来捕获seccomp的
SECCOMP_RET_TRACE
信号,从而达到劫持svc调用的目的。这种方案在可执行文件下,是可行的,能够跑的通。但是在app环境下,ptrace将不再适用,容易触发各种异常信号,并且在ptrace环境下容易被各大厂商app的安全模块检测出来。
- 利用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并注入
对
mincore
实现监测并修改第三个参数指向内存的后4字节为0,表示对应内存不处于物理内存中。PS: 下面的27纯属脑抽写错了(代码已经调整好了),本意为232
2. 利用frida的CMoudle省去注入的步骤
在实战时根据自己需求修改下面的
define
,其中target_nr
是目标系统调用号。在拦截到系统调用后会再次进行系统调用,防止再次被拦截,就需要一个寄存器来放一个标识符
SECMAGIC
,避免反复调用crash
。SECMAGIC_POS
即为对应系统调用所不需要的第一个寄存器,比如openat
需要三个参数,那么SECMAGIC_POS
填3即可,因为寄存器从x0
开始,args[3]
即为第四个寄存器。然后就是在
sig_handler
中写劫持逻辑。如果想拦截更多的系统调用,就需要重写seccomp filter
。监控文件的打开
劫持
mincore svc
调用使其无法检测页面是否被调入物理内存。修改游戏中检测内存扫描的大量
mincore svc
调用0x05 再会WMCTF2023 BabyAnti2
1. 最初的解法
需要处理大量的
mprotect
和mincore
,并且这部分内容必须通过逆向和反复测试来验证,工作量极大(mprotect
开出的内存中存在mincore
的svc
指令调用,通过搜索内存中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进行处理。游戏肉眼可见的卡(极其的卡,对比图可以看下面
能否再快一点!
一节)。3. 能否再快一点!
由于太慢了,所以能不能再快一点,解决方案就是将Frida的异常处理转到APP内部去,那么这个时候就可以注入so来做sigaction信号处理。
将上面的c++代码编译为so,通过frida直接注入,但是最好在
android_dlopen_ext
最初被调用时注入,这时不容易出现问题,并且有时候需要多重试几次,直到logcat输出大量mincore日志。速度对比,左侧使用Frida-seccomp方案,右侧使用Sigaction-seccomp
0x06 总结
通过这篇文章的撰写,基本上掌握了这三种方法,但是对于Ptrace方案还有更多骚操作,但是鉴于篇幅和时间不在这里赘述,参考项目PROOT即可。经过实验可以发现:
- Ptrace-seccomp方案兼容性不太好,容易出现各种问题,而且需要处理进程间的问题,包括防止被附加APP自己开进程自己附加自己等问题。
- Frida-seccomp方案,代码是成型的,每次只需要改一小部分即可。不需要注入so,不需要fork进程,直接起Frida即可,缺点是速度较慢。
- Sigaction-seccomp方案在速度方面和代码复杂度方面以及兼容性方面都有着优势。
0x07 参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/Android-Seccomp
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章