Created
January 6, 2026 20:56
-
-
Save Lunarixus/698d9fee99e75578660a42ef6525d18c to your computer and use it in GitHub Desktop.
iOS 10.x+ kernelcache decompressor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| import os | |
| import struct | |
| # Try to import lzfse | |
| try: | |
| import lzfse | |
| LZFSE_AVAILABLE = True | |
| except ImportError: | |
| LZFSE_AVAILABLE = False | |
| def decompress_lzss(data): | |
| """ | |
| A pure Python implementation of Apple's LZSS decompression. | |
| Note: This is slower than C implementations but works for scripts. | |
| """ | |
| # Skip the 0x180 byte header usually found in complzss | |
| # The header format is usually: Magic(8) + ... | |
| # But often we just skip to the data. | |
| # For complzss, the data usually starts after the header. | |
| # We will assume 'data' passed here is the whole payload starting at 'complzss' | |
| if len(data) < 12: | |
| raise ValueError("Data too short for LZSS") | |
| # complzss header parsing | |
| magic = data[:8] | |
| if magic != b'complzss': | |
| raise ValueError("Invalid LZSS magic") | |
| # Big Endian Uncompressed size is at offset 0x8 | |
| uncompressed_size = struct.unpack('>I', data[8:12])[0] | |
| # The compressed stream usually starts at 0x180 (384 decimal) | |
| # However, sometimes it is packed tighter. | |
| # Standard kernelcache complzss header size is 0x180. | |
| header_size = 0x180 | |
| src = data[header_size:] | |
| dst = bytearray() | |
| src_idx = 0 | |
| # Ring buffer for LZSS | |
| text_buf = bytearray(b' ' * 4096) | |
| r = 4078 | |
| flags = 0 | |
| while src_idx < len(src) and len(dst) < uncompressed_size: | |
| # 8 bits of flags, read one byte | |
| flags >>= 1 | |
| if (flags & 256) == 0: | |
| if src_idx >= len(src): break | |
| c = src[src_idx] | |
| src_idx += 1 | |
| flags = c | 0xff00 | |
| if (flags & 1): | |
| # Literal byte | |
| if src_idx >= len(src): break | |
| c = src[src_idx] | |
| src_idx += 1 | |
| dst.append(c) | |
| text_buf[r] = c | |
| r = (r + 1) & 4095 | |
| else: | |
| # Reference to previous data | |
| if src_idx + 1 >= len(src): break | |
| i = src[src_idx] | |
| j = src[src_idx + 1] | |
| src_idx += 2 | |
| # Decode offset and length | |
| k = i | ((j & 0xf0) << 4) | |
| length = (j & 0x0f) + 3 | |
| for _ in range(length): | |
| c = text_buf[(k + _) & 4095] | |
| dst.append(c) | |
| text_buf[r] = c | |
| r = (r + 1) & 4095 | |
| return bytes(dst) | |
| def decompress_kernel(input_path, output_path): | |
| with open(input_path, 'rb') as f: | |
| file_data = f.read() | |
| print(f"[*] Analyzing {input_path} ({len(file_data)} bytes)...") | |
| # Modern iOS (LZFSE) uses 'bvx2' | |
| # Older iOS (LZSS) uses 'complzss' | |
| offset_lzfse = file_data.find(b'bvx2') | |
| offset_lzss = file_data.find(b'complzss') | |
| matches = [] | |
| if offset_lzfse != -1: matches.append((offset_lzfse, 'LZFSE')) | |
| if offset_lzss != -1: matches.append((offset_lzss, 'LZSS')) | |
| if not matches: | |
| print("[!] No supported compression magic (bvx2/complzss) found.") | |
| print(" Is this file already decompressed or encrypted?") | |
| return | |
| matches.sort(key=lambda x: x[0]) | |
| best_offset, algo = matches[0] | |
| print(f"[*] Detected {algo} compression at offset {hex(best_offset)}") | |
| payload = file_data[best_offset:] | |
| decompressed_data = None | |
| if algo == 'LZFSE': | |
| if not LZFSE_AVAILABLE: | |
| print("[!] Error: LZFSE detected but 'lzfse' module not installed.") | |
| print(" Please run: pip install lzfse") | |
| return | |
| try: | |
| # bvx2 is the magic, usually followed by 4 bytes size, then data. | |
| decompressed_data = lzfse.decompress(payload) | |
| except Exception as e: | |
| print(f"[!] LZFSE Decompression failed: {e}") | |
| return | |
| elif algo == 'LZSS': | |
| print("[*] Starting LZSS decompression (this might take a moment in pure Python)...") | |
| try: | |
| decompressed_data = decompress_lzss(payload) | |
| except Exception as e: | |
| print(f"[!] LZSS Decompression failed: {e}") | |
| return | |
| # Check for Mach-O Magic (0xfeedfacf) in the result | |
| if decompressed_data: | |
| # Check the first 4 bytes for Mach-O 64 magic | |
| # 0xfeedfacf in Little Endian is cf fa ed fe | |
| if decompressed_data.startswith(b'\xcf\xfa\xed\xfe'): | |
| print("[*] Validation Successful: Found Mach-O 64-bit header.") | |
| else: | |
| print("[!] Warning: Decompressed data does not start with Mach-O magic.") | |
| print(f" First 4 bytes: {decompressed_data[:4].hex()}") | |
| with open(output_path, 'wb') as out: | |
| out.write(decompressed_data) | |
| print(f"[*] Success! Decompressed kernel written to: {output_path}") | |
| else: | |
| print("[!] Decompression resulted in empty data.") | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 3: | |
| print("Usage: python3 kernelcache_decompressor.py <input_kernel> <output_raw>") | |
| sys.exit(1) | |
| in_file = sys.argv[1] | |
| out_file = sys.argv[2] | |
| if not os.path.exists(in_file): | |
| print(f"Error: Input file '{in_file}' not found.") | |
| sys.exit(1) | |
| decompress_kernel(in_file, out_file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment