Python数字移动设备取证
本章将解释移动设备上的Python数字取证和所涉及的概念。
简介
移动设备取证是数字取证的一个分支,涉及获取和分析移动设备以恢复调查所需的数字证据。这个分支与计算机取证不同,因为移动设备有一个内置的通信系统,可以提供与位置有关的有用信息。
虽然智能手机在数字取证中的使用与日俱增,但由于其异质性,它仍然被认为是非标准的。另一方面,计算机硬件,如硬盘,也被认为是标准的,并作为一个稳定的学科发展。在数字取证行业中,对用于非标准设备的技术有很多争论,这些设备具有短暂的证据,如智能手机。
可从移动设备中提取的人工制品
与只有通话记录或短信的旧手机相比,现代移动设备拥有大量的数字信息。因此,移动设备可以为调查人员提供很多关于其用户的信息。一些可以从移动设备中提取的人工制品如下所示
- 信息 – 这些是有用的人工制品,可以揭示所有者的思想状态,甚至可以向调查员提供一些以前未知的信息。
-
位置历史 – 位置历史数据是一个有用的人工制品,调查人员可以用它来验证一个人的特定位置。
-
安装的应用程序 – 通过访问安装的应用程序的种类,调查员可以深入了解移动用户的习惯和想法。
证据来源和Python的处理
智能手机有SQLite数据库和PLIST文件作为证据的主要来源。在本节中,我们将用Python来处理证据来源。
分析PLIST文件
PLIST(属性列表)是一种灵活方便的格式,用于存储应用程序的数据,特别是在iPhone设备上。它使用的扩展名是 .plist 。 这类文件用来存储关于捆绑和应用程序的信息。它可以有两种格式: XML 和 二进制。 下面的Python代码将打开并读取PLIST文件。注意,在进入这个过程之前,我们必须创建我们自己的 Info.plist 文件。
首先,通过下面的命令安装一个名为 biplist 的第三方库– 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,此外 ,如果你想用Python详细了解SQLite,你可以按照链接www.tutorialspoint.com/sqlite/sqlite_python.htm 。
在移动取证过程中,我们可以与移动设备的 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备份
iPhone手机取证可以通过iTunes的备份来进行。鉴证人员依靠分析通过iTunes获得的iPhone逻辑备份。AFC(苹果文件连接)协议是由iTunes使用来进行备份的。此外,除了托管密钥记录,备份过程不会修改iPhone上的任何东西。
现在,问题来了,为什么数字取证专家必须了解iTunes备份的技术?这对我们进入犯罪嫌疑人的电脑而不是直接进入iPhone很重要,因为当电脑被用来与iPhone同步时,iPhone上的大部分信息都有可能被备份到电脑上了。
备份的过程和位置
每当苹果产品被备份到电脑上时,它就会与iTunes同步,并且会有一个带有设备独特ID的特定文件夹。在最新的备份格式中,文件被存储在包含文件名前两个十六进制字符的子文件夹中。从这些备份文件中,有一些文件,如info.plist,与名为Manifest.db的数据库一起是很有用的。The following table shows the backup locations, that vary with operating systems of iTunes backups −
操作系统 | 备份位置 |
---|---|
Win7 | C:\Users\[用户名]\AppData\Roaming\AppleComputer\MobileSync\Backup\ |
MAC OS X | ~/Library/Application Suport/MobileSync/Backup/ 仓库。 |
对于用Python处理iTunes的备份,我们首先需要根据我们的操作系统识别备份位置的所有备份。然后,我们将遍历每个备份并读取数据库Manifest.db。
Now, with the help of following Python code we can do the same −
首先,导入必要的库,如下所示
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网络的名称找到设备的位置。
我们可以使用名为 request 的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_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(":")
Now, pass the argument to main as follows −
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
现在,我们将检查’sid’字符串是否存在于该值的文本中。
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/
Now, access all the data we have collected so far as follows −
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文件,正如我们在本章前面的脚本中所做的那样。