基于eBPF的端口复用新解

admin 2024年3月7日22:14:08评论17 views字数 5031阅读16分46秒阅读模式

基于eBPF的端口复用新解

背景

1.1 端口复用

端口复用是一种网络编程技术,允许多个套接字在同一传输层协议(如TCP或UDP)下绑定到相同的端口号上。端口复用的可以提高服务器处理并发连接的能力,尤其是在短时间内有大量连接尝试到达同一端口时,或者用于实现多进程或多线程监听同一端口以提高网络服务的吞吐率。

一句话概括,端口复用就是利用已经被其他服务占用的端口来进行网络通信,这样我们的流量就跟正常服务的流量混在一起,在网络安全领域,端口复用技术拥有防火墙绕过、EDR规避等好处。

传统的端口复用方法通过SO_REUSERADDR选项或者配置iptables实现,但是SO_REUSERADDR的方式需要多块网卡或用IP Alias技术,iptables的方式及其容易发现,因此需要一个新的手段来隐蔽的实现端口复用

1.2 eBPF简介

eBPF是“extended Berkeley Packet Filter”的缩写,最初用于网络流量过滤,目前已发展为一种强大的内核技术,允许用户空间程序注入到内核中执行某些预定义的、安全的代码片段,而不需要更改内核源代码或加载模块。应用包括网络监控、性能分析、安全审计等。eBPF程序运行高效且安全,因为它们在执行前被严格验证和沙箱隔离。

如今,eBPF已经被运用在入侵检测,DDos攻击防御,Rootkit等安全领域,展现出了其巨大潜力。本文将利用eBPF实现端口复用技术,一窥eBPF的强大威力。

基于eBPF的端口复用新解

思路讲解

2.1 端口复用基本思路

网络监控是eBPF的主要功能,它可以hook内核网络处理函数,拿到所有在机器上收发的网络流量,还可以进行过滤等高级操作。

那么,如果要进行端口复用,首先确定机器上可复用的端口,然后分析该端口的网络流量(包括是TCP/UDP流量,有哪些特殊标识等),然后使用eBPF获取机器上收发的网络流量,过滤出我们特殊标记并携带payload的请求包(类似一句话木马,携带一个特殊参数),然后就可以拿取我们要执行的payload执行。

2.2 HTTP协议解析

我们以HTTP协议为例,讲解具体如何使用eBPF进行端口复用,关于HTTP包的解析代码,BCC官方已经提供,我们直接读源码学习即可。

1. 我们使用eBPF可以获取从数据链路层(OSI模型的第二层)开始的整个封包,然后逐层的分解封包内容,找到我们HTTP请求的payload并解析,实现过滤。一开始,我们有一个用于读取流量的指针cursor, 此时指向流量头部。
2. 首先需要解析的是以太网帧头部,一个标准的以太网帧头部是14字节,如图所示:

基于eBPF的端口复用新解

3. 因此我们可以读取以太网帧前14字节的数据,解析为以太网帧头部,然后判断是否是IP数据包,如果不是则直接跳过此包:
struct ethernet_t *ethernet = cursor_advance(cursorsizeof(*ethernet));
 //filter IP packets (ethernet type = 0x0800)
 if (!(ethernet->type == 0x0800)) {
  goto DROP;
 }
4. 读取以太网帧头部后(此时指针指向从以太网帧头部后14字节),我们需要进一步解析IP报文头部,如图:

基于eBPF的端口复用新解

5. 可以看到,IP报文头部最少有20字节,存储报文长度,协议类型,源IP,目的IP等信息,还可以扩至一些可选项,我们首先需要读取前20字节的基本信息,判断是否为TCP协议(HTTP依赖于TCP),不是则跳过:
struct ip_t *ip = cursor_advance(cursorsizeof(*ip));
 //filter TCP packets (ip next protocol = 0x06)
 if (ip->nextp != IP_TCP) {
  goto DROP;
 }
6. 此时指针指向IP报文头部偏移20字节的位置(因为读取了前20字节),然后我们需要计算出报文可选项的长度,然后跳过它。首先通过读取报文长度字段并除以4(报文长度字段单位为32位字),得到IP报文头的实际长度,最后跳过其多出来的字节数:
ip_header_length = ip->hlen << 2;    //SHL 2 -> *4 multiply

//check ip header length against minimum
if (ip_header_length < sizeof(*ip)) {
 goto DROP;
}

//shift cursor forward for dynamic ip header size
void *_ = cursor_advance(cursor, (ip_header_length-sizeof(*ip)));
7. 接下来就是解析TCP报文的头部了,结构如图:

基于eBPF的端口复用新解

8. TCP报文同样存在20字节固定长度以及可选长度,因此我们先读取出固定长度的信息,然后根据固定长度的数据偏移项计算出内容偏移和长度:
struct tcp_t *tcp = cursor_advance(cursorsizeof(*tcp));

//calculate tcp header length
//value to multiply *4
//e.g. tcp->offset = 5 ; TCP Header Length = 5 x 4 byte = 20 byte
tcp_header_length = tcp->offset << 2//SHL 2 -> *4 multiply

//calculate payload offset and length
payload_offset = ETH_HLEN + ip_header_length + tcp_header_length;
payload_length = ip->tlen - ip_header_length - tcp_header_length;
9. 根据我们计算出的payload_offset和payload_length,我们就可以顺利的定位到HTTP协议的具体位置了,HTTP协议结构如图:

基于eBPF的端口复用新解

10. HTTP协议版本起码7位,所以把不符合的包过滤掉:
if(payload_length < 7) {
 goto DROP;
}
11. 然后就可以逐位判断HTTP类型了,不符合说明不是HTTP请求,直接过滤,而匹配上的内容,表明是HTTP协议:
unsigned long p[7];
int i = 0;
for (i = 0; i < 7; i++) {
 p[i] = load_byte(skb, payload_offset + i);
}

//find a match with an HTTP message
//HTTP
if ((p[0] == 'H') && (p[1] == 'T') && (p[2] == 'T') && (p[3] == 'P')) {
 goto KEEP;
}
//GET
if ((p[0] == 'G') && (p[1] == 'E') && (p[2] == 'T')) {
 goto KEEP;
}
//POST
if ((p[0] == 'P') && (p[1] == 'O') && (p[2] == 'S') && (p[3] == 'T')) {
 goto KEEP;
}
//PUT
if ((p[0] == 'P') && (p[1] == 'U') && (p[2] == 'T')) {
 goto KEEP;
}
//DELETE
if ((p[0] == 'D') && (p[1] == 'E') && (p[2] == 'L') && (p[3] == 'E') && (p[4] == 'T') && (p[5] == 'E')) {
 goto KEEP;
}
//HEAD
if ((p[0] == 'H') && (p[1] == 'E') && (p[2] == 'A') && (p[3] == 'D')) {
 goto KEEP;
}

基于eBPF的端口复用新解

eBPF实现HTTP协议端口复用

3.1 eBPF端口复用方案

先讲讲eBPF,其基本思想是在没有改变内核源代码的情况下,允许用户态的程序运行一个在内核态中预先定义好的程序指令集,并与内核资源进行有限的交互。

一般而言,内核会在系统调用等处预留hook点供其他程序hook实现信息获取等。可以通过BCC、libbpf等编写eBPF内核态代码,然后由LLVM编译成字节码,经过其验证器的验证,成功附加到内核的挂载点中,这样就能在内核处理流量的时候抓取流量信息。

eBPF包括内核态代码和用户态代码,在其获得内核关键信息后,会把数据保存在eBPF Maps中,eBPF Maps允许用户态程序读取和修改,因此用户态代码可以通过eBPF Maps拿到内核态获取的数据,实现流量包过滤。

以BCC为例,一个eBPF程序工作流程如下:

基于eBPF的端口复用新解

那么,我们只需要编写在内核中抓取并过滤HTTP包的内核态eBPF代码,将其发送至用户态,eBPF用户态代码解析HTTP数据,然后我们参考一句话木马的思想,预设一个特殊的GET参数,用于发送我们要执行的命令,当用户态源码解析匹配到参数时,就会执行参数的内容

这样,我们就可以在不开新端口的情况下,复用了HTTP的端口,与正常流量混合在一起流动,加大了检测的难度。

3.2 具体实现

我们使用BCC作为eBPF程序的开发框架,BCC官方已经提供了HTTP包捕获和解析的例子,我们可以直接在官方用例上修改代码实现我们的需求,官方例子在这:https://github.com/iovisor/bcc/tree/master/examples/networking/http_filter

1. 我们把例子clone到本地
2. http-parse-simple.c是eBPF的内核态程序,负责过滤到HTTP请求并发送到用户态,其过滤逻辑我们前文已经详细讲述,我们不需要改动它
3. http-parse-simple.py是eBPF的用户态代码,它首先加载eBPF内核态代码,载入内核中。然后把内核态过滤出的包通过阻塞方式附加到新创建的原始套接字中,当新HTTP包到来时,附加的原始套接字就会有包具体内容:
基于eBPF的端口复用新解
4. 然后使用无限循环,一直读取原始套接字的流量内容,并进行解析:
基于eBPF的端口复用新解
5. 用户态代码接收到数据包后,同样需要对HTTP进行解析,解析思路就是我们前文讲述的思路,一层一层的读取直到计算出payload位置:
基于eBPF的端口复用新解
6. 官方给出的例子是直接打印请求内容,这里我们就需要修改,首先尝试匹配我们预设的参数,如果匹配成功,python将执行参数携带的命令:

基于eBPF的端口复用新解

7. 命令是执行了,但是我们该如何获得回显?就需要多预设一个参数echo,携带我们用于接收回显的url,如果匹配到echo,就会把回显post到url上:
基于eBPF的端口复用新解
8. 这样,利用eBPF实现端口复用就大功告成了,我们可以通过实践查看效果。

基于eBPF的端口复用新解

实践

  1. 我们准备一台自己的服务器作为测试机,执行如下命令开启一个HTTP协议端口:
python3 -m http.server 7890
基于eBPF的端口复用新解
  1. 在机器上运行我们的eBPF程序,可以看到已经绑定到指定网卡上了:
基于eBPF的端口复用新解
  1. 先正常请求目标的7890端口,可以看到HTTP功能是正常的:
基于eBPF的端口复用新解
基于eBPF的端口复用新解
  1. 后台也能正常打印请求:
基于eBPF的端口复用新解
  1. 然后我们尝试携带我们预设的特殊参数(文中为fatmo666),携带命令请求,可以看到成功执行了打印出执行回显:
基于eBPF的端口复用新解
基于eBPF的端口复用新解
基于eBPF的端口复用新解
基于eBPF的端口复用新解
  1. 最后尝试带着echo参数执行,获取回显:
基于eBPF的端口复用新解
基于eBPF的端口复用新解

基于eBPF的端口复用新解

总结

本文我们介绍了eBPF和端口复用的背景,探索了eBPF实现端口复用的可行性,总结了eBPF端口复用的思路。然后以复用HTTP协议,精讲了HTTP解析过滤过程,通过BCC框架开发了一个复用HTTP端口的程序,实现了无端口后门。

这体现了eBPF在网络安全领域的巨大威力和潜力。但是,eBPF同样有自身的局限性,其内核版本至少为4.15.0,并且需要Root权限。因此,在实际红队攻击和入侵检测等场景下,需要结合其他方法,最大化eBPF的效果。

此后门代码已开源至:https://github.com/fatmo666/eBPFPortMuxer(已做无害化处理)。

基于eBPF的端口复用新解

                            基于eBPF的端口复用新解点亮“在看”,你最好看

原文始发于微信公众号(中国电信SRC):基于eBPF的端口复用新解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月7日22:14:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于eBPF的端口复用新解http://cn-sec.com/archives/2556127.html

发表评论

匿名网友 填写信息