├── README.md └── mac_location_scraper.py /README.md: -------------------------------------------------------------------------------- 1 | # Mac Locations Scraper 2 | 3 | Dump the contents of the location database files on iOS and macOS. 4 | 5 | iOS: 6 | === 7 | /private/var/root/Library/Caches/locationd/ 8 | * cache_encryptedA.db 9 | * lockCache_encryptedA.db 10 | * cache_encryptedB.db 11 | 12 | /private/var/mobile/Library/Caches/com.apple.routined/ 13 | * cache_encryptedB.db 14 | * CoreRoutine.sqlite (iOS 10) 15 | 16 | macOS: 17 | === 18 | /var/folders/zz/zyxvpxvq6csfxvn_n00000sm00006d/C/ 19 | * cache_encryptedA.db 20 | * lockCache_encryptedA.db 21 | 22 | Usage: 23 | === 24 | `python mac_locations_scraper.py -output {k, c, e} ` 25 | 26 | Output Options: 27 | === 28 | * k - KML 29 | * c - CSV 30 | * e - Everything (KML & CSV) 31 | 32 | Requirements: 33 | === 34 | SimpleKML - https://simplekml.readthedocs.io/en/latest/ 35 | 36 | Related Information: 37 | === 38 | http://www.mac4n6.com/ 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /mac_location_scraper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Copyright (c) 2018, Station X Labs, LLC 4 | All rights reserved. 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the Station X Labs, LLC nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL STATION X LABS, LLC BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | ''' 26 | 27 | import simplekml 28 | import sqlite3 29 | from time import gmtime, localtime, strftime 30 | import csv 31 | import sys 32 | import argparse 33 | from argparse import RawTextHelpFormatter 34 | import os 35 | 36 | def getTablesNColumns(): 37 | global tables 38 | global columns 39 | 40 | tables = [] 41 | 42 | tables = cur.execute('SELECT name FROM sqlite_master').fetchall() 43 | 44 | for table in tables: 45 | columns = [] 46 | cur.execute('PRAGMA TABLE_INFO({})'.format(table[0])) 47 | columns = [tup[1] for tup in cur.fetchall()] 48 | 49 | for item in columns: 50 | if "latitude" in item.lower(): 51 | 52 | extractLocations(table[0]) 53 | 54 | def extractLocations(table): 55 | 56 | global folder_name 57 | folder_name = "" 58 | if output_type == 'k' or output_type == 'e': 59 | dir_name = f + " - " + table 60 | folder_name = kml.newfolder(name=dir_name) 61 | 62 | try: 63 | sql = "select * from " + table 64 | cur.execute(sql) 65 | 66 | rows = cur.fetchall() 67 | 68 | for row in rows: 69 | 70 | col_row = dict(zip(columns,row)) 71 | 72 | global data_stuff 73 | data_stuff = "" 74 | 75 | for k,v in col_row.iteritems(): 76 | data = str(k) + ": " + str(v) + " " 77 | data_stuff = data_stuff + str(data).encode("utf8") 78 | 79 | data_stuff = data_stuff.replace("\n"," ") 80 | data_stuff = data_stuff.replace(",", "_") 81 | 82 | try: 83 | if row["Timestamp"]: 84 | timestamp = row["Timestamp"] + 978307200 85 | timestamp_formatted = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(timestamp)) 86 | 87 | if output_type == 'c' or output_type == 'e': 88 | loccsv.writerow([f,table,timestamp_formatted, str(row["Latitude"]), str(row["Longitude"]), data_stuff]) 89 | 90 | if output_type == 'k' or output_type == 'e': 91 | point = folder_name.newpoint(name=timestamp_formatted) 92 | point.description = ("Original Database Tuple Data: " + data_stuff) 93 | point.coords = [(row["Longitude"],row["Latitude"])] 94 | point.style.iconstyle.color = simplekml.Color.red 95 | point.style.labelstyle.scale = 0.5 96 | point.timestamp.when = timestamp_formatted 97 | except: 98 | pass 99 | 100 | try: 101 | if row["ZTIMESTAMP"]: 102 | timestamp = row["ZTIMESTAMP"] + 978307200 103 | timestamp_formatted = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(timestamp)) 104 | 105 | if output_type == 'c' or output_type == 'e': 106 | loccsv.writerow([f,table,timestamp_formatted, str(row["ZLATITUDE"]), str(row["ZLONGITUDE"]), data_stuff]) 107 | 108 | if output_type == 'k' or output_type == 'e': 109 | point = folder_name.newpoint(name=timestamp_formatted) 110 | point.description = ("Original Database Tuple Data: " + data_stuff) 111 | point.coords = [(row["ZLONGITUDE"],row["ZLATITUDE"])] 112 | point.style.iconstyle.color = simplekml.Color.red 113 | point.style.labelstyle.scale = 0.5 114 | point.timestamp.when = timestamp_formatted 115 | except: 116 | pass 117 | 118 | try: 119 | if row["ZDATE"]: 120 | timestamp = row["ZDATE"] + 978307200 121 | timestamp_formatted = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(timestamp)) 122 | 123 | if output_type == 'c' or output_type == 'e': 124 | if row["ZLATITUDE"]: 125 | loccsv.writerow([f,table,timestamp_formatted, str(row["ZLATITUDE"]), str(row["ZLONGITUDE"]), data_stuff]) 126 | elif rowrow["ZLOCLATITUDE"]: 127 | loccsv.writerow([f,table,timestamp_formatted, str(row["ZLOCLATITUDE"]), str(row["ZLOCLONGITUDE"]), data_stuff]) 128 | 129 | if output_type == 'k' or output_type == 'e': 130 | if row["ZLATITUDE"]: 131 | point = folder_name.newpoint(name=timestamp_formatted) 132 | point.description = ("Original Database Tuple Data: " + data_stuff) 133 | point.coords = [(row["ZLONGITUDE"],row["ZLATITUDE"])] 134 | point.style.iconstyle.color = simplekml.Color.red 135 | point.style.labelstyle.scale = 0.5 136 | point.timestamp.when = timestamp_formatted 137 | elif row["ZLOCLATITUDE"]: 138 | point = folder_name.newpoint(name=timestamp_formatted) 139 | point.description = ("Original Database Tuple Data: " + data_stuff) 140 | point.coords = [(row["ZLOCLONGITUDE"],row["ZLOCLATITUDE"])] 141 | point.style.iconstyle.color = simplekml.Color.red 142 | point.style.labelstyle.scale = 0.5 143 | point.timestamp.when = timestamp_formatted 144 | except: 145 | pass 146 | 147 | except: 148 | pass 149 | 150 | if __name__ == "__main__": 151 | 152 | parser = argparse.ArgumentParser(description="\ 153 | Extract locations from iOS and macOS databases into CSV and KML formats.\ 154 | \n\n\tiOS Location Databases: \ 155 | \n\t/private/var/root/Library/Caches/locationd/\ 156 | \n\t\t- cache_encryptedA.db\ 157 | \n\t\t- lockCache_encryptedA.db\ 158 | \n\t\t- cache_encryptedB.db\ 159 | \n\t/private/var/mobile/Library/Caches/com.apple.routined/\ 160 | \n\t\t- cache_encryptedB.db\ 161 | \n\t\t- CoreRoutine.sqlite (iOS 10)\ 162 | \n\n\tmacOS Location Databases: \ 163 | \n\t/var/folders/zz/zyxvpxvq6csfxvn_n00000sm00006d/C/\ 164 | \n\t\t- cache_encryptedA.db\ 165 | \n\t\t- lockCache_encryptedA.db\ 166 | \n\n\tVersion: 1.2\ 167 | \n\tUpdated: 08/07/2018\ 168 | \n\tAuthor: Sarah Edwards | @iamevltwin | mac4n6.com | oompa@csh.rit.edu" 169 | , prog='mac_locations_scraper.py' 170 | , formatter_class=RawTextHelpFormatter) 171 | parser.add_argument('-output', choices=['k','c','e'], action="store", help="k=KML, c=CSV, e=EVERTHING") 172 | parser.add_argument('directory_of_dbs') 173 | 174 | args = parser.parse_args() 175 | 176 | global output_type 177 | output_type = None 178 | 179 | global csvfile 180 | global loccsv 181 | global kml 182 | 183 | locdir = args.directory_of_dbs 184 | 185 | if args.output == 'c' or args.output == 'e': 186 | output_type = 'c' 187 | 188 | with open('mac_locations_scraped.csv', 'wb') as csvfile: 189 | loccsv = csv.writer(csvfile, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) 190 | loccsv.writerow(['Database','Table','Timestamp (GMT)', 'Latitude', 'Longitude', 'Original Database Tuple Data']) 191 | 192 | for root, dirs, filenames in os.walk(locdir): 193 | for f in filenames: 194 | if f.endswith(".db") or f.endswith(".sqlite"): 195 | print "Scraping locations for CSV from: " + f 196 | db = os.path.join(root,f) 197 | conn = sqlite3.connect(db) 198 | with conn: 199 | conn.row_factory = sqlite3.Row 200 | cur = conn.cursor() 201 | getTablesNColumns() 202 | print "...Locations are being saved in the CSV file mac_locations_scraped.csv" 203 | 204 | if args.output == 'k' or args.output =='e': 205 | output_type = 'k' 206 | kml = simplekml.Kml() 207 | 208 | for root, dirs, filenames in os.walk(locdir): 209 | for f in filenames: 210 | if f.endswith(".db") or f.endswith(".sqlite"): 211 | print "Scraping locations for KML from: " + f 212 | db = os.path.join(root,f) 213 | conn = sqlite3.connect(db) 214 | with conn: 215 | conn.row_factory = sqlite3.Row 216 | cur = conn.cursor() 217 | getTablesNColumns() 218 | 219 | kml.save("mac_locations_scraped.kml") 220 | print "...Locations are being saved in the KML file mac_locations_scraped.kml" 221 | --------------------------------------------------------------------------------