Forensics dei dispositivi mobili digitali Python
Questo capitolo spiegherà la digital forensics di Python su dispositivi mobili e i concetti coinvolti.
introduzione
La mobile device forensics è quella branca della digital forensics che si occupa dell'acquisizione e dell'analisi di dispositivi mobili per il recupero di evidenze digitali di interesse investigativo. Questo ramo è diverso dall'analisi forense del computer perché i dispositivi mobili hanno un sistema di comunicazione integrato che è utile per fornire informazioni utili relative alla posizione.
Sebbene l'uso degli smartphone stia aumentando giorno dopo giorno nell'analisi forense digitale, è ancora considerato non standard a causa della sua eterogeneità. D'altra parte, l'hardware del computer, come il disco rigido, è considerato standard e sviluppato anche come disciplina stabile. Nell'industria forense digitale, c'è molto dibattito sulle tecniche utilizzate per dispositivi non standard, che hanno prove transitorie, come gli smartphone.
Artefatti estraibili da dispositivi mobili
I dispositivi mobili moderni possiedono molte informazioni digitali rispetto ai telefoni più vecchi che hanno solo un registro delle chiamate o messaggi SMS. Pertanto, i dispositivi mobili possono fornire agli investigatori molte informazioni sul suo utente. Alcuni artefatti che possono essere estratti dai dispositivi mobili sono i seguenti:
Messages - Questi sono gli artefatti utili che possono rivelare lo stato d'animo del proprietario e possono persino fornire alcune informazioni precedenti sconosciute all'investigatore.
Location History- I dati della cronologia delle posizioni sono un utile artefatto che può essere utilizzato dagli investigatori per convalidare la posizione particolare di una persona.
Applications Installed - Accedendo al tipo di applicazioni installate, l'investigatore ottiene alcune informazioni sulle abitudini e sul pensiero dell'utente mobile.
Fonti ed elaborazione delle prove in Python
Gli smartphone hanno database SQLite e file PLIST come le principali fonti di prove. In questa sezione elaboreremo i sorgenti delle evidenze in python.
Analisi dei file PLIST
Un PLIST (Property List) è un formato flessibile e conveniente per la memorizzazione dei dati delle applicazioni soprattutto sui dispositivi iPhone. Usa l'estensione.plist. Questo tipo di file viene utilizzato per archiviare informazioni su bundle e applicazioni. Può essere in due formati:XML e binary. Il seguente codice Python si aprirà e leggerà il file PLIST. Nota che prima di procedere in questo, dobbiamo creare il nostroInfo.plist file.
Innanzitutto, installa una libreria di terze parti denominata biplist dal seguente comando -
Pip install biplist
Ora importa alcune utili librerie per elaborare i file plist -
import biplist
import os
import sys
Ora, utilizzare il seguente comando sotto il metodo principale può essere utilizzato per leggere il file plist in una variabile:
def main(plist):
try:
data = biplist.readPlist(plist)
except (biplist.InvalidPlistException,biplist.NotBinaryPlistException) as e:
print("[-] Invalid PLIST file - unable to be opened by biplist")
sys.exit(1)
Ora possiamo leggere i dati sulla console o stamparli direttamente da questa variabile.
Database SQLite
SQLite funge da repository di dati principale sui dispositivi mobili. SQLite una libreria in-process che implementa un motore di database SQL autonomo, senza server, a configurazione zero e transazionale. È un database, che non è configurato, non è necessario configurarlo nel sistema, a differenza di altri database.
Se sei un principiante o non hai familiarità con i database SQLite, puoi seguire il link www.tutorialspoint.com/sqlite/index.htm Inoltre, puoi seguire il link www.tutorialspoint.com/sqlite/sqlite_python.htm nel caso lo desideri entrare nei dettagli di SQLite con Python.
Durante l'analisi forense mobile, possiamo interagire con sms.db file di un dispositivo mobile e può estrarre informazioni preziose da messagetavolo. Python ha una libreria incorporata denominatasqlite3per la connessione con il database SQLite. Puoi importare lo stesso con il seguente comando:
import sqlite3
Ora, con l'aiuto del seguente comando, possiamo connetterci al database, diciamo sms.db in caso di dispositivi mobili -
Conn = sqlite3.connect(‘sms.db’)
C = conn.cursor()
Qui, C è l'oggetto cursore con l'aiuto del quale possiamo interagire con il database.
Supponiamo ora di voler eseguire un particolare comando, diciamo di ottenere i dettagli dal file abc table, può essere fatto con l'aiuto del seguente comando:
c.execute(“Select * from abc”)
c.close()
Il risultato del comando precedente verrà memorizzato nel file cursoroggetto. Allo stesso modo possiamo usarefetchall() metodo per scaricare il risultato in una variabile che possiamo manipolare.
Possiamo usare il seguente comando per ottenere i dati dei nomi delle colonne della tabella dei messaggi in sms.db -
c.execute(“pragma table_info(message)”)
table_data = c.fetchall()
columns = [x[1] for x in table_data
Si noti che qui stiamo usando il comando SQLite PRAGMA che è un comando speciale da utilizzare per controllare varie variabili ambientali e flag di stato all'interno dell'ambiente SQLite. Nel comando precedente, il filefetchall()restituisce una tupla di risultati. Il nome di ogni colonna è memorizzato nel primo indice di ogni tupla.
Ora, con l'aiuto del seguente comando, possiamo interrogare la tabella per tutti i suoi dati e memorizzarli nella variabile denominata data_msg -
c.execute(“Select * from message”)
data_msg = c.fetchall()
Il comando precedente memorizzerà i dati nella variabile e inoltre possiamo anche scrivere i dati sopra nel file CSV utilizzando csv.writer() metodo.
Backup di iTunes
L'iPhone mobile forensics può essere eseguito sui backup effettuati da iTunes. Gli esaminatori forensi si affidano all'analisi dei backup logici dell'iPhone acquisiti tramite iTunes. Il protocollo AFC (connessione file Apple) viene utilizzato da iTunes per eseguire il backup. Inoltre, il processo di backup non modifica nulla sull'iPhone tranne i record della chiave di escrow.
Ora, sorge la domanda: perché è importante per un esperto di medicina legale digitale comprendere le tecniche sui backup di iTunes? È importante nel caso in cui otteniamo l'accesso direttamente al computer del sospetto anziché all'iPhone perché quando un computer viene utilizzato per la sincronizzazione con iPhone, è probabile che la maggior parte delle informazioni su iPhone venga salvata sul computer.
Processo di backup e sua posizione
Ogni volta che viene eseguito il backup di un prodotto Apple sul computer, è sincronizzato con iTunes e ci sarà una cartella specifica con l'ID univoco del dispositivo. Nell'ultimo formato di backup, i file vengono archiviati in sottocartelle contenenti i primi due caratteri esadecimali del nome file. Da questi file di backup, ci sono alcuni file come info.plist che sono utili insieme al database denominato Manifest.db. La tabella seguente mostra le posizioni dei backup, che variano in base ai sistemi operativi dei backup di iTunes:
OS | Posizione di backup |
---|---|
Win7 | C: \ Users \ [nome utente] \ AppData \ Roaming \ AppleComputer \ MobileSync \ Backup \ |
MAC OS X | ~ / Library / Application Suport / MobileSync / Backup / |
Per elaborare il backup di iTunes con Python, dobbiamo prima identificare tutti i backup nella posizione di backup secondo il nostro sistema operativo. Quindi itereremo su ogni backup e leggeremo il database Manifest.db.
Ora, con l'aiuto del codice Python possiamo fare lo stesso -
Innanzitutto, importa le librerie necessarie come segue:
from __future__ import print_function
import argparse
import logging
import os
from shutil import copyfile
import sqlite3
import sys
logger = logging.getLogger(__name__)
Ora, fornisci due argomenti posizionali, ovvero INPUT_DIR e OUTPUT_DIR che rappresenta il backup di iTunes e la cartella di output desiderata -
if __name__ == "__main__":
parser.add_argument("INPUT_DIR",help = "Location of folder containing iOS backups, ""e.g. ~\Library\Application Support\MobileSync\Backup folder")
parser.add_argument("OUTPUT_DIR", help = "Output Directory")
parser.add_argument("-l", help = "Log file path",default = __file__[:-2] + "log")
parser.add_argument("-v", help = "Increase verbosity",action = "store_true") args = parser.parse_args()
Ora, imposta il registro come segue:
if args.v:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
Ora, imposta il formato del messaggio per questo registro come segue:
msg_fmt = logging.Formatter("%(asctime)-15s %(funcName)-13s""%(levelname)-8s %(message)s")
strhndl = logging.StreamHandler(sys.stderr)
strhndl.setFormatter(fmt = msg_fmt)
fhndl = logging.FileHandler(args.l, mode = 'a')
fhndl.setFormatter(fmt = msg_fmt)
logger.addHandler(strhndl)
logger.addHandler(fhndl)
logger.info("Starting iBackup Visualizer")
logger.debug("Supplied arguments: {}".format(" ".join(sys.argv[1:])))
logger.debug("System: " + sys.platform)
logger.debug("Python Version: " + sys.version)
La seguente riga di codice creerà le cartelle necessarie per la directory di output desiderata utilizzando os.makedirs() funzione -
if not os.path.exists(args.OUTPUT_DIR):
os.makedirs(args.OUTPUT_DIR)
Ora, passa le directory di input e output fornite alla funzione main () come segue:
if os.path.exists(args.INPUT_DIR) and os.path.isdir(args.INPUT_DIR):
main(args.INPUT_DIR, args.OUTPUT_DIR)
else:
logger.error("Supplied input directory does not exist or is not ""a directory")
sys.exit(1)
Ora Scrivi main() funzione che chiamerà ulteriormente backup_summary() funzione per identificare tutti i backup presenti nella cartella di input -
def main(in_dir, out_dir):
backups = backup_summary(in_dir)
def backup_summary(in_dir):
logger.info("Identifying all iOS backups in {}".format(in_dir))
root = os.listdir(in_dir)
backups = {}
for x in root:
temp_dir = os.path.join(in_dir, x)
if os.path.isdir(temp_dir) and len(x) == 40:
num_files = 0
size = 0
for root, subdir, files in os.walk(temp_dir):
num_files += len(files)
size += sum(os.path.getsize(os.path.join(root, name))
for name in files)
backups[x] = [temp_dir, num_files, size]
return backups
Ora, stampa il riepilogo di ogni backup sulla console come segue:
print("Backup Summary")
print("=" * 20)
if len(backups) > 0:
for i, b in enumerate(backups):
print("Backup No.: {} \n""Backup Dev. Name: {} \n""# Files: {} \n""Backup Size (Bytes): {}\n".format(i, b, backups[b][1], backups[b][2]))
Ora, scarica il contenuto del file Manifest.db nella variabile denominata db_items.
try:
db_items = process_manifest(backups[b][0])
except IOError:
logger.warn("Non-iOS 10 backup encountered or " "invalid backup. Continuing to next backup.")
continue
Ora, definiamo una funzione che prenderà il percorso della directory del backup -
def process_manifest(backup):
manifest = os.path.join(backup, "Manifest.db")
if not os.path.exists(manifest):
logger.error("Manifest DB not found in {}".format(manifest))
raise IOError
Ora, usando SQLite3 ci collegheremo al database tramite il cursore denominato c -
c = conn.cursor()
items = {}
for row in c.execute("SELECT * from Files;"):
items[row[0]] = [row[2], row[1], row[3]]
return items
create_files(in_dir, out_dir, b, db_items)
print("=" * 20)
else:
logger.warning("No valid backups found. The input directory should be
" "the parent-directory immediately above the SHA-1 hash " "iOS device backups")
sys.exit(2)
Ora, definisci il file create_files() metodo come segue -
def create_files(in_dir, out_dir, b, db_items):
msg = "Copying Files for backup {} to {}".format(b, os.path.join(out_dir, b))
logger.info(msg)
Ora, itera attraverso ogni chiave nel file db_items dizionario -
for x, key in enumerate(db_items):
if db_items[key][0] is None or db_items[key][0] == "":
continue
else:
dirpath = os.path.join(out_dir, b,
os.path.dirname(db_items[key][0]))
filepath = os.path.join(out_dir, b, db_items[key][0])
if not os.path.exists(dirpath):
os.makedirs(dirpath)
original_dir = b + "/" + key[0:2] + "/" + key
path = os.path.join(in_dir, original_dir)
if os.path.exists(filepath):
filepath = filepath + "_{}".format(x)
Ora usa shutil.copyfile() metodo per copiare il file di backup come segue:
try:
copyfile(path, filepath)
except IOError:
logger.debug("File not found in backup: {}".format(path))
files_not_found += 1
if files_not_found > 0:
logger.warning("{} files listed in the Manifest.db not" "found in
backup".format(files_not_found))
copyfile(os.path.join(in_dir, b, "Info.plist"), os.path.join(out_dir, b,
"Info.plist"))
copyfile(os.path.join(in_dir, b, "Manifest.db"), os.path.join(out_dir, b,
"Manifest.db"))
copyfile(os.path.join(in_dir, b, "Manifest.plist"), os.path.join(out_dir, b,
"Manifest.plist"))
copyfile(os.path.join(in_dir, b, "Status.plist"),os.path.join(out_dir, b,
"Status.plist"))
Con lo script Python sopra, possiamo ottenere la struttura dei file di backup aggiornata nella nostra cartella di output. Possiamo usarepycrypto libreria python per decrittografare i backup.
Wi-Fi
I dispositivi mobili possono essere utilizzati per connettersi al mondo esterno connettendosi tramite reti Wi-Fi disponibili ovunque. A volte il dispositivo si connette automaticamente a queste reti aperte.
In caso di iPhone, l'elenco delle connessioni Wi-Fi aperte con cui il dispositivo si è connesso viene memorizzato in un file PLIST denominato com.apple.wifi.plist. Questo file conterrà l'SSID Wi-Fi, il BSSID e il tempo di connessione.
Dobbiamo estrarre i dettagli Wi-Fi dal report XML standard di Cellebrite utilizzando Python. Per questo, dobbiamo utilizzare l'API del Wireless Geographic Logging Engine (WIGLE), una piattaforma popolare che può essere utilizzata per trovare la posizione di un dispositivo utilizzando i nomi delle reti Wi-Fi.
Possiamo usare la libreria Python denominata requestsper accedere all'API da WIGLE. Può essere installato come segue:
pip install requests
API di WIGLE
Dobbiamo registrarci sul sito web di WIGLE https://wigle.net/accountper ottenere un'API gratuita da WIGLE. Lo script Python per ottenere le informazioni sul dispositivo dell'utente e la sua connessione tramite l'API di WIGEL è discusso di seguito:
Innanzitutto, importa le seguenti librerie per gestire cose diverse:
from __future__ import print_function
import argparse
import csv
import os
import sys
import xml.etree.ElementTree as ET
import requests
Fornisci ora due argomenti posizionali, vale a dire INPUT_FILE e OUTPUT_CSV che rappresenterà rispettivamente il file di input con l'indirizzo MAC Wi-Fi e il file CSV di output desiderato -
if __name__ == "__main__":
parser.add_argument("INPUT_FILE", help = "INPUT FILE with MAC Addresses")
parser.add_argument("OUTPUT_CSV", help = "Output CSV File")
parser.add_argument("-t", help = "Input type: Cellebrite XML report or TXT
file",choices = ('xml', 'txt'), default = "xml")
parser.add_argument('--api', help = "Path to API key
file",default = os.path.expanduser("~/.wigle_api"),
type = argparse.FileType('r'))
args = parser.parse_args()
Ora le seguenti righe di codice verificheranno se il file di input esiste ed è un file. In caso contrario, esce dallo script -
if not os.path.exists(args.INPUT_FILE) or \ not os.path.isfile(args.INPUT_FILE):
print("[-] {} does not exist or is not a
file".format(args.INPUT_FILE))
sys.exit(1)
directory = os.path.dirname(args.OUTPUT_CSV)
if directory != '' and not os.path.exists(directory):
os.makedirs(directory)
api_key = args.api.readline().strip().split(":")
Ora, passa l'argomento a main come segue:
main(args.INPUT_FILE, args.OUTPUT_CSV, args.t, api_key)
def main(in_file, out_csv, type, api_key):
if type == 'xml':
wifi = parse_xml(in_file)
else:
wifi = parse_txt(in_file)
query_wigle(wifi, out_csv, api_key)
Ora, analizzeremo il file XML come segue:
def parse_xml(xml_file):
wifi = {}
xmlns = "{http://pa.cellebrite.com/report/2.0}"
print("[+] Opening {} report".format(xml_file))
xml_tree = ET.parse(xml_file)
print("[+] Parsing report for all connected WiFi addresses")
root = xml_tree.getroot()
Ora, itera attraverso l'elemento figlio della radice come segue:
for child in root.iter():
if child.tag == xmlns + "model":
if child.get("type") == "Location":
for field in child.findall(xmlns + "field"):
if field.get("name") == "TimeStamp":
ts_value = field.find(xmlns + "value")
try:
ts = ts_value.text
except AttributeError:
continue
Ora, controlleremo che la stringa 'ssid' sia presente o meno nel testo del valore -
if "SSID" in value.text:
bssid, ssid = value.text.split("\t")
bssid = bssid[7:]
ssid = ssid[6:]
Ora, dobbiamo aggiungere BSSID, SSID e timestamp al dizionario wifi come segue:
if bssid in wifi.keys():
wifi[bssid]["Timestamps"].append(ts)
wifi[bssid]["SSID"].append(ssid)
else:
wifi[bssid] = {"Timestamps": [ts], "SSID":
[ssid],"Wigle": {}}
return wifi
Il parser di testo che è molto più semplice del parser XML è mostrato di seguito -
def parse_txt(txt_file):
wifi = {}
print("[+] Extracting MAC addresses from {}".format(txt_file))
with open(txt_file) as mac_file:
for line in mac_file:
wifi[line.strip()] = {"Timestamps": ["N/A"], "SSID":
["N/A"],"Wigle": {}}
return wifi
Ora, usiamo il modulo delle richieste per fare WIGLE APIchiamate e devi passare al query_wigle() metodo -
def query_wigle(wifi_dictionary, out_csv, api_key):
print("[+] Querying Wigle.net through Python API for {} "
"APs".format(len(wifi_dictionary)))
for mac in wifi_dictionary:
wigle_results = query_mac_addr(mac, api_key)
def query_mac_addr(mac_addr, api_key):
query_url = "https://api.wigle.net/api/v2/network/search?" \
"onlymine = false&freenet = false&paynet = false" \ "&netid = {}".format(mac_addr)
req = requests.get(query_url, auth = (api_key[0], api_key[1]))
return req.json()
In realtà esiste un limite giornaliero per le chiamate API WIGLE, se tale limite supera, deve mostrare un errore come segue:
try:
if wigle_results["resultCount"] == 0:
wifi_dictionary[mac]["Wigle"]["results"] = []
continue
else:
wifi_dictionary[mac]["Wigle"] = wigle_results
except KeyError:
if wigle_results["error"] == "too many queries today":
print("[-] Wigle daily query limit exceeded")
wifi_dictionary[mac]["Wigle"]["results"] = []
continue
else:
print("[-] Other error encountered for " "address {}: {}".format(mac,
wigle_results['error']))
wifi_dictionary[mac]["Wigle"]["results"] = []
continue
prep_output(out_csv, wifi_dictionary)
Ora useremo prep_output() metodo per appiattire il dizionario in blocchi facilmente scrivibili -
def prep_output(output, data):
csv_data = {}
google_map = https://www.google.com/maps/search/
Ora accedi a tutti i dati che abbiamo raccolto finora come segue:
for x, mac in enumerate(data):
for y, ts in enumerate(data[mac]["Timestamps"]):
for z, result in enumerate(data[mac]["Wigle"]["results"]):
shortres = data[mac]["Wigle"]["results"][z]
g_map_url = "{}{},{}".format(google_map, shortres["trilat"],shortres["trilong"])
Ora possiamo scrivere l'output nel file CSV come abbiamo fatto negli script precedenti in questo capitolo usando write_csv() funzione.