linux源碼解讀(三十三):android下boringSSL核心源碼解析&x音防抓包證書校驗原理


  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硬核抓包

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM