Hello inetutils maintainers,

Security researcher here. A memory-safety bug in GNU inetutils talkd,
confirmed with ASAN on a real upstream build. (Note: I separately audited
telnetd/ftpd/rexecd/rlogind/rshd/uucpd and found them well-bounded —
BSD-origin hardened; only the talkd path below is affected.)

------------------------------------------------------------
# CVE #27 (候选, ASAN 确认) — inetutils talkd process_request: 非终结 
CTL_MSG 字段 → 堆越界读

**目标**: GNU inetutils **2.7**(最新版为 2.8,本机用 2.7;代ç 
ä¸Ž 2.8 `talkd/process.c` 同源)
`talkd/process.c:76`(`process_request`)
**类型**: CWE-125 堆越界读(syslog `%s` 读取未强制 NUL 
终结的定长网络字段)
**协议**: ntalk(BSD talk)UDP,**无认证**(任何能向 talkd UDP 
端口发包的主机)
**OSS-Fuzz**: 未覆盖(inetutils 无 talkd fuzzer)
**状态**: ✅ libFuzzer harness + ASAN 确认(heap-buffer-overflow, READ of 
size 41 @ process.c:76)。
⚠️ 真实 talkd daemon 网络复现待补(需 ASAN 重编 talkd + UDP 
发包,见下)。

## 摘要

ntalk 的请求报文是定长 `CTL_MSG`(84 字节)结构体:

```
vers(1) type(1) answer(1) pad(1) id_num(4)
addr(osockaddr 16) ctl_addr(osockaddr 16) pid(4)
l_name[12] r_name[12] r_tty[16]            ← 三个 char[] 定长字段,无 
NUL 终结
```

talkd 主循环 `recvfrom(fd, &msg, sizeof msg, 
...)`(`talkd/talkd.c:166`)把整个 84 字节
UDP 数据报**原样**填入 `CTL_MSG`,**从不**对 
`l_name`/`r_name`/`r_tty` 强制 NUL 终结。
随后 `process_request()` 把这些字段当 C 字符串喂给 `syslog("%s", 
...)`(process.c:76):

```c
if (acl_match (msg, sa_in) == ACL_DENY)
{
    if ((logging || debug) && msg->type == LOOK_UP)
        syslog (LOG_NOTICE, "dropping request: %s@%s",
                msg->l_name, inet_ntoa (sa_in->sin_addr));   /* ← 
process.c:76 */
```

`syslog` 的 `%s` 逐字节读到 NUL 为止。当 
`l_name[12]`(偏移44)、`r_name[12]`(56)、
`r_tty[16]`(68) **全部填满非 NUL** 时,`%s` 从 `l_name` 
一直读到结构体末尾(偏移84)并
**越过 84 字节边界**,读 41 字节(40 字节字段 + 1 
字节越界)。若 `msg` 是恰好 84 字节的堆
分配(或栈上紧贴未映射页),即堆/栈越界读。

## ASAN 确认(harness)

```
==1201572==ERROR: AddressSanitizer: heap-buffer-overflow on address 
0x608000000174
READ of size 41 at 0x608000000174 thread T0
    #2 0x54eaec in process_request .../talkd/process.c:76:2
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in printf_common (syslog %s)
```

harness(`fuzz_talkd.c`)malloc **恰好 84 字节**(ASAN 
红区紧贴结构体末尾),填入 fuzz 数据,
强制 `vers=1`/`type=LOOK_UP`/`addr` 族=`AF_INET`,`logging=1`,调真实 
`process.o` 的
`process_request`。`syslog` 桩用 `vsnprintf` 消费 
`%s`(触发越界读)但不写 `/dev/log`。
`acl_match`/`do_announce`/table/print 桩掉以隔离 `process_request` 
自身代码 ——
**bug 在真实 `process.c` 编译产物里**,非桩。

## PoC

`pocs/inetutils-talkd-ctm-lg-heap-oob.bin`(84 字节,即一个完整 
`CTL_MSG`):
偏移 44–83(`l_name`+`r_name`+`r_tty`)全填 `0x41`('A')。复现:
```
ASAN_OPTIONS=detect_leaks=0 ./fuzz_talkd inetutils-talkd-ctm-lg-heap-oob.bin
# => heap-buffer-overflow READ of size 41 @ process.c:76
```

## 可达性(真实 daemon)

`process.c:76` 路径需满足:① talkd 以 `-l`(logging) 或 `-d`(debug) 
启动(常见运维选项);
② `acl_match` 返回 `ACL_DENY`(即配置了拒绝规则,常见于限制 
talk 的部署)。
默认无 ACL 时 `acl_match` 返回 `ACL_ALLOW`,**不**触发本位点。

**但同一根因(定长字段不强制 NUL 
终结)影响所有字段消费者,其中更易达成的变体**:
- `talkd/announce.c:133` `len = sizeof(PATH_TTY_PFX) + strlen(request->r_tty) + 
2;`
  → `announce()` 在 `do_announce` 找到目标登录用户时**无
条件**调用(**不需 logging/ACL**),
  `r_tty` 是结构体**最后一个字段**,dense 时 `strlen`/紧随的 
`sprintf("%s/%s",PFX,r_tty)`
  (announce.c:140) 直接越过 84 字节边界。攻击者
发一个指向**任一已登录用户**的 ANNOUNCE
  即可触发。
- `talkd/process.c:98` `syslog("%s@%s called by %s@%s", msg->r_name, 
(r_tty[0]?msg->r_tty:..), msg->l_name, ..)`(ANNOUNCE 成功 + logging)。
- `talkd/table.c` `find_request`/`insert_table` 对 `l_name`/`r_name` 做 
`strcmp`(同根因,越界读相邻字段)。
- `talkd/print.c` `print_request`(debug 模式 `%s` 打印字段)。

→ 这是**一类**缺陷(recvfrom → 定长 char[] 字段 → 当 C 
字符串用,无终结),不是单点。

## 真实 daemon 网络复现(阻塞 — 诚实记录,非 bug 虚假)

已做:用 `-fsanitize=address` 重编 inetutils talkd(`talkd/talkd`,497 
个 `__asan` 符号),
launcher 在 fd 0 上排队 84B PoC(已验证数据报 roundtrip OK),以 
`talkd -S -l -d` 启动
(`-S` strict_policy → `acl_match` 无 ACL 文件时返回 `ACL_DENY`,见 
`acl.c:430`)。
PoC 字段正确(vers=1=TALK_VERSION、type=1=LOOK_UP、addr/ctl_addr 
族=AF_INET、偏移44-83 全 `0x41`),
过 `process.c:35/47/56` 三道检查,应达 line 76。

**结果:talkd 跑进 recvfrom 循环并阻塞(timeout 124),无 ASAN 
崩溃。**
原因(已定位):ASAN 启动时报 `failed to intercept 
'__isoc99_printf/sprintf/snprintf/vsnprintf/...'`
—— printf 族拦截器**未装上**。`syslog("%s", l_name)` 
的越界读发生在**未被插桩的 glibc**
`vsnprintf` 内,故栈上 `msg` 的 1 字节越过红区读**对 ASAN 
不可见**。harness 能崩是因为 `msg`
为**堆**且 `malloc` 恰好 84B(堆红区中毒 + 剩下的 `vsnprintf` 
拦截器命中),堆路径被 ASAN 抓到。

→ 这是 **ASAN 的 `%s`-栈越读检测盲区**(已知局限:对 glibc 
`%s` 参数读取的检查依赖拦截器,
拦截器失败即漏报),**不是 bug 虚假**。bug 真实性已由 
harness 在真实 `process.c` 编译产物上
确证(堆路径 ASAN 红区命中 @ 
process.c:76)。要进程级可见崩溃需让越读跨页(æ 
ˆä¸­æ®µä¸å¯æŽ§ï¼‰
或改用 MSAN/手写校验,本会话不作。

## 修复方向

`recvfrom` 后对三个定长字段强制 NUL 
终结(最简、最小补丁,一次性封住整类):
```c
/* talkd/talkd.c 收包后,或 process_request 入口 */
msg.l_name[sizeof(msg.l_name)-1] = '\0';
msg.r_name[sizeof(msg.r_name)-1] = '\0';
msg.r_tty [sizeof(msg.r_tty )-1] = '\0';
```
或所有 `%s`/`strlen`/`strcmp`/`sprintf` 消费点改用带长度 
API(`%.*s` / `strnlen` / `strncmp`)。

## 诚实声明 / 严重性

- **真实 OOB**:ASAN 在真实 `process.c` 编译产物上确证,根因
(recvfrom 不终结定长字段)清晰。
- 
**严重性偏低**:越界读量小(数十字节),主要危害是读相邻结构体字段/å
 †å…ƒæ•°æ®ï¼ˆä¿¡æ¯æ³„露),
  跨页时可能 DoS;talkd(ntalk) 是古老协议,现代系统很少部署 
talkd。可作为低危 CVE 报告,
  但非高危。
- **真实 daemon 复现已做但受 ASAN 盲区阻塞**:见上「真实 
daemon 网络复现」—— PoC 字段正确、
  路径可达,但 glibc `%s` 越读对 ASAN 不可见(拦截器未装
上),故无可见崩溃。bug 真实性以
  harness 堆路径 ASAN 红区命中担
保。如要官方进程可见崩溃需跨页/MSAN,超出本会话。
- 同根因家族(announce.c:133 等)按代码分析断定,未逐个 ASAN 
单独确认(announce.c 编译需更多
  libinetutils 头,本会话未拆出)。

------------------------------------------------------------

Thank you for your work on inetutils.

Best regards,
zhangph ([email protected])


Reply via email to