AMI BIOS Guard Extractor v3.0

Added AMI PFAT Component new extraction method
Added AMI PFAT Nested PFAT Component extraction
Added Intel BIOS Guard Block Header detailed info
Added Intel BIOS Guard Block Script decompilation
Applied various code fixes & improvements

ABGE v3.0 utility's extracting behavior has now changed. Please read the new README Description. The optional support for Intel BGSL decompilation is provided by the 3rd party dependency "BIOS Guard Script Tool" by @allowitsme. Thanks to @NikolajSchlej for bringing ABGE's v2.0 problematic extraction behavior to my attention.
This commit is contained in:
Plato Mavropoulos 2020-12-06 19:23:44 +02:00
parent 65b26c48a9
commit fa1283ee54
2 changed files with 210 additions and 71 deletions

View file

@ -1,17 +1,51 @@
#!/usr/bin/env python3
#coding=utf-8
"""
AMI PFAT Extract
AMI BIOS Guard Extractor
Copyright (C) 2018-2019 Plato Mavropoulos
Copyright (C) 2018-2020 Plato Mavropoulos
"""
print('AMI BIOS Guard Extractor v2.0')
print('AMI BIOS Guard Extractor v3.0')
import sys
# Detect Python version
sys_ver = sys.version_info
if sys_ver < (3,7) :
sys.stdout.write('\n\nError: Python >= 3.7 required, not %d.%d!\n' % (sys_ver[0], sys_ver[1]))
(raw_input if sys_ver[0] <= 2 else input)('\nPress enter to exit')
sys.exit(1)
import os
import sys
import re
import ctypes
import struct
import shutil
import traceback
# https://stackoverflow.com/a/781074 by Torsten Marek
def show_exception_and_exit(exc_type, exc_value, tb) :
if exc_type is KeyboardInterrupt :
print('\n')
else :
print('\nError: ABGE crashed, please report the following:\n')
traceback.print_exception(exc_type, exc_value, tb)
input('\nPress enter to exit')
sys.exit(1)
# Pause after any unexpected python exception
sys.excepthook = show_exception_and_exit
sys.dont_write_bytecode = True
# https://github.com/allowitsme/big-tool by Dmitry Frolov
try :
from big_script_tool import BigScript
is_bgst = True
except :
is_bgst = False
# Set ctypes Structure types
char = ctypes.c_char
@ -20,37 +54,37 @@ uint16_t = ctypes.c_ushort
uint32_t = ctypes.c_uint
uint64_t = ctypes.c_uint64
# noinspection PyTypeChecker
class PFAT_Header(ctypes.LittleEndianStructure) :
_pack_ = 1
_fields_ = [
('Size', uint32_t), # 0x00
('Validation', uint32_t), # 0x04 Unknown 16-bit Checksum ?
('Checksum', uint32_t), # 0x04 Unknown 16-bits
('Tag', char*8), # 0x04 _AMIPFAT
('Control', uint8_t), # 0x10 0x4
('Flags', uint8_t), # 0x10
# 0x11
]
def pfat_print(self) :
print('\nPFAT Main Header:\n')
print(' Size : 0x%X' % self.Size)
print(' Validation : 0x%X' % self.Validation)
print(' Tag : %s' % self.Tag.decode('utf-8'))
print(' Control : 0x%X' % self.Control)
print(' Size : 0x%X' % self.Size)
print(' Checksum : 0x%0.4X' % self.Checksum)
print(' Tag : %s' % self.Tag.decode('utf-8'))
print(' Flags : 0x%0.2X' % self.Flags)
# noinspection PyTypeChecker
class PFAT_Block_Header(ctypes.LittleEndianStructure) :
_pack_ = 1
_fields_ = [
('Revision', uint32_t), # 0x00 PFAT
('Platform', char*16), # 0x04
('Unknown0', uint32_t), # 0x14
('Unknown1', uint32_t), # 0x18
('FlagsSize', uint32_t), # 0x1C From Block Header end
('DataSize', uint32_t), # 0x20 From Block Flags end
('Unknown2', uint32_t), # 0x24
('Unknown3', uint32_t), # 0x28
('Unknown4', uint32_t), # 0x2C
('PFATVerMajor', uint16_t), # 0x00
('PFATVerMinor', uint16_t), # 0x02
('PlatformID', uint8_t*16), # 0x04
('Attributes', uint32_t), # 0x14
('ScriptVerMajor', uint16_t), # 0x16
('ScriptVerMinor', uint16_t), # 0x18
('ScriptSize', uint32_t), # 0x1C
('DataSize', uint32_t), # 0x20
('BIOSSVN', uint32_t), # 0x24
('ECSVN', uint32_t), # 0x28
('VendorInfo', uint32_t), # 0x2C
# 0x30
]
@ -58,19 +92,53 @@ class PFAT_Block_Header(ctypes.LittleEndianStructure) :
super().__init__(*args, **kwargs)
self.count = count
def pfat_print(self) :
print('\n PFAT Block %s Header:\n' % self.count)
print(' Revision : %d' % self.Revision)
print(' Platform : %s' % self.Platform.decode('utf-8'))
print(' Unknown 0 : 0x%X' % self.Unknown0)
print(' Unknown 1 : 0x%X' % self.Unknown1)
print(' Flags Size : 0x%X' % self.FlagsSize)
print(' Data Size : 0x%X' % self.DataSize)
print(' Unknown 2 : 0x%X' % self.Unknown2)
print(' Unknown 3 : 0x%X' % self.Unknown3)
print(' Unknown 4 : 0x%X' % self.Unknown4)
def get_flags(self) :
attr = PFAT_Block_Header_GetAttributes()
attr.asbytes = self.Attributes
return attr.b.SFAM, attr.b.ProtectEC, attr.b.GFXMitDis, attr.b.FTU, attr.b.Reserved
def pfat_print(self) :
no_yes = ['No','Yes']
f1,f2,f3,f4,f5 = self.get_flags()
PlatformID = bytes(self.PlatformID).strip(b'\x00')
if PlatformID.isalpha() : # STRING
PlatformID = PlatformID.decode('utf-8', 'ignore')
else : # GUID
PlatformID = '%0.*X' % (0x10 * 2, int.from_bytes(self.PlatformID, 'big'))
PlatformID = '{%s-%s-%s-%s-%s}' % (PlatformID[:8], PlatformID[8:12], PlatformID[12:16], PlatformID[16:20], PlatformID[20:])
print('\n PFAT Block %s Header:\n' % self.count)
print(' PFAT Version : %d.%d' % (self.PFATVerMajor, self.PFATVerMinor))
print(' Platform ID : %s' % PlatformID)
print(' Signed Flash Address Map : %s' % no_yes[f1])
print(' Protected EC OpCodes : %s' % no_yes[f2])
print(' Graphics Security Disable : %s' % no_yes[f3])
print(' Fault Tolerant Update : %s' % no_yes[f4])
print(' Attributes Reserved : 0x%X' % f5)
print(' Script Version : %d.%d' % (self.ScriptVerMajor, self.ScriptVerMinor))
print(' Script Size : 0x%X' % self.ScriptSize)
print(' Data Size : 0x%X' % self.DataSize)
print(' BIOS SVN : 0x%X' % self.BIOSSVN)
print(' EC SVN : 0x%X' % self.ECSVN)
print(' Vendor Info : 0x%X' % self.VendorInfo)
class PFAT_Block_Header_Attributes(ctypes.LittleEndianStructure):
_fields_ = [
('SFAM', uint32_t, 1), # Signed Flash Address Map
('ProtectEC', uint32_t, 1), # Protected EC OpCodes
('GFXMitDis', uint32_t, 1), # GFX Security Disable
('FTU', uint32_t, 1), # Fault Tolerant Update
('Reserved', uint32_t, 28)
]
class PFAT_Block_Header_GetAttributes(ctypes.Union):
_fields_ = [
('b', PFAT_Block_Header_Attributes),
('asbytes', uint32_t)
]
# noinspection PyTypeChecker
class PFAT_Block_RSA(ctypes.LittleEndianStructure) :
_pack_ = 1
_fields_ = [
@ -87,17 +155,17 @@ class PFAT_Block_RSA(ctypes.LittleEndianStructure) :
self.count = count
def pfat_print(self) :
RSAPublicKey = ''.join('%0.8X' % int.from_bytes(struct.pack('<I', val), 'little') for val in reversed(self.PublicKey))
RSASignature = ''.join('%0.8X' % int.from_bytes(struct.pack('<I', val), 'little') for val in reversed(self.Signature))
PublicKey = '%0.*X' % (0x100 * 2, int.from_bytes(self.PublicKey, 'little'))
Signature = '%0.*X' % (0x100 * 2, int.from_bytes(self.Signature, 'little'))
print('\n PFAT Block %s Signature:\n' % self.count)
print(' Unknown 0 : 0x%X' % self.Unknown0)
print(' Unknown 1 : 0x%X' % self.Unknown1)
print(' Public Key : %s [...]' % RSAPublicKey[:8])
print(' Exponent : 0x%X' % self.Exponent)
print(' Signature : %s [...]' % RSASignature[:8])
print('\n PFAT Block %s Signature:\n' % self.count)
print(' Unknown 0 : 0x%X' % self.Unknown0)
print(' Unknown 1 : 0x%X' % self.Unknown1)
print(' Public Key : %s [...]' % PublicKey[:8])
print(' Exponent : 0x%X' % self.Exponent)
print(' Signature : %s [...]' % Signature[:8])
# Process ctypes Structure Classes
# https://github.com/skochinsky/me-tools/blob/master/me_unpack.py by Igor Skochinsky
def get_struct(buffer, start_offset, class_name, param_list = None) :
if param_list is None : param_list = []
@ -107,7 +175,7 @@ def get_struct(buffer, start_offset, class_name, param_list = None) :
fit_len = min(len(struct_data), struct_len)
if (start_offset >= len(buffer)) or (fit_len < struct_len) :
print('Error: Offset 0x%X out of bounds at %s, possibly incomplete image!' % (start_offset, class_name))
input('\nError: Offset 0x%X out of bounds at %s, possibly incomplete image!' % (start_offset, class_name.__name__))
sys.exit(1)
ctypes.memmove(ctypes.addressof(structure), struct_data, fit_len)
@ -116,72 +184,131 @@ def get_struct(buffer, start_offset, class_name, param_list = None) :
if len(sys.argv) >= 2 :
# Drag & Drop or CLI
pfat = sys.argv[1:]
ami_pfat = sys.argv[1:]
else :
# Folder path
pfat = []
ami_pfat = []
in_path = input('\nEnter the full folder path: ')
print('\nWorking...')
for root, dirs, files in os.walk(in_path):
for name in files :
pfat.append(os.path.join(root, name))
ami_pfat.append(os.path.join(root, name))
for input_file in pfat :
with open(input_file, 'rb') as in_file : buffer = in_file.read()
pfat_index = 1
output_path = ''
block_hdr_size = ctypes.sizeof(PFAT_Block_Header)
block_rsa_size = ctypes.sizeof(PFAT_Block_RSA)
pfat_pat = re.compile(b'_AMIPFAT.AMI_BIOS_GUARD_FLASH_CONFIGURATIONS', re.DOTALL)
for input_file in ami_pfat :
input_name,input_extension = os.path.splitext(os.path.basename(input_file))
input_dir = os.path.dirname(os.path.abspath(input_file))
file_data = b''
final_image = b''
block_name = ''
block_count = 0
file_index = 0
blocks = []
pfat_hdr = get_struct(buffer, 0, PFAT_Header)
with open(input_file, 'rb') as in_file : buffer = in_file.read()
if pfat_hdr.Tag.decode('utf-8', 'ignore') != '_AMIPFAT' : continue
pfat_match = pfat_pat.search(buffer)
if not pfat_match : continue
buffer = buffer[pfat_match.start() - 0x8:]
pfat_hdr = get_struct(buffer, 0, PFAT_Header)
hdr_size = pfat_hdr.Size
hdr_data = buffer[0x11:hdr_size].decode('utf-8').splitlines()
pfat_hdr.pfat_print()
print(' Title : %s' % hdr_data[0])
print(' Title : %s' % hdr_data[0])
if pfat_index == 1 :
output_path = os.path.join(input_dir, '%s%s' % (input_name, input_extension) + '_extracted') # Set extraction directory
if os.path.isdir(output_path) : shutil.rmtree(output_path) # Delete any existing extraction directory
os.mkdir(output_path) # Create extraction directory
file_path = os.path.join(output_path, '%d' % pfat_index)
for entry in hdr_data[1:] :
entry_data = entry.split(' ')
entry_data = [s for s in entry_data if s != '']
entry_flash = int(entry_data[0])
entry_flags = int(entry_data[0])
entry_param = entry_data[1]
entry_blocks = int(entry_data[2])
entry_name = entry_data[3][1:]
for i in range(entry_blocks) : blocks.append([entry_name, entry_param, entry_flash, i + 1, entry_blocks])
for i in range(entry_blocks) : blocks.append([entry_name, entry_param, entry_flags, i + 1, entry_blocks])
block_count += entry_blocks
block_start = hdr_size
for i in range(block_count) :
if blocks[i][0] != block_name : print('\n%s (Parameter: %s, Update: %s)' % (blocks[i][0], blocks[i][1], ['No','Yes'][blocks[i][2]]))
is_file_start = blocks[i][0] != block_name
if is_file_start : print('\n %s (Parameter: %s, Flags: 0x%X)' % (blocks[i][0], blocks[i][1], blocks[i][2]))
block_hdr = get_struct(buffer, block_start, PFAT_Block_Header, ['%d/%d' % (blocks[i][3], blocks[i][4])])
block_hdr_size = ctypes.sizeof(PFAT_Block_Header)
block_flag_size = block_hdr.FlagsSize
flag_data = buffer[block_start + block_hdr_size:block_start + block_hdr_size + block_flag_size] # Flags not parsed
block_data_start = block_start + block_hdr_size + block_flag_size
block_data_end = block_data_start + block_hdr.DataSize
block_hdr.pfat_print()
block_script_size = block_hdr.ScriptSize
block_script_data = buffer[block_start + block_hdr_size:block_start + block_hdr_size + block_script_size]
block_data_start = block_start + block_hdr_size + block_script_size
block_data_end = block_data_start + block_hdr.DataSize
block_data = buffer[block_data_start:block_data_end]
block_rsa = get_struct(buffer, block_data_end, PFAT_Block_RSA, ['%d/%d' % (blocks[i][3], blocks[i][4])])
block_rsa_size = ctypes.sizeof(PFAT_Block_RSA)
#block_rsa_exp = block_rsa.Exponent
#block_rsa_pkey = int((''.join('%0.8X' % int.from_bytes(struct.pack('<I', val), 'little') for val in reversed(block_rsa.PublicKey))), 16)
#block_rsa_sign = int((''.join('%0.8X' % int.from_bytes(struct.pack('<I', val), 'little') for val in reversed(block_rsa.Signature))), 16)
#block_rsa_sign_dec = '%X' % pow(block_rsa_sign, block_rsa_exp, block_rsa_pkey) # Decrypted signature is 4096 bits
block_rsa.pfat_print()
final_image += buffer[block_data_start:block_data_end]
print('\n PFAT Block %d/%d Script:\n' % (blocks[i][3], blocks[i][4]))
is_opcode_div = len(block_script_data) % 8 == 0
is_begin_end = block_script_data[:8] + block_script_data[-8:] == b'\x01' + b'\x00' * 7 + b'\xFF' + b'\x00' * 7
if is_opcode_div and is_begin_end and is_bgst :
block_script_decomp = BigScript(code_bytes=block_script_data)
block_script_lines = block_script_decomp.to_string().replace('\t',' ').split('\n')
for line in block_script_lines :
spacing = ' ' * 12 if line.endswith(('begin','end',':')) else ' ' * 20
operands = [op for op in line.split(' ') if op != '']
print(spacing + ('{:<12s}' + '{:<11s}' * (len(operands) - 1)).format(*operands))
elif not is_opcode_div :
print(' Error: Script not divisible by OpCode length!')
elif not is_begin_end :
print(' Error: Script lacks Begin and/or End OpCodes!')
elif not is_bgst :
print(' Error: BIOS Guard Script Tool dependency missing!')
file_data += block_data
final_image += block_data
if i and is_file_start and file_data :
file_index += 1
with open('%s_%0.2d -- %s' % (file_path, file_index, block_name), 'wb') as o : o.write(file_data)
file_data = b''
block_name = blocks[i][0]
block_start = block_data_end + block_rsa_size
final_image += buffer[block_start:] # Append any data after the end of PFAT
with open('%s_%0.2d -- %s' % (file_path, file_index + 1, block_name), 'wb') as o : o.write(file_data) # Last File
with open('%s_unpacked.bin' % os.path.basename(input_file), 'wb') as final : final.write(final_image)
with open('%s_00 -- AMI_PFAT_%d_DATA_ALL.bin' % (file_path, pfat_index), 'wb') as final : final.write(final_image)
eof_data = buffer[block_start:] # Store any data after the end of PFAT
if eof_data[:-0x100] != b'\xFF' * (len(eof_data) - 0x100) :
eof_path = '%s_%0.2d -- AMI_PFAT_%d_DATA_END.bin' % (file_path, file_index + 2, pfat_index)
with open(eof_path, 'wb') as final : final.write(eof_data)
if pfat_pat.search(eof_data) :
pfat_index += 1
ami_pfat_index = ami_pfat.index(input_file) + 1
ami_pfat.insert(ami_pfat_index, eof_path)
else :
pfat_index = 1
else :
input('\nDone!')

View file

@ -60,11 +60,13 @@ Some Anti-Virus software may claim that the built/frozen/compiled executable con
## **AMI BIOS Guard Extractor**
![](https://i.imgur.com/ci3H4Y3.png)
![](https://i.imgur.com/p0rrlqv.png)
#### **Description**
Parses AMI BIOS Guard (a.k.a. PFAT) images and extracts a proper SPI/BIOS/UEFI firmware.
Parses AMI BIOS Guard (a.k.a. PFAT, Platform Firmware Armoring Technology) images, extracts their SPI/BIOS/UEFI firmware components and decompiles the Intel BIOS Guard Scripts. It supports all AMI PFAT revisions and formats, including those with nested AMI PFAT structures. The output comprises only final firmware components which are directly usable by end users.
Note that the AMI PFAT structure does not have an explicit component order. AMI's BIOS Guard Firmware Update Tool (AFUBGT) updates components based on the user/OEM provided Parameters and Options. That means that merging all the components together does not usually yield a proper SPI/BIOS/UEFI image. The utility does generate such a merged file with the name "X_00 -- AMI_PFAT_X_DATA_ALL.bin" but it is up to the end user to determine its usefulness. Moreover, any custom OEM data after the AMI PFAT structure are stored in a file with the name "X_YY -- AMI_PFAT_X_DATA_END.bin" and it is once again up to the end user to determine its usefulness. In cases where the trailing custom OEM data include a nested AMI PFAT structure, the utility will process and extract it automatically as well.
#### **Usage**
@ -80,7 +82,9 @@ Should work at all Windows, Linux or macOS operating systems which have Python 3
#### **Prerequisites**
To run the utility, you do not need any 3rd party tool.
To decompile the Intel BIOS Guard Scripts via the Python script, you need to additionally have the following 3rd party Python utility at the same directory:
* [BIOS Guard Script Tool](https://github.com/allowitsme/big-tool/tree/sdk-compat) (i.e. big_script_tool.py)
#### **Build/Freeze/Compile with PyInstaller**
@ -94,7 +98,11 @@ PyInstaller can build/freeze/compile the utility at all three supported platform
> pip3 install pyinstaller
3. Build/Freeze/Compile:
3. Copy BIOS Guard Script Tool dependency to build directory:
> AMI_PFAT_Extract.py, big_script_tool.py
4. Build/Freeze/Compile:
> pyinstaller --noupx --onefile AMI_PFAT_Extract.py
@ -104,6 +112,10 @@ At dist folder you should find the final utility executable
Some Anti-Virus software may claim that the built/frozen/compiled executable contains viruses. Any such detections are false positives, usually of PyInstaller. You can switch to a better Anti-Virus software, report the false positive to their support, add the executable to the exclusions, build/freeze/compile yourself or use the Python script directly.
#### **Pictures**
![](https://i.imgur.com/iZD3GY0.png)
## **Apple EFI Sucatalog Link Grabber**
![](https://i.imgur.com/zTVFs4I.png)