./stackplz -p $(pidof com.bin_lab.app_tls_fingerprint_labs) --brk 0x20DA3A:r --brk-lib libapp_tls_fingerprint_labs_lib.so --stack


证书校验

单项认证 

https://codeshare.frida.re/@Q0120S/bypass-ssl-pinning/

Android App 通常使用 okhttp 这类的通用网络库,这类网络库可以使用 hook 脚本通杀。

例如 frida 脚本 https://codeshare.frida.re/@Q0120S/bypass-ssl-pinning/

可以通杀大部分证书校验。

另外,使用 Android 默认网络协议栈的 App 程序,你可以使用 ecapture 这类基于 ebpf 的抓包工具抓取加密流量。


非标准网络库/协议栈
这个就很复杂了,例如 Flutter 的 DIO、Golang 的 gotls,OpenSSL、BoringSSL。虽然 Android 提供了 BoringSSL,但是却没有暴露接口,App 可以自己编译 BoringSSL 使用,也可以编译 OpenSSL、Fizz 等网络通信库。

对于这些 App 自己编译的 Native 网络库,通用脚本几乎无法处理,ecapture 也不行。更有极端情况,例如 Flutter 的 DIO 自带 CA 库,不去系统读取。

更加复杂的情况,我们将在后续课程讨论。

Pinning 进阶
目前大部分 Pinning 的技术实战就仅局限于客户端验证,几乎没有看到服务端验证的例子。

考虑这个场景:

1.客户端验证 Pinning 值

2.客户端记录 Pinning 值

3.客户端将 Pinning 值作为请求 HMAC 签名、或者将 Pinning 值作为风控因子之一

这个场景见上一个实验中的反向绑定。

主动探测证书
客户端在 Native 层可以主动发起 TLS 握手流程,获取服务端证书,解析服务端证书,解析公钥,记录该公钥作为验证数据。

基于这样的思路,所有通杀工具将失效。

DEMO 已经开源

https://github.com/P4nda0s/probe-pin

实操
使用 frida 脚本完成下面这个实操

https://codeshare.frida.re/@Q0120S/bypass-ssl-pinning/

签名验证
什么是签名验证?
签名验证:客户端将请求中的关键数据(参数、时间戳、随机数、环境因子等)按照固定规则计算签名,随请求发送;服务端使用同样规则复算并比对,从而确认请求未被篡改、来源可信、未被重放。

Canonical String
Canonical String 是一种**“标准化后的待签名字符串”,把一次请求中需要参与签名的所有要素**,按固定顺序、固定格式、固定编码拼接成一个字符串,用来作为签名算法的唯一输入。

示例
method=POST
path=/api/login
ts=1700000000000
body=data
devices_id=id
签名验证流程
在开发阶段,首先需要完成签名方案的设计,明确签名算法、参与签名的参数以及 Canonical String 的构造规则。基于这一统一设计,客户端与服务端分别实现各自的签名与验签模块。

客户端在发起请求之前,通过签名模块对当前请求进行处理:按照既定规则提取待签名字段,构造 Canonical String,并使用约定的签名算法计算签名值。计算完成后,客户端将签名结果随请求一并发送至服务端。

服务端在收到请求后,由验签模块对请求中的签名相关字段进行提取,并按照与客户端完全一致的规则重建 Canonical String,随后复算签名结果。服务端将复算得到的签名与客户端传入的签名进行比对。

如果签名不匹配,说明请求在传输过程中被篡改、被重放,或客户端未按约定规则生成签名,服务端将直接拒绝该请求;只有在签名校验通过的情况下,服务端才会执行业务逻辑,并向客户端返回正常的业务响应。

什么是双向验证?
双向认证(Mutual TLS,mTLS): 在 TLS 握手过程中,客户端与服务端相互验证对方的身份。 不仅客户端需要验证服务器证书,服务器同样需要验证客户端证书,任何一方校验失败,TLS 连接都会被中止。

与单向认证(标准 HTTPS)相比,双向认证进一步解决了:服务端如何确认“请求一定来自被允许的客户端”。

双向验证与单向验证的区别
验证方向 验证内容 是否默认 实现方
客户端 → 服务端 服务端证书合法性 ✅ 系统 / TLS 库
服务端 → 客户端 客户端证书合法性 ❌ 开发者

Android / Native 场景下的作用
在 Android 应用中,双向验证常用于:

内部接口或私有 API
高风险接口(登录、鉴权、风控)
替代或增强 AppKey / Token 认证机制
即使攻击者能够:

绕过 SSL Pinning
Hook 网络库
直接构造请求
没有合法客户端证书,也无法完成 TLS 握手。

双向认证,除了单项认证的脚本以外,还需要导出证书, 找到密码,可以用ai, 抓包需要给抓包工具证书和域名,还要在开发阶段利用证书密码,写请求


sign 签名算法用 https://github.com/P4nda0s/IDA-NO-MCP/blob/main/INP.py ,ida 里面回车两次
导入项目,把java 饭编译和,这个的导出,都给ai 让ai 来写


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
双向认证注册用户脚本
使用P12证书进行mTLS认证

签名算法(从libsign.so逆向得出):
1. raw = "okhttp_expert_pro\n{method}\n{path}\n{query}\n{body}\n{timestamp}"
2. RC4加密(raw, timestamp作为密钥)
3. 转大写HEX
4. MD5
"""

import requests
import time
import hashlib
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
import ssl

# ============ 配置 ============
P12_PATH = "/home/laoli/code/fridacode/vpndetetor/ecdh/double/frida-script/kk.p12"
P12_PASSWORD = "okhttp_expert_pro_max"
BASE_URL = "https://nc.bin-lab.com:30791/api"
APP_ID = "okhttp_expert_pro"
APPID_MD5 = "922592dd3bedaafba7fb6bff0547f4a1"

# ============ SSL适配器(支持P12) ============
class Pkcs12Adapter(HTTPAdapter):
def __init__(self, pkcs12_path, pkcs12_password, **kwargs):
self.pkcs12_path = pkcs12_path
self.pkcs12_password = pkcs12_password
super().__init__(**kwargs)

def init_poolmanager(self, *args, **kwargs):
# 加载P12证书
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
import tempfile
import os

with open(self.pkcs12_path, 'rb') as f:
p12_data = f.read()

private_key, certificate, additional_certs = pkcs12.load_key_and_certificates(
p12_data,
self.pkcs12_password.encode()
)

# 保存临时文件
self.temp_cert = tempfile.NamedTemporaryFile(delete=False, suffix='.pem')
self.temp_key = tempfile.NamedTemporaryFile(delete=False, suffix='.pem')

# 写入证书
self.temp_cert.write(certificate.public_bytes(Encoding.PEM))
self.temp_cert.flush()

# 写入私钥
self.temp_key.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
self.temp_key.flush()

ctx = create_urllib3_context()
ctx.load_cert_chain(self.temp_cert.name, self.temp_key.name)

# 不验证服务器证书(如果需要可以改)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)


def rc4_encrypt_modified(data: bytes, key: bytes) -> bytes:
"""
变种RC4加密 (从libsign.so逆向)
标准RC4后每个字节再XOR 0xAA
"""
S = list(range(256))
j = 0

# KSA (Key-Scheduling Algorithm)
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

# PRGA (Pseudo-Random Generation Algorithm) + XOR 0xAA
i = j = 0
result = []
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
# 关键:标准RC4后再XOR 0xAA
result.append((byte ^ K) ^ 0xAA)

return bytes(result)


def md5_str(text: str) -> str:
"""计算MD5(字符串)"""
return hashlib.md5(text.encode()).hexdigest()


def generate_signature(method: str, path: str, query: str, body: str, timestamp: str) -> str:
"""
生成签名(从libsign.so逆向得出)

签名算法:
1. raw = "okhttp_expert_pro\n{method}\n{path}\n{query}\n{body}\n{timestamp}"
2. RC4加密(raw, timestamp作为密钥)
3. 转大写HEX
4. MD5
"""
# 构建原始数据
raw = f"{APP_ID}\n{method}\n{path}\n{query}\n{body}\n{timestamp}"

# 变种RC4加密 (RC4 + XOR 0xAA)
rc4_result = rc4_encrypt_modified(raw.encode(), timestamp.encode())

# 转大写HEX
hex_upper = rc4_result.hex().upper()

# MD5
signature = md5_str(hex_upper)

return signature


def register_user(session, username, password, operation=1):
"""注册单个用户"""
timestamp = str(int(time.time()))
passwd_md5 = hashlib.md5(password.encode()).hexdigest()

# 构建body字符串(URL编码格式)
body_str = f"username={username}&passwd={passwd_md5}&operation={operation}&appid={APPID_MD5}"

# 生成签名
# raw = "okhttp_expert_pro\n{method}\n{path}\n{query}\n{body}\n{timestamp}"
signature = generate_signature(
method="POST",
path="/api",
query="", # 无query参数
body=body_str,
timestamp=timestamp
)

headers = {
'Host': 'nc.bin-lab.com:30791',
'User-Agent': 'okhttp/4.12.0',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'x-app-id': APP_ID,
'x-timestamp': timestamp,
'x-signature': signature,
}

data = {
'username': username,
'passwd': passwd_md5,
'operation': str(operation),
'appid': APPID_MD5
}

try:
resp = session.post(BASE_URL, headers=headers, data=data, verify=False, timeout=30)
return resp.status_code, resp.text
except Exception as e:
return -1, str(e)


def main():
print("=" * 60)
print("双向认证用户注册脚本")
print(f"P12证书: {P12_PATH}")
print(f"目标URL: {BASE_URL}")
print("=" * 60)

# 创建session
session = requests.Session()
session.mount('https://', Pkcs12Adapter(P12_PATH, P12_PASSWORD))

# 禁用SSL警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

success_count = 0
fail_count = 0

# 注册100个用户
for i in range(1, 101):
username = f"testuser{i:03d}" # testuser001, testuser002, ...
password = f"pass{i:03d}123" # pass001123, pass002123, ...

print(f"\n[{i}/100] 注册用户: {username}")

status_code, response = register_user(session, username, password)

if status_code == 200:
print(f" ✓ 成功: {response[:100]}")
success_count += 1
else:
print(f" ✗ 失败 ({status_code}): {response[:100]}")
fail_count += 1

# 间隔1-2秒,避免太快
time.sleep(1.5)

print("\n" + "=" * 60)
print(f"注册完成! 成功: {success_count}, 失败: {fail_count}")
print("=" * 60)


def test_signature():
"""
用抓包数据测试签名算法
抓包数据:
- timestamp: 1766561494
- body: username=adminssd&passwd=098f6bcd4621d373cade4e832627b4f6&operation=1&appid=922592dd3bedaafba7fb6bff0547f4a1
- expected signature: 553cc253566e597e21181d4a7e43af1e
"""
timestamp = "1766561494"
body = "username=adminssd&passwd=098f6bcd4621d373cade4e832627b4f6&operation=1&appid=922592dd3bedaafba7fb6bff0547f4a1"
expected = "553cc253566e597e21181d4a7e43af1e"

signature = generate_signature(
method="POST",
path="/api",
query="",
body=body,
timestamp=timestamp
)

print(f"Expected: {expected}")
print(f"Calculated: {signature}")
print(f"Match: {signature == expected}")

# 打印raw字符串调试
raw = f"{APP_ID}\nPOST\n/api\n\n{body}\n{timestamp}"
print(f"\nRaw string:\n{repr(raw)}")


if __name__ == "__main__":
# 运行主程序,批量注册100个用户
main()


自编译 TLS 协议栈证书绑定逆向实验


ida 中shift + f2


字符串表里面 看到BoringSSL 下载源码
git clone https://github.com/google/boringssl

让ai 分析hook 点

看到源码有字符,优先搜索字符
./eDBG -p com.example.hello_world_app -l libflutter.so -vb 0x71ABFC -show-vertual

用eEBG 下断在函数入口,, 断上后, 给lr 继续下断点(ida 中看下,这个位置的代码),按c 继续, 看到了,这个函数返回值

可以开启抓包和不抓包两种测试,看返回不同

在ida 中 看lr 代码,修改汇编 MOV W0, #1  也见过

patch 后传到app 安装目录 /data/app/~~uC9GDZtSnqRNZJ5s9LYYMg==/com.example.hello_world_app-cG54CmWaa_JZ9CfLIjTZeg==/lib/arm64 授权 chmod + 777

重启app 成功