方式一:采用python的certifi提供的根证书合集进行校验。
此方式校验宽松,基本所有的https均支持打开。
所以现在用.NET 也来实现的这样的一个功能了,共计三种方式。
1、极为宽松的请求策略。所有的HTTPS均可在软件内请求成功。
2、极为严格的请求策略,只允许一个或多个的域名的叶子证书哈希在软件内,才允许请求成功。
3、中等请求策略,仅允许一个或多个的域名根证书哈希在软件内,才允许请求成功(避免证书续签导致频繁维护)。
第二三种都是属于证书固定,用于防止抓包有较强的效果,但需维护。
以下是实现的具体代码段:
Imports System.IO
Imports System.Net
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Imports System.Text.RegularExpressions
Public Class Form1
Private Shared trustedCAs As New List(Of X509Certificate2)
Private Shared Function ReadPemBundleFromResource() As String
Dim pemBytes As Byte() = My.Resources.Resource1.TEMP
Return Encoding.ASCII.GetString(pemBytes)
End Function
Private Shared Function ParseCertificatesFromPem(pem As String) As List(Of X509Certificate2)
Dim list As New List(Of X509Certificate2)
Dim rx = New Regex("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----",
RegexOptions.Singleline Or RegexOptions.CultureInvariant)
For Each m As Match In rx.Matches(pem)
Dim base64 As String = m.Groups(1).Value.Replace(vbCr, "").Replace(vbLf, "").Trim()
Dim raw As Byte() = Convert.FromBase64String(base64)
list.Add(New X509Certificate2(raw))
Next
Return list
End Function
' 初始化 CA
Public Shared Sub InitCA()
Dim pem As String = ReadPemBundleFromResource()
trustedCAs = ParseCertificatesFromPem(pem)
If trustedCAs.Count = 0 Then
Throw New InvalidOperationException("未从资源解析到任何 CA 证书。")
End If
' 设置全局验证回调(也可改为 HttpClient 的实例级回调)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
ServicePointManager.ServerCertificateValidationCallback =
Function(sender, cert, chain, sslErrors)
Try
If sslErrors <> Net.Security.SslPolicyErrors.None Then
Return False
End If
For Each element In chain.ChainElements
For Each ca In trustedCAs
If String.Equals(element.Certificate.Thumbprint, ca.Thumbprint,
StringComparison.OrdinalIgnoreCase) Then
' 输出匹配的证书信息
MessageBox.Show($"匹配证书: {element.Certificate.Subject}{vbCrLf}指纹: {element.Certificate.Thumbprint}",
"匹配成功", MessageBoxButtons.OK, MessageBoxIcon.Information)
Return True
End If
Next
Next
Return False
Catch ex As Exception
Return False
End Try
End Function
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitCA()
End Sub
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Await Task.Run(Async Function()
Await DEMO()
End Function)
End Sub
Public Async Function DEMO() As Task(Of String)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
Dim url As String = $"https://www.jianshu.com/p/a8ddf16fabb2"
Try
Dim request As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
request.Method = "GET"
request.Timeout = 10000
request.KeepAlive = True
request.Accept = "application/json, text/plain, */*"
request.UserAgent = "Mozilla/5.0"
request.Referer = url
Using response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
Using reader As New StreamReader(response.GetResponseStream(), Encoding.UTF8)
Dim result As String = reader.ReadToEnd()
MsgBox(result)
End Using
End Using
Catch ex As Exception
If ex.Message = "基础连接已经关闭: 未能为 SSL/TLS 安全通道建立信任关系。" Then
MessageBox.Show($"NOT", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
MessageBox.Show($"请求异常:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Try
Return ""
End Function
End Class
注意,我的cacert.pem存放到了资源文件中,并命名为TEMP。
这个cacert.pem文件,则需要安装python环境,使用pip install certifi,通过下面的代码来获取。
import certifi
import os
import shutil
path = certifi.where()
script_dir = os.path.dirname(os.path.abspath(__file__))
target_path = os.path.join(script_dir, "TEMP")
shutil.copy(path, target_path)
print(f"证书已复制到: {target_path}")
input("按回车键退出...")
方式二:采用单域名或多域名进行校验
此方式计算远程站点的证书Leaf的值,进行校验。校验不通过,则请求拒绝。
优点:域名针对性极强。
缺点:此方式校验的是叶子证书的leaf值,校验较为风控,如果证书续签,您需要手动需要进行维护软件。
或者将获取TEMP文件的的方式,改为远程获取。
首先通过python代码获取输入域名并将叶子证书哈希,并输出为TEMP文件。
import argparse, base64, hashlib, socket, ssl, sys
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
def vb_compat_pubkey_bytes(cert_der: bytes) -> bytes:
cert = x509.load_der_x509_certificate(cert_der)
pub = cert.public_key()
from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519, ed448, dsa
if isinstance(pub, rsa.RSAPublicKey):
return pub.public_bytes(Encoding.DER, PublicFormat.PKCS1)
if isinstance(pub, ec.EllipticCurvePublicKey):
return pub.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
if isinstance(pub, ed25519.Ed25519PublicKey) or isinstance(pub, ed448.Ed448PublicKey):
return pub.public_bytes(Encoding.Raw, PublicFormat.RawPublicKey)
if isinstance(pub, dsa.DSAPublicKey):
spki = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
i = spki.rfind(b'\x03')
length = spki[i+1]
unused_bits = spki[i+2]
return spki[i+3:i+2+length]
return pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
def get_leaf_der(host: str, port: int = 443) -> bytes:
ctx = ssl.create_default_context()
with socket.create_connection((host, port), timeout=5.0) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
return ssock.getpeercert(True)
def calc_pin(host: str, port: int) -> str:
der = get_leaf_der(host, port)
raw = vb_compat_pubkey_bytes(der)
return base64.b64encode(hashlib.sha256(raw).digest()).decode()
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="计算多个域名的 pin,仅输出 pin 值,一行一个。")
ap.add_argument("hosts", help="多个域名,英文逗号分隔,如: example.com,foo.bar")
ap.add_argument("-p", "--port", type=int, default=443, help="端口(对所有域名生效),默认 443")
args = ap.parse_args()
hosts = [h.strip() for h in args.hosts.split(",") if h.strip()]
pins = []
for host in hosts:
try:
pin = calc_pin(host, args.port)
pins.append(pin)
except Exception as e:
# 错误信息仅写到 stderr,不影响 stdout 的纯 pin 输出和 TEMP 文件内容
sys.stderr.write(f"[ERROR] {host}: {e}\n")
# 标准输出:只打印 pin,每行一个
for pin in pins:
print(pin)
# 写入同目录 TEMP(仅 pin,每行一个;覆盖写入)
temp_path = Path(__file__).resolve().parent / "TEMP"
temp_path.write_text("\n".join(pins) + ("\n" if pins else ""), encoding="utf-8")
将哈希文件保存到资源文件中,并内存读取。
Imports System.IO
Imports System.Net
Imports System.Net.Security
Imports System.Security.Cryptography
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Public Class Form1
' 从资源读取 pin 列表(每行一个 Base64 的哈希)
Private Shared Function ReadPinsFromResource() As HashSet(Of String)
Dim txt As String = Encoding.ASCII.GetString(My.Resources.Resource1.TEMP) ' TEMP:文本文件,非PEM
Dim setPins As New HashSet(Of String)(StringComparer.Ordinal)
For Each line In txt.Split({ControlChars.Cr, ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries)
Dim pin = line.Trim()
If pin.Length > 0 Then setPins.Add(pin)
Next
Return setPins
End Function
Private Shared AllowedPins As HashSet(Of String)
' 零依赖版:仅对“公钥位串”做 SHA-256(不是严格SPKI,但兼容老框架、足够实用)
Private Shared Function SpkOnlySha256B64(cert As X509Certificate2) As String
Using sha = SHA256.Create()
Dim spk As Byte() = cert.PublicKey.EncodedKeyValue.RawData
Return Convert.ToBase64String(sha.ComputeHash(spk))
End Using
End Function
Private Shared Function ValidatePin(sender As Object,
certificate As X509Certificate,
chain As X509Chain,
sslErrors As SslPolicyErrors) As Boolean
Try
If sslErrors <> SslPolicyErrors.None OrElse certificate Is Nothing Then
Return False
End If
Dim leaf As New X509Certificate2(certificate)
' 1) 先校验 leaf
Dim leafPin = SpkOnlySha256B64(leaf)
If AllowedPins.Contains(leafPin) Then
Return True
End If
' 2) 再校验中间证书
If chain IsNot Nothing AndAlso chain.ChainElements IsNot Nothing Then
For i = 1 To chain.ChainElements.Count - 1 ' 跳过 0:leaf
Dim inter = chain.ChainElements(i).Certificate
Dim interPin = SpkOnlySha256B64(inter)
If AllowedPins.Contains(interPin) Then
Return True
End If
Next
End If
' 未命中任何 pin
Return False
Catch
Return False
End Try
End Function
' 初始化:加载 pins 并设置全局验证回调
Public Shared Sub InitCA()
AllowedPins = ReadPinsFromResource()
If AllowedPins Is Nothing OrElse AllowedPins.Count = 0 Then
Throw New InvalidOperationException("资源中未读取到任何 pins。请在 Resource1.TEMP 中按行填写 Base64 pin。")
End If
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
ServicePointManager.ServerCertificateValidationCallback = AddressOf ValidatePin
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitCA()
End Sub
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Await Task.Run(Async Function()
Await DEMO()
End Function)
End Sub
Public Async Function DEMO() As Threading.Tasks.Task(Of String)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
Dim url As String = "https://xxx.cn/test.php"
Try
Dim request As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
request.Method = "GET"
request.Timeout = 10000
request.KeepAlive = True
request.Accept = "application/json, text/plain, */*"
request.UserAgent = "Mozilla/5.0"
request.Referer = url
Using response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
Using reader As New StreamReader(response.GetResponseStream(), Encoding.UTF8)
Dim result As String = reader.ReadToEnd()
' TODO: 使用 result
End Using
End Using
MessageBox.Show("请求成功", "OK", MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As Exception
If ex.Message.Contains("未能为 SSL/TLS 安全通道建立信任关系") Then
MessageBox.Show("证书校验失败(可能被拦截或证书更换)", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
MessageBox.Show("请求异常:" & ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Try
Return ""
End Function
End Class
方式三:采用请求域名的根证书进行校验
这种也是我当前在用的一种方式。
根证书一般是不需要进行维护。域名续签也不会导致失效,比第一种方式严格(仅允许请求域名)但又比第二种宽松(同根证书下的站点都可以请求)
控制台程序(用以获取根证书的哈希):
Imports System.Net
Imports System.Net.Security
Imports System.Security.Cryptography
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Imports System.IO
Module Module1
Sub Main()
Console.OutputEncoding = Encoding.UTF8
Console.InputEncoding = Encoding.UTF8
Console.WriteLine("请输入 HTTPS 域名,多个用英文逗号分隔(例如:https://a.com, b.com):")
Dim input As String = Console.ReadLine().Trim()
If String.IsNullOrWhiteSpace(input) Then
Console.WriteLine("未输入任何域名,程序结束。")
Return
End If
Dim domains = input.Split({","c}, StringSplitOptions.RemoveEmptyEntries) _
.Select(Function(s) s.Trim()) _
.Where(Function(s) Not String.IsNullOrWhiteSpace(s)) _
.ToList()
If domains.Count = 0 Then
Console.WriteLine("没有有效的域名输入。")
Return
End If
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
Dim sb As New StringBuilder()
For Each domain In domains
Dim url As String = domain
If Not url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) Then
url = "https://" & url
End If
Console.WriteLine(vbCrLf & "处理: " & url)
Try
Dim leaf As X509Certificate2 = GetServerCertificate(url)
If leaf Is Nothing Then
Console.WriteLine("⚠️ 无法获取服务器证书。")
Continue For
End If
Dim anchor As X509Certificate2 = GetTrustAnchor(leaf)
If anchor Is Nothing Then
Console.WriteLine("⚠️ 无法构建证书链或未找到根证书。")
Continue For
End If
Dim pin As String = SpkOnlySha256B64(anchor)
Console.WriteLine("根证书: " & anchor.Subject)
Console.WriteLine("颁发者: " & anchor.Issuer)
Console.WriteLine("✅ Root Pin: " & pin)
sb.AppendLine(pin)
Catch ex As Exception
Console.WriteLine("❌ 错误:" & ex.Message)
End Try
Next
' 写入输出文件(只有 pin)
Dim outputPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "root_pins.txt")
File.WriteAllText(outputPath, sb.ToString().Trim(), Encoding.UTF8)
Console.WriteLine(vbCrLf & "✅ 已保存纯 pin 到文件: " & outputPath)
Console.WriteLine("按任意键退出...")
Console.ReadKey()
End Sub
Function SpkOnlySha256B64(cert As X509Certificate2) As String
Using sha256 As SHA256 = SHA256.Create()
Dim spk As Byte() = cert.PublicKey.EncodedKeyValue.RawData
Return Convert.ToBase64String(sha256.ComputeHash(spk))
End Using
End Function
' 构建证书链并返回信任锚(最后一个元素,通常是系统信任库中的根证书)。失败返回 Nothing。
Function GetTrustAnchor(leaf As X509Certificate2) As X509Certificate2
Dim ch As New X509Chain()
ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck ' 需要时可改为 Online
ch.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot
ch.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag
ch.ChainPolicy.VerificationTime = DateTime.UtcNow
If Not ch.Build(leaf) OrElse ch.ChainElements Is Nothing OrElse ch.ChainElements.Count = 0 Then
Return Nothing
End If
Return ch.ChainElements(ch.ChainElements.Count - 1).Certificate
End Function
' 发起一次 TLS 握手以获取服务器返回的叶子证书
Function GetServerCertificate(url As String) As X509Certificate2
Dim cert As X509Certificate2 = Nothing
Dim oldHandler As RemoteCertificateValidationCallback = ServicePointManager.ServerCertificateValidationCallback
' 暂时接受任何证书,以便拿到服务器发来的叶子证书
ServicePointManager.ServerCertificateValidationCallback =
Function(sender, certificate, chain, sslPolicyErrors)
If certificate IsNot Nothing Then
cert = New X509Certificate2(certificate)
End If
Return True
End Function
Try
Try
Dim reqHead As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
reqHead.Method = "HEAD"
reqHead.Timeout = 5000
Using resp As HttpWebResponse = CType(reqHead.GetResponse(), HttpWebResponse)
' 仅用于握手
End Using
Catch
Dim reqGet As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
reqGet.Method = "GET"
reqGet.Timeout = 5000
reqGet.UserAgent = "Mozilla/5.0"
reqGet.Accept = "*/*"
reqGet.ReadWriteTimeout = 5000
' 读取最少量数据即可触发握手
Using resp As HttpWebResponse = CType(reqGet.GetResponse(), HttpWebResponse)
Using s As Stream = resp.GetResponseStream()
Dim buffer(0) As Byte
s.ReadTimeout = 2000
' 尝试读取 1 字节,不关心结果
s.Read(buffer, 0, 1)
End Using
End Using
End Try
Finally
ServicePointManager.ServerCertificateValidationCallback = oldHandler
End Try
Return cert
End Function
End Module
输出的哈希,放至远程服务器,得到链接,打开软件读取哈希,并根据远程哈希值,进行校验。
注意,远程的地址内容,建议加密一下,在软件读取时再进行解密,不然还是能拦截到请求的,并进行伪造请求内容。
Imports System.IO
Imports System.Net
Imports System.Net.Http
Imports System.Net.Security
Imports System.Security.Cryptography
Imports System.Security.Cryptography.X509Certificates
Imports System.Text
Imports System.Threading.Tasks
Public Class Form1
' 远程 pin 列表地址(每行一个 Base64 的 SHA-256 值)
Private Const PinsUrl As String = "https://test.com/test.txt" '此域名为根证书的公钥哈希,一行一个。
Private Shared AllowedPins As HashSet(Of String)
' 仅对“公钥位串”做 SHA-256(不是严格 SPKI,但兼容老框架)
Private Shared Function SpkOnlySha256B64(cert As X509Certificate2) As String
Using sha = SHA256.Create()
Dim spk As Byte() = cert.PublicKey.EncodedKeyValue.RawData
Return Convert.ToBase64String(sha.ComputeHash(spk))
End Using
End Function
' 从远程下载 pin 列表(此阶段未安装 pin 回调 → 不做 pin 校验)
Private Shared Async Function ReadPinsFromRemoteAsync() As Task(Of HashSet(Of String))
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
' 使用默认处理程序即可:只做系统层面的 TLS 校验,不做 pin
Dim handler As New HttpClientHandler() ' .NET Framework 下,未设置自定义回调时不触发 pin
Using client As New HttpClient(handler)
client.Timeout = TimeSpan.FromSeconds(10)
Dim txt As String = Await client.GetStringAsync(PinsUrl).ConfigureAwait(False)
Dim setPins As New HashSet(Of String)(StringComparer.Ordinal)
For Each line In txt.Split({ControlChars.Cr, ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries)
Dim pin = line.Trim()
If pin.Length > 0 Then setPins.Add(pin)
Next
Return setPins
End Using
End Function
' 构建证书链并返回信任锚(最后一个元素,通常是系统信任库中的根证书)。失败返回 Nothing。
Private Shared Function GetTrustAnchor(leaf As X509Certificate2) As X509Certificate2
Dim ch As New X509Chain()
ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck ' 生产可改为 Online
ch.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot
ch.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag
ch.ChainPolicy.VerificationTime = DateTime.UtcNow
If Not ch.Build(leaf) OrElse ch.ChainElements Is Nothing OrElse ch.ChainElements.Count = 0 Then
Return Nothing
End If
' 最后一个就是信任锚(根)
Return ch.ChainElements(ch.ChainElements.Count - 1).Certificate
End Function
' 证书 pin 校验回调 —— 只校验“根”的公钥哈希
Private Shared Function ValidatePin(sender As Object,
certificate As X509Certificate,
chain As X509Chain,
sslErrors As SslPolicyErrors) As Boolean
Try
If sslErrors <> SslPolicyErrors.None OrElse certificate Is Nothing Then
Return False
End If
If AllowedPins Is Nothing OrElse AllowedPins.Count = 0 Then
Return False
End If
Dim leaf As New X509Certificate2(certificate)
Dim anchor As X509Certificate2 = GetTrustAnchor(leaf)
If anchor Is Nothing Then
Return False
End If
Dim rootPin As String = SpkOnlySha256B64(anchor)
Return AllowedPins.Contains(rootPin)
Catch
Return False
End Try
End Function
Public Shared Async Function InitCAAsync() As Task
' 先下载 pin(此时尚未设置 ServerCertificateValidationCallback)
AllowedPins = Await ReadPinsFromRemoteAsync().ConfigureAwait(False)
If AllowedPins Is Nothing OrElse AllowedPins.Count = 0 Then
Throw New InvalidOperationException("未从远程获取到任何 pins。请检查 " & PinsUrl)
End If
' 再安装全局回调:此后所有 TLS 请求才开始做 pin 校验
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
ServicePointManager.ServerCertificateValidationCallback = AddressOf ValidatePin
End Function
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Try
Me.Text = "获取配置中.."
Await Task.Run(Async Function()
Await InitCAAsync()
End Function)
Me.Text = "配置获取完成 - 防抓包已开启"
Catch ex As Exception
MessageBox.Show("初始化失败:" & ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
End
End Try
End Sub
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Await Task.Run(Async Function()
Await DEMO()
End Function)
End Sub
Public Async Function DEMO() As Task(Of String)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
Dim url As String = "https://xxx.cn/test.php"
Try
Dim request As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
request.Method = "GET"
request.Timeout = 10000
request.KeepAlive = True
request.Accept = "application/json, text/plain, */*"
request.UserAgent = "Mozilla/5.0"
request.Referer = url
Using response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
Using reader As New StreamReader(response.GetResponseStream(), Encoding.UTF8)
Dim result As String = reader.ReadToEnd()
MessageBox.Show(result, "OK", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Using
End Using
Catch ex As Exception
If ex.Message.Contains("未能为 SSL/TLS 安全通道建立信任关系") Then
MessageBox.Show("This request has been blocked.", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
MessageBox.Show("请求异常:" & ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Try
Return ""
End Function
End Class
使用方式三的示例:
可测下防抓包强度,已加DNG壳,但我获取哈希时并未进行加密,是可以通过抓包工具,首先替换请求中的哈希,应该是可以突破的,在具体生产中,是必须要进行加密的。但如果进行突破,就算没有对远程地址进行加密,也并不是点几下鼠标就可以搞定的事情。