栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

Https的流量透明

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Https的流量透明

背景

之前写过几篇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("%sn", (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("%sn", 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("%sn", 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("%sn", (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这里的证书校验不是用的根证书。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/963834.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号