Matrix源码分析————IO Canary

概述

年前,微信开源了Matrix项目,提供了Android、ios的APM实现方案。对于Android端实现,主要包括APK CheckerResource CanaryTrace CanarySQLite LintIO 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方法会走到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。

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总结

技术特点:

  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的实现原理

转载于:https://juejin.im/post/5cc07174f265da0382611b66

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要进行正则匹配注入,你可以按照以下步骤来使用HttpCanary: 1. 首先,在Android设备上安装HttpCanary。它是一款用于抓包的手机端软件,可以方便地监控网络请求和响应。 2. 打开HttpCanary应用,并确保你的设备已经root或者安装了平行空间。 3. 在HttpCanary的界面上,你可以看到所有的网络请求和响应。你可以通过筛选器来过滤你感兴趣的请求。 4. 然后,点击你感兴趣的请求,查看请求和响应的详细信息。 5. 在请求或响应的详细信息中,你可以找到相关的数据,并进行注入操作。 6. 对于正则匹配注入,你可以使用HttpCanary提供的正则表达式来匹配你想要注入的内容。 7. 通过使用HttpCanary的正则匹配注入功能,你可以对请求或响应中的特定数据进行替换或修改。 请注意,正则匹配注入需要一定的技术知识和经验。在注入之前,请确保你已经了解了正则表达式的基本语法和HttpCanary的使用方法。同时,也要注意在使用HttpCanary进行注入时,遵守相关法律法规,并且只使用于合法和授权的目的。 希望这些信息对你有帮助!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [JS正则匹配URL网址的方法(可匹配www,http开头的一切网址)](https://download.csdn.net/download/weixin_38614417/13199581)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [HttpCanary 在 Android 11 上的使用](https://blog.csdn.net/weixin_39925813/article/details/117580459)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值