• 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("%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这里的证书校验不是用的根证书。

  • 相关阅读:
    简单例子windows 共享内存 Demo -----(一)
    Qt qss浅析
    基于EntityFramework的权限的配置和验证
    快速获取Windows系统上的国家和地区信息
    Scorm 1.2 开发文档
    SQL Server 联表字段合并查询
    解决 ko mapping 数组无法添加新对象的问题
    SQL Server 数据库初始化准备脚本
    妾心如水,良人不来
    有趣的格子效果
  • 原文地址:https://www.cnblogs.com/csnd/p/16675573.html
Copyright © 2020-2023  润新知