An attacker with local access can exploit this vulnerability to elevate privileges from ring 3 or ring 0 (depends on the operating system) to a DXE driver and execute arbitrary code. Malicious code installed as a result of this exploitation could survive operating system (OS) boot process and runtime, or modify NVRAM area on the SPI flash storage (to gain persistence). Additionally, threat actors could use this vulnerability to bypass OS security mechanisms (modify privileged memory or runtime variables), influence OS boot process, and in some cases allow an attacker to hook or modify EFI Runtime services.
Binarly REsearch Team has discovered wrong error handling during JPEG file processing in Phoenix firmware which leads to several vulnerabilities, including an OOB Write in the Huffman table decoding routine.
An attacker with local access can exploit this vulnerability to elevate privileges from ring 3 or ring 0 (depends on the operating system) to a DXE driver and execute arbitrary code. Malicious code installed as a result of this exploitation could survive operating system (OS) boot process and runtime, or modify NVRAM area on the SPI flash storage (to gain persistence). Additionally, threat actors could use this vulnerability to bypass OS security mechanisms (modify privileged memory or runtime variables), influence OS boot process, and in some cases allow an attacker to hook or modify EFI Runtime services.
The processing of JPEG images in Phoenix firmware is based on the open-source libjpeg-turbo library version 9d. As required by this library, Phoenix firmware initializes the library state and error routines before decompressing the input image:
jpeg_error_mgr *__fastcall jpeg_std_error(jpeg_error_mgr *err)
{
jpeg_error_mgr *result; // rax
err->last_jpeg_message = 126;
*&err->trace_level = 0i64;
err->error_exit = error_exit_function;
err->emit_message = emit_message;
err->output_message = output_message;
err->format_message = format_message;
err->reset_error_mgr = reset_error_mgr;
err->jpeg_message_table = jpeg_std_message_table;
result = err;
err->msg_code = 0;
err->addon_message_table = 0i64;
*&err->first_addon_message = 0i64;
return result;
}
unsigned __int64 __fastcall DecodeJPEG(const JOCTET *a1, unsigned int a2, __int64 a3)
{
struct jpeg_error_mgr *err; // rax
struct jpeg_decompress_struct cinfo; // [rsp+20h] [rbp-E0h] BYREF
jpeg_error_mgr jerr; // [rsp+2A0h] [rbp+1A0h] BYREF
if ( a1 && a3 )
{
err = jpeg_std_error(&jerr);
Buffer = 0i64;
cinfo.err = err;
// BRLY-LOGOFAIL-2023-030: error_exit is overwritten with a dummy function which doesn't terminate execution when invoked.
jerr.error_exit = null_sub;
jerr.emit_message = null_sub;
jerr.output_message = null_sub;
jerr.format_message = null_sub;
jpeg_CreateDecompress(&cinfo, 90, 0x278i64);
jpeg_mem_src(&cinfo, a1, a2);
if ( jpeg_read_header(&cinfo, 0) == 1 && jpeg_start_decompress(&cinfo) == 1 )
{
buffer = AllocateBuffer((cinfo.output_width * cinfo.output_components));
if ( !buffer )
{
v7 = 0x8000000000000009ui64;
LABEL_28:
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return v7;
}
decoded_image = AllocateBuffer(4i64 * cinfo.output_width * cinfo.output_height);
...
The jpeg_std_error function initializes the function pointer fields of the jpeg_error_mgr struct to ensure proper error handling. However, some of these fields are later overwritten by the main DecodeJPEG function with pointers to an empty function (null_sub). One critical field, error_exit, is used by the library for error handling. The original error_exit_function defined in libjpeg-turbo displays an error message and terminates the program when invoked. This terminating behavior is lost in the Phoenix firmware because the error_exit field is overwritten to point to an empty function.
This change turns safe code into vulnerable code, as the parser will not terminate processing on errors but will actually continue execution. An example of this can be seen during the processing of Huffman tables:
char __fastcall get_dht(jpeg_decompress_struct *cinfo)
{
struct jpeg_source_mgr *src; // rbx
size_t bytes_in_buffer_2; // rsi
const JOCTET *next_input_byte; // r14
char result; // al
int v6; // ebp
const JOCTET *v7; // r14
int v8; // ebp
size_t v9; // rsi
int v10; // eax
size_t bytes_in_buffer; // rsi
char *v12; // r14
int length; // ebp MAPDST
__int64 index; // r12
int count; // r13d
__int64 i_1; // r15
int v17; // eax
struct jpeg_error_mgr *err; // rdx
__int64 j; // rcx
struct jpeg_error_mgr *v21; // rdx
__int64 k; // rcx
__int64 i; // r15
JHUFF_TBL **v24; // r15
char v25[32]; // [rsp+20h] [rbp-148h]
char huffval[256]; // [rsp+40h] [rbp-128h]
src = cinfo->src;
bytes_in_buffer_2 = src->bytes_in_buffer;
next_input_byte = src->next_input_byte;
if ( !bytes_in_buffer_2 )
{
if ( !(src->fill_input_buffer)() )
return 0;
next_input_byte = src->next_input_byte;
bytes_in_buffer_2 = src->bytes_in_buffer;
}
v6 = *next_input_byte;
v7 = next_input_byte + 1;
v8 = v6 << 8;
v9 = bytes_in_buffer_2 - 1;
if ( !v9 )
{
if ( !src->fill_input_buffer(cinfo) )
return 0;
v7 = src->next_input_byte;
v9 = src->bytes_in_buffer;
}
v10 = *v7;
bytes_in_buffer = v9 - 1;
v12 = (v7 + 1);
length = v10 + v8 - 2;
while ( length > 16 )
{
if ( !bytes_in_buffer )
{
if ( !src->fill_input_buffer(cinfo) )
return 0;
v12 = src->next_input_byte;
bytes_in_buffer = src->bytes_in_buffer;
}
index = *v12;
--bytes_in_buffer;
++v12;
cinfo->err->msg_code = 82;
cinfo->err->msg_parm.i[0] = index;
cinfo->err->emit_message(cinfo, 1);
count = 0;
v25[0] = 0;
for ( i_1 = 1i64; i_1 <= 16; ++i_1 )
{
if ( !bytes_in_buffer )
{
if ( !src->fill_input_buffer(cinfo) )
return 0;
v12 = src->next_input_byte;
bytes_in_buffer = src->bytes_in_buffer;
}
v17 = *v12;
--bytes_in_buffer;
v25[i_1] = v17;
++v12;
count += v17;
}
err = cinfo->err;
length -= 17;
for ( j = 0i64; j < 8; ++j )
err->msg_parm.i[j] = v25[j + 1];
cinfo->err->msg_code = 88;
cinfo->err->emit_message(cinfo, 2);
v21 = cinfo->err;
for ( k = 0i64; k < 8; ++k )
v21->msg_parm.i[k] = v25[k + 9];
cinfo->err->msg_code = 88;
cinfo->err->emit_message(cinfo, 2);
if ( count > 256 || count > length )
{
cinfo->err->msg_code = JERR_BAD_HUFF_TABLE;
// Problem here: error_exit normally aborts execution, but in Phoenix firmware
// it's a dummy function, so execution just continues.
cinfo->err->error_exit(cinfo);
}
for ( i = 0i64; i < count; huffval[i++] = *v12++ )
{
if ( !bytes_in_buffer )
{
if ( !src->fill_input_buffer(cinfo) )
return 0;
v12 = src->next_input_byte;
bytes_in_buffer = src->bytes_in_buffer;
}
--bytes_in_buffer;
}
length -= count;
if ( (index & 0x10) != 0 )
{
LODWORD(index) = index - 16;
v24 = &cinfo->ac_huff_tbl_ptrs[index];
}
else
{
v24 = &cinfo->dc_huff_tbl_ptrs[index];
}
if ( index > 3 )
{
cinfo->err->msg_code = 31;
cinfo->err->msg_parm.i[0] = index;
cinfo->err->error_exit(cinfo);
}
if ( !*v24 )
*v24 = jpeg_alloc_huff_table(cinfo);
MEMCOPY(*v24);
if ( count > 0 )
MEMCOPY((*v24)->huffval);
}
if ( length )
{
cinfo->err->msg_code = 12;
cinfo->err->error_exit(cinfo);
}
src->next_input_byte = v12;
result = 1;
src->bytes_in_buffer = bytes_in_buffer;
return result;
}
This function checks that the variable count, which depends on untrusted input, does not exceed the length of the huffval array (256) or the value of length. If this condition is met, the function calls error_exit. However, since error_exit is now an empty function, processing does not terminate as intended.
The execution will then continue in the for loop, where the huffval array is filled with input values. Since the huffval array is allocated on stack with a fixed size of 256 and the loop is governed by the count variable, an attacker can easily exploit this condition to overflow this array. Since the stack contains return addresses and many structures with function pointers (e.g., cinfo), this overflow can be leveraged by an attacker to achieve arbitrary code execution.
Binarly REsearch Team