Talos Vulnerability Report

TALOS-2025-2239

The Biosig Project libbiosig Nex parsing heap-based buffer overflow vulnerability

August 25, 2025
CVE Number

CVE-2025-54462

SUMMARY

A heap-based buffer overflow vulnerability exists in the Nex parsing functionality of The Biosig Project libbiosig 3.9.0 and Master Branch (35a819fa). A specially crafted .nex 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-122 - Heap-based Buffer Overflow

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 Plexon NeuroExplorer Nex file format, a file format designed to store signal data with a focus on neurophysiology. Although designed for use with Plexon’s NeuroExplorer software, it is supported by some third-party health data processing libraries, including libbiosig.

To determine if the input file is valid a Nex file, getfiletype runs the following check:

	else if (!memcmp(Header1,"NEX1",3))
			hdr->TYPE = NEX1;

Put simply, libbiosig classifies an input file as Nex by comparing the first few bytes of the header against the string NEX1. Interestingly, only the first 3 bytes are compared, meaning any file starting with NEX will match, likely to accomodate newer versions of the Nex format. The file classification is then stored in the struct member hdr->TYPE.

Further along in sopen_extended, after getfiletype has returned, hdr->TYPE is checked again and, if it’s NEX1, processing unique to the format is then performed:

	else if (hdr->TYPE==NEX1) {

		if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i)\n",__func__,__LINE__);

		if (count < 284) {
			hdr->AS.Header = (uint8_t*)realloc(hdr->AS.Header,284);
			count  += ifread(hdr->AS.Header + count, 1, 284 - count, hdr);
		}

		uint8_t v = hdr->AS.Header[3]-'0';                                                                          // [1]
		const int H1LEN = (v==1) ? (4 + 4 + 256 +   8 + 4*4 + 256)
					: (4 + 4 + 256 +   8 +   8 +  4 +  8 + 64);
		const int H2LEN = (v==1) ? (4 + 4 +  64 + 6*4 + 4*8 + 12 + 16 + 52)
					: (4 + 4 +  64 + 2*8 + 2*4 +  8 + 32 + 4*8 + 4*4 + 60);

		uint32_t k      = leu32p(hdr->AS.Header + 280);                                                             // [2]

		if (count < H1LEN + k * H2LEN) {
			hdr->AS.Header = (uint8_t*)realloc(hdr->AS.Header, H1LEN + k * H2LEN);                                  // [3]
			count  += ifread(hdr->AS.Header + count, 1, H1LEN + k * H2LEN - count, hdr);                            // [4]
		}

		hdr->HeadLen          = count;                                                                              // [5]
		hdr->VERSION          = leu32p(hdr->AS.Header + 4) / 100.0;
		hdr->SampleRate       = lef64p(hdr->AS.Header + 264);
		hdr->SPR              = 1;
		hdr->EVENT.SampleRate = lef64p(hdr->AS.Header + 264);

The code block for processing NEX files begins by computing some header lengths based on the value of v [1], which corresponds to the Nex format version encoded in the fourth byte of the header (after being converted from ASCII to an integer). It appears that NEX1 files use different header lengths than other versions of the Nex format, but in either case these lengths are stored in the variables H1LEN and H2LEN. Based on their usage, H1LEN appears to encode the length of the header’s static fields, while H2LEN seems to encode the length of the “per-channel” fields, as this length is consistently multiplied by the number of channels when used. The number of channels itself is read from a fixed offset into the input file and stored in the variable k [2].

Thus, the total length of the header is computed to be H1LEN + k * H2LEN, and the size of hdr->AS.Header, which is used to store the header data, is resized to accomodate this length [3]. Libbiosig then attempts to read this many bytes from the input file into hdr->AS.Header [4]. The total size is also reflected in the struct member hdr->HeadLen [5].

Next, libbiosig processes the “per-channel” fields, which is done using a for loop:

		hdr->NS = k;                                                                                                // [6]
		hdr->CHANNEL = (CHANNEL_TYPE*)realloc(hdr->CHANNEL, hdr->NS*sizeof(CHANNEL_TYPE));

		if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i)\n",__func__,__LINE__);

		for (k=0; k < hdr->NS; k++) {

			if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i): VarHdr # %i\n",__func__,__LINE__, k);

			CHANNEL_TYPE *hc = hdr->CHANNEL+k;                                                                      // [7]
			uint32_t type = leu32p(hdr->AS.Header + H1LEN + k*H2LEN);

			hc->OnOff = (type==5);

			strncpy(hc->Label, hdr->AS.Header + H1LEN + k*H2LEN + 8, min(64,MAX_LENGTH_LABEL));
			hc->Label[min(64, MAX_LENGTH_LABEL)] = 0;
			hc->Transducer[0] = 0;

			size_t n;
			if (v==5) {
				hc->GDFTYP = (leu32p(hdr->AS.Header + H1LEN + k*H2LEN + 92)==1) ? 16 : 3;
				hc->PhysDimCode = PhysDimCode(hdr->AS.Header + H1LEN + k*H2LEN + 5*8 + 64);
				n       = leu64p(hdr->AS.Header + 80 + H1LEN + k*H2LEN);                                            // [8]
				hc->Cal = lef64p(hdr->AS.Header + 64+8*5+32 + H1LEN + k*H2LEN);
				hc->Off = lef64p(hdr->AS.Header + 64+8*5+40 + H1LEN + k*H2LEN);
				hc->SPR = leu64p(hdr->AS.Header + 64+8*5+48 + H1LEN + k*H2LEN);
				hc->bufptr = hdr->AS.Header + leu64p(hdr->AS.Header + 64+8 + H1LEN + k*H2LEN);                      // [33]
			}
			else {
				hc->GDFTYP = 3;
				hc->PhysDimCode = PhysDimCode("mV");
				n       = leu32p(hdr->AS.Header + 76 + H1LEN + k*H2LEN);                                            // [9]
				hc->Cal = lef64p(hdr->AS.Header + 64+8*4+3*8    + H1LEN + k*H2LEN);
				hc->Off = lef64p(hdr->AS.Header + 64+8*4+3*8+20 + H1LEN + k*H2LEN);
				hc->SPR = leu32p(hdr->AS.Header + 64+8*4+4*8    + H1LEN + k*H2LEN);
				hc->bufptr = hdr->AS.Header+leu32p(hdr->AS.Header + 64+8 + H1LEN + k*H2LEN);                        // [34]
			}

			if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i): VarHdr # %i %i %i %i \n",__func__,__LINE__, k,v,type,(int)n);

			switch (type) {
			case 2:
			//case 6:
			case 0:
			case 1:
				hdr->EVENT.N += n;                                                                                  // [10]
			}
			//if (hc->OnOff) hdr->SPR = lcm(hdr->SPR, hc->SPR);
		}

		if (hdr->EVENT.N > 0) {
			size_t N=hdr->EVENT.N;
			hdr->EVENT.N=0;
			reallocEventTable(hdr,N);                                                                               // [11]

Note: For simplicity, the code block above will be referred to as the “channel code” from here on.

After storing the total number of channels in hdr->NS [6], this loop iterates over each of the channels, storing the current channel index in the variable k. In each iteration, hc contains the handle to the CHANNEL_TYPE data structure for the current channel, obtained by accessing the index k in the hdr->CHANNEL array [7]. Each channel seems to have its own subheader starting at the offset H1LEN + k * H2LEN, where k is the channel index. The first field in each of these subheaders appears to encode the header type, which libbiosig stores in the variable type, and is used to modify how the data is processed.

Of particular interest to this vulnerability is the code used to calculate the number of events. For each channel, libbiosig reads the number of events from a specific offset into that channel’s header and stores it in the variable n [8] [9]. The size of the integer used to encode this number and the exact offset at which this number is found in the input file seems to vary depending on the version of the Nex format used, but the principle is the same in either case. The per-channel number of events (n) is then added to the total number of events, which is stored in the struct member hdr->EVENT.N [10]. Critically, n is only added to hdr->EVENT.N when type is 0, 1, or 2.

Finally, reallocEventTable is called [11] in order to resize the data structures used to store the event table to accomodate the total number of events:

/*------------------------------------------------------------------------
	re-allocates memory for Eventtable.
	hdr->EVENT.N contains actual number of events
	EventN determines the size of the allocated memory

return value:
	in case of success, EVENT_N is returned
	in case of failure SIZE_MAX is returned;
------------------------------------------------------------------------*/
size_t reallocEventTable(HDRTYPE *hdr, size_t EventN)
{
	size_t n;
	hdr->EVENT.POS = (uint32_t*)realloc(hdr->EVENT.POS, EventN * sizeof(*hdr->EVENT.POS));                          // [12]
	hdr->EVENT.DUR = (uint32_t*)realloc(hdr->EVENT.DUR, EventN * sizeof(*hdr->EVENT.DUR));                          // [13]
	hdr->EVENT.TYP = (uint16_t*)realloc(hdr->EVENT.TYP, EventN * sizeof(*hdr->EVENT.TYP));                          // [14]
	hdr->EVENT.CHN = (uint16_t*)realloc(hdr->EVENT.CHN, EventN * sizeof(*hdr->EVENT.CHN));                          // [15]
#if (BIOSIG_VERSION >= 10500)
	hdr->EVENT.TimeStamp = (gdf_time*)realloc(hdr->EVENT.TimeStamp, EventN * sizeof(gdf_time));
#endif

	if (hdr->EVENT.POS==NULL) return SIZE_MAX;
	if (hdr->EVENT.TYP==NULL) return SIZE_MAX;
	if (hdr->EVENT.CHN==NULL) return SIZE_MAX;
	if (hdr->EVENT.DUR==NULL) return SIZE_MAX;
	if (hdr->EVENT.TimeStamp==NULL) return SIZE_MAX;

	for (n = hdr->EVENT.N; n< EventN; n++) {
		hdr->EVENT.TYP[n] = 0;
		hdr->EVENT.CHN[n] = 0;
		hdr->EVENT.DUR[n] = 0;
#if (BIOSIG_VERSION >= 10500)
		hdr->EVENT.TimeStamp[n] = 0;
#endif
	}
	return EventN;
}

As shown above, the event table primarily consists of four heap-allocated buffers: 1. hdr->EVENT.POS, which stores each event’s starting position in samples [12] 2. hdr->EVENT.TYP, which stores each event’s type as an enumeration [13] 3. hdr->EVENT.CHN, which stores each event’s channel index [14] 4. hdr->EVENT.DUR, which stores each event’s duration in samples [15]

reallocEventTable resizes each of these arrays to accomodate a number of entries equal to the second argument. In this case, this is equal to hdr->EVENT.N: the total number of events computed in the channel code.

Unfortunately, there appears to be some inconsitency in how the total number of events is calculated in the Nex file processing code, as demonstrated in the code used to populate the event table:

			N = 0;
			for (k=0; k < hdr->NS; k++) {                                                                           // [16]
				if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i): VarHdr # %i\n",__func__,__LINE__, k);
				CHANNEL_TYPE *hc = hdr->CHANNEL+k;                                                                  // [17]
				uint32_t type = leu32p(hdr->AS.Header + H1LEN + k*H2LEN);

				size_t n,l;
				uint16_t gdftyp = 5;
				if (v==5) {
					n = leu64p(hdr->AS.Header + 80 + H1LEN + k*H2LEN);                                              // [18]
					if (leu32p(hdr->AS.Header + 88 + H1LEN + k*H2LEN))
						gdftyp=7;
				}
				else
					n = leu32p(hdr->AS.Header + 76 + H1LEN + k*H2LEN);                                              // [19]


				switch (type) {
				case 2:
					if (gdftyp==5) {
						for (l=0; l<n; l++)
							hdr->EVENT.DUR[N+l] = leu32p(hc->bufptr+4*(l+n));                                       // [21]
					}
					else {
						for (l=0; l<n; l++)
							hdr->EVENT.DUR[N+l] = leu64p(hc->bufptr+8*(l+n));                                       // [22]
					}
				case 0:
				case 1:
				//case 6:
					if (gdftyp==5) {
						for (l=0; l<n; l++)
							hdr->EVENT.POS[N+l] = leu32p(hc->bufptr+4*l);                                           // [23]
					}
					else {
						for (l=0; l<n; l++)
							hdr->EVENT.POS[N+l] = leu64p(hc->bufptr+8*l);                                           // [24]
					}

					for (l=0; l<n; l++) {
						hdr->EVENT.TYP[N+l] = type;                                                                 // [25]
						hdr->EVENT.CHN[N+l] = k;                                                                    // [26]
						//hdr->EVENT.TimeStamp[N+l] = 0;
					}
				}
				N+=n;                                                                                               // [20]
			}
			hdr->EVENT.N=N;

Note: For simplicity, the code block above will be referred to as the “event code” from here on.

Similar to the channel code, the event code uses a for loop that iterates over the channels [16], with the variable hc once again containing a handle to the CHANNEL_TYPE structure for the current channel [17]. Additionally, the logic used to read the per-channel number of events from the input file is identical to what’s used in the channel code [18] [19]. The inconsistency is instead related to how the total number of events is computed.

As before, the number of events for the current channel is stored in the variable n, but this time an additional variable is used to store the cumulative total: N. Unlike the channel code, which only added n to the cumulative total when type is 0, 1, or 2, the event code adds n to the total regardless of the value of type [20]. In cases where n is non-zero for a channel whose type is greater than 2, the N computed in the event code can exceed the hdr->EVENT.N computed in the channel code.

This quickly becomes problematic, as the size allocated for the event table buffers is based on hdr->EVENT.N, while the event code attempts to write to these same buffers using N as an offset [21] [22] [23] [24] [25] [26]. If N exceeds hdr->EVENT.N during these write operations, a heap-based buffer overflow will occur.

This vulnerability condition can be observed by using the attached POC as the input file to libbiosig and attaching a debugger:

Breakpoint 2, sopen_extended (FileName=<optimized out>, MODE=<optimized out>, hdr=0x55555556e920, biosig_options=<optimized out>)
	at biosig.c:10142
10142                           hdr->EVENT.N=0;
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10142
10137                         //if (hc->OnOff) hdr->SPR = lcm(hdr->SPR, hc->SPR);
10138                 }
10139
10140                 if (hdr->EVENT.N > 0) {
10141                         size_t N=hdr->EVENT.N;
► 10142                         hdr->EVENT.N=0;
10143                         reallocEventTable(hdr,N);
10144
10145                         N = 0;
10146                         for (k=0; k < hdr->NS; k++) {
10147                                 if (VERBOSE_LEVEL>7) fprintf(stdout,"%s (line %i): VarHdr # %i\n",__func__,__LINE__, k);
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7e83428 sopen_extended+82152
1   0x555555555283 main+186
2   0x7ffff762a1ca __libc_start_call_main+122
3   0x7ffff762a28b __libc_start_main+139
4   0x555555555105 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x hdr->EVENT.N
$1 = 0x6b                                                                                                           // [27]

In the GDB output above, execution has been halted right before the call to reallocEventTable, allowing us to determine the value of hdr->EVENT.N, which is used as the size of the event table buffers: 0x6b [27]. Next, execution was halted right before the line that causes the heap-based buffer overflow: hdr->EVENT.POS[N+l] = leu32p(hc->bufptr+4*l);. Similar overflows sharing the same root cause can occur in several locations in the code, but this analysis will focus on the vulnerability exercised by the attached POC.

10177                                                           hdr->EVENT.POS[N+l] = leu32p(hc->bufptr+4*l);
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
RAX  0x6b
RBX  0x5195c7c932
*RCX  0x7ffff733c010 ◂— 0x504d554e9358454e
*RDX  0x569bac7619b8
*RDI  0
RSI  0
R8   0x5556ff01
R9   0x55555556f6b0 ◂— 0
R10  0x7ffff78046a8 (stdout) —▸ 0x7ffff78045c0 (_IO_2_1_stdout_) ◂— 0xfbad2a84
R11  0xa6
R12  0xdf10
R13  0x9e38
R14  0x55555556e920 —▸ 0x55555556f490 ◂— '../biosig_fuzzing_setup/fuzzing/old_bugs/crash-43c9f33db737a9a86e404ac80885fb97ca70890d'
R15  0x7ffff733aeb8 ◂— 0
RBP  0x7fffffffd610 —▸ 0x7fffffffd660 —▸ 0x7fffffffd700 —▸ 0x7fffffffd760 ◂— 0
RSP  0x7fffffffcda0 ◂— 8
*RIP  0x7ffff7e834ee (sopen_extended+82350) ◂— mov r8d, dword ptr [rcx + rdi*4]
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
► 0x7ffff7e834ee <sopen_extended+82350>    mov    r8d, dword ptr [rcx + rdi*4]     R8D, [0x7ffff733c010] => 0x9358454e
0x7ffff7e834f2 <sopen_extended+82354>    mov    dword ptr [rdx + rdi*4], r8d     <Cannot dereference [0x569bac7619b8]>
0x7ffff7e834f6 <sopen_extended+82358>    add    rdi, 1
0x7ffff7e834fa <sopen_extended+82362>    cmp    rax, rdi
0x7ffff7e834fd <sopen_extended+82365>    jne    sopen_extended+82350        <sopen_extended+82350>

0x7ffff7e834ff <sopen_extended+82367>    mov    rdi, qword ptr [r14 + 0x2c0]
0x7ffff7e83506 <sopen_extended+82374>    lea    rcx, [rbx + rbx]
0x7ffff7e8350a <sopen_extended+82378>    xor    edx, edx                         EDX => 0
0x7ffff7e8350c <sopen_extended+82380>    add    rdi, rcx
0x7ffff7e8350f <sopen_extended+82383>    add    rcx, qword ptr [r14 + 0x2d8]
0x7ffff7e83516 <sopen_extended+82390>    mov    word ptr [rdi + rdx*2], si
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig.c:10177
10172                                 case 0:
10173                                 case 1:
10174                                 //case 6:
10175                                         if (gdftyp==5) {
10176                                                 for (l=0; l<n; l++)
► 10177                                                         hdr->EVENT.POS[N+l] = leu32p(hc->bufptr+4*l);
10178                                         }
10179                                         else {
10180                                                 for (l=0; l<n; l++)
10181                                                         hdr->EVENT.POS[N+l] = leu64p(hc->bufptr+8*l);
10182                                         }
────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffcda0 ◂— 8
01:0008│-868 0x7fffffffcda8 ◂— 8
02:0010│-860 0x7fffffffcdb0 ◂— 6
03:0018│-858 0x7fffffffcdb8 ◂— 6
04:0020│-850 0x7fffffffcdc0 ◂— 4
05:0028│-848 0x7fffffffcdc8 ◂— 4
06:0030│-840 0x7fffffffcdd0 ◂— 2
07:0038│-838 0x7fffffffcdd8 ◂— 2
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7e834ee sopen_extended+82350
1   0x555555555283 main+186
2   0x7ffff762a1ca __libc_start_call_main+122
3   0x7ffff762a28b __libc_start_main+139
4   0x555555555105 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x k
$2 = 0xa6
pwndbg> p/x N
$3 = 0x5195c7c932                                                                                                   // [28]
pwndbg> p/x l
$4 = 0x0                                                                                                            // [29]
pwndbg> p/x sizeof(*hdr->EVENT.POS)
$5 = 0x4                                                                                                            // [30]
pwndbg> p/x (0x6b * 0x4)
$6 = 0x1ac                                                                                                          // [31]
pwndbg> p/x hdr->EVENT.POS
$7 = 0x55555556f4f0
pwndbg> heap -v 0x55555556f4e0
Allocated chunk | PREV_INUSE
Addr: 0x55555556f4e0
prev_size: 0x64303938303761
size: 0x1c0 (with flag bits: 0x1c1)                                                                                 // [32]

At this point, N is already 0x5195c7c932 [28], far larger than the computed hdr->EVENT.N value of 0x6b. Since l is 0 [29], this is the index into the hdr->EVENT.POS array that will be written to. With each element of the hdr->EVENT.POS array being 4 bytes [30], we would expect the allocated size to be at least 4 * hdr->EVENT.N, or 0x1ac [31]. Inspecting the heap reveals this to be the case, as the total space allocated is 0x1c0 [32], resulting in 0x1b0 bytes of usable memory after accounting for the heap metadata.

As expected, continuing execution from here results in a segfault when the program attempts to write to hdr->EVENT.POS using this massive array index:

Program received signal SIGSEGV, Segmentation fault.
sopen_extended (FileName=<optimized out>, MODE=<optimized out>, hdr=0x55555556e920, biosig_options=<optimized out>)
	at /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig-dev.h:748
748             return (le32toh(a));
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
RAX  0x6b
RBX  0x5195c7c932
RCX  0x7ffff733c010 ◂— 0x504d554e9358454e
RDX  0x569bac7619b8
RDI  0
RSI  0
*R8   0x9358454e
R9   0x55555556f6b0 ◂— 0
R10  0x7ffff78046a8 (stdout) —▸ 0x7ffff78045c0 (_IO_2_1_stdout_) ◂— 0xfbad2a84
R11  0xa6
R12  0xdf10
R13  0x9e38
R14  0x55555556e920 —▸ 0x55555556f490 ◂— '../biosig_fuzzing_setup/fuzzing/old_bugs/crash-43c9f33db737a9a86e404ac80885fb97ca70890d'
R15  0x7ffff733aeb8 ◂— 0
RBP  0x7fffffffd610 —▸ 0x7fffffffd660 —▸ 0x7fffffffd700 —▸ 0x7fffffffd760 ◂— 0
RSP  0x7fffffffcda0 ◂— 8
*RIP  0x7ffff7e834f2 (sopen_extended+82354) ◂— mov dword ptr [rdx + rdi*4], r8d
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
0x7ffff7e834ee <sopen_extended+82350>    mov    r8d, dword ptr [rcx + rdi*4]     R8D, [0x7ffff733c010] => 0x9358454e
► 0x7ffff7e834f2 <sopen_extended+82354>    mov    dword ptr [rdx + rdi*4], r8d     <Cannot dereference [0x569bac7619b8]>
0x7ffff7e834f6 <sopen_extended+82358>    add    rdi, 1
0x7ffff7e834fa <sopen_extended+82362>    cmp    rax, rdi
0x7ffff7e834fd <sopen_extended+82365>    jne    sopen_extended+82350        <sopen_extended+82350>

0x7ffff7e834ff <sopen_extended+82367>    mov    rdi, qword ptr [r14 + 0x2c0]
0x7ffff7e83506 <sopen_extended+82374>    lea    rcx, [rbx + rbx]
0x7ffff7e8350a <sopen_extended+82378>    xor    edx, edx                         EDX => 0
0x7ffff7e8350c <sopen_extended+82380>    add    rdi, rcx
0x7ffff7e8350f <sopen_extended+82383>    add    rcx, qword ptr [r14 + 0x2d8]
0x7ffff7e83516 <sopen_extended+82390>    mov    word ptr [rdi + rdx*2], si
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig-dev.h:748
743         return ((int16_t)le16toh(a));
744 }
745 static inline uint32_t leu32p(const void* i) {
746         uint32_t a;
747         memcpy(&a, i, sizeof(a));
► 748         return (le32toh(a));
749 }
750 static inline int32_t lei32p(const void* i) {
751         uint32_t a;
752         memcpy(&a, i, sizeof(a));
753         return ((int32_t)le32toh(a));
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7e834f2 sopen_extended+82354
1   0x555555555283 main+186
2   0x7ffff762a1ca __libc_start_call_main+122
3   0x7ffff762a28b __libc_start_main+139
4   0x555555555105 _start+37

Since the final offset into the buffer is computed using values read from the input file, it is attacker-controlled. The data written is an unsigned integer read from the memory location pointed to by hc->bufptr, itself an offset into hdr->AS.Header (the heap-allocated buffer storing the input file data) set using data from the input file. Thus, both the location of the out-of-bounds write and the data written are controlled, at least in part, by the attacker. Depending on the setup of the heap, this flaw can potentially lead to arbitrary code execution.

Crash Information

Program received signal SIGSEGV, Segmentation fault.
sopen_extended (FileName=<optimized out>, MODE=<optimized out>, hdr=0x55555556e920, biosig_options=<optimized out>)
	at /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig-dev.h:748
748             return (le32toh(a));
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
RAX  0x6b
RBX  0x5195c7c932
RCX  0x7ffff733c010 ◂— 0x504d554e9358454e
RDX  0x569bac7619b8
RDI  0
RSI  0
R8   0x9358454e
R9   0x55555556f6b0 ◂— 0
R10  0x7ffff78046a8 (stdout) —▸ 0x7ffff78045c0 (_IO_2_1_stdout_) ◂— 0xfbad2a84
R11  0xa6
R12  0xdf10
R13  0x9e38
R14  0x55555556e920 —▸ 0x55555556f490 ◂— '../biosig_fuzzing_setup/fuzzing/old_bugs/crash-43c9f33db737a9a86e404ac80885fb97ca70890d'
R15  0x7ffff733aeb8 ◂— 0
RBP  0x7fffffffd610 —▸ 0x7fffffffd660 —▸ 0x7fffffffd700 —▸ 0x7fffffffd760 ◂— 0
RSP  0x7fffffffcda0 ◂— 8
RIP  0x7ffff7e834f2 (sopen_extended+82354) ◂— mov dword ptr [rdx + rdi*4], r8d
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
0x7ffff7e834ee <sopen_extended+82350>    mov    r8d, dword ptr [rcx + rdi*4]
► 0x7ffff7e834f2 <sopen_extended+82354>    mov    dword ptr [rdx + rdi*4], r8d     <Cannot dereference [0x569bac7619b8]>
0x7ffff7e834f6 <sopen_extended+82358>    add    rdi, 1
0x7ffff7e834fa <sopen_extended+82362>    cmp    rax, rdi
0x7ffff7e834fd <sopen_extended+82365>    jne    sopen_extended+82350        <sopen_extended+82350>

0x7ffff7e834ff <sopen_extended+82367>    mov    rdi, qword ptr [r14 + 0x2c0]
0x7ffff7e83506 <sopen_extended+82374>    lea    rcx, [rbx + rbx]
0x7ffff7e8350a <sopen_extended+82378>    xor    edx, edx                         EDX => 0
0x7ffff7e8350c <sopen_extended+82380>    add    rdi, rcx
0x7ffff7e8350f <sopen_extended+82383>    add    rcx, qword ptr [r14 + 0x2d8]
0x7ffff7e83516 <sopen_extended+82390>    mov    word ptr [rdi + rdx*2], si
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/mbereza/Projects/BioSig/biosig-code/biosig4c++/biosig-dev.h:748
743         return ((int16_t)le16toh(a));
744 }
745 static inline uint32_t leu32p(const void* i) {
746         uint32_t a;
747         memcpy(&a, i, sizeof(a));
► 748         return (le32toh(a));
749 }
750 static inline int32_t lei32p(const void* i) {
751         uint32_t a;
752         memcpy(&a, i, sizeof(a));
753         return ((int32_t)le32toh(a));
────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffcda0 ◂— 8
01:0008│-868 0x7fffffffcda8 ◂— 8
02:0010│-860 0x7fffffffcdb0 ◂— 6
03:0018│-858 0x7fffffffcdb8 ◂— 6
04:0020│-850 0x7fffffffcdc0 ◂— 4
05:0028│-848 0x7fffffffcdc8 ◂— 4
06:0030│-840 0x7fffffffcdd0 ◂— 2
07:0038│-838 0x7fffffffcdd8 ◂— 2
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0   0x7ffff7e834f2 sopen_extended+82354
1   0x555555555283 main+186
2   0x7ffff762a1ca __libc_start_call_main+122
3   0x7ffff762a28b __libc_start_main+139
4   0x555555555105 _start+37
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.