8点击蓝字
关注我们
声明
本文作者:Jammny
本文字数:8000
阅读时长:20分钟
附件/链接:点击查看原文下载
本文属于【狼组安全社区】原创奖励计划,未经许可禁止转载
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,狼组安全团队以及文章作者不为此承担任何责任。
狼组安全团队有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经狼组安全团队允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
❝
在 TLS 或 SSL 连接中,单向证书检验可以防止连接的另一端冒充其他身份。应用程序可以使用单向证书检验来验证连接的另一端是否是可信任的服务器。
❞
Security.framework 证书检验
SecTrustEvaluate()函数
该函数目前已经不被官方推荐,因为它仅适用于 IOS <13.0 的系统版本。函数原型:
OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result);
该函数的参数如下:
-
「trust」:要评估的信任管理对象。 -
「result」:返回时,指向反映此评估结果的结果类型。
SecTrustCreateWithCertificates() 函数
该函数在IOS>=13.0 的系统版本用于代替 SecTrustEvaluate() 完成 X.509 证书链评估。函数原型:
bool SecTrustEvaluateWithError(SecTrustRef trust, CFErrorRef _Nullable *error);
该函数的参数如下:
-
「trust」:指向 X.509 信任对象的指针。 -
「error」:指向用于存储错误信息的指针。
该函数的返回值为 0 表示成功,非 0 表示失败。 函数的使用示例:
#include <Security/Security.h>
int main() {
// 创建 X.509 信任对象
SecTrustRef trust = SecTrustCreateWithCertificates(NULL, NULL);
if (trust == NULL) {
return 1;
}
// 评估证书链
OSStatus status = SecTrustEvaluateWithError(trust, &error);
if (status != errSecSuccess) {
// 处理错误
CFStringRef errorStr = CFErrorCopyDescription(error);
printf("评估证书链失败:%sn", errorStr);
CFRelease(errorStr);
return 1;
}
// 释放资源
SecTrustRelease(trust);
return 0;
}
libboringssl.dylib 证书校验
libboringssl.dylib 库是 Apple 提供的开源安全库,用于实现 TLS 和 SSL 协议。该库基于 Google 的 BoringSSL 库,并经过 Apple 的修改和优化。
SSL_CTX_set_custom_verify() 函数
libboringssl.dylib 中 SSL_CTX_set_custom_verify() 函数用于设置 SSL 上下文的验证回调函数。在 OpenSSL < 1.1.1 的版本中,该函数原型为:
SSL_CTX_set_custom_verify(SSL_CTX *ctx, verify_callback, NULL);
该函数的参数如下:
-
「ctx」:指向 SSL 上下文的指针。 -
「verify_callback」:验证回调函数的指针。 -
「verify_arg」:验证回调函数的参数。
其中验证回调的函数为:
int verify_callback(SSL *ssl, int preverify_ok, X509_STORE_CTX *x509_store_ctx);
该函数的参数如下:
-
「ssl」:指向 SSL 结构的指针。 -
「preverify_ok」:表示是否已经完成了证书链的验证。 -
「x509_store_ctx」:指向 X.509 存储上下文的指针。
函数返回值为 0 表示成功,非 0 表示失败。 以下是该函数的使用示例:
#include <openssl/ssl.h>
int verify_callback(SSL *ssl, int preverify_ok, X509_STORE_CTX *x509_store_ctx) {
// 检查证书的颁发者
X509 *cert = X509_STORE_CTX_get_current_cert(x509_store_ctx);
X509_NAME *issuer = X509_get_issuer_name(cert);
char issuer_str[256];
X509_NAME_oneline(issuer, issuer_str, sizeof(issuer_str));
// 验证成功
if (preverify_ok) {
printf("证书颁发者:%sn", issuer_str);
return 0;
}
// 验证失败
printf("证书颁发者不受信任:%sn", issuer_str);
return 1;
}
int main() {
SSL_CTX *ctx = SSL_CTX_new(TLS_method());
if (ctx == NULL) {
return 1;
}
// 设置验证回调函数
SSL_CTX_set_custom_verify(ctx, verify_callback, NULL);
// ...
SSL_CTX_free(ctx);
return 0;
}
在 OpenSSL >= 1.1.1 的版本中,该函数原型为:
SSL_CTX_set_custom_verify(SSL_CTX *ctx, enum ssl_verify_mode_t, enum ssl_verify_result_t(*callback)(SSL *ssl, uint8_t *out_alert)))
函数增加了两个参数:
-
「mode」:表示验证模式。可以为以下值之一: -
「SSL_VERIFY_NONE」:不验证证书。 -
「SSL_VERIFY_PEER」:验证证书的身份。 -
「SSL_VERIFY_FAIL_IF_NO_PEER_CERT」:如果没有提供证书,则验证失败。 -
「SSL_VERIFY_CLIENT_ONCE」:仅在第一次握手时验证客户端证书。 -
「out_alert」:指向用于存储警报码的指针。
修改后的签名可以提供更灵活的验证控制。例如,可以使用 mode 参数来指定验证模式,并使用 out_alert 参数来指定验证失败时返回的警报码。 函数如果返回值是 int,为 0 表示成功,非 0 表示失败。如果失败,则可以通过 errno 变量来获取错误码。 如果返回值是 void,则表示函数成功执行。但是,在这种情况下,无法通过 errno 变量来获取错误码。
SSL_get_psk_identity() 函数
libboringssl.dylib 中 SSL_get_psk_identity() 函数用于获取 TLS-PSK 握手中使用的身份。该函数的参数如下:
-
「ssl」:指向 SSL 结构的指针。 -
「identity」:指向用于存储身份的指针。 -
「identity_len」:身份的长度。
该函数的返回值为 0 表示成功,非 0 表示失败。 以下是该函数的使用示例:
#include <openssl/ssl.h>
int main() {
SSL *ssl = SSL_new(ctx);
if (ssl == NULL) {
return 1;
}
// 获取身份
char identity[128];
int identity_len = sizeof(identity);
int ret = SSL_get_psk_identity(ssl, identity, &identity_len);
if (ret != 0) {
printf("获取身份失败:%dn", ret);
return 1;
}
// ...
SSL_free(ssl);
return 0;
}
boringssl_context_set_verify_mode_handle()函数
libboringssl.dylib 中 boringssl_context_set_verify_mode_handle()函数用于设置 SSL 上下文的验证模式。该函数的参数如下:
-
「ctx」:指向 SSL 上下文的指针。 -
「mode」:验证模式。可以为以下值之一: -
「SSL_VERIFY_NONE」:不验证证书。 -
「SSL_VERIFY_PEER」:验证证书的身份。 -
「SSL_VERIFY_FAIL_IF_NO_PEER_CERT」:如果没有提供证书,则验证失败。 -
「SSL_VERIFY_CLIENT_ONCE」:仅在第一次握手时验证客户端证书。
该函数的返回值为 0 表示成功,非 0 表示失败。 以下是该函数的使用示例:
#include <openssl/ssl.h>
int main() {
SSL_CTX *ctx = SSL_CTX_new(TLS_method());
if (ctx == NULL) {
return 1;
}
// 设置验证模式为 SSL_VERIFY_PEER
boringssl_context_set_verify_mode_handle(ctx, SSL_VERIFY_PEER);
// ...
SSL_CTX_free(ctx);
return 0;
}
在该示例中,我们首先创建了一个 SSL 上下文。然后,我们使用 boringssl_context_set_verify_mode_handle()函数将验证模式设置为 SSL_VERIFY_PEER。最后,我们释放 SSL 上下文。 以下是各个验证模式的说明:
-
「SSL_VERIFY_NONE」:不验证证书。如果服务器提供证书,则会接受该证书。如果服务器没有提供证书,则也会接受该连接。 -
「SSL_VERIFY_PEER」:验证证书的身份。如果服务器提供证书,则会验证该证书是否由受信任的 CA 颁发。如果验证失败,则会拒绝该连接。 -
「SSL_VERIFY_FAIL_IF_NO_PEER_CERT」:如果没有提供证书,则验证失败。如果服务器提供证书,则会验证该证书是否由受信任的 CA 颁发。 -
「SSL_VERIFY_CLIENT_ONCE」:仅在第一次握手时验证客户端证书。在以后的握手中,不会验证客户端证书。
Frida Hook SSL Pinning
根据上面学习的内容,可以简单实现绕过的通用脚本。主要思路就是替换证书检验相关函数的判断逻辑。
const log = {
"trace": function(msg){
console.log("x1b[35m", ` ${msg}`, "x1b[0m")
},
"debug": function(msg){
console.log("x1b[32m", ` ${msg}`, "x1b[0m")
},
"info": function(msg){
console.log("x1b[34m", ` ${msg}`, "x1b[0m")
},
"warn": function(msg){
console.log("x1b[33m", ` ${msg}`, "x1b[0m")
},
"error": function(msg){
console.log("x1b[31m", ` ${msg}`, "x1b[0m")
},
};
/** 绕过单向证书校验 */
function bypassSSLPinning() {
let bypass_status = false;
try {
/**
* SecTrustEvaluate:Security.framework的系统函数,iOS版本<13.0 可用它评估一个SecTrust对象是否可以被系统信任。
* 函数原型:OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result);
* 参考:https://developer.apple.com/documentation/security/1394363-sectrustevaluate?language=objc
*/
let SecTrustEvaluate_handle = Module.findExportByName('Security', 'SecTrustEvaluate'); // 如果存在,返回一个地址信息
if (SecTrustEvaluate_handle) {
Interceptor.replace(SecTrustEvaluate_handle, new NativeCallback(function (trust, result) {
return 0;
}, 'int', ['pointer', 'pointer']));
log.debug(`[${SecTrustEvaluate_handle}] SecTrustEvaluate hook installed.`);
bypass_status = true;
};
/**
* SecTrustEvaluateWithError:Security.framework的系统函数,iOS版本>=13.0 可用它评估一个SecTrust对象是否可以被系统信任。
* 函数原型:bool SecTrustEvaluateWithError(SecTrustRef trust, CFErrorRef _Nullable *error);
* 参考:https://developer.apple.com/documentation/security/2980705-sectrustevaluatewitherror?language=objc
*/
let SecTrustEvaluateWithError_handle = Module.findExportByName('Security', 'SecTrustEvaluateWithError');
if (SecTrustEvaluateWithError_handle) {
Interceptor.replace(SecTrustEvaluateWithError_handle, new NativeCallback(function (trust, error) {
return 1;
}, 'int', ['pointer', 'pointer']));
log.debug(`[${SecTrustEvaluateWithError_handle}] SecTrustEvaluateWithError hook installed.`);
bypass_status = true;
};
} catch (error) {
log.error(`[!] Exception: ${error}`);
};
try {
/**
* SSL_CTX_set_custom_verify:该函数用于覆盖默认的ssl验证逻辑,。
* OpenSSL < 1.1.1 ,函数原型:SSL_CTX_set_custom_verify(SSL_CTX *ctx, verify_callback, NULL);
* OpenSSL >= 1.1.1,函数原型:SSL_CTX_set_custom_verify(SSL_CTX *ctx, enum ssl_verify_mode_t, enum ssl_verify_result_t(*callback)(SSL *ssl, uint8_t *out_alert)))
* 第二个参数是选择检验模式,主要看第三个参数,它是一个回调函数,如果数值为0表示检验通过。
* 参考:https://www.cnblogs.com/theseventhson/p/16051195.html
*/
let SSL_CTX_set_custom_verify_handle = Module.findExportByName('libboringssl.dylib', 'SSL_CTX_set_custom_verify');
if (SSL_CTX_set_custom_verify_handle) {
let SSL_CTX_set_custom_verify = new NativeFunction(SSL_CTX_set_custom_verify_handle, 'void', ['pointer', 'int', 'pointer']);
let callback = new NativeCallback(function (ssl, out_alert) {
return 0; // 修改数值为0x0,表示检验通过
}, 'int', ['pointer', 'pointer']);
Interceptor.replace(SSL_CTX_set_custom_verify_handle, new NativeCallback(function (ctx, mode, ssl_verify_result_t) {
SSL_CTX_set_custom_verify(ctx, 0, callback);
}, 'void', ['pointer', 'int', 'pointer'])
);
log.debug(`[${SSL_CTX_set_custom_verify_handle}] SSL_CTX_set_custom_verify hook installed.`);
bypass_status = true;
};
/**
* SSL_get_psk_identity:用于获取 TLS-PSK 握手中使用的身份,该函数的返回值为 0 表示成功,非 0 表示失败。
* 函数原型:SSL_get_psk_identity(SSL_CTX *ctx, char identity, int identity_len);
* 参考:https://nabla-c0d3.github.io/blog/2019/05/18/ssl-kill-switch-for-ios12/
*/
let SSL_get_psk_identity_handle = Module.findExportByName('libboringssl.dylib', 'SSL_get_psk_identity');
if (SSL_get_psk_identity_handle) {
Interceptor.replace(SSL_get_psk_identity_handle, new NativeCallback(function (ctx, identity, identity_len) {
return 0;
}, 'int', ['pointer', 'char', 'int']));
log.debug(`[${SSL_get_psk_identity_handle}] SSL_get_psk_identity hook installed.`);
bypass_status = true;
};
/**
* boringssl_context_set_verify_mode:用于设置 SSL 上下文的验证模式,该函数的返回值为 0 表示成功,非 0 表示失败。
* 函数原型:boringssl_context_set_verify_mode_handle(SSL_CTX *ctx, mode);
* 参考:https://gist.github.com/azenla/37f941de24c5dfe46f3b8e93d94ce909
*/
let boringssl_context_set_verify_mode_handle = Module.findExportByName('libboringssl.dylib', 'boringssl_context_set_verify_mode');
if (boringssl_context_set_verify_mode_handle) {
Interceptor.replace(boringssl_context_set_verify_mode_handle, new NativeCallback(function (ctx, mode) {
return 0;
}, 'int', ['pointer', 'pointer'])
);
log.debug(`[${SSL_get_psk_identity_handle}] boringssl_context_set_verify_mode() hook installed.`);
bypass_status = true;
};
} catch (error) {
log.error(`[!] Exception: ${error}`);
};
if (bypass_status) {
log.info("[+] bypass ssl pinning.");
};
};
bypassSSLPinning();
「参考链接:」https://developer.apple.com/documentation/security/1394363-sectrustevaluate?language=objc
https://www.cnblogs.com/theseventhson/p/16051195.html
https://nabla-c0d3.github.io/blog/2019/05/18/ssl-kill-switch-for-ios12/
https://gist.github.com/azenla/37f941de24c5dfe46f3b8e93d94ce909
系列文章
作者
Jammny
假如你坚持一个月做同一件事情
那么它很快会成为你的习惯,学习亦是如此。
扫描关注公众号回复加群
和师傅们一起讨论研究~
长
按
关
注
WgpSec狼组安全团队
微信号:wgpsec
Twitter:@wgpsec
原文始发于微信公众号(WgpSec狼组安全团队):IOS安全 SSL Pinning 单向证书检验