UPLOAD
This commit is contained in:
parent
e01a8e65f2
commit
f57adf9c1b
@ -1,3 +1,3 @@
|
|||||||
# PlayReady
|
# PlayReady
|
||||||
|
|
||||||
PlayReady stuffs
|
PlayReady stuffs, forks and collections from PlayReady Discord server
|
||||||
|
|||||||
BIN
decbin-pr.zip
Normal file
BIN
decbin-pr.zip
Normal file
Binary file not shown.
464
decrypt_bgc_zgp_pak.py
Normal file
464
decrypt_bgc_zgp_pak.py
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from typing import Union, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
from zlib import crc32
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Hash import CMAC
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.keywrap import aes_key_unwrap
|
||||||
|
|
||||||
|
|
||||||
|
class Pak:
|
||||||
|
def __init__(self, bgroupcert_sizes: dict, zgpriv_sizes: dict):
|
||||||
|
self.mstar_key_bank_magic_id = b"Mstar.Key.Bank"
|
||||||
|
self.secure_store_file_magic_id = b"MSTAR_SECURE_STORE_FILE_MAGIC_ID"
|
||||||
|
self.bgroupcert_sizes = bgroupcert_sizes
|
||||||
|
self.zgpriv_sizes = zgpriv_sizes
|
||||||
|
|
||||||
|
def process_files(self, boot_file: Path, sedata_file: Path, playready_keys: dict) -> None:
|
||||||
|
self.validate_files(boot_file, sedata_file)
|
||||||
|
boot_key = self.get_boot_key(input_file=boot_file).hex()
|
||||||
|
|
||||||
|
boot_keys = {
|
||||||
|
"brand_unknown": {
|
||||||
|
"MODEL_UNKNOWN": boot_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decryption_bgc_zgp = DecryptionBgcZgp(
|
||||||
|
boot_keys=boot_keys,
|
||||||
|
playready_keys=playready_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
chunks = self.get_chunks_from_sedata(input_file=sedata_file)
|
||||||
|
|
||||||
|
for bgp_security_level, bgp_size in self.bgroupcert_sizes.items():
|
||||||
|
print(f"\nTrying get BGCert - Security Level: {bgp_security_level}")
|
||||||
|
for chunk in chunks:
|
||||||
|
data = self.prepare_data(chunk)
|
||||||
|
|
||||||
|
bgroupcert_data = self.decrypt_bgroupcert(decryption_bgc_zgp, data, bgp_size)
|
||||||
|
if not bgroupcert_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("Firmware:")
|
||||||
|
print(f" - Key : {decryption_bgc_zgp.boot_key.hex()}")
|
||||||
|
print(f" - Brand: {decryption_bgc_zgp.brand}")
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
data = self.prepare_data(chunk)
|
||||||
|
|
||||||
|
zgpriv_data = self.decrypt_zgpriv(decryption_bgc_zgp, data, self.zgpriv_sizes[bgp_security_level])
|
||||||
|
if not zgpriv_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
code = self.process_decrypted_data(decryption_bgc_zgp, bgroupcert_data, zgpriv_data)
|
||||||
|
if code == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_boot_key(self, input_file: Path, offset: int = 16) -> bytes:
|
||||||
|
data = DecryptionBgcZgp.get_file_data(input_file)
|
||||||
|
|
||||||
|
index = data.find(self.mstar_key_bank_magic_id)
|
||||||
|
if index == -1 or index < offset:
|
||||||
|
raise ValueError("Error: Boot key not found or invalid position")
|
||||||
|
|
||||||
|
return data[index - offset:index]
|
||||||
|
|
||||||
|
def get_chunks_from_sedata(self, input_file: Path, min_size: int = 0, max_size: int = 2000) -> list[dict]:
|
||||||
|
data = DecryptionBgcZgp.get_file_data(input_file)
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
start_pos = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
pos = data.find(self.secure_store_file_magic_id, start_pos)
|
||||||
|
if pos == -1:
|
||||||
|
break
|
||||||
|
|
||||||
|
header_start = max(0, pos - 32)
|
||||||
|
|
||||||
|
header = data[header_start:pos]
|
||||||
|
|
||||||
|
data_start = pos + len(self.secure_store_file_magic_id)
|
||||||
|
|
||||||
|
next_pos = data.find(self.secure_store_file_magic_id, data_start)
|
||||||
|
if next_pos == -1:
|
||||||
|
data_end = len(data)
|
||||||
|
else:
|
||||||
|
data_end = next_pos
|
||||||
|
|
||||||
|
extracted_data = data[data_start:data_end]
|
||||||
|
total_size = len(header) + len(self.secure_store_file_magic_id) + len(extracted_data)
|
||||||
|
|
||||||
|
if min_size <= total_size <= max_size:
|
||||||
|
chunks.append({
|
||||||
|
"position": header_start if len(header) == 32 else pos,
|
||||||
|
"header": header if len(header) == 32 else b"",
|
||||||
|
"has_full_header": len(header) == 32,
|
||||||
|
"data": extracted_data,
|
||||||
|
"total_size": total_size
|
||||||
|
})
|
||||||
|
|
||||||
|
start_pos = data_end
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def prepare_data(self, chunk: dict) -> bytes:
|
||||||
|
magic_id = self.secure_store_file_magic_id
|
||||||
|
header = chunk["header"] if chunk["has_full_header"] else b""
|
||||||
|
return header + magic_id + chunk["data"]
|
||||||
|
|
||||||
|
def process_decrypted_data(
|
||||||
|
self, decryption_bgc_zgp: "DecryptionBgcZgp", bgroupcert_data: bytes, zgpriv_data: bytes
|
||||||
|
) -> int:
|
||||||
|
bgroupcert_tmp_path = Path("bgroupcert.decrypt_bgc_zgp_pak.tmp")
|
||||||
|
zgpriv_tmp_path = Path("zgpriv_temp.decrypt_bgc_zgp_pak.tmp")
|
||||||
|
|
||||||
|
code = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
bgroupcert_tmp_path.write_bytes(bgroupcert_data)
|
||||||
|
zgpriv_tmp_path.write_bytes(zgpriv_data)
|
||||||
|
|
||||||
|
command = [
|
||||||
|
"pyplayready",
|
||||||
|
"create-device",
|
||||||
|
"-c", str(bgroupcert_tmp_path),
|
||||||
|
"-k", str(zgpriv_tmp_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
prd_path = Path(result.stderr.split("Saved to: ")[-1].strip())
|
||||||
|
folder_path = Path("playready_decryption_bgc_zgp_pak") / prd_path.stem
|
||||||
|
|
||||||
|
if folder_path.exists() and any(item.is_file() for item in folder_path.iterdir()):
|
||||||
|
raise ValueError(f"Error: This certificate already exists in {folder_path}")
|
||||||
|
|
||||||
|
folder_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
bgroupcert_out_path = folder_path / "bgroupcert.dat"
|
||||||
|
bgroupcert_out_path.write_bytes(bgroupcert_data)
|
||||||
|
|
||||||
|
zgpriv_out_path = folder_path / "zgpriv.dat"
|
||||||
|
zgpriv_out_path.write_bytes(zgpriv_data)
|
||||||
|
|
||||||
|
prd_path.rename(folder_path / prd_path.name)
|
||||||
|
|
||||||
|
security_level = decryption_bgc_zgp.get_security_level(bgroupcert_data)
|
||||||
|
|
||||||
|
print("Decryption:")
|
||||||
|
print(f" - BGroupCert Security level: {security_level}")
|
||||||
|
print(f" - BGroupCert Path : {bgroupcert_out_path}")
|
||||||
|
print(f" - ZGPriv Path : {zgpriv_out_path}")
|
||||||
|
print(f" - PRD Path : {folder_path / prd_path.name}")
|
||||||
|
print(f" - Files exported to : {folder_path.parent}")
|
||||||
|
code = result.returncode
|
||||||
|
finally:
|
||||||
|
bgroupcert_tmp_path.unlink(missing_ok=True)
|
||||||
|
zgpriv_tmp_path.unlink(missing_ok=True)
|
||||||
|
return code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_files(boot_file: Path, sedata_file: Path) -> None:
|
||||||
|
if not boot_file.exists():
|
||||||
|
raise ValueError(f"Error: Boot File not found -> {boot_file}")
|
||||||
|
if not sedata_file.exists():
|
||||||
|
raise ValueError(f"Error: SeData File not found -> {sedata_file}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_bgroupcert(decryption_bgc_zgp: "DecryptionBgcZgp", data: bytes, size: int):
|
||||||
|
try:
|
||||||
|
decrypted_bgroupcert_data = decryption_bgc_zgp.decrypt_aes_ecb(
|
||||||
|
inp=data,
|
||||||
|
size=size,
|
||||||
|
is_bgroupcert=True
|
||||||
|
)
|
||||||
|
return decryption_bgc_zgp.process_bgroupcert(data=decrypted_bgroupcert_data)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_zgpriv(decryption_bgc_zgp: "DecryptionBgcZgp", data: bytes, size: int):
|
||||||
|
try:
|
||||||
|
decrypted_zgpriv_data = decryption_bgc_zgp.decrypt_aes_ecb(
|
||||||
|
inp=data,
|
||||||
|
size=size
|
||||||
|
)
|
||||||
|
return decryption_bgc_zgp.process_zgpriv(data=decrypted_zgpriv_data, is_sl3000=False)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DecryptionBgcZgp:
|
||||||
|
def __init__(self, boot_keys: dict, playready_keys: dict):
|
||||||
|
self.boot_keys = boot_keys
|
||||||
|
self.playready_keys = playready_keys
|
||||||
|
self.boot_key: Optional[bytes] = None
|
||||||
|
self.brand: Optional[str] = None
|
||||||
|
self.bgroupcert_start_magic = b"CHAI"
|
||||||
|
self.bgroupcert_end_magic = b"\x93\xfa\xc5\xab"
|
||||||
|
|
||||||
|
def decrypt_aes_ecb(self,inp: Union[Path, bytes],size: Optional[int] = None, is_bgroupcert: bool = False) -> bytes:
|
||||||
|
if isinstance(inp, bytes):
|
||||||
|
encrypted_data = inp
|
||||||
|
else:
|
||||||
|
encrypted_data = self.get_file_data(inp)
|
||||||
|
|
||||||
|
for brand, keys in self.boot_keys.items():
|
||||||
|
for model, boot_key in keys.items():
|
||||||
|
if self.boot_key:
|
||||||
|
key = self.boot_key
|
||||||
|
else:
|
||||||
|
key = self.hex_to_bytes(boot_key)
|
||||||
|
|
||||||
|
cipher = AES.new(key, AES.MODE_ECB)
|
||||||
|
|
||||||
|
if size:
|
||||||
|
data = cipher.decrypt(encrypted_data[:size])
|
||||||
|
else:
|
||||||
|
data = cipher.decrypt(encrypted_data)
|
||||||
|
|
||||||
|
if is_bgroupcert and not self.bgroupcert_start_magic in data and not self.bgroupcert_end_magic in data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_bgroupcert:
|
||||||
|
self.boot_key = key
|
||||||
|
self.brand = brand
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"Error: Failed to decrypt the bgroupcert. No valid AES key matched or the decrypted data is invalid."
|
||||||
|
)
|
||||||
|
|
||||||
|
def decrypt_zgpriv(self, encrypted_data: bytes) -> bytes:
|
||||||
|
transient_key = self.hex_to_bytes(self.playready_keys["porting_kit"]["transient"])
|
||||||
|
cmac = CMAC.new(transient_key, ciphermod=AES)
|
||||||
|
|
||||||
|
intermediate_key = self.hex_to_bytes(self.playready_keys["porting_kit"]["intermediate"])
|
||||||
|
intermediate_data = (
|
||||||
|
b"\x01" +
|
||||||
|
intermediate_key + # Intermediate key
|
||||||
|
b"\x00" +
|
||||||
|
b"\x00" * 16 + # Context
|
||||||
|
b"\x00\x80"
|
||||||
|
)
|
||||||
|
cmac.update(intermediate_data)
|
||||||
|
|
||||||
|
wrapping_key = cmac.digest()
|
||||||
|
|
||||||
|
wrapped_key = encrypted_data[:48]
|
||||||
|
unwrapped_key = aes_key_unwrap(wrapping_key, wrapped_key)
|
||||||
|
|
||||||
|
return unwrapped_key
|
||||||
|
|
||||||
|
def process_zgpriv(self, data: bytes, is_sl3000: bool) -> bytes:
|
||||||
|
content = self.remove_header(data)
|
||||||
|
|
||||||
|
if is_sl3000:
|
||||||
|
content = self.decrypt_zgpriv(encrypted_data=content)
|
||||||
|
|
||||||
|
if self.brand == "lg" and len(data) == 128:
|
||||||
|
zgpriv_data = content[64:-32]
|
||||||
|
else:
|
||||||
|
zgpriv_data = content[:32]
|
||||||
|
|
||||||
|
return zgpriv_data
|
||||||
|
|
||||||
|
def process_bgroupcert(self, data: bytes) -> bytes:
|
||||||
|
content = self.remove_header(data)
|
||||||
|
|
||||||
|
start_index = content.find(self.bgroupcert_start_magic)
|
||||||
|
end_index = content.find(self.bgroupcert_end_magic)
|
||||||
|
|
||||||
|
if start_index == -1:
|
||||||
|
raise ValueError("Error: BGCert start magic sequence not found.")
|
||||||
|
|
||||||
|
if end_index == -1:
|
||||||
|
raise ValueError("Error: BGCert end magic sequence not found.")
|
||||||
|
|
||||||
|
if end_index < start_index:
|
||||||
|
raise ValueError("Error: BGCert end magic sequence found before the start magic sequence.")
|
||||||
|
|
||||||
|
bgroupcert_data = content[start_index:end_index + len(self.bgroupcert_end_magic)]
|
||||||
|
|
||||||
|
return bgroupcert_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_header(data: bytes, header: Union[str, bytes] = "INNER_MSTAR_FILE") -> bytes:
|
||||||
|
if isinstance(header, str):
|
||||||
|
header = header.encode()
|
||||||
|
|
||||||
|
header_index = data.find(header)
|
||||||
|
|
||||||
|
if header_index == -1:
|
||||||
|
return data
|
||||||
|
|
||||||
|
content = data[header_index + len(header):]
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hex_to_bytes(data: Union[str, bytes]) -> bytes:
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = bytes.fromhex(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_file_data(input_file: Path) -> bytes:
|
||||||
|
with open(input_file, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_security_level(data: bytes) -> str:
|
||||||
|
if b"SL3000" in data:
|
||||||
|
security_level = "SL3000"
|
||||||
|
elif b"SL2000" in data:
|
||||||
|
security_level = "SL2000"
|
||||||
|
elif b"SL150" in data:
|
||||||
|
security_level = "SL150"
|
||||||
|
else:
|
||||||
|
security_level = "UNKNOWN"
|
||||||
|
return security_level
|
||||||
|
|
||||||
|
def process_files(self, bgroupcert_file: Path, zgpriv_file: Path):
|
||||||
|
if not bgroupcert_file.exists():
|
||||||
|
raise ValueError(f"Error: BGCert File not found -> {bgroupcert_file}")
|
||||||
|
|
||||||
|
if not zgpriv_file.exists():
|
||||||
|
raise ValueError(f"Error: ZGPriv File not found -> {zgpriv_file}")
|
||||||
|
|
||||||
|
decrypted_bgroupcert_data = self.decrypt_aes_ecb(
|
||||||
|
inp=bgroupcert_file,
|
||||||
|
is_bgroupcert=True
|
||||||
|
)
|
||||||
|
bgroupcert_data = self.process_bgroupcert(
|
||||||
|
data=decrypted_bgroupcert_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Firmware:")
|
||||||
|
print(f" - Key : {decryption_bgc_zgp.boot_key.hex()}")
|
||||||
|
print(f" - Brand: {decryption_bgc_zgp.brand}")
|
||||||
|
|
||||||
|
security_level = self.get_security_level(bgroupcert_data)
|
||||||
|
|
||||||
|
decrypted_zgpriv_data = self.decrypt_aes_ecb(
|
||||||
|
inp=zgpriv_file
|
||||||
|
)
|
||||||
|
zgpriv_data = self.process_zgpriv(
|
||||||
|
data=decrypted_zgpriv_data,
|
||||||
|
is_sl3000=security_level == "SL3000"
|
||||||
|
)
|
||||||
|
|
||||||
|
bgc_zgp_id = hex(crc32(bgroupcert_data + zgpriv_data))[2:]
|
||||||
|
folder_name = f"playready_decryption_bgc_zgp_pak/{self.brand}_{security_level}_{bgc_zgp_id}"
|
||||||
|
folder_path = Path(folder_name)
|
||||||
|
|
||||||
|
if folder_path.exists():
|
||||||
|
raise ValueError(f"Error: This certificate already exists in {folder_path}")
|
||||||
|
|
||||||
|
folder_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
bgroupcert_out_path = Path(folder_path / "bgroupcert.dat")
|
||||||
|
bgroupcert_out_path.write_bytes(bgroupcert_data)
|
||||||
|
|
||||||
|
zgpriv_out_path = Path(folder_path / "zgpriv.dat")
|
||||||
|
zgpriv_out_path.write_bytes(zgpriv_data)
|
||||||
|
|
||||||
|
command = [
|
||||||
|
"pyplayready", "create-device",
|
||||||
|
"-c", str(bgroupcert_out_path),
|
||||||
|
"-k", str(zgpriv_out_path),
|
||||||
|
"-o", folder_path
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
prd_path = None
|
||||||
|
if result.returncode == 0:
|
||||||
|
prd_path = folder_path / Path(result.stderr.split("Saved to: ")[-1].strip()).name
|
||||||
|
|
||||||
|
print("Decryption:")
|
||||||
|
print(f" - BGroupCert Security level: {security_level}")
|
||||||
|
print(f" - BGroupCert Path : {bgroupcert_out_path}")
|
||||||
|
print(f" - ZGPriv Path : {zgpriv_out_path}")
|
||||||
|
if prd_path:
|
||||||
|
print(f" - PRD Path : {prd_path}")
|
||||||
|
print(f" - Files exported to : {folder_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage:")
|
||||||
|
print(" BGroupCert and ZGPriv")
|
||||||
|
print(f" decrypt_bgc_zgp_pak.py <bgroupcert_file> <zgpriv_file>")
|
||||||
|
print(" Pak")
|
||||||
|
print(f" decrypt_bgc_zgp_pak.py <boot_file> <sedata_file>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
boot_keys = {
|
||||||
|
"vestel": {
|
||||||
|
"MB130": "8981D083B3D53B3DF1AC529A70F244C0",
|
||||||
|
"MB_VARIANT_1": "24490B4CC95F739CE34138478E47139E"
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"HE_LCD_NC5U_AAADAIAA": "E33AB4C45C2570B8AD15A921F752DEB6",
|
||||||
|
"HE_DTV_W21A_AFADATAA": "0007FF4154534D92FC55AA0FFF0110E0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playready_keys = {
|
||||||
|
"porting_kit": {
|
||||||
|
"transient": "8B222FFD1E76195659CF2703898C427F",
|
||||||
|
"intermediate": "9CE93432C7D74016BA684763F801E136",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pak / SeData
|
||||||
|
bgroupcert_sizes = {
|
||||||
|
"SL2000": 1760,
|
||||||
|
"SL3000": 1440
|
||||||
|
}
|
||||||
|
zgpriv_sizes = {
|
||||||
|
"SL2000": 176,
|
||||||
|
"SL3000": 176
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Process started...")
|
||||||
|
|
||||||
|
if "boot" in sys.argv[1] and "sedata" in sys.argv[2]:
|
||||||
|
pak = Pak(bgroupcert_sizes, zgpriv_sizes)
|
||||||
|
|
||||||
|
boot_file = Path(sys.argv[1])
|
||||||
|
sedata_file = Path(sys.argv[2])
|
||||||
|
|
||||||
|
pak.process_files(boot_file, sedata_file, playready_keys)
|
||||||
|
else:
|
||||||
|
decryption_bgc_zgp = DecryptionBgcZgp(
|
||||||
|
boot_keys=boot_keys,
|
||||||
|
playready_keys=playready_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
bgroupcert_file = Path(sys.argv[1])
|
||||||
|
zgpriv_file = Path(sys.argv[2])
|
||||||
|
|
||||||
|
decryption_bgc_zgp.process_files(bgroupcert_file, zgpriv_file)
|
||||||
|
|
||||||
|
print("\nProcess completed successfully.")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
BIN
extract_emmc_bin_0.2.zip
Normal file
BIN
extract_emmc_bin_0.2.zip
Normal file
Binary file not shown.
21
ubidump/LICENSE
Normal file
21
ubidump/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Willem Hengeveld <itsme@xs4all.nl>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
129
ubidump/README.md
Normal file
129
ubidump/README.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
UBIFS Dumper
|
||||||
|
============
|
||||||
|
|
||||||
|
This tool can be used to view or extract the contents of UBIFS images.
|
||||||
|
|
||||||
|
About UBIFS
|
||||||
|
===========
|
||||||
|
|
||||||
|
UBIFS is a filesystem specifically designed for used on NAND flash chips.
|
||||||
|
NAND flash is organized in _eraseblocks_. _Eraseblocks_ can be erased,
|
||||||
|
appended to, and read. Erasing is a relatively expensive operation, and can
|
||||||
|
be done only a limited number of times.
|
||||||
|
|
||||||
|
An UBIFS image contains four abstraction layers:
|
||||||
|
* eraseblocks
|
||||||
|
* volumes
|
||||||
|
* b-tree nodes
|
||||||
|
* inodes
|
||||||
|
|
||||||
|
Each eraseblock contains info on how often it has been erased, and which volume it belongs to.
|
||||||
|
A volume contains a b-tree database with keys for:
|
||||||
|
* inodes, indexed by inode number
|
||||||
|
* direntries, indexed by inode number + name hash
|
||||||
|
* datablocks, indexed by inode number + block number
|
||||||
|
|
||||||
|
The inodes are basically a standard unix filesystem, with direntries, regular files, symlinks, devices, etc.
|
||||||
|
|
||||||
|
mounting images on linux
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
modprobe nandsim first_id_byte=0x2c second_id_byte=0xac third_id_byte=0x90 fourth_id_byte=0x26
|
||||||
|
nandwrite /dev/mtd0 firmware-image.ubi
|
||||||
|
modprobe ubi mtd=/dev/mtd0,4096
|
||||||
|
mount -t ubifs -o ro /dev/ubi0_0 mnt
|
||||||
|
|
||||||
|
This will mount a ubi image for a device with eraseblock size 0x40000.
|
||||||
|
If your image has a blocksize of 0x20000, use `fourth_id_byte=0x15`, and specify a pagesize of `2048`
|
||||||
|
with the second modprobe line.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
View the contents of the `/etc/passwd` file in the filesystem image `image.ubi`:
|
||||||
|
|
||||||
|
python ubidump.py -c /etc/passwd image.ubi
|
||||||
|
|
||||||
|
List the files in all the volumes in `image.ubi`:
|
||||||
|
|
||||||
|
python ubidump.py -l image.ubi
|
||||||
|
|
||||||
|
View the contents of b-tree database from the volumes in `image.ubi`:
|
||||||
|
|
||||||
|
python ubidump.py -d image.ubi
|
||||||
|
|
||||||
|
Extract an unsupported volume type, so you can analyze it with other tools:
|
||||||
|
|
||||||
|
python ubidump.py -v 0 --saveraw unknownvol.bin image.ubi
|
||||||
|
|
||||||
|
Note that often ubi images contain squashfs volumes, which can be extracted using tools like
|
||||||
|
[unsquashfs](https://github.com/plougher/squashfs-tools) or [rdsquashfs](https://github.com/AgentD/squashfs-tools-ng)
|
||||||
|
|
||||||
|
Install
|
||||||
|
=======
|
||||||
|
|
||||||
|
Install the required python modules using:
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
or as a pip package:
|
||||||
|
|
||||||
|
pip install ubidump
|
||||||
|
|
||||||
|
You may need to manually install your operarating system libraries for lzo first:
|
||||||
|
|
||||||
|
on linux:
|
||||||
|
|
||||||
|
apt install liblzo2-dev
|
||||||
|
|
||||||
|
on MacOS:
|
||||||
|
|
||||||
|
brew install lzo
|
||||||
|
|
||||||
|
maybe you need to build the python library like this:
|
||||||
|
|
||||||
|
LDFLAGS=-L/usr/local/lib CFLAGS=-I/usr/local/include/lzo pip3 install python-lzo
|
||||||
|
|
||||||
|
|
||||||
|
When you need zstd compression, you will need to install the `zstandard` module.
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
============
|
||||||
|
|
||||||
|
* python2 or python3
|
||||||
|
* python-lzo ( >= 1.09, which introduces the 'header=False' argument )
|
||||||
|
* crcmod
|
||||||
|
* optional: zstandard
|
||||||
|
|
||||||
|
TODO
|
||||||
|
====
|
||||||
|
|
||||||
|
* add option to select a volume
|
||||||
|
* add option to select a older `master` node
|
||||||
|
* parse the journal
|
||||||
|
* analyze b-tree structure for unused nodes
|
||||||
|
* analyze fs structure for unused inodes, dirents
|
||||||
|
* verify that data block size equals the size mentioned in the inode.
|
||||||
|
* add support for ubifs ( without the ubi layer )
|
||||||
|
* add option to extract a raw volume.
|
||||||
|
|
||||||
|
References
|
||||||
|
==========
|
||||||
|
|
||||||
|
* the ubifs/mtd tools http://linux-mtd.infradead.org/
|
||||||
|
* git repos can be found [here](http://git.infradead.org/)
|
||||||
|
|
||||||
|
Similar tools
|
||||||
|
=============
|
||||||
|
|
||||||
|
* another python tool [on github](https://github.com/jrspruitt/ubi_reader/)
|
||||||
|
* does not support listing files.
|
||||||
|
* a closed source windows tool [here](http://ubidump.oozoon.de/)
|
||||||
|
* ubi-utils/ubidump.c [on the mtd mailinglist](http://lists.infradead.org/pipermail/linux-mtd/2014-July/054547.html)
|
||||||
|
|
||||||
|
Author
|
||||||
|
======
|
||||||
|
|
||||||
|
Willem Hengeveld <itsme@xs4all.nl>
|
||||||
|
|
||||||
2
ubidump/requirements.txt
Normal file
2
ubidump/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
python-lzo>=1.11
|
||||||
|
crcmod>=1.7
|
||||||
48
ubidump/setup.py
Normal file
48
ubidump/setup.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
setup(
|
||||||
|
name = "ubidump",
|
||||||
|
version = "1.0.0",
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts': ['ubidump=ubidump:main'],
|
||||||
|
},
|
||||||
|
install_requires=[
|
||||||
|
"python-lzo>=1.11",
|
||||||
|
"crcmod>=1.7",
|
||||||
|
],
|
||||||
|
py_modules=['ubidump'],
|
||||||
|
author = "Willem Hengeveld",
|
||||||
|
author_email = "itsme@xs4all.nl",
|
||||||
|
description = "Commandline tool for viewing or extracting UBIFS images.",
|
||||||
|
long_description="""
|
||||||
|
This tool can be used to view or extract the contents of UBIFS images.
|
||||||
|
|
||||||
|
View the contents of the `/etc/passwd` file in the filesystem image `image.ubi`:
|
||||||
|
|
||||||
|
ubidump -c /etc/passwd image.ubi
|
||||||
|
|
||||||
|
List the files in all the volumes in `image.ubi`:
|
||||||
|
|
||||||
|
ubidump -l image.ubi
|
||||||
|
|
||||||
|
View the contents of b-tree database from the volumes in `image.ubi`:
|
||||||
|
|
||||||
|
ubidump -d image.ubi
|
||||||
|
""",
|
||||||
|
|
||||||
|
license = "MIT",
|
||||||
|
keywords = "ubifs commandline",
|
||||||
|
url = "https://github.com/nlitsme/ubidump/",
|
||||||
|
classifiers = [
|
||||||
|
'Environment :: Console',
|
||||||
|
'Intended Audience :: End Users/Desktop',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Topic :: Utilities',
|
||||||
|
'Topic :: Software Development :: Version Control :: Git',
|
||||||
|
'Topic :: System :: Filesystems',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
1812
ubidump/ubidump.py
Executable file
1812
ubidump/ubidump.py
Executable file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user