给你的网络流量穿件“隐形衣“:手把手教你用对称加密打造透明安全隧道

在网络通信中,我们日常用的TCP、UDP协议本身是不加密的------就像寄快递时不封箱,路上任何人都能拆开看里面的内容。比如远程连接、文件传输时,数据明文传输很容易被窃取、篡改。而TCP/UDP对称加密隧道封装器,就是一个"自动封箱工具":不用修改原有任何程序的代码,就能给所有通过TCP/UDP传输的数据自动加密,让数据变成"乱码",只有持有正确"钥匙"的接收方才能解密,相当于给网络通信加了一层安全防护盾。

它的核心优势很简单:轻量、通用、无侵入。不管是现成的聊天工具、远程控制软件,还是自己写的简单网络程序,只要是用TCP或UDP协议通信,它都能"套上"加密层,不用开发者额外写一行加密代码,大大降低了网络安全开发的门槛。

一、先搞懂:这个"加密封装器"到底是什么?

用大白话讲,这个工具就是一个"中间拦截器",夹在应用程序和系统网络之间。当应用程序要发送数据时,它先把数据拦截下来,加密后再交给系统发送出去;当应用程序要接收数据时,它先把系统收到的加密数据拦截下来,解密后再交给应用程序。

对于应用程序来说,完全感觉不到它的存在------该怎么发送、接收数据,还是怎么操作,只是背后多了一道加密和解密的工序。而对于网络传输来说,传递的不再是明文数据,而是加密后的"密文",就算被截获,没有正确的密钥也无法破解,从而保证数据的机密性和完整性。

举个通俗的例子:你用聊天软件发消息,原本是"明文直发",中途任何人都能看到消息内容;有了这个封装器后,你发的消息会被自动加密成乱码,传到对方那边后,再自动解密成正常消息,全程你和对方都不用做任何额外操作,却实现了消息保密。

二、设计思路:怎么做到"无侵入"加密?

这个封装器的设计核心,就是"挂钩系统网络调用",不用改动应用程序本身,而是从系统层面拦截数据,完成加密和解密。具体思路可以分成3步,非常好理解:

  1. 拦截关键操作:我们知道,任何应用程序要通过TCP/UDP发数据,都会调用系统的两个核心函数------发送函数(send/sendto)和接收函数(recv/recvfrom)。封装器的第一步,就是"挂钩"这四个函数,相当于在这四个函数的门口放了一个"拦截员",所有数据的发送和接收,都要先经过这个拦截员。
  2. 自动加解密:发送数据时,拦截员先把应用程序要发的明文数据截下来,用提前约定好的加密算法和密钥加密,变成密文后,再交给系统的发送函数发送;接收数据时,拦截员先把系统收到的密文截下来,用同样的密钥和解密算法解密,变成明文后,再交给应用程序。
  3. 简化配置,降低门槛:设计时特意做了"极简配置",用户只需要设置一个密码(密钥),就能直接使用,不用懂复杂的加密算法原理;同时支持自动生成加密所需的辅助参数,尽量减少用户的操作成本,让非专业人士也能轻松上手。
    补充一个关键设计细节:为了保证加密的安全性和效率,它采用了"对称加密"(加密和解密用同一把密钥),同时加入了"身份验证"------每一个加密后的数据包,都会附带一个"验证标签",接收方可以通过这个标签判断数据是否被篡改,避免收到伪造的恶意数据。

三、代码实现原理:手把手解读核心逻辑

c 复制代码
...
static void gen_key(void) {
	char *key_var = getenv(KEY_VAR);

	if (key_var) {
		PKCS5_PBKDF2_HMAC_SHA1(key_var,strlen(key_var),(const unsigned char *)KEY_SALT,strlen(KEY_SALT),ITERATIONS,KEY_SIZE,(unsigned char *)glob_key);
	} else {
		PKCS5_PBKDF2_HMAC_SHA1(PASSPHRASE,strlen(PASSPHRASE),(const unsigned char *)KEY_SALT,strlen(KEY_SALT),ITERATIONS,KEY_SIZE,(unsigned char *)glob_key);
	}
}

static int encrypt_data(char *in, int len, char *out) {
...

	memset(temp,0x00,MAX_LEN);
	memcpy(temp,in,len);
	
	if (glob_key[0] == 0x00) 
		gen_key(); 
	RAND_bytes(iv,IV_SIZE); 
	
	EVP_CIPHER_CTX *ctx;
	ctx = EVP_CIPHER_CTX_new();
	EVP_CIPHER_CTX_init (ctx);
	EVP_EncryptInit_ex (ctx, EVP_aes_256_gcm() , NULL, (const unsigned char *)glob_key, (const unsigned char *)iv);

	if (!EVP_EncryptUpdate (ctx, outbuf, &outlen, (const unsigned char *)temp, len)) {
		fprintf(stderr, "[!] Error in EVP_EncryptUpdate()\n");
		EVP_CIPHER_CTX_cleanup (ctx);
		return 0;
	}

	if (!EVP_EncryptFinal_ex (ctx, outbuf + outlen, &tmplen)) {
		fprintf(stderr, "[!] Error in EVP_EncryptFinal_ex()\n");
		EVP_CIPHER_CTX_cleanup (ctx);
		return 0;
	}
	
	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);
	

	out[0]=PACKET_HEADER;
	// Pack full packet length
	out[1]=(0xff00&(len+HEADER_SIZE))>>8;
	out[2]=(0xff&(len+HEADER_SIZE));
	step=(unsigned char *)&out[3];	
	memcpy(step,iv,IV_SIZE);
	step+=IV_SIZE;
	memcpy(step,tag,sizeof(tag));
	step+=sizeof(tag);
	memcpy(step,outbuf,outlen+tmplen);
	
	EVP_CIPHER_CTX_cleanup (ctx);
	return outlen+tmplen+HEADER_SIZE;
}

static int decrypt_data(char *in, int len, char *out) {
...
	if (glob_key[0] == 0x00)  
		gen_key(); 
	
	EVP_CIPHER_CTX *ctx;
	ctx = EVP_CIPHER_CTX_new();
	EVP_CIPHER_CTX_init (ctx);
	EVP_DecryptInit_ex (ctx, EVP_aes_256_gcm() , NULL, (const unsigned char *)glob_key, (const unsigned char *)iv);
	
	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, IV_SIZE, NULL);
	
	if (!EVP_DecryptUpdate (ctx, outbuf, &outlen, (const unsigned char *)step, len)) {
		fprintf(stderr, "[!] Error in EVP_DecryptUpdate()\n");
		EVP_CIPHER_CTX_cleanup (ctx);
		return 0;
	}

	EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, sizeof(tag), tag);

	if (!EVP_DecryptFinal_ex (ctx, outbuf + outlen, &tmplen)) {
		fprintf(stderr, "[!] Error in EVP_DecryptFinal_ex(). Possible foul play involved.\n");
		EVP_CIPHER_CTX_cleanup (ctx);
		return 0;
	}
	
	EVP_CIPHER_CTX_cleanup (ctx);
	
	memcpy(out,outbuf,outlen+tmplen);
	
	return len;
}

ssize_t recv(int sockfd, void *buf, size_t len, int flags) {
	char outbuf[MAX_LEN];
	unsigned char temp[MAX_LEN];
	
	int outlen, ret, packet_len;
	
	memset(outbuf,0x00,MAX_LEN);
	memset(temp,0x00,MAX_LEN);
	
	if (!old_recv)
		old_recv = dlsym(RTLD_NEXT,"recv");
		
	if (sockfd == 0) 
		return old_recv(sockfd, buf, len, flags);
	
	ret = old_recv(sockfd, (void *)temp, 3, MSG_PEEK);
	
	if (ret < 1) { 
		return ret;
	}

	if (temp[0] != PACKET_HEADER) {
		fprintf(stderr,"[!] Client not using CryptHook\n");
		return 0;
	}
	packet_len = (temp[1]<<8)+temp[2];

	ret = old_recv(sockfd, (void *)temp, packet_len, flags);

	outlen = decrypt_data((char *)temp,ret-HEADER_SIZE,&outbuf[0]);

	memcpy((void*)buf,(void*)outbuf,(size_t)outlen);
	
	return outlen;
}

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) {	
	char outbuf[MAX_LEN];
	unsigned char temp[MAX_LEN];
	
	int outlen, ret, packet_len;
	
	memset(outbuf,0x00,MAX_LEN);
	memset(temp,0x00,MAX_LEN);
	
	if (!old_recvfrom)
		old_recvfrom = dlsym(RTLD_NEXT,"recvfrom");
		
	if (sockfd == 0) 
		return old_recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
	

	ret = old_recvfrom(sockfd, (void *)temp, 3, MSG_PEEK, src_addr, addrlen);

	if (ret < 1) {
		return ret;
	}

	if (temp[0] != PACKET_HEADER) {
		fprintf(stderr,"[!] Client not using same crypto algorithm\n");
		return 0;
	}
	// Unpack the full message length
	packet_len = (temp[1]<<8)+temp[2];
	
	ret = old_recvfrom(sockfd, (void *)temp, packet_len, flags, src_addr, addrlen);
	outlen = decrypt_data((char *)temp,ret-HEADER_SIZE,&outbuf[0]);

	memcpy((void*)buf,(void*)outbuf,(size_t)outlen);
	
	return outlen;
}

ssize_t send(int sockfd, const void *buf, size_t len, int flags) {
	char outbuf[MAX_LEN];
	int outlen;
	
	memset(outbuf,0x00,MAX_LEN);
	
	if (!old_send)
		old_send = dlsym(RTLD_NEXT,"send");
		
	outlen = encrypt_data((char *)buf, len, &outbuf[0]);
	if (outlen == 0)
		return 0;
		

	old_send(sockfd, (void *)outbuf, outlen, flags);

	return len; 
}


ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) {
	char outbuf[MAX_LEN];
	int outlen;
	
	memset(outbuf,0x00,MAX_LEN);
	
	if (!old_sendto)
		old_sendto = dlsym(RTLD_NEXT,"sendto");
		
	outlen = encrypt_data((char *)buf, len, &outbuf[0]);
	if (outlen == 0)
		return 0;
		
	old_sendto(sockfd, (void *)outbuf, outlen, flags, dest_addr, addrlen);

	return len; 
}

If you need the complete source code, please add the WeChat number (c17865354792)

下面结合代码,用最通俗的话解读核心实现步骤,不用纠结复杂的语法,重点看"流程",我们还会用一个简单的流程图,帮大家理清逻辑。

(一)核心前提:准备工作

代码首先做了3件基础准备,相当于给"拦截员"准备好工具和规则:

  • 定义关键参数:比如加密密钥的长度(32字节,对应AES-256加密)、初始化向量(IV)的长度(12字节,用于增强加密安全性)、数据包的头部标识(用于区分加密数据和普通数据),还有默认密码和加密迭代次数等。
  • 关联系统原始函数:因为要"挂钩"系统的send/recv等函数,所以先定义了几个指针,指向这些系统函数的原始地址------相当于拦截员先找到"真正的大门",之后才能在门口拦截,拦截完成后还要通过这个大门把数据送出去。
  • 依赖加密库:用到了OpenSSL加密库,里面已经封装好了成熟的AES加密算法,不用自己从零写加密逻辑,直接调用即可,降低开发难度。

(二)核心功能1:生成加密密钥

加密和解密需要同一把密钥,代码里的密钥生成逻辑很灵活,大白话解读如下:

  1. 用户可以通过环境变量设置一个密码(比如"donthackmebro"),如果没设置,就用默认密码;
  2. 用PBKDF2算法,把用户设置的简单密码,结合"盐值"(一个随机字符串,防止相同密码生成相同密钥)和迭代次数,生成一个32字节的密钥(AES-256所需的密钥长度);
  3. 这个密钥会被存在全局变量里,供加密和解密函数调用------相当于拦截员拿到了唯一的"加密/解密钥匙"。
    这里要注意:代码里默认的盐值和迭代次数是需要修改的,不然会降低加密安全性,相当于默认钥匙太简单,容易被破解。

(三)核心功能2:数据加密

当应用程序调用send/sendto发送数据时,拦截员会触发加密函数,流程如下(对应代码里的encrypt_data函数):

  1. 先把应用程序要发送的明文数据,复制到一个临时缓冲区(相当于把要寄的东西放到一个临时盒子里);
  2. 如果是第一次发送数据,就生成密钥(避免密钥提前生成,浪费资源);
  3. 生成一个随机的初始化向量(IV)------相当于给每一次加密加一个"随机后缀",就算是相同的明文,每次加密后的密文也不一样,大大提升安全性;
  4. 调用OpenSSL的AES-GCM加密接口,用生成的密钥和IV,对明文数据进行加密,同时生成一个"验证标签"(MAC值),用于接收方验证数据是否被篡改;
  5. 给加密后的密文加上"数据包头部":包含标识(证明是加密数据)、数据包长度、IV、验证标签,然后把这个完整的"加密数据包"交给系统的send/sendto函数,发送出去。

(四)核心功能3:数据解密

当应用程序调用recv/recvfrom接收数据时,拦截员会触发解密函数,流程和加密对应(对应代码里的decrypt_data函数):

  1. 先接收系统传来的加密数据包,从数据包头部提取出IV、验证标签和密文;
  2. 如果是第一次接收数据,就生成密钥(和发送方用相同的密码、盐值,确保密钥一致);
  3. 调用OpenSSL的AES-GCM解密接口,用密钥、IV对密文进行解密,同时验证标签------如果标签不匹配,说明数据被篡改或伪造,直接返回错误,不把数据交给应用程序;
  4. 把解密后的明文数据,复制到应用程序的接收缓冲区,交给应用程序处理------应用程序拿到的就是正常的明文,完全不知道背后经过了解密操作。

(五)核心功能4:挂钩系统函数

这是最关键的一步,也是实现"不修改应用程序,就能加密"的核心,对应代码里的recv、recvfrom、send、sendto四个函数:

比如recv函数(接收数据):代码重新实现了一个recv函数,当应用程序调用recv时,实际上调用的是我们重新实现的这个函数(这就是"挂钩")。这个函数先调用系统原始的recv函数,拿到加密数据,然后调用解密函数解密,最后把明文返回给应用程序。

同理,send函数(发送数据):应用程序调用send时,调用的是我们重新实现的send函数,先把明文加密,再调用系统原始的send函数发送密文。

这里用到了dlfcn.h库的dlsym函数,作用是找到系统原始函数的地址------相当于拦截员先找到"真正的大门",自己在门口拦截处理后,再把数据交给真正的大门发送/接收。

(六)简单流程原理图

不用画复杂的图,用文字就能理清整个流程:

  1. 发送流程:应用程序 → 调用send/sendto → 被封装器拦截 → 加密函数(明文→密文+头部) → 调用系统原始send/sendto → 网络传输(密文);
  2. 接收流程:网络传输(密文) → 系统原始recv/recvfrom → 被封装器拦截 → 解密函数(密文→明文,验证标签) → 明文交给应用程序;
  3. 密钥流程:用户设置密码 → PBKDF2算法+盐值+迭代次数 → 生成AES-256密钥 → 用于加密和解密(发送方和接收方密码一致,才能生成相同密钥)。

四、相关领域知识点总结

这个封装器的设计和实现,涉及多个网络安全和编程领域的知识点,不用深入钻研,了解核心即可,下面用大白话总结:

(一)对称加密技术(核心领域)

  1. 核心概念:加密和解密用同一把密钥,就像用同一把钥匙锁门和开门,效率高、速度快,适合大量数据传输(比如TCP/UDP的实时通信)。
  2. 用到的算法:AES-256-GCM,AES是目前最常用的对称加密算法,256位密钥意味着加密强度极高,很难被破解;GCM模式是一种"加密+验证"一体的模式,既能加密数据,又能验证数据的完整性和真实性,避免数据被篡改或伪造,比单纯的加密更安全。
  3. 辅助知识点:IV(初始化向量),不是密钥,是随机生成的,每次加密都不一样,作用是防止相同明文加密后出现相同密文,提升加密安全性;PBKDF2算法,用于把用户设置的简单密码(比如"123456"),转换成高强度的密钥,避免密码太简单被破解。

(二)系统调用挂钩技术

  1. 核心概念:在不修改系统函数源码的情况下,拦截系统函数的调用,插入自己的处理逻辑(这里就是加解密),属于"无侵入式开发",广泛用于网络监控、安全防护等场景。
  2. 用到的技术:动态链接库(.so文件)和dlfcn.h库,通过动态链接的方式,替换系统函数的调用入口,实现拦截------这也是为什么用户可以通过"LD_PRELOAD=xxx"的方式,快速启用这个封装器,不用编译到应用程序里。

(三)网络协议基础(TCP/UDP)

  1. TCP和UDP是网络传输的核心协议,所有网络应用几乎都基于这两种协议;但它们本身不提供加密功能,属于"明文传输",这也是这个封装器存在的意义------给这两种协议"补全"安全功能。
  2. 关键区别:TCP是"可靠传输"(比如文件传输、聊天),UDP是"不可靠传输"(比如视频、语音),而这个封装器同时支持两种协议,因为它挂钩的是send/sendto、recv/recvfrom这四个通用的网络调用,不管是TCP还是UDP,都会用到这四个函数。

(四)加密库应用(OpenSSL)

OpenSSL是一个开源的加密库,封装了几乎所有常用的加密算法(AES、RSA等),开发者不用自己从零实现加密逻辑,直接调用接口即可,大大降低了加密开发的难度,是网络安全开发中最常用的工具之一。代码中用到的EVP_EncryptInit_ex、EVP_DecryptInit_ex等函数,都是OpenSSL提供的AES加密和解密接口。

(五)网络安全基础

  1. 数据机密性:通过加密,让未授权者无法读取数据;
  2. 数据完整性:通过验证标签(MAC值),判断数据是否被篡改;
  3. 密钥管理:用户需要妥善保管密码(密钥),发送方和接收方必须使用相同的密码,否则无法解密------这也是对称加密的核心注意点。

五、测试方法(用 ncat 测试)

你需要两个终端窗口

终端1:启动服务端(监听)

bash 复制代码
LD_PRELOAD=./crypthook.so CH_KEY=mypassword123 ncat -l -p 5000

终端2:启动客户端(连接)

bash 复制代码
LD_PRELOAD=./crypthook.so CH_KEY=mypassword123 ncat 127.0.0.1 5000

测试效果

现在你在客户端输入文字:

复制代码
hello this is encrypted

服务端会正常收到明文 ,但网络上抓包看到的是 AES 加密乱码

这就说明:

✅ 自动加密发送

✅ 自动解密接收

✅ 无侵入式加密隧道运行成功


关键说明

  1. 两边密码必须一样
    CH_KEY=密码

    密码不同 → 无法解密

  2. LD_PRELOAD=./crypthook.so

    这是让系统优先加载你的加密库,自动挂钩 send/recv

  3. 支持所有 TCP/UDP 程序

    不只是 ncat,你可以替换成任何程序:

    • nc
    • curl
    • 自己写的网络程序
    • 远程连接工具

例子:

bash 复制代码
LD_PRELOAD=./crypthook.so CH_KEY=123456 ./your_program

抓包验证

你可以开第三个终端抓包,确认数据是加密的:

bash 复制代码
sudo tcpdump -i lo port 5000 -X

你会看到一堆乱码,证明加密生效。

总结

这个TCP/UDP对称加密隧道封装器,本质上是"用简单的方式,解决网络明文传输的安全问题",它的核心价值的是"无侵入、轻量、易用"------不用修改应用程序代码,不用懂复杂的加密原理,设置一个密码就能启用加密,适合各种基于TCP/UDP的网络应用。

常见的应用场景:远程控制(比如用ncat远程连接时,加密传输指令)、文件传输(加密传输敏感文件)、小型网络服务(给自己写的网络程序快速添加加密功能)等。

同时,它也涉及了对称加密、系统调用挂钩、网络协议、加密库应用等多个领域的知识点,是学习网络安全开发的一个很好的案例------既能理解加密的核心逻辑,又能掌握无侵入式开发的思路,还能熟悉OpenSSL库的基本使用。

Welcome to follow WeChat official account【程序猿编码

相关推荐
skilllite作者2 小时前
AI agent 的 Assistant Auto LLM Routing 规划的思考
网络·人工智能·算法·rust·openclaw·agentskills
pengyi8710152 小时前
私网IP映射公网基础原理,搭配代理IP远程访问入门
linux·服务器·网络
aq55356002 小时前
编程语言三巨头:汇编、C++与PHP大比拼
java·开发语言
AILabNotes2 小时前
014、隐私增强技术:零知识证明与混合网络在网关中的应用
网络·区块链·零知识证明
aq55356002 小时前
PHP vs Python:30秒看懂核心区别
开发语言·python·php
我是无敌小恐龙2 小时前
Java SE 零基础入门Day01 超详细笔记(开发前言+环境搭建+基础语法)
java·开发语言·人工智能·opencv·spring·机器学习
深圳市九鼎创展科技3 小时前
MT8883 vs RK3588 开发板全面对比:选型与场景落地指南
大数据·linux·人工智能·嵌入式硬件·ubuntu
码云数智-大飞3 小时前
零基础微信小程序制作平台哪个好
开发语言
神仙别闹3 小时前
基于 MATLAB 实现的 DCT 域的信息隐藏
开发语言·matlab