94 lines
4.4 KiB
Markdown
94 lines
4.4 KiB
Markdown
# 拦截 TLS、放行 RDP
|
||
|
||
## 问题
|
||
|
||
RDP 不是一连上就开始 TLS:客户端先在 TPKT 里发一个 X.224 Connection Request,服务端回一个 Connection Confirm,双方借此协商安全协议([MS-RDPBCGR] 1.3.1.1),协商完才在同一条连接上开始 TLS 握手。
|
||
|
||
`tls` 分析器以前要求流的第一个字节就是 TLS record(`0x16`),碰到 RDP 开头的 `03 00` 直接放弃。结果是:
|
||
|
||
- RDP 里的 TLS 对 `tls` 规则完全不可见,没法按 RDP 单独写规则;
|
||
- 反过来,任何流量只要在 TLS 前面垫一段假的 X.224 协商,就能绕过"拦截所有 TLS"的规则。
|
||
|
||
## 做法
|
||
|
||
`tls` 分析器现在把 RDP 协商当作 hello 之前可选的一段前导来解析:
|
||
|
||
- 第一个字节是 `0x16`:和以前一样,直接当 TLS 解析。
|
||
- 第一个字节是 `0x03`:按 TPKT + X.224 解析。客户端方向要求是 Connection Request,服务端方向要求是 Connection Confirm;长度、class、协商结构任何一处不对都视为非法,分析器取消。
|
||
- 前导解析完后照常解析 ClientHello / ServerHello,record 分片重组等逻辑完全一样。
|
||
|
||
协商一解析出来就上报,不用等 TLS:
|
||
|
||
| 属性 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `tls.rdp_req.cookie` | string | Connection Request 里的 cookie,去掉 `Cookie: ` 前缀和 CRLF,如 `mstshash=alice`;没有 cookie 时不存在 |
|
||
| `tls.rdp_req.protocols` | int | RDP_NEG_REQ 里客户端请求的协议位图;没有 RDP_NEG_REQ 时为 0 |
|
||
| `tls.rdp_resp.protocol` | int | RDP_NEG_RSP 里服务端选定的协议;没有 RDP_NEG_RSP 时为 0 |
|
||
| `tls.rdp_resp.failure` | int | 服务端回的是 RDP_NEG_FAILURE 时的失败码(此时没有 `protocol`) |
|
||
|
||
协议取值:
|
||
|
||
| 值 | 名称 | 是否走 TLS |
|
||
| --- | --- | --- |
|
||
| 0 | PROTOCOL_RDP(标准 RDP 安全,RC4) | 否 |
|
||
| 1 | PROTOCOL_SSL | 是 |
|
||
| 2 | PROTOCOL_HYBRID(CredSSP / NLA) | 是 |
|
||
| 4 | PROTOCOL_RDSTLS | 是 |
|
||
| 8 | PROTOCOL_HYBRID_EX | 是 |
|
||
| 16 | PROTOCOL_RDSAAD | 是 |
|
||
|
||
## 规则
|
||
|
||
规则按顺序匹配、第一条命中的生效,所以放行 RDP 的规则要写在拦截 TLS 的前面:
|
||
|
||
```yaml
|
||
- name: allow-rdp-tls
|
||
action: allow
|
||
log: true
|
||
expr: >
|
||
tls != nil && tls.req != nil &&
|
||
tls.rdp_req != nil && tls.rdp_resp != nil &&
|
||
tls.rdp_resp.protocol in [1, 2, 4, 8, 16]
|
||
|
||
- name: block-tls
|
||
action: block
|
||
log: true
|
||
expr: tls != nil && tls.req != nil
|
||
```
|
||
|
||
放行条件同时要求:
|
||
|
||
- **`tls.req != nil`**:ClientHello 已经完整解析。只看到协商就放行的话,协商之后跟什么都行。
|
||
- **`tls.rdp_resp != nil` 且 `protocol` 是走 TLS 的协议**:真实的 RDP 只有在服务端选了 TLS 类协议之后才会发 ClientHello。服务端选了 PROTOCOL_RDP 或者回了协商失败,后面却出现 TLS,说明是伪造的,交给 `block-tls` 拦掉。
|
||
|
||
正常 RDP 的时序是 CR → CC → ClientHello,ClientHello 到的时候 `rdp_resp` 早已上报,不存在规则先看到 `req` 再看到 `rdp_resp` 的竞争。
|
||
|
||
标准 RDP 安全(`protocol == 0`)的连接里根本没有 TLS,两条规则都不会命中,按默认放行。
|
||
|
||
### 收紧放行条件
|
||
|
||
上面的规则只验证"协商格式正确、协商结果和后续 TLS 一致"。客户端和服务端串通的话,完全可以按规范发一套协商再跑任意 TLS,所以按需叠加:
|
||
|
||
```yaml
|
||
# 只放行标准端口
|
||
expr: ... && port.dst == 3389
|
||
|
||
# 只放行指定服务器
|
||
expr: ... && ip.dst in ["10.0.0.10", "10.0.0.11"]
|
||
|
||
# 要求客户端带 mstshash cookie(mstsc 通常会带,但不保证,先观察日志再启用)
|
||
expr: ... && tls.rdp_req.cookie != nil && tls.rdp_req.cookie startsWith "mstshash="
|
||
```
|
||
|
||
## 行为变化
|
||
|
||
**以前按 `tls != nil && tls.req != nil` 写的规则现在会命中 RDP。** 以前 RDP 里的 TLS 看不到,所以不受影响;现在看得到了。不想拦 RDP 的话,必须像上面那样在前面加一条放行规则。
|
||
|
||
## 局限
|
||
|
||
**只认 RDP 这一种前导。** 其他协议里的 STARTTLS(SMTP、IMAP、XMPP 等)和任何非 TLS、非 TPKT 开头的前缀仍然会让 `tls` 分析器直接放弃。要做到"拦截所有 TLS",还需要配合 `fet` 等不依赖协议开头的检测。
|
||
|
||
**RD Gateway 不在此列。** 经由 RD 网关的 RDP 跑在 443 端口的 HTTPS 上,开头就是普通 TLS,没有 X.224 协商,只能按 SNI / IP 放行。
|
||
|
||
**RDP 的 UDP 传输不在此列。** RDP-UDP 用的是 DTLS,走 UDP,与 `tls` 分析器无关。
|