Android逆向15-抓包

来自吾爱破解-正己

https://www.52pojie.cn/thread-1701353-1-1.html

抓包基础知识

在计算机网络中,“包”通常指的是在网络上传输的数据单元,也被称为数据包。在互联网协议(IP)的语境下,数据包是由报头(Header)和载荷(Payload)组成,其中报头包含了源地址、目的地址、长度等信息,而载荷则是实际要传输的数据。

抓包具体指的是通过某些工具获取安卓App与服务器之间传输的网络数据,这些数据通常用于逆向分析、协议接口分析或App渗透测试,帮助安全工程师理解App与服务器之间的通信细节,如请求和响应的具体内容,从而进行安全评估或逆向工程。

  1. 帮助定位加密或混淆的代码中的关键部分
  2. 逆向接口(比如:一些第三方影视app的解析接口、分析是否为服务器校验)
  3. 篡改数据包实现免重打包破解&屏蔽广告
  4. 协议分析&爬虫需求

网络协议可以分层,每层都有特定的任务和责任,最著名的分层模型是OSI七层模型和TCP/IP四层模型。常见的网络协议包括:

  • TCP/IP(Transmission Control Protocol/Internet Protocol):这是互联网的基础协议,包括了IP协议(用于寻址和路由数据包)、TCP协议(用于建立可靠的数据流)和UDP协议(用于不需要确认的快速数据传输)。
  • HTTP(Hypertext Transfer Protocol):用于从Web服务器向浏览器传输超文本文档(如HTML文件)。
  • FTP(File Transfer Protocol):用于在互联网上传输文件。
  • SMTP(Simple Mail Transfer Protocol):用于电子邮件的传输。
  • DHCP(Dynamic Host Configuration Protocol):自动分配IP地址和其他网络配置参数给网络上的设备。
  • DNS(Domain Name System):将域名转换为IP地址。

URL 的规则定义如下:

https://www.52pojie.cn/forum.php

https 表示资源需要通过 HTTPS 这个协议才能够获取,换句话说,客户端需要通过 HTTPS这个协议请求这个资源。

www.52pojie.cn 表示服务器地址,在互联网中每个服务器都有一个 IP 地址,但对于用户来说 IP 地址很难记住,用户一般只会记住服务器主机(比如www.52pojie.cn)名称。

在 HTTPS 中,客户端发送 HTTPS 请求的时候,必须通过 DNS 协议将服务器主机名转换为IP 地址,这样客户端才能找到服务器。

443 是 HTTPS 协议的默认端口(可以省略不输入),表示服务器通过 443 端口提供 HTTPS服务。

/forum.php 表示服务器在/根目录下有一个 forum.php 资源。

image-20250508211354648

数字证书是网络安全领域的一个重要组成部分,主要用于身份验证和加密通信。它基于公钥基础设施(Public Key Infrastructure, PKI)的原理,由证书颁发机构(Certificate Authority, CA)签发,用于证明公钥的所有者身份。

image-20250508211502247

工具名称 类型 下载链接 简介
Charles 代理抓包工具 https://www.52pojie.cn/thread-1600964-1-1.html Charles 是一个HTTP代理/HTTP监视器/反向代理,它允许开发人员查看所有的HTTP和SSL/HTTPS流量。
Fiddler 代理抓包工具 https://www.alipan.com/s/2W8r2ko7UWz Fiddler 是一个Web调试代理,能够记录和检查从任何浏览器和客户端到服务器的所有HTTP流量。
Burp Suite 代理抓包工具(理论上应该叫渗透必备工具) https://www.52pojie.cn/thread-1544866-1-1.html Burp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具。Burp Suite为这些工具设计了许多接口,以加快攻击应用程序的过程。所有工具都共享一个请求,并能处理对应的HTTP 消息、持久性、认证、代理、日志、警报。
Reqable 代理抓包工具 https://reqable.com/zh-CN/download Reqable = Fiddler + Charles + Postman
Reqable拥有极简的设计、丰富的功能、高效的性能和桌面手机双端平台。
ProxyPin VPN抓包工具 https://github.com/wanghongenpin/network_proxy_flutter/releases/tag/V1.1.0 开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统
可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter开发,UI美观易用。
WireShark 网卡抓包工具 https://www.wireshark.org/download.html Wireshark是非常流行的网络封包分析软件,可以截取各种网络数据包,并显示数据包详细信息。常用于开发测试过程各种问题定位。
r0Capture Hook抓包工具 https://github.com/r0ysue/r0capture 安卓应用层抓包通杀脚本
tcpdump 内核抓包工具 https://github.com/the-tcpdump-group/tcpdump cpdump 是一个强大的命令行网络数据包分析工具,允许用户截获并分析网络上传输的数据包,支持多种协议,包括但不限于TCP、UDP、ICMP等。tcpdump基于libpcap库,该库提供了从网络接口直接访问原始数据包的能力。
eCapture(旁观者) 内核抓包工具 https://github.com/gojue/ecapture/releases 基于eBPF技术实现TLS加密的明文捕获,无需CA证书。

证书安装

选择适合自己的方式安装证书

Reqable使用经典的中间人(MITM)技术分析HTTPS流量,当客户端与Reqable的代理服务器(下文简称中间人)进行通信时,中间人需要重签远程服务器的SSL证书。为了保证客户端与中间人成功进行SSL握手通信,需要将中间人的根证书(下文简称CA根证书)安装到客户端本地的证书管理中心。

  • 状态:表示请求的状态,这里是”Completed”,意味着请求已经完成。
  • 方法:HTTP 请求的方法(也称为动词),这里为 “GET”,说明这是一个获取资源的请求。
  • 协议:使用的 HTTP 协议版本,此处为 “HTTP/1.1”。(Reqable支持HTTP1、HTTP2和HTTP3(QUIC)协议)
  • Code:HTTP 状态码,200 表示服务器成功处理了请求。
  • 服务器地址:请求的目标服务器的 IP 地址和端口号,这里是 “180.76.198.77:443”。
  • Keep Alive:是否保持连接 alive,值为 “true” 意味着会保持连接。
  • Content Type:返回内容的数据类型,这里没有指定具体的类型。
  • 代理协议:如果使用了代理,则显示代理协议,此例中为 “https”。
1
HTTP方法
方法 描述
GET 请求指定的页面信息并返回实体主体
HEAD 类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件),数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或对已有资源的修改
PUT 从客户端向服务器传送的数据取代指定文档的内容
DELETE 请求服务器删除指定的页面

HTTP常见状态码

名 称 释 义
200 OK 服务器成功处理了请求。
301 Moved Permanently 请求的URL已被永久移动。Response应包含Location URL指向新位置。
302 Moved Temporarily 请求的URL被暂时移动。Response应包含Location URL指向临时位置。
304 Not Modified 客户端缓存的资源是最新的,无需重新发送,客户端应使用缓存。
404 Not Found 请求的资源未在服务器上找到。
401 Unauthorized 请求要求用户的身份认证。
500 Internal Server Error 服务器遇到了意外的情况,无法完成请求。

image-20250508215256945

  1. 第一行是请求行,包含了请求方法(GET)、请求路径以及HTTP协议版本(HTTP/1.1)。在这个例子中,请求方法是GET,表明这是一个获取操作;请求路径是/ZJ2595/wuaijie/raw/master/movie/list1.json,这意味着请求的目标资源位于gitee.com网站的某个特定目录下,具体来说是在ZJ2595/wuaijie仓库下的master分支中的movie/list1.json文件;HTTP/1.1表示使用的HTTP协议版本。
  2. 第二行 Host: gitee.com 是一个HTTP头字段,指明了请求要访问的具体主机名,即gitee.com。
  3. 第三行 Connection: Keep-Alive 表示客户端希望保持持久连接。这意味着一旦TCP连接建立之后,可以重复使用该连接来发送多个HTTP请求,而不需要为每个请求单独建立一个新的连接。
  4. 第四行 Accept-Encoding: gzip 告诉服务器客户端支持的压缩编码类型。在这个例子中,客户端表示它可以接受gzip压缩编码的内容。
  5. 最后一行 User-Agent: okhttp/3.12.0 提供了关于发起请求的应用程序的信息。User-Agent头字段通常用于标识发出请求的浏览器或应用程序的类型、版本和其他相关信息。在这里,User-Agent是okhttp/3.12.0,这表示请求是由OkHttp库的一个版本3.12.0发出的。

请求头 (Request Headers)

名称 描述
Accept 指定客户端能接收的媒体类型。
Accept-Charset 指定客户端能接收的字符集。
Accept-Encoding 指定客户端能解码的编码方式,如gzip或deflate。
Accept-Language 指定客户端首选的语言。
Authorization 包含用于访问资源的认证信息。
Cache-Control 控制缓存行为,如no-cache或max-age。
Connection 控制HTTP连接是否保持活动状态,如keep-alive或close。
Content-Length 指明请求体的长度。
Content-Type 指明请求体的数据类型,如application/json。
Cookie 包含客户端的cookie信息。
Date 请求生成的时间。
Expect 指定客户端期望服务器执行的操作。
From 发送请求的用户邮箱地址。
Host 请求的目标服务器的域名和端口号。
If-Modified-Since 用于条件性GET,如果资源自指定日期后未被修改则返回304。
If-None-Match 用于条件性GET,如果资源的ETag与提供的不匹配则返回资源。
Origin 指明请求来源的源站地址,常用于跨域资源共享(CORS)。
Pragma 包含与特定代理有关的指令。
Referer 指明请求前一个页面的URL,可用于跟踪引用页面。
TE 表示客户端能处理的传输编码方式。
Trailer 指明报文主体之后的尾部字段。
Transfer-Encoding 指明报文主体的传输编码方式,如chunked。
Upgrade 指示客户端希望升级到另一种协议。
User-Agent 包含客户端软件的名称和版本信息。
Via 记录请求经过的中间节点,用于追踪和诊断。
Warning 包含非致命问题的警告信息。

响应头 (Response Headers)

名称 描述
Age 响应对象在代理或缓存中的存储时间。
Cache-Control 控制缓存行为,如public、private、no-store、no-cache等。
Connection 指示连接是否保持打开,如keep-alive或close。
Content-Encoding 指明响应体的编码方式,如gzip或deflate。
Content-Length 响应体的长度。
Content-Type 响应体的数据类型,如text/html。
Date 服务器生成响应的时间。
ETag 响应资源的实体标签,用于判断资源是否已被修改。
Expires 响应过期时间,之后缓存不应再使用。
Last-Modified 资源最后修改的时间。
Location 用于重定向,包含资源的新位置。
Pragma 与特定代理有关的指令。
Proxy-Authenticate 当代理服务器需要认证时使用。
Retry-After 在重试之前等待的时间。
Server 服务器软件的名称和版本。
Set-Cookie 用于设置或更新客户端的cookie。
Trailer 指明响应尾部字段。
Transfer-Encoding 响应体的传输编码方式,如chunked。
Upgrade 用于协议升级。
Vary 指明哪些请求头会影响响应的内容,用于缓存控制。
WWW-Authenticate 当服务器需要认证时使用。
X-Frame-Options 控制页面是否可以在iframe中显示。

抓包检测

代理检测

定义

代理检测是用于检测设备是否设置了网络代理。这种检测的目的是识别出设备是否尝试通过代理服务器(如抓包工具)来转发网络流量,从而可能截获和分析App的网络通信。

原理

App会检查系统设置或网络配置,以确定是否有代理服务器被设置为转发流量。例如,它可能会检查系统属性或调用特定的网络信息API来获取当前的网络代理状态。

1
2
3
return System.getProperty("http.proxyHost") == null && System.getProperty("http.proxyPort")  == null

Port跟设置有关,例如Charles默认是8888

强制不走代理

1
2
3
4
5
connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);

OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.build()

Charles安装与配置

证书安装的问题:WIN 11的 charles 安装SSL证书提示 此文件作为下列运行是无效的:安全证书_测试-CSDN问答

安装教程:https://blog.csdn.net/qq_45005145/article/details/141132846

key:

1
2
Registered Name:        52pojie
License Key: d43c11e6697bbe07a8

anti脚本:

1
2
3
4
5
6
7
8
9
function anti_proxy() {
var GetProperty = Java.use("java.lang.System");
GetProperty.getProperty.overload("java.lang.String").implementation = function(getprop) {
if (getprop.indexOf("http.proxyHost") >= 0 || getprop.indexOf("http.proxyPort") >= 0) {
return null;
}
return this.getProperty(getprop);
}
}

透明代理

透明代理(Transparent Proxy)是一种特殊的代理服务类型,它可以在客户端(如浏览器或应用程序)不知道的情况下拦截、转发和处理网络请求。与传统的代理服务不同,透明代理不需要客户端进行任何配置就能工作。

[Clash版]安卓上基于透明代理实现热点抓包

安卓上基于透明代理对特定APP抓包

VPN检测

定义

VPN检测是指应用程序或系统检查用户是否正在使用虚拟专用网络(Virtual Private Network, VPN)的一种技术。当用户使用VPN时,他们的网络流量会被加密并通过一个远程服务器路由,这可以隐藏用户的实际IP地址和位置信息,同时保护数据的安全性和隐私。

原理

当客户端运行VPN虚拟隧道协议时,会在当前节点创建基于eth之上的tun0接口或ppp0接口。这些接口是用于建立虚拟网络连接的特殊网络接口。

根据OSI七层模型,二者分别支持的协议:

VPN OpvenVPN、IPsec、IKEv2、PPTP、L2TP、WireGuard等
代理 HTTP、HTTPS、SOCKS、FTP、RTSP等

VPN 协议大多是作用在 OSI 的第二层和第三层之间,由此可见VPN能抓到代理方式的所有的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public final boolean Check_Vpn1() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
if (networkInterfaces == null) {
return false;
}
Iterator it = Collections.list(networkInterfaces).iterator();
while (it.hasNext()) {
NetworkInterface networkInterface = (NetworkInterface) it.next();
if (networkInterface.isUp() && !networkInterface.getInterfaceAddresses().isEmpty()) {
Log.d("zj595", "isVpn NetworkInterface Name: " + networkInterface.getName());
if (Intrinsics.areEqual(networkInterface.getName(), "tun0") || Intrinsics.areEqual(networkInterface.getName(), "ppp0") || Intrinsics.areEqual(networkInterface.getName(), "p2p0") || Intrinsics.areEqual(networkInterface.getName(), "ccmni0")) {
return true;
}
}
}
return false;
} catch (Throwable th) {
th.printStackTrace();
return false;
}
}

public final boolean Check_Vpn2() {
boolean z;
String networkCapabilities;
try {
Object systemService = getApplicationContext().getSystemService("connectivity");
Intrinsics.checkNotNull(systemService, "null cannot be cast to non-null type android.net.ConnectivityManager");
ConnectivityManager connectivityManager = (ConnectivityManager) systemService;
NetworkCapabilities networkCapabilities2 = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
Log.i("zj595", "networkCapabilities -> " + networkCapabilities2);
boolean z2 = networkCapabilities2 != null && networkCapabilities2.hasTransport(4);
// 检查网络能力是否包含 "WIFI|VPN"
if (networkCapabilities2 != null && (networkCapabilities = networkCapabilities2.toString()) != null) {
if (StringsKt.contains$default((CharSequence) networkCapabilities, (CharSequence) "WIFI|VPN", false, 2, (Object) null)) {
z = true;
return !z || z2;
}
}
z = false;
if (z) {
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

anti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function hook_vpn() {
Java.perform(function () {
var NetworkInterface = Java.use("java.net.NetworkInterface");
NetworkInterface.getName.implementation = function () {
var name = this.getName(); //hook java层的getName方法
console.log("name: " + name);
if (name === "tun0" || name === "ppp0") {
return "rmnet_data0";
} else {
return name;
}
}

var NetworkCapabilities = Java.use("android.net.NetworkCapabilities");
NetworkCapabilities.hasTransport.implementation = function () {
return false;
}

NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) {
if (bitMask == 18) {
console.log("bitMask", bitMask);
sb.append("WIFI");
} else {
console.log(sb, bitMask);
this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator);
}
}

})
}

SSL Pinning

SSL Pinning 也称为证书锁定,是Google官方推荐的检验方式,意思是将服务器提供的SSL/TLS证书内置到移动客户端,当客户端发起请求的时候,通过对比内置的证书与服务器的证书是否一致,来确认这个连接的合法性。 PS:这里还要提到一个概念:单向校验,本质上二者没区别,SSL Pinning可以理解为加强版的单向校验

image-20250509204346269

1
2
3
4
5
6
7
8
9
10
11
12
1.客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。
2.服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书
3.客户端使用服务端返回的信息验证服务器的合法性,包括:
(1)证书是否过期
(2)发型服务器证书的CA是否可靠
(3)返回的公钥是否能正确解开返回证书中的数字签名
(4)服务器证书上的域名是否和服务器的实际域名相匹配、验证通过后,将继续进行通信,否则,终止通信
4.客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择
5.服务器端在客户端提供的加密方案中选择加密程度最高的加密方式。
6.服务器将选择好的加密方案通过明文方式返回给客户端
7.客户端接收服务端返回的加密方式后,使用该加密方式生成产生随机码,用作通信过程中对称加密的密钥,使用服务端返回的公钥进行加密,将加密后的随机码发送至服务器
8.服务器收到客户端返回的加密信息后,使用自己的私钥进行解密,获取对称加密密钥。在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全

SSL Pinning主流的三套方案:公钥校验证书校验Host校验

因为是客户端做的校验,所以可以在本地进行hook对抗,参考以下的两个项目:

JustTrustMesslunpining

指纹校验

在网站中我们可以看到网站的证书相关信息,其中就包含了指纹信息

image-20250509204426702

常见安卓网络开发框架

框架名称 描述 GitHub 地址
Volley 由Google开源的轻量级网络库,支持网络请求处理、小图片的异步加载和缓存等功能 https://github.com/google/volley
Android-async-http 基于Apache HttpClient的一个异步网络请求处理库 https://github.com/android-async-http/android-async-http
xUtils 类似于Afinal,但被认为是Afinal的一个升级版,提供了HTTP请求的支持 https://github.com/wyouflf/xUtils3
OkHttp 一个高性能的网络框架,已经被Google官方认可,在Android 6.0中底层源码已经使用了OkHttp来替代HttpURLConnection https://github.com/square/okhttp
Retrofit 提供了一种类型安全的HTTP客户端接口,简化了HTTP请求的编写,通常与OkHttp配合使用 https://github.com/square/retrofit

OkHttp和Retrofit是非常流行的组合,被广泛应用于现代Android应用开发中

原理(以okhttp框架为例):

在CertificatePinner类里有一个check方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 检查指定主机名的证书链是否符合预设的哈希值(证书固定)。
* @Param hostname 要验证的主机名。
* @param peerCertificates 待验证的证书列表。
* @throws SSLPeerUnverifiedException 如果证书不符合预设的哈希值,则抛出此异常。
*/
public void check(String hostname, List<Certificate> peerCertificates)
throws SSLPeerUnverifiedException {
// 查找与主机名匹配的哈希值列表(证书固定列表)。
List<Pin> pins = findMatchingPins(hostname);
// 如果没有找到任何匹配的哈希值,则直接返回,表示无需进一步检查。
if (pins.isEmpty()) return;

// 如果存在证书链清理器,则先清理证书链中的冗余证书。
if (certificateChainCleaner != null) {
peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
}
// 遍历每一个证书进行检查。
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
// 获取当前证书。
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
// 懒加载计算每个证书的SHA-1和SHA-256哈希值。
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/")) {
// 如果尚未计算SHA-256哈希值,则进行计算。
if (sha256 == null) sha256 = sha256(x509Certificate);
// 如果证书的SHA-256哈希值与预设值相同,则返回成功。
if (pin.hash.equals(sha256)) return;
} else if (pin.hashAlgorithm.equals("sha1/")) {
// 如果尚未计算SHA-1哈希值,则进行计算。
if (sha1 == null) sha1 = sha1(x509Certificate);
// 如果证书的SHA-1哈希值与预设值相同,则返回成功。
if (pin.hash.equals(sha1)) return;
} else {
// 如果遇到不支持的哈希算法,则抛出错误。
throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
}
}
}
// 如果遍历完所有证书和哈希值都没有匹配,则抛出异常。
throw new SSLPeerUnverifiedException("No matching certificate found.");
}

实现方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//指纹检测  
fun check_SSL_PINNING_key() {
// 使用CoroutineScope和Dispatchers.IO在后台线程中执行网络操作
CoroutineScope(Dispatchers.IO).launch {
// 定义需要固定证书的域名
val caDomain = "www.52pojie.cn"
// 使用CertificatePinner.Builder构建一个证书固定器
// 添加一个sha256哈希值,这个哈希值是服务器证书的指纹
// 这个值需要通过openssl工具获取
val pinner = CertificatePinner.Builder()
.add(caDomain, "sha256/WnsD5UGdP5/a65xO1rpH8ru2EjyxkmPEaiNtKixhJLU=") // 添加证书指纹
.build() // 构建完成证书固定器
// 使用newBuilder创建一个新的OkHttpClient实例,用于覆盖默认的客户端配置
val pClient = client.newBuilder()
.certificatePinner(pinner) // 设置证书固定器
.build() // 构建新的OkHttpClient实例
// 构建一个网络请求,访问指定的URL
val request = Request.Builder()
.url("https://www.52pojie.cn/?q=SSLPinningCode") // 设置请求的URL
.build() // 构建请求对象
try {
// 执行网络请求,并获取响应
val response = pClient.newCall(request).execute()
// 如果响应状态码是200,表示指纹检测通过
Log.d(TAG, "指纹检测通过,状态码:${response.code()}")
} catch (e: IOException) {
// 如果发生IOException,表示指纹检测不通过
Log.d(TAG, "指纹检测不通过")
e.printStackTrace() // 打印异常堆栈信息,用于调试
}
}
}

安装openssl,OpenSSL Windows 版本

image-20250509205625373

image-20250509205632298

cmd窗口输入以下命令获取

1
openssl s_client -connect www.52pojie.cn:443 -servername www.52pojie.cn | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

image-20250509205658451

anti脚本

1
2
3
4
5
6
7
function anti_ssl_key() {
//check方法置空即可
var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {
console.log('[+] Bypassing SSL key pinning: ' + a);
return;
}}

证书校验

通过trustManager 类实现的checkServerTrusted接口,核心在于验证服务器证书的公钥。具体步骤包括:获取服务器返回的证书,将其公钥编码为 Base64 字符串;同时从本地资源加载预存的可信客户端证书,并将其公钥也编码为 Base64 字符串。然后,比较这两个公钥是否匹配,以此确认服务器的身份是否合法。最后,使用自定义的 SSLSocketFactory 发起 HTTPS 请求,确保通信过程中只信任预定义的服务器证书,从而有效抵御中间人攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 定义一个函数用于检查SSL证书
private fun check_SSL_PINNING_CA() {
// 创建一个X509TrustManager的匿名对象,用于自定义信任管理器
val trustManager: X509TrustManager = object : X509TrustManager {
// 客户端证书信任检查,这里不实现任何逻辑
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) {
}
// 服务器证书信任检查
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String?) {
// 获取服务器返回的第一个证书
val cf: X509Certificate = chain[0]
// 将服务器证书的公钥编码为Base64字符串
val ServerPubkey: String = Base64.encodeToString(cf.publicKey.encoded, 0)
Log.e(TAG, "服务器端返回的证书:" + ServerPubkey)
// 从客户端资源中读取证书
val client_input = resources.openRawResource(R.raw.wuai)
val certificateFactory = CertificateFactory.getInstance("X.509")
// 生成客户端证书
val realCertificate: X509Certificate = certificateFactory.generateCertificate(client_input) as X509Certificate
// 将客户端证书的公钥编码为Base64字符串
val realPubkey: String = Base64.encodeToString(realCertificate.publicKey.encoded, 0)
Log.e(TAG, "客户端资源目录中的证书:" + realPubkey)
// 检查证书有效期
cf.checkValidity()
// 比较服务器证书和客户端证书的公钥是否相同
val expected = realPubkey.equals(ServerPubkey, ignoreCase = true)
if (!expected) {
Log.e(TAG, "证书检测不通过")
} else {
Log.e(TAG, "证书检测通过")
}
}
// 返回受信任的CA证书数组,这里返回空数组
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls<X509Certificate>(0)
}
}
// 声明SSLSocketFactory变量
var factory: SSLSocketFactory? = null
try {
// 获取SSL上下文
val sslContext = SSLContext.getInstance("SSL")
// 初始化SSL上下文,使用自定义的信任管理器
sslContext.init(null, arrayOf<TrustManager>(trustManager), SecureRandom())
// 获取SSLSocketFactory
factory = sslContext.socketFactory
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
// 确保factory不为空
val finalFactory: SSLSocketFactory? = factory
// 创建并启动一个新线程来执行网络请求
object : Thread() {
override fun run() {
try {
// 使用自定义的SSLSocketFactory创建OkHttpClient
val client =
OkHttpClient.Builder().sslSocketFactory(finalFactory, trustManager).build()
// 构建请求
val req = Request.Builder().url("https://www.52pojie.cn/forum.php").build()
// 发送请求并获取响应
val call: okhttp3.Call = client.newCall(req)
val res: Response = call.execute()
// 打印响应状态码
Log.e("请求发送成功", "状态码:" + res.code())
} catch (e: IOException) {
// 打印网络异常信息
Log.e("请求发送失败", "网络异常$e")
}
}
}.start()
}

cmd窗口输入以下命令获取证书信息

1
openssl s_client -connect 52pojie.cn:443 -servername 52pojie.cn | openssl x509 -out wuai.pem
1
证书信知识补充:
名词 含义
X.509 一种通用的证书格式,包含证书持有人的公钥、加密算法等信息
PKCS1~PKCS12 公钥加密(非对称加密)的一系列标准(Public Key Cryptography Standards),.p12 是包含证书和密钥的封装格式
*.der 证书的二进制存储格式(不常用)
*.pem 证书或密钥的 Base64 文本存储格式,可以单独存放证书或密钥,也可以同时存放证书和密钥
*.key 单独存放的 pem 格式的私钥文件,一般保存为 *.key
.cer / .crt 两者指的都是证书,Linux 下叫 crt,Windows 下叫 cer;存储格式可以是 pem,也可以是 der
*.csr 证书签名请求(Certificate Signing Request),包含证书持有人的信息,如:国家、邮件、域名等
*.pfx 微软 IIS 的实现,包含证书和私钥

有的证书内容是只包含公钥(服务器的公钥),如.crt、.cer、.pem

有的证书既包含公钥又包含私钥(服务器的私钥),如.pfx、.p12

另外有些app的证书不走寻常路,不是上面所罗列到的格式,它有可能伪装成png等其他格式

anti脚本

思路:实例化一个trustManager类,然后里面什么都不写,当上面两处调用到这个类时hook这两个地方,把自己定义的空trustManager类放进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function anti_ssl_cert() {
// 使用Frida获取Java类X509TrustManager的引用
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
// 使用Frida获取Java类SSLContext的引用
var SSLContext = Java.use('javax.net.ssl.SSLContext');
// 注册一个自定义的TrustManager类
var TrustManager = Java.registerClass({
// 指定自定义TrustManager的全名
name: 'dev.asd.test.TrustManager',
// 指定自定义TrustManager实现的接口
implements: [X509TrustManager],
// 定义自定义TrustManager的方法实现
methods: {
// 客户端证书信任检查,这里不实现任何逻辑
checkClientTrusted: function(chain, authType) {},
// 服务器证书信任检查,这里不实现任何逻辑
checkServerTrusted: function(chain, authType) {},
// 返回受信任的CA证书数组,这里返回空数组
getAcceptedIssuers: function() {return []; }
}
});
// 准备一个TrustManager数组,用于传递给SSLContext.init()方法
var TrustManagers = [TrustManager.$new()];
// 获取SSLContext.init()方法的引用,该方法用于初始化SSL上下文
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'
);
try {
// 覆盖init方法的实现,指定使用自定义的TrustManager
SSLContext_init.implementation = function(keyManager, trustManager, secureRandom) {
console.log('[+] Bypassing Trustmanager (Android < 7) pinner');
// 调用原始的init方法,并使用自定义的TrustManager数组
SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
};
} catch (err) {
// 如果覆盖init方法失败,打印错误信息
console.log('[-] TrustManager (Android < 7) pinner not found');
console.log(err); // 可以取消注释来打印异常的详细信息
}
}

双向认证

双向验证,顾名思义就是客户端验证服务器端证书的正确性,服务器端也验证客户端的证书正确性

image-20250509205853538

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。
2.服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书
3.客户端使用服务端返回的信息验证服务器的合法性,包括:
(1)证书是否过期
(2)发型服务器证书的CA是否可靠
(3)返回的公钥是否能正确解开返回证书中的数字签名
(4)服务器证书上的域名是否和服务器的实际域名相匹配、验证通过后,将继续进行通信,否则,终止通信
4.服务端要求客户端发送客户端的证书,客户端会将自己的证书发送至服务端
5.验证客户端的证书,通过验证后,会获得客户端的公钥
6.客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择
7.服务器端在客户端提供的加密方案中选择加密程度最高的加密方式
8.将加密方案通过使用之前获取到的公钥进行加密,返回给客户端
9.客户端收到服务端返回的加密方案密文后,使用自己的私钥进行解密,获取具体加密方式,而后,产生该加密方式的随机码,用作加密过程中的密钥,使用之前从服务端证书中获取到的公钥进行加密后,发送给服务端
10.服务端收到客户端发送的消息后,使用自己的私钥进行解密,获取对称加密的密钥,在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全。

实现方案:

  1. 首先借助openssl生成服务端证书
1
2
3
4
5
# 生成CA私钥 
openssl genrsa -out ca.key 2048

# 生成CA自签名证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1024 -out ca.crt
  1. 生成服务端证书
1
openssl genrsa -out server.key 2048

这个指令生成一个2048位的RSA私钥,并将其保存到名为server.key的文件中

这个指令基于第一步生成的私钥创建一个新的证书签名请求(CSR)。CSR包含了公钥和一些身份信息,这些信息在证书颁发过程中用于识别证书持有者。-out server.csr指定了CSR的输出文件名。

执行这个指令时,系统会提示你输入一些身份信息,如国家代码、组织名等,这些信息将被包含在CSR中。(我们这边测试直接全部按回车键默认即可)

字段名称 描述 默认值/示例值 是否必填
Country Name (2 letter code) 国家代码,两位字母代码。 AU
State or Province Name 州或省份的全名。
Locality Name (eg, city) 城市或地区名称。
Organization Name 组织名称,通常是公司或机构的名称。
Organizational Unit Name (eg, section) 组织单位名称,可以是部门或团队的名称。
Common Name (CN) 完全限定的域名(FQDN)或个人名称,用于标识证书持有者。
Email Address 与证书持有者关联的电子邮件地址。
Challenge Password 挑战密码,用于CSR的额外安全措施。
Optional Company Name 可选的公司名称字段。

-config server_cert.conf创建一个OpenSSL配置文件(如 server_cert.conf)并指定IP地址,具体的ip地址可以由ipconfig获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
这个指令基于第一步生成的私钥创建一个新的证书签名请求(CSR)。CSR包含了公钥和一些身份信息,这些信息在证书颁发过程中用于识别证书持有者。-out server.csr指定了CSR的输出文件名。
执行这个指令时,系统会提示你输入一些身份信息,如国家代码、组织名等,这些信息将被包含在CSR中。(我们这边测试直接全部按回车键默认即可)

字段名称 描述 默认值/示例值 是否必填
Country Name (2 letter code) 国家代码,两位字母代码。 AU 否
State or Province Name 州或省份的全名。 否
Locality Name (eg, city) 城市或地区名称。 否
Organization Name 组织名称,通常是公司或机构的名称。 否
Organizational Unit Name (eg, section) 组织单位名称,可以是部门或团队的名称。 否
Common Name (CN) 完全限定的域名(FQDN)或个人名称,用于标识证书持有者。 是
Email Address 与证书持有者关联的电子邮件地址。 否
Challenge Password 挑战密码,用于CSR的额外安全措施。 否
Optional Company Name 可选的公司名称字段。 否
-config server_cert.conf创建一个OpenSSL配置文件(如 server_cert.conf)并指定IP地址,具体的ip地址可以由ipconfig获取
1
2
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -extfile server_cert.conf -extensions v3_req
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.cer

使用CA证书签发服务器证书。

生成cer证书供服务端验证。

客户端证书:

1
2
3
openssl genrsa -out client.key 2048
openssl req -new -out client.csr -key client.key
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 500 -sha256

生成客户端带密码的p12证书(这步很重要,双向认证的话,浏览器访问时候要导入该证书才行;可能某些Android系统版本请求的时候需要把它转成bks来请求双向认证):

1
openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt

到这一步的时候,设置密码和验证密码光标不会显示,直接输入即可

image-20250509210036558

环境配置

PS:因为双向认证是本地搭建,所以需要完成几个前置条件:

  1. 确保电脑和手机处于同一wifi连接下
  2. 重打包替换生成的server.cer(路径在res/raw),替换ssl_verify方法里的ip地址以及res/xml/network_config.xml的ip地址(通过ipconfig获取实际的ipv4地址)
  3. 运行服务端代码,然后再请求看看是否能正常输出

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from flask import Flask, jsonify  
import ssl

app = Flask(__name__)

# ssl 证书校验
@app.route('/ca')
def ssl_verify():
return jsonify({"message": "HTTPS server with mutual SSL verification started."})

# 配置ssl上下文,关键函数
def get_ssl_context():
# CA根证书路径
ca_crt_path = 'certs/ca.crt'
# 服务端证书和密钥路径
server_crt_path = 'certs/server.crt'
server_key_path = 'certs/server.key'
# 创建SSL上下文,使用TLS服务器模式
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# 设置验证模式为需要客户端证书
ssl_context.verify_mode = ssl.CERT_REQUIRED
# 启用主机名检查(根据需要设置)
ssl_context.check_hostname = False
# 设置加密套件
ssl_context.set_ciphers("HIGH:!SSLv3:!TLSv1:!aNULL:@STRENGTH")
# 加载CA根证书,用于验证客户端证书
ssl_context.load_verify_locations(cafile=ca_crt_path)
# 加载服务端证书和私钥
ssl_context.load_cert_chain(certfile=server_crt_path, keyfile=server_key_path)
return ssl_context

if __name__ == '__main__':
ssl_context = get_ssl_context()
app.run(host="192.168.124.21", port=8088,ssl_context=ssl_context)

dump内置证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function hook_KeyStore_load() {
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var myArray=new Array(1024);
var i = 0
for (i = 0; i < myArray.length; i++) {
myArray[i]= 0x0;
}
var buffer = Java.array('byte',myArray);
var StringClass = Java.use("java.lang.String");
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
console.log("KeyStore.load1:", arg0);
this.load(arg0);
};
KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);
if (arg0){
var file = Java.use("java.io.File").$new("/data/user/0/com.zj.wuaipojie/files/client"+".p12");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while( (r = arg0.read(buffer)) > 0){
out.write(buffer,0,r)
}
console.log("证书保存成功!")
out.close()
}
this.load(arg0, arg1);
};
});
}