跳转至

抓包及其限制绕过

手机模拟器选择

  • windows

较好的可选项如下:

  1. 夜神模拟器(基于visualbox),在主机没有其他虚拟环境时较好
  2. Android Studio
  3. mumu模拟器
  4. 雷电模拟器

  5. macos

  6. Android Studio

如果安装Android Studio后命令行输入adb显示找不到命令,那么请配置环境变量

  • Linux/macOS
Bash
# vim ~/.zshrc, wirte
# Android SDK
export ANDROID_HOME=~/Library/Android/sdk
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/cmdline-tools/latest:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"

image-20250330131028498

  • Windows

编辑用户/系统Path环境变亮,增加sdk路径即可

默认路径为:C:\Users\<username>\AppData\Local\Android\Sdk\platform-tools

安装系统证书

在anroid7以上,系统默认不会信任用户证书,这导致某些app的流量无法抓取到,所以必须要将工具证书导入系统证书

创建burp证书

打开burp,电脑浏览器访问"http://burp“并下载证书cacert.cer,获得证书文件cacert.cer之后,准备将其转换成可放入系统证书库的文件。

先将burp的证书转换为pem格式,再计算其hash值,然后重命名为可以导入Android系统的xxx.0文件。

Bash
openssl x509 -inform DER -in 2.cer -out 2.pem
openssl x509 -inform PEM -subject_hash_old -in 2.pem| head -1
cp 2.pem 9a5ba575.0

Android Studio

模拟器无法root解决

image-20250330133439319

启动模拟器后无法root,这是由于模拟器镜像是从google play下载的,而google play默认无法root,所以我们需要更换镜像

image-20250330141023473

选择不带google play服务的镜像下载

导入系统证书

现在我们必须将 CA 证书放在位于Android 文件系统中的/system/etc/security/cacerts/系统证书存储库中。默认情况下,/system分区以只读方式挂载。因此需要获得/system分区的写入权限并复制上一步中创建的证书。

通过命令行配置启动参数的方式运行AVD(参考:解决Android Studio ADV模拟器无法使用remount命令记录),就可以以写的方式启动AVD并向系统目录写入证书文件。

进入到AVD目录并以 “-writable-system” 的方式启动AVD

注意:如果你想使用你的证书,你必须使用 -writable-system 选项启动模拟器。否则 Android 将加载“干净”的系统镜像。

Bash
cd /Users/xxx/Library/Android/sdk/emulator/
# 查看模拟器名字
./emulator -list-avds
# 以可写权限启动
./emulator -avd '@模拟器名字' -writable-system

image-20230106165016372

然后就可以看到AVD运行了起来,

通过命令行启动AVD 根据API差异,可能存在不同。

API LEVEL > 28 的说明:

Tested on emulators running API LEVEL 29 and 30

Bash
./emulator -list-avds
./emulator -avd <avd_name_here> -writable-system  # (如果要看内核日志,可以加上 -show-kernel)
adb root
adb shell avbctl disable-verification  # 禁用安全启动验证
adb reboot
adb root
adb remount # 将分区重新挂载为读写
adb push <path_to_certificate> /system/etc/security/cacerts
adb shell chmod 644 /system/etc/security/cacerts/<name_of_pushed_certificate>
adb reboot

API LEVEL <= 28 的说明:

Tested on emulators running API LEVEL 26, 27 and 28

Bash
./emulator -list-avds
./emulator -avd <avd_name_here> -writable-system  # (如果要看内核日志,可以加上 -show-kernel)
adb root $ adb remount # 将分区重新挂载为读写
adb push <path_to_certificate> /system/etc/security/cacerts
adb shell chmod 644 /system/etc/security/cacerts/<name_of_pushed_certificate>
adb reboot

image-20230106165136792

Note

windows的默认路径在:C:\Users\%user%\AppData\Local\Android\Sdk\emulator

检查证书权限:

Bash
xavier@Mac emulator % adb shell ls -al /system/etc/security/cacerts/ | grep 9a
-rw-r--r-- 1 root root 8650 2022-07-08 05:11 3c9a4d3b.0
-rw-r--r-- 1 root root 1330 2023-01-05 14:50 9a5ba575.0
-rw-r--r-- 1 root root 2938 2022-07-08 05:11 9aef356c.0
-rw-r--r-- 1 root root 2760 2022-07-08 05:11 ed39abd0.0

检查是否成功加入系统证书:

安卓模拟器: 设置-安全-更多安全设置-加密与凭据-信任的凭据:

image-20230106170059289

不同版本的Android系统路径也不一样,可以进设置后直接搜索“凭据” 或“cerdentials”。AVD启动后默认语言是英语,可以自行修改。

image-20230106170301901

Important

需要注意的是,安装系统证书后,再次启动模拟器必须以可写-writable-system模式启动,如果是在图形化页面启动,则系统镜像以纯净模式启动,加载的证书会消失

模拟器启动卡住问题解决

有时启动模拟器会一直卡住,模拟器一直黑屏,无法正常启动,解决方法如下

image-20250403124856929

进入edit

image-20250403124908804

将default boot由quick改为cold启动

五步解决 Android Studio 模拟器启动失败问题_android studio模拟器运行不出来-CSDN博客

VPN工具

Android Studio可直接配置http/https代理

image-20250330165841955

但如果没有抓到包的话也可以使用代理工具将系统流量代理到burp

clash

配置yaml规则文件,只要将配置文件中的server和port修改成burp监听的ip和port就可以了(至于其他配置,可以查阅相关文档进行自定义的配置)

YAML
mixed-port: 7890
allow-lan: false
mode: global
log-level: info
external-controller: 127.0.0.1:9090
proxies:
  - name: "burp"
    type: http
    server: 192.168.70.72
    port: 8080
proxy-groups:
  - name: Proxy
    type: select
    proxies:
      - burp

修改完之后直接adb push到AVD当中。

Bash
% adb push ~/Desktop/clash-config.yml /sdcard/
/Users/xavier/Desktop/clash-config.yml...kipped. 1.9 MB/s (252 bytes in 0.000s)

然后打开clash,“配置–> + –> 文件:从文件导入 –> 浏览文件 –> 更多:导入"从文件导入配置就可以了。 使用时在"配置"中勾选刚导入的配置文件

appproxy

GitHub - ys1231/appproxy: 基于flutter的Android vpn代理工具 Android VPN Proxy Tool Based on Flutter

详情参考:[原创]appproxy 一个轻量级的VPN代理工具,支持HTTP, SOCKS5协议-Android安全-看雪-安全社区|安全招聘|kanxue.com

SSL Pinning绕过

我们都知道http协议传输的是明文信息,是可以直接捕获的,从而造成了数据泄露。为了防止中间人的拦截,出现了HTTPS加密机制。在HTTPS中,使用了证书+数字签名解决了抓包的问题,这里用到了两个概念:数字签名和数字证书。

  • 数字签名:是发送方的明文经历了两次加密得到的两个东西组成,一个是hash ,一个是经过私钥加密。
  • 数字证书:其实就是明文+数字签名。但是数字证书中的内容远不止这俩,还包括了权威机构的信息,服务器的域名,最重要的是有签名的计算方法,还有就是证书中还包括公钥,公钥用于发放给请求证书的客户端。

SSL(安全套接字层)又被称为TLS(数据层安全协议),是一种为网络通信提供数据完整性的一种安全协议。它位于TCP/IP协议与各种应用协议之间。SSL协议主要分为两个部分:Handshake Protocol和Record Protocol。

Handshake protocol是用来协商加密通信数据的密钥,Record Protocol定义传输内容的格式。事实上,HTTPS就是使用SSL/TLS协议进行加密传输,让客户端拿到服务器的公钥,然后客户端随机生成一个对称加密的秘钥,使用公钥加密,传输给服务端,后续的所有信息都通过该对称秘钥进行加密解密,完成整个HTTPS的流程。

证书锁定本质是对抗中间人攻击,并非用于对抗抓包破解。但如果程序逻辑未被注入运行在"可信环境"中倒是有些作用。SSL证书锁定之前,我们需要理解一些基本的概念:

  • 可信CA: CA(Certificate Authority)是数字证书认证中心的简称,是指发放、管理、废除数字证书的机构。CA的作用是检查证书持有者身份的合法性,并签发证书,以防证书被伪造或篡改,以及对证书和密钥进行管理。
  • 双向锁定:在客户端锁定服务端证书的基础上,服务端对客户端的证书也进行锁定,需要客户端再做一次证书预埋。多见于金融业务场景。
  • 证书链:证书链就是Root CA签发二级Intermediate CA,二级Intermediate CA可以签发三级Intermediate CA,也可以直接签发用户证书。从Root CA到用户证书之间构成了一个信任链:信任Root CA,就应该信任它所信任的二级Intermediate CA,从而就应该信任三级Intermediate CA直至信任用户证书。
  • 逐级验证:客户端对于收到的多级证书,需要从站点证书(leaf certificate)开始逐级验证,直至出现操作系统或浏览器内置的受信任CA 根证书(root certificate)。

image.png

SSL Pinning技术指的是在应用程序中只信任固定证书或是公钥。应用程序开发人员在程序中使用SSL pinning技术作为应用流量的附加安全层。实现SSL pinning的方法主要有两种:证书固定和公钥固定。

  • 证书固定:开发者将SSL证书的某些字节码硬编码在应用程序中。当应用程序与服务器通信时,它将检查证书中是否存在相同的字节码。如果存在,则应用程序将请求发送到服务器;如果字节码不匹配,它将抛出SSL证书错误。此技术可防止攻击者使用自己的自签名证书。
  • 公钥固定:在客户访问网站时进行公钥固定中,服务器将其公钥固定(通过注入)在客户端(客户)浏览器中。当客户端重新访问同一网站时,服务器将标识其公共密钥以检查连接的完整性。此技术还可以防止攻击者使用自签名证书。

通常,我们在测试一个应用的时候会设置代理,从而获取应用在运行过程中的流量。我们使用代理工具获取流量的话,需要在设备中安装我们自己的证书到可信任的根证书中,这样应用程序才会将我们的证书视为可信任的有效证书,允许代理工具拦截应用的流量。但是使用的SSL pinning技术的应用程序,只信任指定的证书,那么我们就算把我们的证书安装到设备中,应用程序也不会信任我们的证书,这样的话我们就不能通过代理的方式来拦截应用的流量。

SSL pinning的实现方式

SSL Pinning 一般实现方式有两种,一种是在代码层进行校验,一种是通过配置文件network_security_config.xml进行配置实现校验。

代码层校验

下面是以 okhttp 网络框架举例,在代码校验证书公钥的代码如下:

1660023498_62f1f2caa5c5196abe5bb.png!small?1660023498423

配置文件实现

首先在AndroidManifest.xml的application配置项中加上android:networkSecurityConfig="@xml/network_security_config"

通过res/xml/network_security_config.xml进行配置,实现SSL Pinning,配置文件可以分为两种,使用证书校验和公钥校验。

示例xml文件:

XML
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
      <certificates src="user" />
    </trust-anchors>
  </base-config>
</network-security-config>

使用证书锁定,由于CA签发证书存在有效期的问题,所以在证书续期后,需要将证书重新内置到App中。

使用公钥锁定,需要提取证书中的公钥并内置到app中,公钥在证书续期后可以保持不变(即密钥对可以不变),可以避免有效期问题。

Note

当该文件不存在即未显示配置时,默认采用系统配置,即信任系统证书(Android7以上)

SSL pinning方案对比

根据安全级别的不同,证书锁定方案大体可以分为如下几种:

安全等级 策略 信任范围 绕过方法
0 完全兼容策略 信任所有证书包括自签发证书 无需特殊操作
1 系统/浏览器默认策略 信任系统或浏览器内置CA证书、用户安装的证书 设备安装代理证书(用户)
2 System CA pinning 只信任系统根证书,不信任用户安装的证书(android7支持配置network-security-config) 注入或者root后将用户证书拷贝到系统证书目录
3 CA pinningRoot(intermediate)certificate pinning 信任指定CA办法的证书 Hook注入等方式篡改锁定逻辑
4 Leaf Certificate pinning 信任指定站点证书 Hook注入等方式篡改锁定逻辑,如遇双向锁定需将app自带证书导入代理软件

Note

一般而言,大部分app的安全等级都设置为2,即信任系统级根证书,解决方法已在前面的章节有过详细描述

参考文章

深入浅出SSL Pinning技术解析 - OSCHINA - 中文开源技术交流社区

绕过示例

Demoapp-http

下载地址:https://github.com/aszx87410/demo/raw/master/android/demoapp-http.apk

这是一个简单的用于发送请求的app,点击GET后会发送一个请求,首先我们尝试配置WIFI代理来抓包

image-20251215142430538

会发现抓不到包,我们逆向app看看是否有什么防护

Note

另外如果使用VPN代理软件,是可以抓到包的

Bash
apktool d demoapp-http.apk -f -o ./demoapp-http

我们尝试修改配置文件的方式绕过

image-20251215143949804

在主配置文件中搜索android:networkSecurityConfig,如果没有,我们可以自定加上,如果有,那么我们就记住路径

我们在application处增加android:networkSecurityConfig="@xml/network_security_config"配置,然后去对应的路径(res/xml/network_security_config)增加如下配置

XML
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
      <certificates src="user" />
    </trust-anchors>
  </base-config>
</network-security-config>

这个配置表示信任系统和用户证书,即该配置表示信任所有的证书

image-20251215144517257

随后我们重打包app,再次安装

image-20251215145110205

此时就能抓到包了

Demoapp-pinning(客户端校验服务端证书)

下载地址:https://github.com/aszx87410/demo/raw/master/android/demoapp-pinning.apk

image-20251215151736647

image-20251215151723215

抓不到包,我们首先考虑改配置文件

image-20251215153833057

仍然抓不到包,此时我们就需要考虑是否是代码中存在SSL pinning机制

image-20251215154053637

反编译后发现使用了okhttp,那么有可能该app使用的就是okhttp的SSL pinning,示例代码如下:

Java
String hostname = "publicobject.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();

我们可以通过关键字"CertificatePinner"或者"sha256/"搜索一下

image-20251215162821345

其逻辑代码如下:

Text Only
.method public run()V
    .registers 14

    .line 1
    new-instance v0, Ljava/util/ArrayList;

    invoke-direct {v0}, Ljava/util/ArrayList;-><init>()V

    const-string v1, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

    .line 2
    filled-new-array {v1}, [Ljava/lang/String;

    move-result-object v1

    const-string v2, "archive.org"

    const-string v3, "pattern"

    .line 3
    invoke-static {v2, v3}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    const-string v3, "pins"

    invoke-static {v1, v3}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    const/4 v3, 0x0

    const/4 v4, 0x0

    :goto_19
    const/4 v5, 0x1

    if-ge v4, v5, :cond_29

    aget-object v5, v1, v4

    new-instance v6, Ld/g$b;

    invoke-direct {v6, v2, v5}, Ld/g$b;-><init>(Ljava/lang/String;Ljava/lang/String;)V

    invoke-virtual {v0, v6}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z

    add-int/lit8 v4, v4, 0x1

    goto :goto_19

    .line 4
    :cond_29
    new-instance v1, Ld/g;

    const-string v2, "$this$toSet"

    .line 5
    invoke-static {v0, v2}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    invoke-virtual {v0}, Ljava/util/ArrayList;->size()I

    move-result v2

    const/4 v4, 0x3

    if-eqz v2, :cond_6c

    if-eq v2, v5, :cond_5e

    new-instance v2, Ljava/util/LinkedHashSet;

    invoke-virtual {v0}, Ljava/util/ArrayList;->size()I

    move-result v6

    if-gez v6, :cond_42

    goto :goto_57

    :cond_42
    if-ge v6, v4, :cond_47

    add-int/lit8 v6, v6, 0x1

    goto :goto_57

    :cond_47
    const/high16 v7, 0x40000000    # 2.0f

    if-ge v6, v7, :cond_54

    int-to-float v6, v6

    const/high16 v7, 0x3f400000    # 0.75f

    div-float/2addr v6, v7

    const/high16 v7, 0x3f800000    # 1.0f

    add-float/2addr v6, v7

    float-to-int v6, v6

    goto :goto_57

    :cond_54
    const v6, 0x7fffffff

    :goto_57
    invoke-direct {v2, v6}, Ljava/util/LinkedHashSet;-><init>(I)V

    invoke-static {v0, v2}, Lc/g/e;->g(Ljava/lang/Iterable;Ljava/util/Collection;)Ljava/util/Collection;

    goto :goto_6e

    :cond_5e
    invoke-virtual {v0, v3}, Ljava/util/ArrayList;->get(I)Ljava/lang/Object;

    move-result-object v0

    .line 6
    invoke-static {v0}, Ljava/util/Collections;->singleton(Ljava/lang/Object;)Ljava/util/Set;

    move-result-object v2

    const-string v0, "java.util.Collections.singleton(element)"

    invoke-static {v2, v0}, Lc/j/b/d;->c(Ljava/lang/Object;Ljava/lang/String;)V

    goto :goto_6e

    .line 7
    :cond_6c
    sget-object v2, Lc/g/j;->b:Lc/g/j;

    :goto_6e
    const/4 v0, 0x2

    const/4 v6, 0x0

    .line 8
    invoke-direct {v1, v2, v6, v0}, Ld/g;-><init>(Ljava/util/Set;Ld/k0/m/c;I)V

    .line 9
    new-instance v0, Ld/a0$a;

    invoke-direct {v0}, Ld/a0$a;-><init>()V

    const-string v2, "certificatePinner"

    .line 10
    invoke-static {v1, v2}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    iget-object v2, v0, Ld/a0$a;->q:Ld/g;

    invoke-static {v1, v2}, Lc/j/b/d;->a(Ljava/lang/Object;Ljava/lang/Object;)Z

    move-result v2

    xor-int/2addr v2, v5

    if-eqz v2, :cond_88

    iput-object v6, v0, Ld/a0$a;->v:Ld/k0/g/k;

    :cond_88
    iput-object v1, v0, Ld/a0$a;->q:Ld/g;

    .line 11
    new-instance v1, Ld/a0;

    invoke-direct {v1, v0}, Ld/a0;-><init>(Ld/a0$a;)V

    const/4 v11, 0x0

    .line 12
    new-instance v0, Ljava/util/LinkedHashMap;

    invoke-direct {v0}, Ljava/util/LinkedHashMap;-><init>()V

    .line 13
    new-instance v2, Ljava/util/ArrayList;

    const/16 v7, 0x14

    invoke-direct {v2, v7}, Ljava/util/ArrayList;-><init>(I)V

    const-string v7, "https://archive.org/wayback/available?url=www.example.com"

    const-string v8, "url"

    .line 14
    invoke-static {v7, v8}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    const-string v9, "ws:"

    invoke-static {v7, v9, v5}, Lc/m/e;->w(Ljava/lang/String;Ljava/lang/String;Z)Z

    move-result v9

    if-eqz v9, :cond_b2

    const-string v5, "http:"

    invoke-static {v5}, Lb/a/a/a/a;->h(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v5

    goto :goto_c1

    :cond_b2
    const-string v4, "wss:"

    invoke-static {v7, v4, v5}, Lc/m/e;->w(Ljava/lang/String;Ljava/lang/String;Z)Z

    move-result v4

    if-eqz v4, :cond_d1

    const-string v4, "https:"

    invoke-static {v4}, Lb/a/a/a/a;->h(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v5

    const/4 v4, 0x4

    :goto_c1
    invoke-virtual {v7, v4}, Ljava/lang/String;->substring(I)Ljava/lang/String;

    move-result-object v4

    const-string v7, "(this as java.lang.String).substring(startIndex)"

    invoke-static {v4, v7}, Lc/j/b/d;->c(Ljava/lang/Object;Ljava/lang/String;)V

    invoke-virtual {v5, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v7

    :cond_d1
    const-string v4, "$this$toHttpUrl"

    .line 15
    invoke-static {v7, v4}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    new-instance v4, Ld/x$a;

    invoke-direct {v4}, Ld/x$a;-><init>()V

    invoke-virtual {v4, v6, v7}, Ld/x$a;->d(Ld/x;Ljava/lang/String;)Ld/x$a;

    invoke-virtual {v4}, Ld/x$a;->a()Ld/x;

    move-result-object v4

    .line 16
    invoke-static {v4, v8}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    .line 17
    new-instance v10, Ld/w;

    new-array v5, v3, [Ljava/lang/String;

    invoke-interface {v2, v5}, Ljava/util/Collection;->toArray([Ljava/lang/Object;)[Ljava/lang/Object;

    move-result-object v2

    if-eqz v2, :cond_155

    check-cast v2, [Ljava/lang/String;

    invoke-direct {v10, v2, v6}, Ld/w;-><init>([Ljava/lang/String;Lc/j/b/c;)V

    .line 18
    invoke-static {v0}, Ld/k0/c;->D(Ljava/util/Map;)Ljava/util/Map;

    move-result-object v12

    new-instance v0, Ld/c0;

    const-string v9, "GET"

    move-object v7, v0

    move-object v8, v4

    invoke-direct/range {v7 .. v12}, Ld/c0;-><init>(Ld/x;Ljava/lang/String;Ld/w;Ld/e0;Ljava/util/Map;)V

    :try_start_101
    const-string v2, "request"

    .line 19
    invoke-static {v0, v2}, Lc/j/b/d;->d(Ljava/lang/Object;Ljava/lang/String;)V

    new-instance v2, Ld/k0/g/e;

    invoke-direct {v2, v1, v0, v3}, Ld/k0/g/e;-><init>(Ld/a0;Ld/c0;Z)V

    .line 20
    invoke-virtual {v2}, Ld/k0/g/e;->d()Ld/f0;

    move-result-object v0
    :try_end_10f
    .catch Ljava/io/IOException; {:try_start_101 .. :try_end_10f} :catch_150

    .line 21
    :try_start_10f
    iget-object v1, v0, Ld/f0;->h:Ld/h0;

    .line 22
    invoke-virtual {v1}, Ld/h0;->s()Le/g;

    move-result-object v2
    :try_end_115
    .catchall {:try_start_10f .. :try_end_115} :catchall_144

    :try_start_115
    invoke-virtual {v1}, Ld/h0;->g()Ljava/nio/charset/Charset;

    move-result-object v1

    invoke-static {v2, v1}, Ld/k0/c;->w(Le/g;Ljava/nio/charset/Charset;)Ljava/nio/charset/Charset;

    move-result-object v1

    invoke-interface {v2, v1}, Le/g;->z(Ljava/nio/charset/Charset;)Ljava/lang/String;

    move-result-object v1
    :try_end_121
    .catchall {:try_start_115 .. :try_end_121} :catchall_13d

    :try_start_121
    invoke-static {v2, v6}, La/b/k/r;->w(Ljava/io/Closeable;Ljava/lang/Throwable;)V

    const-string v2, "okhttp"

    .line 23
    invoke-static {v2, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    iget-object v2, p0, Lcom/cymetrics/demo/FirstFragment$a$a;->c:Lcom/cymetrics/demo/FirstFragment$a;

    iget-object v2, v2, Lcom/cymetrics/demo/FirstFragment$a;->b:Lcom/cymetrics/demo/FirstFragment;

    invoke-virtual {v2}, Landroidx/fragment/app/Fragment;->i()La/k/d/e;

    move-result-object v2

    new-instance v3, Lcom/cymetrics/demo/FirstFragment$a$a$a;

    invoke-direct {v3, p0, v1}, Lcom/cymetrics/demo/FirstFragment$a$a$a;-><init>(Lcom/cymetrics/demo/FirstFragment$a$a;Ljava/lang/String;)V

    invoke-virtual {v2, v3}, Landroid/app/Activity;->runOnUiThread(Ljava/lang/Runnable;)V
    :try_end_139
    .catchall {:try_start_121 .. :try_end_139} :catchall_144

    :try_start_139
    invoke-virtual {v0}, Ld/f0;->close()V
    :try_end_13c
    .catch Ljava/io/IOException; {:try_start_139 .. :try_end_13c} :catch_150

    goto :goto_154

    :catchall_13d
    move-exception v1

    .line 24
    :try_start_13e
    throw v1
    :try_end_13f
    .catchall {:try_start_13e .. :try_end_13f} :catchall_13f

    :catchall_13f
    move-exception v3

    :try_start_140
    invoke-static {v2, v1}, La/b/k/r;->w(Ljava/io/Closeable;Ljava/lang/Throwable;)V

    throw v3
    :try_end_144
    .catchall {:try_start_140 .. :try_end_144} :catchall_144

    :catchall_144
    move-exception v1

    .line 25
    :try_start_145
    throw v1
    :try_end_146
    .catchall {:try_start_145 .. :try_end_146} :catchall_146

    :catchall_146
    move-exception v2

    :try_start_147
    invoke-virtual {v0}, Ld/f0;->close()V
    :try_end_14a
    .catchall {:try_start_147 .. :try_end_14a} :catchall_14b

    goto :goto_14f

    :catchall_14b
    move-exception v0

    :try_start_14c
    invoke-virtual {v1, v0}, Ljava/lang/Throwable;->addSuppressed(Ljava/lang/Throwable;)V

    :goto_14f
    throw v2
    :try_end_150
    .catch Ljava/io/IOException; {:try_start_14c .. :try_end_150} :catch_150

    :catch_150
    move-exception v0

    invoke-virtual {v0}, Ljava/io/IOException;->printStackTrace()V

    :goto_154
    return-void

    .line 26
    :cond_155
    new-instance v0, Ljava/lang/NullPointerException;

    const-string v1, "null cannot be cast to non-null type kotlin.Array<T>"

    invoke-direct {v0, v1}, Ljava/lang/NullPointerException;-><init>(Ljava/lang/String;)V

    goto :goto_15e

    :goto_15d
    throw v0

    :goto_15e
    goto :goto_15d
.end method

在代码中,通过以下步骤实现证书固定:

(1)定义 Pin 规则

  • 这是一个 SHA-256 哈希值,代表目标域名(archive.org)的**证书公钥**(SPKI)的 Base64 编码。
  • 格式:sha256/<Base64(SHA256(PublicKey))>

(2)绑定域名与 Pin

  • 将域名 archive.org 与该 Pin 绑定。

(3)构建 CertificatePinner

  • 生成 CertificatePinner 对象。

(4)注入到 OkHttpClient

  • 最终构建的 OkHttpClient 在发起 HTTPS 请求时,会**校验服务器证书的公钥是否匹配预设 Pin**。
  • 不匹配 → 抛出 SSLPeerUnverifiedException,连接失败。

基于以上流程,我们有以下绕过思路:

  1. 修改域名archive.org为其他任意一个App不会使用到的域名,这样绑定就会失效
  2. 将pin置空,这样就不会校验证书了

首先我们尝试方案一,将archive.org改为xxxxxx.com,重新打包app

image-20251215173717262

能抓到包了

再尝试方案二,将pin置空

抖音

以抖音最新版为例

image-20250418173757817

可以看到打开抖音后提示网络错误,同时没有抓到任何流量包

image-20250418173908824

但我们访问浏览器,可以看到页面正常,并且抓包也正常,说明并不是网络有问题,而是抖音做了一些安全防护

我们即使使用VPN代理的方式抓包,同样无法抓取到抖音的流量包

image-20250418174302007

猜测抖音SSL pinning安全策略等级至少为三,此时我们必须找到抖音验证签名的逻辑并修改它,使其跳过验证或者信任抓包工具的证书

查壳

image-20250507220328218

使用MT管理器可以看到抖音未加壳

查混淆方式

image-20250507220639912

使用Die查抖音是否存在混淆,工具没有检查出来存在混淆

反编译

使用jadx-gui反编译抖音的apk文件,在AndroidManifest.xml文件中搜索networkSecurityConfig

image-20250507221423414

出现这种可以确定是存在混淆的,我们跳转到对应的路径查看该文件

XML
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates
                overridePins="true"
                src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

可以看到抖音配置的是默认信任系统证书,但我们在将工具证书导入系统后仍然抓不到包,这应该是抖音在代码层面做了证书固定,于是我们需要找到抖音证书验证相关的逻辑并修改

从网上收集信息发现抖音使用的是so层的sslpinning技术,相关的so文件为libsscronet.so,将抖音apk重命名为zip,解压得到lib/arm64-v8a/libsscronet.so文件,这里有两种方案绕过

直接删除libsscronet.so

抖音的包名为com.ss.android.ugc.aweme

其他参考:绕过ssl pinning | 网安人,网安魂! SECURITY GO! GO!! GO!!!

双向认证绕过

我们通过上文知道了SSL Pinning是指客户端校验服务端证书,那么在这个基础上我们进一步加强安全性,也就是服务端也要校验客户端的证书

这里以soul V3.100.1为例

image-20251220171932757

当我们绕过了SSL Pinning后会发现服务端返回证书相关错误,这表明服务端启用了双向认证,这也表示证书存放在客户端,所以我们需要提取出客户端证书,并把证书导入到抓包工具中,这样就能继续抓包了

证书格式包括.p12/.fpx/.pem等,但移动端使用的最常见的是p12证书

Bash
apktool d com.soul.android.international.apk -f -o ./soul-apktool

我们使用apktool反编译后使用find搜索证书后缀

Bash
find soul-apktool -type f -iname "*.crt" -o -iname "*.cer" -o -iname "*.pem" -o -iname "*.der" -o -iname "*.p12" -o -iname "*.pfx"

image-20251220172754198

我们发现了client.p12,现在我们还需要找到证书的密钥,我们可以首先尝试弱密码/空密码爆破一下

Bash
openssl pkcs12 -in client.p12 -nokeys -passin pass:""  # 测试空密码
john client.p12  # 弱密码爆破

无果,可能是强密码,接下来我们去代码中找相关的逻辑代码,搜索关键词“client.p12”,"PKCS12",或者搜索java.security.KeyStore.open()方法

image-20251220202340283

这个类应该就和证书校验有关,我们看一下这个类的逻辑

Java
public TLSSocketFactory(Context context, String str) throws NoSuchAlgorithmException, UnrecoverableKeyException, IOException, CertificateException, KeyStoreException, KeyManagementException, IllegalArgumentException {
        CertificateFactory certificateFactory;
        BufferedInputStream bufferedInputStream;
        AppMethodBeat.o(80750);
        cn.soulapp.android.net.sip.k.a("TLSSocketFactory init");
        String strL = SoulPowerful.l(str);
        try {
            certificateFactory = CertificateFactory.getInstance("X.509");
            bufferedInputStream = new BufferedInputStream(context.getResources().getAssets().open(KEY_CRT_CLIENT_PATH));
        } catch (Exception unused) {
        }
        try {
            try {
                Certificate certificateGenerateCertificate = certificateFactory.generateCertificate(bufferedInputStream);
                try {
                    bufferedInputStream.close();
                } catch (Exception unused2) {
                }
                KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
                InputStream inputStreamOpen = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
                try {
                    keyStore.load(inputStreamOpen, strL.toCharArray());
                } catch (Exception unused3) {
                } catch (Throwable th) {
                    try {
                        inputStreamOpen.close();
                    } catch (Exception unused4) {
                    }
                    AppMethodBeat.r(80750);
                    throw th;
                }
                try {
                    inputStreamOpen.close();
                } catch (Exception unused5) {
                }
                keyStore.setCertificateEntry("ca", certificateGenerateCertificate);
                KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
                keyManagerFactory.init(keyStore, strL.toCharArray());
                X509TrustManager[] x509TrustManagerArr = null;
                if (isUseDefault) {
                    this.trustManagers = null;
                } else {
                    cn.soulapp.android.net.sip.k.a("TLSSocketFactory X509TrustManager init");
                    this.trustManagers = new X509TrustManager[]{new a(this, certificateGenerateCertificate)};
                }
                SSLContext sSLContext = SSLContext.getInstance("TLS");
                KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
                if (!isUseDefault) {
                    x509TrustManagerArr = this.trustManagers;
                }
                sSLContext.init(keyManagers, x509TrustManagerArr, new SecureRandom());
                sSLContext.getClientSessionContext().setSessionTimeout(15000);
                sSLContext.getServerSessionContext().setSessionTimeout(15000);
                this.delegate = sSLContext.getSocketFactory();
                AppMethodBeat.r(80750);
            } catch (Exception e2) {
                AppMethodBeat.r(80750);
                throw e2;
            }
        } catch (Throwable th2) {
            try {
                bufferedInputStream.close();
            } catch (Exception unused6) {
            }
            AppMethodBeat.r(80750);
            throw th2;
        }
    }

通过分析我们知道,该程序首先读取了证书流数据(bufferedInputStream = new BufferedInputStream(context.getResources().getAssets().open(KEY_CRT_CLIENT_PATH));),随后调用load方法将证书和密钥作为参数传入(keyStore.load(inputStreamOpen, strL.toCharArray());),其中strL就是我们想要的字符串形式的证书密钥,而这个strL又是通过SoulPowerful.l(str);返回值获取的,所以l函数的返回值就是我们要找的密钥

image-20251220203227164

我们直接hook这个函数,直接打印返回值即可

Java
function run() {
  Java.perform(() => {
    var SoulPowerful = Java.use('cn.soulapp.android.soulpower.SoulPowerful')
    SoulPowerful.l.overload('java.lang.String').implementation = function(s) {
      console.log('SoulPowerful.l called');
      console.log('Argument: ' + s);
      var result = this.l.call(this, s);
      console.log('Return value: ' + result);
      return result;
    }
  })
}

setImmediate(run)

image-20251220204050651

所以密钥就是}%2R+\OSsjpP!w%X

Bash
openssl pkcs12 -in client.p12 -info -passin 'pass:}%2R+\OSsjpP!w%X'

image-20251220210106243

这个报错表明文件使用了 弱加密算法 RC2-40-CBC,而 新版 OpenSSL(3.0+)默认禁用了不安全的弱加密算法(如 40 位密钥的 RC2),导致无法解密,解决方法如下:

创建openssl-legacy.conf文件,写入以下内容:

Text Only
# openssl-legacy.conf
openssl_conf = openssl_init

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
legacy = legacy_sect

[default_sect]
activate = 1

[legacy_sect]
activate = 1

随后重新解密

Bash
OPENSSL_CONF=./openssl-legacy.conf openssl pkcs12 -in client.p12 -info -passin 'pass:}%2R+\OSsjpP!w%X'

image-20251220210432690

可以看到解密成功,说明我们找到的密码是正确的,接下来只需导入抓包工具即可,这里以burp为例,进入proxy-import/export CA certificate

image-20251220210708122

选择导入PKCS#12格式证书,随后导入client.p12输入密码即可

image-20251220211150949

和SSL Pinning的对比

特性 客户端校验服务端(SSL Pinning) 服务端校验客户端(mTLS)
方向 客户端 → 服务端 服务端 ← 客户端(双向)
目的 防止 MITM,确保连接到正确服务器 验证客户端身份,防止未授权访问
证书位置 Pin 值硬编码在客户端 客户端持有证书 + 私钥
常见场景 移动 App(银行、支付) 企业 API、IoT、微服务通信
是否需要 CA 签发 否(可自定义 Pin) 是(客户端证书需被服务端信任的 CA 签发)
能否绕过 可(Hook、Patch) 较难(需私钥 + 证书)
Android 实现 CertificatePinner (OkHttp) 自定义 KeyManager + SSLContext

APP代理和VPN检测绕过对抗

OSI 7层网络模型

下面是对GPT对OSI七层网络模型的简单解释:

  • 应用层:为用户提供网络服务,如电子邮件、文件传输和Web浏览器。
  • 表示层:处理数据的格式转换、加密和压缩,以确保数据能够正确解释。
  • 会话层:建立和管理会话,允许不同设备之间的通信和同步。
  • 传输层:提供端到端的数据传输,确保数据可靠和完整。
  • 网络层:处理数据包的路由和寻址,确定数据的最佳路径。
  • 数据链路层:在直接连接的设备之间传输数据帧,并处理错误检测和纠正。
  • 物理层:处理物理连接和传输介质,如电缆、网卡等。 这些层次按照从底层到顶层的顺序进行,每个层次都负责不同的功能。

image-20250916222110410

知识点:HTTPS协议是HTTP+SSL

根据上表可知,SSL做数据加密是在表示层,也就是说,HTTPS实际上是建立在SSL之上的HTTP协议,而普通的HTTP协议是建立在TCP协议之上的。所以,当HTTPS访问URL时,由于URL在网络传送过程中最后是处于HTTP协议数据报头中,而HTTP协议位于SSL的上层,所以凡是HTTP协议所负责传输的数据就全部被加密了;但是IP地址并没加密,因为处理IP地址的协议(网络层)位于处理SSL协议(表示层)的下方。

网络传输中数据的封装与解封装

层号 名称 功能描述 数据单元
7 应用层 用户接口、应用程序(HTTP、FTP、SMTP) 数据(Data)
6 表示层 数据格式转换、加密、压缩 数据(Data)
5 会话层 建立、管理和终止会话 数据(Data)
4 传输层 端到端连接、可靠/不可靠传输(TCP/UDP) 段(Segment)或数据报(Datagram)
3 网络层 逻辑寻址、路由(IP) 包(Packet)
2 数据链路层 物理寻址、帧同步、差错控制(MAC) 帧(Frame)
1 物理层 传输比特流(电信号、光信号) 比特(Bit)

数据封装过程(发送端)

假设:你用浏览器访问 http://example.com,数据从你的电脑发往服务器。

  1. 应用层 → 表示层 → 会话层(L7~L5)

  2. 你的浏览器生成 HTTP 请求:GET /index.html HTTP/1.1

  3. 数据格式化、可能加密(如 HTTPS)、建立会话
  4. 🧩 输出:原始数据(Data)

🖥️ 示例: "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"


  1. 传输层(L4)→ 添加“端口号”,选择协议(TCP/UDP)

  2. 为数据加上**源端口**(如 50000)和**目标端口**(如 80)

  3. 如果是 TCP,还会建立连接(三次握手)、分段、编号、校验
  4. 🧩 输出:段(TCP Segment)或数据报(UDP Datagram)

📄 TCP 头部示例字段:

  • 源端口:50000
  • 目标端口:80
  • 序列号、确认号、窗口大小、校验和等

  1. 网络层(L3)→ 添加“IP 地址”,路由选择

  2. 添加**源 IP**(如 192.168.1.100)和**目标 IP**(如 93.184.216.34)

  3. 决定下一跳路由(通过路由表)
  4. 🧩 输出:包(Packet)

📄 IP 头部示例字段:

  • 源 IP:192.168.1.100
  • 目标 IP:93.184.216.34
  • TTL、协议类型(TCP=6)、头部校验和

  1. 数据链路层(L2)→ 添加“MAC 地址”,成帧

  2. 查询本地 ARP 表,获取下一跳(如网关)的 MAC 地址

  3. 添加**源 MAC**(你的网卡)和**目标 MAC**(网关或目标主机)
  4. 添加帧头帧尾(含 CRC 校验)
  5. 🧩 输出:帧(Frame)

📄 以太网帧头部示例:

  • 目标 MAC:AA:BB:CC:DD:EE:FF
  • 源 MAC:11:22:33:44:55:66
  • 类型字段:0x0800(表示 IP)

  1. 物理层(L1)→ 转换为“比特流”发送

  2. 把帧转换为**电信号、光信号或无线电波**(0 和 1)

  3. 通过网线、光纤、Wi-Fi 等物理介质传输
  4. 🧩 输出:比特流(Bits)

📡 实际传输:10101100011100101010...

Text Only
[应用数据]
     ↓
┌─────────────┐ ← 应用层/表示层/会话层 → Data
│  TCP/UDP头  │ ← 传输层               → Segment/Datagram
├─────────────┤
│    IP头     │ ← 网络层               → Packet
├─────────────┤
│ 以太网帧头+尾│ ← 数据链路层           → Frame
├─────────────┤
│  01010101   │ ← 物理层               → Bits(在网线/Wi-Fi中传输)
└─────────────┘

数据解封装过程(接收端)

当数据到达目标主机(如 Web 服务器),它从物理层开始,逐层“拆包”。

  1. 物理层(L1)→ 接收比特流

  2. 网卡接收电信号,转换为二进制比特流

  3. 传递给数据链路层

  1. 数据链路层(L2)→ 检查 MAC,去帧头帧尾

  2. 检查目标 MAC 是否是自己 → 是则接收,否则丢弃

  3. 去掉帧头帧尾,CRC 校验数据完整性
  4. 根据“类型字段”决定上交网络层(如 0x0800 → IP)
  5. 🧩 输出:IP 包(Packet)

  1. 网络层(L3)→ 检查 IP,路由或上交

  2. 检查目标 IP 是否是自己 → 是则上交传输层,否则路由转发

  3. TTL 减 1,校验 IP 头部
  4. 🧩 输出:TCP 段(Segment)

  1. 传输层(L4)→ 检查端口,重组数据

  2. 根据端口号(如 80)交给对应应用(如 Web 服务)

  3. TCP 还要确认、排序、重传、流量控制
  4. 🧩 输出:应用数据(Data)

  1. 会话层 → 表示层 → 应用层(L5~L7)

  2. 重建会话、解密、解压缩、格式转换

  3. 最终把原始 HTTP 请求交给 Web 服务器程序处理
  4. 🧩 输出:“GET /index.html HTTP/1.1...”
Text Only
[01010101...] ← 物理层
     ↓
┌─────────────┐
│ 以太网帧头+尾│ ← 数据链路层 → 去掉 → 交给网络层
├─────────────┤
│    IP头     │ ← 网络层     → 去掉 → 交给传输层
├─────────────┤
│  TCP/UDP头  │ ← 传输层     → 去掉 → 交给应用层
├─────────────┤
[应用数据]     ← 应用层/表示层/会话层 → 交给程序处理

流程总结(收发过程)

Text Only
发送端(你的电脑)                             接收端(服务器)
┌─────────────┐                             ┌─────────────┐
│  应用数据   │                             │  应用数据   │ ← L7~L5
├─────────────┤                             ├─────────────┤
│ TCP/UDP 头部│ ← L4 封装                   │ TCP/UDP 头部│ ← L4 解封装
├─────────────┤                             ├─────────────┤
│   IP 头部   │ ← L3 封装                   │   IP 头部   │ ← L3 解封装
├─────────────┤                             ├─────────────┤
│  帧头+帧尾  │ ← L2 封装                   │  帧头+帧尾  │ ← L2 解封装
├─────────────┤                             ├─────────────┤
│  比特流     │ ← L1 发送 → 网络传输 → L1 接收 → 比特流     │
└─────────────┘                             └─────────────┘

代理与VPN

代理

代理(proxy) 也称网络代理,是一种特殊的网络服务,允许一个终端(一般为客户端)通过这个服务与另外一个终端(一般为服务器)进行非直接的连接。

一个**完整的代理请求过程为**:客户端首先根据代理服务器所使用的代理协议,与代理服务器创建连接,接着按照协议请求对目标服务器创建连接、或者获得目标服务器的指定资源。

VPN

VPN(virtual private network)(虚拟专用网络)是常用于连接中、大型企业或团体间私人网络的通讯方法。它利用隧道协议(Tunneling Protocol)来达到发送端认证、消息保密与准确性等功能。

代理和VPN的区别

从各自的定义,我们就能看出VPN的特点是采取隧道协议进行数据传输和保护;而代理使用的则是对应的代理协议。

下面是VPN和代理的常用协议:

名称 使用的协议名称
VPN OpvenVPN、IPsec、IKEv2、PPTP、L2TP、WireGuard等
代理 HTTP、HTTPS、SOCKS、FTP、RTSP等

VPN 协议大多是作用在 OSI 的**第二层和第三层之间**,所以使用 VPN 时,几乎能转发所有的流量。

而代理协议多作用在**应用层,最高层**。

Note

我们从上述知识中知道发送端的数据封装过程是从上到下的,而通常APP的检测都是真对某一层进行检测,那么我们就可以针对下一层进行绕过

参考:APP代理和VPN检测绕过对抗 | 网安人,网安魂! SECURITY GO! GO!! GO!!!

yakit MITM APP

Anroid

用户证书&系统证书

系统 CA 证书
  • 内置证书:系统 CA 证书是 Android 系统内置的,由设备制造商或操作系统开发者预装。这些证书通常来源于全球公认的证书颁发机构(CA)。
  • 存储位置:如你所述,系统 CA 证书存放在 /etc/security/cacerts/ 目录下,文件名基于证书的 subjectDN 的 MD5 值。
  • 安全性:由于系统 CA 证书是预装的,它们通常被视为更加安全和可信。应用程序和系统服务依赖这些证书来建立安全的 HTTPS 连接。
  • 管理权限:修改系统 CA 证书需要 Root 权限,这是因为改动系统证书可能会影响到设备的整体安全性和稳定性。
用户 CA 证书
  • 用户安装:用户 CA 证书由设备的用户手动安装。这对于某些特定的使用场景非常有用,比如开发和测试,或者连接需要特定 CA 证书的企业网络。
  • 存储和管理:用户 CA 证书可以通过设备的设置菜单安装,通常在 设置 → 系统安全 → 加密与凭据 → 从 SD 卡安装 下。安装后,这些证书会显示在 设置 → 系统安全 → 加密与凭据 → 信任的凭据 下的“用户”部分。

但是从 Android 7.0 (Nougat) 开始,Google 引入了新的安全增强功能,系统不再信任用户安装的 CA 证书对于所有应用的 HTTPS 流量。为了让 Yakit 能够解密和捕获 HTTPS 流量,我们需要绕过限制,让 APP “信任” Yakit 的 CA 证书。

Anroid 安装证书

参考:利用yakit进行小程序和app抓包 | CN-SEC 中文网

增加防火墙入站规则

Important

想要让Anroid的Wifi走电脑的代理,必须要配置防火墙,否则根本抓取不到流量

将端口改为yakit监听端口即可

所有配置好后即可抓取到APP流量

yakit 微信小程序抓包

参考:https://www.yaklang.com/wiki/WeChatAppEx/

Tip

Proxifier可以在网上找一些能用的激活码

与微信小程序有关的可执行文件为wechatappex.exe,可以搜索”wechat“查看所有与wechat有关的可执行文件并选择合适的

当都配置好后,能够抓取到微信小程序的包

yakit 客户端抓包

和微信小程序抓包步骤相同

只是可执行文件换为客户端可执行文件即可

Note

burp与yakit原理,操作相同