📜  Python数字移动设备取证

📅  最后修改于: 2020-11-07 08:10:49             🧑  作者: Mango


本章将解释移动设备上的Python数字取证以及相关概念。

介绍

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

尽管智能手机在数字取证中的使用日益增加,但由于其异构性,仍被认为是非标准的。另一方面,计算机硬件(例如硬盘)也被认为是标准的,并且也已发展成为稳定的学科。在数字取证行业中,对于用于非标准设备(具有智能手机等瞬态证据)的技术存在很多争议。

可从移动设备提取的伪像

与仅具有呼叫记录或SMS消息的旧手机相比,现代移动设备拥有大量的数字信息。因此,移动设备可以为调查人员提供有关其用户的大量见解。可以从移动设备提取的一些工件如下所述-

  • 消息-这些是有用的工件,可以显示所有者的心态,甚至可以向调查人员提供一些以前未知的信息。

  • 位置历史记录-位置历史记录数据是有用的工件,调查人员可以使用它来验证有关人的特定位置的信息。

  • 已安装的应用程序-通过访问已安装的应用程序的种类,调查人员可以洞悉移动用户的习惯和想法。

Python的证据来源和处理

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

分析PLIST文件

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

首先,通过以下命令安装名为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.tutorialspoint.com/sqlite/index.htm。此外,如果需要,可以单击链接www.tutorialspoint.com/sqlite/sqlite_python.htm 。使用Python详细了解SQLite。

在移动取证过程中,我们可以与移动设备的sms.db文件进行交互,并且可以从消息表中提取有价值的信息。 Python有一个名为sqlite3的内置库,用于与SQLite数据库连接。您可以使用以下命令导入相同的内容-

import sqlite3

现在,在以下命令的帮助下,我们可以连接数据库,例如在移动设备的情况下为sms.db-

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

在这里,C是光标对象,我们可以借助它与数据库进行交互。

现在,假设我们要执行一个特定的命令,比如说要从abc表中获取详细信息,可以在以下命令的帮助下完成-

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

上面命令的结果将存储在光标对象中。同样,我们可以使用fetchall()方法将结果转储到我们可以操纵的变量中。

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

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备份

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

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

备份过程及其位置

只要将Apple产品备份到计算机上,它就会与iTunes同步,并且会存在一个带有设备唯一ID的特定文件夹。在最新的备份格式中,文件存储在包含文件名的前两个十六进制字符的子文件夹中。在这些备份文件中,有一些文件(如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__)

现在,提供两个位置参数,即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网络进行连接,可以使用移动设备连接到外界。有时设备会自动连接到这些开放的网络。

对于iPhone,设备已连接的打开的Wi-Fi连接的列表存储在名为com.apple.wifi.plist的PLIST文件中。该文件将包含Wi-Fi SSID,BSSID和连接时间。

我们需要使用Python从标准Cellebrite XML报告中提取Wi-Fi详细信息。为此,我们需要使用Wireless Geographic Logging Engine(WIGLE)的API,WIGLE是一种流行的平台,可用于使用Wi-Fi网络的名称查找设备的位置。

我们可以使用名为请求的Python库来从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

现在,提供两个位置参数,即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

现在,让我们使用请求模块进行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文件。