安卓抓包十八式

admin 2024年9月27日18:03:36评论16 views字数 14785阅读49分17秒阅读模式
  1. 证书转移到系统证书目录,过VPN检测

  2. 证书Dump失败

  3. HOOK 发包点

  4. 添加拦截器

  5. HOOK 过监测点之大海捞针

  6. 未实现思路(解决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() {        @Override        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() {              @Override              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--->nullstoreP12: pKey--->android.security.keystore.AndroidKeyStoreRSAPrivateKey@e5d4db87Error 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前添加一个自写的拦截器(确保拦截器在最后)

        @Override        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() {            @Override            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");                }            }            @Override            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 请求,把请求流进行 适当处理后 直接本地转发,抓包软件就能直接抓这个本地转发的明文包,而且方便看来自菜鸟的幻想

其实感觉要是勤快点,找一个开源的抓包软件,照着自己写一个解析要好得多

    第一次跟完安卓java层混淆的,感觉还是收获了很多,猜类更准了建议新手小白都来练练手

安卓抓包十八式

原文始发于微信公众号(逆向成长日记):安卓抓包十八式

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月27日18:03:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓抓包十八式https://cn-sec.com/archives/3215013.html

发表评论

匿名网友 填写信息