原文链接:https://api-caller.com/2019/11/05/capture-note/
抓包应该是被问到最多的事情了, 记录一些片段.
珍惜Any
之前提过 一种思路
是遍历所有类, 按照特征去判断校验类和方法.
此处的思路则是, 先跑一遍并让它失败, 找到特征打印堆栈, 即可锁定位置, 再行 hook 即可.
先去看 okhttp
这一块的 源码:
public void check(String hostname, List<Certificate> peerCertificates)
throws SSLPeerUnverifiedException {
List<Pin> pins = findMatchingPins(hostname);
...
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
// Lazily compute the hashes for each certificate.
ByteString sha1 = null;
ByteString sha256 = null;
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
} else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
} else {
throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
}
}
}
// If we couldn't find a matching pin, format a nice exception.
StringBuilder message = new StringBuilder()
.append("Certificate pinning failure!")
.append("\n Peer certificate chain:");
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate)peerCertificates.get(c);
message.append("\n ").append(pin(x509Certificate))
.append(": ").append(x509Certificate.getSubjectDN().getName());
}
message.append("\n Pinned certificates for ").append(hostname).append(":");
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
message.append("\n ").append(pin);
}
throw new SSLPeerUnverifiedException(message.toString());
}
则去寻找 绝不会被混淆的类
来进行 hook. 锁定了几个选择:
X509Certificate.getPublicKey
/getEncoded
ByteString.sha1
StringBuilder.append
SSLPeerUnverifiedException
此处挑选第四条:
Java.use('javax.net.ssl.SSLPeerUnverifiedException').$init.overload('java.lang.String').implementation = function(reason) {
console.log("SSLPeerUnverifiedException", reason);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return this.$init(reason);
};
运行完堆栈信息如下:
SSLPeerUnverifiedException.$init Certificate pinning failure!
Peer certificate chain:
sha256/bQ3........
Pinned certificates for ****.com:
sha256/**************=
sha256/**************=
sha256/**************=
java.lang.Throwable
at okhttp3.CertificatePinner.a(SourceFile)
at okhttp3.internal.connection.RealConnection.a(SourceFile)
okhttp3.CertificatePinner.a` 即混淆前的 `okhttp3.CertificatePinner.check
Java.use('okhttp3.CertificatePinner')['a'].overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) {
console.log(hostname);
}
遇到个奇怪的样本, 设置了 NO_PROXY
, 一般这种情况我用 drony
, 但或许是实现的问题, 就会造成包体有问题, 遂考虑用 hook 代码设置代理.
先分析是什么网络框架, 发现是 okhttp3, 查看 源码
中返回 proxy 的地方, 编写代码:
// 先构造一个 java.net.Proxy
const host = Java.use('java.lang.String').$new("192.168.0.123");
const port = 8888;
const ProxyClz = Java.use('java.net.Proxy');
const InetSocketAddressClz = Java.use('java.net.InetSocketAddress');
const addr = InetSocketAddressClz.$new.overload('java.lang.String', 'int').call(InetSocketAddressClz, host, port);
const HTTP = Java.use('java.net.Proxy$Type').class.getEnumConstants()[1];
console.log(addr);
console.log(HTTP);
const mProxy = ProxyClz.$new.overload('java.net.Proxy$Type', 'java.net.SocketAddress').call(ProxyClz, HTTP, addr);
console.log(mProxy);
Java.use('okhttp3.OkHttpClient')['proxy'].overload().implementation = function() {
console.log("[OkHttpClient][proxy]");
return mProxy;
}
感觉双向认证其实比双向绑定简单, 核心就是 dump 证书和密码.
console.log("Script loaded successfully ");
Java.perform(function() {
const ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream");
const FileOutputStream = Java.use("java.io.FileOutputStream");
const KeyStore = Java.use('java.security.KeyStore');
const store = KeyStore.store.overload('java.io.OutputStream', '[C');
java_security_KeyStore__load(function(instance, charArray) {
if (instance.getType() === "PKCS12" && !dumped) {
dumped = true;
console.log("dumping...") try {
// var s = ByteArrayOutputStream.$new();
var s = FileOutputStream.$new(filesDir + "/test.cert");
store.call(instance, s, charArray);
// var b = s.toByteArray();
// console.log(byteArrayToHex(b))
} catch(e) {
console.warn(e);
}
}
})
}
function java_security_KeyStore__load(fn) {
try {
var KeyStore = Java.use('java.security.KeyStore');
KeyStore['load'].overload('java.io.InputStream', '[C').implementation = function(stream, charArray) {
if (stream == null) {
this.load(stream, charArray);
return;
}
console.log("[KeyStore.load]");
this.load(stream, charArray);
if (fn) {
fn(this, charArray);
}
console.log("\t", stream.toString());
console.log("\t", this.getType());
console.log("\t", charArrayToAsciiHex(charArray))
}
} catch(e) {
console.warn(e)
}
}
function byteArrayToHex(bytes) {
var hex = [];
var data = [];
for (var i = 0; i < bytes['length']; i++) {
hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice( - 2));
}
return hex.join(' ');
}
function charArrayToAsciiHex(charArray) {
var hex = [];
var data = [];
for (var i = 0; i < charArray['length']; i++) {
hex.push(('0' + (charArray[i].charCodeAt(0) & 0xFF).toString(16)).slice( - 2));
data.push(charArray[i]);
}
return hex.join(' ') + '\t' + data.join('');
}
Java.perform(function () {
// Java.openClassFile("/data/local/tmp/gson.dex").load();
// const Gson = Java.use('com.google.gson.Gson').$new(); // 使用Gson将对象类转成Json对象时出现\u003d 、\u0027等情况的问题
// const gson = Java.use('com.google.gson.GsonBuilder').$new().disableHtmlEscaping().create();
/**
* 解析数据包Response
*/
const getOkHttpClient = Java.use("cn.oyohotels.android.core.net.ApiProxyFactory$getOkHttpClient$1");
getOkHttpClient.intercept.implementation = function (chain) {
console.log("\ngetOkHttpClient -> intercept:");
const Response = Java.use("okhttp3.Response");
const ResponseBody = Java.use("okhttp3.ResponseBody");
const Request = Java.use("okhttp3.Request");
// const Headers = Java.use("okhttp3.Headers");
const tmp_req = chain.request();
const request = Java.cast(tmp_req, Request);
console.log("请求基本信息:\n" + request.toString());
const header = request.headers();
console.log("请求Headers:\n" + header.toString());
const requestBody = request.body();
const method = request.method();
if (method == "POST") {
const Buffer = Java.use("okio.Buffer");
const bufferedSink = Buffer.$new();
requestBody.writeTo(bufferedSink);
console.log("请求参数:\n" + bufferedSink.r());
}
// const response = chain.proceed(request); // reqsponse
const ret = this.intercept(chain); // 函数执行结果,response
// showStacks();
const response = Java.cast(ret, Response);
var responseBody = Java.cast(response.body(), ResponseBody);
console.log("请求结果:\n" + responseBody.string() + "\n");
const ret2 = this.intercept(chain); // 关于调用string()方法后会close,临时再发一次..
return ret2;
};
})
APP有两套方案时,使用Hook的办法让APP不走socket,走http。 使用mtopsdk(Android Mtop网关接入文档。https://help.aliyun.com/document_detail/69785.html)Hook Mtop让其不走socket。其sdk对外开放,使用sdk的APP均可以使用。 https://blog.csdn.net/haoren_xhf/article/details/90449978
var SwitchConfig = Java.use("mtopsdk.mtop.global.SwitchConfig");
SwitchConfig.isGlobalSpdySwitchOpen.implementation = function () {
var ret = this.isGlobalSpdySwitchOpen();
log("ret", ret);
return false;
}