├── README.md ├── iCatch_v1.2.1.pyw ├── license.txt └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | 2 | # iCatch 3 | 4 | The iOS Cache Analysis for Tracking Coordinates History (iCatch) is a utility to process the iOS Cache.sqlite database and create a timelined KML map for use in Google Earth. 5 | 6 | 7 | This utility allows you to export GPS data from the iOS Cache.SQLite database, generate CSV and KMZ files, and log details for analysis. 8 | 9 | A processing log, csv file containing records of processed files (within time/date filter considerations), and KMZ file are output to a new directory at the user-selected location. 10 | 11 | 12 | ************* 13 | *All times are processed and output in Coordinated Universal Time (UTC-0)* 14 | *Due to their size, multiple KMZ files may be generated and are limited to 10,000 records each.* 15 | ************* 16 | 17 | 18 | 19 | ## Requirements 20 | 21 | To install the required Python libraries, use the command: pip install -r requirements.txt 22 | 23 | **This tool is intended to be run using Python. A Windows executable with the latest updates will be distributed from time to time.** 24 | 25 | ## Usage 26 | This tool was designed with Python and can be run as a windowless Python file or as a Windows executable file. The executable file will be updated from time to time and may not be the latest release. 27 | 28 | To open GUI, from the CLI run 29 | 30 | py iCatch_v1.X.pyw 31 | or double-click the .pyw file from your file explorer. 32 | 1. Input case information. 33 | 2. Select the path of the exported **Cache.sqlite** database which is found at: *private/var/mobile/Library/Caches/com.apple.routined/Cache.sqlite* (please ensure the shm and wal files are included in the same directory so all records are processed). 34 | 3. Select the desired color for the pin and accuracy ring. 35 | 4. Select a radius filter to limit the maximum radius of horizontal accuracy. 36 | 5. Select the Date/Time filter options. This option is enabled by default. 37 | 38 | 39 | a. It is highly recommended to use this option as the database contains tens of thousands of points, and can have thousands of points in a short timeframe. 40 | 6. Select Include Speed and Direction if these datapoints are of interest. These values include reported device speed, output in MPH, and reported "Course" direction, output with degrees and cardinal direction. Reported values that fall in a negative range are considered invalid and are noted as such. 41 | 42 | *The **Triage Dates** function will quickly display the dates that are stored within the database, along with the number of records for those dates.* 43 | 44 | ### Google Earth Pro Usage: 45 | • The KMZ file should be used with the Google Earth Pro desktop version. The file is loaded into temporary places. Each point has two items: the "Pin" and the horizontal accuracy overlay. Only the pin is loaded automatically to save system resources. 46 | 47 | • The timeline slider should be adjusted to a smaller timeframe before loading (full-checking) the loaded KMZ folder. This will ensure that not all (up to) 10,000 points and overlays are loaded at once. 48 | 49 | • To adjust the visible timeframe, use the slider points on the timeline bar, as well as the settings option. 50 | 51 | • All records are processed in UTC-0 and the settings options will allow for timezone display adjustments. 52 | 53 | ## Acknowledgements 54 | For more information about the Cache.sqlite database, including speed artifacts, check out the amazing work by Scott Koenig at: https://theforensicscooter.com/2021/09/22/iphone-device-speeds-in-cache-sqlite-zrtcllocationmo/ 55 | 56 | ## Issues 57 | I'm still trying to figure out GitHub, so if you come across an issue or have suggestions on a better way to do things, please let me know! 58 | -------------------------------------------------------------------------------- /iCatch_v1.2.1.pyw: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog, messagebox, ttk, Toplevel 3 | import sqlite3 4 | import base64 5 | import pandas as pd 6 | from io import BytesIO 7 | from PIL import Image, ImageTk 8 | from tkcalendar import DateEntry 9 | import simplekml 10 | import zipfile 11 | import math 12 | import hashlib 13 | from datetime import datetime, timedelta 14 | import os 15 | 16 | 17 | #Creates horizontal accuracy radius/circle 18 | def create_circle(lat, lon, radius, num_segments=12): # Default to 12 segments, limited to reduce size of file. 19 | coords = [] 20 | for i in range(num_segments): 21 | angle = math.radians(float(i) / num_segments * 360) 22 | dx = radius * math.cos(angle) 23 | dy = radius * math.sin(angle) 24 | 25 | d_lat = dy / 111320 # Latitude degrees per meter 26 | d_lon = dx / (111320 * math.cos(math.radians(lat))) # Longitude degrees per meter (adjusted by latitude) 27 | 28 | point_lat = lat + d_lat 29 | point_lon = lon + d_lon 30 | 31 | coords.append((point_lon, point_lat)) 32 | 33 | return coords 34 | 35 | image_base64 = """ 36 | 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 37 | """ 38 | 39 | # Function to hash files (md5, sha1) 40 | def hash_file(file_path): 41 | md5_hash = hashlib.md5() 42 | sha1_hash = hashlib.sha1() 43 | 44 | with open(file_path, 'rb') as f: 45 | for byte_block in iter(lambda: f.read(4096), b""): 46 | md5_hash.update(byte_block) 47 | sha1_hash.update(byte_block) 48 | 49 | return md5_hash.hexdigest(), sha1_hash.hexdigest() 50 | 51 | # Main function to query database and generate CSV, KMZ, and Log 52 | def generate_outputs(): 53 | org = org_var.get() 54 | examiner = examiner_var.get() 55 | case_num = case_num_var.get() 56 | device_info = device_var.get() 57 | db_path = db_var.get() 58 | output_folder = output_var.get() 59 | icon_color = cache_color_var.get() 60 | 61 | 62 | if not all([org, examiner, case_num, device_info, db_path, output_folder]): 63 | messagebox.showerror("Error", "Please fill all fields.") 64 | return 65 | 66 | if date_filter_var.get(): 67 | start_dt = start_date_var.get() + " " + start_time_var.get() 68 | end_dt = end_date_var.get() + " " + end_time_var.get() 69 | 70 | # Convert start and end to datetime objects for validation 71 | try: 72 | start_datetime = datetime.strptime(start_dt, '%Y-%m-%d %H:%M:%S') 73 | end_datetime = datetime.strptime(end_dt, '%Y-%m-%d %H:%M:%S') 74 | 75 | if end_datetime < start_datetime: 76 | messagebox.showerror("Error", "End date/time cannot be earlier than start date/time.") 77 | return 78 | except ValueError as e: 79 | messagebox.showerror("Error", f"Invalid date/time format: {e}") 80 | return 81 | 82 | conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) 83 | cursor = conn.cursor() 84 | 85 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 86 | output_dir = os.path.join(output_folder, f'iCatch Results {timestamp}') 87 | os.makedirs(output_dir, exist_ok=True) 88 | 89 | # Base SQLite query 90 | query = """ 91 | SELECT 92 | ZRTCLLOCATIONMO.Z_PK AS 'Record ID', 93 | DATETIME(978307200 + CAST(ZTIMESTAMP AS REAL), 'unixepoch') || '.' || 94 | SUBSTR(CAST((ZTIMESTAMP - CAST(ZTIMESTAMP AS INTEGER)) * 1000 AS INTEGER) + 1000, 2, 3) AS 'Timestamp', 95 | PRINTF('%.6f', ZRTCLLOCATIONMO.ZLATITUDE) AS 'Latitude', 96 | PRINTF('%.6f', ZRTCLLOCATIONMO.ZLONGITUDE) AS 'Longitude', 97 | PRINTF('%.2f', ZRTCLLOCATIONMO.ZHORIZONTALACCURACY) AS 'Horizontal Accuracy (M)' 98 | """ 99 | 100 | # Add Speed and Course columns if checkbox is selected 101 | if speed_course_var.get(): 102 | query += """, 103 | CASE 104 | WHEN ZSPEED < 0 THEN 'Invalid/No Speed' 105 | ELSE PRINTF('%.1f', ZSPEED * 2.23694) 106 | END AS "Speed (MPH)", 107 | CASE 108 | WHEN ZCOURSE < 0 THEN 'Invalid/No Course' 109 | ELSE 110 | CASE 111 | WHEN ZCOURSE >= 348.75 OR ZCOURSE < 11.25 THEN 'N' 112 | WHEN ZCOURSE >= 11.25 AND ZCOURSE < 33.75 THEN 'NNE' 113 | WHEN ZCOURSE >= 33.75 AND ZCOURSE < 56.25 THEN 'NE' 114 | WHEN ZCOURSE >= 56.25 AND ZCOURSE < 78.75 THEN 'ENE' 115 | WHEN ZCOURSE >= 78.75 AND ZCOURSE < 101.25 THEN 'E' 116 | WHEN ZCOURSE >= 101.25 AND ZCOURSE < 123.75 THEN 'ESE' 117 | WHEN ZCOURSE >= 123.75 AND ZCOURSE < 146.25 THEN 'SE' 118 | WHEN ZCOURSE >= 146.25 AND ZCOURSE < 168.75 THEN 'SSE' 119 | WHEN ZCOURSE >= 168.75 AND ZCOURSE < 191.25 THEN 'S' 120 | WHEN ZCOURSE >= 191.25 AND ZCOURSE < 213.75 THEN 'SSW' 121 | WHEN ZCOURSE >= 213.75 AND ZCOURSE < 236.25 THEN 'SW' 122 | WHEN ZCOURSE >= 236.25 AND ZCOURSE < 258.75 THEN 'WSW' 123 | WHEN ZCOURSE >= 258.75 AND ZCOURSE < 281.25 THEN 'W' 124 | WHEN ZCOURSE >= 281.25 AND ZCOURSE < 303.75 THEN 'WNW' 125 | WHEN ZCOURSE >= 303.75 AND ZCOURSE < 326.25 THEN 'NW' 126 | WHEN ZCOURSE >= 326.25 AND ZCOURSE < 348.75 THEN 'NNW' 127 | END || ' - ' || PRINTF('%.1f', ZCOURSE) || ' degrees' 128 | END AS "Course Direction" 129 | """ 130 | 131 | # Add date filter if selected 132 | if date_filter_var.get(): 133 | query += f""" 134 | FROM ZRTCLLOCATIONMO 135 | WHERE ZRTCLLOCATIONMO.ZTIMESTAMP >= strftime('%s', '{start_dt}') - strftime('%s', '2001-01-01 00:00:00') 136 | AND ZRTCLLOCATIONMO.ZTIMESTAMP <= strftime('%s', '{end_dt}') - strftime('%s', '2001-01-01 00:00:00') 137 | """ 138 | else: 139 | query += " FROM ZRTCLLOCATIONMO " 140 | 141 | query += "ORDER BY ZRTCLLOCATIONMO.ZTIMESTAMP ASC;" 142 | 143 | # Run SQLite query and save results as CSV 144 | try: 145 | df = pd.read_sql_query(query, conn) 146 | if speed_course_var.get(): 147 | if "Speed (MPH)" not in df.columns or "Course Direction" not in df.columns: 148 | messagebox.showerror("Error", "Speed and Course columns are missing from the query results.") 149 | return 150 | 151 | # Convert columns to numeric after query 152 | df['Horizontal Accuracy (M)'] = pd.to_numeric(df['Horizontal Accuracy (M)'], errors='coerce') 153 | df['Latitude'] = pd.to_numeric(df['Latitude'], errors='coerce') 154 | df['Longitude'] = pd.to_numeric(df['Longitude'], errors='coerce') 155 | 156 | csv_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_output.csv") 157 | df.to_csv(csv_file, index=False) 158 | finally: 159 | conn.close() 160 | 161 | # Get the selected accuracy limit from the dropdown 162 | accuracy_limit_selection = accuracy_limit_var.get() 163 | if accuracy_limit_selection == "No Limit": 164 | accuracy_limit = float('inf') # No limit 165 | else: 166 | accuracy_limit = float(accuracy_limit_selection) 167 | 168 | # Add a column to indicate whether the record was included in the KMZ 169 | df['Included in KMZ'] = df['Horizontal Accuracy (M)'] <= accuracy_limit 170 | 171 | csv_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_output.csv") 172 | df.to_csv(csv_file, index=False) 173 | 174 | 175 | # Split records into batches of 10,000 for KMZ files to reduce size of final file 176 | num_records = len(df) 177 | batch_size = 10000 178 | for batch_num in range(0, num_records, batch_size): 179 | batch_df = df.iloc[batch_num:batch_num+batch_size] 180 | 181 | # Filter batch based on horizontal accuracy 182 | batch_df_kmz = batch_df[batch_df['Horizontal Accuracy (M)'] <= accuracy_limit] 183 | 184 | kmz_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_part{batch_num // batch_size + 1}.kmz") 185 | 186 | # Create KMZ file for each batch 187 | create_kmz(batch_df_kmz, kmz_file, org, examiner, case_num, device_info) 188 | 189 | # Hash the input (database) file 190 | db_md5, db_sha1 = hash_file(db_path) 191 | 192 | # Generate log and hash values for CSV and KMZ files 193 | log_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_log.txt") 194 | with open(log_file, 'w') as log: 195 | log.write(f"***********************************************************************\n***********************************************************************\n** iCatch - iOS Cache Analysis for Tracking Coordinates History v1.2.1 **\n** Created by: \tAaron Willmarth, CFCE,\t\t\t\t\t\t\t\t **\n**\t\t\t\tAXYS Cyber Solutions\t\t\t\t\t\t\t\t **\n**\t\t\t\thttps://github.com/AXYS-Cyber/iCatch\t\t\t\t **\n***********************************************************************\n***********************************************************************\n\n\n") 196 | 197 | log.write(f"Date and Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n") 198 | log.write(f"Target File: {db_var.get()}\n") 199 | log.write(f"Target File Hashes: MD5: {db_md5} / SHA1: {db_sha1}\n\n") 200 | log.write(f"Output directory: {output_folder}\n\n") 201 | log.write(f"*****CASE INFORMATION*****\n") 202 | log.write(f"Organization: {org}\nExaminer: {examiner}\nCase #: {case_num}\nDevice Info: {device_info}\n**************************\n\n") 203 | # Log output options 204 | log.write(f"******OUTPUT OPTIONS******\n") 205 | if date_filter_var.get(): 206 | start_dt = start_date_var.get() + " " + start_time_var.get() 207 | end_dt = end_date_var.get() + " " + end_time_var.get() 208 | log.write(f"Date filter options:\n") 209 | log.write(f"\tStart Date/Time: {start_dt}\n") 210 | log.write(f"\tEnd Date/Time: {end_dt}\n") 211 | else: 212 | log.write(f"No date filter applied\n") 213 | 214 | if accuracy_limit_selection == "No Limit": 215 | log.write(f"Accuracy filter selection: No limit selected\n") 216 | else: 217 | log.write(f"Accuracy filter selection: {accuracy_limit_selection} meters or less\n") 218 | log.write(f"Icon Color: {icon_color}\n") 219 | log.write(f"****************************\n\n") 220 | log.write(f"Output Files:\n") 221 | 222 | if speed_course_var.get(): 223 | log.write(f"Speed and course direction included.\n") 224 | else: 225 | log.write(f"Speed and course direction not included.\n") 226 | 227 | # Loop through all files in the output directory, hash, and log them 228 | for root, dirs, files in os.walk(output_dir): 229 | for file_name in files: 230 | file_path = os.path.join(root, file_name) 231 | if file_name != os.path.basename(log_file): # Exclude log file from hashing 232 | log.write(f"File: {file_name}\n") 233 | 234 | # Calculate and log file hashes 235 | file_md5, file_sha1 = hash_file(file_path) 236 | log.write(f"\tFile hashes: MD5: {file_md5} / SHA1: {file_sha1}\n\n") 237 | else: 238 | log.write(f"File: {file_name} (Log file, not hashed)\n\n") 239 | 240 | # After writing the log file and generating the KMZs 241 | if messagebox.askyesno("Success", "Outputs generated successfully. Do you want to open the directory?"): 242 | open_directory(output_dir) 243 | 244 | def open_directory(path): 245 | """Open the directory using the default file explorer.""" 246 | try: 247 | if os.name == 'nt': # For Windows 248 | os.startfile(path) 249 | except Exception as e: 250 | messagebox.showerror("Error", f"Failed to open directory: {e}") 251 | 252 | 253 | # Function to create KMZ files 254 | def create_kmz(df, kmz_file, org, examiner, case_num, device_info): 255 | kml = simplekml.Kml() 256 | 257 | # Get the selected color from the dropdown 258 | selected_color = cache_color_var.get() 259 | 260 | # Define a mapping of colors to KML icon URLs and styles 261 | color_map = { 262 | "Red": ('http://maps.google.com/mapfiles/kml/pushpin/red-pushpin.png', simplekml.Color.red), 263 | "Green": ('http://maps.google.com/mapfiles/kml/pushpin/grn-pushpin.png', simplekml.Color.green), 264 | "Blue": ('http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png', simplekml.Color.blue), 265 | "Yellow": ('http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png', simplekml.Color.yellow), 266 | "Purple": ('http://maps.google.com/mapfiles/kml/pushpin/purple-pushpin.png', simplekml.Color.purple), 267 | } 268 | 269 | # Default values if color is not found 270 | icon_href, pin_color = color_map.get(selected_color, 271 | ('http://maps.google.com/mapfiles/kml/pushpin/red-pushpin.png', simplekml.Color.red)) 272 | 273 | 274 | # Create a KML layer with case_num and device_info in the name 275 | folder = kml.newfolder(name=f"{case_num} - {device_info} Points") 276 | folder.visibility = 0 # Set visibility to 0 to hide initially 277 | 278 | 279 | for _, row in df.iterrows(): 280 | record_number = row['Record ID'] 281 | lat = row['Latitude'] 282 | lon = row['Longitude'] 283 | start_time = pd.to_datetime(row['Timestamp']).strftime("%Y-%m-%dT%H:%M:%S.") + f"{pd.to_datetime(row['Timestamp']).microsecond // 1000:03d}Z" 284 | accuracy = row['Horizontal Accuracy (M)'] 285 | 286 | if speed_course_var.get(): 287 | speed = row['Speed (MPH)'] 288 | direction = row['Course Direction'] 289 | 290 | # Create placemark and description 291 | pnt = folder.newpoint(name=f"{record_number}", coords=[(lon, lat)]) # Add to folder, not KML directly 292 | pnt.timestamp.when = start_time 293 | 294 | # Static text (organized in a table for two columns) 295 | static_text = f""" 296 |
299 | |
301 |
302 | {org}, 303 | {examiner}, 304 | {case_num}, 305 | {device_info} 306 | |
307 |