Python Digital Mobile Device Forensics
Este capítulo explicará a análise forense digital Python em dispositivos móveis e os conceitos envolvidos.
Introdução
A perícia forense de dispositivos móveis é o ramo da perícia digital que lida com a aquisição e análise de dispositivos móveis para recuperar evidências digitais de interesse investigativo. Este ramo é diferente da computação forense porque os dispositivos móveis têm um sistema de comunicação embutido que é útil para fornecer informações úteis relacionadas à localização.
Embora o uso de smartphones esteja aumentando no dia-a-dia da forense digital, ainda é considerado fora do padrão devido à sua heterogeneidade. Por outro lado, o hardware do computador, como o disco rígido, é considerado padrão e também desenvolvido como uma disciplina estável. Na indústria forense digital, há muito debate sobre as técnicas utilizadas para dispositivos fora do padrão, havendo evidências transitórias, como smartphones.
Artefatos extraíveis de dispositivos móveis
Os dispositivos móveis modernos possuem grande quantidade de informações digitais em comparação com os telefones mais antigos, tendo apenas um registro de chamadas ou mensagens SMS. Assim, os dispositivos móveis podem fornecer aos investigadores muitos insights sobre seu usuário. Alguns artefatos que podem ser extraídos de dispositivos móveis são mencionados abaixo -
Messages - Estes são os artefatos úteis que podem revelar o estado de espírito do proprietário e podem até mesmo fornecer algumas informações anteriores desconhecidas ao investigador.
Location History- Os dados do histórico de localização são um artefato útil que pode ser usado por investigadores para validar sobre a localização específica de uma pessoa.
Applications Installed - Acessando o tipo de aplicativo instalado, o investigador obtém algumas informações sobre os hábitos e pensamentos do usuário móvel.
Fontes de evidência e processamento em Python
Smartphones têm bancos de dados SQLite e arquivos PLIST como as principais fontes de evidências. Nesta seção, iremos processar as fontes de evidências em python.
Analisando arquivos PLIST
Uma PLIST (lista de propriedades) é um formato flexível e conveniente para armazenar dados de aplicativos, especialmente em dispositivos iPhone. Usa a extensão.plist. Esse tipo de arquivo usado para armazenar informações sobre pacotes e aplicativos. Pode ser em dois formatos:XML e binary. O seguinte código Python irá abrir e ler o arquivo PLIST. Observe que antes de prosseguirmos com isso, devemos criar nosso próprioInfo.plist Arquivo.
Primeiro, instale uma biblioteca de terceiros chamada biplist pelo seguinte comando -
Pip install biplist
Agora, importe algumas bibliotecas úteis para processar arquivos plist -
import biplist
import os
import sys
Agora, use o seguinte comando no método principal pode ser usado para ler o arquivo plist em uma variável -
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)
Agora, podemos ler os dados no console ou imprimi-los diretamente, a partir desta variável.
Bancos de dados SQLite
SQLite atua como o repositório de dados principal em dispositivos móveis. SQLite uma biblioteca em processo que implementa um mecanismo de banco de dados SQL transacional independente, sem servidor e com configuração zero. É um banco de dados com configuração zero, você não precisa configurá-lo em seu sistema, ao contrário de outros bancos de dados.
Se você é um novato ou não está familiarizado com bancos de dados SQLite, pode seguir o link www.tutorialspoint.com/sqlite/index.htm Além disso, você pode seguir o link www.tutorialspoint.com/sqlite/sqlite_python.htm caso queira entrar em detalhes de SQLite com Python.
Durante a perícia móvel, podemos interagir com o sms.db arquivo de um dispositivo móvel e pode extrair informações valiosas de messagemesa. Python tem uma biblioteca integrada chamadasqlite3para se conectar ao banco de dados SQLite. Você pode importar o mesmo com o seguinte comando -
import sqlite3
Agora, com a ajuda do seguinte comando, podemos nos conectar com o banco de dados, digamos sms.db no caso de dispositivos móveis -
Conn = sqlite3.connect(‘sms.db’)
C = conn.cursor()
Aqui, C é o objeto cursor com a ajuda do qual podemos interagir com o banco de dados.
Agora, suponha que se desejamos executar um comando específico, digamos, para obter os detalhes do abc table, isso pode ser feito com a ajuda do seguinte comando -
c.execute(“Select * from abc”)
c.close()
O resultado do comando acima seria armazenado no cursorobjeto. Da mesma forma, podemos usarfetchall() método para despejar o resultado em uma variável que possamos manipular.
Podemos usar o seguinte comando para obter os dados dos nomes das colunas da tabela de mensagens em sms.db -
c.execute(“pragma table_info(message)”)
table_data = c.fetchall()
columns = [x[1] for x in table_data
Observe que aqui estamos usando o comando SQLite PRAGMA, que é um comando especial a ser usado para controlar várias variáveis ambientais e sinalizadores de estado dentro do ambiente SQLite. No comando acima, ofetchall()método retorna uma tupla de resultados. O nome de cada coluna é armazenado no primeiro índice de cada tupla.
Agora, com a ajuda do seguinte comando, podemos consultar a tabela para todos os seus dados e armazená-los na variável chamada data_msg -
c.execute(“Select * from message”)
data_msg = c.fetchall()
O comando acima irá armazenar os dados na variável e, além disso, também podemos escrever os dados acima no arquivo CSV usando csv.writer() método.
Backups do iTunes
A análise forense móvel do iPhone pode ser realizada nos backups feitos pelo iTunes. Os examinadores forenses confiam na análise dos backups lógicos do iPhone adquiridos por meio do iTunes. O protocolo AFC (conexão de arquivo Apple) é usado pelo iTunes para fazer o backup. Além disso, o processo de backup não modifica nada no iPhone, exceto os registros da chave de garantia.
Agora, surge a pergunta: por que é importante para um especialista forense digital entender as técnicas de backups do iTunes? É importante caso tenhamos acesso ao computador do suspeito em vez do iPhone diretamente, porque quando um computador é usado para sincronizar com o iPhone, é provável que a maioria das informações do iPhone seja armazenada no computador.
Processo de backup e sua localização
Sempre que é feito o backup de um produto Apple no computador, ele é sincronizado com o iTunes e haverá uma pasta específica com a ID exclusiva do dispositivo. No formato de backup mais recente, os arquivos são armazenados em subpastas contendo os dois primeiros caracteres hexadecimais do nome do arquivo. A partir desses arquivos de backup, existem alguns arquivos, como info.plist, que são úteis junto com o banco de dados denominado Manifest.db. A tabela a seguir mostra os locais de backup, que variam com os sistemas operacionais de backups do iTunes -
SO | Localização de backup |
---|---|
Win7 | C: \ Usuários \ [nome de usuário] \ AppData \ Roaming \ AppleComputer \ MobileSync \ Backup \ |
MAC OS X | ~ / Library / Application Suport / MobileSync / Backup / |
Para processar o backup do iTunes com Python, precisamos primeiro identificar todos os backups no local do backup de acordo com nosso sistema operacional. Em seguida, iremos iterar em cada backup e ler o banco de dados Manifest.db.
Agora, com a ajuda de seguir o código Python, podemos fazer o mesmo -
Primeiro, importe as bibliotecas necessárias da seguinte forma -
from __future__ import print_function
import argparse
import logging
import os
from shutil import copyfile
import sqlite3
import sys
logger = logging.getLogger(__name__)
Agora, forneça dois argumentos posicionais, a saber INPUT_DIR e OUTPUT_DIR, que representam o backup do iTunes e a pasta de saída desejada -
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()
Agora, configure o log da seguinte forma -
if args.v:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
Agora, configure o formato da mensagem para este registro da seguinte maneira -
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)
A linha de código a seguir criará as pastas necessárias para o diretório de saída desejado usando os.makedirs() função -
if not os.path.exists(args.OUTPUT_DIR):
os.makedirs(args.OUTPUT_DIR)
Agora, passe os diretórios de entrada e saída fornecidos para a função main () da seguinte maneira -
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)
Agora escreva main() função que irá posteriormente chamar backup_summary() função para identificar todos os backups presentes na pasta de entrada -
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
Agora, imprima o resumo de cada backup no console da seguinte maneira -
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]))
Agora, despeje o conteúdo do arquivo Manifest.db na variável chamada 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
Agora, vamos definir uma função que tomará o caminho do diretório do 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
Agora, usando o SQLite3, iremos conectar ao banco de dados pelo cursor chamado 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)
Agora, defina o create_files() método da seguinte forma -
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)
Agora, itere através de cada chave no db_items dicionário -
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)
Agora usa shutil.copyfile() método para copiar o arquivo de backup da seguinte forma -
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"))
Com o script Python acima, podemos obter a estrutura de arquivo de backup atualizada em nossa pasta de saída. Podemos usarpycrypto biblioteca python para descriptografar os backups.
Wi-fi
Os dispositivos móveis podem ser usados para se conectar com o mundo exterior, conectando-se por meio de redes Wi-Fi que estão disponíveis em todos os lugares. Às vezes, o dispositivo se conecta a essas redes abertas automaticamente.
No caso do iPhone, a lista de conexões Wi-Fi abertas com as quais o dispositivo se conectou é armazenada em um arquivo PLIST chamado com.apple.wifi.plist. Este arquivo conterá o SSID Wi-Fi, BSSID e o tempo de conexão.
Precisamos extrair detalhes de Wi-Fi do relatório XML padrão da Cellebrite usando Python. Para isso, precisamos usar a API do Wireless Geographic Logging Engine (WIGLE), uma plataforma popular que pode ser usada para encontrar a localização de um dispositivo usando os nomes de redes wi-fi.
Podemos usar a biblioteca Python chamada requestspara acessar a API do WIGLE. Ele pode ser instalado da seguinte forma -
pip install requests
API do WIGLE
Precisamos nos registrar no site do WIGLE https://wigle.net/accountpara obter uma API gratuita do WIGLE. O script Python para obter as informações sobre o dispositivo do usuário e sua conexão por meio da API WIGEL é discutido abaixo -
Primeiro, importe as seguintes bibliotecas para lidar com coisas diferentes -
from __future__ import print_function
import argparse
import csv
import os
import sys
import xml.etree.ElementTree as ET
import requests
Agora, forneça dois argumentos posicionais, a saber INPUT_FILE e OUTPUT_CSV que representará o arquivo de entrada com o endereço MAC Wi-Fi e o arquivo CSV de saída desejado, respectivamente -
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()
Agora, as seguintes linhas de código verificarão se o arquivo de entrada existe e é um arquivo. Caso contrário, sai do 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(":")
Agora, passe o argumento para main da seguinte forma -
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)
Agora, vamos analisar o arquivo XML da seguinte maneira -
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()
Agora, itere através do elemento filho da raiz da seguinte maneira -
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
Agora, vamos verificar se a string 'ssid' está presente ou não no texto do valor -
if "SSID" in value.text:
bssid, ssid = value.text.split("\t")
bssid = bssid[7:]
ssid = ssid[6:]
Agora, precisamos adicionar BSSID, SSID e timestamp ao dicionário wi-fi da seguinte maneira -
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
O analisador de texto, que é muito mais simples do que o analisador XML, é mostrado abaixo -
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
Agora, vamos usar o módulo de solicitações para fazer WIGLE APIchamadas e precisa passar para o query_wigle() método -
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()
Na verdade, há um limite por dia para chamadas WIGLE API, se esse limite exceder, ele deve mostrar um erro da seguinte forma -
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)
Agora, vamos usar prep_output() método para nivelar o dicionário em pedaços facilmente graváveis -
def prep_output(output, data):
csv_data = {}
google_map = https://www.google.com/maps/search/
Agora, acesse todos os dados que coletamos até agora da seguinte forma -
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"])
Agora, podemos escrever a saída em um arquivo CSV, como fizemos em scripts anteriores neste capítulo, usando write_csv() função.