这篇文章上次修改于 539 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

这是去年尝试用ebpf实现jnitrace的记录,在此分享


前言

由于eBPF本身的限制,在命中hook点的时候是不能调用用户态空间的函数的

但是提供了读取和修改用户态数据的能力

思路

大部分jni函数到底层经过的地方只能获取到一些零碎的消息

比如可以拿到mid,但是不能直接获取到对应的名字

经过测试,比如CallObjectMethod和CallObjectMethodV必定会调用(大部分情况)

  • InvokeVirtualOrInterfaceWithVarArgs => soa obj mid args

    • 这里是jmethodID
  • InvokeVirtualOrInterfaceWithVarArgs => soa obj interface_method args

    • 这里是ArtMethod

参考ecapture的代码,可以通过读取结构体的方式获取到一些信息

比如读取ArtMethod可以获取到这些信息

GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;

而要获取到具体的方法名,类名等等,还需要调用更多的函数才行,因为函数名并没有直接在ArtMethod

比如以PrettyMethod为例,可以看到需要经过复杂的组合才能获取到完整的信息

std::string ArtMethod::PrettyMethod(bool with_signature) {
  if (UNLIKELY(IsRuntimeMethod())) {
    std::string result = GetDeclaringClassDescriptor();
    result += '.';
    result += GetName();
    // Do not add "<no signature>" even if `with_signature` is true.
    return result;
  }
  ArtMethod* m = GetInterfaceMethodIfProxy(Runtime::Current()->GetClassLinker()->GetImagePointerSize());
  std::string res(m->GetDexFile()->PrettyMethod(m->GetDexMethodIndex(), with_signature));
  if (with_signature && m->IsObsolete()) {
    return "<OBSOLETE> " + res;
  } else {
    return res;
  }
}

对于eBPF来说,虽然此路不通,但是我们可以只记录这些信息就够了,有了这些信息之后

可以手动解析dex去拿到具体的内容信息,FART中就是这样的思路,自己去解析dex;但是eBPF不具有主动执行代码的可能,所以这里只能放到bcc接收完数据之后

另外的方案是我们在APP启动阶段在相应的函数观测对应关系,这样就不用自己去解析了,但这样的弊端是需要记录大量的数据,并且APP必须经过启动阶段,不够灵活

在jni函数会经过的地方进行观测,收集足够的信息,再辅助其他方法,可以实现大部分信息的获取

对于调用的方法,简单参数,在上述基础的支持已经可以了

但是对于参数列表来说,如果是jobject,应该如何获取更详细的信息呢?

对于参数列表,可以在BuildArgArrayFromVarArgs进行观测,常规参数比较容易获取到信息

对于jobject可能得具体分析soa.Decode看看有没有什么可以拿到的信息

这个过程实在是太麻烦了

关键函数FindVirtualMethod

ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj);
ArtMethod* method = FindVirtualMethod(receiver, interface_method);

参考jnitrace只记录经过了调用的那些信息,这样倒是可以平衡(不过这种情况也是需要在一开始就追踪,这样才尽可能完整

总结

简单来说就是基于uprobe断点,在断点处通过不断【读取结构体】获取到想要的信息,采集信息,最终组合输出

下面是基于eBPF进行的jnitrace实验性测试,不提供任何保证,仅供参考:

注意:脚本中的偏移不一定通用

效果:

2023-06-21T02:33:56.png