背景
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的强大威力。
思路讲解
2.1 端口复用基本思路
网络监控是eBPF的主要功能,它可以hook内核网络处理函数,拿到所有在机器上收发的网络流量,还可以进行过滤等高级操作。
那么,如果要进行端口复用,首先确定机器上可复用的端口,然后分析该端口的网络流量(包括是TCP/UDP流量,有哪些特殊标识等),然后使用eBPF获取机器上收发的网络流量,过滤出我们特殊标记并携带payload的请求包(类似一句话木马,携带一个特殊参数),然后就可以拿取我们要执行的payload执行。
2.2 HTTP协议解析
我们以HTTP协议为例,讲解具体如何使用eBPF进行端口复用,关于HTTP包的解析代码,BCC官方已经提供,我们直接读源码学习即可。
struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet));
//filter IP packets (ethernet type = 0x0800)
if (!(ethernet->type == 0x0800)) {
goto DROP;
}
struct ip_t *ip = cursor_advance(cursor, sizeof(*ip));
//filter TCP packets (ip next protocol = 0x06)
if (ip->nextp != IP_TCP) {
goto DROP;
}
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)));
struct tcp_t *tcp = cursor_advance(cursor, sizeof(*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;
if(payload_length < 7) {
goto DROP;
}
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实现HTTP协议端口复用
3.1 eBPF端口复用方案
先讲讲eBPF,其基本思想是在没有改变内核源代码的情况下,允许用户态的程序运行一个在内核态中预先定义好的程序指令集,并与内核资源进行有限的交互。
一般而言,内核会在系统调用等处预留hook点供其他程序hook实现信息获取等。可以通过BCC、libbpf等编写eBPF内核态代码,然后由LLVM编译成字节码,经过其验证器的验证,成功附加到内核的挂载点中,这样就能在内核处理流量的时候抓取流量信息。
eBPF包括内核态代码和用户态代码,在其获得内核关键信息后,会把数据保存在eBPF Maps中,eBPF Maps允许用户态程序读取和修改,因此用户态代码可以通过eBPF Maps拿到内核态获取的数据,实现流量包过滤。
以BCC为例,一个eBPF程序工作流程如下:
那么,我们只需要编写在内核中抓取并过滤HTTP包的内核态eBPF代码,将其发送至用户态,eBPF用户态代码解析HTTP数据,然后我们参考一句话木马的思想,预设一个特殊的GET参数,用于发送我们要执行的命令,当用户态源码解析匹配到参数时,就会执行参数的内容。
这样,我们就可以在不开新端口的情况下,复用了HTTP的端口,与正常流量混合在一起流动,加大了检测的难度。
3.2 具体实现
我们使用BCC作为eBPF程序的开发框架,BCC官方已经提供了HTTP包捕获和解析的例子,我们可以直接在官方用例上修改代码实现我们的需求,官方例子在这:https://github.com/iovisor/bcc/tree/master/examples/networking/http_filter
实践
-
我们准备一台自己的服务器作为测试机,执行如下命令开启一个HTTP协议端口:
python3 -m http.server 7890
-
在机器上运行我们的eBPF程序,可以看到已经绑定到指定网卡上了:
-
先正常请求目标的7890端口,可以看到HTTP功能是正常的:
-
后台也能正常打印请求:
-
然后我们尝试携带我们预设的特殊参数(文中为fatmo666),携带命令请求,可以看到成功执行了打印出执行回显:
-
最后尝试带着echo参数执行,获取回显:
总结
本文我们介绍了eBPF和端口复用的背景,探索了eBPF实现端口复用的可行性,总结了eBPF端口复用的思路。然后以复用HTTP协议,精讲了HTTP解析过滤过程,通过BCC框架开发了一个复用HTTP端口的程序,实现了无端口后门。
这体现了eBPF在网络安全领域的巨大威力和潜力。但是,eBPF同样有自身的局限性,其内核版本至少为4.15.0,并且需要Root权限。因此,在实际红队攻击和入侵检测等场景下,需要结合其他方法,最大化eBPF的效果。
此后门代码已开源至:https://github.com/fatmo666/eBPFPortMuxer(已做无害化处理)。
点亮“在看”,你最好看
原文始发于微信公众号(中国电信SRC):基于eBPF的端口复用新解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论