Talos Vulnerability Report

TALOS-2025-2231

The Biosig Project libbiosig ABF parsing integer overflow to heap-based buffer overflow vulnerability

August 25, 2025
CVE Number

CVE-2025-53518

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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)

PRODUCT URLS

libbiosig - https://biosig.sourceforge.net/index.html

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-190 - Integer Overflow or Wraparound

DETAILS

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.

Crash Information

==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]
TIMELINE

2025-08-06 - Vendor Disclosure
2025-08-24 - Vendor Patch Release
2025-08-25 - Public Release

Credit

Discovered by Mark Bereza and Lilith >_> of Cisco Talos.