1、去年逆向x音15.5.0版本时,可以直接用fiddler抓包。后来貌似升级到17版本时fiddler就抓不到包了,看雪有大佬破解了x音防抓包的功能,原理并不复杂:boringssl源码中有个SSL_CTX_set_custom_verify函数,定义如下:
void SSL_CTX_set_custom_verify(
SSL_CTX *ctx, int mode,
enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert)) {
ctx->verify_mode = mode;
ctx->custom_verify_callback = callback;
}
(1)第二个mode参数就是验证client的关键参数了,有以下4种取值:
// SSL_VERIFY_NONE, on a client, verifies the server certificate but does not
// make errors fatal. The result may be checked with |SSL_get_verify_result|. On
// a server it does not request a client certificate. This is the default.
#define SSL_VERIFY_NONE 0x00
// SSL_VERIFY_PEER, on a client, makes server certificate errors fatal. On a
// server it requests a client certificate and makes errors fatal. However,
// anonymous clients are still allowed. See
// |SSL_VERIFY_FAIL_IF_NO_PEER_CERT|.
#define SSL_VERIFY_PEER 0x01
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT configures a server to reject connections if
// the client declines to send a certificate. This flag must be used together
// with |SSL_VERIFY_PEER|, otherwise it won't work.
#define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 0x02
// SSL_VERIFY_PEER_IF_NO_OBC configures a server to request a client certificate
// if and only if Channel ID is not negotiated.
#define SSL_VERIFY_PEER_IF_NO_OBC 0x04
从注释就能看出:
0x00:client要验证server的证书,但是不会报错;server不会要求client提供证书,这也是默认的参数
0x01:client和server双方都要验证对方的证书,并且会报错
0x02:如果client不提供证书,server可以拒绝连接;这个取值要和SSL_VERIFY_PEER一起配合使用,否则无效
0x04:server向client索要证书
x音默认情况下不能抓包是因为这个参数取值不是0x00,所以直接用frida hook SSL_CTX_set_custom_verify这个函数,把第二个参数改成0x00即可!也可以直接找到libttboringssl.so的源码把第二个参数写死成0x00;
(2)第三个参数callback从名字看就知道是个回调函数,函数返回值ssl_verify_result_t取值如下:
enum ssl_verify_result_t BORINGSSL_ENUM_INT {
ssl_verify_ok,
ssl_verify_invalid,
ssl_verify_retry,
};
从名字也能看出来返回值取第一个表示验证通过!直接通过hook把第三个参数改成0即可!如果觉得用frida hook麻烦,也可以在libsscronet.so偏移0x1CCBBE处,把“movs R0,1”改成“moves R0,0”即可!也就是把返回值从1改成0!
2、为了更好的逆向和ssl相关的功能(抓包、加解密等),有必要了解一些ssl的关键函数!
(1) 站在逆向的角度,我个人觉得最最最重要的就是SSL_write函数了,定义如下:从函数名和参数就能看出是从ssl发送buf的数据,发送长度是num!发送的数据存放在buf的,直接hook这个函数打印buf是不是就能看到网络数据了?
/*num字节从缓冲区buf写入指定的ssl连接*/
int SSL_write(SSL *ssl, const void *buf, int num) {
ssl_reset_error_state(ssl);
if (ssl->quic_method != nullptr) {
OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return -1;
}
if (ssl->do_handshake == NULL) {
OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED);
return -1;
}
int ret = 0;
bool needs_handshake = false;
do {
// If necessary, complete the handshake implicitly.
if (!ssl_can_write(ssl)) {//如果还不能通过这个ssl写数据
ret = SSL_do_handshake(ssl);//开始握手
if (ret < 0) {
return ret;
}
if (ret == 0) {
OPENSSL_PUT_ERROR(SSL, SSL_R_SSL_HANDSHAKE_FAILURE);
return -1;
}
}
//从这里发数据
ret = ssl->method->write_app_data(ssl, &needs_handshake,
(const uint8_t *)buf, num);
} while (needs_handshake);
return ret;
}
(2)既然SSL_write是发数据的,SSL_read岂不就是读数据的?代码如下:
int SSL_read(SSL *ssl, void *buf, int num) {
int ret = SSL_peek(ssl, buf, num);
if (ret <= 0) {
return ret;
}
// TODO(davidben): In DTLS, should the rest of the record be discarded? DTLS
// is not a stream. See https://crbug.com/boringssl/65.
ssl->s3->pending_app_data =
ssl->s3->pending_app_data.subspan(static_cast<size_t>(ret));
if (ssl->s3->pending_app_data.empty()) {
ssl->s3->read_buffer.DiscardConsumed();
}
return ret;
}
代码很简单,对于逆向人员来说,hook这两个函数是可以获取发送和接收数据的,也就是绕开了证书校验,对部分app是有用的!详细代码可以参考文章末尾第4个链接!
(3)通信双方最重要的莫过于密钥的协商了,handshake最重要的就是干这个的,整个方法如下;handshake内部最重要的又莫过于change_cipher_spec:为了保证安全,通信双方每隔一段时间就会改变加解密的参数!
int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) {
SSL *const ssl = hs->ssl;
for (;;) {
// Resolve the operation the handshake was waiting on. Each condition may
// halt the handshake by returning, or continue executing if the handshake
// may immediately proceed. Cases which halt the handshake can clear
// |hs->wait| to re-enter the state machine on the next iteration, or leave
// it set to keep the condition sticky.
/*handshake等待时可能有很多种情况:*/
switch (hs->wait) {
case ssl_hs_error://报错提示
ERR_restore_state(hs->error.get());
return -1;
case ssl_hs_flush: {//刷新缓存?
int ret = ssl->method->flush_flight(ssl);
if (ret <= 0) {
return ret;
}
break;
}
case ssl_hs_read_server_hello:
case ssl_hs_read_message:
/*为保证安全,每隔一段时间就需要改变加解密参数*/
case ssl_hs_read_change_cipher_spec: {
if (ssl->quic_method) {//双方用quic协议
// QUIC has no ChangeCipherSpec messages.
//quic本身比较简单,就没有改变加解密参数的说法
assert(hs->wait != ssl_hs_read_change_cipher_spec);
// The caller should call |SSL_provide_quic_data|. Clear |hs->wait| so
// the handshake can check if there is sufficient data next iteration.
ssl->s3->rwstate = SSL_ERROR_WANT_READ;
hs->wait = ssl_hs_ok;
return -1;
}
uint8_t alert = SSL_AD_DECODE_ERROR;
size_t consumed = 0;
ssl_open_record_t ret;
//现在的状态是要改变加解密参数
if (hs->wait == ssl_hs_read_change_cipher_spec) {
//开始和对方协商改变加解密参数
ret = ssl_open_change_cipher_spec(ssl, &consumed, &alert,
ssl->s3->read_buffer.span());
} else {
/*否则重新handshake;其实handshake的本质就是协商加解密协议和参数,
目的和change_cipher_spec没本质区别*/
ret = ssl_open_handshake(ssl, &consumed, &alert,
ssl->s3->read_buffer.span());
}
if (ret == ssl_open_record_error &&
hs->wait == ssl_hs_read_server_hello) {
uint32_t err = ERR_peek_error();
if (ERR_GET_LIB(err) == ERR_LIB_SSL &&
ERR_GET_REASON(err) == SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE) {
// Add a dedicated error code to the queue for a handshake_failure
// alert in response to ClientHello. This matches NSS's client
// behavior and gives a better error on a (probable) failure to
// negotiate initial parameters. Note: this error code comes after
// the original one.
//
// See https://crbug.com/446505.
OPENSSL_PUT_ERROR(SSL, SSL_R_HANDSHAKE_FAILURE_ON_CLIENT_HELLO);
}
}
bool retry;
int bio_ret = ssl_handle_open_record(ssl, &retry, ret, consumed, alert);
if (bio_ret <= 0) {
return bio_ret;
}
if (retry) {
continue;
}
ssl->s3->read_buffer.DiscardConsumed();
break;
}
case ssl_hs_read_end_of_early_data: {
if (ssl->s3->hs->can_early_read) {
// While we are processing early data, the handshake returns early.
*out_early_return = true;
return 1;
}
hs->wait = ssl_hs_ok;
break;
}
case ssl_hs_certificate_selection_pending:
ssl->s3->rwstate = SSL_ERROR_PENDING_CERTIFICATE;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_handoff:
ssl->s3->rwstate = SSL_ERROR_HANDOFF;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_handback: {
int ret = ssl->method->flush_flight(ssl);
if (ret <= 0) {
return ret;
}
ssl->s3->rwstate = SSL_ERROR_HANDBACK;
hs->wait = ssl_hs_handback;
return -1;
}
// The following cases are associated with callback APIs which expect to
// be called each time the state machine runs. Thus they set |hs->wait|
// to |ssl_hs_ok| so that, next time, we re-enter the state machine and
// call the callback again.
case ssl_hs_x509_lookup:
ssl->s3->rwstate = SSL_ERROR_WANT_X509_LOOKUP;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_private_key_operation:
ssl->s3->rwstate = SSL_ERROR_WANT_PRIVATE_KEY_OPERATION;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_pending_session:
ssl->s3->rwstate = SSL_ERROR_PENDING_SESSION;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_pending_ticket:
ssl->s3->rwstate = SSL_ERROR_PENDING_TICKET;
hs->wait = ssl_hs_ok;
return -1;
case ssl_hs_certificate_verify:
ssl->s3->rwstate = SSL_ERROR_WANT_CERTIFICATE_VERIFY;
hs->wait = ssl_hs_ok;//握手已成功
return -1;
case ssl_hs_early_data_rejected:
assert(ssl->s3->early_data_reason != ssl_early_data_unknown);
assert(!hs->can_early_write);
ssl->s3->rwstate = SSL_ERROR_EARLY_DATA_REJECTED;
return -1;
case ssl_hs_early_return:
if (!ssl->server) {
// On ECH reject, the handshake should never complete.
assert(ssl->s3->ech_status != ssl_ech_rejected);
}
*out_early_return = true;
hs->wait = ssl_hs_ok;
return 1;
case ssl_hs_hints_ready:
ssl->s3->rwstate = SSL_ERROR_HANDSHAKE_HINTS_READY;
return -1;
case ssl_hs_ok:
break;
}
// Run the state machine again.
hs->wait = ssl->do_handshake(hs);
if (hs->wait == ssl_hs_error) {
hs->error.reset(ERR_save_state());
return -1;
}
if (hs->wait == ssl_hs_ok) {
if (!ssl->server) {
// On ECH reject, the handshake should never complete.
assert(ssl->s3->ech_status != ssl_ech_rejected);
}
// The handshake has completed.
*out_early_return = false;
return 1;
}
// Otherwise, loop to the beginning and resolve what was blocking the
// handshake.
}
}
(4)还有个很不起眼、容易被忽视的函数:
void SSL_CTX_set_keylog_callback(SSL_CTX *ctx,
void (*cb)(const SSL *ssl, const char *line)) {
ctx->keylog_callback = cb;
}
从名字就能看出来是存放key日志的,里面记录的全是ssl协商的密钥!有了这些密钥,是不是就能解密双方通信的数据了?https://www.cnblogs.com/theseventhson/p/14618157.html 这是我之前在PC上用浏览器打开网页时做的操作,记录了ssl协议双方协商的密钥,然后wireshark就能用这些密钥解密数据了!但在android上默认是不记录这些的,需要手动hook来记录,js脚本代码如下:
function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
function keyLogger(ssl, line) {
console.log(new NativePointer(line).readCString());
}
const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']);
Interceptor.attach(SSL_CTX_new, {
onLeave: function(retval) {
const ssl = new NativePointer(retval);
const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
}
});
}
startTLSKeyLogger(
Module.findExportByName('libssl.so', 'SSL_CTX_new'),
Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
)
这是我hook x音的结果:
注意:这里抓的是libssl.so的keylog函数,也可以把libssl.so换成libttboringssl.so去获取x音的sslkey!具体操作方式可以参考文章末尾第6个!
参考:
1、https://bbs.pediy.com/thread-267940.htm android抓包整理归纳
2、https://onejane.github.io/2021/05/06/frida%E6%B2%99%E7%AE%B1%E8%87%AA%E5%90%90%E5%AE%9E%E7%8E%B0/#AOSP%E7%BD%91%E7%BB%9C%E5%BA%93%E8%87%AA%E5%90%90 frida沙箱自吐实现
3、https://bbs.pediy.com/thread-268014.htm 绕过非标准http框架和非系统ssl库app的sslpinning
4、https://blog.csdn.net/tzwsoho/article/details/119346275 [frida]拦截SSL_read/SSL_write函数获得HTTPS请求和响应
5、http://buaq.net/go-29171.html 关于抓包碎碎念
6、http://www.zhuoyue360.com/crack/73.html android硬核抓包
离线