CVE-2025-46411
A stack-based buffer overflow vulnerability exists in the MFER parsing functionality of The Biosig Project libbiosig 3.9.0 and Master Branch (35a819fa). A specially crafted MFER 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 3.9.0
The Biosig Project libbiosig Master Branch (35a819fa)
libbiosig - https://biosig.sourceforge.net/index.html
8.1 - CVSS:3.1/AV:N/AC:H/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 strctures, 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; this vulnerability concerns the code path taken when reading from a file.
The file type used to exercise this vulnerability 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. 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. This is reflected in the MFER processing code below, with the relevant blocks for parsing the Tag [1], Length [2], and Value [3] fields annotated appropriately:
/* TAG */ // [1]
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)) {
uint32_t len, val32=0;
int32_t chan=-1;
uint8_t tmplen;
if (tag==255)
break;
else if (tag==63) {
/* CONTEXT */ // [4]
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 */ // [2]
curPos += ifread(&tmplen,1,1,hdr);
char FlagInfiniteLength = 0;
if ((tag==63) && (tmplen==0x80)) {
FlagInfiniteLength = -1; //Infinite Length
len = 0;
}
else if (tmplen & 0x80) {
tmplen &= 0x7f;
curPos += ifread(&buf,1,tmplen,hdr);
len = 0;
k = 0;
while (k<tmplen)
len = (len<<8) + buf[k++];
}
else
len = tmplen;
if (VERBOSE_LEVEL>7)
fprintf(stdout,"MFER: tag=%3i chan=%2i len=%i %3i curPos=%i %li\n",tag,chan,tmplen,len,curPos,iftell(hdr));
/* VALUE */ // [3]
if (tag==0) {
if (len!=1) fprintf(stderr,"Warning MFER tag0 incorrect length %i!=1\n",len);
curPos += ifread(buf,1,len,hdr);
}
else if (tag==1) {
...
The primary job of the TAG block is simply to extract the tag from the first octet of the header, which is then stored in the tag
variable.
From there, the LENGTH block reads the length from the input file into len
, 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].Finally, the bulk of the processing is performed in the VALUE block. The meaning of the data is dependent on the tag, so this code largely consists of numerous conditional statements that perform unique processing depending on the value of tag
. The relevant code path for this vulnerability is taken when tag is 3:
else if (tag==3) {
// character code
char v[17]; // [7]
if (len>16) fprintf(stderr,"Warning MFER tag2 incorrect length %i>16\n",len); // [6]
curPos += ifread(&v,1,len,hdr);
v[len] = 0; // [8]
if (VERBOSE_LEVEL>7) fprintf(stdout,"MFER: character code <%s>\n",v);
}
Based on the code above, it appears that frames using tag 3 carry a “character code” (encoded as a string) as their payload. Further, based on the warning message [6] and the size of the buffer used to store it (v
) [7], the character code should not exceed 16 bytes (excluding the null terminator). Despite this, libbiosig will continue processing using whatever length was read from the input file (len
), which can easily exceed 16 bytes. This becomes problematic when len
is used as the index when writing the terminating null byte to the v
buffer [8], as it results in a stack-based buffer overflow when len
is greater than 16.
This can be confirmed by supplying the attached POC (where such a condition occurs) to libbiosig as input and attaching GDB:
warning MFER tag10 incorrect length 59!=1
Warning MFER tag13 incorrect length 10>8
warning MFER tag10 incorrect length 13!=1
warning MFER tag10 incorrect length 59!=1
warning MFER tag10 incorrect length 59!=1
warning MFER tag10 incorrect length 59!=1
Warning MFER tag2 incorrect length 17>16 // [9]
Breakpoint 2, sopen_extended (FileName=<optimized out>, MODE=<optimized out>, hdr=0x519000001980, biosig_options=<optimized out>)
at biosig.c:8744
8744 curPos += ifread(&v,1,len,hdr);
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:8744
8739 }
8740 else if (tag==3) {
8741 // character code
8742 char v[17];
8743 if (len>16) fprintf(stderr,"Warning MFER tag2 incorrect length %i>16\n",len);
► 8744 curPos += ifread(&v,1,len,hdr);
8745 v[len] = 0;
8746 if (VERBOSE_LEVEL>7) fprintf(stdout,"MFER: character code <%s>\n",v);
8747 }
8748 else if (tag==4) {
8749 // SPR
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0 0x7ffff7342053 sopen_extended+270691
1 0x5555555554c8 main+511
2 0x7ffff6a2a1ca __libc_start_call_main+122
3 0x7ffff6a2a28b __libc_start_main+139
4 0x555555555205 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x v
$1 = <optimized out> // [10]
Based on the GDB output above, we can see that the value of len
is 17 [9] right before libbiosig attempts to write a null byte to v[len]
. Although the address of v
has been optimized out [10], stepping into the call to ifread
reveals it to be 0x7ffff2f02160 (the first argument):
pwndbg> s
0x00007ffff72c6bdc in ifread (ptr=0x7ffff2f02160, size=1, nmemb=17, hdr=0x519000001980) at biosig.c:558
558 return(fread(ptr, size, nmemb, hdr->FILE.FID));
Unsurprisingly, continuing execution from here trips AddressSanitizer, confirming that a stack-based buffer overflow condition has occured:
pwndbg> c
Continuing.
=================================================================
==64871==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff2f02171 at pc 0x7ffff73443a0 bp 0x7fffffffa7f0 sp 0x7fffffffa7e0 // [13]
WRITE of size 1 at 0x7ffff2f02171 thread T0 // [12]
#0 0x7ffff734439f in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:8745 // [11]
#1 0x5555555554c7 in main harness.cpp:38
#2 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
Address 0x7ffff2f02171 is located in stack of thread T0 at offset 8561 in frame
#0 0x7ffff72ffeff in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:3714
This frame has 123 object(s):
[32, 33) 'gdftyp' (line 8666)
...
[8544, 8561) 'v' (line 8742) <== Memory access at offset 8561 overflows this variable
[8608, 8628) 'tmp' (line 5317)
[8672, 8692) 'buf' (line 8183)
[8736, 8776) 'tmp' (line 11751)
[8816, 8897) 'tmp' (line 4076)
[8944, 9025) 'PreFilt' (line 4286)
[9072, 9153) 'line' (line 11400)
[9200, 9281) 'Label' (line 11974)
[9328, 9409) 'tmp' (line 12156)
[9456, 9584) 'buf' (line 8664)
[9616, 9872) 'tmp' (line 7385)
[9936, 10192) 'tmp' (line 9089)
[10256, 10512) 'tmp' (line 11054)
[10576, 10832) 'cmd' (line 12168)
[10896, 11153) 'buf' (line 8882)
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/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:8745 in sopen_extended
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
==64871==ABORTING
[Inferior 1 (process 64871) exited with code 01]
AddressSanitizer attributes the buffer overflow to line 8745 of biosig.c [11], which corresponds to the null byte write mentioned previously: v[len] = 0;
. Additionally, AddressSanitizer confirms that the write is of size 1 [12], which is what we’d expect, and the address is 0x7ffff2f02171, exactly 17 bytes past the address of v
we computed earlier (0x7ffff2f02160).
Since len
is read from the input file, this vulnerability allows the writing of a single null byte past the end of a stack-allocated buffer at an offset largely controlled by the attacker, potentially leading to arbitrary code execution.
==64871==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff2f02171 at pc 0x7ffff73443a0 bp 0x7fffffffa7f0 sp 0x7fffffffa7e0
WRITE of size 1 at 0x7ffff2f02171 thread T0
#0 0x7ffff734439f in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:8745
#1 0x5555555554c7 in main harness.cpp:38
#2 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: 71453a64ceb3bfa137fcadc13ca9ee9004d4faa3)
Address 0x7ffff2f02171 is located in stack of thread T0 at offset 8561 in frame
#0 0x7ffff72ffeff in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:3714
This frame has 123 object(s):
[32, 33) 'gdftyp' (line 8666)
...
[8544, 8561) 'v' (line 8742) <== Memory access at offset 8561 overflows this variable
[8608, 8628) 'tmp' (line 5317)
[8672, 8692) 'buf' (line 8183)
[8736, 8776) 'tmp' (line 11751)
[8816, 8897) 'tmp' (line 4076)
[8944, 9025) 'PreFilt' (line 4286)
[9072, 9153) 'line' (line 11400)
[9200, 9281) 'Label' (line 11974)
[9328, 9409) 'tmp' (line 12156)
[9456, 9584) 'buf' (line 8664)
[9616, 9872) 'tmp' (line 7385)
[9936, 10192) 'tmp' (line 9089)
[10256, 10512) 'tmp' (line 11054)
[10576, 10832) 'cmd' (line 12168)
[10896, 11153) 'buf' (line 8882)
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/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:8745 in sopen_extended
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
==64871==ABORTING
[Inferior 1 (process 64871) exited with code 01]
2025-08-06 - Vendor Disclosure
2025-08-24 - Vendor Patch Release
2025-08-25 - Public Release
Discovered by Mark Bereza and Lilith >_> of Cisco Talos.