Python-digital-forensics-python-digital-mobile-device-forensics
Pythonデジタルモバイルデバイスフォレンジック
この章では、モバイルデバイス上のPythonデジタルフォレンジックと関連する概念について説明します。
前書き
モバイルデバイスフォレンジックは、モバイルデバイスの取得と分析を行い、調査対象のデジタル証拠を回復するデジタルフォレンジックの分野です。 モバイルデバイスには、位置に関する有用な情報を提供するのに役立つ組み込みの通信システムがあるため、このブランチはコンピューターフォレンジックとは異なります。
デジタルフォレンジックではスマートフォンの使用が日々増加していますが、それでもその不均一性のために非標準であると考えられています。 一方、ハードディスクなどのコンピューターハードウェアは標準と見なされ、安定した分野としても開発されています。 デジタルフォレンジック業界では、スマートフォンなどの一時的な証拠を持つ非標準デバイスに使用される技術について多くの議論があります。
モバイルデバイスから抽出可能なアーティファクト
最新のモバイルデバイスは、コールログまたはSMSメッセージのみを持つ古い電話と比較して、多くのデジタル情報を所有しています。 したがって、モバイルデバイスは、ユーザーに関する多くの洞察を調査員に提供できます。 モバイルデバイスから抽出できるいくつかのアーティファクトは以下のとおりです-
- メッセージ-これらは、所有者の心の状態を明らかにすることができ、調査員に以前の未知の情報を提供することもできる便利なアーティファクトです。
- ロケーション履歴-ロケーション履歴データは、人の特定の場所について検証するために調査員が使用できる有用なアーティファクトです。
- インストールされているアプリケーション-インストールされているアプリケーションの種類にアクセスすることにより、調査員はモバイルユーザーの習慣と思考についての洞察を得ます。
エビデンスのソースとPythonでの処理
スマートフォンには、主要な証拠のソースとしてSQLiteデータベースとPLISTファイルがあります。 このセクションでは、Pythonで証拠のソースを処理します。
PLISTファイルの分析
PLIST(プロパティリスト)は、特にiPhoneデバイスにアプリケーションデータを保存するための柔軟で便利な形式です。 拡張子 .plist を使用します。 バンドルとアプリケーションに関する情報を保存するために使用されるこの種のファイル。 XML と binary の2つの形式を使用できます。 次の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データベースに慣れていない場合は、リンクlink:/sqlite/index [www.finddevguides.com/sqlite/index]をたどることができます。さらに、リンクlink:/sqlite/sqlite_python [www.finddevguidesをたどることができます。 .com/sqlite/sqlite_python] PythonでSQLiteの詳細を知りたい場合に備えて。
モバイルフォレンジック中に、モバイルデバイスの sms.db ファイルを操作して、 message テーブルから貴重な情報を抽出できます。 Pythonには、SQLiteデータベースと接続するための sqlite3 という名前の組み込みライブラリがあります。 次のコマンドで同じものをインポートできます-
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 のメッセージテーブルの列名データを取得できます-
c.execute(“pragma table_info(message)”)
table_data = c.fetchall()
columns = [x[1] for x in table_data
ここで、SQLite環境内のさまざまな環境変数と状態フラグを制御するために使用される特別なコマンドであるSQLite PRAGMAコマンドを使用していることに注意してください。 上記のコマンドでは、* 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を持つ特定のフォルダーが作成されます。 最新のバックアップ形式では、ファイルはファイル名の最初の2つの16進文字を含むサブフォルダーに保存されます。 これらのバックアップファイルには、info.plistのようないくつかのファイルがあり、Manifest.dbという名前のデータベースとともに役立ちます。 次の表は、iTunesのバックアップのオペレーティングシステムによって異なるバックアップの場所を示しています-
OS | Backup Location |
---|---|
Win7 | C:\Users\[username]\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__)
今、2つの位置引数、すなわちiTunesバックアップと目的の出力フォルダを表すINPUT_DIRとOUTPUT_DIRを提供します-
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)
さて、入力フォルダに存在するすべてのバックアップを識別するために* backup_summary()関数をさらに呼び出す main()*関数を記述します-
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の詳細を抽出する必要があります。 そのためには、Wi-Fiネットワークの名前を使用してデバイスの場所を見つけるのに使用できる一般的なプラットフォームである、Wireless Geographic Logging Engine(WIGLE)のAPIを使用する必要があります。
*requests* という名前のPythonライブラリを使用して、WIGLEからAPIにアクセスできます。 次のようにインストールできます-
pip install requests
WIGLEのAPI
WIGLEから無料のAPIを取得するには、WIGLEのウェブサイトhttps://wigle.net/accountに登録する必要があります。 WIGELのAPIを介してユーザーデバイスとその接続に関する情報を取得するためのPythonスクリプトについては、以下で説明します-
まず、さまざまなことを処理するために次のライブラリをインポートします-
from __future__ import print_function
import argparse
import csv
import os
import sys
import xml.etree.ElementTree as ET
import requests
次に、2つの位置引数、つまり INPUT_FILE と OUTPUT_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(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:]
今、私たちは次のように無線LAN辞書にBSSID、SSIDとタイムスタンプを追加する必要があります-
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
さて、リクエストモジュールを使用して* 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コールには1日あたりの制限があります。その制限を超えると、次のようなエラーが表示されます。
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ファイルに書き込むことができます。