0%

绕过curl openssl证书校验

概述

某app使用了curl+openssl进行https通信,并开启了证书校验。
通过分析curl源码,定位到证书校验位置,Patch汇编代码,绕过证书校验。

原理

相关代码

SSL_CTX_set_verify定义:openssl/ssl/ssl_lib.c#L3551

1
2
3
4
5
6
void SSL_CTX_set_verify(SSL_CTX *ctx, int mode,
int (*cb) (int, X509_STORE_CTX *))
{
ctx->verify_mode = mode;
ctx->default_verify_callback = cb;
}

调用位置ossl_connect_step1curl/lib/vtls/openssl.c#L3200
相关变量verifypeercurl/lib/vtls/openssl.c#L2676

1
2
3
4
5
6
7
8
9
static CURLcode ossl_connect_step1(struct Curl_easy *data,
struct connectdata *conn, int sockindex)
{
// ...
const bool verifypeer = SSL_CONN_CONFIG(verifypeer);

// ...
SSL_CTX_set_verify(backend->ctx,
verifypeer ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);

SSL_CONN_CONFIGSSL_VERIFY_NONESSL_VERIFY_PEER

1
2
3
4
#define SSL_CONN_CONFIG(var) conn->ssl_config.var

# define SSL_VERIFY_NONE 0x00
# define SSL_VERIFY_PEER 0x01

即获取conn->ssl_config.verifypeer的值,设置给verifypeer变量。

ssl_primary_config结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ssl_primary_config {
long version; /* what version the client wants to use */
long version_max; /* max supported version the client wants to use*/
char *CApath; /* certificate dir (doesn't work on windows) */
char *CAfile; /* certificate to verify peer against */
char *issuercert; /* optional issuer certificate filename */
char *clientcert;
char *random_file; /* path to file containing "random" data */
char *egdsocket; /* path to file containing the EGD daemon socket */
char *cipher_list; /* list of ciphers to use */
char *cipher_list13; /* list of TLS 1.3 cipher suites to use */
char *pinned_key;
struct curl_blob *cert_blob;
struct curl_blob *ca_info_blob;
struct curl_blob *issuercert_blob;
char *curves; /* list of curves to use */
BIT(verifypeer); /* set TRUE if this is desired */
BIT(verifyhost); /* set TRUE if CN/SAN must match hostname */
BIT(verifystatus); /* set TRUE if certificate status must be checked */
BIT(sessionid); /* cache session IDs or not */
};

Patch思路

修改conn->ssl_config.verifypeer的值(只修改最低位),并Patch相关代码。

定位过程

原理

curl/lib/vtls/openssl.c#L3169

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(ssl_crlfile) {
/* tell OpenSSL where to find CRL file that is used to check certificate
* revocation */
lookup = X509_STORE_add_lookup(SSL_CTX_get_cert_store(backend->ctx),
X509_LOOKUP_file());
if(!lookup ||
(!X509_load_crl_file(lookup, ssl_crlfile, X509_FILETYPE_PEM)) ) {
failf(data, "error loading CRL file: %s", ssl_crlfile);
return CURLE_SSL_CRL_BADFILE;
}
/* Everything is fine. */
infof(data, "successfully loaded CRL file:");
X509_STORE_set_flags(SSL_CTX_get_cert_store(backend->ctx),
X509_V_FLAG_CRL_CHECK|X509_V_FLAG_CRL_CHECK_ALL);

infof(data, " CRLfile: %s", ssl_crlfile);
}

if(verifypeer)

操作

IDA搜索字符串CRLfile: %s,查看交叉引用,F5反编译,下一个if使用的变量即为verifypeer
反编译结果:

1
2
3
4
5
6
7
v14 = data;
infof(data, "successfully load CRL file:\n");
v193 = SSL_CTX_get_cert_store(*v302);
X509_STORE_set_flags(v193, 12LL);
infof(data, " CRLfile: %s\n", v289);
}
if ( (verifypeer & 1) != 0 )

将其重命名后,在IDA View查看变量verifypeer交叉引用位置,Patch即可。

Patch

赋值

原始指令:

1
2
68 43 49 39                   LDRB            W8, [X27,#0x250]
E8 5F 00 B9 STR W8, [SP,#0xAD0+verifypeer]

Patch指令:(动态调试可知,原始数据为0xb,最低位替换为0即为0xa

1
2
mov w8, #0xa
str w8, [x27, #0x250]

48 01 80 52 68 53 02 B9

引用

在字符串Cipher selection: %s引用位置下方,有LDR W9, [SP,#0xAD0+verifypeer]指令:

1
2
3
4
5
6
7
8
.text:0000000000089A00 01 0A 00 B0 21 08 33 91       ADRL            X1, aCipherSelectio     ; "Cipher selection: %s\n"
.text:0000000000089A08 E2 03 15 AA MOV X2, X21
.text:0000000000089A0C 1E 6A FF 97 BL infof
.text:0000000000089A0C
.text:0000000000089A10
.text:0000000000089A10 loc_89A10 ; CODE XREF: ossl_connect_step1_88194+1854↑j
.text:0000000000089A10 E1 3B 40 F9 LDR X1, [SP,#0xAD0+var_A60]
.text:0000000000089A14 E9 5F 40 B9 LDR W9, [SP,#0xAD0+verifypeer]

将其Patch为赋值语句即可。

1
49 01 80 52                             MOV             W9, #0xA

源码分析过程

一开始不知道SSL_CTX_set_verify这一函数,分析了连接的创建过程。在此记录一下。
查看docs\examples\https.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <curl/curl.h>

int main(void)
{
CURL *curl;
CURLcode res;

curl_global_init(CURL_GLOBAL_DEFAULT);

curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/");

#ifdef SKIP_PEER_VERIFICATION
/*
* If you want to connect to a site who is not using a certificate that is
* signed by one of the certs in the CA bundle you have, you can skip the
* verification of the server's certificate. This makes the connection
* A LOT LESS SECURE.
*
* If you have a CA cert for the server stored someplace else than in the
* default bundle, then the CURLOPT_CAPATH option might come handy for
* you.
*/
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif

#ifdef SKIP_HOSTNAME_VERIFICATION
/*
* If the site you are connecting to uses a different host name that what
* they have mentioned in their server certificate's commonName (or
* subjectAltName) fields, libcurl will refuse to connect. You can skip
* this check, but this will make the connection less secure.
*/
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#endif

/* Perform the request, res will get the return code */
res = curl_easy_perform(curl);
/* Check for errors */
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));

/* always cleanup */
curl_easy_cleanup(curl);
}

curl_global_cleanup();

return 0;
}

SKIP_PEER_VERIFICATION

调用链:curl_easy_setopt->Curl_vsetopt

1
2
3
4
5
6
7
8
9
CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
{
// ...
case CURLOPT_SSL_VERIFYPEER:
/*
* Enable peer SSL verifying.
*/
data->set.ssl.primary.verifypeer = (0 != va_arg(param, long)) ?
TRUE : FALSE;

实际上也是修改ssl_primary_config结构体。

curl_easy_perform

调用链:curl_easy_perform->easy_perform->easy_transfer->curl_multi_perform->multi_runsingle->Curl_connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static CURLMcode multi_runsingle(struct Curl_multi *multi,
struct curltime *nowp,
struct Curl_easy *data)
{
// ...
result = Curl_connect(data, &async, &protocol_connected);
if(CURLE_NO_CONNECTION_AVAILABLE == result) {
/* There was no connection available. We will go to the pending
state and wait for an available connection. */
multistate(data, MSTATE_PENDING);

/* add this handle to the list of connect-pending handles */
Curl_llist_insert_next(&multi->pending, multi->pending.tail, data,
&data->connect_queue);
result = CURLE_OK;
break;
}
else if(data->state.previouslypending) {
/* this transfer comes from the pending queue so try move another */
infof(data, "Transfer was pending, now try another");
process_pending_handles(data->multi);
}