新手机到了必须狠狠的折腾(也没有多狠),之前用脱壳网站的时候对某bang壳的app进行脱壳会直接任务失败,那要是每次都去分析so中dex的解密逻辑那不麻烦死了,自己弄一个脱壳系统是比较一劳永逸的方案,可以在手机上先把检测项给过掉或者隐藏掉就可以美美脱壳了,社区内的前辈开源了许多的优秀项目作为参考(FART、youpk、FARText、r0dump等等)
在壳保护对抗的过程中,往往伴随着各种各样的检测,譬如:frida检测、root检测、xp检测、调试与adb检测、模拟器检测、完整性与多开检测、签名与ROM校验等等。比较痛苦的就是这些检测项被壳检测到了而触发闪退、上报等操作,影响分析。想要对抗一般需要对壳进行比较深入的挖掘与分析,或者有一定的经验可以快速定位bypass。
那么依托于syscall-trace与隐藏frida特征能否减轻这部分的对抗,如前面的脱壳系统一样做到一定程度上的一劳永逸。
当然是可以的。通过syscall-trace可以快速知道这个app的某个so做了哪些疑点比较大的操作,比如说开辟了一块比较大的匿名内存,比如说将权限修改成RWX,比如说openat了某个路径,这些信息都可以帮助我们在分析过程中以极低成本且能快速获取一部分信息用于加速分析进度。
隐藏frida特征主要是为了缩小壳保护的检测项范围,哪怕被检测到了也可以知道自己大概率是什么地方被检测到了,可以根据自己暴露的特征进行反向定位检测函数,从而达到过检测的目的。
本文是比较详细的入门级实操教程,详细记录了我为了实现上面一劳永逸工具组的落地流程,以及避坑指南。附件提供成品。

先说明 解锁BL会清空设备数据 我这新手机就无所谓了.
能不能解锁就要先进入Fastboot模式,也就是这个页面。

可以通过adb命令进入
然后去看,这里显示unlockable说明OEM解锁是允许的

首先打开开发者模式,关于本机里面狂点build号后即可进入开发者模式

然后找到开发者模式

打开OEM

下载一下platform-tools,里面有 adb 和 fastboot 两个工具
下载好后解压到文件夹

进入fastboot
确认 fastboot 能认到设备:
我这里用的数据线无法找到设备,更换了买iqoo手机送的数据线就可以了,很多线连adb都无法识别到设备一般都是充电线,必须要去网上买数据线(明确标注"支持数据传输 / Data Sync"的线)

执行解锁
执行完这个命令并非就是解锁好了,只是可以选择解锁了

还需要手动去选择,这里通过音量按键上下切换到Unlock the bootloader
然后按下电源键确认

这样就是解锁好了

在安装路径创建这么两个文件夹 VMS 和VMS\Ubuntu-LineageOS23

下载地址
[下载地址](27fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6i4m8H3L8%4u0@1i4K6u0W2j5Y4u0G2j5h3c8U0L8$3#2Q4x3X3g2U0L8$3#2Q4x3V1k6Y4M7X3!0#2M7q4)9J5c8X3g2U0P5q4)9J5c8Y4m8J5L8$3c8#2j5%4c8V1L8%4N6F1L8r3!0S2k6s2y4Q4x3@1k6K6N6h3u0X3j5h3#2A6L8s2W2Q4x3@1c8h3e0i4N6S2M7X3f1`. Workstation Pro&freeDownloads=true)


硬件兼容性:Workstation 17.x
安装来源:稍后安装操作系统




网络类型:使用网络地址转换
I/0 控制器类型:LSI Logic (L) 就是默认的那个

选择磁盘:创建新虚拟磁盘

这里点击自定义硬件

勾选

切换到usb3.1

然后点击关闭,再点击完成



我这边拉取+编译总共花了一天半左右,配置如下可以作为参考

重启后分辨率自适应和剪贴板共享就正常了。
其中 libncurses5 是必须的,AOSP 里部分预编译 host 工具仍然链接 ncurses5,缺了会报 libncurses.so.5: cannot open shared object file。android-sdk-platform-tools-common 提供 adb 的 udev 规则,否则非 root 下连手机会显示 no permissions。
配置 PATH
验证
core.fsyncObjectFiles true 会强制 git 写对象时落盘,牺牲一点速度换取异常断电/写入中断时的数据安全,对虚拟机环境很有必要。
git lfs install 在非 git 目录下会附带一句 rev-parse 的报错,只要最后输出 Git LFS initialized. 就是成功的,可以忽略。



bluejay(Pixel 6a)官方支持 lineage-23.2(基于 Android 16):
三条命令,全部无输出才算成功:
第一条查同步日志有无失败,第二条查工作区文件有无缺失或改动,第三条查每个仓库的 git 对象库是否完整。git 是内容寻址的,fsck 通过就不存在"内容和远端不一致"的可能,只存在"下漏了"和"下坏了",这三条正好覆盖。
blobs 是闭源驱动和固件,不在开源仓库里,必须自己提取。有两种方式。
官方 nightly 是 user 版本,adb root 被禁用,所以大多数情况下只能走这条路。
下载对应设备的官方包:
具体版本号去 40cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8%4N6F1L8r3!0S2k6q4)9J5k6h3I4A6L8X3g2S2k6$3g2G2M7#2)9J5k6h3!0J5k6#2)9J5c8X3c8W2N6X3W2U0k6i4y4Q4x3V1k6T1L8s2g2W2K9X3q4&6i4K6u0r3j5Y4g2A6L8r3c8K6 查最新的。

提取:
方式二:从已刷机的设备提取
仅当手机上跑的是对应版本的 LineageOS 且已获取 root 时可用:
无论哪种方式,驱动都会被提取到 ~/android/lineage/vendor/google。完成后确认一下:
Tensor 芯片的 Pixel(6/6a/7/8/9 系列)内核用 Kleaf/Bazel 单独构建。device/google/bluejay-kernels/6.1/ 在 git 里只有几个模块列表文件,.gitignore 排除了所有编译产物,所以编译前必须往这个目录放入内核产物,否则会报:
LineageOS 已经把内核编译自动化了。device/google/bluejay/device-bluejay.mk 里定义了 TARGET_KERNEL_PLATFORM_SOURCE,执行 breakfast 时会自动调用 build_kernel,依次完成下面几件事:
所以不需要自己去单独同步、编译内核。下面两种方式选一种就行。
直接执行第十一步的 breakfast bluejay。内核 manifest 里的仓库来自 android.googlesource.com,如果配置了镜像,注意下面几点:
看到 Kernel build output copied to device/google/bluejay-kernels/6.1/ 就说明成功了。
第十二步已经下载过 OTA 包,里面有 LineageOS 官方编好的内核,版本和提取的 blobs 完全一致。
1. 拆出分区镜像
2. 解出 dtb 和内核模块
3. 放进内核目录并校验
BoardConfig-common.mk 会从这个目录读取 boot.img、dtbo.img、*.dtb 和 *.ko,这几类文件都有了就可以编译。
4. 关闭 lunch 时的自动内核编译
brunch 会自动选择目标并执行全量编译。
查看产物:
Google 设备的 USB vendor ID 是 18d1
创建 udev 规则并授权:
然后确认在 plugdev 组里
最后重启 adb 服务(手机确认一下)
先刷 boot 和 dtbo,然后重启回 bootloader
刷 vendor_boot(这就是 recovery)
进 recovery
recovery长这样

第一步,先格式化数据


确认格式化

第二步:在 recovery 菜单里选 Apply update → Apply from ADB
到这一步要先确定usb是否能够正常连接到虚拟机,如果不能有两个办法,第一个就是点击下图页面的坐上角返回,然后重新进入。
第二个就是重启虚拟机or重启电脑

确认设备链接
刷入

选 No → 回主菜单 → Reboot system now 先开机再说

首先需要确定设备的内核版本
APatch 官方只支持 ARM64、Android 内核版本 3.18 - 6.12
这个是我的

理想输出是 CONFIG_KALLSYMS=y
如果这条命令报错说没有 /proc/config.gz,说明内核没开 CONFIG_IKCONFIG_PROC
装 APatch Manager
直接从官方的github上去下载就行了,下载好后安装到手机上。
准备 boot.img,这里直接用之前编译出来的boot.img就行,把它传到手机上
在 APatch app里 patch
打开 APatch,然后点击右上角的patch图标,选择/sdcard/Download/boot.img
设置一个 SuperKey
123456abc
patch 完会生成一个新 img
把 patch 好的 img 从手机拉回电脑
一定要记得点一下这个安卓系统补丁!!!

六条,缺一不可
输出里有 uid=0(root)
这五项全部等于 y。不满足:需要重编内核开启对应选项。
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_PERF_EVENTS=y
CONFIG_DEBUG_INFO_BTF=y 并且 /sys/kernel/btf/vmlinux 文件真实存在(通常几 MB)。两个都要满足——配置开了但文件不在也不行。不满足:如果只能重编内核,务必加上 CONFIG_DEBUG_INFO_BTF=y
输出里能看到 sys_enter 和 sys_exit。不满足:正常内核都有。若没有,检查 tracefs 是否挂载。
能 grep 到 bpf_probe_read_user 这个符号(地址显示为全 0 是权限问题,不影响判断,有符号名即可)
不满足:内核太旧,需要 backport 或换设备。
第一次可能是 Enforcing,执行 setenforce 0 后第三次能变成 Permissive
不满足:如果 setenforce 0 无效(有些设备锁死),就得处理 SELinux policy 或换到合适的域运行,会麻烦不少。
编完记下 bpftool 路径(后面 Makefile 要用):
/home/aosp/libbpf-bootstrap/examples/c/.output/bpftool/bootstrap/bpftool

记下 libbpf.a 的实际路径(可能在 lib64/ 也可能在 lib/),Makefile 里要填对。
/home/aosp/syscall-tracer/libbpf-arm64/usr/lib64/libbpf.a

从手机拉 BTF 并生成 vmlinux.h(用第 3 步 find 出来的 bpftool 路径):
ls -l 看到几 MB 的大小就成了。
然后生成 vmlinux.h:
行数几万行、grep 结果是 1,就说明 vmlinux.h 正常。

6.安装vscode
一行搞定
打开项目
7.新建文件
然后新建以下文件
文件一: tracer.bpf.c(跑在内核里的 BPF 程序,就抓 openat)
文件二: tracer.c(跑在手机上的加载器,把抓到的打印出来)
文件三: Makefile(注意:Makefile 里缩进必须是 Tab,不能是空格,VSCode 里粘完检查一下)

8.开始编译
完事之后就直接开始编译
编译之前还需要补一下这两个东西
现在在 VSCode 里打开终端,然后:
先看这条输出的路径末尾是 lib64 还是 lib。确认后(如果是 lib 就去 Makefile 把 lib64 改掉),直接编:

是 lib64,那 Makefile 最后那行的 lib64 不用改,保持原样。
编译完了,没啥毛病

9.刷入测试
运行

前面只是快速的搭建好了开发平台,并且成功的编译刷入做了测试,但是对其的了解还是知之甚少,下面就对ebpf来做扫盲和刚刚的程序解析学习。
计算机运行的程序分为两个世界。
用户态:开发者平时写的所有程序,比如说微信、QQ、浏览器,包括前面写的tracer.c 都跑在这里。用户态权限受限,不能直接碰硬件,不能直接读取别的进程的内存,要想做这些事情就必须要依赖内核。
内核态:操作系统内核本身运行的地方。管理内存、进程、硬件、文件系统,权限最高。
这两个世界是隔离的,中间的桥梁就是系统调用(system call,简称syscall)。当程序要打开一个文件,他自己没有权限直接操作磁盘,于是他会发起一个叫openat的系统调用,把请求交给内核,内核在内核态完成时机操作,再将结果返回。
ebpf是一项能够在内核里运行沙箱程序、而无需修改内核源码或加载内核模块的技术。写一小段代码,它可以被安全的加载进正在运行的内核并挂到某个事件上,事件发生时这段代码就执行。
往内核里塞代码本身是一件风险较高的事情,一旦有bug(死循环、越界访问内存)整个系统会崩。ebpf靠验证器(verifier)保证安全。
官方文档明确说明,程序在加载进内核前,验证器会对他做安全检查,以此来确保他会正常结束(不会死循环)、没有越界内存访问等等,任何一项不满足就拒绝加载。这就是为什么BPF程序有诸多限制(不能用任意循环、栈空间很小、不能调用任意内核函数),这些限制是为了让验证器能够证明程序安全。通过验证后,程序还会被JIT编译成机器码以提升运行速度。
一个ebpf项目由两个程序加一种共享存储组成。
内核程序(tracer.bpf.c)编译成BPF字节码后加载进内核,挂在钩子上运行,负责在事件现场采集信息。
用户态程序(tracer.c)普通程序,负责把内核态程序加载进内核、挂载到钩子上、并循环将采集到的数据取回处理。
map:官方定义中,map是“存在于内核中的数据结构,ebpf程序和用户态程序都能访问,因此是两者之间的通信层”。
前面用到的是其中一种类型叫做ring buffer(环形缓冲区,BPF_MAP_TYPE_RINGBUF),专门用于内核态单项、按顺序的把一条条消息发给用户态。
官方定义:BPF程序不能调用任意内核函数,否则会破坏内核稳定、也会让BPF程序绑死在特定内核版本上。作为替代,内核开放了一批由内核定义、可从ebpf程序调用的函数,这就是辅助函数,他们构成一套知名且稳定的API。在内核态代码里能做的所有事,基本都通过调用这些bpf_开头的辅助函数完成。
从手机BTF导出的头文件,包含这台手机内核的所有数据结构定义。BPF代码要读取内核数据结构就必须知道他们的布局,而不同内核版本布局可能不同,所以针对目标手机生成专属的vmlinux.h
提供辅助函数的声明,以及SEC等宏的定义。
BPF程序必须声明许可证,因为许多辅助函数只允许GPL兼容的程序调用,不声明会导致加载失败。SEC("license")里的SEC是宏,作用是把这个变量放进编译产物中名为license的节区。一个编译好的文件内部分成若干块,每块叫一个section;ebpf用section的名字来标识每块内容的用途,加载器和内核靠section名来识别这是许可证、这是map、这是要挂载的程序,所以后面出现的每个SEC(...)都是在给对应的东西贴用途标签
定义那个ring buffer,变量名 rb.SEC(".maps")标明它是一个map。type指定类型为ring buffer。对ring buffer而言,max_entries表示缓冲区的字节大小,这里是256×1025= 256KB。内核为此缓冲区分配256KB,采集的数据先堆在这里等用户态来取。官方要求ring buffer的大小必须是4KB的整数倍且为2的幂,256KB 满足这个条件。
自定义的一条记录格式,pid和进程名comm。
极其重要:用户态tracer.c里也定义了字段完全相同的 struct event。因为内核态往缓冲区写入的是这块结构体的原始字节,用户态读出来后按自己的结构定义区解读,变量只要有任何差异(字段顺序、类型、大小)解读就会出错。所以两端必须严格一致
tp是tracepoint的缩写。tracepoint是内核开发者预先埋在内核代码里的固定钩子,分布在内核各关键位置。raw_syscalls/sys_enter这个特定tracepoint会在任何系统调用即将开始时触发。所以这行的意思是把handle_sys_enter挂到这个tracepoint上,挂上去后,系统里任何进程发起任何系统调用,内核都会先执行一次这个函数。参数ctx由内核在触发时传入,携带这次系统调用的信息,其类型struct trace_event_raw_sys_enter定义在vmlinux.h内。
sys_enter会捕获所有系统调用,数量极大,所以过滤。ctx->id是这次系统调用的编号,arm64上 openat编号是56,
有一个特别的点 就是这里的系统调用编号是跟架构相关联的,arm64上openat是56,x86_64上则是另外一个号。
bpf_ringbuf_reserve是辅助函数,在ring buffer里预留一块空间。参数依次是:往那个ring buffer预留(&rb)、预留多大(sizeof(*e),刚好一个event)、标志位(填0)。返回指向预留空间的指针e。若缓冲区已满(用户态取得太慢导致堆积)则预留失败返回NULL。这里就必须要对e做检查,这是验证其强制要求的。
ring buffer 的使用套路固定为三步:reserve(预留)→ 填数据 → submit(提交)。
bpf_get_current_pid_tgid()返回一个64位的整数,官方定义是tgid << 32 | pid ,也就是高32位是TGID,低32位是PID。关键在于内核里PID和TGID这两个词的含义跟日常习惯不同,内核里TGID(线程组ID)才是平时说的进程ID,内核里的PID其实指的是单个线程的ID。所以这行>> 32 取出的是高32位,即TGID,也就是日常意义上的进程号。变量名叫pid只是沿用习惯叫法,他存的实际是TGID(进程号)。如果以后想要区分具体哪个是线程,就取低32位(内核意义上的PID)
辅助函数,把当前进程的名字拷贝到指定位置。参数为目标地址(&e->comm)和最多拷贝的字节数(sizeof(e->comm),即16)
bpf_ringbuf_submit 提交这块已填好的数据,提交后这条记录正式进入ring buffer,用户态即可读到。第二个参数是标志位,填0。最后return 0 表示函数正常结束。
内核这边做的事情一个总结就是:每次系统发生调用 》判断是不是openat 》 是则在缓冲区预留空间 》 填入进程号和进程名 》 提交
前面三个是标准c库头文件(打印、信号处理等)。libbpf.h 是libbpf的头文件,libbpf是官方提供的、专门操作ebpf的c库,把加载程序、管理map、读ring buffer这些底层操作都封装好了。
骨架文件,不是手写的,是编译时由bpftool从编译好的内核态目标文件tracer.bpf.o自动生成。它把加载这个特定BPF程序、找到其中的map、挂载它等操作包装成以tracer_bpf__开头的函数,省去手写底层加载代码。
跟内核态那个完全一致,用于正确解读从缓冲区读回的字节。
用于Ctrl+C的退出。exiting是标志变量。初始0。on_sig是信号处理函数,按ctrl+c会产生中断信号,系统调用它把exiting设为1,主循环看到后就退出。volatile告诉编译器这个变量可能被随时改变,不要做优化缓存。
回调函数,处理数据的地方。所谓回调,是把这个函数交给libbpf,之后每当ring buffer里有一条新数据,libbpf就自动调用一次它。参数data指向那条数据(内核态提交的字节),struct event *e = data把它按struct event解读,于是能取出e->pid、e->comm。%-6d表示打印整数左对齐占6格。以后过滤、统计、写文件的等逻辑都加载这个函数里。
main是程序入口。skel是骨架对象指针,rb是ring buffer读取器指针。signal(SIGINT,on_sig)注册:收到SIGINT信号(ctrl+c产生)时调用on_sig。
骨架函数,一步完成打开BPF对象并把内核态程序加载进内核。就在加载这一步,前面讲的验证器会检查程序安全性,不通过则加载失败。
创建ring buffer 读取端。skel->maps.rb是骨架关联好的、内核里那个名叫rb的map;bpf_map__fd(...)取得他的文件描述符;on_event是回调函数,告诉libbpf每读到一条数据就调用它,后面两个是NULL是可选参数了,这里用不着。
打印提示后进入主循环。while(!exiting)表示只要exiting为0就一直循环。ring_buffer__poll(rb,100)去缓冲区查看有无新数据,参数100是超时毫秒数,最多等100毫秒,期间有数据就处理(自动触发on_event),没数据就返回再进入下一轮。用"轮询+超时"是为了让循环能定期检查exiting,从而在ctrl+c时能够及时退出。
退出循环后清理:释放ring buffer 读取器、销毁骨架对象(会自动把BPF程序从内核卸载)。return 0表示正常结束了。
编译阶段clang把tracer.bpf.c编译成BPF字节码 tracer.bpf.o,bpftool从它生成骨架tracer.skel.h交叉编译器把include了骨架的tracer.c编译成手机arm64可执行文件tracer。
运行阶段:在手机运行tracer》骨架函数把内核态程序加载进内核(验证器检查通过)》挂载到sys_enter reacepoint 》进入循环读ring buffer。此后每发生一次系统调用,内核就执行一次handle_sys_enter,它过滤处openat,把进程号和进程名写进ring buffer;用户态读到后触发on_event打印。ctrl+c后清理资源退出,内核程序被卸载。
目前论坛当中比较安利的就是这两篇帖子,讲的很好,推荐看看。
《**Linux 内核监控在 Android 攻防中的应用**》
《**定制bcc/ebpf在android平台上实现基于dwarf的用户态栈回溯**》
下面只是根据程序的设计思路来进行部分讲解,提供编译好的成品。
先明确一下目标:对某个指定的app将他执行的系统调用全部记录下来,并且能够知道每次调用是来自哪个so。
面对反调试的项目有两个比较难的点,首先就是app启动后检测到环境异常或者处于被调试状态会很快的就闪退,常规的做法就是进程活着的时候去读取他的/proc/pid/maps来做地址归属,但是这个进程根本不会活到去读。
另外一个就是进程启动早期身份不明确。app进程是zygote fork出来的。fork出来的那一刻他的uid还是0,要过一会才会被设置成目标uid。这中间有一段盲窗,光看uid是分辨不出来是不是目标app的。
既然不能指望进程活着的时候去读取他的maps,那就自己积累一份这个进程的内存里哪段地址属于哪个so的表,而且这个表不依赖进程还活着。
表数据来源有三个:
一个是zygote。程序启动的时候读取一次 zygote 的 maps 存下来,只留有文件名的段(匿名段不在种子里)。因为所有app都是fork zygote来的,继承了同一批地址布局,所以这就是每个新生app的出生时内存布局,进程一秒没活也能用。
二是增量更新。进程运行中会调 mmap/munmap/mprotect来增删改内存映射,这些调用的参数本身就说明了哪段地址发生了什么变化,抓到这些syscall就能更加精确更新影子表,其中 mmap 只有 MAP_FIXED 能在入口建段,非 FIXED 的段靠 kretprobe 取返回值才建得起来。
第三就是刷新,如果说进程还活着,那就读取一次真的maps,然后做整表覆盖,让影子表更加准确可靠。
这三个来源整合之后,哪怕进程死了,这份表依旧是他最后一刻的内存布局。归属照样成立;但 exec 过的新进程(崩溃转储进程)不适用,它的地址空间和 zygote 无关,只能在它活着时读到 maps
内核侧,目标进程调了一个openat,kprobe触发,BPF抓取寄存器、一小段栈、路径参数,判断这个进程是不是目标,如果是的话就把抓取到的信息写入ringbuf。
用户态侧,一个读取线程从ringbuf把记录读出来,根据进程身份是否确认,决定直接入队处理还是先扣进盲窗暂存,盲窗记录被扣住直到该进程露出目标 uid;退出标记或超时才按出身放行或丢弃——所以端到端滞后几秒是正常的,要看的是排队滞后。入队后由一组worker线程去查影子表定位地址属于哪个so,必要的时候展栈还原调用链,解码syscall参数 (把数字翻译成可读的标志位,把 fd 按事件自己的时间戳回查 fd 时间线翻译成路径,不能用现在的表:读表的 worker 滞后于消费线程,用"现在"会把早期 mmap 命名成后来才打开的文件),最后格式化成一行输出。
其实我做了好几个版本,尝试过交叉编译成一个文件,但是。。。失败了,后边想着不去依赖BPF去做过滤,但是但是但是,整个系统哪怕你不去触碰手机他一秒钟都有大十几万的syscall 数据量过于庞大,导致缓冲区根本接不下,丢包特别严重(通过一系列优化收紧入口其实后面也能用,就是很勉强,丢包概率有百分之四十,很多时候都得重启好几次才能抓到),后面受不了了,重新整一个,也就是现在这个(挺好用的其实)。
内核BPF代码能够做的事情很有限,最主要是验证器很严格、并且不能用复杂数据结构,也不能去做DWARF展栈,所以说这里内核侧只需要去抓信息,所有重活全部交给用户态去做。
内核侧要抓的东西就是寄存器+一段原始栈,为什么不直接去抓调用链?因为展栈要去读取ELF文件,要做比较复杂的计算。所以说内核只需要负责把展栈的原料准备好(寄存器 pc/sp、栈内容)交给用户态慢慢去计算。
混着三种尺寸、四种语义的记录:退出标记走的是大记录(sizeof(stracer_ev)),只是不填栈和路径。
完整事件(stracer_ev,大)用于需要现场快照的普通syscall,带寄存器、栈、路径。紧凑记录(stracer_x,小)用于openat的返回,只需要带回一个fd号。mmap返回记录(stracer_mmapret)用于带回mmap实际映射到的地址。
后两种为什么要小?因为 openat 调用极其频繁,如果每条都用大记录,环会被压垮。它们怎么区分?三种记录的前面一段字节布局故意做成一样,里面有个 sysnr 字段,用户态先读这个字段,再决定按哪种格式解析剩下的部分。
影子表是按照地址区间存的一串段,每段记录了起止地址、这段属于哪个文件(匿名段就没有文件名)、以及权限(可读可写可执行)。查一个地址属于哪个so,就是拿这个地址去这串区间里找,落在哪一段就属于哪一段,因为区间之间互不重叠、按段首排好了序,查起来很快。增量更新就是拿mmap/munmap/mprotect的参数去改这串区间,mmap是在某段地址上建一段新的,munmap是把某段删掉,mprotect是把某段的权限改掉。这三种操作都可能和已有的段部分重叠,所以更新时要把整段按新区间的边界切开,重叠的部分替换掉,两侧没碰到的部分保留下来。刷新则是直接拿读到的真maps整表重建。
要想要知道调用来自哪个so,分为两步。先要把寄存器里的当前地址拿去查影子表,落在哪个段就归属到哪个so,这一步进程死了也成立,因为查的是影子表不是活进程。接着就是展栈,也就是还原出完整的调用链,光知道当前这一层还不够,往往需要回推几层才能知道到底是哪个so发起的。展栈有三级回退,一级比一级糙。第一级是DWARF展栈,靠so文件里的调试信息精确回推,最准。如果说拿不到就退到第二级帧指针链,靠寄存器里约定好的帧指针一层层往回跳,再不行就第三级栈回扫,直接扫那段栈,把落在可执行段里的地址当作候选的返回地址,这一级是猜的、可能误报,所以默认只当参考、不拿他作判定。
支持的30个syscall
openat close read write pread64 pwrite64 faccessat readlinkat newfstatat fstat lseek getdents64 statx mmap munmap mprotect madvise mremap clone execve prctl futex ioctl fcntl socket connect sendto recvfrom getrandom rt_sigprocmask
这里拿两个企业加固做展示
一个就是我上篇帖的某bang企业版的so内存解密



在一个是某数字加固企业版的解密ELF头



效果还是蛮好的,可以快速知道当前app的某个so用了哪些系统调用
之前我是在aosp12上去对Fartext进行移植(这次移植挺轻松的 变化没很大),这次打算在LineageOS23上做移植,
本来打算自己啃的(啃过 坑真多),但是有大佬已经跑通了,那就照抄就行。下面是大佬r0dump的原文及github
原文:[原创] 使用 Kimi K3 进行脱壳工具迁移开发:R0DUMP —— 将 FART 迁移到 Android 16
github:7c2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1K9i4N6W2x3q4)9J5c8Y4t1H3k6s2g2E0M7l9`.`.
下载Android Studio for Platform
下载好了就解压一下
启动 ASfP
next

等他慢慢下

创建项目之前需要先知道lunch target是什么 去跑一下编译,等他跑完
编译完成后打印这两个
将打印出来的结果拼接一下得到
lineage_bluejay-userdebug
然后再拼接上BUILD_ID得到
lineage_bluejay-bp4a-userdebug
这个就是Lunch target 后面用的着

设置一下路径
