背景
为了研究三方应用一些安全机制,进行逆向分析,比如三方应用调用的敏感函数,获取的系统属性、注册的信号等,尝试通过自定义Rom修改系统代码来实现,之所以不使用类似依赖于Magisk Xposed之类的方案 是因为部分APP会对这类技术进行识别,可能会影响真实的程序运行逻辑。
目前该Rom已实现以下能力:
- 监控目标函数的调用
- 监控目标Java字段被访问
- 监控获取系统属性
- 监控执行命令程序
- 监控通过libc进行文件相关操作,比如open access
- 监控直接通过系统调用方式进行的文件操作 (有些三方App可能不通过libc API,而是直接通过系统调用)
接下来本文主要介绍开发过程中的一些核心实现细节,以及遇到的一些问题。
开发环境准备
网上的文章比较多,不详细介绍,建议最好是 ubuntu 22版本,在ubuntu 24上实测还有一些环境问题,比如 lunch menu 不展示编译选项、sandbox 由于ubuntu 24 默认开启kernel.apparmor_restrict_unprivileged_userns 配置导致无法展示 等问题,我使用ubuntu 24 主要是由于笔记本按照ubuntu22 缺少部分驱动,导致笔记本键盘失灵。
个人完整的开发及软硬件环境如下
- 系统: ubuntu 24
- 手机: pxiel 7 pro
- aosp 分支: android-14.0.0_r67
- GKI 内核分支:android13-5.10
这里为了保证构建稳定性,尽量使用官方piexle 已验证过的构建的分支,piexel设备 aosp 最新的构建分支可以通过
source.android.com/docs/setup/… 查看。
framework 层相关监控
代码分析
在之前的jvmti 文章 《基于jvmti实现性能监控》中提到,通过jvmti可以实现一些程序监控功能,其中就包括 函数调用监控和字段访问监控。这里,我们再简单分析jvmti模块是如何实现这些功能的。
android中 JVMTI中的程序执行相关的监控实际是通过art中的 instrumentation模块实现的 ,jvmti的实现是通过注册在 instrumentation中注册了InstrumentationListener接口。
/aosp/art/openjdkjvmti/events.cc
instrumentation.h 头文件提供了MethodEnterEvent、MethodExitEvent、FieldReadEvent等事件函数,事件函数内部便利回调了注册的 InstrumentationListener。
instrumentataion.h MethodEnterEvent函数 -> instrumentation.cc MethodEnterEventImpl函数
继续向上分析MethodEnterEvent的调用点。
调用点主要存在2个文件中,分别是 quick_trampoline_entrypoint 及 interpreter.cc, 先分析 interpreter.cc中的调用点,调用点在 Execute(..)函数中。
Execute函数是switch解释执行模式执行函数的调用点,jit->MethodEntered 内部会判断函数该函数是否可以被JIT编译,如果达到热点函数阀值会触发JIT编译,如果编译成功则会跳转到CompiledCodeBridge。执行编译后的函数。否则继续向下通过调用ExecuteSwitch进入函数的Switch执行模式执行函数代码, 而在ExecuteSwitch执行前,判断了是否存在 Instrumentation Listenere,触发相应的函数回调。
再看quick_trampoline_entrypoints.cc中的调用点。
artQuickProxyInvokeHandler 和 artQuickGenericJniTrampoline是 Java 动态代理函数、JNI函数执行的跳板函数, artJniMethodEntryHook、artMethodEntryHook 是暴露给 quick编译器的Hook函数,这2个函数的地址会被注册到 quick_entry_points中, 但在实际验证中,在这2个函数内部添加了日志发现,这2个函数并没有被调用,待后续研究。
以上是instrumentation函数调用监控的实现的大致代码,主要的实现方式还是在各个调用入口、跳板函数上回调执行相应的Listenerer。
访问成员属性的实现方式同上,也是通过Instrumentation来实现的。
需要注意的是 Instrumentation的执行模式中 ,有三种级别 kInstrumentNothing、kInstrumenWithEntryExitHooks、kInstrumenWithInterpreter,如果只需要监控函数调用则 kInstrumenWithEntryExitHooks即可,如果需要函数字段读写等其他监控,需要使用 kInstrumenWithInterpreter级别。这是因为 art中除了switch解释执行模式,还存在nterp解释执行模式,而nterp译码和翻译执行全程都由汇编代码实现 不支持指令级别的监控,因此对函数体内部指令的执行监控必须将代码执行模式全部回退到switch解释执行,nterp暂 不支持。
代码实现
分别实现2个函数,用于注册或取消Instrumentation,在EnableMethodTracing中 内部会通过ClassVisitor遍历所有ArtMethod函数,并更新对应entrypoint,因此操作前需要通过 ScopedSuspendAll 暂停虚拟机线程执行。
这里为了更好的性能,如果判断不开启FieldRead功能时,调用EnableMethodTracing时,第三个参数传如false,避免使用swtich解释执行模式。
void MoonTracer::enableInstrumentation() {
{
uint32_t events = 0;
if (traceGetField){
events = events | instrumentation::Instrumentation::kFieldRead;
}
if (traceInvokeMethod){
events = events | instrumentation::Instrumentation::kMethodEntered;
}
if (instrumentation_enabled){
//已经生效过,需要进行修改,先关闭再重新开启
//判断当前模式是否相等
if (events == applied_instrumentation_events){
//模式未发生变化,直接返回
return;
}
//模式发生了变化,先还原所有函数
disableInstrumentation();
}
// suspend all
Thread *self = Thread::Current();
jit::ScopedJitSuspend suspend_jit;
gc::ScopedGCCriticalSection gcs(self,
gc::kGcCauseInstrumentation,
gc::kCollectorTypeInstrumentation);
ScopedSuspendAll ssa(__FUNCTION__);
instrumentation::Instrumentation *instrumentation = Runtime::Current()->GetInstrumentation();
instrumentation->AddListener(this,
events,
true);
applied_instrumentation_events = events;
//如果需要监控字段访问
bool needsInterpreter = traceGetField;
Runtime::Current()->GetInstrumentation()->EnableMethodTracing(kTracerInstrumentationKey,
this,
/*needs_interpreter=*/needsInterpreter);
instrumentation_enabled = true;
}
}
void MoonTracer::disableInstrumentation() {
{
Thread *self = Thread::Current();
gc::ScopedGCCriticalSection gcs(
self, gc::kGcCauseInstrumentation, gc::kCollectorTypeInstrumentation);
jit::ScopedJitSuspend suspend_jit;
ScopedSuspendAll ssa(__FUNCTION__);
Runtime *runtime = Runtime::Current();
runtime->GetInstrumentation()->RemoveListener(
this,
applied_instrumentation_events,
true);
runtime->GetInstrumentation()->DisableMethodTracing(kTracerInstrumentationKey);
applied_instrumentation_events = 0;
instrumentation_enabled = true;
}
}
之后在相应的回调中,通过PrettyMethod 获取具体的函数名,并判断是否记录到跟踪日志中
性能优化
上述监控功能实现后,在目标App实际执行时,发现程序异常卡顿,进程启动时间甚至需要20S左右,并且App几乎不可操作,一滑动页面就几乎卡死。
最终通过二分法,逐步注释代码,确认卡顿原因为 在MethodEnter中 调用了PrettyMethod函数, 程序中通过PrettyMethod获取ArtMethod的函数签名字符串表示,并和注册的函数名单判断该函数是否为目标监控函数,由于每次函数执行都需要调用该PrettyMethod函数,而该函数的性能似乎并不高,但每次函数调用都会触发PrettyMethod,函数调用在App运行过程中是非常频繁的。(一些基础函数的会频繁触发,比如基础数字类型int long的装箱拆箱、Object.toString()、hashCode()、List.size() 、等)。
因此将程序优化为在开启监控前,通过ClassVisitor便利所有的函数及类字段,判断是否为目标属性,如果是则将ArtMethod 或 ArtField 指针加入到一个Set类型的集合变量中,最后在 相应的函数回调中判断是否为set中的成员即可。
优化后,进程启动时间缩短为4S左右,App能够正常操作。
class CollectTraceClassVisitor : public ClassVisitor {
public:
explicit CollectTraceClassVisitor(MoonTracer* moonTracer)
: moonTracer_(moonTracer) {}
bool operator()(ObjPtr<mirror::Class> klass) override REQUIRES(Locks::mutator_lock_) {
for (ArtMethod& method : klass->GetMethods(kRuntimePointerSize)) {
//判断是否在目标集合中
if (moonTracer_->isTargetArtMethod(&method)){
moonTracer_->targetArtMethods.insert(&method);
if (moonTracer_->IsDevDebug()){
LOG(ERROR) << "添加目标ArtMethod: " << method.PrettyMethod(false);
}
}
}
LengthPrefixedArray<ArtField>* const sFields = klass->GetSFieldsPtr();
LengthPrefixedArray<ArtField>* const iFields = klass->GetIFieldsPtr();
uint32_t sFieldsSize = klass->NumStaticFields();
uint32_t iFieldsSize = klass->NumInstanceFields();
for (size_t i = 0; i != iFieldsSize; ++i) {
ArtField* field = &iFields->At(i);
if (moonTracer_->isTargetArtField(field)){
moonTracer_->targetArtFields.insert(field);
if (moonTracer_->IsDevDebug()){
LOG(ERROR) << "添加目标ArtField: " << field->PrettyField(false);
}
}
}
for (size_t i = 0; i != sFieldsSize; ++i) {
ArtField* field = &sFields->At(i);
if (moonTracer_->isTargetArtField(field)){
moonTracer_->targetArtFields.insert(field);
if (moonTracer_->IsDevDebug()){
LOG(ERROR) << "添加目标ArtField: " << field->PrettyField(false);
}
}
}
return true; // we visit all classes.
}
private:
MoonTracer* const moonTracer_;
};
栈回溯调用
除了监控到目标函数或字段的获取,我们还需要知道具体的调用路径,因此需要实现栈回溯能力,由于我们是在aosp源码中直接开发的,因此可以直接复用源码成现有的函数。
对于Java线程 通过StackVisitor回溯调用栈, 回溯之前需要确保对 mutator_lock_加锁。
std::string JavaBackTrace() NO_THREAD_SAFETY_ANALYSIS {
std::string r;
Thread *artThread = Thread::Current();
bool lock = false;
if (!Locks::mutator_lock_->IsSharedHeld(artThread)){
Locks::mutator_lock_->SharedLock(artThread);
lock = true;
}
StackVisitor::WalkStack(
[&](const StackVisitor *stack_visitor) NO_THREAD_SAFETY_ANALYSIS {
ArtMethod *m = stack_visitor->GetMethod();
// Ignore runtime frames (in particular callee save).
if (!m->IsRuntimeMethod()) {
r = r.append(m->PrettyMethod(false)).append("\n");
}
return true;
},
artThread,
/* context= */ nullptr,
art::StackVisitor::StackWalkKind::kIncludeInlinedFrames);
if (lock){
Locks::mutator_lock_->SharedUnlock(artThread);
}
}
对于Native栈的获取, 调用 unwindstack::AndoridLocalUnwinder 实现,并且过滤掉解释器执行的相关栈帧,。
std::string NativeBackTrace() {
std::string r;
unwindstack::AndroidLocalUnwinder unwinder;
uint64_t thread_id = android::base::GetThreadId();
unwindstack::AndroidUnwinderData data;
unwinder.Unwind(thread_id, data);
uint32_t ignoreDepth = 0;
for (size_t i = ignoreDepth; i < data.frames.size(); i++) {
auto &frame = data.frames[i];
frame.num -= ignoreDepth;
const std::string &fullName = frame.map_info->GetFullName();
if (frame.map_info != nullptr && isWhiteSo(fullName)) {
continue;
}
if (frame.function_name != "") {
const char *func_name_str = frame.function_name.c_str();
//过滤 interpreter相关的栈帧
// #21 pc 0000000000418134 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.__uniq.112435418011751916792819755956732575238.llvm.233200218098832039)+244) (BuildId: a16352327f304fdd757c84017a60ed48)
// #22 pc 00000000005336e8 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, bool, art::JValue*)+3992) (BuildId: a16352327f304fdd757c84017a60ed48)
// #23 pc 00000000006024d0 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp<false>(art::interpreter::SwitchImplContext*)+14848) (BuildId: a16352327f304fdd757c84017a60ed48)
// #24 pc 00000000003ccfd8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: a16352327f304fdd757c84017a60ed48)
if (ignoreNativeFunc(func_name_str)
) {
continue;
}
r = r.append(unwinder.FormatFrame2(frame)).append("\n");
} else {
}
}
return r;
}
libc 库监控
针对libc函数调用的监控,这里添加了 my_libc_trace.h 及 my_libc_trace.cc 文件,头文件用于直接提供给art模块进行全局指针访问、替换。
//
// Created by nimdanoob on 2024/9/7.
//
#pragma once
#include <sys/cdefs.h>
__BEGIN_DECLS
typedef void (*OnOpenPath)(const char * _Nonnull pathname);
typedef void (*OnStatPath)(const char * _Nonnull pathname);
typedef void (*OnAccessPath)(const char * _Nonnull pathname);
typedef void (*OnGetProperties)(const char * _Nullable key);
typedef void (*OnFork)();
typedef void (*OnExecve)( const char* _Nullable name, char* _Nullable const* _Nullable argv);
__attribute__((visibility("default"))) extern _Nullable OnGetProperties gOnGetProperties;
__attribute__((visibility("default"))) extern _Nullable OnOpenPath gOnOpenPath;
__attribute__((visibility("default"))) extern _Nullable OnAccessPath gOnAccessPath;
__attribute__((visibility("default"))) extern _Nullable OnStatPath gOnStatPath;
__attribute__((visibility("default"))) extern _Nullable OnExecve gOnExecve;
__attribute__((visibility("default"))) extern _Nullable OnFork gOnFork;
__END_DECLS
//
// Created by nimdanoob on 2024/9/7.
//
#pragma once
#include <sys/cdefs.h>
__BEGIN_DECLS
typedef void (*OnOpenPath)(const char * _Nonnull pathname);
typedef void (*OnStatPath)(const char * _Nonnull pathname);
typedef void (*OnAccessPath)(const char * _Nonnull pathname);
typedef void (*OnGetProperties)(const char * _Nullable key);
typedef void (*OnFork)();
typedef void (*OnExecve)( const char* _Nullable name, char* _Nullable const* _Nullable argv);
__attribute__((visibility("default"))) extern _Nullable OnGetProperties gOnGetProperties;
__attribute__((visibility("default"))) extern _Nullable OnOpenPath gOnOpenPath;
__attribute__((visibility("default"))) extern _Nullable OnAccessPath gOnAccessPath;
__attribute__((visibility("default"))) extern _Nullable OnStatPath gOnStatPath;
__attribute__((visibility("default"))) extern _Nullable OnExecve gOnExecve;
__attribute__((visibility("default"))) extern _Nullable OnFork gOnFork;
__END_DECLS
之后 相应的函数中 回调相应的函数指针:
最后在 bionic/libc/Android.bp中,添加 my_libc_trace.cpp文件编译。由于libc 库是通过 libc.map.txt控制对外导出的函数符号,因此还需要添加这些函数符号到文件中。
在art模块中,当判断当前进程需要监控这些函数调用时,设置相应的函数回调指针。然而这里在实际编译时 还是会编译失败,虽然上面已经通过libc.txt 导出了函数符号,art 模块中使用时,也能正常引入 my_libc_trace.h头文件,但在编译阶段还是会出现 相应符号找不到的问题。 最后通过不断尝试,暂时通过判断是否存在 __BIONIC__宏再访问相应指针变量 解决了该问题。
内核修改
有些APP的文件操作可能不直接通过libc来实现,而是直接通过系统调用实现,因此还需要再内核层直接实现文件操作监控。
确定内核版本
内核源码和 aosp源码是独立的repo仓库,默认的aosp 构建是使用的仓库内预编译好的kerne镜像,以Pixel 7 Pro为例,内核镜像 boot.img 位于 aosp/device/google/pantah-kernel 中。
设备内核版本的确认及对应的分支下载参考:KernelSU: Android 内核编译方法和开发环境搭建
修改内核
驱动程序实现
内核层的监控和libc库监控类似,主要还是在对应的系统调用实现函数中添加桩点,调用函数。
| 系统调用 | 对应实现文件 |
|---|---|
| __NR_openat | fs/open.c do_sys_openat2 |
| __NR_faccessat | fs/open.c do_faccessat |
以__NR_openat 为例,在相应的函数实现do_sys_openat2 中,修改代码
内核的监控还需要提供一些功能控制逻辑,比如设置监控的目标程序uid、具体监控的系统调用等,从而过滤器调非目标App或函数的调用,避免日志太多。具体实现时,为了不对内核层代码做过多修改,这里采用新增内核驱动的方式来拓展功能。
在 kernel/common/drivers 目录下添加相应的驱动实现代码。在内核模块初始化时,设置预埋的函数指针为驱动内部的函数。
在内核模块中,创建了一个proc虚拟文件 挂载在 /proc/moontrace路径,在创建该proc文件时,配置相应的文件读写实现, 从而实现通过该proc文件与用户空间的交互。
用户空间可以通过写该proc文件控制监控行为,写 proc文件相应的回调中,解析用户写入的内容。
用户空间可以通过读取该proc文件获取监控记录的内容,在内核空间中创建了一个环形缓冲区,用于缓存最近记录的N条日志信息,当用户读取该文件时,将环形缓冲区的日志 通过 seq_printf 按序输出。
读取 /proc/moontrace文件的示例输出, 第一行默认输出了目前的功能配置。
添加系统调用
添加系统调用的实现逻辑可以参考 github.com/itewqq/andr… ,不过按照文章中添加系统调用后,实际上在user 构建中 由于selinux限制 运行时调用新增的系统调用会被selinux机制拦截。
最后通过分析其他系统调用的配置代码,经过测试验证发现,还需要在 SYSYCALL.TXT 添加系统调用信息(该文件内容 实际上是通过 python脚本自动从linux/common源码中生成的,这里我是手动添加),并在 bioniuc/libc/ 下的 SECCOMP_ 的相应文件中配置允许调用该系统调用。如果希望一个系统调用可以被调用,则添加相应的配置到 SECCOMP_ALLOWLIST_COMMON.TXT 中。
功能管理程序
为了更方便的设置相应的监控配置,可以创建一个系统App用于配置具体的功能,将功能配置存储在系统App路径内。以下是开发的MoonTrace 系统App用于管理监控配置。
- 参考资料