xmurp-ua/README.md
2019-10-20 23:45:55 +08:00

6.6 KiB
Raw Blame History

技术细节

抓取数据包

模块根据握手时的第一个包SYN以后简称首包来确定是否追踪这个 TCP 流以及修改的策略(保留包含 "Windows NT" 的 UA还是替换掉所有的 UA。当首包 mark 的 0x10 位被置为 1 时,追踪这条流。当首包 mark 的 0x20 位被置为 1 时,将保留包含 "Windows NT" 的 UA否则将替换所有的 UA。

模块通过三次握手过程中的第二个包(服务端发来的 SYN ACK来确定连接已经被建立。为了使得这个数据包可以被模块捕获应当将它的 0x10 置为 1。

在确定追踪这条流的前提下,模块根据数据包的 mark 来确定是否抓取这个数据包。仅当 0x10 位被置为 1 时,模块会抓取这个数据包,否则不会抓取之。除了首包以外,0x20 位不起作用。如果一个数据包 mark 的 0x10 位被置为 1但是所属的流没有被跟踪或者这个包是服务端发来的并且不是三次握手时的 SYN ACK 包),则会发出警告并丢弃(返回 NF_DROP)这个数据包。应该避免这样的情况出现。

总之,模块通过 0x10 确定一个数据包是否被捕获通过首包SYN 且无 AKC0x20 确定修改的策略,其它数据包的 0x20 被忽略。如果要追踪一条流,应该保证所有客户端到服务端的数据包的 0x10 位被置为 1、首包的 0x20 被置为合适的值、第二个包的 0x10 被置为 1、其它 IP 协议数据包的 0x10 被置为 0。模块中不会再作除上述内容以外的过滤甚至不会判断目标端口是否为 80。在写防火墙规则时需要特殊考虑本地地址作为服务端的情况,以及(通常情况下)仅仅标记目标 80 端口的数据。

流追踪的过程

我们首先假定不会发生乱序TCP disorder和丢包的情况并假定追踪的流是合法的 HTTP 1.x 请求。捕获到首包之后,会开始追踪这条流并将状态置为 rpstm_connecting,然后返回 NF_ACCEPT。当捕获到接下来的 ACK SYN 之后,流的状态被置为 rpstm_established_sniffing 并返回 NF_ACCEPT。从此开始,每收到一个数据包,都会截留(返回 NF_STOLEN,包括包含 HTTP 头的最后一个数据包)直到捕获到整个 HTTP 头部(在应用层中读到 \r\n\r\n)。当确认捕获到整个 HTTP 头部后,会根据情况检查并修改 UA然后将截获的数据包发出将状态置为 rpstm_established_waiting(除非最后一个包就带有 PUSH后返回 NF_STOLEN。之后不含 PUSH 的数据包都将直接返回 NF_ACCEPT。当捕获到包含 PUSH 的数据包时,会将流的状态置为 established_sniffing,返回 NF_ACCEPT。以此循环,直到读到 RSTFIN,这时会放弃追踪这条流,将截留的数据包发出(如果有的话),返回 NF_ACCEPT

为了处理乱序和丢包的情况,模块会记录建立连接时的序列号,并在每次返回 NF_ACCEPT 时更新这个序列号。对于每个收到的数据包,如果序列号不符合期待,则进行判断:若在期待的序列号之前 0x80000000 的范围内,视作重传,直接返回 NF_ACCEPT;否则,说明发生了乱序,将这个包放到缓存区延迟处理,返回 NF_STOLEN。每处理完成一个数据包后,确认缓存区是否有序列号符合期待的数据包并处理。因为实际情况下

捕获过程中,如果发现 HTTP 头的长度超过 64 个数据包,或者在收集到完整的头部之前就收到 PSH、FIN 或 RST则认为不是有效的 HTTP 1.x 请求,会发出警告,将截获的数据包发出,返回 NF_ACCEPT。如果是 PSH会继续尝试处理如果是 FIN 或 RST则会不再跟踪这条流。

模块不会自动释放过期的流。

假装面向对象

仿照 MBROLA 的设计,为了让代码容易整理,用面向对象的思路。每一个头文件即是一个类(本质上是一个结构体,和很多个以这个结构体的指针为第一个参数的函数)。xxx 类的函数都以 xxx_ 开头。xxx_newxxx_del 分别是 xxx 类的构造函数和析构函数。除了 sk_buff 中的数据,变量都以本机的字节序存储。

rpStream

存储一个流的信息。

成员变量:

  • u_int8_t enum {...} status:流的状态。
  • u_int32_t id[3]:这 16 个字节按顺序存储源地址、目标地址、源端口、目标端口。
  • struct sk_buff* buff:截取的数据包。保证已经 skb_ensure_writable
  • struct sk_buff* buff_prev:由于乱序而需要暂缓处理的数据包。保证已经 skb_ensure_writable
  • u_int32_t seq:存储下一个期待收到的包的序列号。
  • u_int8_t scan_matched:在 rpstm_established_sniffing 状态下,指示直到 buff 中最后一个包的应用层末尾,匹配 "\r\n\r\n" 的字节数。一旦匹配到,则会短暂地被用来记录匹配 "User-Agent: " 等其它字符串的进度。当它没有意义时,总是被置为 0。
  • u_int8_t windows_preserve:当 UA 中包含 "Windows NT" 时,是否保留不变。实际上,如果不需要保留,根本就不需要去确认是否包含 "Windows NT"
  • struct rpStream* next:单向链表用。

成员函数:

  • struct rpStream* rpStream_new():构造函数。
  • void rpStream_del(struct rpStream*):析构函数。
  • u_int8_t rpStream_check_contain(struct rpStream*, struct sk_buff*):检查一个数据包是否属于这个流(id 是否吻合)。对于 SYN ACK,检查时会对调源和目的。
  • u_int8_t rpStream_check_retransmit(struct rpStream*, struct sk_buff*):检查一个数据包是否是重传的数据包。
  • void rpStream_append(struct rpStream*, struct sk_buff*):将一个已经确认属于这个流的数据包保存起来,可能放到 buff 也可能放到 buff_prev。会同时更新 scan_matched
  • int8_t __rpStream_check_sequence(struct rpStream*, struct sk_buff*):视为私有变量。检查一个数据包是重传(返回 -1、乱序返回 1还是正常的返回 0

其它细节

  • 为什么通过第二个包而不是第三个包来判定连接已经建立?

    因为我查了查,原则上第三个包是可以携带应用层数据的。考虑到这一点,如果用第三个判定的话,就会让步骤变得比较混乱。

  • TCP 不是不区分服务端和客户端吗?

    但是 HTTP 区分啊。

  • 为啥文档要写这么详细?

    之前在公司实习的时候,要求我写这样详细。后来我觉得这样挺好的。实际上我是先写文档后写代码的。