内容简介:年前,微信开源了IOCanary大体上从Java Hook、Native Hook两个角度来检测应用的IO行为;并根据不同的策略细化了IO Issue的种类。Java Hook的hook点是系统类
年前,微信开源了 Matrix 项目,提供了Android、ios的APM实现方案。对于Android端实现,主要包括 APK Checker
、 Resource Canary
、 Trace Canary
、 SQLite Lint
、 IO Canary
五部分。本文主要介绍 IO Canary
的源码实现,其他部分的源码分析将在后续推出。
代码框架分析
IOCanary大体上从Java Hook、Native Hook两个角度来检测应用的IO行为;并根据不同的策略细化了IO Issue的种类。
Java Hook
Java Hook的hook点是系统类 CloseGuard
,hook的方式是使用动态代理。
private boolean tryHook() { try { Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard"); Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter"); Field fieldREPORTER = closeGuardCls.getDeclaredField("REPORTER"); Field fieldENABLED = closeGuardCls.getDeclaredField("ENABLED"); fieldREPORTER.setAccessible(true); fieldENABLED.setAccessible(true); sOriginalReporter = fieldREPORTER.get(null); fieldENABLED.set(null, true); // open matrix close guard also MatrixCloseGuard.setEnabled(true); ClassLoader classLoader = closeGuardReporterCls.getClassLoader(); if (classLoader == null) { return false; } fieldREPORTER.set(null, Proxy.newProxyInstance(classLoader, new Class<?>[]{closeGuardReporterCls}, new IOCloseLeakDetector(issueListener, sOriginalReporter))); fieldREPORTER.setAccessible(false); return true; } catch (Throwable e) { MatrixLog.e(TAG, "tryHook exp=%s", e); } return false; } 复制代码
系统 CloseGuard
的实现原理是在一些资源类中预埋一些代码,从而使 CloseGuard
感知到资源是否被正常关闭。例如系统类 FileOutputStream
中有如下代码:
private final CloseGuard guard = CloseGuard.get(); ... public FileOutputStream(File file, boolean append) throws FileNotFoundException { ... guard.open("close"); } ... public void close() throws IOException { ... guard.close(); ... } ... protected void finalize() throws IOException { // Android-added: CloseGuard support. if (guard != null) { guard.warnIfOpen(); } if (fd != null) { if (fd == FileDescriptor.out || fd == FileDescriptor.err) { flush(); } else { // Android-removed: Obsoleted comment about shared FileDescriptor handling. close(); } } } 复制代码
可以看到在调用 finalize
之前未调用 close
方法会走到 CloseGuard
的 warnIfOpen
方法,从而检测到这次资源未正常关闭的行为。
当然应用也有一些自定义的资源类,对于这种情况Matrix建议使用 MatrixCloseGuard
这个类模拟系统埋点的方式,达到资源监控的目的。
Native Hook
Native Hook是采用PLT(GOT) Hook的方式hook了系统so中的IO相关的 open
、 read
、 write
、 close
方法。在代理了这些系统方法后,Matrix做了一些逻辑上的细分,从而检测出不同的IO Issue。
JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) { __android_log_print(ANDROID_LOG_INFO, kTag, "doHook"); for (int i = 0; i < TARGET_MODULE_COUNT; ++i) { const char* so_name = TARGET_MODULES[i]; __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name); loaded_soinfo* soinfo = elfhook_open(so_name); if (!soinfo) { __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name); continue; } elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open); elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64); bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr); if (is_libjavacore) { if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk"); if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk"); return false; } } if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk"); if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk"); return false; } } } elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close); elfhook_close(soinfo); } return true; } 复制代码
hook住系统调用之后,接下来再看看代理方法的实现:
int ProxyOpen64(const char *pathname, int flags, mode_t mode) { if(!IsMainThread()) { return original_open64(pathname, flags, mode); } int ret = original_open64(pathname, flags, mode); if (ret != -1) { DoProxyOpenLogic(pathname, flags, mode, ret); } return ret; } /** * Proxy for read: callback to the java layer */ ssize_t ProxyRead(int fd, void *buf, size_t size) { if(!IsMainThread()) { return original_read(fd, buf, size); } int64_t start = GetTickCountMicros(); size_t ret = original_read(fd, buf, size); long read_cost_μs = GetTickCountMicros() - start; //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyRead fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, read_cost_μs); iocanary::IOCanary::Get().OnRead(fd, buf, size, ret, read_cost_μs); return ret; } /** * Proxy for write: callback to the java layer */ ssize_t ProxyWrite(int fd, const void *buf, size_t size) { if(!IsMainThread()) { return original_write(fd, buf, size); } int64_t start = GetTickCountMicros(); size_t ret = original_write(fd, buf, size); long write_cost_μs = GetTickCountMicros() - start; //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyWrite fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, write_cost_μs); iocanary::IOCanary::Get().OnWrite(fd, buf, size, ret, write_cost_μs); return ret; } /** * Proxy for close: callback to the java layer */ int ProxyClose(int fd) { if(!IsMainThread()) { return original_close(fd); } int ret = original_close(fd); //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyClose fd:%d ret:%d", fd, ret); iocanary::IOCanary::Get().OnClose(fd, ret); return ret; } 复制代码
仔细阅读代理方法的代码,发现所有的代理方法在非主线程都是直接执行原方法(没有添加IO检测的相关逻辑)。这部分Matrix官方承认由于多线程并发的问题暂时支持单线程模型。由于限制了只对主线程进行检测,整体IO检测方案的实际应用场景变得很受限,希望Matrix后续可以优化。
IO检测策略
- 主线程IO
void FileIOMainThreadDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info, std::vector<Issue>& issues) { if (GetMainThreadId() == file_io_info.java_context_.thread_id_) { int type = 0; //可能引起卡顿的主线程IO,默认值13ms if (file_io_info.max_continual_rw_cost_time_μs_ > IOCanaryEnv::kPossibleNegativeThreshold) { type = 1; } //引起主线程严重性能问题的IO,默认500ms if(file_io_info.max_continual_rw_cost_time_μs_ > env.GetMainThreadThreshold()) { type |= 2; } if (type != 0) { Issue issue(kType, file_io_info); issue.repeat_read_cnt_ = type; //use repeat to record type PublishIssue(issue, issues); } } } 复制代码
- Small Buffer IO
void FileIOSmallBufferDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info, std::vector<Issue>& issues) { //单次操作的字节数小于阈值 if (file_io_info.op_cnt_ > env.kSmallBufferOpTimesThreshold && (file_io_info.op_size_ / file_io_info.op_cnt_) < env.GetSmallBufferThreshold() && file_io_info.max_continual_rw_cost_time_μs_ >= env.kPossibleNegativeThreshold) { PublishIssue(Issue(kType, file_io_info), issues); } } 复制代码
- Repeat Read IO
void FileIORepeatReadDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info, std::vector<Issue>& issues) { const std::string& path = file_io_info.path_; if (observing_map_.find(path) == observing_map_.end()) { if (file_io_info.max_continual_rw_cost_time_μs_ < env.kPossibleNegativeThreshold) { return; } observing_map_.insert(std::make_pair(path, std::vector<RepeatReadInfo>())); } std::vector<RepeatReadInfo>& repeat_infos = observing_map_[path]; //有write行为,清空repeat_info if (file_io_info.op_type_ == FileOpType::kWrite) { repeat_infos.clear(); return; } RepeatReadInfo repeat_read_info(file_io_info.path_, file_io_info.java_context_.stack_, file_io_info.java_context_.thread_id_, file_io_info.op_size_, file_io_info.file_size_); if (repeat_infos.size() == 0) { repeat_infos.push_back(repeat_read_info); return; } //read操作间隔17ms,清空repeat_info if((GetTickCount() - repeat_infos[repeat_infos.size() - 1].op_timems) > 17) { //17ms todo astrozhou add to params repeat_infos.clear(); } bool found = false; int repeatCnt; for (auto& info : repeat_infos) { if (info == repeat_read_info) { found = true; info.IncRepeatReadCount(); repeatCnt = info.GetRepeatReadCount(); break; } } if (!found) { repeat_infos.push_back(repeat_read_info); return; } //重复read次数达到阈值,上报IO Issue if (repeatCnt >= env.GetRepeatReadThreshold()) { Issue issue(kType, file_io_info); issue.repeat_read_cnt_ = repeatCnt; issue.stack = repeat_read_info.GetStack(); PublishIssue(issue, issues); } } 复制代码
PLT(GOT)Hook介绍
Native Hook大体上可以分为PLT(GOT) Hook、ART Hook(基于ART虚拟机)、Dalvik Hook(基于Dalvik虚拟机)、inline Hook这几类Hook手段。相关文章可以详见 Android Native Hook技术路线概述 。PLT(GOT) Hook是基于so(实际是一个elf格式的文件)的GOT跳转表实现的。ELF文件格式的详细说明可以参见文章。对于PLT(GOT) HOOK,需要关注的是ELF文件链接视图下名为.plt和.got的Section。
plt Section说明:
got Section说明:
PLT(GOT) HOOK的原理从 Android Native Hook技术路线概述 摘录如下:
先来介绍一下Android PLT Hook的基本原理。Linux在执行动态链接的ELF的时候,为了优化性能使用了一个叫延时绑定的策略。相关资料有很多,这边简述一下:这个策略是为了解决原本静态编译时要把各种系统API的具体实现代码都编译进当前ELF文件里导致文件巨大臃肿的问题。所以当在动态链接的ELF程序里调用共享库的函数时,第一次调用时先去查找PLT表中相应的项目,而PLT表中再跳跃到GOT表中希望得到该函数的实际地址,但这时GOT表中指向的是PLT中那条跳跃指令下面的代码,最终会执行 _dl_runtime_resolve()
并执行目标函数。第二次调用时也是PLT跳转到GOT表,但是GOT中对应项目已经在第一次 _dl_runtime_resolve()
中被修改为函数实际地址,因此第二次及以后的调用直接就去执行目标函数,不用再去执行 _dl_runtime_resolve()
了。因此,PLT Hook通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。
PLT(GOT) Hook代码实现
解析需要hook的so文件,封装一个loaded_soinfo对象。
查找GOT表中是否有对应的方法声明。
locate_symbol
内部调用 locate_symbol_hash
。
备选方案, locate_symbol_hash
失败后会走到这个方法。
实际替换对应的函数地址。
Android系统加载so的过程
源码地址位于 android / platform / bionic / froyo / . / linker / linker.c
链接so文件(elf文件格式)
so文件(ELF文件)中的Section包括三种状态:
- Alloc:Section在进程执行过程中占用内存;
- Write:Section包含进程执行过程中可写的数据;
- Execute:Section包含可执行的机器指令;
Android加载so的过程,暂时未完全弄懂,待后续完善~~
PLT(GOT) Hook总结
技术特点:
- 由于修改的是GOT表中的数据,因此修改后,所有对该函数进行调用的地方就都会被Hook到。这个效果的影响范围是该PLT和GOT所处的整个so库。因此,当目标so库中多行被执行代码都调用了该PLT项所对应的函数,那它们都会去执行Hook功能。
- PLT与GOT表中仅仅包含本ELF需要调用的共享库函数项目,因此不在PLT表中的函数无法Hook到。
应用场景:
- 可以大量Hook那些系统API,但是难以精准Hook住某次函数调用。这比较适用于开发者对于自家APP性能监控的需求。比如Hook住malloc使其输出参数,这样就能大量统计评估该APP对于内存的需求。但是对于一些对Hook对象有一定精准度要求的需求来说很不利,比如说是安全测试或者逆向分析的工作需求,这些工作中往往需要对于目标so中的某些关键点有准确的观察。
- 对于一些so内部自定义的函数无法Hook到。因为这些函数不在PLT表和GOT表里。这个缺点对于不少软件分析者来说可能是无法忍受的。因为许多关键或核心的代码逻辑往往都是自定义的。例如NDK中实现的一些加密工作,即使使用了共享库中的加密函数,但秘钥的保存管理等依然需要进一步分析,而这些工作对于自定义函数甚至是某行汇编代码的监控能力要求是远远超出PLT Hook所能提供的范围。
总结
Matrix IO检测的代码逻辑相对简单。难点在于so(elf文件)文件格式的理解,以及PLT(GOT) Hook的实现原理
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Perl语言入门 第六版(中文版)
Randal L.Schwartz、brian d foy、Tom Phoenix / 盛春 / 东南大学出版社 / 2012-3 / 62.00元
《Perl语言入门(第6版)(中文版)》根据作者施瓦茨、福瓦、菲尼克斯从1991年开始的教学经验积累汇聚而成,多年来十分畅销。此次第六版涵盖了最新的Perl5.14版本的变化。《Perl语言入门(第6版)(中文版)》每章都包含若干习题,帮助你巩固消化刚学到的知识。也许其他书籍只是想着灌输Perl编程的条条框框,但《Perl语言入门(第6版)(中文版)》不同,我们希望把你培养成一名真正的Perl程序......一起来看看 《Perl语言入门 第六版(中文版)》 这本书的介绍吧!