diff --git a/README.md b/README.md index a9cb0e3..961fc20 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Restrictions Pin Finder +# iOS Restrictions Pin Finder [![Build Status](https://travis-ci.org/gwatts/pinfinder.svg?branch=master)](https://travis-ci.org/gwatts/pinfinder) pinfinder is a small application which attempts to to find the restrictions PIN/passcode for an iOS device by brute force examination of its iTunes backup. -It was written after the PIN was forgotten for a kid's device and wiping it +It was written after the PIN was forgotten for a kid's iPod Touch and wiping it would of been more work than writing this little program. **NOTE**: This program will **not** help you unlock a locked device - It can only help recover the restrictoins @@ -23,6 +23,9 @@ Operating-specifc instructions are below. In most cases, simply running the pro OS specific security restrictions) should deliver the right result. Take a look at the Troubleshooting section if you run into issues. +By defalut, it will print out the passcode for all devices it can find an unencrypted backup for, dispalying +the most recently backed up first. + ### Windows 1. Backup the device using iTunes on a desktop computer. @@ -60,15 +63,23 @@ Download, extract and run the binary. ``` $ ./pinfinder -Searching backup at /Users/johndoe/Library/Application\ Support/MobileSync/Backup/9afaaa65041cb570cd393b710f392c8220f2f20e -Finding PIN... FOUND! -PIN number is: 1234 (found in 761.7ms) +PIN Finder 1.3.0 +http://github.com/gwatts/pinfinder + +IOS DEVICE BACKUP TIME RESTRICTIONS PASSCODE +John Doe’s iPad Mini Nov 25, 2015 01:39 PM PST 1234 +John Doe's iPhone 6 Nov 25, 2015 12:15 PM PST 3456 +John Doe's iPhone 5S Sep 19, 2014 03:57 PM PDT No passcode found ``` + ## Troubleshooting -If you have multiple devices or backups, you can pass the exact path to the backup folder to -pinfinder, rather than have it try to find it by itself: +By default the program will look for the restrictions passcode for every device that has been +backed up, and return results of the most recently backed up first. + +You can also specify the backup directory explicitly on the command line to examine the backup +for a single device: On Mac it will be in the home directory as /Library/Application Support/MobileSync/Backup/ eg. @@ -89,8 +100,8 @@ Use whatever directory is the latest as the argument to pinfinder: $ pinfinder /Users/johndoe/Library/Application\ Support/MobileSync/Backup/51957b68226dbc9f59cb5797532afd906ba0a1f8 ``` -The program will find the plist containing the hashed version of the PIN and will then find -the PIN that matches that hash (which can then be used with your device). +The program will find the plist containing the hashed version of the passcode and will then find +the passcode that matches that hash (which can then be used with your device). It shouldn't take more than a few seconds to run. If the program fails to find the passcode for your device, and you're sure it's searching the right diff --git a/pinfinder.go b/pinfinder.go index 516eb77..e486298 100644 --- a/pinfinder.go +++ b/pinfinder.go @@ -23,7 +23,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// iOS Restrictions PIN Finder +// iOS Restrictions Passcode Finder // // This program will examine an iTunes backup folder for an iOS device and attempt // to find the PIN used for restricting permissions on the device (NOT the unlock PIN) @@ -35,7 +35,6 @@ import ( "bytes" "crypto/sha1" "encoding/base64" - "encoding/xml" "errors" "flag" "fmt" @@ -45,6 +44,7 @@ import ( "path" "path/filepath" "runtime" + "sort" "strings" "sync" "time" @@ -53,14 +53,20 @@ import ( ) const ( - maxPIN = 10000 - version = "1.2.1" + maxPIN = 10000 + version = "1.3.0" + restrictionsPlistName = "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40b" ) var ( noPause = flag.Bool("nopause", false, "Set to true to prevent the program pausing for input on completion") ) +type plist struct { + Dict plistDict `xml:"dict"` + path string +} + func isDir(p string) bool { s, err := os.Stat(p) if err != nil { @@ -69,6 +75,15 @@ func isDir(p string) bool { return s.IsDir() } +func dumpFile(fn string) { + if f, err := os.Open(fn); err != nil { + fmt.Printf("Failed to open %s: %s\n", fn, err) + } else { + defer f.Close() + io.Copy(os.Stdout, f) + } +} + // figure out where iTunes keeps its backups on the current OS func findSyncDir() (string, error) { usr, err := user.Current() @@ -95,96 +110,61 @@ func findSyncDir() (string, error) { return dir, nil } -// Fidn the latest backup folder -func findLatestBackup(backupDir string) (string, error) { - d, err := os.Open(backupDir) - if err != nil { - return "", fmt.Errorf("failed to open directory %q: %s", backupDir, err) - } - files, err := d.Readdir(10000) - if err != nil { - return "", fmt.Errorf("failed to read directory %q: %s", backupDir, err) - } - var newest string - var lastMT time.Time - - for _, fi := range files { - if mt := fi.ModTime(); mt.After(lastMT) { - lastMT = mt - newest = fi.Name() - } - } - if newest != "" { - return filepath.Join(backupDir, newest), nil - } - return "", errors.New("no backup directories found in " + backupDir) +type backup struct { + path string + info plist + restrictions plist } -type plist struct { - Path string - Keys []string `xml:"dict>key"` - Data []string `xml:"dict>data"` -} +type backups []*backup -func (p *plist) DumpTo(w io.Writer) error { - f, err := os.Open(p.Path) - if err != nil { - return fmt.Errorf("failed to dump plist data: %s", err) - } - defer f.Close() - io.Copy(w, f) - return nil +func (b backups) Len() int { return len(b) } +func (b backups) Less(i, j int) bool { + di, dj := b[i].info.Dict["Last Backup Date"].Value, b[j].info.Dict["Last Backup Date"].Value + return di < dj } +func (b backups) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func loadPlist(fn string) (*plist, error) { - var p plist - f, err := os.Open(fn) +func loadBackups(syncDir string) (backups backups, err error) { + // loop over all directories and see whether they contain an Info.plist + d, err := os.Open(syncDir) if err != nil { - return nil, err - } - defer f.Close() - if err := xml.NewDecoder(f).Decode(&p); err != nil { - return nil, err - } - p.Path = fn - return &p, nil -} - -func findRestrictions(fpath string) (*plist, error) { - d, err := os.Open(fpath) - if err != nil { - return nil, fmt.Errorf("failed to open directory %q: %s", fpath, err) + return nil, fmt.Errorf("failed to open directory %q: %s", syncDir, err) } defer d.Close() fl, err := d.Readdir(-1) if err != nil { - return nil, fmt.Errorf("failed to read directory %q: %s", fpath, err) + return nil, fmt.Errorf("failed to read directory %q: %s", syncDir, err) } - c := 0 for _, fi := range fl { - if !fi.Mode().IsRegular() { - continue - } - if size := fi.Size(); size < 300 || size > 500 { + if !fi.Mode().IsDir() { continue } - if pl, err := loadPlist(path.Join(fpath, fi.Name())); err == nil { - c++ - if len(pl.Keys) == 2 && len(pl.Data) == 2 && pl.Keys[0] == "RestrictionsPasswordKey" { - return pl, nil - } + path := filepath.Join(syncDir, fi.Name()) + if b, err := loadBackup(path); err == nil { + backups = append(backups, b) } } - if c == 0 { - return nil, errors.New("no plist files; are you sure you have the right backup directory?") + sort.Sort(sort.Reverse(backups)) + return backups, nil +} + +func loadBackup(backupDir string) (*backup, error) { + var b backup + if err := loadXML(filepath.Join(backupDir, "Info.plist"), &b.info); err != nil { + return nil, fmt.Errorf("%s is not a backup directory", backupDir) } - return nil, errors.New("could not find parental restricitons plist file - " + - "Are you sure parental restrictions were turned on when this backup was taken?") + b.info.path = filepath.Join(backupDir, "Info.plist") + if err := loadXML(filepath.Join(backupDir, restrictionsPlistName), &b.restrictions); err == nil { + b.restrictions.path = filepath.Join(backupDir, restrictionsPlistName) + } + b.path = backupDir + return &b, nil } -func parseRestrictions(pl *plist) (pw, salt []byte) { - pw, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(pl.Data[0])) - salt, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(pl.Data[1])) +func (b *backup) parseRestrictions() (pw, salt []byte) { + pw, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(b.restrictions.Dict["RestrictionsPasswordKey"].Value)) + salt, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(b.restrictions.Dict["RestrictionsPasswordSalt"].Value)) return pw, salt } @@ -262,10 +242,12 @@ func init() { } func main() { - var backupDir, syncDir string + var syncDir string var err error + var allBackups backups fmt.Println("PIN Finder", version) + fmt.Println("http://github.com/gwatts/pinfinder") flag.Parse() @@ -277,40 +259,56 @@ func main() { fmt.Println(err.Error) usage() } - backupDir, err = findLatestBackup(syncDir) + allBackups, err = loadBackups(syncDir) if err != nil { exit(101, true, err.Error()) } case 1: - backupDir = args[0] + b, err := loadBackup(args[0]) + if err != nil { + exit(101, true, err.Error()) + } + allBackups = backups{b} default: exit(102, true, "Too many arguments") } - if !isDir(backupDir) { - exit(103, true, "Directory not found: %s", backupDir) - } - - fmt.Println("Searching backup at", backupDir) - pl, err := findRestrictions(backupDir) - if err != nil { - exit(104, false, err.Error()) + fmt.Println() + fmt.Printf("%-40.40s %-25s %s\n", "IOS DEVICE", "BACKUP TIME", "RESTRICTIONS PASSCODE") + failed := make(backups, 0) + for _, b := range allBackups { + info := b.info.Dict + var backupTime string + if t, err := time.Parse(time.RFC3339, info["Last Backup Date"].Value); err != nil { + backupTime = info["Last Backup Date"].Value + } else { + backupTime = t.In(time.Local).Format("Jan _2, 2006 03:04 PM MST") + } + fmt.Printf("%-40.40s %s ", info["Display Name"].Value, backupTime) + if b.restrictions.Dict != nil { + key, salt := b.parseRestrictions() + pin, err := findPIN(key, salt) + if err != nil { + fmt.Println("Failed to find passcode") + failed = append(failed, b) + } else { + fmt.Println(pin) + } + } else { + fmt.Println("No passcode found") + } } - key, salt := parseRestrictions(pl) - - fmt.Print("Finding PIN...") - startTime := time.Now() - pin, err := findPIN(key, salt) - if err != nil { - // Failed to break the PIN; dump the plist data for debugging purposes - fmt.Fprintln(os.Stderr, err.Error()+"\n") - fmt.Fprintln(os.Stderr, "Source data file: ", pl.Path) - pl.DumpTo(os.Stderr) - exit(105, false, "") + fmt.Println() + for _, b := range failed { + fmt.Printf("Failed to find PIN for backup %s\nPlease file a bug report at https://github.com/gwatts/pinfinder/issues\n", b.path) + for _, key := range []string{"Product Name", "Product Type", "Product Version"} { + fmt.Printf("%-20s: %s\n", key, b.info.Dict[key].Value) + } + dumpFile(b.restrictions.path) + fmt.Println() } - fmt.Printf(" FOUND!\nPIN number is: %s (found in %s)\n", pin, time.Since(startTime)) exit(0, false, "") } diff --git a/pinfinder_test.go b/pinfinder_test.go index 7131b7d..359faa3 100644 --- a/pinfinder_test.go +++ b/pinfinder_test.go @@ -32,41 +32,119 @@ var ( dataPIN = "1234" ) +func mkInfo(tm, devname string) []byte { + return []byte(fmt.Sprintf(` + + + Last Backup Date + %s + Device Name + %s + + +`, tm, devname)) +} + func setupDataDir() string { tmp, err := ioutil.TempDir("", "pinfinder") if err != nil { panic("Could not create test directory: " + err.Error()) } + b1path := filepath.Join(tmp, "backup1") + b2path := filepath.Join(tmp, "backup2") + b3path := filepath.Join(tmp, "nobackup") + os.Mkdir(b1path, 0777) + os.Mkdir(b2path, 0777) + ioutil.WriteFile( - filepath.Join(tmp, "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40b"), + filepath.Join(b1path, "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40b"), []byte(pinData), 0644) ioutil.WriteFile( - filepath.Join(tmp, "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40c"), + filepath.Join(b1path, "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40c"), []byte("not a plist"), 0644) + ioutil.WriteFile( + filepath.Join(b1path, "Info.plist"), + mkInfo("2014-11-25T21:39:29Z", "device one"), + 0644) + + // no passcode for b2 + ioutil.WriteFile( + filepath.Join(b2path, "398bc9c2aeeab4cb0c12ada0f52eea12cf14f40c"), + []byte("not a plist"), + 0644) + ioutil.WriteFile( + filepath.Join(b2path, "Info.plist"), + mkInfo("2015-11-25T21:39:29Z", "device two"), + 0644) + + // b3 doesn't contain a backup at all + ioutil.WriteFile( + filepath.Join(b3path, "random file"), + []byte("not a plist"), + 0644) + return tmp } -func TestFindRestrictions(t *testing.T) { +func TestLoadBackup(t *testing.T) { tmpDir := setupDataDir() defer os.RemoveAll(tmpDir) - pl, err := findRestrictions(tmpDir) + path := filepath.Join(tmpDir, "backup1") + backup, err := loadBackup(path) if err != nil { - t.Fatal("Unexpected error", err) + t.Fatal("loadBackup failed", err) } - if pl.Keys[0] != "RestrictionsPasswordKey" { - t.Error("Incorrect plist") + if backup.path != path { + t.Errorf("Path incorrect expected=%q actual=%q", path, backup.path) + } + + if backup.info.Dict["Device Name"].Value != "device one" { + t.Errorf("Incorrect device name: %v", backup.info.Dict) + } +} + +func TestLoadBackups(t *testing.T) { + tmpDir := setupDataDir() + defer os.RemoveAll(tmpDir) + + b, err := loadBackups(tmpDir) + if err != nil { + t.Fatal("loadBackups failed", err) + } + if len(b) != 2 { + t.Fatal("Incorrect backup count", len(b)) + } + // Should of been sorted into reverse time order + if devname := b[0].info.Dict["Device Name"].Value; devname != "device two" { + t.Errorf("First entry is not device two, got %q", devname) + } + if devname := b[1].info.Dict["Device Name"].Value; devname != "device one" { + t.Errorf("Second entry is not device one, got %q", devname) } } func TestParseRestriction(t *testing.T) { - pl := &plist{ - Keys: []string{"RestrictionsPasswordKey", "RestrictionsPasswordSalt"}, - Data: []string{"ioN63+yl6OFZ4/C7xl9VejMLDi0=", "iNciDA=="}, + tmpDir := setupDataDir() + defer os.RemoveAll(tmpDir) + + path := filepath.Join(tmpDir, "backup1") + b, err := loadBackup(path) + if err != nil { + t.Fatal("Failed to load backup", err) } - key, salt := parseRestrictions(pl) + + key, salt := b.parseRestrictions() + + /* + pl := &plist{ + Keys: []string{"RestrictionsPasswordKey", "RestrictionsPasswordSalt"}, + Data: []string{"ioN63+yl6OFZ4/C7xl9VejMLDi0=", "iNciDA=="}, + } + key, salt := parseRestrictions(pl) + */ if !bytes.Equal(key, dataKey) { t.Error("key doesn't match") } diff --git a/plist.go b/plist.go new file mode 100644 index 0000000..1e1a717 --- /dev/null +++ b/plist.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/xml" + "io" + "os" +) + +type plistval struct { + Type string + Value string +} + +// simple helper to load plist dicts +type plistDict map[string]plistval + +func (p *plistDict) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *p = make(plistDict) + + var sval struct { + XMLName xml.Name + Value string `xml:",chardata"` + } + + var key string + for { + t, err := d.Token() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch t1 := t.(type) { + case xml.StartElement: + if err := d.DecodeElement(&sval, &t1); err != nil { + return err + } + if sval.XMLName.Local == "key" { + key = sval.Value + } else { + (*p)[key] = plistval{Type: sval.XMLName.Local, Value: sval.Value} + } + } + } +} + +func loadXML(path string, v interface{}) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + return xml.NewDecoder(f).Decode(v) +} diff --git a/plist_test.go b/plist_test.go new file mode 100644 index 0000000..be2cfbd --- /dev/null +++ b/plist_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/xml" + "reflect" + "testing" +) + +const plistTest = ` + + + + Key One + String One + Key Two + Data Two + + +` + +func TestPlist(t *testing.T) { + var a struct { + D plistDict `xml:"dict"` + } + + if err := xml.Unmarshal([]byte(plistTest), &a); err != nil { + t.Fatal("Unmarshal failed", err) + } + + expected := plistDict{ + "Key One": plistval{Type: "string", Value: "String One"}, + "Key Two": plistval{Type: "data", Value: "Data Two"}, + } + if !reflect.DeepEqual(a.D, expected) { + t.Fatal("Unexpected result ", a.D) + } +}