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对抗,参考以下的两个项目:

JustTrustMe、sslunpining

指纹校验

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

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);
};
});
}