CVE-2024-21795
A heap-based buffer overflow vulnerability exists in the .egi parsing functionality of The Biosig Project libbiosig 2.5.0 and Master Branch (ab0ee111). A specially crafted .egi file can lead to arbitrary code execution. An attacker can provide a malicious file to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
The Biosig Project libbiosig 2.5.0
The Biosig Project libbiosig Master Branch (ab0ee111)
libbiosig - https://biosig.sourceforge.net/index.html
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-122 - Heap-based Buffer Overflow
Libbiosig is an open source library designed to process various types of medical signal data (EKG, EEG, etc) within a vast variety of different file formats. Libbiosig is also at the core of biosig APIs in Octave and Matlab, sigviewer, and other scientific software utilized for interpreting biomedical signal data.
When reading in or writing out data of any filetype, libbiosig will always end up hitting the sopen_extended
function:
HDRTYPE* sopen_extended(const char* FileName, const char* MODE, HDRTYPE* hdr, biosig_options_type *biosig_options) {
/*
MODE="r"
reads file and returns HDR
MODE="w"
writes HDR into file
*/
This is where the vast majority of parsing logic is for most file types, albeit with some exceptions to this generalization which end up calling more specific sopen_*
functions. Regardless, unless specifically stated, it’s safe to assume we’re somewhere in this extremely large function. The general flow of sopen_extended
is as one might expect: initialize generic structures, figure out what file type we’re dealing with, parse the filetype, and finally populate the generic structures that can be utilized by whatever is calling sopen_extended
. To determine file type, sopen_extended
calls getfiletype
, which goes through a list of magic byte comparisons. Alternatively we could force a particular file type, but this is generally more useful when writing data to a file.
Moving on from the generic overview, we can get to be more specific. For today’s example we’re dealing with the parsing code of the .egi
file format, a format seemingly adapted for compatibility with the commercial MagStim EGI Net Station software suite. To determine if a file is a .egi
file, getfiletype
runs the following check:
else if ((beu32p(hdr->AS.Header) > 1) && (beu32p(hdr->AS.Header) < 8) && !hdr->AS.Header[6] && !hdr->AS.Header[8] && !hdr->AS.Header[10] && !hdr->AS.Header[12] && !hdr->AS.Header[14] && !hdr->AS.Header[26] ) {
/* sanity check: the high byte of month, day, hour, min, sec and bits must be zero */
hdr->TYPE = EGI;
hdr->VERSION = hdr->AS.Header[3];
}
Since there doesn’t seem to be any sort of magic bytes, there’s just a simple sanity check on a set of high bytes. Regardless, assuming we hit this code branch, the code flow in sopen_extended
looks as such:
else if (hdr->TYPE==EGI) {
fprintf(stdout,"Reading EGI is under construction\n"); // [1]
uint16_t NEC = 0; // specific for EGI format
uint16_t gdftyp = 3;
// BigEndian
hdr->FILE.LittleEndian = 0;
hdr->VERSION = beu32p(hdr->AS.Header); // [2]
// [...]
hdr->SampleRate = beu16p(hdr->AS.Header+20);
hdr->NS = beu16p(hdr->AS.Header+22);
// uint16_t Gain = beu16p(Header1+24); // not used
uint16_t Bits = beu16p(hdr->AS.Header+26);
uint16_t PhysMax= beu16p(hdr->AS.Header+28);
size_t POS;
It’s appropriately stated that reading this file format is under construction [1], but since that fprintf
doesn’t stop us from hitting this code path based on our file input, it doesn’t particularly matter for our purposes. Tangent aside, out of the fields read in this portion, only the hdr->VERSION
is important [2], since it determines how far we must read to get to our other important input data. Continuing immediately after, still within sopen_extended
:
if (hdr->AS.Header[3] & 0x01) // [3]
{ // Version 3,5,7
POS = 32;
for (k=0; k < beu16p(hdr->AS.Header+30); k++) { // [4]
char tmp[256];
int len = hdr->AS.Header[POS]; // [5]
strncpy(tmp,Header1+POS,len);
tmp[len]=0;
if (VERBOSE_LEVEL>7)
fprintf(stdout,"EGI categorie %i: <%s>\n",(int)k,tmp);
POS += *(hdr->AS.Header+POS); // skip EGI categories
if (POS > count-8) {
hdr->AS.Header = (uint8_t*)realloc(hdr->AS.Header,2*count);
count += ifread(hdr->AS.Header,1,count,hdr);
}
}
hdr->NRec= beu16p(hdr->AS.Header+POS);
hdr->SPR = beu32p(hdr->AS.Header+POS+2);
NEC = beu16p(hdr->AS.Header+POS+6); // EGI.N // [6]
POS += 8;
}
else
{ // Version 2,4,6
hdr->NRec = beu32p(hdr->AS.Header+30);
NEC = beu16p(hdr->AS.Header+34); // EGI // [7]
hdr->SPR = 1;
/* see also end-of-sopen
hdr->AS.spb = hdr->SPR+NEC;
hdr->AS.bpb = (hdr->NS + NEC)*GDFTYP_BITS[hdr->CHANNEL[0].GDFTYP]>>3;
*/
POS = 36;
}
Assuming our hdr->VERSION
is odd, we hit the more complex branch at [3]. Depending on hdr->AS.Header+0x1E
[4], up to 0xFFFF fields can be skipped over, each of which can be 0xFF bytes long [5]. Again, this portion only matters because it determines where we read in the important field, NEC
at [6], which is read in at wherever our POS
is and then add 0x6. Assuming that the hdr->VERSION
is even, then our task of finding the NEC
is easier, since it’s at a static hdr->AS.Header+0x22
[7]. In either case, now that we have our uint16_t NEC
variable populated, let’s see why it’s important. Immediately following the above code, still in sopen_extended
:
/* read event code description */
hdr->AS.auxBUF = (uint8_t*) realloc(hdr->AS.auxBUF,5*NEC);
hdr->EVENT.CodeDesc = (typeof(hdr->EVENT.CodeDesc)) realloc(hdr->EVENT.CodeDesc,257*sizeof(*hdr->EVENT.CodeDesc)); // [8]
hdr->EVENT.CodeDesc[0] = ""; // typ==0, is always empty
hdr->EVENT.LenCodeDesc = NEC+1;
for (k=0; k < NEC; k++) { // [9]
memcpy(hdr->AS.auxBUF+5*k,Header1+POS,4); // [10]
hdr->AS.auxBUF[5*k+4]=0;
hdr->EVENT.CodeDesc[k+1] = (char*)hdr->AS.auxBUF+5*k; // [11]
POS += 4;
}
At [8], we allocate a static length buffer of size 257*sizeof(*hdr->EVENT.CodeDesc)
, which ends up being 0x808 on 64-bit machines and 0x404 on 32-bit machines, since hdr->EVENT.CodeDesc
is a Char **
. Next, based on our paramount NEC
variable, we loop around [9] a variable amount of times, each time writing five bytes into our appropriately allocated hdr->AS.auxBUF
[10], and then writing a pointer in the hdr->EVENT.CodeDesc
buffer that points to these four bytes. Hopefully the flaw in this logic can be seen, as if we pass in any NEC
variable that is greater than 0x101 , then we will start to write out-of-bounds from our statically sized hdr->EVENT.CodeDesc
heap buffer, a flaw that can potentially lead to arbitrary code execution, depending on the setup of the heap.
=================================================================
==24242==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61d000000888 at pc 0x7ffff75daf89 bp 0x7fffffffad30 sp 0x7fffffffad28
WRITE of size 8 at 0x61d000000888 thread T0
[Detaching after fork from child process 24246]
#0 0x7ffff75daf88 in sopen_extended /biosig/stable_release/biosig-2.5.0/biosig4c++/biosig.c:7579:29
#1 0x55555566d35f in LLVMFuzzerTestOneInput /biosig/stable_release/./fuzz_biosig.cpp:84:20
#2 0x5555555934d3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/biosig/stable_release/biosig_fuzzer.bin+0x3f4d3) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#3 0x55555557d24f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/biosig/stable_release/biosig_fuzzer.bin+0x2924f) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#4 0x555555582fa6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/biosig/stable_release/biosig_fuzzer.bin+0x2efa6) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#5 0x5555555acdc2 in main (/biosig/stable_release/biosig_fuzzer.bin+0x58dc2) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#6 0x7ffff7029d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#7 0x7ffff7029e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#8 0x555555577b14 in _start (/biosig/stable_release/biosig_fuzzer.bin+0x23b14) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
0x61d000000888 is located 0 bytes to the right of 2056-byte region [0x61d000000080,0x61d000000888)
allocated by thread T0 here:
#0 0x55555562ff76 in __interceptor_realloc (/biosig/stable_release/biosig_fuzzer.bin+0xdbf76) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#1 0x7ffff75dada7 in sopen_extended /biosig/stable_release/biosig-2.5.0/biosig4c++/biosig.c:7573:55
#2 0x55555566d35f in LLVMFuzzerTestOneInput /biosig/stable_release/./fuzz_biosig.cpp:84:20
#3 0x5555555934d3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/biosig/stable_release/biosig_fuzzer.bin+0x3f4d3) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#4 0x55555557d24f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/biosig/stable_release/biosig_fuzzer.bin+0x2924f) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#5 0x555555582fa6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/biosig/stable_release/biosig_fuzzer.bin+0x2efa6) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#6 0x5555555acdc2 in main (/biosig/stable_release/biosig_fuzzer.bin+0x58dc2) (BuildId: 9ffac83f55dadf5472f09c72de5ba7a7aa4860e0)
#7 0x7ffff7029d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
SUMMARY: AddressSanitizer: heap-buffer-overflow /biosig/stable_release/biosig-2.5.0/biosig4c++/biosig.c:7579:29 in sopen_extended
Shadow bytes around the buggy address:
0x0c3a7fff80c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff80d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff80e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff80f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c3a7fff8100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c3a7fff8110: 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff8120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff8130: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff8140: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff8150: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c3a7fff8160: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
==24242==ABORTING
[Thread 0x7ffff3df9640 (LWP 24245) exited]
[Inferior 1 (process 24242) exited with code 01]
The vendor provided a new release at: https://biosig.sourceforge.net/download.html
2024-02-05 - Initial Vendor Contact
2024-02-05 - Vendor Disclosure
2024-02-19 - Vendor Patch Release
2024-02-20 - Public Release
Discovered by Lilith >_> of Cisco Talos.