CVE-2025-66048,CVE-2025-66043,CVE-2025-66047,CVE-2025-66044,CVE-2025-66046,CVE-2025-66045
Several stack-based buffer overflow vulnerabilities exists in the MFER parsing functionality of The Biosig Project libbiosig 3.9.1. A specially crafted MFER file can lead to arbitrary code execution. An attacker can provide a malicious file to trigger these vulnerabilities.
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 3.9.1
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-121 - Stack-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.
Within libbiosig, the sopen_extended function is the common entry point for file parsing, regardless of the specific file type:
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
*/
The general flow of sopen_extended is as one might expect: initialize generic structures, determine the relevant file type, parse the file, and finally populate the generic structures that can be utilized by whatever is calling sopen_extended. To determine the file type, sopen_extended calls getfiletype, which attempts to fingerprint the file based on the presence of various magic bytes within the header. Libbiosig also allows for these heuristics to be bypassed by setting the file type manually, but that approach is more applicable when writing data to a file; these vulnerabilities concern the code path taken when reading from a file.
The file type used to exercise these vulnerabilities is the Medical waveform Format Encoding Rules (MFER), a file format for encoding medical waveforms from various different kinds of medical devices, such as ECG and EEG. The MFER standard aims to abstract away the encoding of the waveform data itself, independent of the specifics of the recording device.
To determine if the input file is valid MFER, getfiletype runs the following check:
else if (!memcmp(Header1,"@ MFER ",8))
hdr->TYPE = MFER;
else if (!memcmp(Header1,"@ MFR ",6))
hdr->TYPE = MFER;
Put simply, libbiosig classifies an input file as MFER if the first few bytes match one of two magic byte sequences and stores the file classification in the struct member hdr->TYPE.
Further along in sopen_extended, after getfiletype has returned, hdr->TYPE is checked again and, if it’s MFER, processing unique to the format is then performed. This code block starts by defining some temporary variables that will be used for parsing the MFER file, including a statically-sized 128-byte stack-allocated buffer named buf [1] which will become important later. From there, the bulk of the file processing is done inside a while loop [2] that continues so long as there are remaining bytes to be read from the input file, which hdr is a handle to:
uint8_t buf[128]; // [1]
void* ptrbuf = buf;
uint8_t gdftyp = 3; // default: int16
uint8_t UnitCode=0;
double Cal = 1.0, Off = 0.0;
char SWAP = ( __BYTE_ORDER == __LITTLE_ENDIAN); // default of MFER is BigEndian
hdr->FILE.LittleEndian = 0;
hdr->SampleRate = 1000; // default sampling rate is 1000 Hz
hdr->NS = 1; // default number of channels is 1
hdr->CHANNEL = (CHANNEL_TYPE*)realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE));
{
CHANNEL_TYPE *hc = hdr->CHANNEL+0;
hc->SPR = 0;
hc->PhysDimCode = 4275; // uV : default value in Table 5, ISO/FDIS 22077-1(E)ISO/WD 22077-1
hc->Cal = 1.0;
hc->Off = 0.0;
hc->OnOff = 1;
hc->LeadIdCode = 0;
hc->GDFTYP = 3;
hc->Transducer[0] = 0;
hc->Label[0] = 0;
}
/* TAG */
uint8_t tag = hdr->AS.Header[0];
ifseek(hdr,1,SEEK_SET);
int curPos = 1;
size_t N_EVENT=0; // number of events, memory is allocated for in the event table.
while (!ifeof(hdr)) { // [2]
uint32_t len, val32=0;
int32_t chan=-1;
uint8_t tmplen;
if (tag==255)
break;
else if (tag==63) {
/* CONTEXT */
curPos += ifread(buf,1,1,hdr);
chan = buf[0] & 0x7f;
while (buf[0] & 0x80) {
curPos += ifread(buf,1,1,hdr);
chan = (chan<<7) + (buf[0] & 0x7f);
}
}
/* LENGTH */
curPos += ifread(&tmplen,1,1,hdr);
char FlagInfiniteLength = 0;
if ((tag==63) && (tmplen==0x80)) { // [3]
FlagInfiniteLength = -1; //Infinite Length
len = 0;
}
else if (tmplen & 0x80) { // [5]
tmplen &= 0x7f;
curPos += ifread(&buf,1,tmplen,hdr);
len = 0;
k = 0;
while (k<tmplen)
len = (len<<8) + buf[k++];
}
else
len = tmplen; // [4]
The encoding rules for MFER, as described in ISO-22066-1-2022, specify data should be structured in the form Tag, Data Length, Value, or TLV. Broadly speaking, the Tag portion classifies the data, the Data Length portion encodes the length of the data in octets (bytes), and the Value portion encodes the actual payload. Of particular interest to these vulnerabiities is the Length portion, which the MFER specification allows to be encoded in one of three ways.
tmplen) and checking it against 0x80 [3].len) equal to the value of the first octet read (tmplen) [4].buf equal to the value of the first octet read (tmplen), interpreting this sequence of bytes as an integer, and storing the result in len [5].Although libbiosig’s handling of the length encoding superficially adheres to the MFER specification, there are locations in the MFER parsing code that will attempt to copy this potentially huge length variable into a small, fixed-size buffer without ensuring that the destination buffer is large enough to hold len bytes.
For this general analysis, the specific vulnerability exercised by the attached POC will be used, which occurs when the Tag is 3:
else if (tag==3) {
// character code
char v[17];
if (len>16) fprintf(stderr,"Warning MFER tag2 incorrect length %i>16\n",len); // [6]
curPos += ifread(&v,1,len,hdr); // [7]
v[len] = 0;
if (VERBOSE_LEVEL>7) fprintf(stdout,"MFER: character code <%s>\n",v);
}
In this branch, a buffer named v is allocated on the stack with a fixed size of 17 bytes. Based on the warning message [6], it appears that Tag 3 is a special case where the length should never be greater than 16. Despite this, libbiosig will continue processing using whatever length was read from the input file, and will attempt to read that many bytes from the file into v via a call to ifread (which is simply a wrapper for fread) [7]. Although libbiosig will usually output an error message when the length for a particular frame doesn’t match what is expected based on its Tag, this length check does not impact the flow of execution and thus remains a legitimate attack vector.
As mentioned previously, v is a statically-sized 17-byte stack-allocated buffer. This becomes problematic in cases where len is greater than 17 bytes. In our testing, a maliciously crafted MFER file was supplied to libbiosig where the encoded length was 0x44 (68). This was confirmed via the error message [8] printed right before the ifread call that exercises the vulnerability:
pwndbg> n
Warning MFER tag2 incorrect length 68>16 // [8]
8775 curPos += ifread(&v,1,len,hdr);
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:8775
8770 }
8771 else if (tag==3) {
8772 // character code
8773 char v[17];
8774 if (len>16) fprintf(stderr,"Warning MFER tag2 incorrect length %i>16\n",len);
► 8775 curPos += ifread(&v,1,len,hdr);
8776 v[len] = 0;
8777 if (VERBOSE_LEVEL>7) fprintf(stdout,"MFER: character code <%s>\n",v);
8778 }
8779 else if (tag==4) {
8780 // SPR
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0 0x7ffff733a31d sopen_extended+238605
1 0x5555555554c8 main+511
2 0x7ffff6a2a1ca __libc_start_call_main+122
3 0x7ffff6a2a28b __libc_start_main+139
4 0x555555555205 _start+37
From there, the program attempts to read that many bytes into v via a call to ifread [7]:
0x00007ffff72c6bdc in ifread (ptr=0x7ffff2f02160, size=1, nmemb=68, hdr=0x519000001980) at biosig.c:558
558 return(fread(ptr, size, nmemb, hdr->FILE.FID));
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:558
553 #ifdef ZLIB_H
554 if (hdr->FILE.COMPRESSION>0)
555 return(gzread(hdr->FILE.gzFID, ptr, size * nmemb)/size);
556 else
557 #endif
► 558 return(fread(ptr, size, nmemb, hdr->FILE.FID));
559 }
560
561 size_t ifwrite(void* ptr, size_t size, size_t nmemb, HDRTYPE* hdr) {
562 #ifdef ZLIB_H
563 if (hdr->FILE.COMPRESSION)
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0 0x7ffff72c6bdc ifread+12
1 0x7ffff733a345 sopen_extended+238645
2 0x5555555554c8 main+511
3 0x7ffff6a2a1ca __libc_start_call_main+122
4 0x7ffff6a2a28b __libc_start_main+139
5 0x555555555205 _start+37
This results in a stack-based buffer overflow, as shown in the following AddressSanitizer output:
Run till exit from #0 0x00007ffff72c6bdc in ifread (ptr=0x7ffff2f02160, size=1, nmemb=68, hdr=0x519000001980) at biosig.c:558
=================================================================
==65283==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff2f02171 at pc 0x7ffff787ed7f bp 0x7fffffffa7e0 sp 0x7fffffff9f88
WRITE of size 68 at 0x7ffff2f02171 thread T0
#0 0x7ffff787ed7e in fread ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996
#1 0x7ffff733a344 in sopen_extended /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:8775
#2 0x5555555554c7 in main harness.cpp:38
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_release/harness+0x1204) (BuildId: 72ee8fff2934eb1751765bd13e507472e6245218)
Address 0x7ffff2f02171 is located in stack of thread T0 at offset 8561 in frame
#0 0x7ffff72fff1f in sopen_extended /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:3720
This frame has 123 object(s):
[32, 33) 'gdftyp' (line 8673)
...
[8544, 8561) 'v' (line 8773)
[8608, 8628) 'tmp' (line 5323) <== Memory access at offset 8561 partially underflows this variable
[8672, 8692) 'buf' (line 8189)
...
[10896, 11153) 'buf' (line 8947)
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 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 in fread
Shadow bytes around the buggy address:
0x7ffff2f01e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7ffff2f01f00: 00 00 00 00 00 00 00 00 00 f2 f2 f2 f2 f2 f2 f2
0x7ffff2f01f80: f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 02 f2 03 f2 03 f2
0x7ffff2f02000: 04 f2 04 f2 05 f2 f2 f2 05 f2 f2 f2 05 f2 f2 f2
0x7ffff2f02080: 06 f2 f2 f2 00 f2 f2 f2 00 01 f2 f2 00 01 f2 f2
=>0x7ffff2f02100: 00 01 f2 f2 00 00 01 f2 f2 f2 f2 f2 00 00[01]f2
0x7ffff2f02180: f2 f2 f2 f2 00 00 04 f2 f2 f2 f2 f2 00 00 04 f2
0x7ffff2f02200: f2 f2 f2 f2 00 00 00 00 00 f2 f2 f2 f2 f2 00 00
0x7ffff2f02280: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 00 00
0x7ffff2f02300: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 00 00
0x7ffff2f02380: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 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
==65283==ABORTING
[Inferior 1 (process 65283) exited with code 01]
Since the data written past the end of this buffer is read from hdr, which itself is populated using data from the input file, this data is attacker-controlled. The end result is a set of vulnerabilities where an attacker can write arbitrary data past the end of a stack-allocated buffer, potentially resulting in arbitrary code execution.
==65283==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff2f02171 at pc 0x7ffff787ed7f bp 0x7fffffffa7e0 sp 0x7fffffff9f88
WRITE of size 68 at 0x7ffff2f02171 thread T0
#0 0x7ffff787ed7e in fread ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996
#1 0x7ffff733a344 in sopen_extended /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:8775
#2 0x5555555554c7 in main harness.cpp:38
#3 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_release/harness+0x1204) (BuildId: 72ee8fff2934eb1751765bd13e507472e6245218)
Address 0x7ffff2f02171 is located in stack of thread T0 at offset 8561 in frame
#0 0x7ffff72fff1f in sopen_extended /home/mbereza/Projects/BioSig/biosig-3.9.1/biosig4c++/biosig.c:3720
This frame has 123 object(s):
[32, 33) 'gdftyp' (line 8673)
...
[8544, 8561) 'v' (line 8773)
[8608, 8628) 'tmp' (line 5323) <== Memory access at offset 8561 partially underflows this variable
[8672, 8692) 'buf' (line 8189)
...
[10896, 11153) 'buf' (line 8947)
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 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:996 in fread
Shadow bytes around the buggy address:
0x7ffff2f01e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7ffff2f01f00: 00 00 00 00 00 00 00 00 00 f2 f2 f2 f2 f2 f2 f2
0x7ffff2f01f80: f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 02 f2 03 f2 03 f2
0x7ffff2f02000: 04 f2 04 f2 05 f2 f2 f2 05 f2 f2 f2 05 f2 f2 f2
0x7ffff2f02080: 06 f2 f2 f2 00 f2 f2 f2 00 01 f2 f2 00 01 f2 f2
=>0x7ffff2f02100: 00 01 f2 f2 00 00 01 f2 f2 f2 f2 f2 00 00[01]f2
0x7ffff2f02180: f2 f2 f2 f2 00 00 04 f2 f2 f2 f2 f2 00 00 04 f2
0x7ffff2f02200: f2 f2 f2 f2 00 00 00 00 00 f2 f2 f2 f2 f2 00 00
0x7ffff2f02280: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 00 00
0x7ffff2f02300: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 00 00
0x7ffff2f02380: 00 00 00 00 00 00 00 00 01 f2 f2 f2 f2 f2 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
==65283==ABORTING
[Inferior 1 (process 65283) exited with code 01]
This vulnerability manifests on line 9151 of biosig.c, when the Tag is 64:
else if (tag==64) //0x40
{
// preamble
char tmp[256]; // [1]
curPos += ifread(tmp,1,len,hdr);
In this case, the overflowed buffer is the newly-declared tmp [1] instead of buf. While tmp is larger than buf, having a size of 256 bytes, a stack overflow can still occur in cases where len is encoded using multiple octets and is greater than 256.
This vulnerability manifests on line 9162 of biosig.c, when the Tag is 65:
else if (tag==65) //0x41: patient event
{
// event table
curPos += ifread(buf,1,len,hdr);
This vulnerability manifests on line 9203 of biosig.c, when the Tag is 67:
else if (tag==67) //0x43: Sample skew
{
int skew=0; // [1]
curPos += ifread(&skew, 1, len,hdr);
In this case, the address of the newly-defined integer skew [1] is overflowed. This means a stack overflow can occur using much smaller values of len in this code path.
This vulnerability manifests on line 9245 of biosig.c, when the Tag is 131:
else if (tag==131) //0x83
{
// Patient Age
if (len!=7) fprintf(stderr,"Warning MFER tag131 incorrect length %i!=7\n",len);
curPos += ifread(buf,1,len,hdr);
This vulnerability manifests on line 9268 of biosig.c, when the Tag is 133:
else if (tag==133) //0x85
{
curPos += ifread(buf,1,len,hdr);
2025-12-10 - Vendor Disclosure
2025-12-10 - Vendor Patch Release
2025-12-11 - Public Release
Discovered by Mark Bereza and Lilith >_> of Cisco Talos.