From 40686d5edf7504838667334efa2ca3e2e96d33d6 Mon Sep 17 00:00:00 2001 From: platomav Date: Fri, 15 Apr 2022 18:17:58 +0300 Subject: [PATCH] Improved AMI UCP > NAL unpacking Fix potential illegal path traversals --- AMI_PFAT_Extract.py | 8 ++++---- AMI_UCP_Extract.py | 36 +++++++++++++++++++++++------------ Dell_PFS_Extract.py | 16 ++++++++-------- common/a7z_comp.py | 2 +- common/checksums.py | 2 +- common/efi_comp.py | 2 +- common/externals.py | 2 +- common/num_ops.py | 2 +- common/path_ops.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- common/patterns.py | 6 +++--- common/struct_ops.py | 2 +- common/system.py | 11 ++++------- common/text_ops.py | 11 ++++++++++- 13 files changed, 102 insertions(+), 43 deletions(-) diff --git a/AMI_PFAT_Extract.py b/AMI_PFAT_Extract.py index 651e155..8a0bbc5 100644 --- a/AMI_PFAT_Extract.py +++ b/AMI_PFAT_Extract.py @@ -7,7 +7,7 @@ AMI BIOS Guard Extractor Copyright (C) 2018-2022 Plato Mavropoulos """ -title = 'AMI BIOS Guard Extractor v4.0_a5' +title = 'AMI BIOS Guard Extractor v4.0_a6' import os import re @@ -20,7 +20,7 @@ sys.dont_write_bytecode = True from common.externals import get_bgs_tool from common.num_ops import get_ordinal -from common.path_ops import safe_name +from common.path_ops import get_safe_name from common.patterns import PAT_AMI_PFAT from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t from common.system import script_init, argparse_init, printer @@ -139,7 +139,7 @@ def get_ami_pfat(input_buffer): return match, buffer def get_file_name(index, title): - return safe_name('%0.2d -- %s' % (index, title)) + return get_safe_name('%0.2d -- %s' % (index, title)) def parse_bg_script(script_data, padding): is_opcode_div = len(script_data) % 8 == 0 @@ -224,7 +224,7 @@ def parse_pfat_file(buffer, output_path, padding): extract_name = os.path.basename(output_path) - extract_path = os.path.join(output_path + '_extracted', '') + extract_path = os.path.join(output_path + '_extracted') if os.path.isdir(extract_path): shutil.rmtree(extract_path) diff --git a/AMI_UCP_Extract.py b/AMI_UCP_Extract.py index 2a72353..53b8b3b 100644 --- a/AMI_UCP_Extract.py +++ b/AMI_UCP_Extract.py @@ -7,7 +7,7 @@ AMI UCP BIOS Extractor Copyright (C) 2021-2022 Plato Mavropoulos """ -title = 'AMI UCP BIOS Extractor v2.0_a6' +title = 'AMI UCP BIOS Extractor v2.0_a7' import os import re @@ -16,6 +16,7 @@ import shutil import struct import ctypes import contextlib +from pathlib import Path, PurePath # Stop __pycache__ generation sys.dont_write_bytecode = True @@ -23,10 +24,11 @@ sys.dont_write_bytecode = True from common.a7z_comp import a7z_decompress, is_7z_supported from common.checksums import get_chk_16 from common.efi_comp import efi_decompress, is_efi_compressed -from common.path_ops import safe_name +from common.path_ops import get_safe_name, get_safe_path from common.patterns import PAT_AMI_UCP, PAT_INTEL_ENG from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t from common.system import script_init, argparse_init, printer +from common.text_ops import to_string from AMI_PFAT_Extract import get_ami_pfat, parse_pfat_file @@ -211,7 +213,7 @@ def ucp_extract(buffer, out_path, ucp_tag='@UAF', padding=0, is_checksum=False): printer('Utility Configuration Program', padding) - extract_path = os.path.join(out_path + '_extracted', '') + extract_path = os.path.join(out_path + '_extracted') if os.path.isdir(extract_path): shutil.rmtree(extract_path) @@ -259,7 +261,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na is_comp = uaf_mod.CompressSize != uaf_mod.OriginalSize # Detect @UAF|@HPU Module EFI Compression - if uaf_tag in nal_dict: uaf_name = nal_dict[uaf_tag] # Always prefer @NAL naming first + if uaf_tag in nal_dict: uaf_name = nal_dict[uaf_tag][1] # Always prefer @NAL naming first elif uaf_tag in UAF_TAG_DICT: uaf_name = UAF_TAG_DICT[uaf_tag][0] # Otherwise use built-in naming elif uaf_tag == '@ROM': uaf_name = 'BIOS.bin' # BIOS/PFAT Firmware (w/o Signature) elif uaf_tag.startswith('@R0'): uaf_name = 'BIOS_0%s.bin' % uaf_tag[3:] # BIOS/PFAT Firmware @@ -278,10 +280,16 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na # Check if unknown @UAF|@HPU Module Tag is present in @NAL but not in built-in dictionary if uaf_tag in nal_dict and uaf_tag not in UAF_TAG_DICT and not uaf_tag.startswith(('@ROM','@R0','@S0','@DR','@DS')): - printer('Note: Detected new AMI UCP Module %s (%s) in @NAL!' % (uaf_tag, nal_dict[uaf_tag]), padding + 4, pause=True) + printer('Note: Detected new AMI UCP Module %s (%s) in @NAL!' % (uaf_tag, nal_dict[uaf_tag][1]), padding + 4, pause=True) # Generate @UAF|@HPU Module File name, depending on whether decompression will be required - uaf_fname = os.path.join(extract_path, safe_name(uaf_name + ('.temp' if is_comp else uaf_fext))) + uaf_sname = get_safe_name(uaf_name + ('.temp' if is_comp else uaf_fext)) + if uaf_tag in nal_dict: + uaf_npath = get_safe_path(extract_path, nal_dict[uaf_tag][0]) + Path.mkdir(Path(uaf_npath), parents=True, exist_ok=True) + uaf_fname = get_safe_path(uaf_npath, uaf_sname) + else: + uaf_fname = get_safe_path(extract_path, uaf_sname) if is_checksum: chk16_validate(uaf_data_all, uaf_tag, padding + 4) @@ -375,15 +383,19 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na # Parse all @NAL Module Entries for info in nal_info: - info_tag,info_val = info.split(':',1) + info_tag,info_value = info.split(':',1) - printer(info_tag + ' : ' + info_val, padding + 8, False) # Print @NAL Module Tag-Path Info + printer(info_tag + ' : ' + info_value, padding + 8, False) # Print @NAL Module Tag-Path Info - nal_dict[info_tag] = os.path.basename(info_val) # Assign a file name (w/o path) to each Tag + info_part = PurePath(info_value.replace('\\', os.sep)).parts # Split path in parts + info_path = to_string(info_part[1:-1], os.sep) # Get path without drive/root or file + info_name = info_part[-1] # Get file from last path part + + nal_dict[info_tag] = (info_path,info_name) # Assign a file path & name to each Tag # Parse Insyde BIOS @UAF|@HPU Module (@INS) if uaf_tag == '@INS' and is_7z_supported(uaf_fname): - ins_dir = os.path.join(extract_path, safe_name(uaf_tag + '_nested-SFX')) # Generate extraction directory + ins_dir = os.path.join(extract_path, get_safe_name(uaf_tag + '_nested-SFX')) # Generate extraction directory printer('Insyde BIOS 7z SFX Archive:', padding + 4) @@ -394,7 +406,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na pfat_match,pfat_buffer = get_ami_pfat(uaf_data_raw) if pfat_match: - pfat_dir = os.path.join(extract_path, safe_name(uaf_name)) + pfat_dir = os.path.join(extract_path, get_safe_name(uaf_name)) parse_pfat_file(pfat_buffer, pfat_dir, padding + 4) @@ -410,7 +422,7 @@ def uaf_extract(buffer, extract_path, mod_info, padding=0, is_checksum=False, na # Parse Nested AMI UCP Structure if nested_uaf_off: - uaf_dir = os.path.join(extract_path, safe_name(uaf_tag + '_nested-UCP')) # Generate extraction directory + uaf_dir = os.path.join(extract_path, get_safe_name(uaf_tag + '_nested-UCP')) # Generate extraction directory ucp_extract(nested_uaf_bin, uaf_dir, nested_uaf_tag, padding + 4, is_checksum) # Call recursively diff --git a/Dell_PFS_Extract.py b/Dell_PFS_Extract.py index 601b95a..b679fdb 100644 --- a/Dell_PFS_Extract.py +++ b/Dell_PFS_Extract.py @@ -7,7 +7,7 @@ Dell PFS Update Extractor Copyright (C) 2018-2022 Plato Mavropoulos """ -title = 'Dell PFS Update Extractor v6.0_a4' +title = 'Dell PFS Update Extractor v6.0_a5' import os import io @@ -22,7 +22,7 @@ import contextlib sys.dont_write_bytecode = True from common.checksums import get_chk_8_xor -from common.path_ops import safe_name +from common.path_ops import get_safe_name from common.patterns import PAT_DELL_HDR, PAT_DELL_FTR, PAT_DELL_PKG from common.struct_ops import get_struct, char, uint8_t, uint16_t, uint32_t, uint64_t from common.system import script_init, argparse_init, printer @@ -243,7 +243,7 @@ def pfs_section_parse(zlib_data, zlib_start, output_path, pfs_name, pfs_index, p printer('Extracting Dell PFS %d >%s > %s' % (pfs_index, pfs_name, section_name), padding) # Set PFS ZLIB Section extraction sub-directory path - section_path = os.path.join(output_path, section_name) + section_path = os.path.join(output_path, get_safe_name(section_name)) # Delete existing extraction sub-directory (not in recursions) if os.path.isdir(section_path) and not is_rec: shutil.rmtree(section_path) @@ -406,7 +406,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i name_start = info_start + PFS_INFO_LEN + PFS_NAME_LEN # PFS Entry's FileName start offset name_size = entry_info_mod.CharacterCount * 2 # PFS Entry's FileName buffer total size name_data = filename_info[name_start:name_start + name_size] # PFS Entry's FileName buffer - entry_name = safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value + entry_name = get_safe_name(name_data.decode('utf-16').strip()) # PFS Entry's FileName value # Show PFS FileName Structure info if is_structure: @@ -443,7 +443,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i # As Nested PFS Entry Name, we'll use the actual PFS File Name # Replace common Windows reserved/illegal filename characters - entry_name = safe_name(entry_info.FileName.decode('utf-8').strip('.exe')) + entry_name = get_safe_name(entry_info.FileName.decode('utf-8').strip('.exe')) # As Nested PFS Entry Version, we'll use the actual PFS File Version entry_version = entry_info.FileVersion.decode('utf-8') @@ -537,7 +537,7 @@ def pfs_extract(buffer, pfs_index, pfs_name, pfs_count, output_path, pfs_padd, i sub_pfs_name = ' %s v%s' % (info_all[pfs_count - 2][1], info_all[pfs_count - 2][2]) if info_all else ' UNKNOWN' # Set the sub-PFS output path (create sub-folders for each sub-PFS and its ZLIB sections) - sub_pfs_path = os.path.join(output_path, str(pfs_count) + sub_pfs_name) + sub_pfs_path = os.path.join(output_path, str(pfs_count) + get_safe_name(sub_pfs_name)) # Recursively call the PFS ZLIB Section Parser function for the sub-PFS Volume (pfs_index = pfs_count) pfs_section_parse(entry_data, offset, sub_pfs_path, sub_pfs_name, pfs_count, pfs_count, True, pfs_padd + 4, is_structure, is_advanced) @@ -844,7 +844,7 @@ def chk_pfs_ftr(footer_buffer, data_buffer, data_size, text, padding, is_structu def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding, is_structure=True, is_advanced=True): # Store Data/Metadata Signature (advanced users only) if bin_name.startswith('sign'): - final_name = '%s.%s.sig' % (safe_name(full_name), bin_name.split('_')[1]) + final_name = '%s.%s.sig' % (get_safe_name(full_name), bin_name.split('_')[1]) final_path = os.path.join(out_path, final_name) with open(final_path, 'wb') as pfs_out: pfs_out.write(bin_buff) # Write final Data/Metadata Signature @@ -857,7 +857,7 @@ def pfs_file_write(bin_buff, bin_name, bin_type, full_name, out_path, padding, i # Some Data may be Text or XML files with useful information for non-advanced users is_text,final_data,file_ext,write_mode = bin_is_text(bin_buff, bin_type, bin_name == 'meta', padding, is_structure, is_advanced) - final_name = '%s%s' % (safe_name(full_name), bin_ext[:-4] + file_ext if is_text else bin_ext) + final_name = '%s%s' % (get_safe_name(full_name), bin_ext[:-4] + file_ext if is_text else bin_ext) final_path = os.path.join(out_path, final_name) with open(final_path, write_mode) as pfs_out: pfs_out.write(final_data) # Write final Data/Metadata Payload diff --git a/common/a7z_comp.py b/common/a7z_comp.py index 7e6364e..7226015 100644 --- a/common/a7z_comp.py +++ b/common/a7z_comp.py @@ -42,4 +42,4 @@ def a7z_decompress(in_path, out_path, in_name, padding, static=False): printer('Succesfull %s decompression via 7-Zip!' % in_name, padding) - return 0 \ No newline at end of file + return 0 diff --git a/common/checksums.py b/common/checksums.py index 02b6a74..ea150e9 100644 --- a/common/checksums.py +++ b/common/checksums.py @@ -18,4 +18,4 @@ def get_chk_8_xor(data, value=0): value ^= 0x0 - return value \ No newline at end of file + return value diff --git a/common/efi_comp.py b/common/efi_comp.py index 853f15d..ed18cf5 100644 --- a/common/efi_comp.py +++ b/common/efi_comp.py @@ -48,4 +48,4 @@ def efi_decompress(in_path, out_path, padding, comp_type='--uefi'): printer('Succesfull EFI/Tiano decompression via TianoCompress!', padding) - return 0 \ No newline at end of file + return 0 diff --git a/common/externals.py b/common/externals.py index 1ce7156..674b65f 100644 --- a/common/externals.py +++ b/common/externals.py @@ -8,4 +8,4 @@ def get_bgs_tool(): except: BigScript = None - return BigScript \ No newline at end of file + return BigScript diff --git a/common/num_ops.py b/common/num_ops.py index 7272fc6..35e8044 100644 --- a/common/num_ops.py +++ b/common/num_ops.py @@ -8,4 +8,4 @@ def get_ordinal(number): v = number % 100 - return f'{number}{s[v % 10]}' if v > 13 else f'{number}{s[v]}' \ No newline at end of file + return f'{number}{s[v % 10]}' if v > 13 else f'{number}{s[v]}' diff --git a/common/path_ops.py b/common/path_ops.py index efbf018..9722a63 100644 --- a/common/path_ops.py +++ b/common/path_ops.py @@ -7,14 +7,55 @@ import sys import inspect from pathlib import Path +from common.text_ops import to_string + # Fix illegal/reserved Windows characters -def safe_name(in_name): +def get_safe_name(in_name): raw_name = repr(in_name).strip("'") fix_name = re.sub(r'[\\/*?:"<>|]', '_', raw_name) return fix_name +# Check and attempt to fix illegal/unsafe OS path traversals +def get_safe_path(base_path, user_paths, follow_symlinks=False): + # Convert user path(s) to string w/ OS separators + user_path = to_string(user_paths, os.sep) + + # Create target path from base + requested user path + target_path = get_norm_path(base_path, user_path) + + # Check if target path is OS illegal/unsafe + if is_safe_path(base_path, target_path, follow_symlinks): + return target_path + + # Re-create target path from base + leveled/safe illegal "path" (now file) + nuked_path = get_norm_path(base_path, get_safe_name(user_path)) + + # Check if illegal path leveling worked + if is_safe_path(base_path, nuked_path, follow_symlinks): + return nuked_path + + # Still illegal, create fallback base path + placeholder file + failed_path = get_norm_path(base_path, 'illegal_path_traversal') + + return failed_path + +# Check for illegal/unsafe OS path traversal +def is_safe_path(base_path, target_path, follow_symlinks=True): + if follow_symlinks: + actual_path = os.path.realpath(target_path) + else: + actual_path = os.path.abspath(target_path) + + common_path = os.path.commonpath((base_path, actual_path)) + + return base_path == common_path + +# Create normalized base path + OS separator + user path +def get_norm_path(base_path, user_path): + return os.path.normpath(base_path + os.sep + user_path) + # Walk path to get all files def get_path_files(in_path): path_files = [] @@ -76,4 +117,4 @@ def get_script_dir(follow_symlinks=True): if follow_symlinks: path = os.path.realpath(path) - return os.path.dirname(path) \ No newline at end of file + return os.path.dirname(path) diff --git a/common/patterns.py b/common/patterns.py index 56bee4f..2e416f8 100644 --- a/common/patterns.py +++ b/common/patterns.py @@ -5,7 +5,7 @@ import re PAT_AMI_PFAT = re.compile(b'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS', re.DOTALL) PAT_AMI_UCP = re.compile(br'@(UAF|HPU).{12}@', re.DOTALL) -PAT_DELL_PKG = re.compile(br'\x72\x13\x55\x00.{45}7zXZ', re.DOTALL) -PAT_DELL_HDR = re.compile(br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C', re.DOTALL) PAT_DELL_FTR = re.compile(br'\xEE\xAA\xEE\x8F\x49\x1B\xE8\xAE\x14\x37\x90') -PAT_INTEL_ENG = re.compile(br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))', re.DOTALL) \ No newline at end of file +PAT_DELL_HDR = re.compile(br'\xEE\xAA\x76\x1B\xEC\xBB\x20\xF1\xE6\x51.\x78\x9C', re.DOTALL) +PAT_DELL_PKG = re.compile(br'\x72\x13\x55\x00.{45}7zXZ', re.DOTALL) +PAT_INTEL_ENG = re.compile(br'\x04\x00{3}[\xA1\xE1]\x00{3}.{8}\x86\x80.{9}\x00\$((MN2)|(MAN))', re.DOTALL) diff --git a/common/struct_ops.py b/common/struct_ops.py index 9cb63fe..bd418da 100644 --- a/common/struct_ops.py +++ b/common/struct_ops.py @@ -21,4 +21,4 @@ def get_struct(buffer, start_offset, class_name, param_list=None): ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len) - return structure \ No newline at end of file + return structure diff --git a/common/system.py b/common/system.py index 449e1b7..8fcf156 100644 --- a/common/system.py +++ b/common/system.py @@ -6,7 +6,7 @@ import ctypes import argparse import traceback -from common.text_ops import padder +from common.text_ops import padder, to_string from common.path_ops import process_input_files # Get Python Version (tuple) @@ -106,11 +106,8 @@ def nice_exc_handler(exc_type, exc_value, tb): sys.exit(3) # Show message(s) while controlling padding, newline, pausing & separator -def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' '): - if type(in_message).__name__ in ('list','tuple'): - message = sep_char.join(map(str, in_message)) - else: - message = str(in_message) +def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' '): + message = to_string(in_message, sep_char) padding = padder(padd_count) @@ -118,4 +115,4 @@ def printer(in_message='', padd_count=0, new_line=True, pause=False, sep_char=' output = newline + padding + message - (input if pause and not is_auto_exit() else print)(output) \ No newline at end of file + (input if pause and not is_auto_exit() else print)(output) diff --git a/common/text_ops.py b/common/text_ops.py index adc9e6e..dbb40a4 100644 --- a/common/text_ops.py +++ b/common/text_ops.py @@ -3,4 +3,13 @@ # Generate padding (spaces or tabs) def padder(padd_count, tab=False): - return ('\t' if tab else ' ') * padd_count \ No newline at end of file + return ('\t' if tab else ' ') * padd_count + +# Get String from given input object +def to_string(input_object, sep_char=''): + if type(input_object).__name__ in ('list','tuple'): + output_string = sep_char.join(map(str, input_object)) + else: + output_string = str(input_object) + + return output_string