mirror of
https://github.com/chipsec/chipsec
synced 2026-06-08 13:31:00 +00:00
79f81bbb7e
This commit fixes whitespace errors from checkers W291,W292,W293,W391. They were found by running "flake8 ." from the root directory. These are errors defined by "pycodestyle", and have no functional impact to the source. This is simply fixing whitespace as defined by CHIPSEC's .flake8 configuration. background: https://www.flake8rules.com/rules/W291.html https://www.flake8rules.com/rules/W292.html https://www.flake8rules.com/rules/W293.html https://www.flake8rules.com/rules/W391.html tool versions: flake8 v7.3.0, python v3.13.11 Signed-off-by: William Leara <william.leara@dell.com>
1019 lines
38 KiB
Python
1019 lines
38 KiB
Python
# CHIPSEC: Platform Security Assessment Framework
|
|
# Copyright (c) 2025, Intel Corporation
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; Version 2.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# Contact information:
|
|
# chipsec@intel.com
|
|
#
|
|
|
|
|
|
"""
|
|
Simple ACPI AML Binary Parser
|
|
Extracts OperationRegion definitions and _CRS resource descriptors from DSDT/SSDT binaries without external tools.
|
|
Supports both static OperationRegion declarations and dynamic _CRS method resource descriptors.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict, Optional, Tuple
|
|
import struct
|
|
|
|
|
|
@dataclass
|
|
class OperationRegion:
|
|
"""Represents a single OperationRegion definition."""
|
|
name: str
|
|
space_type: int
|
|
space_type_name: str
|
|
base: int
|
|
length: int
|
|
source: str = "OperationRegion" # Track region source for debugging
|
|
|
|
|
|
class AMLParser:
|
|
"""Simple AML binary parser for OperationRegion enumeration."""
|
|
|
|
# AML Opcodes
|
|
OPCODE_OPERATION_REGION = 0x5B
|
|
SUBOPCODE_OPERATION_REGION = 0x80
|
|
OPCODE_NAME = 0x08
|
|
OPCODE_STORE = 0x70
|
|
OPCODE_METHOD = 0x14
|
|
OPCODE_RETURN = 0xA4
|
|
|
|
# Integer encoding opcodes
|
|
ZERO_OP = 0x00
|
|
ONE_OP = 0x01
|
|
BYTE_PREFIX = 0x0A
|
|
WORD_PREFIX = 0x0B
|
|
DWORD_PREFIX = 0x0C
|
|
QWORD_PREFIX = 0x0E
|
|
|
|
# Space type names
|
|
SPACE_TYPE_NAMES = {
|
|
0x00: "SystemMemory",
|
|
0x01: "SystemIO",
|
|
0x02: "PCI_Config",
|
|
0x03: "EmbeddedControl",
|
|
0x04: "SMBus",
|
|
0x05: "SystemCMOS",
|
|
0x06: "PciBarTarget",
|
|
0x07: "IPMI",
|
|
0x08: "GeneralPurposeIO",
|
|
0x09: "GenericSerialBus",
|
|
0x0A: "PCC",
|
|
0x0B: "PRM",
|
|
0x0C: "FFH",
|
|
0x0D: "GSBUS_OR_CCAGO",
|
|
}
|
|
|
|
def __init__(self):
|
|
self.regions: List[OperationRegion] = []
|
|
self.names: Dict[str, int] = {} # Symbol table for Name() declarations
|
|
|
|
def parse(self, dsdt_and_ssdts: List[bytes]) -> List[OperationRegion]:
|
|
"""
|
|
Parse DSDT and optional SSDTs for OperationRegion definitions.
|
|
|
|
Args:
|
|
dsdt_and_ssdts: List of raw ACPI table binaries (header + content)
|
|
|
|
Returns:
|
|
List of OperationRegion objects
|
|
"""
|
|
self.regions = []
|
|
self.names = {}
|
|
|
|
# First pass: collect all Name() declarations for symbol resolution
|
|
for table_data in dsdt_and_ssdts:
|
|
if len(table_data) < 36:
|
|
continue
|
|
aml_data = table_data[36:]
|
|
self._scan_for_names(aml_data)
|
|
|
|
# Second pass: scan for OperationRegion definitions
|
|
for table_data in dsdt_and_ssdts:
|
|
if len(table_data) < 36:
|
|
continue
|
|
aml_data = table_data[36:]
|
|
self._scan_for_regions(aml_data)
|
|
|
|
# Third pass: extract CRS Store field assignments (e.g., BAS1, LEN1, LIM1)
|
|
for table_data in dsdt_and_ssdts:
|
|
if len(table_data) < 36:
|
|
continue
|
|
aml_data = table_data[36:]
|
|
crs_executor = CRSExecutor()
|
|
crs_fields = crs_executor.extract_crs_fields(aml_data)
|
|
|
|
# Create synthetic OperationRegions for each BAS field found
|
|
for field_name, value in crs_fields.items():
|
|
if field_name.startswith('BAS'):
|
|
# Extract the numeric suffix (e.g., "BAS1" -> 1)
|
|
base_name = field_name.rstrip('\x00')
|
|
suffix = base_name[3:] # Get "1" from "BAS1"
|
|
|
|
# Look for corresponding LEN and LIM fields
|
|
len_name = f'LEN{suffix}'
|
|
lim_name = f'LIM{suffix}'
|
|
|
|
length = crs_fields.get(len_name, 0)
|
|
limit = crs_fields.get(lim_name, 0)
|
|
|
|
if value > 0 and length > 0:
|
|
# Try to get the buffer name (PMCR, SPIR, P2BR) from mapping
|
|
buffer_name = crs_executor.field_to_buffer.get(base_name, '_CRS')
|
|
|
|
# Create synthetic region from CRS fields
|
|
# Name format: PMCR:BAS1 or _CRS:BAS1 if buffer not found
|
|
region = OperationRegion(
|
|
name=f'{buffer_name}:{base_name}',
|
|
space_type=0, # SystemMemory
|
|
space_type_name='SystemMemory',
|
|
base=value,
|
|
length=length,
|
|
source='CRS_Fields'
|
|
)
|
|
self.regions.append(region)
|
|
|
|
# Sort by address for presentation
|
|
self.regions = sorted([r for r in self.regions if r.base < 0x1000000000],
|
|
key=lambda x: x.base)
|
|
|
|
return self.regions
|
|
|
|
def _scan_for_regions(self, aml_binary: bytes) -> None:
|
|
"""Scan binary for OperationRegion opcodes."""
|
|
i = 0
|
|
max_search = len(aml_binary)
|
|
iter_count = 0
|
|
max_iters = max_search * 2 # Safety: prevent infinite looping
|
|
|
|
while i < max_search - 1 and iter_count < max_iters:
|
|
iter_count += 1
|
|
try:
|
|
# Look for opcode sequence: 0x5B 0x80
|
|
if (aml_binary[i] == self.OPCODE_OPERATION_REGION and
|
|
i + 1 < len(aml_binary) and
|
|
aml_binary[i + 1] == self.SUBOPCODE_OPERATION_REGION):
|
|
|
|
# Found OperationRegion opcode, try to decode
|
|
region = self._decode_operation_region(aml_binary, i + 2)
|
|
if region:
|
|
self.regions.append(region)
|
|
# Always skip forward by at least 5 bytes
|
|
i += 5
|
|
else:
|
|
i += 1
|
|
except Exception:
|
|
# On any error, skip forward
|
|
i += 5
|
|
|
|
def _scan_for_names(self, aml_binary: bytes) -> None:
|
|
"""Scan binary for Name() declarations to build symbol table."""
|
|
i = 0
|
|
max_search = len(aml_binary)
|
|
iter_count = 0
|
|
max_iters = max_search * 2
|
|
|
|
while i < max_search - 1 and iter_count < max_iters:
|
|
iter_count += 1
|
|
try:
|
|
# Look for Name opcode: 0x08
|
|
if aml_binary[i] == self.OPCODE_NAME:
|
|
# Name(name, value) - try to extract
|
|
name, name_len = self._decode_name_string_simple(aml_binary, i + 1)
|
|
if name and name_len > 0 and name_len < 20:
|
|
pos = i + 1 + name_len
|
|
if pos < len(aml_binary):
|
|
value, val_len = self._decode_aml_integer(aml_binary, pos)
|
|
if value is not None and val_len > 0:
|
|
# Store in symbol table
|
|
self.names[name] = value
|
|
i += 5
|
|
else:
|
|
i += 1
|
|
except Exception:
|
|
i += 5
|
|
def _decode_operation_region(self, aml_binary: bytes, offset: int) -> Optional[OperationRegion]:
|
|
"""
|
|
Decode a single OperationRegion definition.
|
|
|
|
After opcode (0x5B 0x80):
|
|
NameString (variable length, 1-4 bytes typically)
|
|
SpaceType (1 byte)
|
|
Address (AML Integer, 1-9 bytes)
|
|
Length (AML Integer, 1-9 bytes)
|
|
"""
|
|
if offset >= len(aml_binary) or offset + 5 > len(aml_binary):
|
|
return None
|
|
|
|
# Extract NameString (simple: just read up to 4 bytes as NameSeg)
|
|
name, name_len = self._decode_name_string_simple(aml_binary, offset)
|
|
if not name or name_len <= 0 or name_len > 20:
|
|
return None
|
|
|
|
pos = offset + name_len
|
|
if pos >= len(aml_binary):
|
|
return None
|
|
|
|
# Extract SpaceType (1 byte)
|
|
space_type = aml_binary[pos]
|
|
pos += 1
|
|
|
|
if pos >= len(aml_binary):
|
|
return None
|
|
|
|
# Extract Address (AML Integer)
|
|
base, int_len = self._decode_aml_integer(aml_binary, pos)
|
|
if base is None:
|
|
# Try to resolve as variable reference
|
|
var_name, var_len = self._decode_name_string_simple(aml_binary, pos)
|
|
if var_name and var_name in self.names:
|
|
base = self.names[var_name]
|
|
int_len = var_len
|
|
else:
|
|
return None
|
|
|
|
if int_len <= 0 or int_len > 10:
|
|
return None
|
|
|
|
pos += int_len
|
|
if pos >= len(aml_binary):
|
|
return None
|
|
|
|
# Extract Length (AML Integer)
|
|
length, int_len = self._decode_aml_integer(aml_binary, pos)
|
|
if length is None:
|
|
# Try to resolve as variable reference
|
|
var_name, var_len = self._decode_name_string_simple(aml_binary, pos)
|
|
if var_name and var_name in self.names:
|
|
length = self.names[var_name]
|
|
int_len = var_len
|
|
else:
|
|
return None
|
|
|
|
if int_len <= 0 or int_len > 10:
|
|
return None
|
|
|
|
# Sanity checks
|
|
if length == 0 or length > 0x10000000: # Skip regions > 256MB
|
|
return None
|
|
if base == 0 or base == 0xFFFFFFFF: # Skip invalid addresses
|
|
return None
|
|
|
|
space_type_name = self.SPACE_TYPE_NAMES.get(space_type, f"Unknown(0x{space_type:02X})")
|
|
|
|
return OperationRegion(
|
|
name=name,
|
|
space_type=space_type,
|
|
space_type_name=space_type_name,
|
|
base=base,
|
|
length=length
|
|
)
|
|
|
|
def _decode_name_string_simple(self, aml_binary: bytes, offset: int) -> Tuple[Optional[str], int]:
|
|
"""
|
|
Simplified name string decoder - handles most common cases.
|
|
Returns (name_string, bytes_consumed) or (None, 0) on error.
|
|
"""
|
|
if offset >= len(aml_binary):
|
|
return None, 0
|
|
|
|
byte = aml_binary[offset]
|
|
|
|
# RootPath prefix (0x5C)
|
|
if byte == 0x5C:
|
|
pos = offset + 1
|
|
# Read NameSeg after root path
|
|
if pos + 4 <= len(aml_binary):
|
|
seg = self._read_name_seg(aml_binary, pos)
|
|
if seg:
|
|
return '\\' + seg, 5
|
|
return '\\', 1
|
|
|
|
# ParentPath prefix (0x5E) - skip for now, just return None
|
|
if byte == 0x5E:
|
|
return None, 0
|
|
|
|
# DualNamePath (0x2E)
|
|
if byte == 0x2E:
|
|
pos = offset + 1
|
|
parts = []
|
|
for _ in range(2):
|
|
if pos + 4 <= len(aml_binary):
|
|
seg = self._read_name_seg(aml_binary, pos)
|
|
if seg:
|
|
parts.append(seg)
|
|
pos += 4
|
|
else:
|
|
break
|
|
if len(parts) == 2:
|
|
return '.'.join(parts), pos - offset
|
|
return None, 0
|
|
|
|
# MultiNamePath (0x2F)
|
|
if byte == 0x2F:
|
|
pos = offset + 1
|
|
if pos >= len(aml_binary):
|
|
return None, 0
|
|
count = aml_binary[pos]
|
|
pos += 1
|
|
if count > 20 or count == 0: # Sanity check
|
|
return None, 0
|
|
parts = []
|
|
for _ in range(count):
|
|
if pos + 4 <= len(aml_binary):
|
|
seg = self._read_name_seg(aml_binary, pos)
|
|
if seg:
|
|
parts.append(seg)
|
|
pos += 4
|
|
else:
|
|
break
|
|
if len(parts) == count:
|
|
return '.'.join(parts), pos - offset
|
|
return None, 0
|
|
|
|
# Simple NameSeg (4 characters: A-Z, 0-9, _)
|
|
if self._is_name_char(byte):
|
|
if offset + 4 <= len(aml_binary):
|
|
seg = self._read_name_seg(aml_binary, offset)
|
|
if seg:
|
|
return seg, 4
|
|
|
|
return None, 0
|
|
|
|
def _read_name_seg(self, aml_binary: bytes, offset: int) -> Optional[str]:
|
|
"""
|
|
Read exactly 4 bytes as a NameSeg.
|
|
Valid characters: A-Z, a-z, 0-9, _
|
|
"""
|
|
if offset + 4 > len(aml_binary):
|
|
return None
|
|
|
|
seg = b''
|
|
for i in range(4):
|
|
byte = aml_binary[offset + i]
|
|
if self._is_name_char(byte):
|
|
seg += bytes([byte])
|
|
elif byte == 0x5F: # _ padding
|
|
seg += bytes([byte])
|
|
else:
|
|
# Invalid character in NameSeg
|
|
return None
|
|
|
|
# Decode and strip trailing underscores
|
|
name = seg.decode('ascii', errors='ignore').rstrip('_')
|
|
return name if name else None
|
|
|
|
def _is_name_char(self, byte: int) -> bool:
|
|
"""Check if byte is valid in a name (A-Z, a-z, 0-9, _)."""
|
|
return ((ord('A') <= byte <= ord('Z')) or
|
|
(ord('a') <= byte <= ord('z')) or
|
|
(ord('0') <= byte <= ord('9')) or
|
|
byte == ord('_'))
|
|
|
|
def _decode_aml_integer(self, aml_binary: bytes, offset: int) -> Tuple[Optional[int], int]:
|
|
"""
|
|
Decode AML Integer (variable-length encoding).
|
|
Returns (value, bytes_consumed) or (None, 0) if variable reference detected.
|
|
|
|
Formats:
|
|
0x00 = zero (1 byte)
|
|
0x01 = one (1 byte)
|
|
0x0A = byte value follows (2 bytes total)
|
|
0x0B = word value follows (3 bytes total)
|
|
0x0C = dword value follows (5 bytes total)
|
|
0x0E = qword value follows (9 bytes total)
|
|
0x02-0x09 = literal single-byte values
|
|
"""
|
|
if offset >= len(aml_binary):
|
|
return None, 0
|
|
|
|
opcode = aml_binary[offset]
|
|
|
|
# Zero and one
|
|
if opcode == self.ZERO_OP:
|
|
return 0, 1
|
|
elif opcode == self.ONE_OP:
|
|
return 1, 1
|
|
|
|
# Prefixed integers
|
|
elif opcode == self.BYTE_PREFIX:
|
|
if offset + 2 <= len(aml_binary):
|
|
return aml_binary[offset + 1], 2
|
|
elif opcode == self.WORD_PREFIX:
|
|
if offset + 3 <= len(aml_binary):
|
|
return struct.unpack('<H', aml_binary[offset + 1:offset + 3])[0], 3
|
|
elif opcode == self.DWORD_PREFIX:
|
|
if offset + 5 <= len(aml_binary):
|
|
return struct.unpack('<I', aml_binary[offset + 1:offset + 5])[0], 5
|
|
elif opcode == self.QWORD_PREFIX:
|
|
if offset + 9 <= len(aml_binary):
|
|
return struct.unpack('<Q', aml_binary[offset + 1:offset + 9])[0], 9
|
|
|
|
# Direct single-byte values (0x02-0x09)
|
|
elif 0x02 <= opcode <= 0x09:
|
|
return opcode, 1
|
|
|
|
# If it looks like a NameSeg or path, it's a variable reference
|
|
# Return None to indicate this can't be decoded statically
|
|
elif self._is_name_char(opcode) or opcode in (0x5C, 0x5E, 0x2E, 0x2F):
|
|
return None, 0
|
|
|
|
return None, 0
|
|
|
|
|
|
class ResourceDescriptorParser:
|
|
"""Parse resource descriptors in _CRS methods."""
|
|
|
|
# Large resource descriptor opcodes (bit 7 = 1)
|
|
DWORD_MEMORY = 0x87 # DWordMemory
|
|
QWORD_MEMORY = 0x8A # QWordMemory
|
|
DWORD_IO = 0x81 # DWordIO
|
|
QWORD_IO = 0x8B # QWordIO
|
|
DWORD_SPACE = 0x8C # DWordSpace
|
|
QWORD_SPACE = 0x8E # QWordSpace
|
|
|
|
def __init__(self):
|
|
self.regions: List[Dict] = []
|
|
|
|
def extract_from_crs_binary(self, aml_binary: bytes, field_values: Dict[str, int]) -> List[Dict]:
|
|
"""
|
|
Scan _CRS method binary for resource descriptors and extract memory regions.
|
|
field_values: dictionary of Store assignments (BAS1=0x..., LEN1=0x..., etc.)
|
|
"""
|
|
regions = []
|
|
i = 0
|
|
|
|
while i < len(aml_binary) - 2:
|
|
opcode = aml_binary[i]
|
|
|
|
# Large resource items have bit 7 set (0x80-0xFF)
|
|
if opcode >= 0x80:
|
|
try:
|
|
# Large resource format: opcode, length_low, length_high, data...
|
|
if i + 2 >= len(aml_binary):
|
|
i += 1
|
|
continue
|
|
|
|
length = aml_binary[i + 1] | (aml_binary[i + 2] << 8)
|
|
data_start = i + 3
|
|
data_end = data_start + length
|
|
|
|
if data_end > len(aml_binary):
|
|
i += 1
|
|
continue
|
|
|
|
# Parse based on opcode
|
|
if opcode in (self.DWORD_MEMORY, self.QWORD_MEMORY,
|
|
self.DWORD_IO, self.QWORD_IO,
|
|
self.DWORD_SPACE, self.QWORD_SPACE):
|
|
region = self._parse_address_space(opcode, aml_binary[data_start:data_end], field_values)
|
|
if region:
|
|
regions.append(region)
|
|
|
|
i = data_end
|
|
except Exception:
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
return regions
|
|
|
|
def _parse_address_space(self, opcode: int, data: bytes, field_values: Dict[str, int]) -> Optional[Dict]:
|
|
"""Parse DWordMemory, QWordMemory, etc. resource descriptor."""
|
|
if len(data) < 1:
|
|
return None
|
|
|
|
# Skip resource type specific parts, look for address/length
|
|
# Format varies, but generally: flags, type_specific, then min/max/length
|
|
|
|
try:
|
|
if opcode in (self.DWORD_MEMORY, self.DWORD_IO, self.DWORD_SPACE):
|
|
# DWord format
|
|
if len(data) >= 20:
|
|
# Typical: flags(1), type(1), ... min(4), max(4), len(4)
|
|
# This is simplified - actual format varies by type
|
|
base = struct.unpack('<I', data[10:14])[0] if len(data) >= 14 else None
|
|
size = struct.unpack('<I', data[16:20])[0] if len(data) >= 20 else None
|
|
|
|
if base and size and size > 0 and size < 0x10000000:
|
|
return {
|
|
'name': f'MEM_0x{base:08X}',
|
|
'base': base,
|
|
'size': size
|
|
}
|
|
|
|
elif opcode in (self.QWORD_MEMORY, self.QWORD_IO, self.QWORD_SPACE):
|
|
# QWord format
|
|
if len(data) >= 36:
|
|
# Similar structure but 64-bit addresses
|
|
base = struct.unpack('<Q', data[14:22])[0] if len(data) >= 22 else None
|
|
size = struct.unpack('<Q', data[28:36])[0] if len(data) >= 36 else None
|
|
|
|
if base and size and size > 0 and size < 0x10000000000:
|
|
return {
|
|
'name': f'MEM_0x{base:016X}',
|
|
'base': base,
|
|
'size': size
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
class CRSExecutor:
|
|
"""Simple extractor for field assignments in _CRS methods."""
|
|
|
|
OPCODE_STORE = 0x70
|
|
OPCODE_METHOD = 0x14
|
|
OPCODE_DEVICE = 0x82
|
|
OPCODE_SCOPE = 0x10
|
|
OPCODE_CREATE_DWORD_FIELD = 0x8A
|
|
|
|
def __init__(self):
|
|
self.field_values: Dict[str, int] = {}
|
|
self.field_to_buffer: Dict[str, str] = {} # Maps BAS1 -> PMCR, etc.
|
|
self.context_method: str = "_CRS" # Default to _CRS method name
|
|
|
|
def extract_crs_fields(self, aml_binary: bytes) -> Dict[str, int]:
|
|
"""
|
|
Scan for Store opcode patterns targeting BAS/LEN/LIM field assignments.
|
|
Also maps field names to their source buffer names via CreateDWordField.
|
|
"""
|
|
try:
|
|
# First pass: build field-to-buffer mapping from CreateDWordField
|
|
self._map_fields_to_buffers(aml_binary)
|
|
|
|
# Second pass: scan for Store operations
|
|
i = 0
|
|
iterations = 0
|
|
max_iter_limit = 1000000 # Hard stop after 1M iterations
|
|
|
|
while i < len(aml_binary) - 5 and iterations < max_iter_limit:
|
|
iterations += 1
|
|
|
|
if aml_binary[i] == self.OPCODE_STORE:
|
|
try:
|
|
# Decode value following Store opcode
|
|
value, val_len = self._decode_simple_int(aml_binary, i + 1)
|
|
|
|
if value is not None and 0 < val_len < 10:
|
|
# Check for valid name at target position
|
|
name_pos = i + 1 + val_len
|
|
if name_pos + 4 <= len(aml_binary):
|
|
# Extract 4-byte name
|
|
name_bytes = aml_binary[name_pos:name_pos+4]
|
|
|
|
# Check if bytes are ASCII-like
|
|
try:
|
|
name = name_bytes.decode('ascii')
|
|
# Keep only BAS, LEN, LIM patterns
|
|
if any(name.startswith(p) for p in ['BAS', 'LEN', 'LIM']):
|
|
clean_name = name.rstrip('\x00')
|
|
if clean_name and len(clean_name) >= 4:
|
|
self.field_values[clean_name] = value
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
i += 1
|
|
except:
|
|
pass
|
|
|
|
return self.field_values
|
|
|
|
def _map_fields_to_buffers(self, aml_binary: bytes) -> None:
|
|
"""Build mapping of field names to buffer names via CreateDWordField opcodes."""
|
|
try:
|
|
i = 0
|
|
while i < len(aml_binary) - 10:
|
|
if aml_binary[i] == self.OPCODE_CREATE_DWORD_FIELD:
|
|
try:
|
|
# After CreateDWordField opcode comes:
|
|
# SourceBuffer (Name), ByteIndex (Integer), FieldName (Name)
|
|
|
|
# Try to extract buffer name (usually 4 bytes)
|
|
pos = i + 1
|
|
if pos + 4 <= len(aml_binary):
|
|
# Extract potential buffer name (4 bytes)
|
|
buf_bytes = aml_binary[pos:pos+4]
|
|
try:
|
|
buf_name = buf_bytes.decode('ascii').rstrip('\x00')
|
|
if buf_name and len(buf_name) >= 3 and buf_name[0] not in '\x00\x01\x02':
|
|
# Skip the buffer name and what follows (typically variable-length)
|
|
# Try to find field name (BAS, LEN, LIM) in nearby bytes
|
|
for j in range(pos, min(pos + 20, len(aml_binary) - 4)):
|
|
field_bytes = aml_binary[j:j+4]
|
|
try:
|
|
field_name = field_bytes.decode('ascii').rstrip('\x00')
|
|
if field_name and any(field_name.startswith(p) for p in ['BAS', 'LEN', 'LIM']):
|
|
# Found a field name, map it to buffer
|
|
self.field_to_buffer[field_name] = buf_name
|
|
break
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
i += 1
|
|
except:
|
|
pass
|
|
|
|
def _find_crs_method_context(self, aml_binary: bytes) -> None:
|
|
"""Scan for _CRS method name for context."""
|
|
try:
|
|
# Look for pattern: Method opcode (0x14) followed by name string "_CRS"
|
|
i = 0
|
|
while i < len(aml_binary) - 10:
|
|
if aml_binary[i] == self.OPCODE_METHOD:
|
|
# After Method opcode comes name (typically "_CRS" = 0x5F 0x43 0x52 0x53)
|
|
if i + 5 < len(aml_binary):
|
|
potential_name = aml_binary[i+1:i+5]
|
|
if b'_CRS' in potential_name or b'_CRS' == potential_name:
|
|
self.context_method = "_CRS"
|
|
return
|
|
i += 1
|
|
except:
|
|
pass
|
|
|
|
def _decode_simple_int(self, data: bytes, offset: int) -> Tuple[Optional[int], int]:
|
|
"""Decode integer at offset."""
|
|
if offset >= len(data):
|
|
return None, 0
|
|
|
|
op = data[offset]
|
|
if op == 0x00:
|
|
return 0, 1
|
|
elif op == 0x01:
|
|
return 1, 1
|
|
elif op == 0x0A and offset + 2 <= len(data): # Byte
|
|
return data[offset + 1], 2
|
|
elif op == 0x0B and offset + 3 <= len(data): # Word
|
|
return struct.unpack('<H', data[offset + 1:offset + 3])[0], 3
|
|
elif op == 0x0C and offset + 5 <= len(data): # DWord
|
|
return struct.unpack('<I', data[offset + 1:offset + 5])[0], 5
|
|
elif op == 0x0E and offset + 9 <= len(data): # QWord
|
|
return struct.unpack('<Q', data[offset + 1:offset + 9])[0], 9
|
|
elif 0x02 <= op <= 0x09:
|
|
return op, 1
|
|
|
|
return None, 0
|
|
|
|
def _decode_simple_name(self, data: bytes, offset: int) -> Tuple[Optional[str], int]:
|
|
"""Decode a simple 4-byte name segment."""
|
|
if offset + 4 > len(data):
|
|
return None, 0
|
|
|
|
seg = data[offset:offset+4]
|
|
# Check if all bytes are valid name chars
|
|
for b in seg:
|
|
if not ((ord('A') <= b <= ord('Z')) or (ord('a') <= b <= ord('z')) or
|
|
(ord('0') <= b <= ord('9')) or b == ord('_')):
|
|
return None, 0
|
|
|
|
name = seg.decode('ascii', errors='ignore').rstrip('_')
|
|
return name if name else None, 4
|
|
|
|
|
|
class CRSResourceParser:
|
|
"""
|
|
Extracts and decodes _CRS method resource descriptors from AML binary.
|
|
Handles both static _CRS methods with direct Buffer returns and dynamic field assignments.
|
|
"""
|
|
|
|
@staticmethod
|
|
def parse_pkg_length(aml: bytes, pos: int) -> Tuple[int, int]:
|
|
"""
|
|
Parse AML package length (variable-size encoding).
|
|
|
|
Returns:
|
|
(length, new_pos)
|
|
"""
|
|
if pos >= len(aml):
|
|
return 0, pos
|
|
|
|
lead = aml[pos]
|
|
pos += 1
|
|
|
|
if lead < 0x40:
|
|
return lead, pos
|
|
|
|
byte_count = (lead >> 6) & 0x03
|
|
length = lead & 0x0F
|
|
|
|
for i in range(byte_count):
|
|
if pos >= len(aml):
|
|
return length, pos
|
|
length |= (aml[pos] << (4 + 8 * i))
|
|
pos += 1
|
|
|
|
return length, pos
|
|
|
|
@staticmethod
|
|
def extract_crs_buffer(aml_bytes: bytes, target_name: bytes = b'_CRS') -> Optional[bytes]:
|
|
"""
|
|
Walk AML binary to find _CRS method and extract its Buffer if simple Return(Buffer).
|
|
|
|
Args:
|
|
aml_bytes: Raw AML binary content
|
|
target_name: Method name to search for (e.g., b'_CRS')
|
|
|
|
Returns:
|
|
Raw buffer bytes if found and parseable, None otherwise
|
|
"""
|
|
pos = 0
|
|
while pos < len(aml_bytes):
|
|
try:
|
|
opcode = aml_bytes[pos]
|
|
|
|
if opcode == 0x14: # MethodOp
|
|
pos += 1
|
|
pkg_len, pos = CRSResourceParser.parse_pkg_length(aml_bytes, pos)
|
|
end_pos = pos + pkg_len
|
|
|
|
# NameString (4 bytes for simple name, or 0x00 0x00 for multi-segment)
|
|
if pos + 4 > len(aml_bytes):
|
|
pos = end_pos
|
|
continue
|
|
|
|
name = aml_bytes[pos:pos+4]
|
|
pos += 4
|
|
|
|
# Check if this is our target _CRS method
|
|
is_match = False
|
|
if target_name == b'_CRS' and name[:4] == b'_CRS':
|
|
is_match = True
|
|
elif name == target_name + b'\x00' * (4 - len(target_name)):
|
|
is_match = True
|
|
|
|
if is_match:
|
|
# Skip flags byte
|
|
if pos >= len(aml_bytes):
|
|
pos = end_pos
|
|
continue
|
|
pos += 1
|
|
|
|
# Look for ReturnOp (0xA4)
|
|
while pos < end_pos and pos < len(aml_bytes):
|
|
if aml_bytes[pos] == 0xA4: # ReturnOp
|
|
pos += 1
|
|
|
|
# Check for BufferOp (0x11)
|
|
if pos >= len(aml_bytes):
|
|
break
|
|
if aml_bytes[pos] == 0x11:
|
|
pos += 1
|
|
buf_pkg_len, pos = CRSResourceParser.parse_pkg_length(aml_bytes, pos)
|
|
|
|
# Parse BufferSize (assume simple ByteConst/WordConst)
|
|
if pos >= len(aml_bytes):
|
|
break
|
|
|
|
if aml_bytes[pos] == 0x0A: # ByteConst
|
|
pos += 1
|
|
if pos >= len(aml_bytes):
|
|
break
|
|
buf_size = aml_bytes[pos]
|
|
pos += 1
|
|
elif aml_bytes[pos] == 0x0B: # WordConst
|
|
pos += 1
|
|
if pos + 2 > len(aml_bytes):
|
|
break
|
|
buf_size = struct.unpack('<H', aml_bytes[pos:pos+2])[0]
|
|
pos += 2
|
|
elif aml_bytes[pos] == 0x0C: # DWordConst
|
|
pos += 1
|
|
if pos + 4 > len(aml_bytes):
|
|
break
|
|
buf_size = struct.unpack('<I', aml_bytes[pos:pos+4])[0]
|
|
pos += 4
|
|
else:
|
|
break
|
|
|
|
# Extract ByteList
|
|
if pos + buf_size > len(aml_bytes):
|
|
buf_size = len(aml_bytes) - pos
|
|
|
|
buffer_data = aml_bytes[pos:pos + buf_size]
|
|
return buffer_data
|
|
break
|
|
pos += 1
|
|
|
|
pos = end_pos
|
|
else:
|
|
pos += 1
|
|
except Exception:
|
|
pos += 1
|
|
|
|
return None
|
|
|
|
@staticmethod
|
|
def decode_crs_buffer(buffer_bytes: bytes) -> List[Dict]:
|
|
"""
|
|
Decode resource descriptors from _CRS Buffer bytes.
|
|
|
|
Args:
|
|
buffer_bytes: Raw resource descriptor buffer
|
|
|
|
Returns:
|
|
List of dicts: {type, description, address, size} for memory/IO resources
|
|
"""
|
|
pos = 0
|
|
resources = []
|
|
|
|
while pos < len(buffer_bytes):
|
|
if pos >= len(buffer_bytes):
|
|
break
|
|
|
|
tag = buffer_bytes[pos]
|
|
|
|
# Check for End Tag (0x79)
|
|
if tag == 0x79:
|
|
if pos + 1 < len(buffer_bytes):
|
|
checksum = buffer_bytes[pos + 1]
|
|
# Optional: verify checksum
|
|
break
|
|
|
|
try:
|
|
if tag & 0x80 == 0: # Small descriptor
|
|
item_type = (tag >> 3) & 0x0F
|
|
length = tag & 0x07
|
|
data = buffer_bytes[pos + 1:pos + 1 + length]
|
|
pos += 1 + length
|
|
else: # Large descriptor
|
|
item_type = tag
|
|
if pos + 3 > len(buffer_bytes):
|
|
break
|
|
length = struct.unpack_from('<H', buffer_bytes, pos + 1)[0]
|
|
data = buffer_bytes[pos + 3:pos + 3 + length]
|
|
pos += 3 + length
|
|
|
|
# Decode specific descriptor type
|
|
res_dict = CRSResourceParser._decode_descriptor(item_type, data)
|
|
if res_dict:
|
|
resources.append(res_dict)
|
|
except Exception:
|
|
pos += 1
|
|
|
|
return resources
|
|
|
|
@staticmethod
|
|
def _decode_descriptor(item_type: int, data: bytes) -> Optional[Dict]:
|
|
"""Decode a single resource descriptor and extract address/size for memory resources."""
|
|
|
|
try:
|
|
# 0x84: 32-bit Memory Range (large)
|
|
if item_type == 0x84:
|
|
if len(data) >= 9:
|
|
flags = data[0]
|
|
min_addr = struct.unpack('<I', data[1:5])[0]
|
|
max_addr = struct.unpack('<I', data[5:9])[0]
|
|
alignment = struct.unpack('<I', data[9:13])[0] if len(data) >= 13 else 0
|
|
length = struct.unpack('<I', data[13:17])[0] if len(data) >= 17 else 0
|
|
|
|
return {
|
|
'type': 'Memory32',
|
|
'address': min_addr,
|
|
'size': length if length > 0 else (max_addr - min_addr + 1),
|
|
'flags': flags,
|
|
'description': f'Memory32: base=0x{min_addr:08x}, size=0x{length:08x}'
|
|
}
|
|
|
|
# 0x85: 32-bit Fixed Memory (large)
|
|
elif item_type == 0x85:
|
|
if len(data) >= 9:
|
|
flags = data[0]
|
|
base = struct.unpack('<I', data[1:5])[0]
|
|
length = struct.unpack('<I', data[5:9])[0]
|
|
|
|
return {
|
|
'type': 'FixedMemory32',
|
|
'address': base,
|
|
'size': length,
|
|
'flags': flags,
|
|
'description': f'FixedMemory32: base=0x{base:08x}, size=0x{length:08x}'
|
|
}
|
|
|
|
# 0x87: DWord Address Space (large) - Memory or I/O
|
|
elif item_type == 0x87:
|
|
if len(data) >= 23:
|
|
res_type = data[0]
|
|
gen_flags = data[1]
|
|
spec_flags = data[2]
|
|
min_addr = struct.unpack('<I', data[4:8])[0]
|
|
max_addr = struct.unpack('<I', data[8:12])[0]
|
|
gran = struct.unpack('<I', data[12:16])[0]
|
|
trans = struct.unpack('<I', data[16:20])[0]
|
|
length = struct.unpack('<I', data[20:24])[0]
|
|
|
|
type_names = {0: 'Memory', 1: 'I/O', 2: 'BusNumber', 3: 'Reserved'}
|
|
type_str = type_names.get(res_type, 'Unknown')
|
|
|
|
if res_type == 0: # Memory
|
|
return {
|
|
'type': f'DWord{type_str}',
|
|
'address': min_addr,
|
|
'size': length if length > 0 else (max_addr - min_addr + 1),
|
|
'flags': gen_flags,
|
|
'description': f'DWord Memory: base=0x{min_addr:08x}, size=0x{length:08x}'
|
|
}
|
|
elif res_type == 1: # I/O
|
|
return {
|
|
'type': f'DWord{type_str}',
|
|
'address': min_addr,
|
|
'size': length if length > 0 else (max_addr - min_addr + 1),
|
|
'flags': gen_flags,
|
|
'description': f'DWord I/O: base=0x{min_addr:04x}, size=0x{length:04x}'
|
|
}
|
|
|
|
# 0x8A: QWord Address Space (large)
|
|
elif item_type == 0x8A:
|
|
if len(data) >= 47:
|
|
res_type = data[0]
|
|
gen_flags = data[1]
|
|
spec_flags = data[2]
|
|
min_addr = struct.unpack('<Q', data[4:12])[0]
|
|
max_addr = struct.unpack('<Q', data[12:20])[0]
|
|
gran = struct.unpack('<Q', data[20:28])[0]
|
|
trans = struct.unpack('<Q', data[28:36])[0]
|
|
length = struct.unpack('<Q', data[36:44])[0]
|
|
|
|
type_names = {0: 'Memory', 1: 'I/O', 2: 'BusNumber', 3: 'Reserved'}
|
|
type_str = type_names.get(res_type, 'Unknown')
|
|
|
|
if res_type == 0: # Memory
|
|
return {
|
|
'type': f'QWord{type_str}',
|
|
'address': min_addr,
|
|
'size': length if length > 0 else (max_addr - min_addr + 1),
|
|
'flags': gen_flags,
|
|
'description': f'QWord Memory: base=0x{min_addr:016x}, size=0x{length:016x}'
|
|
}
|
|
|
|
# 0x04: IRQ (small) - included for completeness
|
|
elif item_type == 0x04:
|
|
if len(data) >= 2:
|
|
flags = data[0]
|
|
irq_mask = struct.unpack('<H', data[1:3])[0] if len(data) >= 3 else data[1]
|
|
return {
|
|
'type': 'IRQ',
|
|
'description': f'IRQ: mask=0x{irq_mask:04x}'
|
|
}
|
|
|
|
# 0x05: DMA (small)
|
|
elif item_type == 0x05:
|
|
if len(data) >= 2:
|
|
flags = data[0]
|
|
chan_mask = data[1]
|
|
return {
|
|
'type': 'DMA',
|
|
'description': f'DMA: channels=0x{chan_mask:02x}'
|
|
}
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def parse_operation_regions(tables: List[bytes]) -> List[Dict]:
|
|
"""
|
|
Parse DSDT and SSDTs for all OperationRegion definitions.
|
|
|
|
Args:
|
|
tables: List of raw ACPI table binaries (header + AML content)
|
|
|
|
Returns:
|
|
List of dicts: {name, space_type, space_type_name, base, length}
|
|
"""
|
|
parser = AMLParser()
|
|
regions = parser.parse(tables)
|
|
|
|
return [
|
|
{
|
|
'name': r.name,
|
|
'space_type': r.space_type,
|
|
'space_type_name': r.space_type_name,
|
|
'base': r.base,
|
|
'length': r.length,
|
|
'source': r.source
|
|
}
|
|
for r in regions
|
|
]
|