CVE-2025-64736
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.
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)
libbiosig - https://biosig.sourceforge.net/index.html
6.1 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:L
CWE-125 - Out-of-bounds Read
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.
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
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2026-01-12 - Vendor Disclosure
2026-02-15 - Vendor Patch Release
2026-03-03 - Public Release
Discovered by Mark Bereza of Cisco Talos.