├── .gitignore ├── README.md ├── changelog ├── general.py ├── iOSForensic.py └── package.py /.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | *.pyc 3 | *.pyo 4 | *~ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOSForensic 2 | 3 | iosForensic is a python tool to help in forensics analysis on iOS. 4 | It get files, logs, extract sqlite3 databases and uncompress .plist files in xml. 5 | 6 | 7 | ## Installation 8 | Simply clone this git repository and install dependencies. 9 | 10 | ### Dependencies 11 | 12 | #### Linux 13 | - OpenSSH 14 | - sshpass 15 | - sqlite3 16 | - python >= 2.6 17 | - [Python-magic](https://github.com/ahupp/python-magic/) 18 | - [plistutil](http://cgit.sukimashita.com/libplist.git) 19 | 20 | #### Device 21 | - a jailbroken device 22 | - OpenSSH 23 | - syslogd to /var/log/syslog (it's the name of the application, restart your phone after install) 24 | - wifi ON 25 | - on some firmware, usb connection needed 26 | 27 | ## How to use 28 | 29 | ### Options 30 | - -h --help : show help message 31 | - -a --about : show informations 32 | - -v --verbose : verbose mode 33 | - -i --ip : local ip address of the iOS terminal 34 | - -p --port : ssh port of the iOS terminal (default 22) 35 | - -P --password : root password of the iOS terminal (default alpine) 36 | 37 | ## Examples 38 | ./iOSForensic.py -i 192.168.1.10 [OPTIONS] APP_NAME.app INCOMPLETE_APP_NAME APP_NAME2_WITHOUT_DOT_APP 39 | ./iOSForensic.py -i 192.168.1.10 -p 1337 -P pwd MyApp.app angry MyApp2 40 | 41 | ## Author 42 | Written by Florian Pradines (Phonesec), this tool is a referenced OWASP iOS security project since june 2014. 43 | 44 | You can contact me via my [website](http://florianpradines.com) 45 | 46 | ## Licence 47 | This program is free software: you can redistribute it and/or modify 48 | it under the terms of the GNU General Public License as published by 49 | the Free Software Foundation, either version 3 of the License, or 50 | (at your option) any later version. 51 | 52 | This program is distributed in the hope that it will be useful, 53 | but WITHOUT ANY WARRANTY; without even the implied warranty of 54 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 55 | GNU General Public License for more details. 56 | 57 | You should have received a copy of the GNU General Public License 58 | along with this program. If not, see . 59 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | Version 1.0 (May 2013 - Initial Release) 2 | - Get application's files 3 | - Convert .plist files in XML 4 | - Extract all databases 5 | - Convert binary cookies 6 | - Get application's logs 7 | - List all packages 8 | - Find packages 9 | - Extract multiple packages 10 | -------------------------------------------------------------------------------- /general.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from subprocess import Popen, PIPE, STDOUT 5 | import sys 6 | 7 | def about(): 8 | print "####################################################" 9 | print "# @author Florian Pradines #" 10 | print "# @company Phonesec #" 11 | print "# @mail f.pradines@phonesec.com #" 12 | print "# @mail florian.pradines@owasp.org #" 13 | print "# @version 2.0 #" 14 | print "# @licence GNU GPL v3 #" 15 | print "# @dateCreation 20/05/2014 #" 16 | print "# @lastModified 23/05/2014 #" 17 | print "####################################################" 18 | print "" 19 | print "iosForensic is a python tool to help in forensics analysis on iOS." 20 | 21 | def help(): 22 | print "Usage : "+ sys.argv[0] +" [OPTIONS] APP_NAME.app INCOMPLETE_APP_NAME APP_NAME2_WITHOUT_DOT_APP" 23 | print "-h --help : show help message" 24 | print "-a --about : show informations" 25 | print "-v --verbose : verbose mode" 26 | print "-i --ip : local ip address of the iOS terminal" 27 | print "-p --port : ssh port of the iOS terminal (default 22)" 28 | print "-P --password : root password of the iOS terminal (default alpine)" 29 | print "" 30 | print "Examples" 31 | print sys.argv[0] + "-i 192.168.1.10 [OPTIONS] APP_NAME.app INCOMPLETE_APP_NAME APP_NAME2_WITHOUT_DOT_APP" 32 | print sys.argv[0] + "-i 192.168.1.10 -p 1337 -P pwd MyApp.app angry MyApp2" 33 | 34 | 35 | def printVerbose (process): 36 | while process.poll() is None: 37 | print process.stdout.readline().replace("\n", "").replace("\r", "") 38 | process.communicate() 39 | 40 | def writeResultToFile (cmd, filename, verbose): 41 | try: 42 | f = open(filename, "w") 43 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 44 | 45 | while True: 46 | line = process.stdout.readline() 47 | if not line: 48 | break 49 | 50 | f.write(line) 51 | 52 | if verbose: 53 | print line.replace("\n", "").replace("\r", "") 54 | 55 | process.communicate() 56 | f.close() 57 | 58 | return True 59 | except IOError as e: 60 | print "File " + e.filename +" not created" 61 | print "Exception : "+ e.strerror 62 | 63 | def removeDuplicates (seq): 64 | seen = set() 65 | seen_add = seen.add 66 | return [ x for x in seq if x not in seen and not seen_add(x)] 67 | -------------------------------------------------------------------------------- /iOSForensic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | #Copyright (C) <2014> 5 | 6 | #This program is free software: you can redistribute it and/or modify 7 | #it under the terms of the GNU General Public License as published by 8 | #the Free Software Foundation, either version 3 of the License, or 9 | #(at your option) any later version. 10 | # 11 | #This program is distributed in the hope that it will be useful, 12 | #but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | #GNU General Public License for more details. 15 | # 16 | #You should have received a copy of the GNU General Public License 17 | #along with this program. If not, see . 18 | 19 | from subprocess import Popen, PIPE, STDOUT 20 | import getopt 21 | import sys 22 | 23 | from general import * 24 | from package import * 25 | 26 | def main(): 27 | try: 28 | opts, args = getopt.getopt(sys.argv[1:], "ahvi:p:P:", ["about", "help", "verbose", "ip=", "port=", "password="]) 29 | except getopt.GetoptError, err: 30 | print err 31 | help() 32 | sys.exit() 33 | 34 | ip = False 35 | port = "22" 36 | password = "alpine" 37 | verbose = False 38 | 39 | #Parse options 40 | for opt, arg in opts: 41 | if opt in ("-a", "--about"): 42 | about() 43 | sys.exit() 44 | elif opt in ("-h", "--help"): 45 | help() 46 | sys.exit() 47 | elif opt in ("-v", "--verbose"): 48 | verbose = True 49 | elif opt in ("-i", "--ip"): 50 | ip = arg 51 | elif opt in ("-p", "--port"): 52 | port = str(arg) 53 | elif opt in ("-P", "--password"): 54 | password = arg 55 | 56 | if not ip: 57 | print "Error : you must give the local ip address of the device" 58 | help() 59 | sys.exit() 60 | 61 | print "Test connection to the device" 62 | cmd = "sshpass -p " + password + " ssh root@" + ip + " -p " + port + " -oStrictHostKeyChecking=no echo ok" 63 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 64 | stdout, stderr = process.communicate() 65 | if stdout.replace("\n", "").replace("\r", "")[-2:] != "ok": 66 | print "Error : " + stdout 67 | sys.exit() 68 | print "Connection successful" 69 | print "" 70 | 71 | print "Searching packages" 72 | found = [] 73 | if len(args) is 0: 74 | args.append("") 75 | 76 | for arg in args: 77 | if arg[-4:] == ".app": 78 | arg = arg[:-4] 79 | 80 | package = Package(ip, port, password, arg, verbose) 81 | justFound = package.find() 82 | 83 | if justFound: 84 | found = removeDuplicates(found + justFound) 85 | 86 | if not found: 87 | print "no packages found" 88 | sys.exit() 89 | 90 | i = 1 91 | for package in found: 92 | print str(i) +") "+ package.split("/", 1)[1].replace(".app", "") 93 | i += 1 94 | 95 | choices = raw_input("Which packages do you want extract. Ex: 1 3 6 (type 0 to quit) : ").split() 96 | if choices[0] is "0": 97 | sys.exit() 98 | 99 | packages = [] 100 | for choice in map(int,choices): 101 | if choice < 1 or choice > len(found): 102 | print str(choice) + " is not a good value" 103 | else: 104 | packages.append(found[choice - 1]) 105 | 106 | for package in packages: 107 | print "" 108 | print "" 109 | 110 | if package[-4:] == ".app": 111 | package = package[:-4] 112 | 113 | package = Package(ip, port, password, package, verbose) 114 | package.extract() 115 | 116 | if __name__ == "__main__": 117 | main () 118 | -------------------------------------------------------------------------------- /package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from subprocess import Popen, PIPE, STDOUT 5 | import os 6 | import sys 7 | import datetime 8 | import time 9 | import fnmatch 10 | import magic 11 | 12 | from general import * 13 | 14 | class Package(): 15 | def __init__(self, ip, port, password, package, verbose): 16 | self.package = package 17 | self.appname = False 18 | self.verbose = verbose 19 | self.basepath = "/var/mobile/Applications/" 20 | self.basecmd = "sshpass -p " + password + " " 21 | self.basesshcmd = self.basecmd + "ssh root@" + ip + " -p " + port + " " 22 | self.basescpcmd = self.basecmd + "scp -r -v -P " + port + " root@" + ip + ":" + self.basepath 23 | 24 | def find(self): 25 | cmd = self.basesshcmd + "find -L " + self.basepath + " -iname *" + self.package + "*.app -type d -prune" 26 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 27 | stdout, stderr = process.communicate() 28 | if stdout == "": 29 | return False 30 | else: 31 | return stdout.replace(self.basepath, "").split() 32 | 33 | def extract(self): 34 | self.appname = self.package.split("/", 1)[1].replace(".app", "") 35 | self.package = self.package.split("/", 1)[0] 36 | 37 | self.createDirectories() 38 | if self.verbose: print "" 39 | 40 | self.getDatas() 41 | if self.verbose: print "" 42 | 43 | self.getSQL() 44 | if self.verbose: print "" 45 | 46 | self.getPlist() 47 | if self.verbose: print "" 48 | 49 | self.getLogs() 50 | 51 | return True 52 | 53 | def createDirectories(self): 54 | try: 55 | self.path = "output/"+ self.appname 56 | if os.path.exists (self.path): 57 | self.path = "output/"+ self.appname +"-"+ datetime.datetime.fromtimestamp(time.time()).strftime('%Y%m%d%H%M%S') 58 | 59 | self.pathData = self.path + "/data" 60 | self.pathSQL = self.path + "/SQL" 61 | self.pathPlist = self.path + "/plist" 62 | 63 | if not self.verbose: 64 | print "Creating directories..." 65 | 66 | if self.verbose: 67 | print "Creating directory : " + self.pathData 68 | os.makedirs(self.pathData) 69 | if self.verbose: 70 | print "Creating directory : "+ self.pathSQL 71 | os.makedirs(self.pathSQL) 72 | if self.verbose: 73 | print "Creating directory : "+ self.pathPlist 74 | os.makedirs(self.pathPlist) 75 | except OSError as e: 76 | print "Folder " + e.filename +" not created" 77 | print "Exception : "+ e.strerror 78 | 79 | def getDatas(self): 80 | print "Downloading datas..." 81 | 82 | cmd = self.basescpcmd + "/" + self.package + "/* " + self.pathData 83 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 84 | if self.verbose: 85 | printVerbose (process) 86 | else: 87 | process.communicate() 88 | 89 | def getSQL(self): 90 | print "Finding databases files..." 91 | mime = magic.Magic() 92 | for root, dirnames, filenames in os.walk(self.pathData): 93 | for filename in fnmatch.filter(filenames, "*"): 94 | try: 95 | typeFile = mime.from_file(root +"/"+ filename) 96 | if typeFile is not None and typeFile.find("SQLite", 0, 6) is not -1: 97 | if self.verbose: 98 | print "Database found : "+ root +"/"+ filename 99 | 100 | os.makedirs(self.pathSQL + "/" + filename) 101 | 102 | cmd = "sqlite3 "+ root +"/"+ filename +" .tables" 103 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 104 | stdout, stderr = process.communicate() 105 | 106 | cmd = "sqlite3 "+ root +"/"+ filename 107 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE, stdin=PIPE) 108 | process.stdin.write(".headers on\n") 109 | process.stdin.write(".mode csv\n") 110 | for table in stdout.split(): 111 | if self.verbose: 112 | print "\tExtracting table : "+ table 113 | process.stdin.write(".output "+ self.pathSQL +"/"+ filename +"/"+ table +".csv\n") 114 | process.stdin.write("select * from "+ table +";\n") 115 | process.stdin.write(".quit\n") 116 | stdout, stderr = process.communicate() 117 | except IOError: 118 | continue 119 | except OSError as e: 120 | print "Folder " + e.filename +" not created" 121 | print "Exception : "+ e.strerror 122 | 123 | def getPlist(self): 124 | print "Decompressing plist files" 125 | mime = magic.Magic() 126 | for root, dirnames, filenames in os.walk(self.pathData): 127 | for filename in fnmatch.filter(filenames, "*.plist"): 128 | try: 129 | if self.verbose: 130 | print "Plist file found : "+ root +"/"+ filename 131 | 132 | if not os.path.isdir(self.pathPlist + "/" + root.replace(self.pathData, "")): 133 | os.makedirs(self.pathPlist + "/" + root.replace(self.pathData, "")) 134 | 135 | cmd = "plistutil -i " + root + "/" + filename + " -o " + self.pathPlist + "/" + root.replace(self.pathData, "") + "/" + filename 136 | process = Popen(cmd.split(), stderr=STDOUT, stdout=PIPE) 137 | stdout, stderr = process.communicate() 138 | except IOError: 139 | continue 140 | except OSError as e: 141 | print "Folder " + e.filename +" not created" 142 | print "Exception : "+ e.strerror 143 | 144 | def getLogs(self): 145 | print "Getting logs" 146 | cmd = self.basesshcmd + "grep -Ei \"" + self.appname + "\[[0-9]{1,4}\]: \" /var/log/syslog" 147 | writeResultToFile(cmd, self.path + "/logs", self.verbose) 148 | --------------------------------------------------------------------------------