背景
之前写过几篇https的流量的文章,但是很多部分都不完善,比如WinDivert+MITM实现https流量透明的实现是直接转发443端口的数据,且不是在本机做的代理,而且使用mitmproxy项目(tls相关的处理并没有介绍);利用Openssl写一个简陋的https劫持也是直接转发的443端口数据,并且在证书处理上还有很大的问题。
这是优化之后的代码
分析
那么目前就有两个问题:
1、将443的流量在本地处理。
2、识别当前tls hello的dnsName,返回包含DNS的ca证书,避免浏览器报错。
问题一
利用WinDivert的streamdump项目作本地代理,简单的画一下图,数据的传输方式如下所示
可以看到源端口本来应该和远程的目标端口创建一个socket连接,但是因为使用了WinDivet将网络的数据包进行了修改,使得能够在alt port发送数据和proxy port接受数据的时候修改和监控数据。对应的数据交换的代码如下。
static DWORD proxy_transfer_handler(LPVOID arg)
{
PPROXY_TRANSFER_CONFIG config = (PPROXY_TRANSFER_CONFIG)arg;
BOOL inbound = config->inbound;
SOCKET s = config->s, t = config->t;
char buf[8192];
int len, len2, i;
HANDLE console;
free(config);
while (TRUE)
{
// Read data from s.
len = recv(s, buf, sizeof(buf), 0);
if (len == SOCKET_ERROR)
{
warning("failed to recv from socket (%d)", WSAGetLastError());
shutdown(s, SD_BOTH);
shutdown(t, SD_BOTH);
return 0;
}
if (len == 0)
{
shutdown(s, SD_RECEIVE);
shutdown(t, SD_SEND);
return 0;
}
// Dump stream information to the screen.
console = GetStdHandle(STD_OUTPUT_HANDLE);
WaitForSingleObject(lock, INFINITE);
printf("[%.4d] ", len);
SetConsoleTextAttribute(console,
(inbound? FOREGROUND_RED: FOREGROUND_GREEN));
for (i = 0; i < len && i < MAX_LINE; i++)
{
putchar((isprint(buf[i])? buf[i]: '.'));
}
SetConsoleTextAttribute(console,
FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
printf("%s\n", (len > MAX_LINE? "...": ""));
ReleaseMutex(lock);
// Send data to t.
for (i = 0; i < len; )
{
len2 = send(t, buf+i, len-i, 0);
if (len2 == SOCKET_ERROR)
{
warning("failed to send to socket (%d)", WSAGetLastError());
shutdown(s, SD_BOTH);
shutdown(t, SD_BOTH);
return 0;
}
i += len2;
}
}
return 0;
}
好了现在本地代理流量的问题解决了。
问题二
首先是需要获得,目标访问时候的DNSname的值,之前的不标准解决方式是,直接peek第一次传输的数据,拿到tls hello中的dns name。
int size = recv(server_sock, buf, 0x100, MSG_PEEK);
这样确实能够获取到dns name,但是这样做感觉很拉跨,能不能先访问对应的ip指定之后获取到目标的dns name然后拷贝过来,于是有了下面的代码,通过目标x509证书得到对应的dns name。
GENERAL_NAMES* altNames = (GENERAL_NAMES*)X509_get_ext_d2i(server_x509, NID_subject_alt_name, NULL, NULL);
if (altNames)
{
GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null();
for (int i = 0; i < sk_GENERAL_NAME_num(altNames); i++)
{
char tmp[0x100] = { 0 };
GENERAL_NAME* altName = sk_GENERAL_NAME_value(altNames, i);
GENERAL_NAME* gen = GENERAL_NAME_new();
ASN1_IA5STRING* ia5 = ASN1_IA5STRING_new();
ASN1_STRING_set(ia5, ASN1_STRING_data(GENERAL_NAME_get0_value(altName, NULL)), strlen(ASN1_STRING_data(GENERAL_NAME_get0_value(altName, NULL))));
GENERAL_NAME_set0_value(gen, GEN_DNS, ia5);
sk_GENERAL_NAME_push(gens, gen);
//ASN1_IA5STRING_free(ia5);
//GENERAL_NAME_free(gen);
}
X509_add1_ext_i2d(*crt, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
GENERAL_NAMES_free(gens);
}
实际上访问百度的首页确实也能工作,但是当随便点击一个搜索之后会发现图片加载不出来,经过研究之后,发现返回的证书的dns name是cdn.xxx.com。正常网站为了能过快速的响应会购买cdn加速服务,而这里的代码是直接访问的cdn的ip,那么返回的就是cdn的证书,那cdn又是怎么正确的返回证书的呢?答案就在tls hello的时候会传 dns name,然后服务器根据对应的dns返回对应的证书。了解过ssl_accept的小伙伴已经发现了,在ssl_accept之前就已经初始化了服务器的证书,它又是在什么时候更换,或者自己实现的tls hello,而不是利用ssl相关的api呢?
SSL* server_ssl = Server_SSL_Init(serverNameCallback, pConnData);//https://stackoom.com/question/1Wr8z
if (NULL == server_ssl)//初始化ssl失败
{
SSL_Error("server_ssl error");
closesocket(s);
closesocket(t);
return 0;
}
SSL_set_fd(server_ssl, s);
int ret = SSL_accept(server_ssl);
if (ret <= 0)//接受数据失败
{
SSL_free(server_ssl);
closesocket(s);
closesocket(t);
printf("server_ssl SSL_accept error %d \n", SSL_get_error(server_ssl, ret));
return 0;
}
经过短暂的查找之后找到了对应的函数,原来ssl给我们提供了一个回调函数,可以在里面跟换对应的证书。
//设置回调函数
SSL_CTX_set_tlsext_servername_callback(server_ctx, callback);
//这里在设置回调的参数
SSL_CTX_set_tlsext_servername_arg(server_ctx, args);
更换证书的代码如下,在tls hello的时候首先向目标主机发送ssl_connect请求,获取到对应的x509证书中的dns name来更新当前的server证书,简单阅读一下代码,会发现不难理解。
int serverNameCallback(SSL* ssl, int* ad, void* arg)
{
PCONNDATA pConnData = (PCONNDATA)arg;
if (ssl == NULL)
{
SSL_Error("serverNameCallback ssl NULL");
return SSL_TLSEXT_ERR_NOACK;
}
//初始化一个ssl
SSL* client_ssl = Client_SSL_Init();
if (NULL == client_ssl)
{
SSL_shutdown(ssl);
SSL_Error((char*)"client_ssl error");
return SSL_TLSEXT_ERR_NOACK;
}
//指定ssl的dns name
const char* servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
if (servername)
{
//printf("dnsName %s \n", servername);
SSL_set_tlsext_host_name(client_ssl, servername);
}
//绑定连接的socket
SSL_set_fd(client_ssl, pConnData->t);
int ret = SSL_connect(client_ssl);
if (ret <= 0)
{
SSL_free(client_ssl);
SSL_shutdown(ssl);
SSL_Error("SSL_connect error");
return SSL_TLSEXT_ERR_NOACK;
}
//获取x509dns name指向的数据 https://blog.csdn.net/propro1314/article/details/72571807
X509* server_x509 = SSL_get_peer_certificate(client_ssl);
if (!server_x509)
{
SSL_shutdown(client_ssl);
SSL_free(client_ssl);
SSL_shutdown(ssl);
SSL_Error("Get server_x509 error");
return SSL_TLSEXT_ERR_NOACK;
}
EVP_PKEY* pKey = NULL;
X509* px509 = UpdateX509(server_x509, &pKey);
if (px509 && pKey)
{
SSL_set_SSL_CTX(ssl, GetSSLCTX(px509, pKey));//更新证书
EVP_PKEY_free(pKey);
X509_free(px509);
}
else
{
SSL_Error("UpdateX509 error");
}
X509_free(server_x509);
pConnData->client_ssl = client_ssl;
SetEvent(pConnData->hEvent);//设置有信号
return SSL_TLSEXT_ERR_OK;
}
返回一个正常的证书,这样浏览器就不会报证书异常(首先得安装ca的证书)。
数据交换不能使用之前的代码,这个代码是利用linux的自身支持的端口代理,socket处理上会出现各种乱七八糟连接异常。
void transfer(LPVOID argument)
{
fd_set fd_read;
timeval time_out;
char buffer[4096] = { 0 };
PTRANSFER pData = (PTRANSFER)argument;
int socket_to_client = SSL_get_fd(pData->client_ssl);
int socket_to_server = SSL_get_fd(pData->server_ssl);
while (TRUE)
{
int max;
FD_ZERO(&fd_read);
FD_SET(socket_to_server, &fd_read);
FD_SET(socket_to_client, &fd_read);
max = socket_to_client > socket_to_server ? socket_to_client + 1
: socket_to_server + 1;
int ret = select(max, &fd_read, NULL, NULL, &timeout);
if (ret < 0) {
SSL_Error("Fail to select!");
break;
}
else if (ret == 0) {
continue;
}
if (FD_ISSET(socket_to_client, &fd_read)) {
memset(buffer, 0, sizeof(buffer));
ret = SSL_read(pData->client_ssl, buffer, sizeof(buffer));
if (ret > 0) {
if (ret != SSL_write(pData->server_ssl, buffer, ret)) {
SSL_Error("Fail to write to server!");
break;
}
else {
//printf("%s\n", buffer);
}
}
else {
SSL_Error("Fail to read from client!");
break;
}
}
if (FD_ISSET(socket_to_server, &fd_read)) {
memset(buffer, 0, sizeof(buffer));
ret = SSL_read(pData->server_ssl, buffer, sizeof(buffer));
if (ret > 0) {
if (ret != SSL_write(pData->client_ssl, buffer, ret)) {
SSL_Error("Fail to write to client!");
break;
}
else {
//printf("%s\n", buffer);
}
}
else {
SSL_Error("Fail to read from server!");
break;
}
}
}
SSL_shutdown(pData->server_ssl);
SSL_shutdown(pData->client_ssl);
SSL_free(pData->server_ssl);
SSL_free(pData->client_ssl);
free(argument);
}
正常的使用方式是按照streamdump代码中的处理方式来过滤数据,别做多余的FD_SET操作
#define MAX_LINE 65
int transfers(PVOID argument)
{
int len, len2;
char buf[8192] = { 0 };
PTRANSFER pData = (PTRANSFER)argument;
BOOL inbound = pData->inbound;
SSL* client_ssl = pData->client_ssl;
SSL* server_ssl = pData->server_ssl;
free(argument);
while (TRUE)
{
//读取数据
len = SSL_read(client_ssl, buf, sizeof(buf));
if (len <= 0)
{
//SSL_Error("SSL_read failed");
SSL_shutdown(client_ssl);
SSL_shutdown(server_ssl);
return 0;
}
// Dump stream information to the screen.
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
WaitForSingleObject(lock, INFINITE);
printf("[%.4d] ", len);
SetConsoleTextAttribute(console, (inbound ? FOREGROUND_RED : FOREGROUND_GREEN));
for (int i = 0; i < len && i < MAX_LINE; i++)
{
if (isprint(buf[i]))
putchar(buf[i]);
else
printf(".");
}
SetConsoleTextAttribute(console, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
printf("%s\n", (len > MAX_LINE ? "..." : ""));
ReleaseMutex(lock);
//写数据
for (int i = 0; i < len; )
{
len2 = SSL_write(server_ssl, buf + i, len - i);
if (len2 <= 0)
{
//SSL_Error("SSL_write failed");
SSL_shutdown(server_ssl);
SSL_shutdown(client_ssl);
return 0;
}
i += len2;
}
}
return 0;
}
结果
最后chrome访问网站就正常了
36o浏览器会出现SSL_accept失败的情况,原因是它访问的是36oxxx.cn这里的证书校验不是用的根证书。