Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Python build artifacts
__pycache__/
*.egg-info/

# Test output directories
test_output/
test_output_boot_v2/
.pytest_cache/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Android 15 Firmware and Recovery Tool

A command-line tool for working with Android 15 firmware and recovery images.

## Features

* **Search:** Scan a file for Android-specific magic signatures (`boot.img`, `super.img`, etc.).
* **Extract:** Unpack sparse images, EROFS filesystems, and boot/recovery images.
* **Repack:** Re-create a `boot.img` or `recovery.img` from its components.
* **DTC:** Decompile and compile Device Tree Blobs (.dtb/.dts).
* **Dump:** Dump partitions from a rooted Android device using `adb`.

## Usage

### Search
```bash
python3 -m android_15_tool search <file>
```

### Extract
```bash
python3 -m android_15_tool extract <file> <output_dir>
```
**Note:** The `super` partition unpacking is not yet implemented.

### Repack
```bash
python3 -m android_15_tool repack --header_info <header_info.txt> --kernel <kernel> --ramdisk <ramdisk> --output <new_image.img>
```

### DTC
```bash
python3 -m android_15_tool dtc decompile <input.dtb> <output.dts>
python3 -m android_15_tool dtc compile <input.dts> <output.dtb>
```

### Dump
```bash
python3 -m android_15_tool dump <partition_name> <output_dir>
```
2 changes: 2 additions & 0 deletions android_15_tool/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
48 changes: 48 additions & 0 deletions android_15_tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Android 15 Firmware and Recovery Tool

This is a command-line tool for extracting and repacking Android 15 firmware and recovery images. It is designed to be a low-level binary extraction tool that can handle the complexities of modern Android firmware.

## Features

* **Image Identification:** The tool can identify various Android image types, including:
* Android Sparse Images (`system.img`, `vendor.img`, etc.)
* Super Partitions (`super.img`)
* EROFS Filesystems
* OTA Payloads (`payload.bin`)
* Boot and Recovery Images (`boot.img`, `recovery.img`)
* Device Tree Blobs (DTBs)
* **Firmware Extraction:** The tool can extract the contents of these images, including:
* Un-sparsing sparse images to raw images.
* Extracting EROFS filesystems.
* Unpacking boot and recovery images into their components (kernel, ramdisk, DTB).
* **Recovery and DTB Handling:** The tool can decompile and recompile Device Tree Blobs, which is essential for modifying and rebuilding custom recovery images.
* **Repacking:** The tool can repack boot and recovery images, preserving the original header information to ensure that the repacked image is a drop-in replacement.

## Installation

To install the tool, clone this repository and install it in editable mode:

```bash
git clone <repository-url>
cd android_15_tool
pip install -e .
```

## Usage

The tool is used via the `android-15-tool` command-line interface. The following commands are available:

* `search`: Search for magic signatures in a file.
* `extract`: Extract a firmware or recovery image.
* `repack`: Repack a boot/recovery image.
* `dtc`: Decompile or recompile a Device Tree Blob.

For more detailed information on each command, use the `--help` flag. For example:

```bash
android-15-tool extract --help
```

## Disclaimer

This tool is designed for advanced users who are familiar with the Android build system and firmware structure. Modifying and flashing firmware can be a risky process, and this tool is provided as-is with no warranty. Always be sure to back up your data before making any changes to your device.
Empty file added android_15_tool/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions android_15_tool/android_15_tool.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Metadata-Version: 2.4
Name: android-15-tool
Version: 0.1.0
Summary: A tool for extracting and repacking Android 15 firmware.
Author-email: Jules <jules@example.com>
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
17 changes: 17 additions & 0 deletions android_15_tool/android_15_tool.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pyproject.toml
android_15_tool.egg-info/PKG-INFO
android_15_tool.egg-info/SOURCES.txt
android_15_tool.egg-info/dependency_links.txt
android_15_tool.egg-info/entry_points.txt
android_15_tool.egg-info/top_level.txt
lib/__init__.py
lib/boot_image.py
lib/dtc_handler.py
lib/erofs_parser.py
lib/repacker.py
lib/scanner.py
lib/super_unpacker.py
lib/unsparse.py
tests/test_integration.py
tests/test_scanner.py
tests/test_unsparse.py
Empty file.
2 changes: 2 additions & 0 deletions android_15_tool/android_15_tool.egg-info/entry_points.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[console_scripts]
android-15-tool = android_15_tool.main:main
1 change: 1 addition & 0 deletions android_15_tool/android_15_tool.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
79 changes: 79 additions & 0 deletions android_15_tool/device_dumper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Module for interacting with a rooted Android device to dump partitions.
"""
import subprocess
import logging
import os

logging.basicConfig(level=logging.INFO)

def run_adb_command(command):
"""Runs an ADB command and returns its output."""
try:
logging.info(f"Running command: {' '.join(command)}")
result = subprocess.run(command, capture_output=True, text=True, check=True)
if result.stderr:
logging.warning(f"STDERR: {result.stderr.strip()}")
return result.stdout.strip()
except FileNotFoundError:
logging.error("`adb` command not found. Is it installed and in your PATH?")
raise
except subprocess.CalledProcessError as e:
logging.error(f"Command failed: {' '.join(e.cmd)}")
logging.error(f"Exit Code: {e.returncode}")
if e.stdout:
logging.error(f"STDOUT: {e.stdout.strip()}")
if e.stderr:
logging.error(f"STDERR: {e.stderr.strip()}")
raise
except Exception as e:
logging.error(f"An unexpected error occurred: {e}")
raise

def dump_partition(partition_name: str, output_dir: str):
"""
Dumps a partition from the device to a local file.

Args:
partition_name: The name of the partition to dump (e.g., "boot").
output_dir: The local directory to save the dumped image to.
"""
device_tmp_path = f"/data/local/tmp/{partition_name}.img"
local_path = os.path.join(output_dir, f"{partition_name}.img")

os.makedirs(output_dir, exist_ok=True)

logging.info(f"Starting dump for '{partition_name}' partition...")

# 1. Dump partition to temporary location on device using dd
dd_command = [
"adb", "shell", "su", "-c",
f"\"dd if=/dev/block/by-name/{partition_name} of={device_tmp_path}\""
]
try:
run_adb_command(dd_command)
logging.info(f"Successfully dumped '{partition_name}' to '{device_tmp_path}' on device.")
except subprocess.CalledProcessError:
logging.error(f"Failed to dump partition '{partition_name}'. Does it exist? Do you have root?")
return

# 2. Pull the dumped image from device
pull_command = ["adb", "pull", device_tmp_path, local_path]
try:
run_adb_command(pull_command)
logging.info(f"Successfully pulled image to '{local_path}'.")
except subprocess.CalledProcessError:
logging.error(f"Failed to pull '{device_tmp_path}' from the device.")
# Attempt to clean up even if pull fails
run_adb_command(["adb", "shell", "su", "-c", f"\"rm {device_tmp_path}\""])
return

# 3. Clean up the temporary file on device
rm_command = ["adb", "shell", "su", "-c", f"\"rm {device_tmp_path}\""]
try:
run_adb_command(rm_command)
logging.info(f"Successfully cleaned up temporary file on device.")
except subprocess.CalledProcessError:
logging.warning(f"Failed to clean up '{device_tmp_path}' on the device. Manual cleanup may be required.")

logging.info(f"Partition dump complete for '{partition_name}'.")
Empty file added android_15_tool/lib/__init__.py
Empty file.
117 changes: 117 additions & 0 deletions android_15_tool/lib/boot_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import struct
import os

def _get_padded_size(size, page_size):
"""Calculates the size padded to the page size."""
return (size + page_size - 1) // page_size * page_size

class BootImage:
"""
Parses boot.img and recovery.img files (v3/v4).
"""

BOOT_MAGIC = b'ANDROID!'
BOOT_ARGS_SIZE = 512
BOOT_EXTRA_ARGS_SIZE = 1024
CMDLINE_SIZE = BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE

def __init__(self, filepath, page_size=4096):
self.filepath = filepath
self.page_size = page_size
self.header = None
self.kernel = None
self.ramdisk = None
self.dtb = None

def _parse_header(self, f):
"""
Parses the boot image header (v3/v4).
"""
f.seek(0)
magic = f.read(len(self.BOOT_MAGIC))
if magic != self.BOOT_MAGIC:
raise ValueError("Invalid boot image: incorrect magic.")

# Read the main part of the header (up to header_version)
header_v3_v4_bin = f.read(36)
if len(header_v3_v4_bin) < 36:
raise ValueError("Invalid boot image header: too short.")

header_data = struct.unpack('<9I', header_v3_v4_bin)

self.header = {
'kernel_size': header_data[0],
'ramdisk_size': header_data[1],
'os_version': header_data[2],
'header_size': header_data[3],
'header_version': header_data[8],
'dtb_size': 0,
}

if self.header['header_version'] >= 4:
# For v4, the dtb_size is after the cmdline
f.seek(len(self.BOOT_MAGIC) + 36 + self.CMDLINE_SIZE)
dtb_size_bin = f.read(4)
if len(dtb_size_bin) < 4:
raise ValueError("Could not read dtb_size for v4 header.")
self.header['dtb_size'] = struct.unpack('<I', dtb_size_bin)[0]

# Calculate padded sizes
padded_kernel_size = _get_padded_size(self.header['kernel_size'], self.page_size)
padded_ramdisk_size = _get_padded_size(self.header['ramdisk_size'], self.page_size)

# Calculate offsets
kernel_offset = self.page_size
ramdisk_offset = kernel_offset + padded_kernel_size
dtb_offset = ramdisk_offset + padded_ramdisk_size

# Read kernel
f.seek(kernel_offset)
self.kernel = f.read(self.header['kernel_size'])

# Read ramdisk
f.seek(ramdisk_offset)
self.ramdisk = f.read(self.header['ramdisk_size'])

# Read DTB if it exists
if self.header['header_version'] >= 4 and self.header['dtb_size'] > 0:
f.seek(dtb_offset)
self.dtb = f.read(self.header['dtb_size'])

# For Android 15+, os_version might be in AVB footer
if self.header['os_version'] == 0:
# Placeholder for AVB footer parsing logic
# This would involve finding and parsing the AVB metadata
self.header['avb_os_version'] = "parsed_from_avb"
self.header['avb_security_patch'] = "parsed_from_avb"


def unpack(self, output_dir):
"""
Extracts the kernel, ramdisk, and DTB to the output directory.
"""
try:
with open(self.filepath, 'rb') as f:
self._parse_header(f)

if self.kernel:
with open(os.path.join(output_dir, 'kernel'), 'wb') as f:
f.write(self.kernel)
if self.ramdisk:
with open(os.path.join(output_dir, 'ramdisk'), 'wb') as f:
f.write(self.ramdisk)
if self.dtb:
with open(os.path.join(output_dir, 'dtb'), 'wb') as f:
f.write(self.dtb)

# Save header info for repacking
with open(os.path.join(output_dir, 'header_info.txt'), 'w') as f:
for key, value in self.header.items():
f.write(f"{key}:{value}\n")

except (ValueError, struct.error) as e:
raise RuntimeError(f"Error processing boot image: {e}")
except FileNotFoundError:
raise RuntimeError(f"Input file not found: {self.filepath}")
except IOError as e:
raise RuntimeError(f"I/O error: {e}")
53 changes: 53 additions & 0 deletions android_15_tool/lib/dtc_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import subprocess
import shutil

class DtcHandler:
"""
A wrapper for the dtc (Device Tree Compiler) tool.
"""

def __init__(self):
self._check_for_dtc()

def _check_for_dtc(self):
"""
Checks if dtc is installed and in the system's PATH.
"""
if not shutil.which("dtc"):
raise EnvironmentError(
"dtc (Device Tree Compiler) is not installed or not in the "
"system's PATH. Please install it to continue."
)

def decompile(self, dtb_path, dts_path):
"""
Decompiles a Device Tree Blob (.dtb) to a Device Tree Source (.dts) file.
"""
try:
subprocess.run(
["dtc", "-I", "dtb", "-O", "dts", "-o", dts_path, dtb_path],
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Error decompiling DTB: {e.stderr}")
except FileNotFoundError:
raise RuntimeError("dtc command not found.")

def compile(self, dts_path, dtb_path):
"""
Compiles a Device Tree Source (.dts) file to a Device Tree Blob (.dtb).
"""
try:
# The -@ flag is important for Android 15 overlays
subprocess.run(
["dtc", "-@", "-I", "dts", "-O", "dtb", "-o", dtb_path, dts_path],
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Error compiling DTS: {e.stderr}")
except FileNotFoundError:
raise RuntimeError("dtc command not found.")
Loading