CVE-2025-53518
An integer overflow vulnerability exists in the ABF parsing functionality of The Biosig Project libbiosig 3.9.0 and Master Branch (35a819fa). A specially crafted ABF 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
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-190 - Integer Overflow or Wraparound
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; this vulnerability concerns the code path taken when reading from a file.
The file type used to exercise this vulnerability is the ABF (Axon Binary File) file format, a Molecular Devices propriety format. According to Molecular Device’s user guide, this file format is used for the storage of binary experimental data. To determine if a file is an ABF file, getfiletype
runs the following check:
else if (!memcmp(hdr->AS.Header, "ABF ", 4)) { // [1]
// else if (!memcmp(Header1,"ABF \x66\x66\xE6\x3F",4)) { // ABF v1.8
hdr->TYPE = ABF;
hdr->VERSION = lef32p(hdr->AS.Header+4);
}
else if (!memcmp(hdr->AS.Header, "ABF2\x00\x00", 6) && ( hdr->AS.Header[6] < 10 ) && ( hdr->AS.Header[7] < 10 ) ) {
hdr->TYPE = ABF2;
hdr->VERSION = hdr->AS.Header[7] + ( hdr->AS.Header[6] / 10.0 );
}
Note that getfiletype
distinguishes between two types of ABF files based on the first few bytes of the header: ABF and ABF2. The original ABF format was superceded by ABF 2.0, but is still supported by libbiosig. This vulnerability is located in the libbiosig code that handles parsing of files using the original ABF format, not 2.0.
Assuming we hit the first branch [1] above, the code flow in sopen_extended
looks as such:
else if (hdr->TYPE==ABF) {
hdr->HeadLen = count;
sopen_abf_read(hdr);
}
For most file types, the parsing logic is contained within sopen_extended
itself, with a select few having their own dedicated sopen_*
function that is called from sopen_extended
. Such is the case for the ABF file format, which defers to sopen_abf_read
for the bulk of its parsing logic. For this vulnerability, the first relevant code block within sopen_abf_read
is the following:
/* ===== EVENT TABLE ===== */
uint32_t n1,n2;
n1 = lei32p(hdr->AS.Header + offsetof(struct ABFFileHeader, lActualEpisodes)) - 1;
n2 = lei32p(hdr->AS.Header + offsetof(struct ABFFileHeader, lNumTagEntries));
hdr->EVENT.N = n1+n2; // [2]
This block computes the total number of events [2] by summing two values read from the ABF input file: lActualEpisodes
and lNumTagEntries
, saved to n1
and n2
, respectively. Unfortunately, the struct member where the total number of events is stored (hdr->EVENT.N
) is of the same size as n1
and n2
, with all three being uint32_t
[3]:
/* EVENTTABLE */
struct {
double SampleRate ATT_ALI; /* for converting POS and DUR into seconds */
uint16_t *TYP ATT_ALI; /* defined at http://biosig.svn.sourceforge.net/viewvc/biosig/trunk/biosig/doc/eventcodes.txt */
uint32_t *POS ATT_ALI; /* starting position [in samples] using a 0-based indexing */
uint32_t *DUR ATT_ALI; /* duration [in samples] */
uint16_t *CHN ATT_ALI; /* channel number; 0: all channels */
#if (BIOSIG_VERSION >= 10500)
gdf_time *TimeStamp ATT_ALI; /* store time stamps */
#endif
const char* *CodeDesc ATT_ALI; /* describtion of "free text"/"user specific" events (encoded with TYP=0..255 */
uint32_t N ATT_ALI; /* number of events */ // [3]
uint16_t LenCodeDesc ATT_ALI; /* length of CodeDesc Table */
} EVENT ATT_ALI;
This means that for sufficiently large values of n1
and/or n2
, both of which are attacker-controlled via the input file, it is possible to overflow the value of hdr->EVENT.N
. This becomes particularly problematic as this value is used to compute the size of heap buffers allocated to store the hdr->EVENT.POS
[4] and hdr->EVENT.TYP
[5] arrays, with hdr->EVENT.N
encoding the number of elements in both arrays:
/* add breaks between sweeps */
size_t spr = lei32p(hdr->AS.Header + offsetof(struct ABFFileHeader, lNumSamplesPerEpisode))/hdr->NS; // [9]
hdr->EVENT.SampleRate = hdr->SampleRate;
hdr->EVENT.POS = (uint32_t*) realloc(hdr->EVENT.POS, hdr->EVENT.N * sizeof(*hdr->EVENT.POS)); // [4]
hdr->EVENT.TYP = (uint16_t*) realloc(hdr->EVENT.TYP, hdr->EVENT.N * sizeof(*hdr->EVENT.TYP)); // [5]
for (k=0; k < n1; k++) { // [6]
hdr->EVENT.TYP[k] = 0x7ffe; // [7]
hdr->EVENT.POS[k] = (k+1)*spr; // [8]
}
The relevant vulnerability is exercised soon after, in a loop used to populate the hdr->EVENT.POS
and hdr->EVENT.TYP
arrays [6]. The upper bound for this loop is n1
, with lActualEpisodes
read from the ABF file header. If the aforementioned integer overflow occured when computing hdr->EVENT.N
, it is possible for n1
to be greater than hdr->EVENT.N
, meaning that this loop will eventually write past the end of the buffers allocated for hdr->EVENT.POS
and hdr->EVENT.TYP
, resulting in a heap-based buffer overflow condition.
While the data written past the end of the buffer is fixed for hdr->EVENT.TYP
[7], the data written past the end of the hdr->EVENT.POS
buffer [8] is dynamic, the product of the current event being processed (k
) and the variable spr
. spr
is itself a function of two other values: the lNumberSamplesPerEpisode
read from the ABF file header, and hdr->NS
[9]. Further up in sopen_abf_read
, we can see where hdr->NS
is initialized:
hdr->NS = lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCNumChannels));
if (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nDigitalEnable)))
hdr->NS += lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nDigitalDACChannel));
The code above shows that hdr->NS
is computed using the nADCNumChannels
(and optionally the nDigitalDACChannel
) fields from the ABF file header. Since all the values used to compute spr
are read from the input ABF file, spr
is attacker-controlled, albeit bounded. By extension, the value potentially written past the end of the hdr->EVENT.POS
buffer is also in part attacker-controlled, as it is a function of the loop counter and spr
. Zooming out, if this second buffer overflow condition [8] can be reached without the first overflow condition [7] terminating execution, the result is a heap-based buffer overflow where the data written is in part controlled by the attacker. Depending on the setup of the heap, this flaw can potentially lead to arbitrary code execution.
A similar heap-based buffer overflow condition is present a little further down in sopen_abf_read
, also resulting from the same incorrect computation of the size of the heap buffer used to store hdr->EVENT.POS
:
/* add tags */
hdr->AS.auxBUF = realloc(hdr->AS.auxBUF, n2 * sizeof(struct ABFTag));
ifseek(hdr, lei32p(hdr->AS.Header + offsetof(struct ABFFileHeader, lTagSectionPtr))*ABF_BLOCKSIZE, SEEK_SET);
count = ifread(hdr->AS.auxBUF, sizeof(struct ABFTag), n2, hdr); // [11]
if (count>255) { // [12]
count = 255;
fprintf(stderr,"Warning ABF: more than 255 tags cannot be read");
}
hdr->EVENT.N = n1+count;
for (k=0; k < count; k++) { // [10]
uint8_t *abftag = hdr->AS.auxBUF + k * sizeof(struct ABFTag);
hdr->EVENT.POS[k+n1] = leu32p(abftag)/hdr->NS; // [13]
abftag[ABF_TAGCOMMENTLEN+4-1]=0;
FreeTextEvent(hdr, k+n1, (char*)(abftag+4));
}
Here, the upper bound for the loop used to populate the hdr->EVENT.POS
array is count
[10]. Further up, we can see that count
is set to the return value of a call to ifread
[11] (essentially a wrapper for fread
), with n2
used as the number of ABFTag
structs to read into hdr->AS.auxBUF
. Like fread
, the return value of ifread
is the number of objects successfully read, so count
should be the same as n2
. An exception is made in cases where n2
is greater than 255, which count cannot exceed [12]. Despite this constraint, a buffer overflow condition is still possible, as the entries are written starting at an offset of n1
[13], which we’ve already established can exceed the number of elements that hdr->EVENT.POS
can hold.
==219122==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fff7b905758 at pc 0x7ffff73a0fc2 bp 0x7fffffffa600 sp 0x7fffffffa5f0
WRITE of size 2 at 0x7fff7b905758 thread T0
#0 0x7ffff73a0fc1 in sopen_abf_read t210/sopen_abf_read.c:552
#1 0x7ffff7315e49 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:4539
#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_master/harness+0x1204) (BuildId: d6104fe93bdc0734bb9067239dafd94e3ef8fc2f)
0x7fff7b905758 is located 0 bytes after 984629080-byte region [0x7fff40e01800,0x7fff7b905758)
allocated by thread T0 here:
#0 0x7ffff78fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85
#1 0x7ffff739dfa7 in sopen_abf_read t210/sopen_abf_read.c:550
#2 0x7ffff7315e49 in sopen_extended /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:4539
#3 0x5555555554c7 in main harness.cpp:38
#4 0x7ffff6a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#5 0x7ffff6a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#6 0x555555555204 in _start (/home/mbereza/Projects/BioSig/repro_master/harness+0x1204) (BuildId: d6104fe93bdc0734bb9067239dafd94e3ef8fc2f)
SUMMARY: AddressSanitizer: heap-buffer-overflow t210/sopen_abf_read.c:552 in sopen_abf_read
Shadow bytes around the buggy address:
0x7fff7b905480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fff7b905500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fff7b905580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fff7b905600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fff7b905680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7fff7b905700: 00 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa
0x7fff7b905780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fff7b905800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fff7b905880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fff7b905900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fff7b905980: 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
==219122==ABORTING
[Inferior 1 (process 219122) 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.