Matrix源码分析————IO Canary

栏目: IOS · Android · 发布时间: 7年前

内容简介:年前,微信开源了IOCanary大体上从Java Hook、Native Hook两个角度来检测应用的IO行为;并根据不同的策略细化了IO Issue的种类。Java Hook的hook点是系统类

年前,微信开源了 Matrix 项目,提供了Android、ios的APM实现方案。对于Android端实现,主要包括 APK CheckerResource CanaryTrace CanarySQLite LintIO Canary 五部分。本文主要介绍 IO Canary 的源码实现,其他部分的源码分析将在后续推出。

代码框架分析

IOCanary大体上从Java Hook、Native Hook两个角度来检测应用的IO行为;并根据不同的策略细化了IO Issue的种类。

Matrix源码分析————IO Canary

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 方法会走到 CloseGuardwarnIfOpen 方法,从而检测到这次资源未正常关闭的行为。

当然应用也有一些自定义的资源类,对于这种情况Matrix建议使用 MatrixCloseGuard 这个类模拟系统埋点的方式,达到资源监控的目的。

Native Hook

Native Hook是采用PLT(GOT) Hook的方式hook了系统so中的IO相关的 openreadwriteclose 方法。在代理了这些系统方法后,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。

Matrix源码分析————IO Canary

plt Section说明:

Matrix源码分析————IO Canary

got Section说明:

Matrix源码分析————IO Canary

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代码实现

Matrix源码分析————IO Canary

解析需要hook的so文件,封装一个loaded_soinfo对象。

Matrix源码分析————IO Canary

查找GOT表中是否有对应的方法声明。

Matrix源码分析————IO Canary

locate_symbol 内部调用 locate_symbol_hash

Matrix源码分析————IO Canary

备选方案, locate_symbol_hash 失败后会走到这个方法。

Matrix源码分析————IO Canary

实际替换对应的函数地址。

Android系统加载so的过程

源码地址位于 android / platform / bionic / froyo / . / linker / linker.c

Matrix源码分析————IO Canary

链接so文件(elf文件格式)

Matrix源码分析————IO Canary
Matrix源码分析————IO Canary

so文件(ELF文件)中的Section包括三种状态:

  • Alloc:Section在进程执行过程中占用内存;
  • Write:Section包含进程执行过程中可写的数据;
  • Execute:Section包含可执行的机器指令;
Matrix源码分析————IO Canary

Android加载so的过程,暂时未完全弄懂,待后续完善~~

PLT(GOT) Hook总结

技术特点:

  1. 由于修改的是GOT表中的数据,因此修改后,所有对该函数进行调用的地方就都会被Hook到。这个效果的影响范围是该PLT和GOT所处的整个so库。因此,当目标so库中多行被执行代码都调用了该PLT项所对应的函数,那它们都会去执行Hook功能。
  2. PLT与GOT表中仅仅包含本ELF需要调用的共享库函数项目,因此不在PLT表中的函数无法Hook到。

应用场景:

  1. 可以大量Hook那些系统API,但是难以精准Hook住某次函数调用。这比较适用于开发者对于自家APP性能监控的需求。比如Hook住malloc使其输出参数,这样就能大量统计评估该APP对于内存的需求。但是对于一些对Hook对象有一定精准度要求的需求来说很不利,比如说是安全测试或者逆向分析的工作需求,这些工作中往往需要对于目标so中的某些关键点有准确的观察。
  2. 对于一些so内部自定义的函数无法Hook到。因为这些函数不在PLT表和GOT表里。这个缺点对于不少软件分析者来说可能是无法忍受的。因为许多关键或核心的代码逻辑往往都是自定义的。例如NDK中实现的一些加密工作,即使使用了共享库中的加密函数,但秘钥的保存管理等依然需要进一步分析,而这些工作对于自定义函数甚至是某行汇编代码的监控能力要求是远远超出PLT Hook所能提供的范围。

总结

Matrix IO检测的代码逻辑相对简单。难点在于so(elf文件)文件格式的理解,以及PLT(GOT) Hook的实现原理


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mission Python

Mission Python

Sean McManus / No Starch Press / 2018-9-18 / GBP 24.99

Launch into coding with Mission Python, a space-themed guide to building a complete computer game in Python. You'll learn programming fundamentals like loops, strings, and lists as you build Escape!, ......一起来看看 《Mission Python》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具