Talos Vulnerability Report

TALOS-2025-2323

The Biosig Project libbiosig ABF parsing out-of-bounds read vulnerability

March 3, 2026
CVE Number

CVE-2025-64736

SUMMARY

An out-of-bounds read vulnerability exists in the ABF parsing functionality of The Biosig Project libbiosig 3.9.2 and Master Branch (5462afb0). A specially crafted .abf file can lead to an information leak. 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.2
The Biosig Project libbiosig Master Branch (5462afb0)

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-125 - Out-of-bounds Read

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 relevant code block within sopen_abf_read is the following:

		hdr->CHANNEL = realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE));                                                             // [2]
		uint32_t k1=0,k;	// ABF is 32 bits only, no need for more                                                                    // [3]
		for (k=0; k < ABF_ADCCOUNT + ABF_DACCOUNT; k++) {                                                                               // [4]
			CHANNEL_TYPE *hc = hdr->CHANNEL+k1;                                                                                         // [8]
			if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) {		// [5]
				hc->OnOff = 1;
				hc->bufptr = NULL;
				hc->LeadIdCode = 0;
				int ll = min(ABF_ADCNAMELEN, MAX_LENGTH_LABEL);                                                                         // [7]
				int16_t nss = lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k);                         // [9]
				strncpy(hc->Label, (char*)hdr->AS.Header + offsetof(struct ABFFileHeader, sADCChannelName) + nss*ABF_ADCNAMELEN, ll);   // [6]

This block begins by resizing hdr->CHANNEL, the heap-allocated array that stores data for each channel in the input file, to accomodate the number of channels defined in that same input file: hdr-NS [2]. Next, two counters are initialized, k and k1, both of which will hold the index of the current channel being processed [3]. The only difference between the two is that k1 is only used when iterating over the ADC channels, while k is used as the index for all channels (both ADC and DAC). From there, the bulk of the processing is done within a for loop that iterates over these channels [4].

This vulnerability concerns the code path taken when the first if statement inside this for loop [5] evaluates to true, which occurs when the current channel being processed (k) is an ADC channel. The vulnerability itself is exercised during a call to strncpy [6] that attempts to populate the Label field for the current channel by reading off the corresponding entry in the sADCChannelName array of the ABFFileHeader structure pointed to by hdr->AS.Header.

To better understand the cause of this vulnerability, let’s analyze each of the arguments passed to strncpy. We’ll start with the simplest: the number of bytes to copy. In this case, it is set to the variable ll, which is defined as the smaller of two constants: ABF_ADCNAMELEN and MAX_LENGTH_LABEL [7]. Since abfheadr.h defines ABF_ADCNAMELEN as 10:

#define ABF_ADCNAMELEN         10    // length of ADC channel name strings

and biosig-dev.h defines MAX_LENGTH_LABEL as 80:

#define MAX_LENGTH_LABEL 	80	// TMS: 40, AXG: 79

we know that ll, and thus the total number of bytes copied, is 10 (0xa).

Next, the destination is hc->Label, which refers to the Label member of the structure pointed to by hc. The Label member is defined as a character array of size MAX_LENGTH_LABEL+1 (81) in biosig-dev.h:

	char		Label[MAX_LENGTH_LABEL+1] ATT_ALI; 	/* Label of channel */

hc, on the other hand, is defined at the very start of the for loop [8] as a pointer to the CHANNEL_TYPE entry in the hdr->CHANNEL array for the current ADC channel being processed, whose index is k1. Thus, the destination for the strncpy call is the Label field for the ADC channel in the hdr->CHANNEL array with index k1.

The source is a bit more complicated, being the result of the following expression:

(char*)hdr->AS.Header + offsetof(struct ABFFileHeader, sADCChannelName) + nss*ABF_ADCNAMELEN

The expression first computes the offset of the sADCChannelName member of the ABFFileHeader structure pointed to by hdr->AS.Header, and then offsets it by nss*ABF_ADCNAMELEN. Looking at the definition of ABFFileHeader located in abfheadr.h, sADCChannelName is an array of strings (implented as a two-dimensional char array) with ABF_ADCCOUNT entries, and each entry having a fixed size of ABF_ADCNAMELEN:

   char     sADCChannelName[ABF_ADCCOUNT][ABF_ADCNAMELEN];

Thus, the source is the address of the nssth entry in this sADCChannelName array. The question then becomes, where does nss come from? nss is actually initialized just before the call to strncpy [9] via a call to lei16p, which reads a signed 16-bit integer from a given address:

static inline int16_t lei16p(const void* i) {
	uint16_t a;
	memcpy(&a, i, sizeof(a));
	return ((int16_t)le16toh(a));
}

In this case, the address read by lei16p is the result of the following expression:

hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k

The expression first computes the offset of the nADCSamplingSeq member of the ABFFileHeader structure pointed to by hdr->AS.Header, and then offsets it by 2 * k. Looking at the definition of ABFFileHeader located in abfheadr.h, nADCSamplingSeq is an array of shorts (16-bit integers) with ABF_ADCCOUNT entries:

   short    nADCSamplingSeq[ABF_ADCCOUNT];

Since each entry in this array is two bytes, an offset of 2 * k corresponds to the kth entry. Thus, nss is computed by reading the kth entry of hdr->AS.Header’s nADCSamplingSeq array.

This computation of nss is actually the root cause of the out-of-bounds read. As stated previously, nss is used as the index for the entry in the sADCChannelName array that strncpy attempts to copy into hc->Label. We also know that the sADCChannelName array has a total of ABF_ADCCOUNT entries, which is defined to be 16 in abfheadr.h:

#define ABF_ADCCOUNT           16    // number of ADC channels supported.

nss, on other hand, is computed by reading a signed 16-bit integer from the nADCSamplingSeq array, which is populated using data from the input file provided to libbiosig and is thus attacker-controlled. This means that nss can take on any signed 16-bit value and can therefore greatly exceed 16, the maximum number of the entries in the sADCChannelName array. Since libbiosig does not perform any checks to ensure that nss is less than 16 before using it to compute the source address for strncpy, an out-of-bounds read condition is made possible.

This can be confirmed by supplying the attached POC (where such a condition occurs) to libbiosig as input and attaching GDB:

Breakpoint 2, sopen_abf_read (hdr=hdr@entry=0x55555556e920) at ./t210/sopen_abf_read.c:410
410                             if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) {
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/t210/sopen_abf_read.c:410
405
406                 hdr->CHANNEL = realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE));
407                 uint32_t k1=0,k;        // ABF is 32 bits only, no need for more
408                 for (k=0; k < ABF_ADCCOUNT + ABF_DACCOUNT; k++) {
409                         CHANNEL_TYPE *hc = hdr->CHANNEL+k1;
► 410                         if ((k < ABF_ADCCOUNT) && (lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k) >= 0)) {
411                                 hc->OnOff = 1;
412                                 hc->bufptr = NULL;
413                                 hc->LeadIdCode = 0;
414                                 int ll = min(ABF_ADCNAMELEN, MAX_LENGTH_LABEL);
415                                 int16_t nss = lei16p(hdr->AS.Header + offsetof(struct ABFFileHeader, nADCSamplingSeq) + 2 * k);
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7ea6b20 sopen_abf_read+736
1   0x7ffff7e7f912 sopen_extended+30546
2   0x555555555283 main+186
3   0x7ffff762a1ca __libc_start_call_main+122
4   0x7ffff762a28b __libc_start_main+139
5   0x555555555105 _start+37

By setting a breakpoint shortly before the vulnerable call to strncpy, we can retrieve the addresses of hc->Label [10] and hdr->AS.Header [11], as well as inspect the heap to see hdr->AS.Header’s currently allocated size [12]:

pwndbg> p/x &(hc->Label)
$1 = 0x7ffff4f4d040                                                                                                                     // [10]
pwndbg> p/x hdr->AS.Header
$2 = 0x5555555717e0                                                                                                                     // [11]
pwndbg> heap -v 0x5555555717d0
Allocated chunk | PREV_INUSE
Addr: 0x5555555717d0
prev_size: 0x00
size: 0x1010 (with flag bits: 0x1011)                                                                                                   // [12]
fd: 0x2046444720464241
bk: 0xfd9df90435322e31
fd_nextsize: 0xfd12fc37fd37ff8d
bk_nextsize: 0x3730303220

Thus, hdr->AS.Header has an allocated size of 0x1010 (4112) bytes. By stepping a few more times, we can inspect the final state of the arguments passed to strncpy, as well as retrieve the value of nss right before the call is made:

0x00007ffff7ea7a11      95        return __builtin___strncpy_chk (__dest, __src, __len,
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
0x7ffff7ea79f8 <sopen_abf_read+4536>    lea    esi, [rax + rax + 0x1ba]         ESI => 0x142fa
0x7ffff7ea79ff <sopen_abf_read+4543>    mov    dword ptr [rbp - 0x98], r8d      [0x7fffffffcc88] <= 0x2020
0x7ffff7ea7a06 <sopen_abf_read+4550>    movsxd rsi, esi                         RSI => 0x142fa
0x7ffff7ea7a09 <sopen_abf_read+4553>    add    rsi, rdx                         RSI => 0x555555585ada (0x142fa + 0x5555555717e0)
0x7ffff7ea7a0c <sopen_abf_read+4556>    mov    edx, 0xa                         EDX => 0xa
► 0x7ffff7ea7a11 <sopen_abf_read+4561>    call   strncpy@plt                 <strncpy@plt>
		dest: 0x7ffff4f4d040 ◂— 0                                                                                                       // [13]
		src: 0x555555585ada                                                                                                             // [14]
		n: 0xa                                                                                                                          // [15]

0x7ffff7ea7a16 <sopen_abf_read+4566>    mov    edx, 0xa                        EDX => 0xa
0x7ffff7ea7a1b <sopen_abf_read+4571>    mov    qword ptr [rbp - 0xa8], rbx
0x7ffff7ea7a22 <sopen_abf_read+4578>    mov    rbx, rdx
0x7ffff7ea7a25 <sopen_abf_read+4581>    jmp    sopen_abf_read+4624         <sopen_abf_read+4624>
	↓
0x7ffff7ea7a50 <sopen_abf_read+4624>    test   rbx, rbx
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7ea7a11 sopen_abf_read+4561
1   0x7ffff7ea7a11 sopen_abf_read+4561
2   0x7ffff7e7f912 sopen_extended+30546
3   0x555555555283 main+186
4   0x7ffff762a1ca __libc_start_call_main+122
5   0x7ffff762a28b __libc_start_main+139
6   0x555555555105 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x nss
$2 = 0x2020                                                                                                                             // [16]

The size [15] is 0xa (10), as expected. The destination address [13] matches the address of hc->Label [10] we computed earlier. The source address of 0x555555585ada [14], as expected, is equal to the address of hdr->AS.Header (0x5555555717e0) plus the offset of the sADCChannelName member (0x1ba) plus nss*ABF_ADCNAMELEN (0x2020 * 0xa = 0x14140). Since the value of nss [16] exceeds 16, strncpy will attempt to read past the end of the sADCChannelName array. In fact, the source address about to be passed to strncpy actually exceeds the bounds of the total memory allocated to hdr->AS.Header on the heap, since 0x5555555717e0 + 0x1010 = 0x5555555727f0 < 0x555555585ada.

Unsuprisingly, continuing execution from here results in a segfault as strncpy attempts to read the out-of-bounds source address:

Program received signal SIGSEGV, Segmentation fault.
__strncpy_evex () at ../sysdeps/x86_64/multiarch/strncpy-evex.S:112
warning: 112    ../sysdeps/x86_64/multiarch/strncpy-evex.S: No such file or directory
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
*RAX  0xada
RBX  0x55555556e920 —▸ 0x55555556f490 ◂— '/home/mbereza/Share/Cisco/abf_read_oob_heap_write_poc'
RCX  0
*RDX  9
*RDI  0x7ffff4f4d040 ◂— 0
*RSI  0x555555585ada
*R8   0x2020
R9   7
*R10  0x19a
R11  0x15d879cd5e791d69
R12  0
R13  0
R14  0x7ffff4f4d010 ◂— 0
R15  0
RBP  0x7fffffffcd20 —▸ 0x7fffffffd620 —▸ 0x7fffffffd670 —▸ 0x7fffffffd710 —▸ 0x7fffffffd770 ◂— ...
*RSP  0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa
*RIP  0x7ffff779d0a5 (__strncpy_evex+37) ◂— vmovdqu64 ymm16, ymmword ptr [rsi]
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
► 0x7ffff779d0a5 <__strncpy_evex+37>    vmovdqu64 ymm16, ymmword ptr [rsi]
0x7ffff779d0ab <__strncpy_evex+43>    vptestnmb k0, ymm16, ymm16
0x7ffff779d0b1 <__strncpy_evex+49>    kmovd  ecx, k0
0x7ffff779d0b5 <__strncpy_evex+53>    mov    rax, rdi
0x7ffff779d0b8 <__strncpy_evex+56>    cmp    rdx, 0x20
0x7ffff779d0bc <__strncpy_evex+60>    jb     __strncpy_evex+1152         <__strncpy_evex+1152>

0x7ffff779d0c2 <__strncpy_evex+66>    vmovdqu64 ymmword ptr [rdi], ymm16
0x7ffff779d0c8 <__strncpy_evex+72>    test   ecx, ecx
0x7ffff779d0ca <__strncpy_evex+74>    jne    __strncpy_evex+855          <__strncpy_evex+855>

0x7ffff779d0d0 <__strncpy_evex+80>    lea    rdx, [rsi + rdx - 0x20]
0x7ffff779d0d5 <__strncpy_evex+85>    sub    rdi, rsi
────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa
01:0008│-0c0 0x7fffffffcc60 ◂— 0
02:0010│-0b8 0x7fffffffcc68 ◂— 0
03:0018│-0b0 0x7fffffffcc70 ◂— 0x3000000000000
04:0020│-0a8 0x7fffffffcc78 ◂— 0
05:0028│-0a0 0x7fffffffcc80 ◂— 0x2020202000000000
06:0030│-098 0x7fffffffcc88 ◂— 0x2020202000002020 /* '  ' */
07:0038│-090 0x7fffffffcc90 ◂— 0x19a
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff779d0a5 __strncpy_evex+37
1   0x7ffff7ea7a16 sopen_abf_read+4566
2   0x7ffff7ea7a16 sopen_abf_read+4566
3   0x7ffff7e7f912 sopen_extended+30546
4   0x555555555283 main+186
5   0x7ffff762a1ca __libc_start_call_main+122
6   0x7ffff762a28b __libc_start_main+139
7   0x555555555105 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

As mentioned previously, the source address for this out-of-bounds read is attacker-controlled. Additionally, the destination is also in part controlled by attacker, as hc points to the CHANNEL_TYPE structure for the current channel, and the attacker can pad the input file with one or more benign channels before the one that ultimately triggers the vulnerability.

Crash Information

Program received signal SIGSEGV, Segmentation fault.
__strncpy_evex () at ../sysdeps/x86_64/multiarch/strncpy-evex.S:112
warning: 112    ../sysdeps/x86_64/multiarch/strncpy-evex.S: No such file or directory
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
*RAX  0xada
RBX  0x55555556e920 —▸ 0x55555556f490 ◂— '/home/mbereza/Share/Cisco/abf_read_oob_heap_write_poc'
RCX  0
*RDX  9
*RDI  0x7ffff4f4d040 ◂— 0
*RSI  0x555555585ada
*R8   0x2020
R9   7
*R10  0x19a
R11  0x15d879cd5e791d69
R12  0
R13  0
R14  0x7ffff4f4d010 ◂— 0
R15  0
RBP  0x7fffffffcd20 —▸ 0x7fffffffd620 —▸ 0x7fffffffd670 —▸ 0x7fffffffd710 —▸ 0x7fffffffd770 ◂— ...
*RSP  0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa
*RIP  0x7ffff779d0a5 (__strncpy_evex+37) ◂— vmovdqu64 ymm16, ymmword ptr [rsi]
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
► 0x7ffff779d0a5 <__strncpy_evex+37>    vmovdqu64 ymm16, ymmword ptr [rsi]
0x7ffff779d0ab <__strncpy_evex+43>    vptestnmb k0, ymm16, ymm16
0x7ffff779d0b1 <__strncpy_evex+49>    kmovd  ecx, k0
0x7ffff779d0b5 <__strncpy_evex+53>    mov    rax, rdi
0x7ffff779d0b8 <__strncpy_evex+56>    cmp    rdx, 0x20
0x7ffff779d0bc <__strncpy_evex+60>    jb     __strncpy_evex+1152         <__strncpy_evex+1152>

0x7ffff779d0c2 <__strncpy_evex+66>    vmovdqu64 ymmword ptr [rdi], ymm16
0x7ffff779d0c8 <__strncpy_evex+72>    test   ecx, ecx
0x7ffff779d0ca <__strncpy_evex+74>    jne    __strncpy_evex+855          <__strncpy_evex+855>

0x7ffff779d0d0 <__strncpy_evex+80>    lea    rdx, [rsi + rdx - 0x20]
0x7ffff779d0d5 <__strncpy_evex+85>    sub    rdi, rsi
────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffcc58 —▸ 0x7ffff7ea7a16 (sopen_abf_read+4566) ◂— mov edx, 0xa
01:0008│-0c0 0x7fffffffcc60 ◂— 0
02:0010│-0b8 0x7fffffffcc68 ◂— 0
03:0018│-0b0 0x7fffffffcc70 ◂— 0x3000000000000
04:0020│-0a8 0x7fffffffcc78 ◂— 0
05:0028│-0a0 0x7fffffffcc80 ◂— 0x2020202000000000
06:0030│-098 0x7fffffffcc88 ◂— 0x2020202000002020 /* '  ' */
07:0038│-090 0x7fffffffcc90 ◂— 0x19a
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff779d0a5 __strncpy_evex+37
1   0x7ffff7ea7a16 sopen_abf_read+4566
2   0x7ffff7ea7a16 sopen_abf_read+4566
3   0x7ffff7e7f912 sopen_extended+30546
4   0x555555555283 main+186
5   0x7ffff762a1ca __libc_start_call_main+122
6   0x7ffff762a28b __libc_start_main+139
7   0x555555555105 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
TIMELINE

2026-01-12 - Vendor Disclosure
2026-02-15 - Vendor Patch Release
2026-03-03 - Public Release

Credit

Discovered by Mark Bereza of Cisco Talos.