# 简介
WinDivert是一种用户模式的数据包捕获和转移包,可用于windows vista、windows 2008、windows 7、windows 8 和 windows 10。WinDivert允许用户模式应用程序捕获、修改、丢弃从windows网络堆栈发送的网络数据包,而无需编写内核模式代码。
主要有以下功能:
1. 捕获网络数据包
2. 筛选/丢弃网络数据包
3. 嗅探网络数据包
4. 注入网络数据包
5. 修改网络数据包
主要特点:可抓包,修改包
**注意:需要管理员权限!!!**
原理图如上,WinDivert.sys 驱动安装在Windows network stack 下,其具体步骤为:
- 一个新网络包进入到network stack 之后会被WinDivert.sys 截获;
- 如果截获的包被 PROGRAM-defined 过滤器匹配成功,该包将会被重定向,PROGRAM 就会调用 WinDivertRecv() 方法来读取包;如果匹配失败,截获的包将不会被重定向;
- PROGRAM 将对包进行 drops,modify, 或者 re-inject 操作。
# 基本api
## WinDivertOpen 打开
打开一个windivert对象,返回一个对象指针。打开的过程中,需要指定过滤规则,过滤层,过滤器的优先级,以及windivert对象的工作模式。
```c++
WINDIVERTEXPORT HANDLE WinDivertOpen(
__in const char *filter,
__in WINDIVERT_LAYER layer,
__in INT16 priority,
__in UINT64 flags);
```
filter:过滤条件,类似于tcpdump的抓包条件
layer:过滤层,一般network和forward,network类似于input和output的链(本机收和本机发),forward是收发本机转发包
```c++
typedef enum
{
WINDIVERT_LAYER_NETWORK = 0, /* Network layer. */
WINDIVERT_LAYER_NETWORK_FORWARD = 1,/* Network layer (forwarded packets) */
WINDIVERT_LAYER_FLOW = 2, /* Flow layer. */
WINDIVERT_LAYER_SOCKET = 3, /* Socket layer. */
WINDIVERT_LAYER_REFLECT = 4, /* Reflect layer. */
} WINDIVERT_LAYER, *PWINDIVERT_LAYER;
```
priority:优先级,值越大,优先级越高
flags:windivert对象到底是用于监听、丢包、还是修改包等模式,可选WINDIVERT_FLAG_SNIFF表示仅仅收包不修改原包,原包还是正常发送
```c++
#define WINDIVERT_FLAG_SNIFF 0x0001
#define WINDIVERT_FLAG_DROP 0x0002
#define WINDIVERT_FLAG_RECV_ONLY 0x0004
#define WINDIVERT_FLAG_READ_ONLY WINDIVERT_FLAG_RECV_ONLY
#define WINDIVERT_FLAG_SEND_ONLY 0x0008
#define WINDIVERT_FLAG_WRITE_ONLY WINDIVERT_FLAG_SEND_ONLY
#define WINDIVERT_FLAG_NO_INSTALL 0x0010
#define WINDIVERT_FLAG_FRAGMENTS 0x0020
```
打开这个windivert对象 后就可以进行收包了
## WinDivertRecv 接收原包
```c++
WINDIVERTEXPORT BOOL WinDivertRecv(
__in HANDLE handle,
__out_opt VOID *pPacket,
__in UINT packetLen,
__out_opt UINT *pRecvLen,
__out_opt WINDIVERT_ADDRESS *pAddr);
```
接收特定WinDivert对象的捕获的包的函数, 这里符合过滤条件的包都会依次被此接口收上来处理
## WinDivertHelperParsePacket 解析原包信息
```c++
WINDIVERTEXPORT BOOL WinDivertHelperParsePacket(
__in const VOID *pPacket,
__in UINT packetLen,
__out_opt PWINDIVERT_IPHDR *ppIpHdr,
__out_opt PWINDIVERT_IPV6HDR *ppIpv6Hdr,
__out_opt UINT8 *pProtocol,
__out_opt PWINDIVERT_ICMPHDR *ppIcmpHdr,
__out_opt PWINDIVERT_ICMPV6HDR *ppIcmpv6Hdr,
__out_opt PWINDIVERT_TCPHDR *ppTcpHdr,
__out_opt PWINDIVERT_UDPHDR *ppUdpHdr,
__out_opt PVOID *ppData,
__out_opt UINT *pDataLen,
__out_opt PVOID *ppNext,
__out_opt UINT *pNextLen);
```
解析的形成的数据包如下格式
```
typedef struct
{
UINT32 HeaderLength:17;
UINT32 FragOff:13;
UINT32 Fragment:1;
UINT32 MF:1;
UINT32 PayloadLength:16;
UINT32 Protocol:8;
UINT32 Truncated:1;
UINT32 Extended:1;
UINT32 Reserved1:6;
PWINDIVERT_IPHDR IPHeader;
PWINDIVERT_IPV6HDR IPv6Header;
PWINDIVERT_ICMPHDR ICMPHeader;
PWINDIVERT_ICMPV6HDR ICMPv6Header;
PWINDIVERT_TCPHDR TCPHeader;
PWINDIVERT_UDPHDR UDPHeader;
UINT8 *Payload;
} WINDIVERT_PACKET, *PWINDIVERT_PACKET;
```
解析完后就可以进行修改了,比如修改5元祖
## WinDivertHelperCalcChecksums
修改后的包重新进行校验和计算
```c++
WINDIVERTEXPORT BOOL WinDivertHelperCalcChecksums(
__inout VOID *pPacket,
__in UINT packetLen,
__out_opt WINDIVERT_ADDRESS *pAddr,
__in UINT64 flags);
```
## WinDivertSend 发送原包
```c++
WINDIVERTEXPORT BOOL WinDivertSend(
__in HANDLE handle,
__in const VOID *pPacket,
__in UINT packetLen,
__out_opt UINT *pSendLen,
__in const WINDIVERT_ADDRESS *pAddr);
```
校验和重新计算后的包就会重新发送
windivert源码也有一些实例,都是c写的,这里不再过多介绍
# snat实现
PyDivert基于python 的WinDivert实现,我们以python这种简单语言快速验证,实例类似于iptables中的snat功能。
我们实现的场景如下: A和B设备通过wan或者隧道(wifi也行)连接,A设备的lan口连接内网服务器和打印机等,由于打印机无法配置路由(与设备A的lan同网段),因此设备B想通打印机需要设备A做snat. 这样回包才能达到A后再打到B
这里我们需要把ipv4的主要协议包icmp、tcp、udp代理,需要snat的网络接口设备A的lan口,假设ifIdx为12, 在设备A上首先需要抓取ifIdx口forward出去的数据包,其次在修改了src_ip后,由于打印机回包是dst_ip会是设备A的lan接口ip,要捕获这个包,还需要抓取network上进来的包,在进来的数据包上做还原,这样dst_ip被还原后才能从A回到B
过滤条件为
```python
outgoing_filter = f"ip and (tcp or udp or icmp) and ifIdx == 12" layer = forward
incoming_filter = f"ip and (tcp or udp or icmp) and ifIdx == 12" layer = network
NEW_SRC_IP = '192.168.1.1' #ifIdx=12的接口ip
```
其次,由于需要还原包,我们要根据连接信息进行匹配还原,类似于linux连接跟踪conntrack, 由于线程共用connections,还需要锁机制保护
```python
connections = [key:['proto', 'orig_src_ip', 'orig_src_port', 'orig_dst_ip', 'orig_dst_port', 'nat_src_ip']....]
connection_lock = threading.Lock()
```
这里连接跟踪为一个dict,key为连接跟踪信息,后续回包需要根据此key找到对应的value, 还原出ip,注意有与icmp和tcp/udp的协议字段不一致,需要做区分, tcp及udp用五元组做key, icmp用ip及icmp_id做key
这里我们起2个线程分别抓forward和network的包
```python
thread_forward = threading.Thread(target=handle_forward_layer)
thread_network = threading.Thread(target=handle_network_layer)
```
处理forward和network的接口如下
```python
def handle_forward_layer():
with pydivert.WinDivert(outgoing_filter,1,0,0) as w_forward:
while True:
packet = w_forward.recv()
if packet.direction == pydivert.Direction.OUTBOUND:
ip_header = packet.ipv4
tcp_header = packet.tcp
udp_header = packet.udp
icmp_header = packet.icmpv4
with connection_lock:
# 处理外网到内网的包,修改源 IP 为 LAN IP
if tcp_header is not None:
# 构建正向和反向键
#print("origin forward %s packet, TCP %s:%s --> %s:%s" % (a_1,ip_header.src_addr,tcp_header.src_port,ip_header.dst_addr,tcp_header.dst_port))
rev_key = ('TCP', ip_header.dst_addr, tcp_header.dst_port, NEW_SRC_IP, tcp_header.src_port)
connections[rev_key] = ConnectionInfo('TCP', ip_header.src_addr, tcp_header.src_port, ip_header.dst_addr, tcp_header.dst_port, NEW_SRC_IP, current_time)
ip_header.src_addr = NEW_SRC_IP
#print("modify forward %s packet, TCP %s:%s --> %s:%s" % (a_1,ip_header.src_addr,tcp_header.src_port,ip_header.dst_addr,tcp_header.dst_port))
elif udp_header is not None:
#print("origin forward %s packet, UDP %s:%s --> %s:%s" % (a_1,ip_header.src_addr,udp_header.src_port,ip_header.dst_addr,udp_header.dst_port))
rev_key = ('UDP', ip_header.dst_addr, udp_header.dst_port, NEW_SRC_IP, udp_header.src_port)
connections[rev_key] = ConnectionInfo('UDP', ip_header.src_addr, udp_header.src_port, ip_header.dst_addr, udp_header.dst_port, NEW_SRC_IP, current_time)
ip_header.src_addr = NEW_SRC_IP
#print("modify forward %s packet, TCP %s:%s --> %s:%s" % (a_1,ip_header.src_addr,udp_header.src_port,ip_header.dst_addr,udp_header.dst_port))
elif icmp_header is not None:
icmp_id = struct.unpack('!H', icmp_header.payload[0:2])[0]
#print("origin forward %s packet, ICMP %s --> %s %s" % (a_1,ip_header.src_addr,ip_header.dst_addr,icmp_id))
rev_key = ('ICMP', ip_header.dst_addr, icmp_id, NEW_SRC_IP)
connections[rev_key] = ConnectionInfo('ICMP', ip_header.src_addr, 0, ip_header.dst_addr, 0, NEW_SRC_IP, current_time)
ip_header.src_addr = NEW_SRC_IP
#print("modify forward %s packet, ICMP %s --> %s %s" % (a_1,ip_header.src_addr,ip_header.dst_addr,icmp_id))
packet.recalculate_checksums()
w_forward.send(packet)
```
```python
def handle_network_layer():
with pydivert.WinDivert(incoming_filter,0,0,0) as w_network:
while True:
packet = w_network.recv()
# 从此接口进来的包需要做还原
if packet.direction == pydivert.Direction.INBOUND:
ip_header = packet.ipv4
tcp_header = packet.tcp
udp_header = packet.udp
icmp_header = packet.icmpv4
with connection_lock:
if tcp_header is not None:
#print("origin network %s packet, TCP %s:%s --> %s:%s" % (a_2,ip_header.src_addr,tcp_header.src_port,ip_header.dst_addr,tcp_header.dst_port))
key = ('TCP', ip_header.src_addr, tcp_header.src_port, ip_header.dst_addr, tcp_header.dst_port)
if key in connections:
nat_info = connections[key]
ip_header.dst_addr = nat_info.orig_src_ip
updated_conn_info = nat_info._replace(last_access_time=current_time)
connections[key] = updated_conn_info
#print("modify network %s packet, TCP %s:%s --> %s:%s" % (a_2,ip_header.src_addr,tcp_header.src_port,ip_header.dst_addr,tcp_header.dst_port))
elif udp_header is not None:
#print("origin network %s packet, UDP %s:%s --> %s:%s" % (a_2,ip_header.src_addr,udp_header.src_port,ip_header.dst_addr,udp_header.dst_port))
key = ('UDP', ip_header.src_addr, udp_header.src_port, ip_header.dst_addr, udp_header.dst_port)
if key in connections:
nat_info = connections[key]
ip_header.dst_addr = nat_info.orig_src_ip
updated_conn_info = nat_info._replace(last_access_time=current_time)
connections[key] = updated_conn_info
#print("modify network %s packet, UDP %s:%s --> %s:%s" % (a_2,ip_header.src_addr,udp_header.src_port,ip_header.dst_addr,udp_header.dst_port))
elif icmp_header is not None:
icmp_id = struct.unpack('!H', icmp_header.payload[0:2])[0]
#print("origin network %s packet, ICMP %s --> %s %s" % (a_2,ip_header.src_addr,ip_header.dst_addr,icmp_id))
key = ('ICMP', ip_header.src_addr, icmp_id, ip_header.dst_addr)
if key in connections:
nat_info = connections[key]
ip_header.dst_addr = nat_info.orig_src_ip
updated_conn_info = nat_info._replace(last_access_time=current_time)
connections[key] = updated_conn_info
#print("modify network %s packet, ICMP %s --> %s %s" % (a_2,ip_header.src_addr,ip_header.dst_addr,icmp_id))
packet.recalculate_checksums()
w_network.send(packet)
```
按照这个逻辑后,就可以实现snat功能
当然连接跟踪还需要考虑性能和超时时间等,当前demo仅做参考