Linux网络数据包透明加解密
1. 概述
TCP
协议本身不提供加密功能,通常需要通过像TLS
这样的加密协议来保障通信安全。然而,TLS
的实现通常需要修改应用程序代码,有时候这是很难做到的。本文提出一种透明的加解密方案,旨在无需修改应用程序即可自动加密和解密数据,确保传输安全。
我们将讨论两种实现透明加解密的方案:应用层加密和内核层加密,介绍它们的工作原理、优缺点、适用场景及实现细节。
2. 应用层加密方案
2.1 方案设计
在应用层加密方案中,我们的做法是通过拦截并重写send
和recv
函数来实现数据的加解密。具体来说,我们用LD_PRELOAD
来加载一个自定义的动态库,进而拦截这些函数调用。
工作流程:
- 当用户调用
send
函数时,我们先捕获到这个请求。 - 拦截器会对传输的数据进行加密处理。
- 加密后的数据通过原始
send
函数发送到TCP协议栈中去。 - 在接收端,拦截器会捕获到
recv
函数的调用,并对接收到的数据进行解密。 - 解密后的数据会返回给用户应用。
2.2 技术实现详解
(1)加密逻辑
对于加密部分,我们推荐使用强加密算法,比如AES
,而不推荐使用一些不再安全的老算法(像RC4
)。为了便于理解,下面给出了一个简单的异或加密的例子,主要是为了展示加解密的基本概念。请注意,这个方法仅适用于教学演示,不适合在生产环境中使用来保护数据安全。
void encrypt(char *data, size_t len) {for (size_t i = 0; i < len; ++i) {data[i] ^= 0xAA; // 简单的异或加密,纯粹为教学示范}
}
(2)send
函数挂钩
为了拦截send
函数,我们实现一个自定义so
库, 通过LD_PRELOAD
加载我们的动态库。这样, 应用程序每次调用send
时,我们就能拦截到,进行加密处理。为了避免直接修改原始数据,我们会创建一个临时的缓冲区来存放加密后的数据。
#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>static ssize_t (*original_send)(int sockfd, const void *buf, size_t len, int flags);ssize_t send(int sockfd, const void *buf, size_t len, int flags) {char *encrypted_data = malloc(len);if (!encrypted_data) return -ENOMEM;memcpy(encrypted_data, buf, len);// 对数据加密encrypt(encrypted_data, len);// 调用原始 send 函数发送加密数据ssize_t ret = original_send(sockfd, encrypted_data, len, flags);free(encrypted_data);return ret;
}__attribute__((constructor))
static void init() {original_send = dlsym(RTLD_NEXT, "send");
}
(3)动态库加载
编译生成动态库后,通过LD_PRELOAD
机制注入该库,以挂钩目标函数。
gcc -shared -fPIC -o libencrypt.so encrypt.c -ldl
export LD_PRELOAD=./libencrypt.so
(4)解密逻辑
接收端同样通过挂钩recv
函数来实现解密。在接收到数据后,拷贝数据到临时缓冲区并进行解密,最后将解密后的数据传回用户空间。
static ssize_t (*original_recv)(int sockfd, void *buf, size_t len, int flags);ssize_t recv(int sockfd, void *buf, size_t len, int flags) {ssize_t ret = original_recv(sockfd, buf, len, flags);if (ret > 0) {// 创建临时缓冲区以防覆盖原始数据char *decrypted_data = malloc(ret);if (!decrypted_data) return -ENOMEM;memcpy(decrypted_data, buf, ret);encrypt(decrypted_data, ret); // 解密memcpy(buf, decrypted_data, ret);free(decrypted_data);}return ret;
}__attribute__((constructor))
static void init() {original_recv = dlsym(RTLD_NEXT, "recv");
}
2.3 优缺点分析
优点:
- 实现简单:无需修改内核代码,减少了系统级别的复杂度。
- 灵活性高:支持不同加密算法的快速切换,适应多种需求。
- 稳定性好:操作仅限于用户态,调试和维护更加简便。
缺点:
- 应用范围有限:每个目标进程需单独配置
LD_PRELOAD
,增加了部署的复杂性。 - 性能损耗:加密处理、动态库加载以及内存分配等操作会带来额外的性能开销。
- 静态编译限制:静态编译的程序无法通过
LD_PRELOAD
挂钩,限制了该方案的适用场景。
3 内核层加密方案
3.1 方案设计
在内核态,通过挂钩sendmsg
和recvmsg
函数,透明地加解密数据。这种方法直接修改了内核网络协议栈的行为,对用户进程完全透明。
工作流程:
- 用户调用
send
,数据进入内核。 - 内核挂钩
sendmsg
,对数据进行加密。 - 数据进入网络层传输。
- 接收端挂钩
recvmsg
,对接收到的数据解密。 - 解密后的数据交还用户空间。
3.2 技术实现详解
(1)挂钩tcp_sendmsg
和tcp_recvmsg
通过kallsyms_lookup_name
获取tcp_prot
结构的地址, 替换其中的tcp_sendmsg
为自定义函数。
tcp_prot
是 Linux 内核中用于管理 TCP 协议的结构体,主要用于实现和维护 TCP 协议的相关功能。它定义了 TCP 协议栈中的许多操作,包括连接管理、数据发送
和接收
、拥塞控制等。
#include <linux/module.h>
#include "../../lib/klog.h"
#include <linux/kallsyms.h>
#include <linux/fs.h>
#include <net/sock.h>#define log_tag "[net-crypt] "// 关闭写保护
unsigned long write_protect_disable(void)
{unsigned long cr0 = 0;unsigned long ret = 0;asm volatile("movq %%cr0, %%rax": "=a"(cr0));// 保存原始cr0的值ret = cr0;cr0 &= 0xfffffffffffeffff;asm volatile("movq %%rax, %%cr0":: "a"(cr0));// 返回原始cr0值return ret;
}// 开启写保护
void write_protect_enable(unsigned long oldval)
{asm volatile("movq %%rax, %%cr0":: "a"(oldval));
}// 保存proto结构地址
struct proto *orig_tcp_prot;
// 保存原来的
int (*orig_tcp_sendmsg)(struct sock *sk, struct msghdr *msg, size_t size);
int (*orig_tcp_recvmsg)(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len);static void __encrypt_buf(char *buf, int size)
{int i;for(i = 0; i < size; i++){buf[i] ^= 0xae;}
}// 我们的
static int my_tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{void *buf = NULL;struct iov_iter from, to;if(strcmp(current->comm, "nc")){goto out;}klogw(log_tag "my_tcp_sendmsg IN, size=%d, msg_len=%d", (int)size, (int)msg_data_left(msg));buf = kmalloc(size, GFP_KERNEL);if(buf == NULL){goto out;}from = to = msg->msg_iter;_copy_from_iter_full(buf, size, &from);__encrypt_buf(buf, size);_copy_to_iter(buf, size, &to);out:if(buf){kfree(buf);}return orig_tcp_sendmsg(sk, msg, size);
}
static int my_tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
{int rv;void *buf = NULL;struct iov_iter from, to;from = to = msg->msg_iter;rv = orig_tcp_recvmsg(sk, msg, len, nonblock, flags, addr_len);if(rv <= 0){goto out;}if(strcmp(current->comm, "nc")){goto out;}klogw(log_tag "my_tcp_recvmsg IN, size=%d, msg_len=%d, rv=%d", (int)len, (int)msg_data_left(msg), rv);buf = kmalloc(rv, GFP_KERNEL);if(buf == NULL){goto out;}_copy_from_iter_full(buf, rv, &from);__encrypt_buf(buf, rv);_copy_to_iter(buf, rv, &to);out:if(buf){kfree(buf);}return rv;
}/** 模块初始化*/
static __init int main_init(void)
{int rv = -1;unsigned long cr0;// 1. 获取struct proto 结构地址orig_tcp_prot = (struct proto *)kallsyms_lookup_name("tcp_prot");// 2. 替换我们的orig_tcp_sendmsg = orig_tcp_prot->sendmsg;orig_tcp_recvmsg = orig_tcp_prot->recvmsg;cr0 = write_protect_disable();orig_tcp_prot->sendmsg = my_tcp_sendmsg; orig_tcp_prot->recvmsg = my_tcp_recvmsg; write_protect_enable(cr0);klogw(log_tag "kernel module load.");// 设置成功标识rv = 0x0;goto out;out:return rv;
}/** 模块清理*/
static __exit void main_exit(void)
{// 恢复原来的unsigned long cr0;cr0 = write_protect_disable();orig_tcp_prot->sendmsg = orig_tcp_sendmsg; orig_tcp_prot->recvmsg = orig_tcp_recvmsg; write_protect_enable(cr0);klogw(log_tag "kernel module unload.");
}module_init(main_init);
module_exit(main_exit);
代码的主要功能是通过替换 TCP 协议栈中 sendmsg
和 recvmsg
函数来对通过 TCP 发送和接收的数据进行加密处理。下面是对代码的简单解释:
1. 禁用和启用写保护(write_protect_disable
和 write_protect_enable
)
这两个函数是用来控制 CPU 的写保护寄存器 cr0
的状态,目的是为了能够修改内核中的只读数据结构。
write_protect_disable()
:通过修改cr0
寄存器来禁用写保护,从而允许修改内核中的只读区域(比如 TCP 协议栈中的tcp_prot
结构)。write_protect_enable()
:恢复原来cr0
寄存器的值,重新启用写保护。
2. 替换 sendmsg
和 recvmsg
函数(my_tcp_sendmsg
和 my_tcp_recvmsg
)
这两个函数是对 TCP 发送和接收数据的替代实现, 用于实现透明加解密:
-
注意这里为了测试方便只处理了
nc
进程 -
my_tcp_sendmsg
:当调用sendmsg
时,首先检查当前进程的名称是否为nc
(netcat)。如果是,则会将发送的数据进行加密。加密后的数据会被重新复制到消息迭代器中,并继续调用原始的sendmsg
函数将数据发送出去。 -
my_tcp_recvmsg
:当调用recvmsg
时,也会检查当前进程的名称是否为nc
。如果是,则会对接收到的数据进行加密处理后再返回。
3. 模块初始化(main_init
)
在模块加载时,会执行以下步骤:
-
获取
tcp_prot
结构:通过kallsyms_lookup_name
函数获取tcp_prot
结构体的地址,这是一个包含sendmsg
和recvmsg
函数指针的结构体。 -
替换函数指针:将
tcp_prot
中的sendmsg
和recvmsg
函数指针替换为自定义的my_tcp_sendmsg
和my_tcp_recvmsg
函数。这会使得所有通过TCP
发送和接收的数据都会经过加密处理。 -
禁用和启用写保护:修改
cr0
寄存器来禁用写保护,替换函数指针后,再恢复写保护。
3.3 优缺点分析
优点:
- 全局覆盖:所有TCP通信均被加密。
- 完全透明:对应用程序无感知。
缺点:
- 开发复杂:需理解内核网络栈的细节。
- 高风险:错误可能导致系统崩溃。
- 维护难度大:内核版本升级可能导致挂钩代码失效。
4 应用场景对比与总结
在选择加密方案时,应用层加密和内核层加密各自有不同的优势和适用场景。以下是它们的对比:
特性 | 应用层加密 | 内核层加密 |
---|---|---|
实现复杂度 | 低 | 高 |
全局适用性 | 需逐个进程配置 | 全局生效 |
性能影响 | 用户态加密性能损耗较高 | 内核态加密更高效 |
调试难度 | 易于调试 | 高,需内核调试工具支持 |
风险 | 无内核崩溃风险 | 潜在系统崩溃风险 |
总结与建议:
-
推荐场景:
- 应用层加密:适用于小规模部署或特定应用,尤其是在需要灵活性和易于调试的场景中。
- 内核层加密:适合对全局网络安全性有严格要求的环境,能确保所有网络通信都受到加密保护。
-
开发建议:
- 如果是一般性的网络加密需求,优先选择应用层加密,因为它实现简便,且维护成本低。
- 若项目需求必须在内核层进行加密,务必充分测试并做好风险评估,避免潜在的系统崩溃风险。
通过这两种方案,可以有效地提升基于TCP协议的数据传输安全性,并根据不同场景的需求选择最合适的加密方式。