├── .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 |
--------------------------------------------------------------------------------