Skip to content

Commit

Permalink
Import extract_android_ota_payload
Browse files Browse the repository at this point in the history
From cyxx/extract_android_ota_payload#8
Commit 254cbdc9825056fd6ac49aea5a4fc9bcefe0b3d0
"Support selective extraction of partitions"

Signed-off-by: spezi77 <[email protected]>
  • Loading branch information
chirayudesai authored and spezi77 committed Sep 18, 2019
1 parent a5e28e5 commit 940193a
Show file tree
Hide file tree
Showing 5 changed files with 828 additions and 0 deletions.
1 change: 1 addition & 0 deletions scripts/extract_android_ota_payload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.pyc
34 changes: 34 additions & 0 deletions scripts/extract_android_ota_payload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# extract_android_ota_payload.py

Extract Android firmware images from an OTA payload.bin file.

With the introduction of the A/B system update, the OTA file format changed.
This tool allows to extract and decompress the firmware images packed using the 'brillo' toolset.

Incremental firmware images are not supported (source_copy, source_bsdiff operations).

## Usage

```
$ extract_android_ota_payload.py <payload.bin> [target_dir] [partition.img]
<payload.bin> : file extracted from the OTA zip file or the OTA zip file
<target_dir> : output directory for the extracted file
<partition_img> : name of partition to be extracted
```

## Example

```
$ python extract_android_ota_payload.py marlin-ota-opm4.171019.021.d1-fd6998a5.zip /tmp/
Extracting 'boot.img'
Extracting 'system.img'
Extracting 'vendor.img'
...
Extracting 'modem.img'
```

## Dependencies

```
python-protobuf
```
161 changes: 161 additions & 0 deletions scripts/extract_android_ota_payload/extract_android_ota_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env python

import hashlib
import os
import os.path
import shutil
import struct
import subprocess
import sys
import zipfile

# protobufs compiled with protoc 2.5.0 are not compatible with python3
if sys.version_info[0] != 2:
raise Exception("Python 2.x is required")

# from https://android.googlesource.com/platform/system/update_engine/+/refs/heads/master/scripts/update_payload/
import update_metadata_pb2

PROGRAMS = [ 'bzcat', 'xzcat' ]

BRILLO_MAJOR_PAYLOAD_VERSION = 2

class PayloadError(Exception):
pass

class Payload(object):
class _PayloadHeader(object):
_MAGIC = 'CrAU'

def __init__(self):
self.version = None
self.manifest_len = None
self.metadata_signature_len = None
self.size = None

def ReadFromPayload(self, payload_file):
magic = payload_file.read(4)
if magic != self._MAGIC:
raise PayloadError('Invalid payload magic: %s' % magic)
self.version = struct.unpack('>Q', payload_file.read(8))[0]
self.manifest_len = struct.unpack('>Q', payload_file.read(8))[0]
self.size = 20
self.metadata_signature_len = 0
if self.version != BRILLO_MAJOR_PAYLOAD_VERSION:
raise PayloadError('Unsupported payload version (%d)' % self.version)
self.size += 4
self.metadata_signature_len = struct.unpack('>I', payload_file.read(4))[0]

def __init__(self, payload_file):
self.payload_file = payload_file
self.header = None
self.manifest = None
self.data_offset = None
self.metadata_signature = None
self.metadata_size = None

def _ReadManifest(self):
return self.payload_file.read(self.header.manifest_len)

def _ReadMetadataSignature(self):
self.payload_file.seek(self.header.size + self.header.manifest_len)
return self.payload_file.read(self.header.metadata_signature_len);

def ReadDataBlob(self, offset, length):
self.payload_file.seek(self.data_offset + offset)
return self.payload_file.read(length)

def Init(self):
self.header = self._PayloadHeader()
self.header.ReadFromPayload(self.payload_file)
manifest_raw = self._ReadManifest()
self.manifest = update_metadata_pb2.DeltaArchiveManifest()
self.manifest.ParseFromString(manifest_raw)
metadata_signature_raw = self._ReadMetadataSignature()
if metadata_signature_raw:
self.metadata_signature = update_metadata_pb2.Signatures()
self.metadata_signature.ParseFromString(metadata_signature_raw)
self.metadata_size = self.header.size + self.header.manifest_len
self.data_offset = self.metadata_size + self.header.metadata_signature_len

def decompress_payload(command, data, size, hash):
p = subprocess.Popen([command, '-'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
r = p.communicate(data)[0]
if len(r) != size:
print("Unexpected size %d %d" % (len(r), size))
elif hashlib.sha256(data).digest() != hash:
print("Hash mismatch")
return r

def parse_payload(payload_f, partition, out_f):
BLOCK_SIZE = 4096
for operation in partition.operations:
e = operation.dst_extents[0]
data = payload_f.ReadDataBlob(operation.data_offset, operation.data_length)
out_f.seek(e.start_block * BLOCK_SIZE)
if operation.type == update_metadata_pb2.InstallOperation.REPLACE:
out_f.write(data)
elif operation.type == update_metadata_pb2.InstallOperation.REPLACE_XZ:
r = decompress_payload('xzcat', data, e.num_blocks * BLOCK_SIZE, operation.data_sha256_hash)
out_f.write(r)
elif operation.type == update_metadata_pb2.InstallOperation.REPLACE_BZ:
r = decompress_payload('bzcat', data, e.num_blocks * BLOCK_SIZE, operation.data_sha256_hash)
out_f.write(r)
else:
raise PayloadError('Unhandled operation type (%d)' % operation.type)

def main(filename, output_dir, partition):
if filename.endswith('.zip'):
print("Extracting 'payload.bin' from OTA file...")
ota_zf = zipfile.ZipFile(filename)
payload_file = open(ota_zf.extract('payload.bin', output_dir))
else:
payload_file = file(filename)

payload = Payload(payload_file)
payload.Init()

for p in payload.manifest.partitions:
name = p.partition_name + '.img'
if (partition is not None and name != partition):
continue
print("Extracting '%s'" % name)
fname = os.path.join(output_dir, name)
out_f = open(fname, 'w')
try:
parse_payload(payload, p, out_f)
except PayloadError as e:
print('Failed: %s' % e)
out_f.close()
os.unlink(fname)

if __name__ == '__main__':
try:
filename = sys.argv[1]
except:
print('Usage: %s payload.bin [output_dir] [partition.img]' % sys.argv[0])
sys.exit()

try:
output_dir = sys.argv[2]
except IndexError:
output_dir = os.getcwd()

try:
partition = sys.argv[3]
except IndexError:
try:
partition = sys.argv[2]
except IndexError:
partition = None

if partition is not None and not partition.endswith('.img'):
partition = None

if output_dir.endswith('.img'):
output_dir = os.getcwd()

if not os.path.exists(output_dir):
os.makedirs(output_dir)

main(filename, output_dir, partition)
1 change: 1 addition & 0 deletions scripts/extract_android_ota_payload/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
protobuf>=2.5.0
Loading

0 comments on commit 940193a

Please sign in to comment.