首页
社区
课程
招聘
[转帖]Frida.Android.Practice (ssl unpinning)
发表于: 2018-12-5 13:58 6312

[转帖]Frida.Android.Practice (ssl unpinning)

2018-12-5 13:58
6312

原帖:https://xz.aliyun.com/t/2336


Author:瘦蛟舞
Create:20180124

安卓证书锁定解除的工具
对之前发布工具的文章补充,后续还会写一篇证书锁定方案的文章.

目录:

android下hook框架对比

基础设置

免root使用frida

hook java 实战 ssl pinning bypass

hook native

some tips

推荐工具和阅读

0x00 功能介绍竞品对比

官方主页

github

Inject JavaScript to explore native apps on Windows, Mac, Linux, iOS and Android.

Hooking Functions

Modifying Function Arguments

Calling Functions

Sending messages from a target process

Handling runtime errors from JavaScript

Receiving messages in a target process

Blocking receives in the target process

相对于xposed或cydia

优势:

更改脚本不用重启设备(有些xposed插件也可以做到)

对native hook支持较好

开发更便捷(简单的模块确实如此)

兼容性更好,支持设备和系统版本更广

不用单独处理multidex(classLoader问题).

劣势:

不适合写过于复杂的项目,影响app性能比较明显

需要自己注意脚本加载时机

相对容易被检测到,都这样吧.

app启动后进行attach.可以使用-f参数frida来生成已经注入的进程(先注入Zygote为耗时操作),通常配合--no-pause使用.

PY JS脚本混杂排错困难(-l 选项直接写js脚本,新版本错误提示已经非常人性化了.)

E4A这种中文的代码直接GG.

不能全局hook也就是不能一次性hook所有app.只能指定进程hook.

0x01 基础入门设置

PC端设置

python环境

$ pip install -U frida

可选:源码编译

$ git clone git://github.com/frida/frida.git
$ cd frida
$ make

Android设备设置

首先下载android版frida-server,尽量保证与fridaServer与pc上的frida版本号一致.

» frida --version

10.6.55

完整frida-server release地址

https://github.com/frida/frida/releases

# getprop ro.product.cpu.abi                                                                                                    

x86

下一步部署到android设备上:

#!bash
$ adb push frida-server /data/local/tmp/

跑起来

设备上运行frida-server:

root@android:/ # chmod 700 frida-server 
root@android:/ # /data/local/tmp/frida-server -t 0 (注意在root下运行)
root@android:/ # /data/local/tmp/frida-server

电脑上运行adb forward tcp转发:

adb forward tcp:27042 tcp:27042
    adb forward tcp:27043 tcp:27043

27042端口用于与frida-server通信,之后的每个端口对应每个注入的进程.

运行如下命令验证是否成功安装:

#!bash
$ frida-ps -R

正常情况应该输出进程列表如下:

PID NAME
 1590 com.facebook.katana
13194 com.facebook.katana:providers
12326 com.facebook.orca
13282 com.twitter.android
…

0x02 免root使用frida

针对无壳app,有壳app需要先脱壳.

手动完成frida gadget注入和调用.

1.apktool反编译apk

$ apktool d test.apk -o test

2.将对应版本的gadget拷贝到/lib没有了下.例如arm32的设备路径如下.

/lib/armeabi/libfrida-gadget.so

下载地址:

https://github.com/frida/frida/releases/

3.smali注入加载library,选择application类或者Activity入口.

const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

4.如果apk没有网络权限需要在配置清单中加入如下权限申明

<uses-permission android:name="android.permission.INTERNET" />

5.回编译apk

$ apktool b -o newtest.apk test/

6.重新签名安装运行.成功后启动app会有如下日志

Frida: Listening on TCP port 27042

使用objection自动完成frida gadget注入到apk中.

兼容性较差,不是很推荐.

» pip3 install -U objection
» objection patchapk -s yourapp.apk

0x03 JAVA hook 实战 SSL Pinning bypass

实战如何使用Frida,就较常见的证书锁定来做演练.要想绕过证书锁定抓明文包就得先知道app是如何进行锁定操作的.然后再针对其操作进行注入解锁.

客户端关于证书处理的逻辑按照安全等级我做了如下分类:

安全等级策略信任范围破解方法Level 0完全兼容策略信任所有证书包括自签发证书无需特殊操作1系统/浏览器默认策略信任系统或浏览内置CA证书以及用户安装证书
(android 7.0开始默认不信任用户导入的证书)设备安装代理证书2CA Pinning
Root (intermediate) certificate pinning信任指定CA颁发的证书hook注入等方式篡改锁定逻辑3Leaf Certificate pinning信任指定站点证书hook注入等方式篡改锁定逻辑
如遇双向锁定需将app自带证书导入代理软件

文章要对抗的是最后两种锁定的情况(预告:关于证书锁定方案细节另有文章待发布).

注意这里要区分开攻击场景,证书锁定是用于对抗中间人攻击的而非客户端注入,不要混淆.

工具已经开源 :https://github.com/WooyunDota/DroidSSLUnpinning

HttpsURLConnection with a PinningTrustManager

apache http client 因为从api23起被android抛弃,使用率太低就先不管了.

使用传统的HttpURLConnection类封装请求,客户端锁定操作需要实现X509TrustManager接口的checkServerTrusted方法,通过对比预埋证书信息与请求网站的的证书来判断.

https://github.com/moxie0/AndroidPinning/blob/master/src/org/thoughtcrime/ssl/pinning/PinningTrustManager.java

public void checkServerTrusted(X509Certificate[] chain, String authType)
      throws CertificateException
  {
    if (cache.contains(chain[0])) {
      return;
    }

    // Note: We do this so that we'll never be doing worse than the default
    // system validation.  It's duplicate work, however, and can be factored
    // out if we make the verification below more complete.
    checkSystemTrust(chain, authType);
    checkPinTrust(chain);
    cache.add(chain[0]);
  }

知道锁定方法就可以hook解锁了,注入SSLContext的init方法替换信任所有证书的TrustManger

// Get a handle on the init() on the SSLContext class
var SSLContext_init = SSLContext.init.overload(
    '[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');

// Override the init method, specifying our new TrustManager
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {

    quiet_send('Overriding SSLContext.init() with the custom TrustManager');

    SSLContext_init.call(this, null, TrustManagers, null);
};

okhttp ssl pinning

okhttp将锁定操作封装的更人性化,你只要在client build时加入域名和证书hash即可.

okhttp3.x 锁定证书示例代码

String hostname = "yourdomain.com";
 CertificatePinner certificatePinner = new CertificatePinner.Builder()
     .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
     .build();
 OkHttpClient client = OkHttpClient.Builder()
     .certificatePinner(certificatePinner)
     .build();

 Request request = new Request.Builder()
     .url("https://" + hostname)
     .build();
 client.newCall(request).execute();

frida Unpinning script for okhttp

setTimeout(function(){
        Java.perform(function () {
            //okttp3.x unpinning
            try {
                var CertificatePinner = Java.use("okhttp3.CertificatePinner");
                CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(p0, p1){
                    // do nothing
                    console.log("Called! [Certificate]");
                    return;
                };
                CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(p0, p1){
                    // do nothing
                    console.log("Called! [List]");
                    return;
                };
            } catch (e) {
             console.log("okhttp3 not found");
            }
            //okhttp unpinning
            try {
                var OkHttpClient = Java.use("com.squareup.okhttp.OkHttpClient");
                OkHttpClient.setCertificatePinner.implementation = function(certificatePinner){
                    // do nothing
                    console.log("Called!");
                    return this;
                };

                // Invalidate the certificate pinnet checks (if "setCertificatePinner" was called before the previous invalidation)
                var CertificatePinner = Java.use("com.squareup.okhttp.CertificatePinner");
                CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(p0, p1){
                    // do nothing
                    console.log("Called! [Certificate]");
                    return;
                };
                CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(p0, p1){
                    // do nothing
                    console.log("Called! [List]");
                    return;
                };
            } catch (e) {
             console.log("okhttp not found");
            }


        });

},0);

webview ssl pinning

这种场景比很少见,本文拿一个开源项目举例.
https://github.com/menjoo/Android-SSL-Pinning-WebViews
例子中的网站https://www.infosupport.com/证书已经更新过一次,代码中的证书info是2015年的,而线上证书已于2017年更换,所以导致pinning失效,直接使用pinning无法访问网站.

这个开源项目的锁定操作本质是拦截webview的请求后自己用httpUrlConnection复现请求再锁定证书.貌似和之前一样,但是这里的关键不是注入点而是注入时机!

这个例子和上文注入点一样hook SSLcontext即可Unpinning,关键在于hook时机,如果用xposed来hook就没有问题,但是用frida来hook在app启动后附加便会失去hook到init方法的时机.因为pinning操作在Activity onCreate时调用而我们附加是在onCreate之后执行.需要解决能像xposed一样启动前就注入或者启动时第一时间注入.

private void prepareSslPinning() {
        // Create keystore
        KeyStore keyStore = initKeyStore();

        // Setup trustmanager factory
        String algorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = null;
        try {
            tmf = TrustManagerFactory.getInstance(algorithm);
            tmf.init(keyStore);

            // Set SSL context
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
    }

首选想到是spawn,但是spawn后并没有将脚本自动load..(
LD_PRELOAD 条件苛刻不考虑),也就是使用-f参数的时候-l参数并未生效.

frida -U -f com.example.mennomorsink.webviewtest2 --no-pause -l sharecode/objectionUnpinning.js

改由python 来完成spawn注入

#!/usr/bin/python  
# -*- coding: utf-8 -*-
import frida, sys, re, sys, os
from subprocess import Popen, PIPE, STDOUT
import codecs, time

if (len(sys.argv) > 1):
    APP_NAME = str(sys.argv[1])
else:
    APP_NAME = "sg.vantagepoint.uncrackable3"

def sbyte2ubyte(byte):
    return (byte % 256)

def print_result(message):
    print ("[!] Received: [%s]" %(message))

def on_message(message, data):
    if 'payload' in message:
        data = message['payload']
        if type(data) is str:
            print_result(data)
        elif type(data) is list:
            a = data[0]
            if type(a) is int:
                hexstr = "".join([("%02X" % (sbyte2ubyte(a))) for a in data])
                print_result(hexstr)
                print_result(hexstr.decode('hex'))
            else:
                print_result(data)
                print_result(hexstr.decode('hex'))
        else:
            print_result(data)
    else:
        if message['type'] == 'error':
            print (message['stack'])
        else:
            print_result(message)


def kill_process():
    cmd = "adb shell pm clear {} 1> /dev/null".format(APP_NAME)
    os.system(cmd)

kill_process()

try:
    with codecs.open("hooks.js", 'r', encoding='utf8') as f:
        jscode  = f.read()
        device  = frida.get_usb_device(timeout=5)
        pid     = device.spawn([APP_NAME])
        session = device.attach(pid)
        script  = session.create_script(jscode)
        device.resume(APP_NAME)
        script.on('message', on_message)
        print ("[*] Intercepting on {} (pid:{})...".format(APP_NAME,pid))
        script.load()
        sys.stdin.read()
except KeyboardInterrupt:
        print ("[!] Killing app...")
        kill_process()
        time.sleep(1)
        kill_process()

成功Unpinning .(app启动后需要前后台切换一次才会成功hook到init,猜测是因为pinning初始化是在Activity onCreate时完成的.frida注入onCreate有点问题.https://github.com/frida/frida-java/issues/29)

'use strict';
setImmediate(function() {
  send("hooking started");
  Java.perform(function() {
  var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
  var SSLContext = Java.use('javax.net.ssl.SSLContext');

  var TrustManager = Java.registerClass({
      name: 'com.sensepost.test.TrustManager',
      implements: [X509TrustManager],
      methods: {
          checkClientTrusted: function (chain, authType) {
          },
          checkServerTrusted: function (chain, authType) {
          },
          getAcceptedIssuers: function () {
              return [];
          }
      }
  });
  // Prepare the TrustManagers array to pass to SSLContext.init()
  var TrustManagers = [TrustManager.$new()];
  send("Custom, Empty TrustManager ready");
  // Override the init method, specifying our new TrustManager
  SSLContext.init.implementation = function (keyManager, trustManager, secureRandom) {
      send("Overriding SSLContext.init() with the custom TrustManager");
      this.init.call(this, keyManager, TrustManagers, secureRandom);
  };
  });
});

日志如下

» python application.py com.example.mennomorsink.webviewtest2
[*] Intercepting on com.example.mennomorsink.webviewtest2 (pid:1629)...

[!] Received: [hooking started]

[!] Received: [Custom, Empty TrustManager ready]

[!] Received: [Overriding SSLContext.init() with the custom TrustManager]

0x04 Native hook

没有合适公开的例子,就拿https://www.52pojie.cn/thread-611938-1-1.html帖子中提到的无法 hook ndk 中 getInt 函数问题来做演示.

ndk代码

#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO, "hooktest", __VA_ARGS__)
int getInt(int i)
{
    return i+99;
}

extern "C"   JNIEXPORT jstring   JNICALL Java_mi_ndk4frida_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    LOGI("[+] %d\n", getInt(2));
    return env->NewStringUTF("Hello from C++");
}

关键在于对指针和函数入口的理解,例子用了偏移寻址和符号寻址两种方式做对比,偏移和导出符号均可通过IDA静态分析取得,最后效果是一样的.

hook 代码

var fctToHookPtr = Module.findBaseAddress("libnative-lib.so").add(0x5A8);

console.log("fctToHookPtr is at " + fctToHookPtr.or(1));

var getIntAddr = Module.findExportByName("libnative-lib.so" , "_Z6getInti");

console.log("getIntAddr is at " + getIntAddr);

var errorAddr = Module.findExportByName("libnative-lib.so","getInt");

var absoluteAddr;
exports = Module.enumerateExportsSync("libnative-lib.so");
for(i=0; i<exports.length; i++){
    console.log("exports func " + i + " " + exports[i].name);
    if (exports[i].name == "_Z6getInti") {
        absoluteAddr = exports[i].address ;
        console.log("_Z6getInti addr = " + exports[i].address);
        var offset = exports[i].address - Module.findBaseAddress("libnative-lib.so") ;
        console.log("offset addr = " + offset.toString(16).toUpperCase() );
    }
    // exports func 0 _Z6getInti
    // exports func 1 Java_mi_ndk4frida_MainActivity_stringFromJNI
    // exports func 2 _ZN7_JNIEnv12NewStringUTFEPKc
}

//fctToHookPtr.or(1) , getIntAddr , absoluteAddr  are  function hook enter address.
try {
    var fungetInt = new NativeFunction(fctToHookPtr.or(1), 'int', ['int']);
    console.log("invoke 99 > " + fungetInt(99) );
} catch (e) {
    console.log("invoke getInt failed >>> " + e.message);
} finally {

}



Interceptor.attach(getIntAddr, {
    onEnter: function(args) {
        //args and retval are nativePointer...
        console.log("arg = " + args[0].toInt32());
        // //Error: access violation accessing 0x2
        // console.log(hexdump(Memory.readInt(args[0]), {
        //                           offset: 0,
        //                           length: 32,
        //                           header: true,
        //                           ansi: true
        //                         }));

        args[0] = ptr("0x100");
    },
    onLeave:function(retval){
        console.log("ret = " + retval.toInt32());
        // retval.replace(ptr("0x1"));
        retval.replace(222);
    }
});

0x05 tips

获取app context

var currentApplication = Dalvik.use("android.app.ActivityThread").currentApplication(); 
    var context = currentApplication.getApplicationContext();

创建对象示例

obj.$new();

hook 构造方法

obj.$init.implementation = function (){
}

实现java接口

https://gist.github.com/oleavr/3ca67a173ff7d207c6b8c3b0ca65a9d8

java接口使用参考,其中X509TrustManager是interface类型.TrustManager为其实现类.manager为实例.

我就成功过这一个接口,其他接口比如Runnable , HostNamerVerifier都没成功.

'use strict';

var TrustManager;
var manager;

Java.perform(function () {
  var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');

  TrustManager = Java.registerClass({
    name: 'com.example.TrustManager',
    implements: [X509TrustManager],
    methods: {
      checkClientTrusted: function (chain, authType) {
        console.log('checkClientTrusted');
      },
      checkServerTrusted: function (chain, authType) {
        console.log('checkServerTrusted');
      },
      getAcceptedIssuers: function () {
        console.log('getAcceptedIssuers');
        return [];
      }
    }
  });
  manager = TrustManager.$new();
});

str int指针操作,有点乱
utf8 string写
Memory.allocUtf8String(str)
var stringVar = Memory.allocUtf8String("string");
utf8 string读
Memory.readUtf8String(address[, size = -1])

int写
var intVar = ptr("0x100");
var intVar = ptr("256");
int读
toInt32(): cast this NativePointer to a signed 32-bit integer

二进制读取
hexdump(target[, options]): generate a hexdump from the providedArrayBufferor _NativePointer_target, optionally withoptionsfor customizing the output.

0x06 推荐工具和阅读

frida api
https://www.frida.re/docs/javascript-api
中文翻译
https://zhuanlan.kanxue.com/article-342.htm
https://zhuanlan.kanxue.com/article-414.htm

工具推荐
appmon :https://github.com/dpnishant/appmon
droidSSLUnpinning :https://github.com/WooyunDota/DroidSSLUnpinning
objection :https://github.com/sensepost/objection

0x07 reference

https://github.com/datatheorem/TrustKit-Android
https://github.com/moxie0/AndroidPinning
https://koz.io/using-frida-on-android-without-root/
https://medium.com/@appmattus/android-security-ssl-pinning-1db8acb6621e
https://developer.android.com/training/articles/security-ssl.html#Pinning
https://developer.android.com/training/articles/security-config.html?hl=zh-cn


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2018-12-5 13:59 被roysue编辑 ,原因:
收藏
免费 0
支持
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  junkboy   +1.00 2018/12/05
最新回复 (5)
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
支持
2018-12-5 14:24
0
雪    币: 40
活跃值: (1157)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
这个非常好。。。。
2018-12-5 14:42
0
雪    币: 40
活跃值: (1157)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
这个非常好。。。。
2018-12-5 14:42
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
objection自动完成frida gadget注入到apk中报错  
2019-8-9 17:18
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
IndexError: list index out of range
Cleaning up temp files...
Failed to cleanup with error: [WinError 2] 系统找不到指定的文件。: 'C:\\Users\\python\\AppData\\Local\\Temp\\tmpaqhqysrn.apktemp.objection.apk'
什么原因啊
2019-8-9 17:18
0
游客
登录 | 注册 方可回帖
返回
//