feat: add --feat flag to ipsw diff to diff Feature Flags

This commit is contained in:
blacktop
2024-07-07 12:16:10 -06:00
parent 5cce056e0c
commit 7f91c09a1f
5 changed files with 318 additions and 48 deletions
+3
View File
@@ -44,6 +44,7 @@ func init() {
diffCmd.Flags().StringArrayP("kdk", "k", []string{}, "Path to KDKs to diff")
diffCmd.Flags().Bool("launchd", false, "Diff launchd configs")
diffCmd.Flags().Bool("fw", false, "Diff other firmwares")
diffCmd.Flags().Bool("feat", false, "Diff feature flags")
diffCmd.Flags().StringSliceP("filter", "f", []string{}, "Filter MachO sections to diff (e.g. __TEXT.__text)")
diffCmd.Flags().StringP("output", "o", "", "Folder to save diff output")
diffCmd.MarkFlagDirname("output")
@@ -56,6 +57,7 @@ func init() {
viper.BindPFlag("diff.kdk", diffCmd.Flags().Lookup("kdk"))
viper.BindPFlag("diff.launchd", diffCmd.Flags().Lookup("launchd"))
viper.BindPFlag("diff.fw", diffCmd.Flags().Lookup("fw"))
viper.BindPFlag("diff.feat", diffCmd.Flags().Lookup("feat"))
viper.BindPFlag("diff.filter", diffCmd.Flags().Lookup("filter"))
viper.BindPFlag("diff.output", diffCmd.Flags().Lookup("output"))
}
@@ -115,6 +117,7 @@ var diffCmd = &cobra.Command{
KDKs: viper.GetStringSlice("diff.kdk"),
LaunchD: viper.GetBool("diff.launchd"),
Firmware: viper.GetBool("diff.fw"),
Features: viper.GetBool("diff.feat"),
Filter: viper.GetStringSlice("diff.filter"),
Output: viper.GetString("diff.output"),
})
+3 -3
View File
@@ -432,15 +432,15 @@ func PatchMachoMod(m *macho.File, machoPath, loadCommand string, args []string)
}
platform, err := types.GetPlatformByName(args[2])
if err != nil {
return fmt.Errorf("failed to parse platform name %s: %v", args[2], err)
return fmt.Errorf("failed to parse platform name '%s': %v", args[2], err)
}
var minos types.Version
if err := minos.Set(args[3]); err != nil {
return fmt.Errorf("failed to parse min OS versionc: %v", args[3], err)
return fmt.Errorf("failed to parse min OS version '%s': %v", args[3], err)
}
var sdk types.Version
if err := sdk.Set(args[4]); err != nil {
return fmt.Errorf("failed to parse SDK version: %v", args[4], err)
return fmt.Errorf("failed to parse SDK version '%s': %v", args[4], err)
}
var tools []types.BuildVersionTool
if len(args) > 5 {
+93
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
@@ -21,6 +22,7 @@ import (
"github.com/blacktop/ipsw/internal/commands/extract"
kcmd "github.com/blacktop/ipsw/internal/commands/kernel"
mcmd "github.com/blacktop/ipsw/internal/commands/macho"
"github.com/blacktop/ipsw/internal/search"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/aea"
"github.com/blacktop/ipsw/pkg/dyld"
@@ -40,6 +42,12 @@ type mount struct {
IsMounted bool
}
type PlistDiff struct {
New []string `json:"new,omitempty"`
Removed []string `json:"removed,omitempty"`
Updated map[string]string `json:"changed,omitempty"`
}
type Config struct {
Title string
IpswOld string
@@ -47,6 +55,7 @@ type Config struct {
KDKs []string
LaunchD bool
Firmware bool
Features bool
Filter []string
Output string
}
@@ -84,6 +93,7 @@ type Diff struct {
Machos *mcmd.MachoDiff `json:"machos,omitempty"`
Firmwares *mcmd.MachoDiff `json:"firmwares,omitempty"`
Launchd string `json:"launchd,omitempty"`
Features *PlistDiff `json:"features,omitempty"`
tmpDir string `json:"-"`
conf *Config
@@ -254,6 +264,13 @@ func (d *Diff) Diff() (err error) {
}
}
if d.conf.Features {
log.Info("Diffing Feature Flags")
if err := d.parseFeatureFlags(); err != nil {
return err
}
}
log.Info("Diffing ENTITLEMENTS")
d.Ents, err = d.parseEntitlements()
if err != nil {
@@ -555,3 +572,79 @@ func (d *Diff) parseFirmwares() (err error) {
})
return
}
func (d *Diff) parseFeatureFlags() (err error) {
d.Features = &PlistDiff{
Updated: make(map[string]string),
}
conf := &mcmd.DiffConfig{
Markdown: true,
Color: false,
DiffTool: "git",
}
oldPlists := make(map[string]string)
if err := search.ForEachPlistInIPSW(d.Old.IPSWPath, "/System/Library/FeatureFlags", func(path string, content string) error {
oldPlists[path] = content
return nil
}); err != nil {
return err
}
var prevFiles []string
for f := range oldPlists {
prevFiles = append(prevFiles, f)
}
slices.Sort(prevFiles)
newPlists := make(map[string]string)
if err := search.ForEachPlistInIPSW(d.New.IPSWPath, "/System/Library/FeatureFlags", func(path string, content string) error {
newPlists[path] = content
return nil
}); err != nil {
return err
}
var nextFiles []string
for f := range newPlists {
nextFiles = append(nextFiles, f)
}
slices.Sort(nextFiles)
/* DIFF IPSW */
d.Features.New = utils.Difference(nextFiles, prevFiles)
d.Features.Removed = utils.Difference(prevFiles, nextFiles)
// gc
prevFiles = []string{}
for _, f2 := range nextFiles {
dat2 := newPlists[f2]
if dat1, ok := oldPlists[f2]; ok {
if strings.EqualFold(dat2, dat1) {
continue
}
var out string
if conf.Markdown {
out, err = utils.GitDiff(dat1+"\n", dat2+"\n", &utils.GitDiffConfig{Color: conf.Color, Tool: conf.DiffTool})
if err != nil {
return err
}
} else {
out, err = utils.GitDiff(dat1+"\n", dat2+"\n", &utils.GitDiffConfig{Color: conf.Color, Tool: conf.DiffTool})
if err != nil {
return err
}
}
if len(out) == 0 { // no diff
continue
}
if conf.Markdown {
d.Features.Updated[f2] = "```diff\n" + out + "\n```\n"
} else {
d.Features.Updated[f2] = out
}
}
}
return nil
}
+89 -16
View File
@@ -35,22 +35,24 @@ func (d *Diff) Markdown() error {
)
// SECTION: Kernel
out.WriteString(
fmt.Sprintf(
"## Kernel\n\n"+
"### Version\n\n"+
"| iOS | Version | Build | Date |\n"+
"| :-- | :------ | :---- | :--- |\n"+
"| %s *(%s)* | %s | %s | %s |\n"+
"| %s *(%s)* | %s | %s | %s |\n\n",
d.Old.Version, d.Old.Build,
d.Old.Kernel.Version.KernelVersion.Darwin, d.Old.Kernel.Version.KernelVersion.XNU,
d.Old.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
d.New.Version, d.New.Build,
d.New.Kernel.Version.KernelVersion.Darwin, d.New.Kernel.Version.KernelVersion.XNU,
d.New.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
),
)
if d.Old.Kernel.Version != nil && d.New.Kernel.Version != nil {
out.WriteString(
fmt.Sprintf(
"## Kernel\n\n"+
"### Version\n\n"+
"| iOS | Version | Build | Date |\n"+
"| :-- | :------ | :---- | :--- |\n"+
"| %s *(%s)* | %s | %s | %s |\n"+
"| %s *(%s)* | %s | %s | %s |\n\n",
d.Old.Version, d.Old.Build,
d.Old.Kernel.Version.KernelVersion.Darwin, d.Old.Kernel.Version.KernelVersion.XNU,
d.Old.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
d.New.Version, d.New.Build,
d.New.Kernel.Version.KernelVersion.Darwin, d.New.Kernel.Version.KernelVersion.XNU,
d.New.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
),
)
}
// SUB-SECTION: Kexts
if d.Kexts != nil && (len(d.Kexts.New) > 0 || len(d.Kexts.Removed) > 0 || len(d.Kexts.Updated) > 0) {
@@ -351,6 +353,77 @@ func (d *Diff) Markdown() error {
}
}
// SUB-SECTION: Feature Flags
if d.Features != nil && (len(d.Features.New) > 0 || len(d.Features.Removed) > 0 || len(d.Features.Updated) > 0) {
out.WriteString("### Feature Flags\n\n")
if len(d.Features.New) > 0 {
out.WriteString(fmt.Sprintf("#### 🆕 NEW (%d)\n\n", len(d.Features.New)))
if len(d.Features.New) > 30 {
out.WriteString("<details>\n" +
" <summary><i>View NEW</i></summary>\n\n")
}
for _, k := range d.Features.New {
out.WriteString(fmt.Sprintf("- `%s`\n", k))
}
if len(d.Features.New) > 30 {
out.WriteString("\n</details>\n")
}
out.WriteString("\n")
}
if len(d.Features.Removed) > 0 {
out.WriteString(fmt.Sprintf("#### ❌ Removed (%d)\n\n", len(d.Features.Removed)))
if len(d.Features.Removed) > 30 {
out.WriteString("<details>\n" +
" <summary><i>View Removed</i></summary>\n\n")
}
for _, k := range d.Features.Removed {
out.WriteString(fmt.Sprintf("- `%s`\n", k))
}
if len(d.Features.Removed) > 30 {
out.WriteString("\n</details>\n")
}
out.WriteString("\n")
}
if len(d.Features.Updated) > 0 {
out.WriteString(fmt.Sprintf("#### ⬆️ Updated (%d)\n\n", len(d.Features.Updated)))
out.WriteString("<details>\n" +
" <summary><i>View Updated</i></summary>\n\n")
if len(d.Features.Updated) < 20 {
for k, v := range d.Features.Updated {
out.WriteString(fmt.Sprintf("#### %s\n\n", filepath.Base(k)))
out.WriteString(fmt.Sprintf("> `%s`\n\n", k))
out.WriteString(fmt.Sprintf("%s\n", v))
}
} else {
if err := os.MkdirAll(filepath.Join(d.conf.Output, "Features"), 0o750); err != nil {
return err
}
keys := make([]string, 0, len(d.Features.Updated))
for k := range d.Features.Updated {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fname := filepath.Join(d.conf.Output, "Features", strings.ReplaceAll(filepath.Base(k), " ", "_")+".md")
if _, err := os.Stat(fname); os.IsExist(err) {
fname = filepath.Join(d.conf.Output, "Features", fmt.Sprintf("%s.%d.md", strings.ReplaceAll(filepath.Base(k), " ", "_"), rand.Intn(20)))
}
log.Debugf("Creating diff dylib Markdown file: %s", fname)
f, err := os.Create(fname)
if err != nil {
return fmt.Errorf("failed to create diff file: %w", err)
}
fmt.Fprintf(f, "## %s\n\n", filepath.Base(k))
fmt.Fprintf(f, "> `%s`\n\n", k)
fmt.Fprintf(f, d.Features.Updated[k])
f.Close()
out.WriteString(fmt.Sprintf("- [%s](%s)\n", k, filepath.Join("FEATURES", strings.ReplaceAll(filepath.Base(k), " ", "_")+".md")))
}
}
out.WriteString("\n</details>\n\n")
}
}
out.WriteString("## EOF\n")
// Write TOC.md
+130 -29
View File
@@ -20,7 +20,8 @@ import (
"github.com/blacktop/ipsw/pkg/info"
)
func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File) error) error {
// TODO: make this an array of handlers to perform multiple actions on each file
func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, string) error) error {
// check if filesystem DMG already exists (due to previous mount command)
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool {
@@ -62,13 +63,40 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
}
var files []string
// Use a map to keep track of visited directories to avoid infinite loops
visited := make(map[string]bool)
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", path, err)
return nil
}
if !info.IsDir() {
files = append(files, path)
if info.Mode()&os.ModeSymlink != 0 { // follow symlinks
// Resolve the symlink
if linkPath, err := filepath.EvalSymlinks(path); err == nil {
// Get the info of the target file/directory
info, err = os.Stat(linkPath)
if err != nil {
return err
}
// If it's a directory and not visited, follow it
if info.IsDir() && !visited[linkPath] {
visited[linkPath] = true
return filepath.Walk(linkPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
if subErr != nil {
return subErr
}
files = append(files, subPath)
return nil
})
}
}
} else {
if !info.IsDir() {
if !visited[path] {
visited[path] = true
files = append(files, path)
}
}
}
return nil
}); err != nil {
@@ -77,28 +105,7 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
for _, file := range files {
if err := func() error {
if ok, _ := magic.IsMachO(file); ok {
var m *macho.File
// UNIVERSAL MACHO
if fat, err := macho.OpenFat(file); err == nil {
defer fat.Close()
m = fat.Arches[len(fat.Arches)-1].File
} else { // SINGLE MACHO
if errors.Is(err, macho.ErrNotFat) {
m, err = macho.Open(file)
if err != nil {
return nil
}
defer m.Close()
} else { // NOT a macho file
return nil
}
}
if err := handler(strings.TrimPrefix(file, mountPoint), m); err != nil {
return fmt.Errorf("failed to handle macho %s: %w", file, err)
}
}
return nil
return handler(mountPoint, file)
}(); err != nil {
return err
}
@@ -109,6 +116,31 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
// ForEachMachoInIPSW walks the IPSW and calls the handler for each macho file found
func ForEachMachoInIPSW(ipswPath string, handler func(string, *macho.File) error) error {
scanMacho := func(mountPoint, machoPath string) error {
if ok, _ := magic.IsMachO(machoPath); ok {
var m *macho.File
// UNIVERSAL MACHO
if fat, err := macho.OpenFat(machoPath); err == nil {
defer fat.Close()
m = fat.Arches[len(fat.Arches)-1].File
} else { // SINGLE MACHO
if errors.Is(err, macho.ErrNotFat) {
m, err = macho.Open(machoPath)
if err != nil {
return nil
}
defer m.Close()
} else { // NOT a macho file
return nil
}
}
if err := handler(strings.TrimPrefix(machoPath, mountPoint), m); err != nil {
return fmt.Errorf("failed to handle macho %s: %w", machoPath, err)
}
}
return nil
}
i, err := info.Parse(ipswPath)
if err != nil {
return fmt.Errorf("failed to parse IPSW: %v", err)
@@ -116,25 +148,25 @@ func ForEachMachoInIPSW(ipswPath string, handler func(string, *macho.File) error
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
log.Info("Scanning filesystem")
if err := scanDmg(ipswPath, fsOS, "filesystem", handler); err != nil {
if err := scanDmg(ipswPath, fsOS, "filesystem", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in filesystem %s: %w", fsOS, err)
}
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
log.Info("Scanning SystemOS")
if err := scanDmg(ipswPath, systemOS, "SystemOS", handler); err != nil {
if err := scanDmg(ipswPath, systemOS, "SystemOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in SystemOS %s: %w", systemOS, err)
}
}
if appOS, err := i.GetAppOsDmg(); err == nil {
log.Info("Scanning AppOS")
if err := scanDmg(ipswPath, appOS, "AppOS", handler); err != nil {
if err := scanDmg(ipswPath, appOS, "AppOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in AppOS %s: %w", appOS, err)
}
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
log.Info("Scanning ExclaveOS")
if err := scanDmg(ipswPath, excOS, "ExclaveOS", handler); err != nil {
if err := scanDmg(ipswPath, excOS, "ExclaveOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in ExclaveOS %s: %w", excOS, err)
}
}
@@ -186,3 +218,72 @@ func ForEachIm4pInIPSW(ipswPath string, handler func(string, *macho.File) error)
return nil
}
func ForEachPlistInIPSW(ipswPath string, directory string, handler func(string, string) error) error {
i, err := info.Parse(ipswPath)
if err != nil {
return fmt.Errorf("failed to parse IPSW: %v", err)
}
scanPlist := func(mountPoint, plistPath string) error {
// filter to only scan a specific directory (if provided)
if directory != "" && !strings.Contains(plistPath, directory) {
return nil
}
if strings.HasSuffix(plistPath, ".plist") {
// settings := make(map[string]interface{})
data, err := os.ReadFile(plistPath)
if err != nil {
return fmt.Errorf("failed to read plist %s: %v", plistPath, err)
}
// TODO: add support for binary plists
// pdata, err := plist.MarshalIndent(data, plist.XMLFormat, " ")
// if err != nil {
// return fmt.Errorf("failed to marshal plist %s: %v", plistPath, err)
// }
// if err := plist.NewDecoder(bytes.NewReader(data)).Decode(&settings); err != nil {
// return fmt.Errorf("failed to decode plist %s: %v", plistPath, err)
// }
// jdata, err := json.MarshalIndent(settings, "", " ")
// if err != nil {
// return fmt.Errorf("failed to marshal plist %s: %v", plistPath, err)
// }
plistPath = strings.TrimPrefix(plistPath, mountPoint)
plistPath, err = filepath.Rel(directory, plistPath)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %v", plistPath, err)
}
if err := handler(strings.TrimPrefix(plistPath, mountPoint), string(data)); err != nil {
return fmt.Errorf("failed to handle plist %s: %v", plistPath, err)
}
}
return nil
}
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
log.Info("Scanning filesystem")
if err := scanDmg(ipswPath, fsOS, "filesystem", scanPlist); err != nil {
return fmt.Errorf("failed to scan files in filesystem %s: %w", fsOS, err)
}
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
log.Info("Scanning SystemOS")
if err := scanDmg(ipswPath, systemOS, "SystemOS", scanPlist); err != nil {
return fmt.Errorf("failed to scan files in SystemOS %s: %w", systemOS, err)
}
}
if appOS, err := i.GetAppOsDmg(); err == nil {
log.Info("Scanning AppOS")
if err := scanDmg(ipswPath, appOS, "AppOS", scanPlist); err != nil {
return fmt.Errorf("failed to scan files in AppOS %s: %w", appOS, err)
}
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
log.Info("Scanning ExclaveOS")
if err := scanDmg(ipswPath, excOS, "ExclaveOS", scanPlist); err != nil {
return fmt.Errorf("failed to scan files in ExclaveOS %s: %w", excOS, err)
}
}
return nil
}