Files
ipsw/internal/commands/extract/extract.go

1567 lines
47 KiB
Go

// Package extract contains the extract commands.
package extract
import (
"archive/zip"
"bytes"
"crypto/aes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"time"
"github.com/apex/log"
"github.com/blacktop/go-macho"
fwcmd "github.com/blacktop/ipsw/internal/commands/fw"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/magic"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/aea"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/blacktop/ipsw/pkg/img4"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/kernelcache"
"github.com/blacktop/ipsw/pkg/ota"
"github.com/blacktop/ipsw/pkg/plist"
)
var ErrNoDecryptionKey = errors.New("no decryption key found")
type remoteKernelcacheMember struct {
file *zip.File
devices []string
output string
// iv/key are populated when a wiki key matches the member; payload is populated
// when the member was peeked and found unencrypted (already decompressed).
iv []byte
key []byte
payload []byte
}
type remoteReaderCache struct {
client *http.Client
readers map[int]*zip.Reader
url string
proxy string
insecure bool
folder string
folderDevice string
folderSet bool
}
// Config is the extract command configuration.
type Config struct {
// path to the IPSW
IPSW string `json:"ipsw,omitempty"`
// url to the remote IPSW
URL string `json:"url,omitempty"`
// regex pattern to search for in the IPSW
Pattern string `json:"pattern,omitempty"`
// arches of the DSCs to extract
Arches []string `json:"arches,omitempty"`
// extract the DriverKit DSCs
DriverKit bool `json:"driver_kit,omitempty"`
// extract the DriverKit DSCs
AllDSCs bool `json:"all_dscs,omitempty"`
// extract a single device's kernelcache
KernelDevice string `json:"kernel_device,omitempty"`
// http proxy to use
Proxy string `json:"proxy,omitempty"`
// don't verify the certificate chain
Insecure bool `json:"insecure,omitempty"`
// search the DMGs for files
DMGs bool `json:"dmgs,omitempty"`
// type of DMG to extract
// pattern: (app|sys|fs)
DmgType string `json:"dmg_type,omitempty"`
// flatten the extracted files paths (remove the folders)
Flatten bool `json:"flatten,omitempty"`
// show the progress bar (when using the CLI)
Progress bool `json:"progress,omitempty"`
// Is AEA private key encrypted
Encrypted bool `json:"encrypted,omitempty"`
// AEA private key PEM DB JSON file
PemDB string `json:"pem_db,omitempty"`
// AEA private key in base64 format
AEAKey string `json:"aea_key,omitempty"`
// output directory to write extracted files to
Output string `json:"output,omitempty"`
// output as JSON
JSON bool `json:"json,omitempty"`
// show info
Info bool `json:"info,omitempty"`
// Lookup decryption keys from theapplewiki.com
Lookup bool `json:"lookup,omitempty"`
// FirmwareKeys are caller-provided decryption keys from theapplewiki.com
FirmwareKeys download.WikiFWKeys `json:"-"`
// BuildManifest identity selector (used for rdisk)
Ident string `json:"ident,omitempty"`
info *info.Info
wikiKeys download.WikiFWKeys
remote remoteReaderCache
}
func isURL(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}
// decryptExtractedIM4P decrypts an extracted IM4P file using wiki keys if available
// Returns the new path to the decrypted file (with original extension removed)
func decryptExtractedIM4P(extractedPath string, wikiKeys download.WikiFWKeys) (string, error) {
if wikiKeys == nil {
return extractedPath, nil
}
// Create decrypted output file without the .im4p/.img3 extension
// e.g., DeviceTree.n51ap.im4p -> DeviceTree.n51ap
decryptedPath := strings.TrimSuffix(extractedPath, filepath.Ext(extractedPath))
decrypted, err := DecryptPayloadWithKeys(extractedPath, decryptedPath, wikiKeys)
if err != nil {
return extractedPath, fmt.Errorf("failed to decrypt %s: %v", filepath.Base(extractedPath), err)
}
if !decrypted {
log.Debugf("no key found for %s", filepath.Base(extractedPath))
return extractedPath, nil
}
log.Infof("Decrypting %s", filepath.Base(extractedPath))
// Remove original encrypted file
if err := os.Remove(extractedPath); err != nil {
log.Warnf("failed to remove encrypted file: %v", err)
}
return decryptedPath, nil
}
// DecryptPayloadWithKeys decrypts an extracted IM4P/IMG3 payload if a matching wiki key exists.
func DecryptPayloadWithKeys(inputPath, outputPath string, wikiKeys download.WikiFWKeys) (bool, error) {
iv, key, ok, err := firmwareKeyForFile(wikiKeys, inputPath)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
if err := os.MkdirAll(filepath.Dir(outputPath), 0750); err != nil {
return false, fmt.Errorf("failed to create output directory: %v", err)
}
if err := img4.DecryptPayload(inputPath, outputPath, iv, key); err != nil {
return false, err
}
return true, nil
}
func firmwareKeyForFile(wikiKeys download.WikiFWKeys, filename string) ([]byte, []byte, bool, error) {
if wikiKeys == nil {
return nil, nil, false, nil
}
for _, fwKey := range wikiKeys {
for idx, keyFilename := range fwKey.Filename {
if !firmwareKeyFilenameMatches(filename, keyFilename) {
continue
}
iv, key, ok, err := decodeFirmwareKeyMaterial(fwKey, idx)
if err != nil || ok {
return iv, key, ok, err
}
}
}
return nil, nil, false, nil
}
func firmwareKeyFilenameMatches(path, keyFilename string) bool {
if len(keyFilename) == 0 {
return false
}
normalizedPath := strings.ToLower(strings.ReplaceAll(path, " ", "_"))
normalizedKeyFilename := strings.ToLower(strings.ReplaceAll(keyFilename, " ", "_"))
return strings.HasSuffix(normalizedPath, normalizedKeyFilename)
}
func decodeFirmwareKeyMaterial(fwKey download.WikiFWKey, idx int) ([]byte, []byte, bool, error) {
if idx < len(fwKey.Iv) && idx < len(fwKey.Key) && isKnownKeyPart(fwKey.Iv[idx]) && isKnownKeyPart(fwKey.Key[idx]) {
iv, err := hex.DecodeString(fwKey.Iv[idx])
if err != nil {
return nil, nil, false, fmt.Errorf("failed to decode iv: %v", err)
}
key, err := hex.DecodeString(fwKey.Key[idx])
if err != nil {
return nil, nil, false, fmt.Errorf("failed to decode key: %v", err)
}
return iv, key, true, nil
}
if idx < len(fwKey.Kbag) && isKnownKeyPart(fwKey.Kbag[idx]) {
kbag, err := hex.DecodeString(fwKey.Kbag[idx])
if err != nil {
return nil, nil, false, fmt.Errorf("failed to decode kbag: %v", err)
}
if len(kbag) <= aes.BlockSize {
return nil, nil, false, fmt.Errorf("kbag is too short: %d bytes", len(kbag))
}
return kbag[:aes.BlockSize], kbag[aes.BlockSize:], true, nil
}
return nil, nil, false, nil
}
func isKnownKeyPart(value string) bool {
return len(value) > 0 && !strings.EqualFold(value, "Unknown")
}
func getFolder(c *Config) (*info.Info, string, error) {
if c.info == nil {
var err error
if c.Lookup && c.wikiKeys == nil {
fPath := filepath.Clean(c.IPSW)
log.Debugf("Lookup enabled, parsing IPSW path: %s", fPath)
log.Info("Looking up decryption keys...")
wkeys, lookupErr := download.LookupKeysFromPath(fPath, "", false)
if lookupErr != nil {
log.Warnf("failed to lookup keys from theapplewiki.com: %v", lookupErr)
} else {
c.wikiKeys = wkeys // Store keys for later use in extraction
dtkey, keyErr := wkeys.GetKeyByRegex(`.*DeviceTree.*(img3|im4p)$`)
if keyErr != nil {
log.Warnf("failed to get DeviceTree key: %v", keyErr)
} else {
log.Debugf("Found DeviceTree key: %s", dtkey)
c.info, err = info.Parse(fPath, dtkey)
if err != nil {
return nil, "", fmt.Errorf("failed to parse plists in IPSW: %v", err)
}
}
}
}
if c.info == nil {
c.info, err = info.Parse(filepath.Clean(c.IPSW))
if err != nil {
return nil, "", fmt.Errorf("failed to parse plists in IPSW: %v", err)
}
}
}
folder, err := c.info.GetFolder(c.KernelDevice)
if err != nil {
return c.info, folder, fmt.Errorf("failed to get folder from IPSW metadata: %v", err)
}
return c.info, folder, nil
}
func getRemoteFolder(c *Config) (*info.Info, *zip.Reader, string, error) {
return getRemoteFolderWithBlockSize(c, 0)
}
func getRemoteFolderWithBlockSize(c *Config, blockSize int) (*info.Info, *zip.Reader, string, error) {
zr, err := c.remoteZipReader(blockSize)
if err != nil {
return nil, nil, "", fmt.Errorf("unable to download remote zip: %v", err)
}
if c.info == nil {
if c.Lookup {
log.Info("Looking up decryption keys...")
wkeys, lookupErr := download.LookupKeysFromPath(c.URL, c.Proxy, c.Insecure)
if lookupErr != nil {
log.Warnf("failed to lookup keys from theapplewiki.com: %v", lookupErr)
} else {
c.wikiKeys = wkeys // Store keys for later use in extraction
dtkey, keyErr := wkeys.GetKeyByRegex(`.*DeviceTree.*(img3|im4p)$`)
if keyErr != nil {
log.Warnf("failed to get DeviceTree key: %v", keyErr)
} else {
c.info, err = info.ParseZipFiles(zr.File, dtkey)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to parse plists in remote zip: %v", err)
}
}
}
}
if c.info == nil {
c.info, err = info.ParseZipFiles(zr.File)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to parse plists in remote zip: %v", err)
}
}
}
if !c.remote.folderSet || c.remote.folderDevice != c.KernelDevice {
folder, err := c.info.GetFolder(c.KernelDevice)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to get folder from remote zip metadata: %v", err)
}
c.remote.folder = folder
c.remote.folderDevice = c.KernelDevice
c.remote.folderSet = true
}
return c.info, zr, c.remote.folder, nil
}
func (c *Config) remoteZipReader(blockSize int) (*zip.Reader, error) {
c.resetRemoteReaderCacheIfNeeded()
blockSize = download.NormalizeRemoteZipBlockSize(blockSize)
if c.remote.readers == nil {
c.remote.readers = make(map[int]*zip.Reader)
}
if zr := c.remote.readers[blockSize]; zr != nil {
return zr, nil
}
zr, err := download.NewRemoteZipReader(c.URL, &download.RemoteConfig{
Proxy: c.Proxy,
Insecure: c.Insecure,
Client: c.remoteHTTPClient(),
BlockSize: blockSize,
})
if err != nil {
return nil, err
}
c.remote.readers[blockSize] = zr
return zr, nil
}
func (c *Config) resetRemoteReaderCacheIfNeeded() {
if c.remote.url == c.URL && c.remote.proxy == c.Proxy && c.remote.insecure == c.Insecure {
return
}
c.Close()
c.remote = remoteReaderCache{
url: c.URL,
proxy: c.Proxy,
insecure: c.Insecure,
}
c.info = nil
c.wikiKeys = nil
}
// Close closes idle remote HTTP connections held by the config.
func (c *Config) Close() {
if c == nil || c.remote.client == nil {
return
}
c.remote.client.CloseIdleConnections()
}
func (c *Config) remoteHTTPClient() *http.Client {
if c.remote.client == nil {
c.remote.client = download.NewRemoteHTTPClient(c.Proxy, c.Insecure)
}
return c.remote.client
}
func tuneRemoteZipReader(c *Config, zr *zip.Reader, files []*zip.File) (*zip.Reader, error) {
blockSize := download.RemoteZipBlockSizeForFiles(files)
if blockSize <= download.DefaultRemoteZipBlockSize {
return zr, nil
}
_, tuned, _, err := getRemoteFolderWithBlockSize(c, blockSize)
if err != nil {
return nil, err
}
return tuned, nil
}
func exactZipNamePattern(name string) *regexp.Regexp {
return regexp.MustCompile("^" + regexp.QuoteMeta(name) + "$")
}
func keybagZipFiles(files []*zip.File, pattern string) ([]*zip.File, error) {
if len(pattern) == 0 {
pattern = `.*im4p$`
}
re, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("failed to compile --pattern regexp: %v", err)
}
return matchingZipFiles(files, re), nil
}
func matchingZipFiles(files []*zip.File, pattern *regexp.Regexp) []*zip.File {
matches := make([]*zip.File, 0)
for _, file := range files {
if file.FileInfo().IsDir() || !pattern.MatchString(file.Name) {
continue
}
matches = append(matches, file)
}
return matches
}
func matchingZipFileName(files []*zip.File, name string) []*zip.File {
matches := make([]*zip.File, 0)
for _, file := range files {
if file.FileInfo().IsDir() || file.Name != name {
continue
}
matches = append(matches, file)
}
return matches
}
func ExtractFromDMG(ipswPath, dmgPath, destPath, pemDB string, pattern *regexp.Regexp) ([]string, error) {
skipCleanup := false
tmpExtractDir := ""
// For AEA-encrypted DMGs, check if the decrypted version already exists
// (e.g. already extracted + mounted by a prior step like mountSystemOsDMGs).
// Reuse it to avoid overwriting a mounted DMG's backing file.
if filepath.Ext(dmgPath) == ".aea" {
decryptedPath := strings.TrimSuffix(dmgPath, filepath.Ext(dmgPath))
// Only reuse if this is a real path (not a bare IPSW-internal filename).
if filepath.IsAbs(decryptedPath) || filepath.Dir(decryptedPath) != "." {
if _, err := os.Stat(decryptedPath); err == nil {
dmgPath = decryptedPath
skipCleanup = true
}
}
}
if !skipCleanup {
// If dmgPath is a bare filename (e.g. 043-....dmg.aea), treat it as an IPSW-internal
// identifier and always extract it into a fresh temp dir. This avoids collisions with
// same-named files left in the current working directory from prior runs.
dmgNameOnly := !filepath.IsAbs(dmgPath) && filepath.Dir(dmgPath) == "."
if dmgNameOnly || func() bool {
_, err := os.Stat(dmgPath)
return os.IsNotExist(err)
}() {
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_dmg")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %v", err)
}
tmpExtractDir = tmpDIR
defer os.RemoveAll(tmpExtractDir)
dmgs, err := utils.Unzip(ipswPath, tmpExtractDir, func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), filepath.Base(dmgPath))
})
if err != nil {
return nil, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return nil, fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
dmgPath = dmgs[0] // update dmgPath to the actual extracted file path
}
if filepath.Ext(dmgPath) == ".aea" {
var err error
dmgPath, err = aea.Decrypt(&aea.DecryptConfig{
Input: dmgPath,
Output: filepath.Dir(dmgPath),
PemDB: pemDB,
Proxy: "", // TODO: make proxy configurable
Insecure: false, // TODO: make insecure configurable
})
if err != nil {
return nil, fmt.Errorf("failed to parse AEA encrypted DMG: %v", err)
}
defer os.Remove(dmgPath)
}
}
utils.Indent(log.Info, 2)(fmt.Sprintf("Mounting DMG %s", dmgPath))
mountPoint, alreadyMounted, err := utils.MountDMG(dmgPath, "")
if err != nil {
return nil, fmt.Errorf("failed to IPSW FS dmg: %v", err)
}
if alreadyMounted {
utils.Indent(log.Debug, 3)(fmt.Sprintf("%s already mounted", dmgPath))
} else {
defer func() {
utils.Indent(log.Debug, 2)(fmt.Sprintf("Unmounting %s", dmgPath))
if err := utils.Retry(3, 2*time.Second, func() error {
return utils.Unmount(mountPoint, false)
}); err != nil {
log.Errorf("failed to unmount DMG %s at %s: %v", dmgPath, mountPoint, err)
}
}()
}
var artifacts []string
// Track visited to avoid loops/duplicates (symlinks)
visited := make(map[string]bool)
extractMatchingFile := func(path string) error {
rel := strings.TrimPrefix(path, mountPoint)
rel = strings.TrimPrefix(rel, string(os.PathSeparator))
rel = filepath.Clean(rel)
if pattern.MatchString(string(os.PathSeparator)+rel) || pattern.MatchString(rel) {
fname := filepath.Join(destPath, rel)
if err := os.MkdirAll(filepath.Dir(fname), 0o755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", filepath.Join(destPath, filepath.Dir(fname)), err)
}
utils.Indent(log.Debug, 3)(fmt.Sprintf("Extracting to %s", fname))
if err := utils.Copy(path, fname); err != nil {
log.WithError(err).Errorf("failed to copy %s to %s", path, fname)
return nil // keep going
}
artifacts = append(artifacts, fname)
}
return nil
}
// extract files that match regex pattern (follows symlinked directories)
if err := filepath.Walk(mountPoint, func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsPermission(err) {
log.Debugf("skipping path due to permission denied: %s", path)
return nil
}
log.WithError(err).Debugf("failed to walk %s", mountPoint)
return nil // keep going
}
if info.Mode()&os.ModeSymlink != 0 {
// Follow symlinked directories to find files reachable only through symlinks
// (e.g. /sbin -> /usr/sbin on modern Apple firmwares)
if linkPath, err := filepath.EvalSymlinks(path); err == nil {
linfo, err := os.Stat(linkPath)
if err != nil {
return nil
}
if linfo.IsDir() && !visited[linkPath] {
visited[linkPath] = true
return filepath.Walk(linkPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
if subErr != nil {
log.WithError(subErr).Debug("failed to walk symlinked path")
return nil
}
if !subInfo.IsDir() && !visited[subPath] {
visited[subPath] = true
return extractMatchingFile(subPath)
}
return nil
})
} else if !linfo.IsDir() {
// Symlink to a file — check against pattern
return extractMatchingFile(linkPath)
}
}
return nil
}
if info.IsDir() {
return nil // skip directories
}
if !visited[path] {
visited[path] = true
return extractMatchingFile(path)
}
return nil
}); err != nil {
return nil, fmt.Errorf("failed to extract File System files from IPSW: %v", err)
}
return artifacts, nil
}
// FirmwareType returns the type of the firmware: IPSW or OTA
func FirmwareType(c *Config) (string, error) {
var err error
if len(c.IPSW) > 0 {
// Use getFolder which handles key lookup if enabled
_, _, err = getFolder(c)
if err != nil {
return "", err
}
return c.info.Plists.Type, nil
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return "", fmt.Errorf("invalid URL provided: %s", c.URL)
}
c.info, _, _, err = getRemoteFolder(c)
if err != nil {
return "", err
}
return c.info.Plists.Type, nil
}
return "", fmt.Errorf("no IPSW or URL provided")
}
func IsAEA(c *Config) (bool, error) {
if len(c.IPSW) > 0 {
return magic.IsAA(filepath.Clean(c.IPSW))
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return false, fmt.Errorf("invalid URL provided: %s", c.URL)
}
req, err := http.NewRequest("GET", c.URL, nil)
if err != nil {
return false, fmt.Errorf("failed to create HTTP request: %v", err)
}
req.Header.Set("Range", "bytes=0-4")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, fmt.Errorf("client failed to perform request: %v", err)
}
defer resp.Body.Close()
mdata, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("failed to read remote data: %v", err)
}
return magic.IsAEAData(bytes.NewReader(mdata))
}
return false, fmt.Errorf("no IPSW or URL provided")
}
// Kernelcache extracts the kernelcache from an IPSW
func Kernelcache(c *Config) (map[string][]string, error) {
if len(c.IPSW) > 0 {
_, folder, err := getFolder(c)
if err != nil {
return nil, err
}
return kernelcache.Extract(c.IPSW, filepath.Join(filepath.Clean(c.Output), folder), c.KernelDevice)
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
destPath := filepath.Join(filepath.Clean(c.Output), folder)
zr, err = tuneRemoteZipReader(c, zr, remoteKernelcacheCandidates(i, zr.File, destPath, c.KernelDevice))
if err != nil {
return nil, err
}
keys := c.FirmwareKeys
if len(keys) == 0 {
keys = c.wikiKeys
}
if len(keys) > 0 {
return remoteKernelcacheWithKeys(i, zr, destPath, c.KernelDevice, keys, c.Progress)
}
return kernelcache.RemoteParseWithInfo(i, zr, destPath, c.KernelDevice)
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
func remoteKernelcacheWithKeys(i *info.Info, zr *zip.Reader, destPath, device string, wikiKeys download.WikiFWKeys, progress bool) (map[string][]string, error) {
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_kcache")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store kernelcache: %v", err)
}
defer os.RemoveAll(tmpDIR)
artifacts := make(map[string][]string)
selected, err := selectRemoteKernelcacheMembers(i, zr.File, destPath, device, wikiKeys)
if err != nil {
return nil, err
}
for _, kc := range selected {
if err := os.MkdirAll(filepath.Dir(kc.output), 0750); err != nil {
return nil, fmt.Errorf("failed to create output directory: %v", err)
}
if kc.payload != nil {
if err := os.WriteFile(kc.output, kc.payload, 0660); err != nil {
return nil, fmt.Errorf("failed to write kernelcache %s: %v", kc.file.Name, err)
}
artifacts[kc.output] = kc.devices
continue
}
extracted, err := utils.SearchZip([]*zip.File{kc.file}, regexp.MustCompile("^"+regexp.QuoteMeta(kc.file.Name)+"$"), tmpDIR, true, progress)
if err != nil {
return nil, fmt.Errorf("failed to extract kernelcache %s: %v", kc.file.Name, err)
}
if len(extracted) == 0 {
return nil, fmt.Errorf("failed to extract kernelcache %s", kc.file.Name)
}
if err := img4.DecryptPayload(extracted[0], kc.output, kc.iv, kc.key); err != nil {
return nil, fmt.Errorf("failed to decrypt kernelcache %s: %v", kc.file.Name, err)
}
artifacts[kc.output] = kc.devices
}
return artifacts, nil
}
func readZipMember(f *zip.File) ([]byte, error) {
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open zip member %s: %v", f.Name, err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
return nil, fmt.Errorf("failed to read zip member %s: %v", f.Name, err)
}
return data, nil
}
func remoteKernelcacheCandidates(i *info.Info, files []*zip.File, destPath, device string) []*zip.File {
matches := make([]*zip.File, 0)
for _, file := range files {
if file.FileInfo().IsDir() || !strings.Contains(file.Name, "kernelcache.") {
continue
}
devices := i.GetDevicesForKernelCache(file.Name)
if len(device) > 0 && !slices.Contains(devices, device) {
continue
}
fname := filepath.Join(destPath, filepath.Clean(i.GetKernelCacheFileName(file.Name)))
if _, err := os.Stat(fname); err == nil {
continue
}
matches = append(matches, file)
}
return matches
}
func selectRemoteKernelcacheMembers(i *info.Info, files []*zip.File, destPath, device string, wikiKeys download.WikiFWKeys) ([]remoteKernelcacheMember, error) {
var selected []remoteKernelcacheMember
var missingKeys []string
var foundKernelcache bool
for _, f := range files {
if !strings.Contains(f.Name, "kernelcache.") {
continue
}
devices := i.GetDevicesForKernelCache(f.Name)
if len(device) > 0 && !slices.Contains(devices, device) {
continue
}
foundKernelcache = true
fname := filepath.Join(destPath, filepath.Clean(i.GetKernelCacheFileName(f.Name)))
if _, err := os.Stat(fname); err == nil {
log.Warnf("kernelcache already exists: %s", fname)
continue
}
iv, key, ok, err := firmwareKeyForFile(wikiKeys, f.Name)
if err != nil {
return nil, fmt.Errorf("failed to resolve key for kernelcache %s: %w", f.Name, err)
}
var payload []byte
if !ok {
data, err := readZipMember(f)
if err != nil {
return nil, err
}
cc, err := kernelcache.ParseImg4Data(data)
if errors.Is(err, kernelcache.ErrEncryptedKernelCache) {
missingKeys = append(missingKeys, f.Name)
continue
}
if err != nil {
return nil, fmt.Errorf("failed to parse kernelcache %s: %v", f.Name, err)
}
payload, err = kernelcache.DecompressData(cc)
if err != nil {
return nil, fmt.Errorf("failed to decompress kernelcache %s: %v", f.Name, err)
}
}
selected = append(selected, remoteKernelcacheMember{
file: f,
devices: devices,
output: fname,
iv: iv,
key: key,
payload: payload,
})
}
if !foundKernelcache {
if len(device) > 0 {
return nil, fmt.Errorf("no kernelcache found for device %s in IPSW", device)
}
return nil, fmt.Errorf("no kernelcache found in IPSW")
}
if len(missingKeys) > 0 {
return nil, fmt.Errorf("%w for %s", ErrNoDecryptionKey, strings.Join(missingKeys, ", "))
}
return selected, nil
}
// SPTM extracts the SPTM firmware from an IPSW
func SPTM(c *Config) ([]string, error) {
var tmpOut []string
var outfiles []string
origOutput := c.Output
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_sptm")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store SPTM im4p: %v", err)
}
defer os.RemoveAll(tmpDIR)
c.Output = tmpDIR
c.Pattern = `.*sptm.*im4p$`
out, err := Search(c)
if err != nil {
return nil, err
}
if len(out) == 0 {
return nil, fmt.Errorf("no SPTM firmware found")
}
tmpOut = append(tmpOut, out...)
c.Pattern = `.*txm.*im4p$`
out, err = Search(c)
if err != nil {
return nil, err
}
if len(out) == 0 {
return nil, fmt.Errorf("no TXM firmware found")
}
tmpOut = append(tmpOut, out...)
c.Output = origOutput
for _, f := range tmpOut {
dat, err := os.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("failed to open '%s': %v", f, err)
}
im4p, err := img4.ParsePayload(dat)
if err != nil {
return nil, fmt.Errorf("failed to parse '%s': %v", f, err)
}
folder := filepath.Join(filepath.Clean(c.Output), strings.TrimPrefix(filepath.Dir(f), tmpDIR))
if err := os.MkdirAll(folder, 0o750); err != nil {
return nil, fmt.Errorf("failed to create output directory '%s': %v", folder, err)
}
fname := filepath.Join(folder, strings.TrimSuffix(filepath.Base(f), ".im4p"))
data, err := im4p.GetData()
if err != nil {
return nil, fmt.Errorf("failed to get data from '%s': %v", f, err)
}
if err = os.WriteFile(fname, data, 0o644); err != nil {
return nil, fmt.Errorf("failed to write '%s': %v", fname, err)
}
outfiles = append(outfiles, fname)
}
return outfiles, nil
}
func Exclave(c *Config) ([]string, error) {
var (
err error
tmpOut []string
outfiles []string
excs [][]byte
)
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_exclave")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store Exlave im4p: %v", err)
}
defer os.RemoveAll(tmpDIR)
c.Pattern = `.*exclavecore_bundle.*im4p$`
out, err := Search(c, tmpDIR)
if err != nil {
return nil, err
}
if len(out) == 0 {
return nil, fmt.Errorf("no Exclave bundles found")
}
tmpOut = append(tmpOut, out...)
for _, f := range tmpOut {
if strings.Contains(f, ".restore.") {
continue // TODO: skip restore bundles for now
}
im4p, err := img4.OpenPayload(f)
if err != nil {
return nil, fmt.Errorf("failed to parse '%s': %v", f, err)
}
excData, err := im4p.GetData()
if err != nil {
return nil, fmt.Errorf("failed to get data from '%s': %v", f, err)
}
if !c.Info {
// save BUND file
baseName := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
excFile := filepath.Join(filepath.Clean(c.Output), baseName)
if err := os.MkdirAll(filepath.Dir(excFile), 0o750); err != nil {
return nil, fmt.Errorf("failed to create output directory '%s': %v", excFile, err)
}
if err := os.WriteFile(excFile, excData, 0o644); err != nil {
return nil, fmt.Errorf("failed to write '%s': %v", excFile, err)
}
outfiles = append(outfiles, excFile)
}
// append to exclave cores for kernel/app extraction
excs = append(excs, excData)
}
for _, exc := range excs {
if c.Info {
fwcmd.ShowExclaveCores(exc)
continue
}
out, err := fwcmd.ExtractExclaveCores(exc, filepath.Clean(c.Output))
if err != nil {
return nil, fmt.Errorf("failed to extract files from exclave bundle: %v", err)
}
outfiles = append(outfiles, out...)
}
if c.Info {
return nil, nil
}
return outfiles, nil
}
// DSC extracts the DSC file from an IPSW
func DSC(c *Config) ([]string, error) {
if len(c.IPSW) > 0 {
_, folder, err := getFolder(c)
if err != nil {
return nil, err
}
return dyld.Extract(c.IPSW, filepath.Join(filepath.Clean(c.Output), folder), c.PemDB, c.Arches, c.DriverKit, c.AllDSCs)
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
if i.Plists.Type == "OTA" {
if runtime.GOOS == "darwin" {
zr, err = tuneRemoteZipReader(c, zr, dyld.RemoteCryptexFiles(zr.File, c.Arches))
if err != nil {
return nil, err
}
out, err := dyld.ExtractFromRemoteCryptex(zr, filepath.Join(filepath.Clean(c.Output), folder), c.PemDB, c.Arches, c.DriverKit, c.AllDSCs)
if err != nil {
if errors.Is(err, dyld.ErrNoCryptex) {
if len(c.Arches) == 0 {
log.Warnf("%v; trying to extract dyld_shared_cache from payload files", err)
} else {
log.Warnf("%v for the specified arch(es): %s; trying to extract dyld_shared_cache from payload files (older OTAs didn't use cryptexes)", err, strings.Join(c.Arches, ", "))
}
c.Pattern = `^` + dyld.CacheRegex
rfiles, err := ota.RemoteList(zr)
if err != nil {
return nil, fmt.Errorf("failed to list files in remote OTA: %v", err)
}
var dcaches []string
for _, f := range rfiles {
if regexp.MustCompile(c.Pattern).MatchString(f.Name()) {
dcaches = append(dcaches, f.Name())
}
}
zr, err = tuneRemoteZipReader(c, zr, matchingZipFiles(zr.File, regexp.MustCompile(`payload\.0\d+$`)))
if err != nil {
return nil, err
}
out, err = ota.RemoteExtract(zr, c.Pattern, filepath.Join(filepath.Clean(c.Output), folder), func(s string) bool {
s = strings.TrimPrefix(s, folder+string(os.PathSeparator))
if slices.Contains(dcaches, s) {
dcaches = utils.RemoveStrFromSlice(dcaches, s)
if len(dcaches) == 0 {
return true
}
}
return false
})
if err != nil {
return nil, fmt.Errorf("failed to extract OTA: %v", err)
}
} else {
return nil, fmt.Errorf("failed to extract dyld_shared_cache from remote OTA: %v", err)
}
}
return out, nil
}
return nil, fmt.Errorf("extracting dyld_shared_cache from remote OTA is only supported on macOS")
}
sysDMG, err := i.GetSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("only iOS16.x/macOS13.x+ supported: failed to get SystemOS DMG from remote zip metadata: %v", err)
}
if len(sysDMG) == 0 {
return nil, fmt.Errorf("only iOS16.x/macOS13.x+ supported: no SystemOS DMG found in remote zip metadata")
}
zr, err = tuneRemoteZipReader(c, zr, matchingZipFileName(zr.File, sysDMG))
if err != nil {
return nil, err
}
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_remote_dyld")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store SystemOS DMG: %v", err)
}
defer os.RemoveAll(tmpDIR)
sysDMGRegex := exactZipNamePattern(sysDMG)
extracted, err := utils.SearchZip(zr.File, sysDMGRegex, tmpDIR, c.Flatten, true)
if err != nil {
return nil, fmt.Errorf("failed to extract SystemOS DMG from remote IPSW: %v", err)
}
if len(extracted) == 0 {
return nil, fmt.Errorf("failed to extract SystemOS DMG %s from remote IPSW", sysDMG)
}
return dyld.ExtractFromDMG(i, extracted[0], filepath.Join(filepath.Clean(c.Output), folder), c.PemDB, c.Arches, c.DriverKit, c.AllDSCs)
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
// DMG extracts the DMG from an IPSW
func DMG(c *Config) ([]string, error) {
if len(c.IPSW) == 0 && len(c.URL) == 0 {
return nil, fmt.Errorf("no IPSW or URL provided")
}
var err error
var i *info.Info
var folder string
var zr *zip.Reader
if len(c.IPSW) > 0 {
i, folder, err = getFolder(c)
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Clean(c.IPSW))
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
defer f.Close()
finfo, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("failed to stat IPSW: %v", err)
}
zr, err = zip.NewReader(f, finfo.Size())
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err = getRemoteFolder(c)
if err != nil {
return nil, err
}
}
var dmgPath string
switch c.DmgType {
case "app":
dmgPath, err = i.GetAppOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find appOS DMG in IPSW: %v", err)
}
case "sys":
dmgPath, err = i.GetSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find systemOS DMG in IPSW: %v", err)
}
case "fs":
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to find filesystem DMG in IPSW: %v", err)
}
case "exc":
dmgPath, err = i.GetExclaveOSDmg()
if err != nil {
return nil, fmt.Errorf("failed to find exclaveOS DMG in IPSW: %v", err)
}
case "rdisk":
dmgPath, err = i.GetRestoreRamDiskDmg(c.Ident)
if err != nil {
return nil, fmt.Errorf("failed to find RestoreRamDisk DMG in IPSW: %v", err)
}
default:
return nil, fmt.Errorf("unknown DMG type %q", c.DmgType)
}
dmgRegex := exactZipNamePattern(dmgPath)
if len(c.URL) > 0 {
zr, err = tuneRemoteZipReader(c, zr, matchingZipFileName(zr.File, dmgPath))
if err != nil {
return nil, err
}
}
return utils.SearchZip(zr.File, dmgRegex, filepath.Join(filepath.Clean(c.Output), folder), c.Flatten, c.Progress)
}
func extractRemoteDMG(c *Config, zr *zip.Reader, dmgPath, destPath, pemDB string, pattern *regexp.Regexp) ([]string, error) {
if dmgPath == "" {
return nil, nil
}
tmpDIR, err := os.MkdirTemp("", "ipsw_extract_remote_dmg")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory to store %s: %v", dmgPath, err)
}
defer os.RemoveAll(tmpDIR)
dmgRegex := exactZipNamePattern(dmgPath)
tuned, err := tuneRemoteZipReader(c, zr, matchingZipFileName(zr.File, dmgPath))
if err != nil {
return nil, err
}
extracted, err := utils.SearchZip(tuned.File, dmgRegex, tmpDIR, false, true)
if err != nil {
return nil, err
}
var artifacts []string
for _, dmg := range extracted {
out, err := ExtractFromDMG(dmg, dmg, destPath, pemDB, pattern)
if err != nil {
return nil, err
}
artifacts = append(artifacts, out...)
}
return artifacts, nil
}
// Keybags extracts the keybags from an IPSW
func Keybags(c *Config) (fname string, err error) {
if len(c.IPSW) == 0 && len(c.URL) == 0 {
return "", fmt.Errorf("no IPSW or URL provided")
}
var i *info.Info
var folder string
var kbags *img4.KeyBags
if len(c.IPSW) > 0 {
i, folder, err = getFolder(c)
if err != nil {
return "", err
}
zr, err := zip.OpenReader(filepath.Clean(c.IPSW))
if err != nil {
return "", fmt.Errorf("failed to open IPSW: %v", err)
}
defer zr.Close()
kbags, err = img4.GetKeybagsFromIPSW(zr.File, img4.KeybagMetaData{
Type: i.Plists.Type,
Version: i.Plists.BuildManifest.ProductVersion,
Build: i.Plists.BuildManifest.ProductBuildVersion,
Devices: i.Plists.Restore.SupportedProductTypes,
}, c.Pattern)
if err != nil {
return "", fmt.Errorf("failed to parse im4p kbags: %v", err)
}
} else if len(c.URL) > 0 {
var zr *zip.Reader
if !isURL(c.URL) {
return "", fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err = getRemoteFolder(c)
if err != nil {
return "", err
}
matches, err := keybagZipFiles(zr.File, c.Pattern)
if err != nil {
return "", fmt.Errorf("failed to parse im4p kbags: %v", err)
}
zr, err = tuneRemoteZipReader(c, zr, matches)
if err != nil {
return "", err
}
kbags, err = img4.GetKeybagsFromIPSW(zr.File, img4.KeybagMetaData{
Type: i.Plists.Type,
Version: i.Plists.BuildManifest.ProductVersion,
Build: i.Plists.BuildManifest.ProductBuildVersion,
Devices: i.Plists.Restore.SupportedProductTypes,
}, c.Pattern)
if err != nil {
return "", fmt.Errorf("failed to parse im4p kbags: %v", err)
}
}
out, err := json.Marshal(kbags)
if err != nil {
return "", fmt.Errorf("failed to marshal im4p kbags: %v", err)
}
if c.JSON {
return string(out), nil
}
fname = filepath.Join(filepath.Join(filepath.Clean(c.Output), folder), "kbags.json")
if err := os.MkdirAll(filepath.Dir(fname), 0o750); err != nil {
return "", fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
}
if err := os.WriteFile(fname, out, 0o666); err != nil {
return "", fmt.Errorf("failed to write %s: %v", filepath.Join(filepath.Join(filepath.Clean(c.Output), folder), "kbags.json"), err)
}
return
}
// FcsKeys extracts the AEA1 DMG fsc-keys from an IPSW
func FcsKeys(c *Config) ([]string, error) {
if len(c.IPSW) == 0 && len(c.URL) == 0 {
return nil, fmt.Errorf("no IPSW or URL provided")
}
var artifacts []string
var err error
var i *info.Info
var folder string
var zr *zip.Reader
if len(c.IPSW) > 0 {
i, folder, err = getFolder(c)
if err != nil {
return nil, err
}
f, err := os.Open(filepath.Clean(c.IPSW))
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
defer f.Close()
finfo, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("failed to stat IPSW: %v", err)
}
zr, err = zip.NewReader(f, finfo.Size())
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err = getRemoteFolder(c)
if err != nil {
return nil, err
}
}
dmgPath, err := i.GetSystemOsDmg()
if err != nil {
if errors.Is(err, info.ErrorCryptexNotFound) {
log.Warn("could not find SystemOS DMG; trying filesystem DMG (older IPSWs don't have cryptexes)")
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get filesystem DMG: %v", err)
}
} else {
return nil, fmt.Errorf("failed to get SystemOS DMG: %v", err)
}
}
kmap := make(map[string]aea.PrivateKey)
if filepath.Ext(dmgPath) != ".aea" {
return nil, fmt.Errorf("fcs-keys are only found in AEA1 DMGs: found '%s'", filepath.Base(dmgPath))
}
out, err := utils.SearchPartialZip(zr.File, exactZipNamePattern(dmgPath), os.TempDir(), 0x1000, false, false)
if err != nil {
return nil, fmt.Errorf("failed to extract fcs-keys from DMG: %v", err)
}
defer func() {
for _, f := range out {
os.Remove(f)
}
}()
for _, f := range out {
metadata, err := aea.Info(filepath.Clean(f))
if err != nil {
return nil, fmt.Errorf("failed to parse AEA1 metadata: %v", err)
}
pkmap, err := metadata.GetPrivateKey(nil, c.PemDB, true, c.Proxy, c.Insecure)
if err != nil {
return nil, err
}
if c.JSON {
// check if json file exists
if _, err := os.Stat(filepath.Join(filepath.Clean(c.Output), "fcs-keys.json")); !os.IsNotExist(err) {
existingPath := filepath.Join(filepath.Clean(c.Output), "fcs-keys.json")
data, err := os.ReadFile(existingPath)
if err != nil {
return nil, fmt.Errorf("failed to read fcs-keys.json: %v", err)
}
existingKeys := make(map[string]aea.PrivateKey)
if err := json.Unmarshal(data, &existingKeys); err != nil {
log.WithError(err).Warnf("failed to parse existing fcs-keys JSON '%s'; rebuilding file", existingPath)
} else {
maps.Copy(kmap, existingKeys)
}
}
maps.Copy(kmap, pkmap)
} else {
for _, pk := range pkmap {
fname := filepath.Join(filepath.Clean(c.Output), folder, filepath.Base(dmgPath)+".pem")
if err := os.MkdirAll(filepath.Dir(fname), 0o750); err != nil {
return nil, fmt.Errorf("failed to create directory %s: %v", filepath.Dir(fname), err)
}
if err := os.WriteFile(fname, pk, 0o644); err != nil {
return nil, fmt.Errorf("failed to write fcs-key.pem: %v", err)
}
artifacts = append(artifacts, fname)
}
}
}
if c.JSON {
out, err := json.Marshal(kmap)
if err != nil {
return nil, fmt.Errorf("failed to marshal fcs-keys: %v", err)
}
fname := filepath.Join(filepath.Clean(c.Output), "fcs-keys.json")
if err := os.WriteFile(fname, out, 0o644); err != nil {
return nil, fmt.Errorf("failed to write fcs-keys.json: %v", err)
}
artifacts = append(artifacts, fname)
}
return artifacts, nil
}
// Search searches for files matching a pattern in an IPSW
func Search(c *Config, tempDirectory ...string) ([]string, error) {
var artifacts []string
if len(c.Pattern) == 0 {
return nil, fmt.Errorf("no pattern provided")
}
re, err := regexp.Compile(c.Pattern)
if err != nil {
return nil, fmt.Errorf("failed to compile regexp '%s': %v", c.Pattern, err)
}
if len(c.IPSW) > 0 {
i, folder, err := getFolder(c)
if err != nil {
return nil, err
}
destPath := filepath.Join(filepath.Clean(c.Output), folder)
if len(tempDirectory) > 0 {
destPath = tempDirectory[0]
}
zr, err := zip.OpenReader(c.IPSW)
if err != nil {
return nil, fmt.Errorf("failed to open IPSW: %v", err)
}
defer zr.Close()
out, err := utils.SearchZip(zr.File, re, destPath, c.Flatten, false)
if err != nil && !c.DMGs {
return nil, fmt.Errorf("failed to extract files matching pattern from ZIP: %v", err)
}
artifacts = append(artifacts, out...)
if c.DMGs { // SEARCH THE DMGs
if appOS, err := i.GetAppOsDmg(); err == nil {
out, err := ExtractFromDMG(c.IPSW, appOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from AppOS %s: %v", appOS, err)
}
artifacts = append(artifacts, out...)
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
out, err := ExtractFromDMG(c.IPSW, systemOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from SystemOS %s: %v", systemOS, err)
}
artifacts = append(artifacts, out...)
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
out, err := ExtractFromDMG(c.IPSW, fsOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from filesystem %s: %v", fsOS, err)
}
artifacts = append(artifacts, out...)
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
out, err := ExtractFromDMG(c.IPSW, excOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from ExclaveOS %s: %v", excOS, err)
}
artifacts = append(artifacts, out...)
}
}
// Decrypt extracted IM4P files if wiki keys are available
if c.wikiKeys != nil {
for i, artifact := range artifacts {
if strings.HasSuffix(strings.ToLower(artifact), ".im4p") || strings.HasSuffix(strings.ToLower(artifact), ".img3") {
newPath, err := decryptExtractedIM4P(artifact, c.wikiKeys)
if err != nil {
log.Warnf("failed to decrypt %s: %v", filepath.Base(artifact), err)
} else {
artifacts[i] = newPath
}
}
}
}
return artifacts, nil
} else if len(c.URL) > 0 {
if !isURL(c.URL) {
return nil, fmt.Errorf("invalid URL provided: %s", c.URL)
}
i, zr, folder, err := getRemoteFolder(c)
if err != nil {
return nil, err
}
destPath := filepath.Join(filepath.Clean(c.Output), folder)
if c.Output == "" {
destPath = folder
}
zr, err = tuneRemoteZipReader(c, zr, matchingZipFiles(zr.File, re))
if err != nil {
return nil, err
}
out, err := utils.SearchZip(zr.File, re, destPath, c.Flatten, true)
if err != nil && !c.DMGs {
return nil, fmt.Errorf("failed to extract files matching pattern '%s' in remote IPSW: %v", c.Pattern, err)
}
artifacts = append(artifacts, out...)
if c.DMGs { // SEARCH THE DMGs
if appOS, err := i.GetAppOsDmg(); err == nil {
out, err := extractRemoteDMG(c, zr, appOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from AppOS %s: %v", appOS, err)
}
artifacts = append(artifacts, out...)
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
out, err := extractRemoteDMG(c, zr, systemOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from SystemOS %s: %v", systemOS, err)
}
artifacts = append(artifacts, out...)
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
out, err := extractRemoteDMG(c, zr, fsOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from filesystem %s: %v", fsOS, err)
}
artifacts = append(artifacts, out...)
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
out, err := extractRemoteDMG(c, zr, excOS, destPath, c.PemDB, re)
if err != nil {
return nil, fmt.Errorf("failed to extract files from ExclaveOS %s: %v", excOS, err)
}
artifacts = append(artifacts, out...)
}
}
// Decrypt extracted IM4P files if wiki keys are available
if c.wikiKeys != nil {
for i, artifact := range artifacts {
if strings.HasSuffix(strings.ToLower(artifact), ".im4p") || strings.HasSuffix(strings.ToLower(artifact), ".img3") {
newPath, err := decryptExtractedIM4P(artifact, c.wikiKeys)
if err != nil {
log.Warnf("failed to decrypt %s: %v", filepath.Base(artifact), err)
} else {
artifacts[i] = newPath
}
}
}
}
return artifacts, nil
}
return nil, fmt.Errorf("no IPSW or URL provided")
}
// LaunchdConfig extracts launchd config from an IPSW
func LaunchdConfig(path, pemDB string) (string, error) {
ipswPath := filepath.Clean(path)
i, err := info.Parse(ipswPath)
if err != nil {
return "", fmt.Errorf("failed to parse IPSW: %v", err)
}
fsDMG, err := i.GetFileSystemOsDmg()
if err != nil {
return "", fmt.Errorf("failed to get filesystem DMG path: %v", err)
}
extracted, err := ExtractFromDMG(ipswPath, fsDMG, os.TempDir(), pemDB, regexp.MustCompile(`.*/sbin/launchd$`))
if err != nil {
return "", fmt.Errorf("failed to extract launchd from %s: %v", fsDMG, err)
}
if len(extracted) == 0 {
return "", fmt.Errorf("failed to extract launchd from %s: no files extracted", fsDMG)
} else if len(extracted) > 1 {
return "", fmt.Errorf("failed to extract launchd from %s: too many files extracted", fsDMG)
}
defer os.Remove(filepath.Clean(extracted[0]))
var m *macho.File
fat, err := macho.OpenFat(filepath.Clean(extracted[0]))
if err == nil {
m = fat.Arches[len(fat.Arches)-1].File // grab last arch (probably arm64e)
} else {
if err == macho.ErrNotFat {
m, err = macho.Open(filepath.Clean(extracted[0]))
if err != nil {
return "", fmt.Errorf("failed to open macho file: %v", err)
}
} else {
return "", fmt.Errorf("failed to open universal macho file: %v", err)
}
}
data, err := m.Section("__TEXT", "__config").Data()
if err != nil {
return "", fmt.Errorf("failed to get launchd config: %v", err)
}
return string(data), nil
}
// SystemVersion extracts the system version info from an IPSW
func SystemVersion(path, pemDB string) (*plist.SystemVersion, error) {
ipswPath := filepath.Clean(path)
i, err := info.Parse(ipswPath)
if err != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
fsDMG, err := i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get filesystem DMG path: %v", err)
}
extracted, err := ExtractFromDMG(ipswPath, fsDMG, os.TempDir(), pemDB, regexp.MustCompile(`System/Library/CoreServices/SystemVersion.plist$`))
if err != nil {
return nil, fmt.Errorf("failed to extract launchd from %s: %v", fsDMG, err)
}
if len(extracted) == 0 {
return nil, fmt.Errorf("failed to extract SystemVersion.plist from %s: no files extracted", fsDMG)
} else if len(extracted) > 1 {
return nil, fmt.Errorf("failed to extract SystemVersion.plist from %s: too many files extracted", fsDMG)
}
defer os.Remove(filepath.Clean(extracted[0]))
dat, err := os.ReadFile(extracted[0])
if err != nil {
return nil, fmt.Errorf("failed to read SystemVersion.plist: %v", err)
}
return plist.ParseSystemVersion(dat)
}