chore: add ability to diff IPSW vs. OTA entitlements

This commit is contained in:
blacktop
2025-09-16 17:59:57 -06:00
parent 8070b02c88
commit 58c3d6214c
3 changed files with 741 additions and 262 deletions
+5 -3
View File
@@ -75,15 +75,17 @@ func init() {
// diffCmd represents the diff command
var diffCmd = &cobra.Command{
Use: "diff <IPSW> <IPSW>",
Short: "Diff IPSWs",
Use: "diff <OLD_FW> <NEW_FW>",
Short: "Diff IPSWs and OTAs",
Example: heredoc.Doc(`
# Diff two IPSWs
ipsw diff <old.ipsw> <new.ipsw> --fw --launchd --output <output/folder> --markdown
# Diff two IPSWs with KDKs
ipsw diff <old.ipsw> <new.ipsw> --output <output/folder> --markdown
--kdk /Library/Developer/KDKs/KDK_15.0_24A5264n.kdk/System/Library/Kernels/kernel.release.t6031
--kdk /Library/Developer/KDKs/KDK_15.0_24A5279h.kdk/System/Library/Kernels/kernel.release.t6031`),
--kdk /Library/Developer/KDKs/KDK_15.0_24A5279h.kdk/System/Library/Kernels/kernel.release.t6031
# Diff entitlements between an IPSW and an OTA
ipsw diff <old.ipsw> <new.ota> --ent --markdown`),
Args: cobra.ExactArgs(2),
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
+568 -186
View File
@@ -6,11 +6,17 @@ import (
"bufio"
"bytes"
"compress/gzip"
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"io/fs"
"maps"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"time"
@@ -21,6 +27,9 @@ import (
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/aea"
"github.com/blacktop/ipsw/pkg/info"
"github.com/blacktop/ipsw/pkg/ota"
"github.com/blacktop/ipsw/pkg/ota/pbzx"
"github.com/blacktop/ipsw/pkg/ota/yaa"
"github.com/fatih/color"
)
@@ -47,136 +56,579 @@ type Config struct {
func GetDatabase(conf *Config) (map[string]string, error) {
entDB := make(map[string]string)
// create or load entitlement database
if _, err := os.Stat(conf.Database); os.IsNotExist(err) {
utils.Indent(log.Info, 2)("Generating entitlement database file...")
if len(conf.IPSW) > 0 {
i, err := info.Parse(conf.IPSW)
if err != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
if appOS, err := i.GetAppOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning AppOS")
if ents, err := scanEnts(conf.IPSW, appOS, "AppOS", conf.PemDB); err != nil {
return nil, fmt.Errorf("failed to scan files in AppOS %s: %v", appOS, err)
} else {
maps.Copy(entDB, ents)
}
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning SystemOS")
if ents, err := scanEnts(conf.IPSW, systemOS, "SystemOS", conf.PemDB); err != nil {
return nil, fmt.Errorf("failed to scan files in SystemOS %s: %v", systemOS, err)
} else {
maps.Copy(entDB, ents)
}
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning FileSystem")
if ents, err := scanEnts(conf.IPSW, fsOS, "filesystem", conf.PemDB); err != nil {
return nil, fmt.Errorf("failed to scan files in FileSystem %s: %v", fsOS, err)
} else {
maps.Copy(entDB, ents)
}
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning ExclaveOS")
if ents, err := scanEnts(conf.IPSW, excOS, "ExclaveOS", conf.PemDB); err != nil {
return nil, fmt.Errorf("failed to scan files in ExclaveOS %s: %v", excOS, err)
} else {
maps.Copy(entDB, ents)
}
}
if conf.Database != "" {
if _, err := os.Stat(conf.Database); err == nil {
return loadEntitlementDatabase(conf.Database)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("failed to stat entitlement database file %s: %v", conf.Database, err)
}
}
if len(conf.Folder) > 0 {
var files []string
if err := filepath.Walk(conf.Folder, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("failed to walk mount %s: %v", conf.Folder, err)
return nil
}
if !info.IsDir() {
files = append(files, path)
}
utils.Indent(log.Info, 2)("Generating entitlement database file...")
if len(conf.IPSW) > 0 {
ents, err := collectEntitlementsFromArchive(conf.IPSW, conf.PemDB)
if err != nil {
return nil, err
}
maps.Copy(entDB, ents)
}
if len(conf.Folder) > 0 {
var files []string
if err := filepath.Walk(conf.Folder, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("failed to walk mount %s: %v", conf.Folder, err)
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk files in dir %s: %v", conf.Folder, err)
}
if !info.IsDir() {
files = append(files, path)
}
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk files in dir %s: %v", conf.Folder, err)
}
for _, file := range files {
var m *macho.File
fat, err := macho.OpenFat(file)
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(file)
if err != nil {
log.WithError(err).Warnf("failed to get entitlements for %s", file)
continue // bad macho file (skip)
}
} else {
continue // not a macho file (skip)
for _, file := range files {
var m *macho.File
fat, err := macho.OpenFat(file)
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(file)
if err != nil {
log.WithError(err).Warnf("failed to get entitlements for %s", file)
continue // bad macho file (skip)
}
}
if m.CodeSignature() != nil && len(m.CodeSignature().Entitlements) > 0 {
entDB[strings.TrimPrefix(file, conf.Folder)] = m.CodeSignature().Entitlements
} else {
entDB[strings.TrimPrefix(file, conf.Folder)] = ""
continue // not a macho file (skip)
}
}
}
if len(conf.Database) > 0 {
buff := new(bytes.Buffer)
e := gob.NewEncoder(buff)
// Encoding the map
err := e.Encode(entDB)
if err != nil {
return nil, fmt.Errorf("failed to encode entitlement db to binary: %v", err)
}
of, err := os.Create(conf.Database)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %v", conf.Database, err)
}
defer of.Close()
gzw := gzip.NewWriter(of)
defer gzw.Close()
if _, err := buff.WriteTo(gzw); err != nil {
return nil, fmt.Errorf("failed to write entitlement db to gzip file: %v", err)
if m.CodeSignature() != nil && len(m.CodeSignature().Entitlements) > 0 {
entDB[strings.TrimPrefix(file, conf.Folder)] = m.CodeSignature().Entitlements
} else {
entDB[strings.TrimPrefix(file, conf.Folder)] = ""
}
}
} else {
log.WithField("database", filepath.Base(conf.Database)).Info("Loading Entitlement DB")
}
edbFile, err := os.Open(conf.Database)
if err != nil {
return nil, fmt.Errorf("failed to open entitlement database file %s; %v", conf.Database, err)
}
defer edbFile.Close()
gzr, err := gzip.NewReader(edbFile)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
}
defer gzr.Close()
// Decoding the serialized data
if err := gob.NewDecoder(gzr).Decode(&entDB); err != nil {
return nil, fmt.Errorf("failed to decode entitlement database; %v", err)
if len(conf.Database) > 0 {
if err := saveEntitlementDatabase(conf.Database, entDB); err != nil {
return nil, err
}
}
return entDB, nil
}
func loadEntitlementDatabase(path string) (map[string]string, error) {
entDB := make(map[string]string)
log.WithField("database", filepath.Base(path)).Info("Loading Entitlement DB")
edbFile, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open entitlement database file %s; %v", path, err)
}
defer edbFile.Close()
gzr, err := gzip.NewReader(edbFile)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
}
defer gzr.Close()
if err := gob.NewDecoder(gzr).Decode(&entDB); err != nil {
return nil, fmt.Errorf("failed to decode entitlement database; %v", err)
}
return entDB, nil
}
func saveEntitlementDatabase(path string, entDB map[string]string) error {
buff := new(bytes.Buffer)
if err := gob.NewEncoder(buff).Encode(entDB); err != nil {
return fmt.Errorf("failed to encode entitlement db to binary: %v", err)
}
of, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create file %s: %v", path, err)
}
defer of.Close()
gzw := gzip.NewWriter(of)
defer gzw.Close()
if _, err := buff.WriteTo(gzw); err != nil {
return fmt.Errorf("failed to write entitlement db to gzip file: %v", err)
}
return nil
}
func collectEntitlementsFromArchive(path, pemDb string) (map[string]string, error) {
infoData, parseErr := info.Parse(path)
if parseErr == nil && infoData != nil && infoData.Plists != nil && infoData.Plists.Type != "OTA" {
return collectEntitlementsFromIPSW(path, infoData, pemDb)
}
aa, otaErr := ota.Open(path)
if otaErr != nil {
if parseErr != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v; failed to open OTA: %w", parseErr, otaErr)
}
return nil, fmt.Errorf("failed to open OTA: %w", otaErr)
}
defer aa.Close()
if _, err := aa.Info(); err != nil {
return nil, fmt.Errorf("failed to parse OTA metadata: %w", err)
}
return collectEntitlementsFromOTA(aa, pemDb)
}
func collectEntitlementsFromIPSW(ipswPath string, i *info.Info, pemDb string) (map[string]string, error) {
entDB := make(map[string]string)
if appOS, err := i.GetAppOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning AppOS")
ents, err := scanEnts(ipswPath, appOS, "AppOS", pemDb)
if err != nil {
return nil, fmt.Errorf("failed to scan files in AppOS %s: %v", appOS, err)
}
maps.Copy(entDB, ents)
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning SystemOS")
ents, err := scanEnts(ipswPath, systemOS, "SystemOS", pemDb)
if err != nil {
return nil, fmt.Errorf("failed to scan files in SystemOS %s: %v", systemOS, err)
}
maps.Copy(entDB, ents)
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning FileSystem")
ents, err := scanEnts(ipswPath, fsOS, "filesystem", pemDb)
if err != nil {
return nil, fmt.Errorf("failed to scan files in FileSystem %s: %v", fsOS, err)
}
maps.Copy(entDB, ents)
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
utils.Indent(log.Info, 3)("Scanning ExclaveOS")
ents, err := scanEnts(ipswPath, excOS, "ExclaveOS", pemDb)
if err != nil {
return nil, fmt.Errorf("failed to scan files in ExclaveOS %s: %v", excOS, err)
}
maps.Copy(entDB, ents)
}
return entDB, nil
}
func collectEntitlementsFromOTA(aa *ota.AA, pemDb string) (map[string]string, error) {
entDB := make(map[string]string)
tmpDir, err := os.MkdirTemp("", "ipsw-ota-ents")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory for OTA entitlements: %w", err)
}
defer os.RemoveAll(tmpDir)
cryptexes := []struct {
name string
label string
required bool
}{
{"system", "OTA System Cryptex", true},
{"app", "OTA App Cryptex", false},
}
for _, cx := range cryptexes {
dmgPath, err := aa.ExtractCryptex(cx.name, tmpDir)
if err != nil {
if cx.required {
return nil, fmt.Errorf("failed to extract %s: %w", cx.label, err)
}
log.WithError(err).Debugf("skipping %s", cx.label)
continue
}
ents, err := scanEntsFromDMG(dmgPath, cx.label, pemDb)
if err != nil {
if cx.required {
return nil, fmt.Errorf("failed to scan %s entitlements: %w", cx.label, err)
}
log.WithError(err).Warnf("failed to scan %s entitlements", cx.label)
continue
}
maps.Copy(entDB, ents)
}
payloadEnts, err := collectEntitlementsFromPayload(aa)
if err != nil {
return nil, err
}
maps.Copy(entDB, payloadEnts)
// NOTE: this found nothing when ran on iOS 26 IPSW vs. OTA
// looseEnts, err := collectEntitlementsFromLooseFiles(aa)
// if err != nil {
// return nil, err
// }
// maps.Copy(entDB, looseEnts)
if len(entDB) == 0 {
return nil, fmt.Errorf("no entitlements extracted from OTA payload")
}
return entDB, nil
}
func collectEntitlementsFromPayload(aa *ota.AA) (map[string]string, error) {
entDB := make(map[string]string)
pre := regexp.MustCompile(`^payload.\d+$`)
files := aa.Files()
if len(files) == 0 {
return entDB, nil
}
tmpDir, err := os.MkdirTemp("", "ipsw-ota-payload")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory for OTA payload extraction: %w", err)
}
defer os.RemoveAll(tmpDir)
for _, file := range files {
if file == nil || file.IsDir() {
continue
}
if !pre.MatchString(file.Base()) {
continue
}
rc, err := aa.Open(file.Name(), false)
if err != nil {
return nil, fmt.Errorf("failed to open OTA payload %s: %w", file.Name(), err)
}
var payloadBuf bytes.Buffer
if err := pbzx.Extract(context.Background(), rc, &payloadBuf, runtime.NumCPU()); err != nil {
rc.Close()
return nil, fmt.Errorf("failed to extract OTA payload %s: %w", file.Name(), err)
}
rc.Close()
y := &yaa.YAA{}
if err := y.Parse(bytes.NewReader(payloadBuf.Bytes())); err != nil {
return nil, fmt.Errorf("failed to parse OTA payload %s: %w", file.Name(), err)
}
payloadRoot := filepath.Join(tmpDir, file.Base())
if err := extractYAAEntries(y, payloadRoot); err != nil {
return nil, fmt.Errorf("failed to extract OTA payload %s: %w", file.Name(), err)
}
payloadEnts, err := scanEntsFromFolder(payloadRoot, "OTA payload")
if err != nil {
return nil, fmt.Errorf("failed to scan OTA payload %s: %w", file.Name(), err)
}
maps.Copy(entDB, payloadEnts)
}
return entDB, nil
}
func scanEntsFromDMG(dmgPath, dmgLabel, pemDbPath string) (map[string]string, error) {
originalPath := dmgPath
if filepath.Ext(dmgPath) == ".aea" {
var err error
dmgPath, err = aea.Decrypt(&aea.DecryptConfig{
Input: dmgPath,
Output: filepath.Dir(dmgPath),
PemDB: pemDbPath,
Insecure: false, // TODO: make insecure configurable
})
if err != nil {
return nil, fmt.Errorf("failed to parse AEA encrypted DMG %s: %v", originalPath, err)
}
defer os.Remove(dmgPath)
}
utils.Indent(log.Debug, 2)(fmt.Sprintf("Mounting %s %s", dmgLabel, dmgPath))
mountPoint, alreadyMounted, err := utils.MountDMG(dmgPath, "")
if err != nil {
return nil, fmt.Errorf("failed to mount 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, true)
}); err != nil {
utils.Indent(log.Error, 3)(fmt.Sprintf("failed to unmount %s at %s: %v", dmgPath, mountPoint, err))
}
}()
}
entDB := make(map[string]string)
if err := filepath.Walk(mountPoint, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("failed to walk mount %s: %v", mountPoint, err)
return nil
}
if info.IsDir() {
return nil
}
var m *macho.File
fat, ferr := macho.OpenFat(path)
if ferr == nil {
m = fat.Arches[len(fat.Arches)-1].File // grab last arch (probably arm64e)
} else {
if ferr == macho.ErrNotFat {
m, ferr = macho.Open(path)
if ferr != nil {
log.WithError(ferr).Warnf("failed to get entitlements for %s", path)
return nil
}
} else {
return nil // not a macho file (skip)
}
}
relPath := strings.TrimPrefix(path, mountPoint)
if m.CodeSignature() != nil && len(m.CodeSignature().Entitlements) > 0 {
entDB[relPath] = m.CodeSignature().Entitlements
} else {
entDB[relPath] = ""
}
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk files in dir %s: %v", mountPoint, err)
}
return entDB, nil
}
func collectEntitlementsFromLooseFiles(aa *ota.AA) (map[string]string, error) {
entDB := make(map[string]string)
files := aa.Files()
if len(files) == 0 {
return entDB, nil
}
payloadRE := regexp.MustCompile(`^payload.\d+$`)
cryptexRE := regexp.MustCompile(`^cryptex-`)
for _, file := range files {
if file == nil || file.IsDir() {
continue
}
base := file.Base()
if payloadRE.MatchString(base) {
continue
}
if cryptexRE.MatchString(base) {
continue
}
if strings.Contains(file.Name(), "payloadv2/") {
continue
}
rc, err := aa.Open(file.Name(), true)
if err != nil {
return nil, fmt.Errorf("failed to open OTA file %s: %w", file.Name(), err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("failed to read OTA file %s: %w", file.Name(), err)
}
ent, ok, err := entitlementsFromData(data)
if err != nil {
log.WithError(err).Warnf("failed to parse entitlements for OTA file %s", file.Name())
continue
}
if !ok {
continue
}
rel := sanitizeLoosePath(file.Name())
if _, exists := entDB[rel]; !exists {
entDB[rel] = ent
}
}
return entDB, nil
}
func scanEntsFromFolder(root, label string) (map[string]string, error) {
entDB := make(map[string]string)
if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Errorf("failed to walk %s %s: %v", label, path, err)
return nil
}
if d.IsDir() {
return nil
}
if d.Type()&os.ModeSymlink != 0 {
return nil
}
ent, ok, err := readEntitlements(path)
if err != nil {
log.WithError(err).Warnf("failed to read entitlements from %s", path)
return nil
}
if !ok {
return nil
}
rel, relErr := filepath.Rel(root, path)
if relErr != nil {
rel = d.Name()
}
rel = filepath.ToSlash(rel)
if !strings.HasPrefix(rel, "/") {
rel = "/" + rel
}
entDB[rel] = ent
return nil
}); err != nil {
return nil, fmt.Errorf("failed to scan %s folder: %w", label, err)
}
return entDB, nil
}
func readEntitlements(path string) (string, bool, error) {
if fat, err := macho.OpenFat(path); err == nil {
defer fat.Close()
for _, arch := range fat.Arches {
if arch.File == nil {
continue
}
if cs := arch.File.CodeSignature(); cs != nil {
return cs.Entitlements, true, nil
}
}
return "", true, nil
}
m, err := macho.Open(path)
if err != nil {
return "", false, nil
}
defer m.Close()
if cs := m.CodeSignature(); cs != nil {
return cs.Entitlements, true, nil
}
return "", true, nil
}
func entitlementsFromData(data []byte) (string, bool, error) {
if len(data) < 4 {
return "", false, nil
}
if fat, err := macho.NewFatFile(bytes.NewReader(data)); err == nil {
defer fat.Close()
for _, arch := range fat.Arches {
if arch.File == nil {
continue
}
if cs := arch.File.CodeSignature(); cs != nil {
return cs.Entitlements, true, nil
}
}
return "", true, nil
}
m, err := macho.NewFile(bytes.NewReader(data))
if err != nil {
return "", false, nil
}
defer m.Close()
if cs := m.CodeSignature(); cs != nil {
return cs.Entitlements, true, nil
}
return "", true, nil
}
func sanitizeLoosePath(name string) string {
clean := filepath.Clean(name)
clean = filepath.ToSlash(clean)
for strings.HasPrefix(clean, "./") {
clean = strings.TrimPrefix(clean, "./")
}
clean = strings.TrimPrefix(clean, "AssetData/")
for strings.HasPrefix(clean, "/") {
clean = strings.TrimPrefix(clean, "/")
}
if clean == "" {
return "/" + filepath.ToSlash(filepath.Base(name))
}
return "/" + clean
}
func extractYAAEntries(archive *yaa.YAA, dest string) error {
if err := os.MkdirAll(dest, 0o755); err != nil {
return fmt.Errorf("failed to create OTA payload root %s: %w", dest, err)
}
for _, entry := range archive.Entries {
cleanName := filepath.Clean(entry.Path)
if cleanName == "." || cleanName == "" {
continue
}
dstPath := filepath.Join(dest, cleanName)
rel, err := filepath.Rel(dest, dstPath)
if err != nil {
return fmt.Errorf("failed to compute relative OTA payload path for %s: %w", entry.Path, err)
}
if strings.HasPrefix(rel, "..") {
return fmt.Errorf("invalid OTA payload path traversal detected: %s", entry.Path)
}
switch entry.Type {
case yaa.Directory:
if err := os.MkdirAll(dstPath, 0o755); err != nil {
return fmt.Errorf("failed to create OTA payload directory %s: %w", dstPath, err)
}
case yaa.RegularFile:
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return fmt.Errorf("failed to prepare OTA payload directory %s: %w", filepath.Dir(dstPath), err)
}
data := make([]byte, entry.Size)
if _, err := entry.Read(data); err != nil {
return fmt.Errorf("failed to read OTA payload entry %s: %w", entry.Path, err)
}
if err := os.WriteFile(dstPath, data, 0o644); err != nil {
return fmt.Errorf("failed to write OTA payload entry %s: %w", entry.Path, err)
}
case yaa.SymbolicLink:
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return fmt.Errorf("failed to prepare OTA payload symlink dir %s: %w", filepath.Dir(dstPath), err)
}
if err := os.Symlink(entry.Link, dstPath); err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("failed to create OTA payload symlink %s: %w", entry.Path, err)
}
default:
continue
}
}
return nil
}
// DiffDatabases compares two entitlement databases and returns a diff
func DiffDatabases(db1, db2 map[string]string, conf *Config) (string, error) {
var err error
@@ -253,7 +705,7 @@ func DiffDatabases(db1, db2 map[string]string, conf *Config) (string, error) {
}
func scanEnts(ipswPath, dmgPath, dmgType, pemDbPath string) (map[string]string, error) {
// check if filesystem DMG already exists (due to previous mount command)
localPath := dmgPath
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
@@ -264,81 +716,11 @@ func scanEnts(ipswPath, dmgPath, dmgType, pemDbPath string) (map[string]string,
if len(dmgs) == 0 {
return nil, fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
defer os.Remove(dmgs[0])
localPath = dmgs[0]
defer os.Remove(localPath)
} else {
utils.Indent(log.Debug, 2)(fmt.Sprintf("Found extracted %s", dmgPath))
}
if filepath.Ext(dmgPath) == ".aea" {
var err error
dmgPath, err = aea.Decrypt(&aea.DecryptConfig{
Input: dmgPath,
Output: filepath.Dir(dmgPath),
PemDB: pemDbPath,
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.Debug, 2)(fmt.Sprintf("Mounting %s %s", dmgType, dmgPath))
mountPoint, alreadyMounted, err := utils.MountDMG(dmgPath, "")
if err != nil {
return nil, fmt.Errorf("failed to mount 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, true)
}); err != nil {
utils.Indent(log.Error, 3)(fmt.Sprintf("failed to unmount %s at %s: %v", dmgPath, mountPoint, err))
}
}()
}
var files []string
if err := filepath.Walk(mountPoint, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("failed to walk mount %s: %v", mountPoint, err)
return nil
}
if !info.IsDir() {
files = append(files, path)
}
return nil
}); err != nil {
return nil, fmt.Errorf("failed to walk files in dir %s: %v", mountPoint, err)
}
entDB := make(map[string]string)
for _, file := range files {
var m *macho.File
fat, err := macho.OpenFat(file)
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(file)
if err != nil {
log.WithError(err).Warnf("failed to get entitlements for %s", file)
continue // bad macho file (skip)
}
} else {
continue // not a macho file (skip)
}
}
if m.CodeSignature() != nil && len(m.CodeSignature().Entitlements) > 0 {
entDB[strings.TrimPrefix(file, mountPoint)] = m.CodeSignature().Entitlements
} else {
entDB[strings.TrimPrefix(file, mountPoint)] = ""
}
}
return entDB, nil
return scanEntsFromDMG(localPath, dmgType, pemDbPath)
}
+168 -73
View File
@@ -32,10 +32,29 @@ import (
"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/signature"
"golang.org/x/exp/maps"
)
type InputKind int
const (
InputKindIPSW InputKind = iota
InputKindOTA
)
func (k InputKind) String() string {
switch k {
case InputKindOTA:
return "ota"
case InputKindIPSW:
fallthrough
default:
return "ipsw"
}
}
type kernel struct {
Path string
Version *kernelcache.Version
@@ -93,6 +112,7 @@ type Context struct {
Version string
Build string
Folder string
Kind InputKind
Mount map[string]mount
SystemOsDmgPath string
MountPath string
@@ -206,37 +226,82 @@ func (d *Diff) Save() error {
return nil
}
func (d *Diff) populateContext(ctx *Context, label string) error {
ctx.Kind = InputKindIPSW
meta, parseErr := info.Parse(ctx.IPSWPath)
if parseErr == nil && meta != nil && meta.Plists != nil && meta.Plists.Type != "OTA" {
ctx.Info = meta
} else {
aa, otaErr := ota.Open(ctx.IPSWPath)
if otaErr != nil {
if parseErr != nil {
return fmt.Errorf("failed to parse '%s' IPSW: %v; failed to open OTA: %w", strings.ToLower(label), parseErr, otaErr)
}
return fmt.Errorf("failed to open '%s' OTA: %w", strings.ToLower(label), otaErr)
}
defer aa.Close()
otaInfo, err := aa.Info()
if err != nil {
return fmt.Errorf("failed to parse '%s' OTA metadata: %w", strings.ToLower(label), err)
}
ctx.Info = otaInfo
ctx.Kind = InputKindOTA
}
if ctx.Info == nil || ctx.Info.Plists == nil {
return fmt.Errorf("missing metadata for %s firmware", strings.ToLower(label))
}
if ctx.Info.Plists.BuildManifest != nil {
ctx.Version = ctx.Info.Plists.BuildManifest.ProductVersion
ctx.Build = ctx.Info.Plists.BuildManifest.ProductBuildVersion
} else if ctx.Info.Plists.AssetDataInfo != nil {
ctx.Version = ctx.Info.Plists.AssetDataInfo.ProductVersion
ctx.Build = ctx.Info.Plists.AssetDataInfo.Build
} else if ctx.Info.Plists.SystemVersion != nil {
ctx.Version = ctx.Info.Plists.SystemVersion.ProductVersion
ctx.Build = ctx.Info.Plists.SystemVersion.ProductBuildVersion
}
if folder, err := ctx.Info.GetFolder(); err == nil {
ctx.Folder = filepath.Join(d.tmpDir, folder)
} else {
log.Errorf("failed to get folder from '%s' firmware metadata: %v", strings.ToLower(label), err)
}
if ctx.Info.IsMacOS() {
ctx.IsMacOS = true
}
return nil
}
func (d *Diff) getInfo() (err error) {
d.Old.Info, err = info.Parse(d.Old.IPSWPath)
if err != nil {
return fmt.Errorf("failed to parse 'Old' IPSW: %v", err)
if err := d.populateContext(&d.Old, "Old"); err != nil {
return err
}
d.New.Info, err = info.Parse(d.New.IPSWPath)
if err != nil {
return fmt.Errorf("failed to parse 'New' IPSW: %v", err)
if err := d.populateContext(&d.New, "New"); err != nil {
return err
}
d.Old.Version = d.Old.Info.Plists.BuildManifest.ProductVersion
d.Old.Build = d.Old.Info.Plists.BuildManifest.ProductBuildVersion
d.Old.Folder, err = d.Old.Info.GetFolder()
if err != nil {
log.Errorf("failed to get folder from 'Old' IPSW metadata: %v", err)
}
d.Old.Folder = filepath.Join(d.tmpDir, d.Old.Folder)
d.New.Version = d.New.Info.Plists.BuildManifest.ProductVersion
d.New.Build = d.New.Info.Plists.BuildManifest.ProductBuildVersion
d.New.Folder, err = d.New.Info.GetFolder()
if err != nil {
log.Errorf("failed to get folder from 'New' IPSW metadata: %v", err)
}
d.New.Folder = filepath.Join(d.tmpDir, d.New.Folder)
if d.Title == "" {
d.Title = fmt.Sprintf("%s (%s) .vs %s (%s)", d.Old.Version, d.Old.Build, d.New.Version, d.New.Build)
fallback := func(val, def string) string {
if val == "" {
return def
}
return val
}
d.Title = fmt.Sprintf("%s (%s) .vs %s (%s)",
fallback(d.Old.Version, "unknown"),
fallback(d.Old.Build, "unknown"),
fallback(d.New.Version, "unknown"),
fallback(d.New.Build, "unknown"),
)
}
if d.Old.Info.IsMacOS() || d.New.Info.IsMacOS() {
if d.Old.IsMacOS || d.New.IsMacOS {
d.Old.IsMacOS = true
d.New.IsMacOS = true
}
@@ -244,6 +309,30 @@ func (d *Diff) getInfo() (err error) {
return nil
}
func (d *Diff) logIPSWOnlySectionsSkipped() {
skipped := []string{"kernelcache", "dyld_shared_cache", "machos"}
if len(d.conf.KDKs) == 2 {
skipped = append(skipped, "kdks")
}
if d.conf.LaunchD {
skipped = append(skipped, "launchd")
}
if d.conf.Firmware {
skipped = append(skipped, "firmware", "iboot")
}
if d.conf.Features {
skipped = append(skipped, "feature-flags")
}
if d.conf.Files {
skipped = append(skipped, "files")
}
log.WithFields(log.Fields{
"old": d.Old.Kind.String(),
"new": d.New.Kind.String(),
}).Warnf("Skipping %s diff sections; OTA support is currently limited to --ent", strings.Join(skipped, ", "))
}
// Diff diffs the diff
func (d *Diff) Diff() (err error) {
@@ -257,63 +346,69 @@ func (d *Diff) Diff() (err error) {
return err
}
log.Info("Diffing KERNELCACHES")
if err := d.parseKernelcache(); err != nil {
log.WithError(err).Error("failed to parse kernelcaches")
}
bothIPSW := d.Old.Kind == InputKindIPSW && d.New.Kind == InputKindIPSW
if d.Old.KDK != "" && d.New.KDK != "" {
log.Info("Diffing KDKS")
if err := d.parseKDKs(); err != nil {
log.WithError(err).Error("failed to parse KDKs")
if bothIPSW {
log.Info("Diffing KERNELCACHES")
if err := d.parseKernelcache(); err != nil {
log.WithError(err).Error("failed to parse kernelcaches")
}
}
log.Info("Diffing DYLD_SHARED_CACHES")
if err := d.mountSystemOsDMGs(); err != nil {
return fmt.Errorf("failed to mount DMGs: %v", err)
}
defer d.unmountSystemOsDMGs()
if err := d.parseDSC(); err != nil {
log.WithError(err).Error("failed to parse DSCs")
}
log.Info("Diffing MachOs")
if err := d.parseMachos(); err != nil {
log.WithError(err).Error("failed to parse MachOs")
}
if d.conf.LaunchD {
log.Info("Diffing launchd PLIST")
if err := d.parseLaunchdPlists(); err != nil {
log.WithError(err).Error("failed to parse launchd plists")
if d.Old.KDK != "" && d.New.KDK != "" {
log.Info("Diffing KDKS")
if err := d.parseKDKs(); err != nil {
log.WithError(err).Error("failed to parse KDKs")
}
}
}
if d.conf.Firmware {
log.Info("Diffing Firmware")
if err := d.parseFirmwares(); err != nil {
log.WithError(err).Error("failed to parse firmwares")
log.Info("Diffing DYLD_SHARED_CACHES")
if err := d.mountSystemOsDMGs(); err != nil {
return fmt.Errorf("failed to mount DMGs: %v", err)
}
log.Info("Diffing iBoot")
if err := d.parseIBoot(); err != nil {
log.WithError(err).Error("failed to parse iBoot")
}
}
defer d.unmountSystemOsDMGs()
if d.conf.Features {
log.Info("Diffing Feature Flags")
if err := d.parseFeatureFlags(); err != nil {
log.WithError(err).Error("failed to parse feature flags")
if err := d.parseDSC(); err != nil {
log.WithError(err).Error("failed to parse DSCs")
}
}
if d.conf.Files {
log.Info("Diffing Files")
if err := d.parseFiles(); err != nil {
log.WithError(err).Error("failed to parse files")
log.Info("Diffing MachOs")
if err := d.parseMachos(); err != nil {
log.WithError(err).Error("failed to parse MachOs")
}
if d.conf.LaunchD {
log.Info("Diffing launchd PLIST")
if err := d.parseLaunchdPlists(); err != nil {
log.WithError(err).Error("failed to parse launchd plists")
}
}
if d.conf.Firmware {
log.Info("Diffing Firmware")
if err := d.parseFirmwares(); err != nil {
log.WithError(err).Error("failed to parse firmwares")
}
log.Info("Diffing iBoot")
if err := d.parseIBoot(); err != nil {
log.WithError(err).Error("failed to parse iBoot")
}
}
if d.conf.Features {
log.Info("Diffing Feature Flags")
if err := d.parseFeatureFlags(); err != nil {
log.WithError(err).Error("failed to parse feature flags")
}
}
if d.conf.Files {
log.Info("Diffing Files")
if err := d.parseFiles(); err != nil {
log.WithError(err).Error("failed to parse files")
}
}
} else {
d.logIPSWOnlySectionsSkipped()
}
if d.conf.Entitlements {
@@ -640,12 +735,12 @@ func (d *Diff) parseDSC() error {
}
func (d *Diff) parseEntitlements() (string, error) {
oldDB, err := ent.GetDatabase(&ent.Config{IPSW: d.Old.IPSWPath})
oldDB, err := ent.GetDatabase(&ent.Config{IPSW: d.Old.IPSWPath, PemDB: d.conf.PemDB})
if err != nil {
return "", err
}
newDB, err := ent.GetDatabase(&ent.Config{IPSW: d.New.IPSWPath})
newDB, err := ent.GetDatabase(&ent.Config{IPSW: d.New.IPSWPath, PemDB: d.conf.PemDB})
if err != nil {
return "", err
}