Python 数字移动设备取证


本章将解释移动设备上的 Python 数字取证及其所涉及的概念。

介绍


移动设备取证是数字取证的一个分支,它处理移动设备的获取和分析,以恢复调查兴趣的数字证据。该分支与计算机取证不同,因为移动设备具有内置的通信系统,可用于提供与位置相关的有用信息。

尽管智能手机在数字取证中的使用日益增加,但由于其异质性,它仍然被认为是非标准的。另一方面,计算机硬件,如硬盘,也被认为是标准的,也被发展为一门稳定的学科。在数字取证行业,关于用于非标准设备的技术存在很多争论,这些设备具有短暂的证据,例如智能手机。

可从移动设备中提取的工件


与只有通话记录或 SMS 消息的旧手机相比,现代移动设备拥有大量数字信息。因此,移动设备可以为调查人员提供有关其用户的大量见解。一些可以从移动设备中提取的神器如下:

  • Messages : 这些都是有用的神器,可以揭示主人的心境,甚至可以将一些以前不为人知的信息提供给调查员。

  • 位置记录 : 位置历史数据是一个有用的人工制品,调查人员可以使用它来验证一个人的特定位置。

  • 已安装的应用程序 : 通过访问所安装的应用程序,调查员可以深入了解移动用户的习惯和想法。

Python中的证据来源和处理


智能手机有 SQLite 数据库和 PLIST 文件作为主要的证据来源。在本节中,我们将在 python 中处理证据的来源。

分析 PLIST 文件

PLIST(属性列表)是一种灵活方便的格式,用于存储应用程序数据,尤其是在 iPhone 设备上。它使用扩展名 .plist .此类文件用于存储有关捆绑包和应用程序的信息。它可以有两种格式: XML and binary .以下 Python 代码将打开并读取 PLIST 文件。请注意,在进行此操作之前,我们必须创建自己的 信息列表 file.

首先,安装一个名为的第三方库 biplist 通过以下命令:

Pip install biplist

现在,导入一些有用的库来处理 plist 文件:

import biplist
import os
import sys

现在,在 main 方法下使用以下命令可以将 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.newbiego.com/sqlite/index.htm 此外,你可以点击链接 www.newbiego.com/sqlite/sqlite_python.htm 如果你想使用 Python 详细了解 SQLite。

在移动取证过程中,我们可以与 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 目的。同样我们可以使用 获取所有() 方法将结果转储到我们可以操作的变量中。

我们可以使用以下命令获取消息表中的列名数据 sms.db

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

注意这里我们使用的是 SQLite PRAGMA 命令,这是一个特殊的命令,用于控制 SQLite 环境中的各种环境变量和状态标志。在上述命令中, 获取所有() 方法返回结果元组。每个列的名称存储在每个元组的第一个索引中。

现在,借助以下命令,我们可以查询表中的所有数据并将其存储在名为的变量中 data_msg

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

上面的命令会将数据存储在变量中,此外我们还可以使用以下命令将上述数据写入 CSV 文件 csv.writer() method.

iTunes 备份


可以对 iTunes 制作的备份执行 iPhone 移动取证。法医检查员依靠分析通过 iTunes 获得的 iPhone 逻辑备份。 iTunes 使用 AFC(Apple 文件连接)协议进行备份。此外,备份过程不会修改 iPhone 上的任何内容,除了托管密钥记录。

现在,问题出现了,为什么数字取证专家了解 iTunes 备份技术很重要?如果我们直接访问嫌疑人的计算机而不是 iPhone,这一点很重要,因为当使用计算机与 iPhone 同步时,iPhone 上的大部分信息很可能会备份在计算机上。

备份过程及其位置

每当将 Apple 产品备份到计算机时,它都会与 iTunes 同步,并且会有一个带有设备唯一 ID 的特定文件夹。在最新的备份格式中,文件存储在包含文件名前两个十六进制字符的子文件夹中。从这些备份文件中,有一些文件(如 info.plist)与名为 Manifest.db 的数据库一起使用。下表列出了备份位置,因 iTunes 备份的操作系统而异:

OS 备份位置
Win7 C:\Users\[用户名]\AppData\Roaming\AppleComputer\MobileSync\Backup\
MAC OS X ~/库/应用程序支持/MobileSync/备份/

为了使用 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 = "输出 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() 将进一步调用的函数 备份摘要() 识别输入文件夹中存在的所有备份的功能:

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)

现在,定义 创建文件() 方法如下:

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)

Now, use 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 连接列表存储在名为 PLIST 的文件中 com.apple.wifi.plist .该文件将包含 Wi-Fi SSID、BSSID 和连接时间。

我们需要使用 Python 从标准 Cellebrite XML 报告中提取 Wi-Fi 详细信息。为此,我们需要使用来自无线地理记录引擎 (WIGLE) 的 API,这是一个流行的平台,可用于使用 Wi-Fi 网络名称查找设备的位置。

我们可以使用名为的 Python 库 requests 从 WIGLE 访问 API。可以按如下方式安装:

pip install requests

来自 WIGLE 的 API

我们需要在 WIGLE 的网站上注册 https://wigle.net/account 从 WIGLE 获得免费的 API。下面讨论通过 WIGEL 的 API 获取用户设备及其连接信息的 Python 脚本:

首先,导入以下库来处理不同的事情:

from __future__ import print_function

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

现在,提供两个位置参数,即 输入文件 and 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 = "输出 CSV File")
    parser.add_argument("-t", help = "输入 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 模块来制作 摆动 API 电话,需要继续 query_wigle() method:

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)

现在,我们将使用 准备输出() 将字典扁平化为易于写入的块的方法:

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

现在,我们可以将输出写入 CSV 文件,就像我们在本章前面的脚本中所做的那样,使用 write_csv() 功能。