安全小课堂144期【如何搭建CTF线下赛协作网络环境】

讲师简介:于国瑞,北京大学王选计算机研究所研究生,r3kapig队员,精通网络协议,主攻PWN,密码学,区块链。

Prerequisites

建议读者对网络协议的分层(如OSI模型)和常见设备工作的层级有基本的了解。

应用层协议是大家最熟悉的,例如HTTP,SSH等。Apache和WAF等是工作在这一层级的软件/设备。

传输层的主要目标就是该层的协议为应用进程提供端到端的通信服务,在这层我们才有连接的概念,它会为我们提供可靠性、流量控制、多路复用等服务。传输层距离大家很近,常见的协议就是如TCP,UDP等。一个传输层协议再加上合适的payload解析器,我们就可以实现和任何人通信。

网络层提供寻址(我要到哪去)和路由(我怎么去)的功能。网络层协议时至今日,我们最熟悉就是IP协议。但除了这个明星外,每次网络不通你打开cmd输入ping baidu.com的时候,这时你使用了ICMP协议;小时候计算机课上你和其他同学联机玩红色警戒,这时候你使用了IPX协议。

链路层提供了最基本的,在两个邻接节点上传输信息的能力,常见的协议包括Ethernet,ARP和PPP协议等。

Iptables

你是否考虑过,Iptables是如何与数据包进行交互的呢?

Iptables是一个配置Linux内核防火墙的命令行工具,每当一个数据包进入或离开Linux的网络协议栈,都会触发内核钩(Hooks),这些钩子就是所谓的Netfiller框架,它们被设置到一些关键的位置,以方便对数据包进行处理。内核模块Iptables会按序注册这些钩子,以保证经过该主机的流量满足对应的防火墙规则要求。

Netfilter Hooks

以下是5个可以被程序注册使用的钩子,当数据包进入到Linux的网络协议栈后就有可能触发对应的钩子。但一个数据包究竟是不是会触发它们取决于,数据包的流向(流入/流出)、数据包的目的地址和数据包的状态(是否被丢弃/拒绝等)。

NF_IP_PRE_ROUTING:这个钩子会在数据包刚刚进入的时候就被触发,它甚至早于数据包被路由之前,也就是说当我们还不知道这个数据包的走向时就会触发该钩子。

NF_IP_LOCAL_IN:在路由完成后,如果该数据包的目的地址是本机,触发该钩子。

NF_IP_FORWARD:在路由完成后,如果该数据包的目的地址是其他主机,则触发该钩子。

NF_IP_LOCAL_OUT:我们本机应用程序所产生的流量会触发该钩子。

NF_IP_POST_ROUTING:任何的网络流量(本机或经由本机转发的)在经过路由并流出时,在其真正发送到外界前会触发此钩子。

内核模块在注册这些钩子时,除了需要提供对应的事件处理函数外,还需要指定事件处理函数的优先级,以保证每次对于数据包的处理都是一致的。这样的话每个注册了钩子的模块都会在恰当的时机被调用,调用完成后模块将会把自己的决策返回给Netfilter模块,以决定对数据包的处理。

Iptables的Table和Chain

虽然内核给我们留了一些用于进行数据包交互的钩子,但是这样肯定是不方便运维人员进行日常维护的,所以IPtables被开发出来用于组织防火墙规则。Iptables使用Table这一概念组织其所有的规则,每个规则都会被分类并被放入对应的table中。例如用于处理网络地址翻译的都会被放入nat表中,用于拦截进入数据包的规则都会被放到filter表中。

具体地,每个表(table)中的规则又会以链(chain)的形式被组织起来。每个规则都会被根据用途的不同划分到不同的表中,而表中的内置链(build-inchains)则是以触发的 netfilter hooks 的不同被分别地组织到不同的链中。

事实上,Iptables中的内置链的名字和Hooks的名字存在着极大的相似度。

Built-in  chains

Hooks

PREROUTING

NFIPPRE_ROUTING

INPUT

NFIPLOCAL_IN

FORWARD

NFIPFORWARD

OUTPUT

NFIPLOCAL_OUT

POSTROUTING

NFIPPOST_ROUTING

如上文所示,内核里只有5个Netfilter钩子,所以每个钩子上可能与多个table的chain相关联。例如,三个table具有PREROUTINGchain。当这些chain在关联的NF_IP_PRE_ROUTING钩子上注册时,它们将指定优先级,该优先级则被用于评定每个table中PREROUTING中规则评估的顺序。

IPtables具体有哪些Table呢?

Filter

这是我们最常用的一个Table,当我们把IPtables当成一个“防火墙(firewall)”使用时,我们一般是用的是其filter表,filter表被用于决定是否让一个数据包进入到后续的处理流程(例如转发到其他主机、访问本机的某个服务等等)。正如其名,其主要的作用就是过滤(filter)。

NAT

它主要用于实现网络地址翻译功能(NAT),在这个表中的规则会修改数据包的源地址或目的地址以实现SNAT,DNAT等等功能,而端口映射就是其功能最基本的例子。

Mangle

在这个表中你可以修改数据包的各种header数据,例如TTL(timeto live)等等。除此之外,你还可以在数据包上作标记(mark),以便于后续的数据包处理(典型的例子如,搭配iproute命令即可实现策略路由)。但是这个标记(mark)其实并不会修改原有的数据包,从内核的角度才能看到这个标记。

Raw

正常情况下iptables不是以packet为逻辑单位处理网络数据包的,因为数据包太多了,如果对每个数据包都进行完整流程的处理会显得过于多余且缓慢。所以iptables默认都是以“连接(connections)”为单位对数据包进行处理,这就是所谓的“连接跟踪(connection tracking)”,在某些特殊情况我们可能希望以数据包为单位对数据进行处理,这时我们就会使用raw表。这通常避免了我们单独实现一个内核模块来实现某个“简单”的功能。

Security

此表是为了实现强制访问控制(MAC)而存在的,它通常会与SELinux配合使用,以防止高密级的数据包流向低密级,它可以工作在packet-based也可以在connection-based的情况下运行。

每个Table中具体包含了哪些Chain呢?

内核中只有5个钩子可以用于防止事件处理函数,来实现IPtables的功能。同一个链(chain)也可能出现在多个Table中,那么他们的优先级究竟是怎么界定的呢?

该图由上到下阅读,就可以得到下面的一个数据包处理流程(忽略了Raw和Mangle),一个更详细的图可以参考wiki。

我们再来举几个实际的例子:

当用户尝试访问本地的HTTP服务时。它所发送的数据包走过的流程是,nat-PREROUTING->路由(发现目标地址是本机)->filter-INPUT->本地Web服务器

当本机是一个路由器,内网用户的数据包从本机转发。数据包的流程是,nat-PREROUTING->路由(发现目标地址不是本机)->filter-FOWARD(判断是不是要转发数据)->nat-POSTROUTING(修改源地址以将内网地址改成当前主机的公网地址)

命令的实例

#禁止远程主机访问本地的TCP 22端口

iptables -t filter -A INPUT -p tcp --dport 22-j DROP

#端口映射,将路由器上的[destination-port]转发到内网机器[inet-ip]的[target-port]端口

iptables -t nat -A PREROUTING -p [proto] -i[interface] --dport [destination-port] -j DNAT --to [inet-ip:target-port]

# 如何将本机转换为路由器?sysctl -w net.ipv4.ip_forward=1 # 允许数据包转发,默认情况下会被丢弃iptables -t nat -A POSTROUTING -o [interface] -jMASQUERADE # 当数据包从[interface]出去时,将其源地址改为本机地址,从而使内网机器获得Internet连接。

调试方法

虽然我们已经对iptables的架构有了足够的理解,我们应该可以编写出无误的规则,所以理论上我们应该不需要这一步?不过世事无常,我们在极小的几率下还是需要对rule进行调试。

当我们需要知道某个规则会不会被触及到的话,我们就可以在欲调试的规则之前加入一个TARGET为TRACE的规则。

例如你可以加入以下规则(如果出现了错误可以考虑通过命令添加额外模块 modprobe ipt_LOG):

iptables -t nat -A PREROUTING -p tcp--destination 192.168.0.0/24 --dport 80 -j TRACE这时我们再去查看/var/log/kern.log就可以看到对应日志。

OpenVPN

OpenVPN是常用的VPN软件,它的核心技术在于虚拟网卡,工作于传输层UDP/TCP。它主要包括两种工作模式:

TAP,类似真正的网卡,可用作网桥,可承载数据链路层的数据,支持IPv6。

TUN,开销低,但只能传输IP数据包,不能像网桥一样工作。

OpenVPN的特点就在于身份认证方式较为灵活,它支持以下三种认证方式。部署脚本可以参考 。

Pre-share私钥

公/私钥认证(打比赛时的网络使用的是这种)

用户名-密码认证,一个典型的网络拓扑结构如下:

             

只有OpenVPN的客户端的加密流量会通过TCP/UDP这里.

未加密的流量会通过这里,这是VPN流量的入口/出口.

这里tun0的地址被配置为10.8.0.1,整个VPN的子网范围是10.8.0.0/24.

 

整个VPN的工作流程是:

OpenVPN从eth1端口接受client的连接完成认证流程后,OpenVPN会将解密后的数据放入到tun0网卡中,到这里OpenVPN的使命就基本完成了,剩下的工作由iptables和系统的路由策略来完成。根据目标的数据包的目标地址来决定数据包最终被分发到eth0或eth1。根据Iptables的配置来决定是否对数据包进行过滤(filter)或伪装(masquerade)。而此时的IPtables配置如下:

# 允许内核进行数据包转发sysctl -w net.ipv4.ip_forward=1# 允许从VPN到LAN的流量iptables -I FORWARD -i tun0 -o eth0 -s 10.8.0.0/24 -d 192.168.0.0/24 -m conntrack \--ctstate NEW -j ACCEPT# 允许VPN到外网的流量iptables -I FORWARD -i tun0 -o eth1 -s 10.8.0.0/24 -m conntrack --ctstate NEW -j ACCEPT# 允许LAN到外网的流量iptables -I FORWARD -i eth0 -o eth1 -s 192.168.0.0/24 -m conntrack \--ctstate NEW -j ACCEPT# 允许已经建立连接的流量通过iptables -I FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT# 当VPN网络范围内的流量从eth1流出时,进行源地址伪装。iptables -t nat -I POSTROUTING -o eth1 -s 10.8.0.0/24 -j MASQUERADE# 当LAN网络范围内的流量从eth1流出时,进行源地址伪装。iptables -t nat -I POSTROUTING -o eth1 -s 192.168.0.0/24 -j MASQUERADE

OpenVPN配置文件

dev tuntopology subnetserver 10.8.0.0 255.255.255.0push "route 192.168.0.0 255.255.255.0"push "redirect-gateway def1"

tun模式下如何访问客户端子网?

上面介绍了基本的OpenVPN配置过程,但是还是没有解决一个常见需求,如何实现访问客户端的子网。

当OpenVPN在tun/tap网卡上接收要转发的数据包或帧时,它将对其进行加密并将其封装到一个或多个UDP数据中,然后将其发送到某个远程(通常是公共的)IP地址中,另一个VPN节点将在该IP地址中接收该数据包在其公共IP上,对其进行解封装和解密,然后将其发送到本地tun/tap接口,操作系统最终将在此看到它们。当然,该过程也可以沿相反的方向进行。

如果涉及的IP地址仅是属于VPN的IP地址,则OpenVPN可以轻松地将某个VPN IP与远程VPN client的IP地址相关联。但是,当涉及非VPN数据包时,OpenVPN需要更多信息, 也即OpenVPN需要知道每个客户端背后的网络 。如果客户端拥有公网IP,那我们就可以直接在上面搭建一个OpenVPN的server,然后连接到其上的客户端就能直接访问服务器的子网了. 但是很多情况下, 我们并不具有这种条件, 那一个简单的想法自然就是设置一条路由。

例如, client1连接到了VPN(其IP为10.0.1.2/24), 与此同时, 它还拥有我们想要访问的子网192.168.42.0/24, 而OpenVPN的子网被设置为了10.0.1.0/24. 一些同学可能会认为只需要在OpenVPN服务端上设置一条路由即可, 例如:

ip route add 192.168.42.0/24 via 10.0.1.2dev tun0

但是这在tun模式下是不能实现的, 因为OpenVPN不会根据路由表推断每个客户端背后的网络. 我们必须明确告诉OpenVPN每个客户端背后的网络, 这是我们的iroute指令起作用的地方。

在这种情况下,我们可以在一个client-config-dir文件夹下使用iroute指令针对性的设置每个client背后的子网。具体的配置方法,我们暂且按下不表。

CTF线下赛网络环境的搭建

如何通过OpenVPN实现我们的网络环境配置?

答案是,我们需要构建一个具有公共IP的VPS,以达到将所有的客户端连接在一起的目的,这个VPS是我们所有主机沟通的桥梁。

1.从github构建docker镜像

git clonehttps://github.com/yuguorui/docker-offline-game-vpncd docker-offline-game-vpn/docker build . -t ctf_vpn_docker

2.初始化OpenVPN配置文件

docker volume create --name $OVPN_DATAdocker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm ctf_vpn_docker ovpn_genconfig -uudp://YOUR_VPS_ADDRESS:4242docker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm -it ctf_vpn_docker ovpn_initpki nopass

3.启动OpenVPN主进程

docker run -v $OVPN_DATA:/etc/openvpn -d-p 4242:1194/udp --cap-add=NET_ADMIN --name ctf_vpn ctf_vpn_docker

下面要做的是,添加你希望通过client访问的子网,例如CTF比赛的内网。

docker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm -it ctf_vpn_docker ovpn_addiroute NETWORK_ID MASKdocker restart ctf_vpn

4.生成一个router配置文件和player配置文件

docker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm -it ctf_vpn_docker easyrsa build-client-full routernopassdocker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm ctf_vpn_docker ovpn_getclient router > router.ovpndocker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm -it ctf_vpn_docker easyrsa build-client-full playernopassdocker run -v $OVPN_DATA:/etc/openvpn--log-driver=none --rm ctf_vpn_docker ovpn_getclient player > player.ovpn

router.ovpn是那个需要做路由工作的客户端配置文件,而player.ovpn则是普通用户的配置文件,使用player.ovpn的配置文件的用户可以连接到router.ovpn的子网(iroute配置过的)。

然后我们需要那个运行router.ovpn配置文件的设备拥有一定的路由转发能力。

# Allow forward packets in kernelsysctl -w net.ipv4.ip_forward=1# Run VPN clientnohup openvpn --config router.ovpn &# Configure the iptablestun_interface=tun0# Attention: tun0 is your tun interface,you should modify the interface to fit your need.wan_interface=eth0# eth0 is your WAN interfaceiptables -t filter -I FORWARD -i${tun_interface} -o ${wan_interface} -j ACCEPTiptables -t filter -I FORWARD -i${wan_interface} -o ${tun_interface} -j ACCEPT # Dual directioniptables -t nat -I POSTROUTING -o${wan_interface} -j MASQUERADE# setting SNAT其他用户正常使用player.ovpn连接server即可。

 Q & A

Q:为什么tcpdump会比内核的模块更早接收到数据包?

A : 当数据流入时,tcpdump是第一个接收到数据包的软件;当数据流出时,tcpdump是路径上的最后一环。即:

IN: Wire -> NIC -> tcpdymp->netfilter/iptablesOUT: iptables -> tcpdump -> NIC -> Wire

tcpdump底层依赖的是BPF(BerkeleyPacklet Filter),是类Unix系统上数据链路层的一种原始接口,提供原始链路层封包的收发。

 

Q :  OpenVPN配置环节有更简单的办法吧?

A: OpenVPN横向对比来讲,肯定是最为复杂的,但是这也从另外一个角度证明了OpenVPN功能的多样性,能够满足我们的各种需求。WireGuard目前为止还不够成熟,缺少配置文件自动生成的基础设施,也没有一个可靠的自动分配IP/分发路由的机制。PPTP问题也类似,不过其最主要的问题在于苹果设备已经全线移除对其的支持,而且其也存在一些安全问题,使其无法在跨国情况下有效工作。

rp在小范围内使用还是很不错的,灵活性也足够,但是其主要的问题在于其不够透明化,单纯的映射端口会有很多问题,例如在包含了很多外部CCS的HTML页面上。

OpenVPN配置起来真的很麻烦么?其实在加入了自动化的脚本后,大部分的工作也不过是复制粘贴而已,部署个docker几分钟而已。

 

Q: 为什么在主机的流量都能够被正确代理的情况下,Linux本机的流量却不能被代理?

A:大部分的教程都是在nat表中的PREROUTING链中插入的代理规则,而在iptables的架构中,只有来自外部的流量才会经过PREROUTING链,而本机的流量并不会被做任何的修改。解决方法就是在nat表的OUTPUT链中插入对应的代理规则。但是别着急,这样的修改我强烈不建议在远程进行,因为一个配置失误你就无法通过SSH连接到远程主机了,三思而后行。

 

Q: 为什么我的透明代理程序ss、v2ray、clash不能在docker中运行?

A: 当我们使用iptables-t nat [.....] -j REDIRECT --to-ports 1080这样的类似的命令时,iptables实际上就是将数据包的目标地址改成localhost,端口同样也改为对应的port。但是,等等,那后面内核怎么能知道数据包原始的目标地址呢?答案是,iptables在修改原始目标地址时,同样也记录了它。用户空间的应用程序可以通过一个特别的Socket选项SO_ORIGINAL_DST得到它。

回到docker上,docker的网络和host的网络在默认情况下在不同的namespace中,这就使得透明代理程序无法找到原链接的目标地址了,自然也就无法正常工作。在这种情况下,你会发现该应用程序的套接字消耗的很快,因为这时网络成了一个环。解决办法也很简单,在启动docker时加上一个选项即可--net host。

 关注JSRC

获取更多“技术干货”