概述
运维管理过程中,有时需要对网络中访问站点做限制,比如禁止访问某个网站。接下来给大家介绍一种方法 :拦截 HTTP 访问请求。本文以 http://www.demo.com
为例进行说明。
环境依赖
为了拦截 HTTP 请求,我们需要在网关处抓取数据包,并且发送我们自己的包给用户。这里我们使用 scapy,这是一个 Python 包,用来抓取和构造数据包很方便。在装 scapy 之前需要先安装 libpcap,之后即可使用如下命令安装:
# pip install scapy
通信过程分析
对于 TCP 通信,最基本的便是三次握手。为了拦截用户的 HTTP 请求,我们需要详细了解一下 HTTP 的通信过程,如图 1 为一次完整的 HTTP 请求:
图 1:HTTP通信过程
从图 1 可看出前 3 个包为三次握手过程,第 4 个包为客户端发往服务端的请求。详细构成如图 2 所示:
图 2:HTTP请求
紧接着服务端发送了 5、6 两个包。从图 2 和图 3 红色框标记的地方可知,其中第 5 个包是服务端对客户端发送的第 4 个包的应答。
图3:服务端应答报文
第 6 个包为服务端对客户端 HTTP 请求的应答。详细构成如图 4 所示:
图4:HTTP应答报文
接着第 7 个包是客户端对第 6 个包的应答报文,最后第 8-10 个包为四次挥手过程,断开连接。
拦截实现
通过上面对 HTTP 通信过程的分析,不难发现为了拦截用户特定的 HTTP 请求,需要从第 5 和第 6 个包入手。
判断一个包是否为 HTTP 请求报文
首先我们需要判断抓到的包是否为 HTTP 请求报文,因为 HTTP 报文是通过明文传输的,如图5所示。一个简单的方法是判断数据包中是否存在\r\nHost: www.demo.com\r\n
即可。
图 5:HTTP报文明文
构造自己的数据包
当收到一个数据包匹配到了我们指定的域名时,我们需要构建自己的数据包发给用户。通过上面的分析,我们至少需要构造 2 个包,图 1 中的第 5 和第 6 个包。另外,还有两点需要注意,其一:因为 HTTP 请求还是会到达服务端的,为了阻止服务端继续返回数据,我们需要再构造一个 RST 报文,断开该 TCP 连接。其二:为了阻止用户继续访问该域名,我们同样需要构造一个发往客户端的 RST 报文。最终结果如图 6 所示。
图 6:拦截结果
下面对图六中部分数据包说明:
第 5-6 个 RST 包对应第 3 个包,第 7-9 个 RST 包对应第 4 个包,为了防止丢失,我们多发了几次。
第 10 和第 15 个包是我们构造的对应第 4 个包的 HTTP 请求的应答(发送了两次)。
第 11 个包是客户端对第 10 个包的应答。
第 12 和第 14 个包是我们构造的发给客户端的RST报文。
第 13 个包则是服务端发送的FIN报文。
第 16-19 个包则是客户端和服务端产生的额外控制报文。
运行结果
图7是在我们没有拦截的情况下返回的输出,为 WWW.DEMO.COM
,图 8 是在拦截的情况下的输出,为 HELLO WORLD
。
图 7:正常情况下输出
图 8:拦截情况下输出
部分代码
最后,给出部分代码(两个用来构造包的函数),仅供大家参考。
from scapy.all import * content = '''HELLO WORLD '''
HTTP404 = '''HTTP/1.1 404 Not Found\r Server: 127.0.0.1\r Cache-Control: no-cache\r Content-Type: text/html\r Content-Length: %s\r %s ''' % (len(content), content) DF = 2 # Don't Fragment
RST = 4 # Reset
PA = 24 # Push, Acknowledgment
def genServerRstPack(pkg):
pkgs = [] eDst = pkg.dst # 以太网包 目的MAC地址 eSrc = pkg.src # 以太网包 源MAC地址 dst = pkg.payload.dst # IP包 目的IP src = pkg.payload.src # IP包 源IP idn = pkg.id # IP包 id字段 tcpPayload = pkg.payload.payload sport = tcpPayload.sport # TCP报文 sport字段 dport = tcpPayload.dport # TCP报文 dport字段 seq = tcpPayload.seq # TCP报文 seq字段 nSeq = seq + len(tcpPayload)-4*tcpPayload.dataofs
# 计算下一个包的 seq字段 for i in range(2): # 通过上面的那些字段构造要发送的包 # TCP 的 flags 字段设置为 RST = 4 p = Ether(dst=eDst, src=eSrc)/IP(dst=dst, src=src, id=idn+1, flags=DF)/TCP(sport=sport, dport=dport, seq=seq, window=0, flags=RST)/Padding("\x00\x00\x00\x00\x00\x00") pkgs.append(p)
for i in range(3): p = Ether(dst=eDst, src=eSrc)/IP(dst=dst, src=src, id=idn+2, flags=DF)/TCP(sport=sport, dport=dport, seq=nSeq, window=0, flags=RST)/Padding("\x00\x00\x00\x00\x00\x00") pkgs.append(p)
return pkgs
def genClientRstPack(pkg): pkgs = [] eDst = pkg.dst eSrc = pkg.src dst = pkg.payload.dst src = pkg.payload.src tcpPayload = pkg.payload.payload sport = tcpPayload.sport dport = tcpPayload.dport ack = tcpPayload.ack seq = tcpPayload.seq nSeq = seq + len(tcpPayload)-4*tcpPayload.dataofs # tcpPayload.dataofs 为TCP首部长度,单位为4字节 p = Ether(dst=eSrc,src=eDst)/IP(dst=src,src=dst,flags=DF)/TCP(sport=dport,dport=sport,seq=ack,ack=nSeq, window=65535, flags=PA)/Raw(HTTP404) pkgs.append(p) pkgs.append(p) p = Ether(dst=eSrc,src=eDst)/IP(dst=src,src=dst,flags=DF)/TCP(sport=dport,dport=sport,seq=ack+len(HTTP404), window=0, flags=RST)/Padding("\x00\x00\x00\x00\x00\x00") pkgs.append(p) pkgs.append(p)
return pkgs