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
21 changes: 11 additions & 10 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ You might get this error:

If that is the case, you need to install `defusedxml` with python bindings.

After installing the library you can use `kextractor` script from the
You also need to make sure you can run perl scripts and have installed
`Crypt::Rijndael` perl module. Before runing `kextractor`, please make
sure you can run `decrypt.pl` by making it an executable and that you
run the `make` command in the `/kextractor/kextractorlib/lzssdec`
directory.

After completing these steps you can use `kextractor` script from the
`/scripts` directory.

## Usage
Expand Down Expand Up @@ -56,15 +62,10 @@ argument followed by the name of the targeted extension:
```bash
$ kextractor -K <name_of_extension> KCACHE
```
Kextractor also has a decompression feature integrated. For that, you need to provide a path that will be used when generating the decompressed file and you need to install `pyliblzfse`(https://github.com/ydkhatri/pyliblzfse). Please note that for iOS <= 8 the kernel cache file is encrypted and it needs to be decrypted before the extensions can be extracted. Kextractor now offers this functionality, and you
will need to provide the Key and IV for the kernel cache. You can find those here: https://www.theiphonewiki.com/wiki.

Kextractor also has a decompression script for kernel cache from iOS 14 onwards. For that, use the `decompress` script from `kextractorlib`. Before using the script, you will need to create a file with the extension `mach.arm` which will be used to store the result and you need to install `pyliblzfse`(https://github.com/ydkhatri/pyliblzfse). The script requires 2 offsets which can be found by using the `get_bvx_section_offsets` script from `kextractorlib`. Note that the decompression script doesn't need any arguments, while the `get_bvx_section_offsets` script needs the path to the compressed kernel cache file.
## Supported iOS versions

Kextractor is currently working for every iOS version up to iOS 12.
For versions starting from iOS 12 Kextractor is only able to extract the text
section. The format of kernelcaches has changed significantly from this version
onwards:
- Some segments from the extensions have been integrated into the `__TEXT`,`__DATA_CONST`, `__TEXT_EXEC`, `__DATA` and `__LINKEDIT` segments
- There are some new sections
- Some old segments are now 0 bytes long (`__PRELINK_TEXT`, `__PLK_TEXT_EXEC`, `__PRELINK_DATA` and `__PLK_DATA_CONST`)
- There is no symbol information
Kextractor is currently working for every iOS version up to iOS 15.

2 changes: 2 additions & 0 deletions kextractorlib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from kextractorlib.kext import *
from kextractorlib.extractor import *
73 changes: 73 additions & 0 deletions kextractorlib/applexml_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from base64 import b64decode
from defusedxml.ElementTree import fromstring as xmlparse
from xml.etree.ElementTree import Element as XmlElement

__all__ = ['parse']

def xml_transform(element: XmlElement, ids):
"""Transform the given apple XML into an hierarchy based on
python specific objects(list, dictionary). It also requires a
dictionary(ids) for searching the referenced objects.
"""
if 'IDREF' in element.attrib:
assert element.tag in ['string', 'integer']
assert element.attrib['IDREF'] in ids
data = ids[element.attrib['IDREF']]
else:
data = element.text

if element.tag == 'true':
return True
if element.tag == 'false':
return False
if element.tag == 'data':
return b64decode(data)
if element.tag == 'string':
return data if data != None else ''
if element.tag == 'integer':
if data == None:
print("WARNING int NULL", file=sys.stderr)
return None
return int(data,
16 if data[:2].lower() == '0x' else
8 if data[0] == '0' else 10)
if element.tag == 'dict':
result = {}
key = None
for subelement in element:
if subelement.tag == 'key':
assert type(subelement.text) == str
key = subelement.text
elif key != None:
result[key] = xml_transform(subelement, ids)
key = None
return result
if element.tag == 'array':
return [xml_transform(subelement, ids) for subelement in element]
print("WARNING unkown tag {}".format(v.tag), file=sys.stderr)


def get_xml_ids(element: XmlElement, ids: dict):
"""Extract elements with ID attributes from given XML and inserts them into
the ids dictionary.
"""
if 'ID' in element.attrib:
assert element.tag in ['string', 'integer', 'data']
assert element.text != None or element.tag == 'string'
assert element.attrib['ID'] not in ids
ids[element.attrib['ID']] = element.text
if element.tag in ['dict', 'array']:
for subelement in element:
get_xml_ids(subelement, ids)


def parse(data: str):
"""Parse apple XML from given string.
"""
while data[-1] == '\0':
data = data[:-1]

ids = {}
xml = xmlparse(data)
get_xml_ids(xml, ids)
return xml_transform(xml, ids)
35 changes: 35 additions & 0 deletions kextractorlib/binary_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import lief

def bianary_get_word_size(binary: lief.MachO.Binary):
"""Returns the word size of a given MachO Binary.
It return 4 for a 32bit binary and 8 for 64bit binary
"""
assert(binary.header.magic in
[lief.MachO.MACHO_TYPES.MAGIC, lief.MachO.MACHO_TYPES.MAGIC_64])
return 4 if binary.header.magic == lief.MachO.MACHO_TYPES.MAGIC else 8


def binary_get_string_from_address(binary: lief.MachO.Binary, vaddr: int):
"""Returns the ascii string from the given virtual address(vaddr) of
a given MachO binary
"""
s = ''
while True:
byte = binary.get_content_from_virtual_address(vaddr, 1)
if byte == None:
break
byte = byte[0]
if byte == 0:
break
vaddr += 1
s += chr(byte)
return s


def untag_pointer(p):
"""Returns the untaged pointer. On iOS 12 the first 16 bits(MSB) of a
pointer are used to store extra information. We asy that the pointers
from iOS 12 are tagged. More information can be found here:
https://bazad.github.io/2018/06/ios-12-kernelcache-tagged-pointers/
"""
return (p & ((1 << 48) -1)) | (0xffff << 48)
219 changes: 219 additions & 0 deletions kextractorlib/decodeimg3.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/perl
#!perl -w
# (C)2009 Willem Hengeveld itsme@xs4all.nl
use strict;
# this script prints all sections found in the .img3 file, and optionally extracts, and
# decrypts the DATA section

sub usage {
return <<__EOF__
Usage: decodeimg3 [-v] [-o OUTFILE] [-l] [-k XXXX -iv XXXX]
-v : increase verbosity level
-o OUTFILE : save DATA section to OUTFILE
-l : don't decrypt the last DATA block ( for 7a341 ipsw )
-k XX -iv X: specify key + iv when you want to decrypt the DATA section
__EOF__
}

# filetypes: ( * = s5l8900x,s5l8920x # = m68ap,n82ap,n88ap )
# bat0 - batterylow0.*.img3
# bat1 - batterylow1.*.img3
# batF - batteryfull.*.img3
# chg0 - batterycharging0.*.img3
# chg1 - batterycharging1.*.img3
# dtre - DeviceTree.#.img3
# glyC - glyphcharging.*.img3
# glyP - glyphplugin.*.img3
# ibec - iBEC.#.RELEASE.dfu
# ibot - iBoot.#.RELEASE.img3
# ibss - iBSS.#.RELEASE.dfu
# illb - LLB.#.RELEASE.img3
# krnl - kernelcache.release.*
# logo - applelogo.*.img3
# nsrv - needservice.*.img3
# recm - recoverymode.*.img3
# rdsk - {restore/update}.dmg files
# cert - inside certificates

# tagtypes:
# BORD : DWORD: 0 or 4
# CERT : certificate
# DATA : the encrypted data
# - echo <DATA> 00000000000000000000000000000000 | unhex | openssl enc -aes-128-cbc -K d2dad0c5dc935afddd628a2c2c243c4f -iv 1b8a5224f45aa94cfc02a8ceba55d6d8 -d | dump -
# KBAG : DWORD type,aes, iv, key - used to calc the iv+key by encrypting with the GID key
# SEPO : DWORD: 2, 3 or 5
# SHSH : 128 bytes
# TYPE : equal to the header filetype
# VERS : DWORD len + ascii string

# ---- in 'cert' files:
# SDOM : DWORD - security domain
# PROD : DWORD - product
# CHIP : DWORD - chipset
#
use IO::File;
#use WildcardArgs;
use Getopt::Long;
use Crypt::Rijndael;

$|=1;
my $verbose=0;
my $recurse;
my ($aeskey,$aesiv);
my $g_dontdecryptlastblock;
my $outfilename;
GetOptions(
"v+"=>\$verbose,
# "r"=>\$recurse,
"o=s"=>\$outfilename,
"l"=>\$g_dontdecryptlastblock,
"k=s"=>sub { $aeskey= pack("H*", $_[1]) },
"iv=s"=>sub { $aesiv= pack("H*", $_[1]) },
) or die usage();
#handlearg($_, \&processfile, recurse=>$recurse) for @ARGV;

die usage() if !@ARGV;

processfile(shift);

sub processfile {
my $fn= shift;
my $fh= IO::File->new($fn, "r");
if (!$fh) {
warn "$fn: $!\n";
return;
}
binmode $fh;

undef $/;
my $data= <$fh>;
$fh->close();

my %hdr;
(
$hdr{filemagic}, # 00 'Img3'
$hdr{filesize}, # 04
$hdr{contentsize}, # 08
$hdr{certarea}, # 0c
$hdr{filetype}, # 10 'illb', 'ibot' ...
)= unpack("a4VVVa4", $data);
if ($hdr{filemagic} ne "3gmI") {
warn "incorrect filemagic: $hdr{filemagic} - $fn\n";
return;
}
if ($hdr{filesize} != length($data)) {
warn sprintf("header:filesize=%08lx, file:%08lx\n", $hdr{filesize}, length($data));
}
if ($hdr{filesize} < $hdr{contentsize}) {
warn sprintf("filesize < contentsize: %08lx < %08lx\n", $hdr{filesize}, $hdr{contentsize});
return;
}
if ($hdr{contentsize} < $hdr{certarea}) {
warn sprintf("contentsize < certarea: %08lx < %08lx\n", $hdr{contentsize}, $hdr{certarea});
return;
}
$hdr{filetype}= reverse $hdr{filetype};
printf("%s - %s\n", $hdr{filetype}, $fn);
my %sections;
my $ofs= 20;
my $datarest="";
while ($ofs<20+$hdr{contentsize}) {
my %tag;
(
$tag{magic}, # TYPE DATA VERS SEPO BORD KBAG KBAG SHSH CERT
$tag{blocksize},
$tag{payloadsize},
)= unpack("a4VV", substr($data, $ofs, 12));

last if $tag{blocksize}==0;
$tag{magic}= reverse $tag{magic};
$tag{data}= substr($data, $ofs+12, $tag{payloadsize});
$tag{rest}= substr($data, $ofs+12+ $tag{payloadsize}, $tag{blocksize} - $tag{payloadsize}-12);

$sections{$tag{magic}}= $tag{data};

$datarest = $tag{rest} if $tag{magic} eq 'DATA';

if ($tag{magic} eq "TYPE") {
printf(" %s %08x %6x: '%s'\n", $tag{magic}, $ofs+12, $tag{payloadsize}, scalar reverse $tag{data});
}
elsif ($tag{magic} eq "SDOM") {
printf(" %s %08x %6x: 0x%x\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("V", $tag{data}));
}
elsif ($tag{magic} eq "PROD") {
printf(" %s %08x %6x: 0x%x\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("V", $tag{data}));
}
elsif ($tag{magic} eq "CHIP") {
printf(" %s %08x %6x: 0x%x\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("V", $tag{data}));
}
elsif ($tag{magic} eq "BORD") {
printf(" %s %08x %6x: 0x%02lx\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("V", $tag{data}));
}
elsif ($tag{magic} eq "SEPO") {
printf(" %s %08x %6x: 0x%02lx\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("V", $tag{data}));
}
elsif ($tag{magic} eq "KBAG") {
my ($ivtype, $aes, $iv, $key)= unpack("VVa16a*", $tag{data});
printf(" %s %08x %6x: %d %3d %s %s\n", $tag{magic}, $ofs+12, $tag{payloadsize}, $ivtype, $aes, unpack("H*",$iv), unpack("H*",$key));
}
elsif ($tag{magic} eq "VERS") {
printf(" %s %08x %6x: 0x%02lx '%s'\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("Va*",$tag{data}));
}
else {
printf(" %s %08x %6x: %s%s\n", $tag{magic}, $ofs+12, $tag{payloadsize}, unpack("H*", substr($tag{data}, 0, $tag{payloadsize}<32?$tag{payloadsize}:32)), $tag{payloadsize}>32?"...":"");
}
$ofs += $tag{blocksize};
}
if ($ofs != 20+$hdr{contentsize}) {
printf("ofs=%08lx, expected: %08lx\n", $ofs, 20+$hdr{contentsize});
}

if ($aeskey && $aesiv) {
my $encdata= $sections{DATA}.$datarest;
$encdata .= "\x00" x (16-(length($encdata)%16)) if (length($encdata)%16);
my $decrypted= aesdecrypt($encdata, $aeskey, $aesiv);
my $restsize= length($encdata)-length($sections{DATA});
my $hexsize= $restsize;
$hexsize += (16-($restsize%16)) if($restsize%16);
printf("last[%2d]: org:%s dec:%s\n", $restsize, unpack("H*", substr($encdata,-$hexsize)), unpack("H*", substr($decrypted, -$hexsize))) if ($hexsize);

substr($decrypted, -16, 16)= substr($encdata,-16,16) if $g_dontdecryptlastblock;

if ($outfilename) {
my $ofh= IO::File->new($outfilename, "w") or die "$outfilename: $!\n";
binmode $ofh;
$ofh->print(substr($decrypted, 0, length($sections{DATA})));
$ofh->close();
}
else {
printf("decrypted: %s\n", unpack("H*", substr($decrypted, 0, 256)));
}
}
elsif ($outfilename) {
my $ofh= IO::File->new($outfilename, "w") or die "$outfilename: $!\n";
binmode $ofh;
$ofh->print(substr($sections{DATA}, 0, length($sections{DATA})));
$ofh->close();
}
elsif (exists $sections{DATA}) {
my $encdata= $sections{DATA}.$datarest;

my $hexsize= length($datarest);
$hexsize += (16-($hexsize%16)) if($hexsize%16);
printf("last[%2d]: org:%s\n", length($datarest), unpack("H*", substr($encdata,-$hexsize))) if ($hexsize);
}
}
sub aesdecrypt {
my ($encdata, $key, $iv)= @_;
my $aes= Crypt::Rijndael->new($key);
my $decdata= "";

for (my $ofs=0 ; $ofs<length($encdata) ; $ofs+=16)
{
my $ciph= substr($encdata,$ofs,16);
my $decr= $aes->decrypt($ciph) ^ $iv;
$decdata .= $decr;
$iv= $ciph;
}
return $decdata;
}
Loading