概述
证书绑定即客户端在收到服务器的证书后,对该证书进行强校验,验证该证书是不是客户端承认的证书,如果不是,则直接断开连接。浏览器其实已经这样做了,但是如“前面”所说,选择权交给了用户,且浏览器由于其开放性允许让用户自导入自己的证书到受信任区域。但是在APP里面就不一样,APP是HTTPS的服务提供方自己开发的客户端,开发者可以先将自己服务器的证书打包内置到自己的APP中,或者将证书签名内置到APP中,当客户端在请求服务器建立连接期间收到服务器证书后,先使用内置的证书信息校验一下服务器证书是否合法,如果不合法,直接断开。
证书锁定(SSL/TLS Pinning)顾名思义,将服务器提供的SSL/TLS证书内置到移动端开发的APP客户端中,当客户端发起请求时,通过比对内置的证书和服务器端证书的内容,以确定这个连接的合法性。证书锁定需要把服务器的公钥证书(.crt 或者 .cer 等格式)提前下载并内置到App客户端中,创建TrustManager 时将公钥证书加进去。当请求发起时,通过比对证书内容来确定连接的合法性。
但由于证书存在过期时间,因此当服务器端证书更换时,需同时更换客户端证书。既然是要锁定证书,那么我们客户端上应该事先存在一个证书,我们才能锁定这个证书来验证我们真正的服务端,而不是代理工具伪造的服务端。如果是锁定证书,那通常情况下会将证书放置在app/asset目录下。
公钥锁定则需提取证书中的公钥内置到客户端中,通过比对公钥值来验证连接的合法性,由于证书更换依然可以保证公钥一致,所以公钥锁定不存在客户端频繁更换证书的问题。指 Client 端内置 Server 端真正的公钥证书。在 HTTPS 请求时,Server 端发给客户端的公钥证书必须与 Client 端内置的公钥证书一致,请求才会成功。
通过res/xml/network_security_config.xml配置文件对证书进行校验。对apk反编译后查看res/xml目录下的network_security_config.xml文件,打开看到<domain-config>标签,说明使用了证书绑定机制。生效范围:app全局,包含webview请求。
证书锁定
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
</domain-config>
</network-security-config>
(向右滑动,查看更多)
公钥锁定
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2099-01-01">
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
</domain-config>
</network-security-config>
(向右滑动,查看更多)
生效范围:配置了该参数的实例。
证书锁定
// 获取证书输入流
InputStream openRawResource = getApplicationContext().getResources().openRawResource(R.raw.bing);
Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(openRawResource);
// 创建 Keystore 包含我们的证书
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // 建议不要使用自己实现的X509TrustManager,而是使用默认的X509TrustManager
trustManagerFactory.init(keyStore);
// 用 TrustManager 初始化一个 SSLContext
sslContext = SSLContext.getInstance("TLS"); //定义:public static SSLContext sslContext = null;
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
OkHttpClient pClient2 = client.newBuilder().sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0]).build();
Request request2 = new Request.Builder()
.url("https://www.bing.com/?q=SSLPinningCAfile")
.build();
try (Response response2 = pClient2.newCall(request2).execute()) {
message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com success";
Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com success return code:"+response2.code());
} catch (IOException e) {
message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com failed";
Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com failed");
e.printStackTrace();
}
(向右滑动,查看更多)
公钥锁定
client = OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("xxxxxx.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build())
.build();
(向右滑动,查看更多)
javax.net.ssl.X509TrustManager接口被用来校验证书是否被信任。通常会校验 CA 是否为系统内置权威机构,证书有效期等。这个接口有三个方法,分别用来校验客户端证书、校验服务端证书、获取可信证书数组。其中我们重点关注checkServerTrusted方法。
// 该方法检查客户端的证书,若不信任该证书则抛出异常
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
... ...
}
// 该方法检查服务器的证书,若不信任该证书同样抛出异常
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
... ...
}
// 返回受信任的X509证书数组
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
xpose的justTrustme模块,也可以使用现成的frida hook脚本或使用objection,其核心都是hook HTTP请求库中用于校验证书的API,将结果返回正常。
Java.perform(function(){
let SecureX509TrustManager = Java.use("com.xxx.xxxx.xxxx.common.ssl.SecureX509TrustManager");
SecureX509TrustManager["checkServerTrusted"].implementation = function (x509CertificateArr, str) {
console.log('checkServerTrusted is called' + ', ' + 'x509CertificateArr: ' + x509CertificateArr + ', ' + 'str: ' + str);
return;
objection -g com.xxxx.xxxx explore -s "android sslpinning disable"
(向右滑动,查看更多)
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.verifyChain.implementation = function(untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log("[+] bypass success!");
return untrustedChain;
};
frida -U -f com.xxxx.xxx -l anti-ssl.js --no-pause
概述
public class SSLHelper {
/** * 存储客户端自己的密钥 */ private final static String CLIENT_PRI_KEY = "client.bks";
/** * 存储服务器的公钥 */ private final static String TRUSTSTORE_PUB_KEY = "publickey.bks";
/** * 读取密码 */ private final static String CLIENT_BKS_PASSWORD = "123321";
/** * 读取密码 */ private final static String PUCBLICKEY_BKS_PASSWORD = "123321";
private final static String KEYSTORE_TYPE = "BKS";
private final static String PROTOCOL_TYPE = "TLS";
private final static String CERTIFICATE_STANDARD = "X509";
public static SSLSocketFactory getSSLCertifcation(Context context) {
SSLSocketFactory sslSocketFactory = null;
try {
// 服务器端需要验证的客户端证书,其实就是客户端的keystore
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
// 客户端信任的服务器端证书
KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);
//读取证书
InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);
//加载证书
keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
trustStore.load(tsIn, PUCBLICKEY_BKS_PASSWORD.toCharArray());
//关闭流
ksIn.close();
tsIn.close();
//初始化SSLContext
SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_STANDARD);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_STANDARD);
trustManagerFactory.init(trustStore);
keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return sslSocketFactory;
}
}
.p12
.bks
.pfx
objection -g cn.soulapp.android explore --startup-command "android hooking watch class_method java.io.File.$init --dump-args
(向右滑动,查看更多)
.p12"
getAssets().open
Java.perform(function () {
console.log("start..... ");
var hook = Java.use('com.xxx.xxx.xxx');
console.log(hook);
hook.getValue.implementation=function(a){
console.log(a);
console.log(this.getValue(a));
return this.getValue(a);
}
});
logcat |grep flutter
static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
SSL_HANDSHAKE *hs,
uint8_t *out_alert) {
*out_alert = SSL_AD_INTERNAL_ERROR;
STACK_OF(X509) *const cert_chain = session->x509_chain;
if (cert_chain == nullptr || sk_X509_num(cert_chain) == 0) {
return false;
}
SSL *const ssl = hs->ssl;
SSL_CTX *ssl_ctx = ssl->ctx.get();
X509_STORE *verify_store = ssl_ctx->cert_store;
if (hs->config->cert->verify_store != nullptr) {
verify_store = hs->config->cert->verify_store;
}
X509 *leaf = sk_X509_value(cert_chain, 0);
const char *name;
size_t name_len;
SSL_get0_ech_name_override(ssl, &name, &name_len);
UniquePtr<X509_STORE_CTX> ctx(X509_STORE_CTX_new());
if (!ctx ||
!X509_STORE_CTX_init(ctx.get(), verify_store, leaf, cert_chain) ||
!X509_STORE_CTX_set_ex_data(ctx.get(),
SSL_get_ex_data_X509_STORE_CTX_idx(), ssl) ||
// We need to inherit the verify parameters. These can be determined by
// the context: if its a server it will verify SSL client certificates or
// vice versa.
!X509_STORE_CTX_set_default(ctx.get(),
ssl->server ? "ssl_client" : "ssl_server") ||
// Anything non-default in "param" should overwrite anything in the ctx.
!X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(ctx.get()),
hs->config->param) ||
// ClientHelloOuter connections use a different name.
(name_len != 0 &&
!X509_VERIFY_PARAM_set1_host(X509_STORE_CTX_get0_param(ctx.get()), name,
name_len))) {
OPENSSL_PUT_ERROR(SSL, ERR_R_X509_LIB);
return false;
}
if (hs->config->verify_callback) {
X509_STORE_CTX_set_verify_cb(ctx.get(), hs->config->verify_callback);
}
int verify_ret;
if (ssl_ctx->app_verify_callback != nullptr) {
verify_ret =
ssl_ctx->app_verify_callback(ctx.get(), ssl_ctx->app_verify_arg);
} else {
verify_ret = X509_verify_cert(ctx.get());
}
session->verify_result = X509_STORE_CTX_get_error(ctx.get());
// If |SSL_VERIFY_NONE|, the error is non-fatal, but we keep the result.
if (verify_ret <= 0 && hs->config->verify_mode != SSL_VERIFY_NONE) {
*out_alert = SSL_alert_from_verify_result(session->verify_result);
return false;
}
ERR_clear_error();
return true;
}
function hook_ssl() {
var base = Module.findBaseAddress("libflutter.so");
console.log("base: " + base);
var ssl_crypto_x509_session_verify_cert_chain = base.add(0x5c6b7c);
Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, {
onEnter: function(args) {
},
onLeave: function(retval) {
console.log("校验函数返回值: " + retval);
retval.replace(0x1);
}
});
}
frida -U xxxxx -l hook.js
精彩推荐