# Frida.Android.Practice (ssl unpinning)
---
**Author:瘦蛟舞**
**Create:20180124**
[[toc]]
https://github.com/WooyunDota/DroidSSLUnpinning 对之前发布工具的文章补充,后续还会发一篇证书锁定方案的文章.
目录:
* android下hook框架对比
* 基础设置
* 免root使用frida
* hook java 实战 ssl pinning bypass
* hook native
* some tips
* 推荐工具和阅读
## 0x00 功能介绍竞品对比
---
[官方主页](http://www.frida.re/)
[github](http://github.com/frida/)
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环境
```bash
pip install frida-tools # CLI tools
pip install frida # Python bindings
npm install frida # Node.js bindings
```
可选:源码编译
$ 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](https://github.com/frida/frida/releases)
```
# getprop ro.product.cpu.abi
x86
```
下一步部署到android设备上:
#!bash
$ adb push frida-server /data/local/tmp/
**跑起来**
设备上运行frida-server:
```bash
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转发:
```bash
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
```bash
$ 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入口.
```smali
const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
```
4.如果apk没有网络权限需要在配置清单中加入如下权限申明
```xml
```
5.回编译apk
```bash
$ apktool b -o newtest.apk test/
```
6.重新签名安装运行.成功后启动app会有如下日志
```bash
Frida: Listening on TCP port 27042
```
### 使用objection自动完成frida gadget注入到apk中.
兼容性较差,不是很推荐.
```bash
» pip3 install -U objection
» objection patchapk -s yourapp.apk
```
## 0x03 JAVA hook 实战 SSL Pinning bypass
实战如何使用Frida,就较常见的证书锁定来做演练.要想绕过证书锁定抓明文包就得先知道app是如何进行锁定操作的.然后再针对其操作进行注入解锁.
客户端关于证书处理的逻辑按照安全等级我做了如下分类:
| 安全等级 | 策略 | 信任范围 | 破解方法 |
|---|---|---|---|
| 0 | 完全兼容策略 | 信任所有证书包括自签发证书 | 无需特殊操作 |
| 1 | 系统/浏览器默认策略 | 信任系统或浏览内置CA证书以及用户安装证书| 设备安装代理证书 |
| 2 | system CA pinning | 只信任系统根证书,不信任用户安装的证书
(android 7.0支持配置network-security-config) | 注入或者root后将用户证书拷贝到系统证书目录 |
| 3 | CA Pinning
Root (intermediate) certificate pinning | 信任指定CA颁发的证书 | hook注入等方式篡改锁定逻辑 |
| 4 | Leaf Certificate pinning | 信任指定站点证书 | hook注入等方式篡改锁定逻辑
如遇双向锁定需将app自带证书导入代理软件 |
文章要对抗的是最后两种锁定的情况(预告:关于证书锁定方案细节另有文章待发布).
注意这里要区分开攻击场景,证书锁定是用于对抗中间人攻击的而非客户端注入,不要混淆.
### 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
```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
```JavaScript
// 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 锁定证书示例代码
```java
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
```JavaScript
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一样启动前就注入或者启动时第一时间注入.
```Java
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参数并未生效.(需要用%resume命令)
```bash
frida -U -f com.example.mennomorsink.webviewtest2 --no-pause -l sharecode/objectionUnpinning.js
```
改由python 来完成spawn注入
```python
#!/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)
```JavaScript
'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);
};
});
});
```
日志如下
```bash
» 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](https://www.52pojie.cn/thread-611938-1-1.html) 帖子中提到的无法 hook ndk 中 getInt 函数问题来做演示.
ndk代码
```C
#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 代码
```javascript
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 " + 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
### 插件加载Dynamic load
`Java.openClassFile(dexPath).load();`
### Classloader枚举
```JavaScript
Java.enumerateClassLoaders({
onMatch: function (loader)
{
console.log("loader = " + loader);
// count++;
},
onComplete: function ()
{
// send((count > 0) ? 'count > 0' : 'count == 0');
}
});
```
### UI thread 注入
`Java.scheduleOnMainThread(fn)`: run `fn` on the main thread of the VM.
示例
```JavaScript
Java.perform(function() {
var Toast = Java.use('android.widget.Toast');
var currentApplication = Java.use('android.app.ActivityThread').currentApplication();
var context = currentApplication.getApplicationContext();
Java.scheduleOnMainThread(function() {
Toast.makeText(context, "Hello World", Toast.LENGTH_LONG.value).show();
})
})
```
### 获取对象实例
`Java.choose(className, callbacks)`
example
```JavaScript
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function () {
Java.choose("android.view.View", {
"onMatch":function(instance){//This function will be called for every instance found by frida
console.log("[*] Instance found");
},
"onComplete":function() {
console.log("[*] Finished heap search")
}
});
});
});
```
### 泛型处理
Use `Java.cast()` when dealing with generics. 可以理解为java中的强制类型转换.
```javascript
var Activity = Java.use("android.app.Activity");
var activity = Java.cast(ptr("0x1234"), Activity);
```
Generics currently result in `java.lang.Object`. 泛型Frida默认当为object对象.
List ,其中泛型E在frida中会被认为object.
### 创建Java数组
byte数组创建如下
```javascript
var buffer = Java.array('byte', [ 13, 37, 42 ]);
````
### 获取app context
对于动态加载的dex,context的获取时机很重要
```javascript
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var context = currentApplication.getApplicationContext();
```
### 创建对象示例
```
obj.$new();
```
### hook 构造方法
```
obj.$init.implementation = function (){
}
```
### 实现java接口interface
https://gist.github.com/oleavr/3ca67a173ff7d207c6b8c3b0ca65a9d8
java接口使用参考,其中X509TrustManager是interface类型.TrustManager为其实现类.manager为实例.
我就成功过这一个接口,其他接口比如Runnable , HostNamerVerifier都没成功.
```JavaScript
'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 provided_ArrayBuffer_ or \_NativePointer_ `target`, optionally with `options` for 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