Python數字移動裝置取證



本章將解釋移動裝置上的Python數字取證及其涉及的概念。

介紹

移動裝置取證是數字取證的一個分支,它處理移動裝置的採集和分析,以恢復具有調查意義的數字證據。這個分支與計算機取證不同,因為移動裝置具有內建的通訊系統,這對於提供與位置相關的有用資訊非常有用。

雖然智慧手機在數字取證中的使用日益增加,但由於其異構性,它仍然被認為是非標準的。另一方面,計算機硬體,例如硬碟,被認為是標準的,並且也發展成為一個穩定的學科。在數字取證行業中,關於用於非標準裝置(具有瞬態證據,例如智慧手機)的技術存在很多爭論。

可從移動裝置中提取的工件

與僅具有通話記錄或簡訊的舊手機相比,現代移動裝置擁有大量的數字資訊。因此,移動裝置可以為調查人員提供對其使用者的大量見解。可以從移動裝置中提取的一些工件如下所述:

  • 訊息- 這些是有用的工件,可以揭示所有者的思想狀態,甚至可以為調查員提供一些以前未知的資訊。

  • 位置歷史記錄- 位置歷史記錄資料是一個有用的工件,調查人員可以使用它來驗證某人的特定位置。

  • 已安裝的應用程式- 透過訪問已安裝的應用程式型別,調查人員可以深入瞭解移動使用者的習慣和想法。

Python中的證據來源和處理

智慧手機以SQLite資料庫和PLIST檔案作為主要的證據來源。在本節中,我們將使用python處理證據來源。

分析PLIST檔案

PLIST(屬性列表)是一種靈活且方便的格式,用於儲存應用程式資料,尤其是在iPhone裝置上。它使用副檔名.plist。此類檔案用於儲存有關捆綁包和應用程式的資訊。它可以有兩種格式:XML二進位制。以下Python程式碼將開啟並讀取PLIST檔案。請注意,在繼續執行此操作之前,我們必須建立我們自己的Info.plist檔案。

首先,使用以下命令安裝名為biplist的第三方庫:

Pip install biplist

現在,匯入一些有用的庫來處理plist檔案:

import biplist
import os
import sys

現在,在主方法下使用以下命令可以將plist檔案讀取到變數中:

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)

現在,我們可以從這個變數中讀取控制檯上的資料或直接列印它。

SQLite資料庫

SQLite充當移動裝置上的主要資料儲存庫。SQLite是一個程序內庫,它實現了一個自包含的、無伺服器的、零配置的、事務性SQL資料庫引擎。它是一個零配置的資料庫,你不需要在你的系統中配置它,不像其他資料庫。

如果您是新手或不熟悉SQLite資料庫,您可以訪問以下連結www.tutorialspoint.com/sqlite/index.htm此外,如果您想詳細瞭解SQLite和Python,您可以訪問以下連結www.tutorialspoint.com/sqlite/sqlite_python.htm

在移動取證過程中,我們可以與移動裝置的sms.db檔案互動,並可以從message表中提取有價值的資訊。Python有一個名為sqlite3的內建庫,用於連線SQLite資料庫。您可以使用以下命令匯入它:

import sqlite3

現在,藉助以下命令,我們可以連線到資料庫,例如移動裝置的sms.db

Conn = sqlite3.connect(‘sms.db’)
C = conn.cursor()

這裡,C是遊標物件,藉助它我們可以與資料庫互動。

現在,假設如果我們想執行一個特定的命令,例如獲取abc表的詳細資訊,可以使用以下命令:

c.execute(“Select * from abc”)
c.close()

上述命令的結果將儲存在cursor物件中。類似地,我們可以使用fetchall()方法將結果轉儲到我們可以操作的變數中。

我們可以使用以下命令獲取sms.db中message表的列名資料:

c.execute(“pragma table_info(message)”)
table_data = c.fetchall()
columns = [x[1] for x in table_data

請注意,這裡我們使用的是SQLite PRAGMA命令,這是一個特殊的命令,用於控制SQLite環境中的各種環境變數和狀態標誌。在上述命令中,fetchall()方法返回一個結果元組。每一列的名稱都儲存在每個元組的第一個索引中。

現在,藉助以下命令,我們可以查詢該表的所有資料並將其儲存在名為data_msg的變數中:

c.execute(“Select * from message”)
data_msg = c.fetchall()

上述命令將資料儲存在變數中,此外我們還可以使用csv.writer()方法將上述資料寫入CSV檔案。

iTunes備份

iPhone移動取證可以在iTunes建立的備份上執行。取證檢查員依靠分析透過iTunes獲取的iPhone邏輯備份。iTunes使用AFC(Apple檔案連線)協議進行備份。此外,備份過程不會修改iPhone上的任何內容,除了託管金鑰記錄。

現在,問題出現了,為什麼數字取證專家瞭解iTunes備份技術很重要?如果我們直接訪問嫌疑人的計算機而不是iPhone,這很重要,因為當計算機用於與iPhone同步時,iPhone上的大部分資訊都可能備份到計算機上。

備份過程及其位置

每當Apple產品備份到計算機時,它都會與iTunes同步,並且將會有一個包含裝置唯一ID的特定資料夾。在最新的備份格式中,檔案儲存在包含檔名前兩個十六進位制字元的子資料夾中。從這些備份檔案中,有一些檔案如info.plist與名為Manifest.db的資料庫一起使用。下表顯示了iTunes備份的作業系統不同的備份位置:

作業系統 備份位置
Win7 C:\Users\[使用者名稱]\AppData\Roaming\AppleComputer\MobileSync\Backup\
MAC OS X ~/Library/Application Suport/MobileSync/Backup/

要使用Python處理iTunes備份,我們需要首先根據我們的作業系統識別備份位置中的所有備份。然後,我們將遍歷每個備份並讀取Manifest.db資料庫。

現在,藉助以下Python程式碼,我們可以做到這一點:

首先,匯入必要的庫,如下所示:

from __future__ import print_function
import argparse
import logging
import os

from shutil import copyfile
import sqlite3
import sys
logger = logging.getLogger(__name__)

現在,提供兩個位置引數,即INPUT_DIR和OUTPUT_DIR,它們分別表示iTunes備份和所需的輸出資料夾:

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()

現在,設定日誌,如下所示:

if args.v:
   logger.setLevel(logging.DEBUG)
else:
   logger.setLevel(logging.INFO)

現在,為該日誌設定訊息格式,如下所示:

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)

以下程式碼行將使用os.makedirs()函式為所需的輸出目錄建立必要的資料夾:

if not os.path.exists(args.OUTPUT_DIR):
   os.makedirs(args.OUTPUT_DIR)

現在,將提供的輸入和輸出目錄傳遞給main()函式,如下所示:

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)

現在,編寫main()函式,該函式將進一步呼叫backup_summary()函式以識別輸入資料夾中存在的所有備份:

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

現在,將每個備份的摘要列印到控制檯,如下所示:

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]))

現在,將Manifest.db檔案的內容轉儲到名為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

現在,讓我們定義一個函式,該函式將獲取備份的目錄路徑:

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

現在,使用SQLite3,我們將透過名為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)

現在,定義create_files()方法,如下所示:

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)

現在,迭代db_items字典中的每個鍵:

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)

現在,使用shutil.copyfile()方法複製備份的檔案,如下所示:

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"))

使用上述Python指令碼,我們可以在輸出資料夾中獲得更新的備份檔案結構。我們可以使用pycrypto python庫來解密備份。

Wi-Fi

移動裝置可以透過連線到隨處可見的Wi-Fi網路來連線到外部世界。有時裝置會自動連線到這些開放網路。

對於iPhone,裝置已連線到的開放Wi-Fi連線列表儲存在一個名為com.apple.wifi.plist的PLIST檔案中。此檔案將包含Wi-Fi SSID、BSSID和連線時間。

我們需要使用Python從標準Cellebrite XML報告中提取Wi-Fi詳細資訊。為此,我們需要使用無線地理位置記錄引擎 (WIGLE) 的API,這是一個流行的平臺,可用於使用Wi-Fi網路名稱查詢裝置的位置。

我們可以使用名為requests的Python庫來訪問WIGLE的API。它可以按如下方式安裝:

pip install requests

WIGLE的API

我們需要在WIGLE的網站https://wigle.net/account上註冊以從WIGLE獲取免費API。下面討論了獲取有關使用者裝置及其透過WIGLE API連線的資訊的Python指令碼:

首先,匯入以下庫來處理不同的事情:

from __future__ import print_function

import argparse
import csv
import os
import sys
import xml.etree.ElementTree as ET
import requests

現在,提供兩個位置引數,即INPUT_FILEOUTPUT_CSV,它們分別表示包含Wi-Fi MAC地址的輸入檔案和所需的輸出CSV檔案:

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()

現在,以下程式碼行將檢查輸入檔案是否存在並且是一個檔案。如果不是,它將退出指令碼:

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(":")

現在,將引數傳遞給main,如下所示:

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)

現在,我們將解析XML檔案,如下所示:

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()

現在,迭代根的子元素,如下所示:

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

現在,我們將檢查'ssid'字串是否存在於值的文字中:

if "SSID" in value.text:
   bssid, ssid = value.text.split("\t")
   bssid = bssid[7:]
   ssid = ssid[6:]

現在,我們需要將BSSID、SSID和時間戳新增到wifi字典中,如下所示:

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

文字解析器比XML解析器簡單得多,如下所示:

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

現在,讓我們使用requests模組來進行WIGLE API呼叫,並將繼續進行query_wigle()方法。

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()

實際上,WIGLE API呼叫每天都有限制,如果超過此限制,則會顯示如下錯誤:

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)

現在,我們將使用prep_output()方法將字典展平為易於寫入的塊。

def prep_output(output, data):
   csv_data = {}
   google_map = https://www.google.com/maps/search/

現在,訪問我們目前為止收集到的所有資料,如下所示:

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"])

現在,我們可以像本章前面指令碼中所做的那樣,使用write_csv()函式將輸出寫入CSV檔案。

廣告