283 lines
10 KiB
Python
283 lines
10 KiB
Python
|
#!/usr/bin/python3
|
||
|
|
||
|
# This file is part of Extractor.
|
||
|
|
||
|
# Copyright (C) 2021 Security Research Labs GmbH
|
||
|
# SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
|
||
|
from typing import BinaryIO, List, Dict, Union
|
||
|
import os
|
||
|
import copy
|
||
|
import sys
|
||
|
import mmap
|
||
|
import hashlib
|
||
|
from construct import Struct, Int32ul, Int16ul, Int64ul, Bytes
|
||
|
from construct_typing import TypedContainer
|
||
|
|
||
|
|
||
|
def main():
|
||
|
super_img = SuperImage(sys.argv[1])
|
||
|
print("PARTITIONS: %r" % super_img.get_partition_names())
|
||
|
for partition_name in super_img.get_partition_names():
|
||
|
if partition_name in ("system", "vendor", "product"):
|
||
|
with open("%s.img" % partition_name, 'wb') as f:
|
||
|
super_img.write_partition(partition_name, f)
|
||
|
|
||
|
|
||
|
def check_magic(f: BinaryIO):
|
||
|
buf = f.read(4096)
|
||
|
if buf != b'\0' * 4096:
|
||
|
return False
|
||
|
buf = f.read(4)
|
||
|
return buf == b'\x67\x44\x6c\x61'
|
||
|
|
||
|
|
||
|
class SuperImage:
|
||
|
"""
|
||
|
Class to parse a liblp super image.
|
||
|
More details at
|
||
|
https://android.googlesource.com/platform/system/core
|
||
|
=> fs_mgr/liblp/include/liblp/metadata_format.h
|
||
|
"""
|
||
|
filename: str
|
||
|
file_size: int
|
||
|
fh: BinaryIO
|
||
|
mmap: mmap.mmap
|
||
|
geometry: "LpMetadataGeometry"
|
||
|
metadata_header: "LpMetadataHeader"
|
||
|
partitions: List["LpMetadataPartition"]
|
||
|
extents: List["LpMetadataExtent"]
|
||
|
block_device: "LpMetadataBlockDevice"
|
||
|
partition_name_to_nr: Dict[str, int]
|
||
|
|
||
|
def __init__(self, filename: Union[str, bytes]):
|
||
|
self.filename = filename
|
||
|
self.file_size = os.stat(self.filename).st_size
|
||
|
self.fh = open(self.filename, 'rb')
|
||
|
self.mmap = mmap.mmap(self.fh.fileno(), 0, access=mmap.ACCESS_READ)
|
||
|
# Read and validate LpMetadataGeometry
|
||
|
lmg = LpMetadataGeometry.parse(self.mmap[0x1000:0x1000 + LpMetadataGeometry.sizeof()])
|
||
|
lmg.validate()
|
||
|
lmg_copy = LpMetadataGeometry.parse(self.mmap[0x2000:0x2000 + LpMetadataGeometry.sizeof()])
|
||
|
lmg_copy.validate()
|
||
|
assert lmg == lmg_copy
|
||
|
# Read and validate
|
||
|
self.metadata_header = LpMetadataHeader.parse(self.mmap[0x3000:0x3000 + LpMetadataHeader.sizeof()])
|
||
|
self.metadata_header.validate()
|
||
|
table_data = self.mmap[0x3000 + self.metadata_header.header_size:0x3000 + self.metadata_header.header_size + self.metadata_header.tables_size]
|
||
|
self.metadata_header.validate_table_data(table_data)
|
||
|
# Read partitions from table_data
|
||
|
self.partition_name_to_nr = {}
|
||
|
self.partitions = []
|
||
|
for partition_nr in range(self.metadata_header.partitions.num_entries):
|
||
|
pos = self.metadata_header.partitions.offset + partition_nr * LpMetadataPartition.sizeof()
|
||
|
partition = LpMetadataPartition.parse(table_data[pos:pos + LpMetadataPartition.sizeof()])
|
||
|
print("partition %d: %r" % (partition_nr, partition))
|
||
|
self.partitions.append(partition)
|
||
|
assert partition.get_name() not in self.partition_name_to_nr, "Duplicate partition %r" % partition.get_name()
|
||
|
self.partition_name_to_nr[partition.get_name()] = partition_nr
|
||
|
# Read extents from table_data
|
||
|
self.extents = []
|
||
|
for extent_nr in range(self.metadata_header.extents.num_entries):
|
||
|
pos = self.metadata_header.extents.offset + extent_nr * LpMetadataExtent.sizeof()
|
||
|
extent = LpMetadataExtent.parse(table_data[pos:pos + LpMetadataExtent.sizeof()])
|
||
|
print("Extent %d: %r" % (extent_nr, extent))
|
||
|
self.extents.append(extent)
|
||
|
# Read block devices from table_data
|
||
|
assert self.metadata_header.block_devices.num_entries == 1, "Not exactly one block device: self.metadata_header.block_devices.num_entries=%r" % self.metadata_header.block_devices.num_entries
|
||
|
pos = self.metadata_header.block_devices.offset
|
||
|
self.block_device = LpMetadataBlockDevice.parse(table_data[pos:pos + LpMetadataBlockDevice.sizeof()])
|
||
|
assert self.block_device.alignment % 512 == 0
|
||
|
|
||
|
def get_partition_names(self) -> List[str]:
|
||
|
return [partition.get_name() for partition in self.partitions]
|
||
|
|
||
|
def close(self):
|
||
|
self.mmap.close()
|
||
|
self.fh.close()
|
||
|
|
||
|
def write_partition(self, partition_name: str, f: BinaryIO):
|
||
|
partition_nr = self.partition_name_to_nr[partition_name]
|
||
|
partition = self.partitions[partition_nr]
|
||
|
assert partition.num_extents == 1, "Not exactly one extent: %d" % partition.num_extents
|
||
|
extent = self.extents[partition.first_extent_index]
|
||
|
assert extent.target_source == 0
|
||
|
assert extent.target_type == 0 # LP_TARGET_TYPE_LINEAR
|
||
|
start_pos = extent.target_data * 512
|
||
|
assert start_pos % self.block_device.alignment == 0, "Alignment error: start_pos=%d self.block_device.alignment=%d offset=%r" % (start_pos, self.block_device.alignment, start_pos % self.block_device.alignment)
|
||
|
end_pos = start_pos + extent.num_sectors * 512
|
||
|
pos = start_pos
|
||
|
while pos < end_pos:
|
||
|
next_pos = min(end_pos, pos + 1024**2)
|
||
|
f.write(self.mmap[pos:next_pos])
|
||
|
pos = next_pos
|
||
|
|
||
|
|
||
|
class LpMetadataGeometry(TypedContainer):
|
||
|
magic: int
|
||
|
struct_size: int
|
||
|
checksum: bytes
|
||
|
metadata_max_size: int
|
||
|
metadata_slot_count: int
|
||
|
logical_block_size: int
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"magic" / Int32ul,
|
||
|
"struct_size" / Int32ul,
|
||
|
"checksum" / Bytes(32),
|
||
|
"metadata_max_size" / Int32ul,
|
||
|
"metadata_slot_count" / Int32ul,
|
||
|
"logical_block_size" / Int32ul,
|
||
|
)
|
||
|
|
||
|
def validate(self):
|
||
|
assert self.magic == 0x616c4467
|
||
|
assert self.struct_size == LpMetadataGeometry.sizeof()
|
||
|
tmp = copy.copy(self)
|
||
|
tmp.checksum = b'\0' * 32
|
||
|
tmp_encoded = tmp.build()
|
||
|
digest = hashlib.sha256(tmp_encoded).digest()
|
||
|
assert self.checksum == digest
|
||
|
|
||
|
|
||
|
assert LpMetadataGeometry.sizeof() == 52
|
||
|
|
||
|
|
||
|
class LpMetadataTableDescriptor(TypedContainer):
|
||
|
offset: int
|
||
|
num_entries: int
|
||
|
entry_size: int
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"offset" / Int32ul,
|
||
|
"num_entries" / Int32ul,
|
||
|
"entry_size" / Int32ul
|
||
|
)
|
||
|
|
||
|
|
||
|
assert LpMetadataTableDescriptor.sizeof() == 12
|
||
|
|
||
|
|
||
|
class LpMetadataHeader(TypedContainer):
|
||
|
magic: int
|
||
|
major_version: int
|
||
|
minor_version: int
|
||
|
header_size: int
|
||
|
header_checksum: bytes
|
||
|
tables_size: int
|
||
|
tables_checksum: bytes
|
||
|
partitions: LpMetadataTableDescriptor
|
||
|
extents: LpMetadataTableDescriptor
|
||
|
groups: LpMetadataTableDescriptor
|
||
|
block_devices: LpMetadataTableDescriptor
|
||
|
# flags: int
|
||
|
# reserved: bytes
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"magic" / Int32ul,
|
||
|
"major_version" / Int16ul,
|
||
|
"minor_version" / Int16ul,
|
||
|
"header_size" / Int32ul,
|
||
|
"header_checksum" / Bytes(32),
|
||
|
"tables_size" / Int32ul,
|
||
|
"tables_checksum" / Bytes(32),
|
||
|
"partitions" / LpMetadataTableDescriptor.as_inner_type(),
|
||
|
"extents" / LpMetadataTableDescriptor.as_inner_type(),
|
||
|
"groups" / LpMetadataTableDescriptor.as_inner_type(),
|
||
|
"block_devices" / LpMetadataTableDescriptor.as_inner_type()
|
||
|
)
|
||
|
|
||
|
def validate(self):
|
||
|
assert self.magic == 0x414C5030
|
||
|
assert self.header_size == LpMetadataHeader.sizeof(), "Bad LpMetadataHeader.header_size %d, should be %d" % (self.header_size, LpMetadataHeader.sizeof())
|
||
|
tmp = copy.copy(self)
|
||
|
tmp.header_checksum = b'\0' * 32
|
||
|
tmp_encoded = tmp.build()
|
||
|
digest = hashlib.sha256(tmp_encoded).digest()
|
||
|
assert self.header_checksum == digest
|
||
|
assert self.partitions.entry_size == LpMetadataPartition.sizeof(), "Bad LpMetadataHeader.partitions.entry_size %d, should be %d" % (self.partitions.entry_size, LpMetadataPartition.sizeof())
|
||
|
assert self.extents.entry_size == LpMetadataExtent.sizeof(), "Bad LpMetadataHeader.extents.entry_size %d, should be %d" % (self.extents.entry_size, LpMetadataExtent.sizeof())
|
||
|
assert self.tables_size < 1e6
|
||
|
|
||
|
def validate_table_data(self, buf: bytes):
|
||
|
assert len(buf) == self.tables_size
|
||
|
digest = hashlib.sha256(buf).digest()
|
||
|
assert self.tables_checksum == digest
|
||
|
|
||
|
|
||
|
class LpMetadataPartition(TypedContainer):
|
||
|
name: bytes
|
||
|
attributes: int
|
||
|
first_extent_index: int
|
||
|
num_extents: int
|
||
|
group_index: int
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"name" / Bytes(36),
|
||
|
"attributes" / Int32ul,
|
||
|
"first_extent_index" / Int32ul,
|
||
|
"num_extents" / Int32ul,
|
||
|
"group_index" / Int32ul
|
||
|
)
|
||
|
|
||
|
def get_name(self) -> str:
|
||
|
return self.name.rstrip(b'\0').decode()
|
||
|
|
||
|
|
||
|
assert LpMetadataPartition.sizeof() == 52
|
||
|
|
||
|
|
||
|
class LpMetadataExtent(TypedContainer):
|
||
|
num_sectors: int
|
||
|
target_type: int
|
||
|
target_data: int
|
||
|
target_source: int
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"num_sectors" / Int64ul,
|
||
|
"target_type" / Int32ul,
|
||
|
"target_data" / Int64ul,
|
||
|
"target_source" / Int32ul,
|
||
|
)
|
||
|
|
||
|
|
||
|
assert LpMetadataExtent.sizeof() == 24
|
||
|
|
||
|
|
||
|
class LpMetadataBlockDevice(TypedContainer):
|
||
|
first_logical_sector: int
|
||
|
alignment: int
|
||
|
alignment_offset: int
|
||
|
size: int
|
||
|
partition_name: bytes
|
||
|
flags: int
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
construct_struct = Struct(
|
||
|
"first_logical_sector" / Int64ul,
|
||
|
"alignment" / Int32ul,
|
||
|
"alignment_offset" / Int32ul,
|
||
|
"size" / Int64ul,
|
||
|
"partition_name" / Bytes(36),
|
||
|
"flags" / Int32ul
|
||
|
)
|
||
|
|
||
|
|
||
|
assert LpMetadataBlockDevice.sizeof() == 64
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|