抛砖引玉。

前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

工具

  1. Visual Studio Code
  2. Frida

简要介绍

Frida

官方解释

It’s Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX. Frida also provides you with some simple tools built on top of the Frida API. These can be used as-is, tweaked to your needs, or serve as examples of how to use the API.

简单来说最主要的特色就是一个跨平台的可以使用JavaScript代码片段来进行程序安全测试的工具集,并提供较多的易用API与各种强大的特性。

SSLPinning

我们都知道通过安装伪造的证书与中间人攻击可以轻松的抓取HTTPS的加密流量信息,如mitmproxyCharlesFiddler等工具都是基于这个原理。

而SSLPinning就是对于HTTPS协议的反中间人攻击的一种手段。将服务器返回的证书与本地客户端的证书进行校验,如不一致则中断连接。

Frida检测

顾名思义,就是APP中包含了有专门用来检测Frida是否附加了或正在调试自身进程的代码。如果有检测到的话轻则APP闪退,重则封号。如一些游戏的反作弊引擎就会检测Frida,运气不好就会被封,但非游戏的大部分APP只做闪退处理。

禁用SSLPinning

通过Hook之类的方式在证书获取环节返回一个空的证书管理器,从而实现不让客户端校验或者通过Hook关键校验函数并返回true来实现校验成功。

需求分析

现如今的APP只要是正经一点的基本上都会使用SSLPinning来防止用户轻松的抓取HTTPS流量信息,所以我们如果要抓包的话就必须得把SSLPinning干掉。

对于如何干掉SSLPinning其实已经有很多的开源项目了。有基于Xposed的、LSPosed的,甚至Magisk的,但这些都存在一定的兼容性问题,手机变砖那真的是常态了。

而Frida的话则只需要有root权限即可,这也是选择Frida的主要原因之一。但Frida又属于检测的重点照顾对象之一,因此我们还需要进行Anti Anti-Frida

防止SSLPinning

/**
 * 修改默认临时文件名前缀
 * @see https://github.com/frida/frida-java-bridge/blob/main/lib/class-factory.js#L103
*/
if ('frida' === Java.classFactory.tempFileNaming.prefix)
    Java.classFactory.tempFileNaming.prefix = 'gc';

Java.perform(() => {
    const X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
    const SSLContext = Java.use("javax.net.ssl.SSLContext");

    // 注册个假的TrustManager类
    const TrustManager = Java.registerClass({
        implements: [X509TrustManager],
        methods: {
            checkClientTrusted(chain, authType) { },
            checkServerTrusted(chain, authType) { },
            getAcceptedIssuers() {
                return [];
            },
        },
        name: "com.network.TrustManager",
    });
    // 创建用来替换的TrustManager对象数组
    const trustManagers = [TrustManager.$new()];

    const SSLContextInit = SSLContext.init.overload(
        "[Ljavax.net.ssl.KeyManager;",
        "[Ljavax.net.ssl.TrustManager;",
        "java.security.SecureRandom"
    );
    // Hook SSLContext.init 方法
    SSLContextInit.implementation = function (keyManager, trustManager, secureRandom) {
        console.log('[=] Intercepted SSLContext.init()');
    
        // 替换假的TrustManagers
        SSLContextInit.call(this, keyManager, trustManagers, secureRandom);
    };
});

这里只做了一个最基本的防止SSLPinning校验的演示,如果你想了解更多的话推荐查看objection的实现。

反Frida检测

let i = 0;

Interceptor.attach(Module.findExportByName('libc.so', 'strstr'), {
    onEnter(args) {
        this.fridaDetection = false;
        const haystack = Memory.readCString(args[0]).toLowerCase();
        const needle = Memory.readCString(args[1]).toLowerCase();

        if (-1 !== haystack.indexOf(needle) && (
            -1 !== haystack.indexOf('frida') || // frida相关的字符串
            -1 !== haystack.indexOf('gdbus') || // dbus线程名
            -1 !== haystack.indexOf('gum-js-loop') || // gumjs线程名
            -1 !== haystack.indexOf('gmain') || // vala线程名
            -1 !== haystack.indexOf('linjector') || // 命名管道相关
            -1 !== needle.indexOf('/data/local/tmp')) // 目录检测
        ) {
            this.fridaDetection = true;

            if (100 > i) {
                console.log(
                    `[=] 检测你🐎呢\n` +
                    `    haystack: ${haystack}\n` +
                    `    needle: ${needle}\n` +
                    `    i: ${i++}\n`
                );
            } else {
                const detectionModule = Process.findModuleByAddress(this.returnAddress);

                console.log(
                    `[=] 检测你🐎呢\n` +
                    `    haystack: ${haystack}\n` +
                    `    moduleName: ${detectionModule ? detectionModule.name : 'null'}\n` +
                    `    modulePath: ${detectionModule ? detectionModule.path : 'null'}\n` +
                    `    moduleBase: ${detectionModule ? detectionModule.base : 'null'} moduleSize: ${detectionModule ? detectionModule.size : 'null'}\n` +
                    `    detectionPointOffset: ${detectionModule ? this.returnAddress.sub(detectionModule.base) : this.returnAddress}\n`
                );
            }
        }
    },
    onLeave(retval) {
        if (this.fridaDetection)
            retval.replace(0);
    }
});

在这个地方我们通过Hooklibc中的strstr函数来实现简易的过检测,究其原因是因为不管你用啥方法,基于字符串查找的检测最终还是得调用这个方法。

但需要注意的是这种方法不是肯定能过,只是适用部分情况。很多用心点做的检测是可以轻松绕过这份代码的,当然也能Anti Anti就是了,这部分放在后面再说,先来看看代码效果。

未加载脚本时对进程附加Frida

UnAntiAntiFrida

加载反Frida检测脚本后对进程附加Frida

AntiAntiFrida

可以看到,在未加载脚本之前我们附加到进程之后是会直接被卸载掉的,而加载之后则可以正常附加,并且Frida API的调用也正常。

思考

上面说到举例的这种方法只适用于基于字符串匹配的Frida特征检测的情况,那么接下来就举出一些不适用的情况以及一些反检测的思路。列举的一些情况仅为我所知道的,如果你知道更多的请告诉我。

nice

  1. 自己编写执行syscall的汇编代码来代替对于libc中函数的调用。
  2. 使用自己实现的模式匹配算法。
  3. 扫描可执行文件所属内存块,并暴力匹配frida相关特征。
  4. 对指定lib进行监控,循环计算checksum并校验,如果对不上说明被修改或者Hook了,这种方式比较暴力无差别。
  5. 对指定函数头部几个字节进行扫描,检测是否被Hook以及特征是否为Frida所属。

对于其中245三种情况目前我并没有思路,比较好的方法就是找到检测点直接把检测做掉。

所以我们来看一下13情况的处理方式,对于这两种情况有一个通用的做法。即通过ptrace(PTRACE_SYSCALL)的方式来监听APP的syscall调用,并拦截函数序号__NR_openat(对应为libc中的openat函数)。

相关函数与宏在linux/ptrace.hfcntl.h中有定义。在拦截到相关调用的时候检查调用参数pathname是否包含/proc/self/,如果包含则取消调用并将返回值也即fd改为我们提前准备好的去掉frida特征的相关临时文件的fd

这样当程序在对打开的信息流进行扫描的时候就发现不了frida了。

结语

到这里文章就结束了,迷迷糊糊的也不知道有没有说清楚,又水了一篇文章

那么有缘再见~