-
证书转移到系统证书目录,过VPN检测
-
证书Dump失败
-
HOOK 发包点
-
添加拦截器
-
HOOK 过监测点之大海捞针
-
未实现思路(解决HOOK日志不好看的问题)
一、证书转移到系统目录
证书转移到系统的证书目录肯定是抓不到包的,要不然也不会这么费劲了,刚开始怀疑是检测到了VPN,然后在rom里过了VPN检测,发现依旧抓不到包。
本来是使用reqable抓包,但是他不会显示抓包失败的具体原因,于是开始更换成charles进行抓包
可以看到在这个请求失败的原因是客户端主动断开
二、证书Dump
还是先把这个写了,这个是真踩坑里了,本想学习一下 r0capture 的dump证书的方法,自己写一个证书dump,从hook到拿值一路顺风,但是保存成p12证书时报错
r0capture 代码
function storeP12(pri, p7, p12Path, p12Password) {
var X509Certificate = Java.use("java.security.cert.X509Certificate")
var p7X509 = Java.cast(p7, X509Certificate);
var chain = Java.array("java.security.cert.X509Certificate", [p7X509])
var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC");
ks.load(null, null);
ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain);
try {
var out = Java.use("java.io.FileOutputStream").$new(p12Path);
ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray())
} catch (exp) {
console.log(exp)
}
}
//在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式,证书密码为r0ysue
Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () {
var result = this.getPrivateKey()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue');
var message = {};
message["function"] = "dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12' + ' pwd: r0ysue';
message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());
var data = Memory.alloc(1);
send(message, Memory.readByteArray(data, 1))
return result;
}
Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function () {
var result = this.getCertificateChain()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue');
var message = {};
message["function"] = "dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12' + ' pwd: r0ysue';
message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());
var data = Memory.alloc(1);
send(message, Memory.readByteArray(data, 1))
return result;
}
改写成xposed
// 实现 storeP12 逻辑,将私钥和证书存储为 PKCS#12 文件
private static void storeP12(Object privateKey, Object cert, String p12Path, String p12Password) throws Exception {
// Cast 证书为 X509Certificate 类型
X509Certificate p7X509 = (X509Certificate) cert;
PrivateKey pKey = (java.security.PrivateKey) privateKey;
// Log.d(TAG, "storeP12: p7X509--->" + p7X509);
// Log.d(TAG, "storeP12: p7X509 getPublicKey--->" + p7X509.getPublicKey());
// Log.d(TAG, "storeP12: p7X509 getSigAlgName--->" + p7X509.getSigAlgName());
// Log.d(TAG, "storeP12: p7X509 getSigAlgName--->" + p7X509);
// Log.d(TAG, "storeP12: pKey getEncoded--->" + Arrays.toString(pKey.getEncoded()));
X509Certificate[] chain = new X509Certificate[]{p7X509};
KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
ks.load(null, null); // 加载空的 KeyStore
ks.setKeyEntry("client", pKey, p12Password.toCharArray(), chain); // 设置私钥和证书链
try (FileOutputStream fos = new FileOutputStream(p12Path)) {
ks.store(fos, p12Password.toCharArray());
Log.e("Lychow666", "Success saving P12: ");
} catch (Exception e) {
Log.e("Lychow666", "Error saving P12: " + e.getMessage());
}
}
Class<?> privateKeyEntry = getClass("java.security.KeyStore$PrivateKeyEntry");
if (privateKeyEntry != null) {
xposedHelpers.findAndHookMethod(privateKeyEntry, "getPrivateKey", new XC_MethodHook() {
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
try {
String packName = getPackageName();
Object privateKey = param.getResult();
Object cert = rHelpers.callMethod(param.thisObject, "getCertificate");
Log.d(TAG, "getPrivateKey privateKey: " + privateKey.toString());
Log.d(TAG, "getPrivateKey cert: " + cert.toString());
String p12Path = "/data/data/" + packName + "/" + UUID.randomUUID().toString() + ".p12";
storeP12(privateKey, cert, p12Path, "lychow");
} catch (Throwable e) {
Log.e(TAG, "afterHookedMethod wrong: " + e);
}
}
});
xposedHelpers.findAndHookMethod(privateKeyEntry, "getCertificateChain",
new XC_MethodHook() {
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
String packName = getPackageName();
Object privateKey = xposedHelpers.callMethod(param.thisObject, "getPrivateKey");
Object cert = xposedHelpers.callMethod(param.thisObject, "getCertificate");
Log.d(TAG, "getCertificateChain privateKey: " + privateKey.toString());
Log.d(TAG, "getCertificateChain cert: " + cert.toString());
// 保存到 .p12 文件
String p12Path = "/sdcard/Download/" + packName + UUID.randomUUID().toString() + ".p12";
storeP12(privateKey, cert, p12Path, "Lychow");
}
});
}
然后运行报错
storeP12: pKey getEncoded--->null
storeP12: pKey--->android.security.keystore.AndroidKeyStoreRSAPrivateKey@e5d4db87
Error saving P12: exception encrypting data - java.security.InvalidKeyException: Cannot wrap key, null encoding.
privateKey.getEncoded 的返回值是null,导致保存失败,问了下 AI 说是私钥不可导出。。。。
TM r0 咋保存的呢
三、HOOK 发包点
JAVA 层
这里参考了han佬的星球文章 《Android10集成r0capture》
优化建议:
1、代码放在锁后面
2、保存文件时同时来两个请求可能会存在覆盖问题(保存两次,后面一次只有一个请求把前面的全部覆盖掉了)
① 文件名在保存文件时再加上时间计算,但是这样会导致非常多的请求文件
②自己想办法解决掉覆盖,加锁啊、判断啥的
虽然拿请求流没毛病,但是也有一点小问题,请求体是明文,但是请求体是乱码。
这是因为请求是http2,请求头被压缩了,由于rom里没有okhttp等三方包,所以解析http2的请求头只能用纯原生库进行强行解析,写一个识别h2请求头和解析的代码就行
判断是否http2
public static boolean isHttp2(byte[] buffer) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(buffer)) {
int initialByte = byteArrayInputStream.read();
// 判断是否为HTTP/2.0请求
if (initialByte == 0x0 || initialByte == 0x4 || initialByte == 0x8) {
return true;
} else {
return false;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
如果是http2,对请求头进行解析
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder;
import java.io.UnsupportedEncodingException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
public class Http2HeaderParsingExample {
static String request_txt = "";
static String response_txt = "";
public static String parseHttp2Frame(byte[] buffer) throws UnsupportedEncodingException {
int offset = 0;
while (offset < buffer.length) {
if (buffer.length - offset < 9) {
request_txt += "不完整的帧n";
return request_txt;
}
// 获取长度(前3字节)
int length = ((buffer[offset] & 0xFF) << 16) |
((buffer[offset + 1] & 0xFF) << 8) |
(buffer[offset + 2] & 0xFF);
// 获取类型(1字节)
int type = buffer[offset + 3] & 0xFF;
// 获取标志(1字节)
int flags = buffer[offset + 4] & 0xFF;
// 获取流ID(4字节,忽略最高位)
int streamId = ((buffer[offset + 5] & 0x7F) << 24) |
((buffer[offset + 6] & 0xFF) << 16) |
((buffer[offset + 7] & 0xFF) << 8) |
(buffer[offset + 8] & 0xFF);
offset += 9; // 跳过帧头部
if (buffer.length - offset < length) {
request_txt += "不完整的帧负载n";
return request_txt;
}
byte[] payload = new byte[length];
System.arraycopy(buffer, offset, payload, 0, length);
switch (type) {
case 0x01: // HEADERS
request_txt += "HEADERS:n";
try {
// parseHeadersFrame(payload);
// 2. 将字节数组转换为 Netty 的 ByteBuf
ByteBuf inputBuffer = Unpooled.wrappedBuffer(payload);
// 使用 Netty 的 DefaultHttp2HeadersDecoder 来解码
DefaultHttp2HeadersDecoder decoder = new DefaultHttp2HeadersDecoder();
// 解码 HPACK 数据
Http2Headers headers = decoder.decodeHeaders(0, inputBuffer);
for (Map.Entry<CharSequence, CharSequence> entry : headers) {
request_txt += entry.getKey() + ": " + entry.getValue() + "n";
}
}catch (Throwable e){
request_txt += "HEADERS 帧解析错误n";
}
break;
case 0x00: // DATA
request_txt += "数据内容: " + new String(payload, "UTF-8")+"n";
break;
// 其他帧类型可以根据HTTP/2.0规范进行处理
default:
request_txt += "其他帧类型n";
break;
}
// 跳到下一帧
offset += length;
}
return request_txt;
}
}
另外,HOOK抓包的通病:发出去的请求与返回请求无法 一 一对应起来
解决办法:参考r0capture,每一个请求一个ssl_session_id,这里我还没有去写,因为老大不想要 hook 抓包,以后再实现吧
function getSslSessionId(ssl) {
var session = SSL_get_session(ssl);
if (session == 0) {
return 0;
}
var len = Memory.alloc(4);
var p = SSL_SESSION_get_id(session, len);
len = Memory.readU32(len);
var session_id = "";
for (var i = 0; i < len; i++) {
// Read a byte, convert it to a hex string (0xAB ==> "AB"), and append
// it to session_id.
session_id +=
("0" + Memory.readU8(p.add(i)).toString(16).toUpperCase()).substr(-2);
}
return session_id;
}
SO层
JAVA 层的这个hook虽然能抓到包,但是,有漏包。。。只好去改SO层发包点了
测试时改了boringssl 的SSL_write,和SSL_read,文件路径是
/external/boringssl/src/ssl/ssl_lib.cc --SSL_write && --SSL_read
同样的,把前面的java层的代码翻译成c代码加到里面就是了
but,有tmd大问题,多线程频繁的文件操作写入,句柄的释放,会导致内存问题,多线程操作太不安全了
也懒得去解决这玩意儿,建议就直接放弃文件写入的方式,太拉了,至于解决在后面的未实现思路里
通过HOOK抓包经常看到有一些请求,无论是headers 还是body,全部都乱码,这也是放弃hook抓包的主要原因
四、添加拦截器
找到 Okhttp 的Builder,在build前添加一个自写的拦截器(确保拦截器在最后)
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 打印请求信息
long t1 = System.nanoTime();
Log.d(TAG, String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
// 如果有请求体,打印请求体内容
if (request.body() != null) {
Buffer buffer = new Buffer();
request.body().writeTo(buffer);
Log.d(TAG, "Request Body: " + buffer.readUtf8());
}
// 执行请求并获取响应
Response response = chain.proceed(request);
// 打印响应信息
long t2 = System.nanoTime();
Log.d(TAG, String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
// 打印响应体内容
if (response.body() != null) {
String responseBody = response.body().string();
Log.d(TAG, "Response Body: " + responseBody);
// 为了不影响后续读取响应体内容,这里需要重新创建响应体
return response.newBuilder()
.body(ResponseBody.create(response.body().contentType(), responseBody))
.build();
}
return response;
}
有这么简单吗?没有
我们终究不是真正的在目标APP内,而自写拦截器需要 implements Interceptor,我们没有这个Interceptor。
拿到类直接 implements 会报错,试了半天也不行
再加上类名、方法名全部都被混淆。类型转换稍微有点麻烦。
所以,建议用反射去写,将方法名类名改成混淆后的就行了,方便改,然后找到 .addInterceptor 这个方法,进行hook,在最后一个拦截器添加后,通过xposed,加上我们自己的这段逻辑
public Object inter_reflact(Interceptor.Chain chain) {
try {
Class<?> chainClass = chain.getClass();
// 获取 Request 对象
Method requestMethod = chainClass.getMethod("request");
Object requestObj = requestMethod.invoke(chain);
// 打印请求信息
Class<?> requestClass = requestObj.getClass();
Method urlMethod = requestClass.getMethod("url");
Object urlObj = urlMethod.invoke(requestObj);
Method connectionMethod = chainClass.getMethod("connection");
Object connectionObj = connectionMethod.invoke(chain);
Method headersMethod = requestClass.getMethod("headers");
Object headersObj = headersMethod.invoke(requestObj);
long t1 = System.nanoTime();
Log.d(TAG, String.format("Sending request %s on %s%n%s",
urlObj, connectionObj, headersObj));
// 如果有请求体,打印请求体内容
Method bodyMethod = requestClass.getMethod("body");
Object bodyObj = bodyMethod.invoke(requestObj);
if (bodyObj != null) {
Class<?> bodyClass = bodyObj.getClass();
Method writeToMethod = bodyClass.getMethod("writeTo", Buffer.class);
Buffer buffer = new Buffer();
writeToMethod.invoke(bodyObj, buffer);
String requestBody = buffer.readUtf8();
Log.d(TAG, "Request Body: " + requestBody);
}
// 获取并调用 chain.proceed(request)
Method proceedMethod = chainClass.getMethod("proceed", requestClass);
Object responseObj = proceedMethod.invoke(chain, requestObj);
// 打印响应信息
Class<?> responseClass = responseObj.getClass();
Method responseRequestMethod = responseClass.getMethod("request");
Object responseRequestObj = responseRequestMethod.invoke(responseObj);
long t2 = System.nanoTime();
Log.d(TAG, String.format("Received response for %s in %.1fms%n%s",
urlMethod.invoke(responseRequestObj), (t2 - t1) / 1e6d, headersMethod.invoke(responseRequestObj)));
// 打印响应体内容
Method responseBodyMethod = responseClass.getMethod("body");
Object responseBodyObj = responseBodyMethod.invoke(responseObj);
if (responseBodyObj != null) {
// 获取响应体内容
Method stringMethod = responseBodyObj.getClass().getMethod("string");
String responseBody = (String) stringMethod.invoke(responseBodyObj);
Log.d(TAG, "Response Body: " + responseBody);
// 获取 contentType 方法
Method contentTypeMethod = responseBodyObj.getClass().getMethod("contentType");
Object contentTypeObj = contentTypeMethod.invoke(responseBodyObj);
// 使用反射重新构造 ResponseBody
Class<?> responseBodyClass = responseBodyObj.getClass();
Method createMethod = responseBodyClass.getMethod("create", contentTypeObj.getClass(), String.class);
Object newResponseBody = createMethod.invoke(null, contentTypeObj, responseBody);
// 重新创建 Response 并返回
Method newBuilderMethod = responseClass.getMethod("newBuilder");
Object responseBuilderObj = newBuilderMethod.invoke(responseObj);
Method bodyBuilderMethod = responseBuilderObj.getClass().getMethod("body", responseBodyClass);
Object finalResponse = bodyBuilderMethod.invoke(responseBuilderObj, newResponseBody);
Method buildMethod = responseBuilderObj.getClass().getMethod("build");
return buildMethod.invoke(responseBuilderObj);
}
return requestObj;
} catch (Throwable e) {
Log.d(TAG, "LoggingInterceptor wrong " + e);
}
return null;
}
五、HOOK过检测
纯纯大海捞针,使用justtrustme的各种版本、sslunping,都不行,不用还好,用了之后不抓包都会报错
在各种通用HOOK都无效时,那就只能去看他的代码逻辑了,看检测了啥
但是代码全部被混淆,再加上实际上他有好几个不同的发包:APP服务端、谷歌、以及一个性能监测服务,都是不同的连接,用到证书相关的地方海了去了,在找到几个检测点跟完后发现不是发往APP服务端的检测后心态彻底爆炸,再加上代码混淆跳来跳去的,直接选择放弃挨个过这种方法
没办法,只能大海捞针了,方法:
这里收集了几个证书相关的类,HOOK这些类所有方法,打印调用入参返回和流程,查看在开启抓包前后是否有区别,然后在第一个变化的地方进行打堆栈,往上找调用方法,看逻辑,然后HOOK
HOOK 类的所有方法
public static void HookAllMethod(Class<?> targetClass) {
Method[] methods = targetClass.getDeclaredMethods();
// 遍历所有方法
for (final Method method : methods) {
xposedBridge.hookMethod(method, new XC_MethodHook() {
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
// 打印方法名和入参
Log.e(TAG + "hook", "Hooking method: " + targetClass.getName() + "." + method.getName());
Object[] args = param.args;
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
Log.e(TAG + "hook", targetClass.getName() + "." + method.getName() + " Arg[" + i + "]: " + args[i]);
}
} else {
Log.e(TAG + "hook", targetClass.getName() + "." + method.getName() + "Method has no arguments");
}
}
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 打印返回结果
Object result = param.getResult();
Log.e(TAG + "hook", targetClass.getName() + "." + method.getName() + " Result: " + result);
}
});
}
}
HOOK 的类
Class<?> keyStore = getClass("java.security.KeyStore");
Class<?> certificate = getClass("java.security.cert.X509Certificate");
Class<?> trustManager = getClass("javax.net.ssl.TrustManager");
Class<?> certificateFactory = getClass("java.security.cert.CertificateFactory");
Class<?> trustManagerFactory = getClass("javax.net.ssl.TrustManagerFactory");
Class<?> TrustManagerImpl = getClass("com.android.org.conscrypt.TrustManagerImpl");
Class<?> RootTrustManager = getClass("android.security.net.config.RootTrustManager");
Class<?> PrivateKeyEntry = getClass("java.security.KeyStore$PrivateKeyEntry");
Class<?> NetworkSecurityTrustManagery = getClass("android.security.net.config.NetworkSecurityTrustManagery");
Class<?> CertPathValidatorException = getClass("java.security.cert.CertPathValidatorException");
if (keyStore != null && certificate != null && trustManager != null && certificateFactory != null && trustManagerFactory != null) {
try {
HookAllMethod(keyStore);
HookAllMethod(certificate);
HookAllMethod(trustManager);
HookAllMethod(certificateFactory);
HookAllMethod(trustManagerFactory);
} catch (Throwable e) {}
}
还有 SSLContext、HostnameVerifier 等等,可以去看看justtrustMe等框架 hook了哪些类,自己补充进去
然后把抓包开启前后的日志,放到文本对比工具中一对比,找到最先变化的方法调用,直接打堆栈查就完了
虽然是大海捞针,最后还真让我捞到了
六、没踩下去的坑
之前HOOK发包点有一个问题请求很不方便看,既然HTTPS = HTTP+TLS,那么在这里是否可以将 https 转成 http 请求,把请求流进行 适当处理后 直接本地转发,抓包软件就能直接抓这个本地转发的明文包,而且方便看(来自菜鸟的幻想)
其实感觉要是勤快点,找一个开源的抓包软件,照着自己写一个解析要好得多
原文始发于微信公众号(逆向成长日记):安卓抓包十八式
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论