在网络通信中,我们日常用的TCP、UDP协议本身是不加密的------就像寄快递时不封箱,路上任何人都能拆开看里面的内容。比如远程连接、文件传输时,数据明文传输很容易被窃取、篡改。而TCP/UDP对称加密隧道封装器,就是一个"自动封箱工具":不用修改原有任何程序的代码,就能给所有通过TCP/UDP传输的数据自动加密,让数据变成"乱码",只有持有正确"钥匙"的接收方才能解密,相当于给网络通信加了一层安全防护盾。
它的核心优势很简单:轻量、通用、无侵入。不管是现成的聊天工具、远程控制软件,还是自己写的简单网络程序,只要是用TCP或UDP协议通信,它都能"套上"加密层,不用开发者额外写一行加密代码,大大降低了网络安全开发的门槛。
一、先搞懂:这个"加密封装器"到底是什么?
用大白话讲,这个工具就是一个"中间拦截器",夹在应用程序和系统网络之间。当应用程序要发送数据时,它先把数据拦截下来,加密后再交给系统发送出去;当应用程序要接收数据时,它先把系统收到的加密数据拦截下来,解密后再交给应用程序。
对于应用程序来说,完全感觉不到它的存在------该怎么发送、接收数据,还是怎么操作,只是背后多了一道加密和解密的工序。而对于网络传输来说,传递的不再是明文数据,而是加密后的"密文",就算被截获,没有正确的密钥也无法破解,从而保证数据的机密性和完整性。
举个通俗的例子:你用聊天软件发消息,原本是"明文直发",中途任何人都能看到消息内容;有了这个封装器后,你发的消息会被自动加密成乱码,传到对方那边后,再自动解密成正常消息,全程你和对方都不用做任何额外操作,却实现了消息保密。
二、设计思路:怎么做到"无侵入"加密?
这个封装器的设计核心,就是"挂钩系统网络调用",不用改动应用程序本身,而是从系统层面拦截数据,完成加密和解密。具体思路可以分成3步,非常好理解:
- 拦截关键操作:我们知道,任何应用程序要通过TCP/UDP发数据,都会调用系统的两个核心函数------发送函数(send/sendto)和接收函数(recv/recvfrom)。封装器的第一步,就是"挂钩"这四个函数,相当于在这四个函数的门口放了一个"拦截员",所有数据的发送和接收,都要先经过这个拦截员。
- 自动加解密:发送数据时,拦截员先把应用程序要发的明文数据截下来,用提前约定好的加密算法和密钥加密,变成密文后,再交给系统的发送函数发送;接收数据时,拦截员先把系统收到的密文截下来,用同样的密钥和解密算法解密,变成明文后,再交给应用程序。
- 简化配置,降低门槛:设计时特意做了"极简配置",用户只需要设置一个密码(密钥),就能直接使用,不用懂复杂的加密算法原理;同时支持自动生成加密所需的辅助参数,尽量减少用户的操作成本,让非专业人士也能轻松上手。
补充一个关键设计细节:为了保证加密的安全性和效率,它采用了"对称加密"(加密和解密用同一把密钥),同时加入了"身份验证"------每一个加密后的数据包,都会附带一个"验证标签",接收方可以通过这个标签判断数据是否被篡改,避免收到伪造的恶意数据。
三、代码实现原理:手把手解读核心逻辑
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:生成加密密钥
加密和解密需要同一把密钥,代码里的密钥生成逻辑很灵活,大白话解读如下:
- 用户可以通过环境变量设置一个密码(比如"donthackmebro"),如果没设置,就用默认密码;
- 用PBKDF2算法,把用户设置的简单密码,结合"盐值"(一个随机字符串,防止相同密码生成相同密钥)和迭代次数,生成一个32字节的密钥(AES-256所需的密钥长度);
- 这个密钥会被存在全局变量里,供加密和解密函数调用------相当于拦截员拿到了唯一的"加密/解密钥匙"。
这里要注意:代码里默认的盐值和迭代次数是需要修改的,不然会降低加密安全性,相当于默认钥匙太简单,容易被破解。
(三)核心功能2:数据加密
当应用程序调用send/sendto发送数据时,拦截员会触发加密函数,流程如下(对应代码里的encrypt_data函数):
- 先把应用程序要发送的明文数据,复制到一个临时缓冲区(相当于把要寄的东西放到一个临时盒子里);
- 如果是第一次发送数据,就生成密钥(避免密钥提前生成,浪费资源);
- 生成一个随机的初始化向量(IV)------相当于给每一次加密加一个"随机后缀",就算是相同的明文,每次加密后的密文也不一样,大大提升安全性;
- 调用OpenSSL的AES-GCM加密接口,用生成的密钥和IV,对明文数据进行加密,同时生成一个"验证标签"(MAC值),用于接收方验证数据是否被篡改;
- 给加密后的密文加上"数据包头部":包含标识(证明是加密数据)、数据包长度、IV、验证标签,然后把这个完整的"加密数据包"交给系统的send/sendto函数,发送出去。
(四)核心功能3:数据解密
当应用程序调用recv/recvfrom接收数据时,拦截员会触发解密函数,流程和加密对应(对应代码里的decrypt_data函数):
- 先接收系统传来的加密数据包,从数据包头部提取出IV、验证标签和密文;
- 如果是第一次接收数据,就生成密钥(和发送方用相同的密码、盐值,确保密钥一致);
- 调用OpenSSL的AES-GCM解密接口,用密钥、IV对密文进行解密,同时验证标签------如果标签不匹配,说明数据被篡改或伪造,直接返回错误,不把数据交给应用程序;
- 把解密后的明文数据,复制到应用程序的接收缓冲区,交给应用程序处理------应用程序拿到的就是正常的明文,完全不知道背后经过了解密操作。
(五)核心功能4:挂钩系统函数
这是最关键的一步,也是实现"不修改应用程序,就能加密"的核心,对应代码里的recv、recvfrom、send、sendto四个函数:
比如recv函数(接收数据):代码重新实现了一个recv函数,当应用程序调用recv时,实际上调用的是我们重新实现的这个函数(这就是"挂钩")。这个函数先调用系统原始的recv函数,拿到加密数据,然后调用解密函数解密,最后把明文返回给应用程序。
同理,send函数(发送数据):应用程序调用send时,调用的是我们重新实现的send函数,先把明文加密,再调用系统原始的send函数发送密文。
这里用到了dlfcn.h库的dlsym函数,作用是找到系统原始函数的地址------相当于拦截员先找到"真正的大门",自己在门口拦截处理后,再把数据交给真正的大门发送/接收。
(六)简单流程原理图
不用画复杂的图,用文字就能理清整个流程:
- 发送流程:应用程序 → 调用send/sendto → 被封装器拦截 → 加密函数(明文→密文+头部) → 调用系统原始send/sendto → 网络传输(密文);
- 接收流程:网络传输(密文) → 系统原始recv/recvfrom → 被封装器拦截 → 解密函数(密文→明文,验证标签) → 明文交给应用程序;
- 密钥流程:用户设置密码 → PBKDF2算法+盐值+迭代次数 → 生成AES-256密钥 → 用于加密和解密(发送方和接收方密码一致,才能生成相同密钥)。
四、相关领域知识点总结
这个封装器的设计和实现,涉及多个网络安全和编程领域的知识点,不用深入钻研,了解核心即可,下面用大白话总结:
(一)对称加密技术(核心领域)
- 核心概念:加密和解密用同一把密钥,就像用同一把钥匙锁门和开门,效率高、速度快,适合大量数据传输(比如TCP/UDP的实时通信)。
- 用到的算法:AES-256-GCM,AES是目前最常用的对称加密算法,256位密钥意味着加密强度极高,很难被破解;GCM模式是一种"加密+验证"一体的模式,既能加密数据,又能验证数据的完整性和真实性,避免数据被篡改或伪造,比单纯的加密更安全。
- 辅助知识点:IV(初始化向量),不是密钥,是随机生成的,每次加密都不一样,作用是防止相同明文加密后出现相同密文,提升加密安全性;PBKDF2算法,用于把用户设置的简单密码(比如"123456"),转换成高强度的密钥,避免密码太简单被破解。
(二)系统调用挂钩技术
- 核心概念:在不修改系统函数源码的情况下,拦截系统函数的调用,插入自己的处理逻辑(这里就是加解密),属于"无侵入式开发",广泛用于网络监控、安全防护等场景。
- 用到的技术:动态链接库(.so文件)和dlfcn.h库,通过动态链接的方式,替换系统函数的调用入口,实现拦截------这也是为什么用户可以通过"LD_PRELOAD=xxx"的方式,快速启用这个封装器,不用编译到应用程序里。
(三)网络协议基础(TCP/UDP)
- TCP和UDP是网络传输的核心协议,所有网络应用几乎都基于这两种协议;但它们本身不提供加密功能,属于"明文传输",这也是这个封装器存在的意义------给这两种协议"补全"安全功能。
- 关键区别:TCP是"可靠传输"(比如文件传输、聊天),UDP是"不可靠传输"(比如视频、语音),而这个封装器同时支持两种协议,因为它挂钩的是send/sendto、recv/recvfrom这四个通用的网络调用,不管是TCP还是UDP,都会用到这四个函数。
(四)加密库应用(OpenSSL)
OpenSSL是一个开源的加密库,封装了几乎所有常用的加密算法(AES、RSA等),开发者不用自己从零实现加密逻辑,直接调用接口即可,大大降低了加密开发的难度,是网络安全开发中最常用的工具之一。代码中用到的EVP_EncryptInit_ex、EVP_DecryptInit_ex等函数,都是OpenSSL提供的AES加密和解密接口。
(五)网络安全基础
- 数据机密性:通过加密,让未授权者无法读取数据;
- 数据完整性:通过验证标签(MAC值),判断数据是否被篡改;
- 密钥管理:用户需要妥善保管密码(密钥),发送方和接收方必须使用相同的密码,否则无法解密------这也是对称加密的核心注意点。
五、测试方法(用 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 加密乱码。
这就说明:
✅ 自动加密发送
✅ 自动解密接收
✅ 无侵入式加密隧道运行成功
关键说明
-
两边密码必须一样
CH_KEY=密码密码不同 → 无法解密
-
LD_PRELOAD=./crypthook.so
这是让系统优先加载你的加密库,自动挂钩 send/recv
-
支持所有 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【程序猿编码】