精通-Pyrhon-取证-二-

69 阅读34分钟

精通 Pyrhon 取证(二)

原文:annas-archive.org/md5/89513dcba4f06502fb29f333da05d5b3

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:使用 Python 进行移动法医分析

尽管标准计算机硬件(如硬盘)的法医分析已发展为一门稳定的学科,并有许多参考资料,例如*《文件系统法医分析》一书,作者为Brian Carrier*,由Addison-Wesley Professional出版,以及我们之前的章节,关于分析非标准硬件或瞬态证据的技术仍然存在许多争议。尽管智能手机在数字调查中的作用日益增加,但由于其异质性,智能手机仍被视为非标准设备。在所有调查中,遵循基本的法医原则是必要的。法医调查的两大原则如下:

  • 必须非常小心,以确保证据被尽可能少地篡改或改变。

  • 数字调查的过程必须易于理解,并且接受审查。最好是,调查结果必须能够被独立调查员复现。

特别是第一个原则,在智能手机的情况下尤其具有挑战性,因为大多数智能手机采用特定的操作系统和硬件保护方法,这些方法会阻止对系统中数据的无限制访问。

硬盘的数据保存在大多数情况下是一个简单且熟知的程序。调查员将硬盘从计算机或笔记本中取出,借助写保护器(例如,Tableau TK35)将其连接到工作站,并使用知名且经过认证的软件解决方案进行分析。与此相比,智能手机世界显然没有这样的程序。几乎每款智能手机都有其自己构建存储的方式,因此对于每款智能手机,调查员需要有自己的方法来获取存储的转储数据。虽然从智能手机中获取数据非常困难,但与数据的多样性相比,可以获取更多的数据。智能手机存储的数据,除了常见的文件(例如,照片和文档),还包括 GPS 坐标以及智能手机在关闭之前所连接的移动基站的位置。

考虑到由此产生的机会,结果表明,额外的支出对于调查员而言是值得的。

本章将涵盖以下主题:

  • Eoghan Casey提出的调查模型被智能手机采用

  • Android 智能手机的分析(手动分析以及通过Android 数据提取器 LiteADEL)进行自动化分析)

  • iOS 智能手机的分析

智能手机的调查模型

Eoghan Casey调查过程模型,也被称为楼梯模型,提供了一个实用的、系统的逐步指南,以进行有效的数字调查。该模型被描绘为一系列上升的楼梯,从事件警报或指控开始,到证言结束。这些步骤的设计尽可能通用。该模型试图融合警方职责和法医专家的任务。以下要点解释了调查过程模型的每个步骤以及处理智能手机与计算机之间的差异:

  • 事件警报或指控:指控是整个过程的起始信号。在此阶段,来源将被评估并请求详细的询问。

  • 价值评估:在价值评估范围内,公诉方的利益与起诉犯罪行为所需的成本进行比较。对于公司而言,这通常会导致不提起诉讼(至少对于较小的事件)。起诉的优势在于可能的赔偿、提升自身安全性以及一定的威慑效应。起诉的劣势在于需要资源、在调查过程中可能出现的系统停机无法进行生产性使用,以及大多数时候带来的负面公众扩散效应。

  • 事件或犯罪现场协议:在经典的刑事学中,通常要求对犯罪现场进行大范围封闭。Eoghan Casey 表达了以下观点:

    “冻结”证据并为随后的所有活动提供“真实依据”。

    对于不同种类的数字痕迹,必须根据个别情况检查冷冻过程的具体定义。总的来说,必须最大限度地减少改变痕迹的风险。对于智能手机,这意味着它们必须放入一个连接到外部电源的法拉第袋中。

  • 身份识别或扣押:在传统的扣押过程中,所有可能作为证据的物品和对象都会被收取。在这里,重要的是不能对证据进行任何更改。此外,证据的环境可能具有重大意义。扣押时,证据链同时开始。关于扣押的推荐读物是由美国司法部出版的小册子《电子犯罪现场调查:初步响应者指南》。该小册子为非技术人员提供了准确且详细的建议。另一个好的来源是文件《搜查和扣押计算机及获取电子证据的刑事调查》,同样由美国司法部出版。

  • 保留:在确保证据的过程中,必须确保这些证据未被修改。这就是为什么所有证据都会被记录、拍照、封存,然后锁起来。在数字证据的情况下,这意味着首先创建证据的副本;进一步的调查仅在副本上进行。为了证明证据副本的真实性,使用加密哈希函数。通常情况下,这对于手机取证是最困难的部分,因为某些类型的手机无法创建一对一的副本。我们将在接下来的部分中展示如何创建可以在调查中使用的备份。

  • 恢复Eoghan Casey将恢复描述为撒网。特别是这一阶段包括恢复已删除、隐藏、掩盖或以其他方式无法访问的证据。建议利用与其他证据的协同作用。例如,在需要读取加密数据的情况下,合理的做法是测试是否在犯罪现场找到了包含密码的便签。

  • 采集:在证据分析过程中,需要一个结构良好的组织来处理大量的数据。因此,应该首先调查元数据,而不是实际数据。例如,数据可以根据文件类型或访问时间进行分组。这直接引导到下一个阶段——数据缩减

  • 缩减:缩减任务的目的是消除不相关的数据。也可以使用元数据来实现这一目的。例如,数据可以根据数据类型进行缩减。一个合适的场景是,如果指控允许,可以将所有数据缩减为图像数据。该阶段的结果是——根据Eoghan Casey的说法:

    最小的数字信息集合,具有最高的潜力来包含有证明价值的数据。

    这意味着找到最小的、最有可能相关且具有证明价值的数据。在这种情况下,已知文件的哈希数据库,例如国家软件参考库NIST),有助于排除已知文件(我们在第二章中描述了如何使用这个库,法医算法)。

  • 组织与搜索:组织的方面包括结构化数据并使其能够进行扫描。因此,通常会创建索引和概览,或者根据其类型将文件排序到有意义的目录中。这简化了后续步骤中数据的引用。

  • 分析:此阶段包括对文件内容的详细分析。其中特别需要绘制数据与人员之间的联系,以确定负责人员。此外,内容和背景的评估是根据手段、动机和机会进行的。在此步骤中,实验对于确定未记录的行为和开发新方法是有帮助的。所有结果需要经过测试,并且应该能够通过科学方法进行验证。

  • 报告:报告不仅仅是呈现结果,还需要展示如何得出这些结果。为此,所有考虑的规则和标准应当记录下来。此外,所有得出的结论需要有依据,并且需要讨论其他解释模型。

  • 说服与证词:最后,证人出庭作证,提供关于该主题的权威意见。最重要的方面是该权威的可信度。例如,技术排斥的听众或辩护律师提出的困难类比可能会成为问题。

通过查看前面描述的过程,可以发现处理智能手机时,与其他类型证据的处理几乎没有太大变化。然而,对于调查人员来说,了解在哪些步骤上需要特别注意是非常重要的。

Android

我们将通过 Python 帮助检查的第一个移动操作系统是 Android。在第一小节中,我们将展示如何手动检查智能手机,接下来是使用 ADEL 的自动化方法。最后,我们将演示如何将分析结果的数据合并,创建移动轨迹。

手动检查

第一步是获取智能手机的 root 权限。这样做是为了绕过内部系统保护并访问所有数据。获取 root 权限对大多数手机来说是不同的,并且强烈依赖于操作系统版本。最好的方法是创建自己的恢复镜像并通过内置的恢复模式启动手机。

获取 root 权限后,下一步是尝试获取明文屏幕锁,因为这个秘密通常用于不同的保护措施(例如,屏幕锁可以作为手机应用程序的密码)。破解 PIN 码或密码的屏幕锁可以通过以下脚本完成:

import os, sys, subprocess, binascii, struct
import sqlite3 as lite

def get_sha1hash(backup_dir):

    # dumping the password/pin from the device
    print "Dumping PIN/Password hash ..."
    password = subprocess.Popen(['adb', 'pull', '/data/system/password.key', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    password.wait()

    # cutting the HASH within password.key
    sha1hash = open(backup_dir + '/password.key', 'r').readline()[:40]
    print "HASH: \033[0;32m" + sha1hash + "\033[m"

    return sha1hash

def get_salt(backup_dir):

    # dumping the system DB containing the SALT
    print "Dumping locksettings.db ..."
    saltdb = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb.wait()
    saltdb2 = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db-wal', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb2.wait()
    saltdb3 = subprocess.Popen(['adb', 'pull', '/data/system/locksettings.db-shm', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    saltdb3.wait()

    # extract the SALT
    con = lite.connect(backup_dir + '/locksettings.db')
    cur = con.cursor()    
    cur.execute("SELECT value FROM locksettings WHERE name='lockscreen.password_salt'")
    salt = cur.fetchone()[0]
    con.close()

    # convert SALT to Hex
    returnedsalt =  binascii.hexlify(struct.pack('>q', int(salt) ))
    print "SALT: \033[0;32m" + returnedsalt + "\033[m"

    return returnedsalt

def write_crack(salt, sha1hash, backup_dir):

    crack = open(backup_dir + '/crack.hash', 'a+')

    # write HASH and SALT to cracking file
    hash_salt = sha1hash + ':' + salt
    crack.write(hash_salt)
    crack.close()

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory and the crack file used for hashcat
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    sha1hash = get_sha1hash(backup_dir)
    salt = get_salt(backup_dir)
    write_crack(salt, sha1hash, backup_dir)

这个脚本会生成一个名为crack.hash的文件,可以用于向hashcat提供数据以进行屏幕锁的暴力破解。如果智能手机的用户使用了 4 位数的 PIN 码,执行 hashcat 的命令如下:

user@lab:~$ ./hashcat -a 3 -m 110 out/crack.hash -1 ?d ?1?1?1?1
Initializing hashcat v0.50 with 4 threads and 32mb segment-size...

Added hashes from file crack.hash: 1 (1 salts)
Activating quick-digest mode for single-hash with salt

c87226fed37977772be870d722c449f915844922:256c05b54b73308b:0420

All hashes have been recovered

Input.Mode: Mask (?1?1?1?1) [4]
Index.....: 0/1 (segment), 10000 (words), 0 (bytes)
Recovered.: 1/1 hashes, 1/1 salts
Speed/sec.: - plains, 7.71k words
Progress..: 7744/10000 (77.44%)
Running...: 00:00:00:01
Estimated.: --:--:--:--

Started: Sat Jul 20 17:14:52 2015
Stopped: Sat Jul 20 17:14:53 2015

通过查看输出中的标记行,可以看到 sha256 哈希值,后面跟着盐值和用于解锁屏幕的暴力破解的 PIN 码。

如果智能手机用户使用了手势解锁,你可以使用一个预生成的彩虹表和以下脚本:

import hashlib, sqlite3, array, datetime
from binascii import hexlify

SQLITE_DB = "GestureRainbowTable.db"

def crack(backup_dir):

    # dumping the system file containing the hash
    print "Dumping gesture.key ..."

    saltdb = subprocess.Popen(['adb', 'pull', '/data/system/gesture.key', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

    gesturehash = open(backup_dir + "/gesture.key", "rb").readline()
    lookuphash = hexlify(gesturehash).decode()
    print "HASH: \033[0;32m" + lookuphash + "\033[m"

    conn = sqlite3.connect(SQLITE_DB)
    cur = conn.cursor()
    cur.execute("SELECT pattern FROM RainbowTable WHERE hash = ?", (lookuphash,))
    gesture = cur.fetchone()[0]

    return gesture

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory and the crack file used for hashcat
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    gesture = crack(backup_dir)

    print "screenlock gesture: \033[0;32m" + gesture + "\033[m""

在寻找潜在感染设备时,另一个可能非常重要的因素是已安装应用的列表及其哈希值,以便将它们与AndroTotalMobile-Sandbox进行对比。可以通过以下脚本完成此操作:

import os, sys, subprocess, hashlib

def get_apps():

    # dumping the list of installed apps from the device
    print "Dumping apps meta data ..."

    meta = subprocess.Popen(['adb', 'shell', 'ls', '-l', '/data/app'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    meta.wait()

    apps = []
    while True:
        line = meta.stdout.readline()
        if line != '':
            name = line.split(' ')[-1].rstrip()
            date = line.split(' ')[-3]
            time = line.split(' ')[-2]
            if name.split('.')[-1] == 'apk':
                app = [name, date, time]
            else:
                continue
        else:
            break
        apps.append(app)

    return apps

def dump_apps(apps, backup_dir):

    # dumping the apps from the device
    print "Dumping the apps ..."

    for app in apps:
        app = app[0]
        subprocess.Popen(['adb', 'pull', '/data/app/' + app, backup_dir], 
            stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

def get_hashes(apps, backup_dir):

    # calculating the hashes
    print "Calculating the sha256 hashes ..."

    meta = []
    for app in apps:
        sha256 = hashlib.sha256(open(backup_dir + '/' + app[0], 'rb').read()).hexdigest()
        app.append(sha256)
        meta.append(app)

    return meta

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    apps = get_apps()
    dump_apps(apps, backup_dir)
    meta = get_hashes(apps, backup_dir)

    # printing the list of installed apps
    print 'Installed apps:'
    for app in meta:
        print "\033[0;32m" + ' '.join(app) + "\033[m"

执行前述打印脚本后,您将得到以下输出,包括重要的元数据:

user@lab:~$ ./get_installed_apps.py out

Dumping apps meta data ...
Dumping the apps ...
Calculating the sha256 hashes ...

Installed apps:
com.android.SSLTrustKiller-1.apk 2015-05-18 17:11 52b4d6a1888a6514b62f6607cebf8c2c2aa4e4857319ec67b24be601db5243fb
com.android.chrome-2.apk 2015-06-16 20:50 191cd720626df38eaedf3301826e72330493cdeb8c45da4e309939cfe5633d61
com.android.vending-1.apk 2015-07-25 12:05 7be9f8f99e8c1a6c3be1edb01d84aba14619e3c67c14856755523413ba8e2d98
com.google.android.GoogleCamera-2.apk 2015-06-16 20:49 6936f3c17948c767550c206ff0ae0f44f1f4da0fcb85125da722e0c709787894
com.google.android.apps.authenticator2-1.apk 2015-06-05 10:14 11bcfcf1c853b1eb567c9453507c3413b09a1d70fd3085013f4a091719560ab6
...

通过这些信息,您可以将应用程序与在线服务进行对比,以了解它们是否安全可用,或是否潜在恶意。如果您不想提交它们,您可以结合使用apk_analyzer.py脚本和Androguard进行快速分析,这通常能揭示出重要信息。

在获取所有已安装应用的列表并检查它们是否存在恶意行为后,获取设备的所有分区和挂载点信息也非常有用。可以通过以下脚本实现这一点:

import sys, subprocess

def get_partition_info():

    # dumping the list of installed apps from the device
    print "Dumping partition information ..."

    partitions = subprocess.Popen(['adb', 'shell', 'mount'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    partitions.wait()

    while True:
        line = partitions.stdout.readline().rstrip()
        if line != '':
            print "\033[0;32m" + line + "\033[m"
        else:
            break

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    get_partition_info()

根手机的输出可能如下所示:

user@lab:~$ ./get_partitions.py 

Dumping partition information ...
rootfs / rootfs rw,relatime 0 0
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0
devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
proc /proc proc rw,relatime 0 0
sysfs /sys sysfs rw,seclabel,relatime 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
none /acct cgroup rw,relatime,cpuacct 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
tmpfs /mnt/asec tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
tmpfs /mnt/obb tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
none /dev/cpuctl cgroup rw,relatime,cpu 0 0
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/userdata /data ext4 rw,seclabel,nosuid,nodev,noatime,nomblk_io_submit,noauto_da_alloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/cache /cache ext4 rw,seclabel,nosuid,nodev,noatime,nomblk_io_submit,noauto_da_alloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/persist /persist ext4 rw,seclabel,nosuid,nodev,relatime,nomblk_io_submit,nodelalloc,errors=panic,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/modem /firmware vfat ro,relatime,uid=1000,gid=1000,fmask=0337,dmask=0227,codepage=cp437,iocharset=iso8859-1,shortname=lower,errors=remount-ro 0 0
/dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

在本节的最后,我们将向您展示如何收集更多有关基于安卓智能手机使用情况的详细信息。在以下示例中,我们将使用联系人数据库,该数据库还存储着通话记录。这个示例可以轻松地被调整,以获取日历条目或从任何其他已安装应用的数据库中提取内容:

import os, sys, subprocess
import sqlite3 as lite
from prettytable import from_db_cursor

def dump_database(backup_dir):

    # dumping the password/pin from the device
    print "Dumping contacts database ..."

    contactsDB = subprocess.Popen(['adb', 'pull', '/data/data/com.android.providers.contacts/databases/contacts2.db', 
        backup_dir], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    contactsDB.wait()

def get_content(backup_dir):

    # getting the content from the contacts database
    con = lite.connect(backup_dir + '/contacts2.db')
    cur = con.cursor()    
    cur.execute("SELECT contacts._id AS _id,contacts.custom_ringtone AS custom_ringtone, name_raw_contact.display_name_source AS display_name_source, name_raw_contact.display_name AS display_name, name_raw_contact.display_name_alt AS display_name_alt, name_raw_contact.phonetic_name AS phonetic_name, name_raw_contact.phonetic_name_style AS phonetic_name_style, name_raw_contact.sort_key AS sort_key, name_raw_contact.phonebook_label AS phonebook_label, name_raw_contact.phonebook_bucket AS phonebook_bucket, name_raw_contact.sort_key_alt AS sort_key_alt, name_raw_contact.phonebook_label_alt AS phonebook_label_alt, name_raw_contact.phonebook_bucket_alt AS phonebook_bucket_alt, has_phone_number, name_raw_contact_id, lookup, photo_id, photo_file_id, CAST(EXISTS (SELECT _id FROM visible_contacts WHERE contacts._id=visible_contacts._id) AS INTEGER) AS in_visible_group, status_update_id, contacts.contact_last_updated_timestamp, contacts.last_time_contacted AS last_time_contacted, contacts.send_to_voicemail AS send_to_voicemail, contacts.starred AS starred, contacts.pinned AS pinned, contacts.times_contacted AS times_contacted, (CASE WHEN photo_file_id IS NULL THEN (CASE WHEN photo_id IS NULL OR photo_id=0 THEN NULL ELSE 'content://com.android.contacts/contacts/'||contacts._id|| '/photo' END) ELSE 'content://com.android.contacts/display_photo/'||photo_file_id END) AS photo_uri, (CASE WHEN photo_id IS NULL OR photo_id=0 THEN NULL ELSE 'content://com.android.contacts/contacts/'||contacts._id|| '/photo' END) AS photo_thumb_uri, 0 AS is_user_profile FROM contacts JOIN raw_contacts AS name_raw_contact ON(name_raw_contact_id=name_raw_contact._id)")
    pt = from_db_cursor(cur)
    con.close()

    print pt    

if __name__ == '__main__':

    # check if device is connected and adb is running as root
    if subprocess.Popen(['adb', 'get-state'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "unknown":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    dump_database(backup_dir)
    get_content(backup_dir)

在您了解如何手动分析智能手机后,我们将在接下来的部分向您展示如何借助 ADEL 自动执行相同的操作。

借助 ADEL 进行自动化检查

我们开发了一个名为 ADEL 的工具。最初,这个工具是为 Android 2.x 版本开发的,但随后更新以适应分析 Android 4.x 智能手机的需求。该工具能够自动从 Android 设备中转储所选的 SQLite 数据库文件,并提取存储在转储文件中的内容。作为进一步的选项,ADEL 还能够分析那些事先手动转储的数据库。此选项的实现是为了支持那些由于安全功能(如锁定引导加载程序)使 ADEL 无法访问设备文件系统的智能手机。在接下来的部分,我们将描述 ADEL 的主要任务以及该工具实际执行的步骤。

系统背后的理念

在 ADEL 的开发过程中,我们主要考虑了以下设计指南:

  • 取证原则:ADEL 旨在以取证正确的方式处理数据。这个目标是通过以下方式实现的:操作不是直接在手机上进行,而是在数据库的副本上进行。此过程确保数据不会被 ADEL 的用户或受损操作系统修改。为了提供 ADEL 取证正确性的证明,分析前后会计算哈希值,以确保转储数据在分析过程中未被修改。

  • 可扩展性:ADEL 是模块化构建的,包含两个独立的模块:分析模块和报告模块。这两个模块之间存在预定义的接口,并且都可以通过附加功能轻松修改。模块化结构使得你能够轻松地转储和分析更多智能手机的数据库,同时为未来系统的更新提供便利。

  • 可用性:ADEL 的使用目标是尽可能简单,以便专业人员和非专家都能使用。最好是,手机的分析能够自动进行,以便用户不会收到任何内部过程的通知。此外,报告模块会生成一份详细的报告,报告以可读的形式包含所有解码后的数据。在执行过程中,ADEL 可选地会写入一份详细的日志文件,记录执行的所有重要步骤。

实现和系统工作流程

显示 ADEL 结构的流程图如下所示:

实现和系统工作流程

ADEL 使用 Android 软件开发工具包 (Android SDK) 将数据库文件转储到调查员的计算机中。为了提取 SQLite 数据库文件中的内容,ADEL 会解析底层数据结构。在以只读模式打开要解析的数据库文件后,ADEL 会读取数据库头部(文件的前 100 字节)并提取每个头部字段的值。并非所有字段值都需要,但某些头部字段的值对于解析数据库文件的其余部分是必要的。一个重要的值是数据库文件中页面的大小,这对于解析 B 树结构(逐页面)至关重要。在读取完数据库头部字段后,ADEL 会解析包含 sqlite_master 表的 B 树,其中数据库的第一页面始终是根页面。对于每个数据库表,都会提取 SQL CREATE 语句和 B 树根页面的页面编号。此外,SQL CREATE 语句还会进一步分析,以提取对应表中每列的名称和数据类型。

最后,完整的 B 树结构会为每个表解析,从 sqlite_master 表提取的 B 树根页面开始。通过跟踪所有内部页面的指针,可以识别出 B 树的每个叶子页面。最终,每个表的行内容会从属于该表 B 树的任何叶子页面中的单元格提取出来。

在接下来的章节中,我们将介绍报告模块及其功能。在当前开发阶段,以下数据库将进行取证处理和解析:

  • 电话和 SIM 卡信息(例如 国际移动用户身份 (IMSI) 和序列号)

  • 电话簿和通话记录

  • 日历条目

  • 短信消息

  • Google 地图

通过这种方式检索到的数据会被报告模块写入一个 XML 文件,以便于后续使用和数据呈现。与分析模块类似,它可以轻松更新,以适应未来 Android 版本或底层数据库模式的可能变化。因此,我们创建了不同的元组——例如,[表,行,列]——来定义在两个模块之间交换的数据。如果未来数据库设计发生变化,只需要调整元组即可。报告模块会自动为每种之前列出的数据类型创建 XML 文件。此外,还会生成一份报告,包含从分析的数据库中提取的所有数据。通过 XSL 文件的帮助,报告将被图形化展示。所有由 ADEL 创建的文件都会存储在当前项目的子文件夹中。

为了访问智能手机上的必要数据库和系统文件夹,ADEL 需要设备的 root 权限。

与 ADEL 一起使用

在我们描述了 ADEL 是什么以及它如何工作之后,接下来我们将进入本节的实际部分并开始使用它。你可以从以下网址下载 ADEL:mspreitz.github.io/ADEL

你需要做的就是检查设备是否已经包含在 ADEL 的配置文件 /xml/phone_config.xml 中。如果设备缺失,有两种方式可以继续操作:

  1. 选择一个相同 Android 版本的不同设备(这会生成一个警告,但在大多数情况下是有效的)。

  2. 生成一个新的设备配置,匹配目标设备的设备类型和 Android 版本。

如果选择第二种方式,你可以复制一个已经正常工作的设备的配置,并采用 XML 文件中的数字。这些数字表示已记录数据库中的表和列。更准确地说,如果你尝试采用 SMS 数据库,你必须检查以下表和列的数字:

<sms>
  <db_name>mmssms.db</db_name>
  <table_num>10</table_num>
  <sms_entry_positions> 
    <id>0</id>
    <thread_id>1</thread_id>
    <address>2</address>
    <person>3</person>
    <date>4</date>
    <read>7</read>
    <type>9</type>
    <subject>11</subject>
    <body>12</body>
  </sms_entry_positions>
</sms>

table_num 标签的数字必须设置为与名为 sms 的表对应的数字。以下数字必须与 sms 表中名称相同的列匹配。前面的打印示例适用于 Nexus 5 和 Android 4.4.4。其他数据库也需要进行相同的操作。

在一台已 root 的 Nexus 5 上运行 ADEL,设备安装了 Android 4.4.4 并填充了测试数据——将生成以下输出:

user@lab:~$./adel.py -d nexus5 -l 4

 _____  ________  ___________.____
 /  _  \ \______ \ \_   _____/|    |
 /  /_\  \ |    |  \ |    __)_ |    |
 /    |    \|    `   \|        \|    |___
 \____|__  /_______  /_______  /|_______ \ 
 \/        \/        \/         \/
 Android Data Extractor Lite v3.0

ADEL MAIN:     ----> starting script....
ADEL MAIN:     ----> Trying to connect to smartphone or emulator....
dumpDBs:       ----> opening connection to device: 031c6277f0a6a117
dumpDBs:       ----> evidence directory 2015-07-20__22-53-22__031c6277f0a6a117 created
ADEL MAIN:     ----> log file 2015-07-20__22-53-22__031c6277f0a6a117/log/adel.log created
ADEL MAIN:     ----> log level: 4
dumpDBs:       ----> device is running Android OS 4.4.4
dumpDBs:       ----> dumping all SQLite databases....
dumpDBs:       ----> auto dict doesn't exist!
dumpDBs:       ----> weather database doesn't exist!
dumpDBs:       ----> weather widget doesn't exist!
dumpDBs:       ----> Google-Maps navigation history doesn't exist!
dumpDBs:       ----> Facebook database doesn't exist!
dumpDBs:       ----> Cached geopositions within browser don't exist!
dumpDBs:       ----> dumping pictures (internal_sdcard)....
dumpDBs:       ----> dumping pictures (external_sdcard)....
dumpDBs:       ----> dumping screen captures (internal_sdcard)....
dumpDBs:       ----> dumping screen captures (internal_sdcard)....
dumpDBs:       ----> all SQLite databases dumped
Screenlock:    ----> Screenlock Hash: 6a062b9b3452e366407181a1bf92ea73e9ed4c48
Screenlock:    ----> Screenlock Gesture: [0, 1, 2, 4, 6, 7, 8]
LocationInfo:  ----> Location map 2015-07-20__22-53-22__031c6277f0a6a117/map.html created
analyzeDBs:    ----> starting to parse and analyze the databases....
parseDBs:      ----> starting to parse smartphone info
parseDBs:      ----> starting to parse calendar entries
parseDBs:      ----> starting to parse SMS messages
parseDBs:      ----> starting to parse call logs
parseDBs:      ----> starting to parse address book entries
analyzeDBs:    ----> all databases parsed and analyzed....
createReport:  ----> creating report....
ADEL MAIN:     ----> report 2015-07-20__22-53-22__031c6277f0a6a117/xml/report.xml created
compareHash:   ----> starting to compare calculated hash values
ADEL MAIN:     ----> stopping script....

 (c) m.spreitzenbarth & s.schmitt 2015

在此输出中,你可以看到所有数据存储的文件夹名称,以及生成的报告所在位置。此外,你还可以看到自动提取的屏幕锁手势,并与预先生成的彩虹表进行比较,如下所示:

与 ADEL 一起使用

移动配置文件

除了关于个别通信的数据外,2006 年的欧盟指令还要求网络运营商保留某些位置数据。特别是,该指令要求以下数据至少保留六个月:

  • 用户开始电话通话时所在无线电小区的身份和准确的 GPS 坐标

  • GPRS 数据传输开始时活动的无线电小区的身份和坐标

  • 与这些数据相关的时间戳

这些信息可以帮助调查人员创建嫌疑人的运动档案。此外,这些信息还可以用来定位和监视嫌疑人。

许多欧盟成员国已将此指令纳入国家法律。然而,在一些国家,关于这些法律的公众辩论非常激烈,特别是与隐私威胁相关的讨论。在德国,这些讨论因德国政治家马尔特·斯皮茨提供的数据集而愈加激烈。该数据集包含了六个月的位置信息,这些数据在数据保留法下由他的移动网络运营商保存。一家德国报纸创建了一个图形界面,使用户能够直观地重播斯皮茨的详细行动。

总体来说,有人认为,保留大量数据会带来新的滥用风险。此外,要求存储与数百万无辜人相关的数据,与执法部门仅在少数情况下使用这些数据的情况不成比例。因此,在 2011 年,德国宪法法院驳回了要求数据保留的原始立法。同时,寻找不那么侵入性的技术来分析犯罪分子的活动仍在继续。

近年来,许多新型手机(智能手机)涌入市场。由于它们本质上是小型个人计算机,因此它们提供的功能远远超过了打电话和上网。越来越多的用户使用应用程序(主要是直接安装在手机上的第三方应用程序),并通过社交网络如 Facebook、Google+ 和 Twitter 与朋友和家人进行沟通。

出于性能等原因,移动设备会在本地存储位置数据。2011 年 4 月,报道称安卓和 iOS 系统存储敏感的地理数据。这些数据保存在系统缓存文件中,并定期发送给平台开发者。然而,生成地理数据并不限于操作系统——许多提供基于位置服务的应用程序也会创建和存储此类数据。例如,本福德曾展示过用 iPhone 拍摄的照片包含拍摄位置的 GPS 坐标。这些数据具有敏感性,因为它们可以用于创建运动档案,如下图所示。与网络运营商保留的位置信息不同,存储在智能手机上的位置数据可以通过公开扣押供执法机构访问。

运动档案

Apple iOS

在我们了解了如何检查 Android 系统的智能手机后,接下来我们将展示如何在基于 iOS 的设备上执行类似的调查。在第一部分,我们使用 Secure ShellSSH)连接到设备,并向您展示如何从越狱的 iOS 设备的钥匙串中获取存储的数据。

在本节的第二部分,我们将使用 libimobiledevice。这个库是一个跨平台的库,使用协议支持 iOS 设备,并允许您轻松访问设备的文件系统,检索设备及其内部信息,备份/恢复设备,管理已安装的应用程序,检索个人信息管理(PIM)数据以及书签等等。最重要的一点是,iOS 设备不需要越狱即可使用 libimobiledevice。

从越狱的 iDevice 获取钥匙串

在许多情况下,获取用户在 iDevice 上使用的帐户的用户名和密码会非常有帮助。这类数据位于 iOS 钥匙串中,并可以通过以下脚本从 iDevice 中提取:

import os, sys, subprocess

def get_kc(ip, backup_dir):

    # dumping the keychain
    print "Dumping the keychain ..."

    kc = subprocess.Popen(['scp', 'root@' + ip + ':/private/var/Keychains/keychain-2.db', backup_dir], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kc.communicate()

def push_kcd(ip):

    # dumping the keychain
    print "Pushing the Keychain Dumper to the device ..."

    kcd = subprocess.Popen(['scp', 'keychain_dumper' 'root@' + ip + ':~/'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kcd.communicate()

def exec_kcd(ip, backup_dir):
    # pretty print keychain
    kcc = subprocess.Popen(['ssh', 'root@' + ip, './keychain_dumper'], 
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    kcc.communicate()
    kcc.stdout

if __name__ == '__main__':

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    # get the IP of the iDevice from user input
    ip = sys.argv[2]

    get_kc(ip, backup_dir)
    push_kcd(ip)
    exec_kcd(ip, backup_dir)

在前面脚本的输出中,您还可以找到设备注册的 Apple 帐户的密码:

Generic Password
----------------
Service: com.apple.account.AppleAccount.password
Account: 437C2D8F-****-****-****-************
Entitlement Group: apple
Label: (null)
Generic Field: (null)
Keychain Data: *************************

使用 libimobiledevice 进行手动检查

这个库使用常见的 iOS 协议来进行调查者的机器与连接的 iDevice 之间的通信。为了正确工作,设备必须解锁并配对,因为否则设备上的大量数据仍然是加密的,从而受到保护。

通过以下脚本,您可以创建设备的完整备份(类似于 iTunes 备份)。之后,脚本将解包备份并打印备份中所有文件和文件夹的层级列表。根据 iDevice 的大小,脚本可能会运行几分钟。

import os, sys, subprocess

def get_device_info():

    # getting the udid of the connected device
    udid = subprocess.Popen(['idevice_id', '-l'], stdout=subprocess.PIPE).stdout.readline().rstrip()

    print "connected device: \033[0;32m" + udid + "\033[m"
    return udid

def create_backup(backup_dir):

    # creating a backup of the connected device
    print "creating backup (this can take some time) ..."

    backup = subprocess.Popen(['idevicebackup2', 'backup', backup_dir], stdout=subprocess.PIPE)
    backup.communicate()

    print "backup successfully created in ./" + backup_dir + "/"

def unback_backup(udid, backup_dir):

    # unpacking the backup
    print "unpacking the backup ..."

    backup = subprocess.Popen(['idevicebackup2', '-u', udid, 'unback', backup_dir], stdout=subprocess.PIPE)
    backup.communicate()

    print "backup successfully unpacked and ready for analysis"

def get_content(backup_dir):

    # printing content of the created backup
    content = subprocess.Popen(['tree', backup_dir + '/_unback_/'], stdout=subprocess.PIPE).stdout.read()
    f = open(backup_dir + '/filelist.txt', 'a+')
    f.write(content)
    f.close

    print "list of all files and folders of the backup are stored in ./" + backup_dir + "/filelist.txt"

if __name__ == '__main__':

    # check if device is connected
    if subprocess.Popen(['idevice_id', '-l'], stdout=subprocess.PIPE).communicate(0)[0].split("\n")[0] == "":
        print "no device connected - exiting..."
        sys.exit(2)

    # starting to create the output directory
    backup_dir = sys.argv[1]

    try:
        os.stat(backup_dir)
    except:
        os.mkdir(backup_dir)

    udid = get_device_info()
    create_backup(backup_dir)
    unback_backup(udid, backup_dir)
    get_content(backup_dir)

该脚本的最终输出将类似于以下内容:

user@lab:~$ ./create_ios_backup.py out

connected device: 460683e351a265a7b9ea184b2802cf4fcd02526d
creating backup (this can take some time) ...
backup successfully created in ./out
unpacking the backup ...
backup successfully unpacked and ready for analysis
list of all files and folders of the backup are stored in ./out/filelist.txt

借助文件和文件夹的列表,您可以使用常见的工具,如 plist 文件查看器或 SQLite 浏览器,开始分析备份。搜索该生成文件中的 Cydia App Store 也有助于识别智能手机是否已被用户或攻击者越狱。

总结

在本章中,我们介绍了 Eoghan Casey 的调查过程模型,并将其应用到智能手机的案例中。随后,我们通过 Python 脚本和 ADEL 框架以手动和自动化的方式对 Android 智能手机进行了分析。在最后一节中,我们介绍了基于 iOS 的智能手机的分析。

在处理智能手机的取证调查后,我们完成了物理和虚拟获取及分析,并将在下一章中将调查转移到设备的易失性区域。

第七章. 使用 Python 进行内存取证

现在你已经在基础设施中进行了调查(参见第四章,使用 Python 进行网络取证),常见的 IT 设备(参见第三章,使用 Python 进行 Windows 和 Linux 取证),甚至在虚拟化环境(参见第五章,使用 Python 进行虚拟化取证)和移动世界(参见第六章,使用 Python 进行移动取证)中进行了调查,在本章中,我们将向你展示如何使用基于 Python 的取证框架 Volatility,在以下平台上对易失性内存进行调查:

  • Android

  • Linux

在向你展示了一些适用于 Android 和 Linux 的基本 Volatility 插件,并说明如何获取所需的 RAM 转储进行分析之后,我们将开始在 RAM 中寻找恶意软件。因此,我们将使用基于模式匹配的 YARA 规则,并将其与 Volatility 的强大功能结合起来。

理解 Volatility 基础

一般来说,内存取证遵循与其他取证调查相同的模式:

  1. 选择调查目标。

  2. 获取取证数据。

  3. 取证分析。

在前面的章节中,我们已经介绍了多种选择调查目标的技术,例如,从虚拟化层中具有异常设置的系统开始。

内存分析的取证数据获取高度依赖于环境,我们将在本章的在 Linux 上使用 Volatility在 Android 上使用 Volatility部分进行讨论。

提示

始终将虚拟化层视为数据源

从正在运行的操作系统中获取内存始终需要对该系统的管理员权限,并且这是一个侵入性的过程,也就是说,数据获取过程会改变内存数据。此外,先进的恶意软件能够操控操作系统的内存管理,以防止其被获取。因此,始终按照第五章,使用 Python 进行虚拟化取证中所描述的方法,检查并尽量在虚拟机监控程序层面获取内存。

到目前为止,用于内存数据分析的最重要工具是Volatility。Volatility 可在Volatility Foundation网站上获取。

该工具用 Python 编写,可以在 GNU 通用公共许可证GPL)第 2 版的条款下免费使用。Volatility 能够读取多种文件格式的内存转储,例如,休眠文件、原始内存转储、VMware 内存快照文件,以及将会在本章后面讨论的由 LiME 模块生成的 Linux 内存提取器LiME)格式。

Volatility 世界中最重要的术语如下:

  • 配置文件:配置文件帮助 Volatility 解释内存偏移量和内存结构。配置文件取决于操作系统,尤其是操作系统内核、机器和 CPU 架构。Volatility 包含许多适用于最常见用例的配置文件。在本章的 在 Linux 上使用 Volatility 部分中,我们将介绍如何创建您的配置文件。

  • 插件:插件用于对内存转储执行操作。您使用的每个 Volatility 命令都会调用一个插件来执行相应的操作。例如,要获取在 Linux 系统内存转储期间运行的所有进程的列表,可以使用 linux_pslist 插件。

Volatility 提供了全面的文档,我们建议您熟悉所有模块描述,以便充分利用 Volatility。

在 Android 上使用 Volatility

要分析 Android 设备的易失性内存,首先需要 LiME。LiME 是一个可加载内核模块LKM),它可以访问设备的整个 RAM,并将其转储到物理 SD 卡或网络中。在使用 LiME 获取易失性内存转储后,我们将向您展示如何安装和配置 Volatility 以解析 RAM 转储。在最后一节中,我们将演示如何从 RAM 转储中提取特定信息。

LiME 和恢复映像

LiME 是一个可加载内核模块(LKM),它允许从 Linux 和基于 Linux 的设备(如 Android)获取易失性内存。这使得 LiME 非常独特,因为它是第一个可以在 Android 设备上进行完整内存捕获的工具。它还最小化了在获取过程中用户空间和内核空间进程之间的交互,从而使其生成的内存捕获比其他为 Linux 内存获取设计的工具更加法医可靠。

为了在 Android 上使用 LiME,必须为设备上使用的内核进行交叉编译。在接下来的章节中,我们将展示如何在 Nexus 4 上为 Android 4.4.4 执行这些步骤(不过,这种方法可以适配到任何 Android 设备,只要该设备的内核——或者至少是内核配置——作为开源提供)。

首先,我们需要在实验室系统上安装一些额外的软件包,具体如下:

user@lab:~$ sudo apt-get install bison g++-multilib git gperf libxml2-utils make python-networkx zlib1g-dev:i386 zip openjdk-7-jdk

安装完所有必要的软件包后,我们现在需要配置对 USB 设备的访问。在 GNU/Linux 系统下,普通用户默认无法直接访问 USB 设备。系统需要配置以允许这种访问。通过以 root 用户身份创建名为 /etc/udev/rules.d/51-android.rules 的文件,并在其中插入以下内容来实现这一点:

# adb protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e12", MODE="0600", OWNER="user"
# fastboot protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0fff", MODE="0600", OWNER="user"
# adb protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e22", MODE="0600", OWNER="user"
# fastboot protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e20", MODE="0600", OWNER="user"
# adb protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="70a9", MODE="0600", OWNER="user"
# fastboot protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="708c", MODE="0600", OWNER="user"
# adb protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6860", MODE="0600", OWNER="user"
# fastboot protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e30", MODE="0600", OWNER="user"
# adb protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d101", MODE="0600", OWNER="user"
# adb protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="d002", MODE="0600", OWNER="user"
# fastboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d022", MODE="0600", OWNER="user"
# usbboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d00f", MODE="0600", OWNER="user"
# usbboot protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d010", MODE="0600", OWNER="user"
# adb protocol on grouper/tilapia (Nexus 7)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e42", MODE="0600", OWNER="user"
# fastboot protocol on grouper/tilapia (Nexus 7)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e40", MODE="0600", OWNER="user"
# adb protocol on manta (Nexus 10)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee2", MODE="0600", OWNER="user"
# fastboot protocol on manta (Nexus 10)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee0", MODE="0600", OWNER="user"

现在最耗时的部分来了——检查正在使用的 Android 版本的源代码。根据硬盘和互联网连接的速度,这一步可能需要几个小时,因此请提前规划。此外,请记住源代码文件非常大,所以请使用至少 40 GB 空闲空间的第二个分区。我们按如下方式安装 Android 4.4.4 的源代码:

user@lab:~$ mkdir ~/bin

user@lab:~$ PATH=~/bin:$PATH

user@lab:~$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo

user@lab:~$ chmod a+x ~/bin/repo

user@lab:~$ repo init -u https://android.googlesource.com/platform/manifest -b android-4.4.4_r1

user@lab:~$ repo sync

在我们安装了 Android 4.4.4 的源代码之后,我们现在需要设备上运行的内核源代码。对于我们在此使用的 Nexus 4,正确的内核是 mako 内核。可以在 source.android.com/source/building-kernels.html 找到所有可用的 Google 手机内核的列表。

user@lab:~$ git clone https://android.googlesource.com/device/lge/mako-kernel/kernel

user@lab:~$ git clone https://android.googlesource.com/kernel/msm.git

现在我们已经获得了交叉编译 LiME 所需的所有源代码,接下来是获取 LiME 本身:

user@lab:~$ git clone https://github.com/504ensicsLabs/LiME.git

在将 git 仓库克隆到实验机器上之后,我们需要设置一些在构建过程中需要的环境变量:

user@lab:~$ export SDK_PATH=/path/to/android-sdk-linux/

user@lab:~$ export NDK_PATH=/path/to/android-ndk/

user@lab:~$ export KSRC_PATH=/path/to/kernel-source/

user@lab:~$ export CC_PATH=$NDK_PATH/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/bin/

user@lab:~$ export LIME_SRC=/path/to/lime/src

接下来,我们需要获取目标设备的当前内核配置,并将其复制到 LiME 源代码的正确位置。在我们的 Nexus 4 上,可以通过输入以下命令来完成:

user@lab:~$ adb pull /proc/config.gz

user@lab:~$ gunzip ./config.gz

user@lab:~$ cp config $KSRC_PATH/.config

user@lab:~$ cd $KSRC_PATH 

user@lab:~$ make ARCH=arm CROSS_COMPILE=$CC_PATH/arm-eabi-modules_prepare

在我们构建 LiME 内核模块之前,我们需要编写我们定制的 Makefile:

obj-m := lime.o
lime-objs := main.o tcp.o disk.o
KDIR := /path/to/kernel-source
PWD := $(shell pwd)
CCPATH := /path/to/android-ndk/toolchains/arm-linux-androideabi-4.4.4/prebuilt/linux-x86/bin/
default:
 $(MAKE) ARCH=arm CROSS_COMPILE=$(CCPATH)/arm-eabi- -C $(KDIR) M=$(PWD) modules

借助这个 Makefile,我们可以构建从 Android 设备中提取易失性内存所需的内核模块。输入 make 可以启动该过程。

在接下来的示例中,我们将演示如何将我们新生成的内核模块推送到目标设备,并通过 TCP 将整个易失性内存转储到我们的实验环境中。

如果你的设备的内核不允许动态加载模块,你应该考虑创建自己的恢复镜像(例如,定制版的 TWRPCWM),将 LiME 内核模块包含其中,并将其刷入相关设备。如果在刷机操作过程中足够快速,几乎不会丢失数据(更多信息,请参考 www1.informatik.uni-erlangen.de/frost)。

LiME 模块提供了三种不同的镜像格式,可用于将捕获的内存镜像保存到磁盘上:raw、padded 和 lime。第三种格式——lime——在本文中将详细讨论,因为它是我们首选的格式。lime 格式专门开发用于与 Volatility 配合使用,旨在使得使用 Volatility 进行分析变得更加简便,且为处理该格式,增加了特定的地址空间。基于 lime 格式的每个内存转储都有一个固定大小的头部,包含每个内存范围的特定地址空间信息。这消除了仅为了填充未映射或内存映射 I/O 区域而需要额外填充的需求。LiME 头部规范如下所示:

typedef struct {
  unsigned int magic;         // Always 0x4C694D45 (LiME)
  unsigned int version;         // Header version number
  unsigned long long s_addr;     // Starting address of physical RAM
  unsigned long long e_addr;     // Ending address of physical RAM
  unsigned char reserved[8];     // Currently all zeros
  } __attribute__ ((__packed__)) lime_mem_range_header;

要从相关 Android 设备获取这样的转储,首先通过adb连接到 Android 设备,然后输入以下命令:

user@lab:~$ adb push lime.ko /sdcard/lime.ko
user@lab:~$ adb forward tcp:4444 tcp:4444
user@lab:~$ adb shell
nexus4:~$ su
nexus4:~$ insmod /sdcard/lime.ko "path=tcp:4444 format=lime"

在实验室机器上,输入以下命令,以接受通过 TCP 端口 4444 从 Android 设备发送到本地实验室机器的数据:

user@lab:~$ nc localhost 4444 > nexus4_ram.lime

如果前述命令执行成功,您将得到一个 RAM 转储文件,可以借助 Volatility 或其他工具进行进一步分析(请参见下一节)。

Android 的 Volatility

在通过我们在上一节中创建的工具获取表示目标系统物理内存的转储文件后,我们打算从中提取数据工件。如果不对 Android 的内存结构进行深入分析,我们只能提取已知的文件格式,如 JPEG,或仅提取包含 EXIF 数据的 JPEG 头部(使用工具如PhotoRec),或者提取存储为连续格式的简单 ASCII 字符串(使用常见的 Linux 工具如strings),这些字符串可以用来对相关设备的密码进行暴力破解。这种方法非常有限,因为它适用于任何磁盘或内存转储,但并不专注于操作系统和应用程序特定的结构。由于我们打算从 Android 系统中提取完整的数据对象,因此我们将使用流行的易失性内存取证框架:Volatility

在本节中,我们将使用一个支持 ARM 架构的 Volatility 版本(至少需要版本 2.3)。给定一个内存镜像,Volatility 可以提取正在运行的进程、打开的网络套接字、每个进程的内存映射以及内核模块。

注意

在分析内存镜像之前,必须创建一个 Volatility 配置文件,并将其作为命令行参数传递给 Volatility 框架。这样的 Volatility 配置文件是一组vtype定义和可选的符号地址,Volatility 用它们来定位敏感信息并解析。

基本上,配置文件是一个压缩档案,其中包含以下两个文件:

  • System.map文件包含 Linux 内核中静态数据结构的符号名称和地址。对于 Android 设备,该文件可以在内核编译后,在内核源码树中找到。

  • module.dwarf文件是在编译模块并针对目标内核提取 DWARF 调试信息时生成的。

为了创建module.dwarf文件,需要使用名为dwarfdump的工具。Volatility 源代码树中包含tools/linux目录。如果在该目录下运行make命令,该命令会编译模块并生成所需的 DWARF 文件。创建实际的配置文件只需运行以下命令:

user@lab $ zip Nexus4.zip module.dwarf System.map

生成的 ZIP 文件需要复制到volatility/plugins/overlays/linux目录下。成功复制文件后,配置文件将在 Volatility 帮助输出的配置文件部分显示。

尽管 Volatility 对 Android 的支持相对较新,但已有大量的 Linux 插件在 Android 上也能完美运行。例如:

  • linux_pslist:它枚举系统中所有正在运行的进程,类似于 Linux 的 ps 命令

  • linux_ifconfig:该插件模拟 Linux 的ifconfig命令

  • linux_route_cache:它读取并打印路由缓存,存储最近使用的路由条目在哈希表中的信息

  • linux_proc_maps:该插件获取每个独立进程的内存映射

如果你对如何编写自定义 Volatility 插件并解析达尔文虚拟机DVM)中的未知结构感兴趣,请查看我和我的同事所写的以下论文:冷启动 Android 设备的事后内存分析(参考www1.informatik.uni-erlangen.de/filepool/publications/android.ram.analysis.pdf)。

在下一部分,我们将示范如何借助 LiME 和 Volatility 重建特定的应用数据。

为 Android 重建数据

现在,我们将展示如何在 Volatility 的帮助下以及通过自定义插件重建应用程序数据。因此,我们选择了通话历史和键盘缓存。如果你正在调查一个普通的 Linux 或 Windows 系统,已经有大量的插件可以使用,正如你将在本章的最后部分看到的那样。不幸的是,在 Android 上,你必须编写自己的插件。

通话历史

我们的目标之一是从 Android 内存转储中恢复最近的来电和去电电话列表。此列表在打开电话应用时加载。负责电话应用和通话历史记录的进程是com.android.contacts。该进程加载PhoneClassDetails.java类文件,该文件建模了所有电话通话的数据,保存在历史结构中。每个历史记录条目对应一个类实例。每个实例的数据字段是电话的典型元信息,如下所示:

  • 类型(来电、去电或未接)

  • 时长

  • 日期和时间

  • 电话号码

  • 联系人姓名

  • 联系人指定的照片

为了自动提取并显示这些元数据,我们提供了一个 Volatility 插件,名为dalvik_app_calllog,如下所示:

class dalvik_app_calllog(linux_common.AbstractLinuxCommand):

     def __init__(self, config, *args, **kwargs):
          linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs)
          dalvik.register_option_PID(self._config)
          dalvik.register_option_GDVM_OFFSET(self._config)
          self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None,
          help = 'This is the offset (in hex) of system class PhoneCallDetails.java', action = 'store', type = 'str')

     def calculate(self):
          # if no gDvm object offset was specified, use this one
          if not self._config.GDVM_OFFSET:
               self._config.GDVM_OFFSET = str(hex(0x41b0))

          # use linux_pslist plugin to find process address space and ID if not specified
          proc_as = None
          tasks = linux_pslist.linux_pslist(self._config).calculate()
          for task in tasks:
               if str(task.comm) == "ndroid.contacts":
                    proc_as = task.get_process_address_space()
                    if not self._config.PID:
                         self._config.PID = str(task.pid)
                    break

          # use dalvik_loaded_classes plugin to find class offset if not specified
          if not self._config.CLASS_OFFSET:
              classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate()
              for task, clazz in classes:
                   if (dalvik.getString(clazz.sourceFile)+"" == "PhoneCallDetails.java"):
                        self._config.CLASS_OFFSET = str(hex(clazz.obj_offset))
                        break

          # use dalvik_find_class_instance plugin to find a list of possible class instances
          instances = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate()
          for sysClass, inst in instances:
               callDetailsObj = obj.Object('PhoneCallDetails', offset = inst, vm = proc_as)
               # access type ID field for sanity check
               typeID = int(callDetailsObj.callTypes.contents0)
               # valid type ID must be 1,2 or 3
               if (typeID == 1 or typeID == 2 or typeID == 3):
                    yield callDetailsObj

     def render_text(self, outfd, data):
          self.table_header(outfd, [    ("InstanceClass", "13"),
                                        ("Date", "19"),
                                        ("Contact", "20"),
                                        ("Number", "15"),
                                        ("Duration", "13"),
                                        ("Iso", "3"),
                                        ("Geocode", "15"),
                                        ("Type", "8")                                      
                                        ])
          for callDetailsObj in data:
               # convert epoch time to human readable date and time
               rawDate = callDetailsObj.date / 1000
               date =    str(time.gmtime(rawDate).tm_mday) + "." + \
                         str(time.gmtime(rawDate).tm_mon) + "." + \
                         str(time.gmtime(rawDate).tm_year) + " " + \
                         str(time.gmtime(rawDate).tm_hour) + ":" + \
                         str(time.gmtime(rawDate).tm_min) + ":" + \
                         str(time.gmtime(rawDate).tm_sec)

               # convert duration from seconds to hh:mm:ss format
               duration =     str(callDetailsObj.duration / 3600) + "h " + \
                              str((callDetailsObj.duration % 3600) / 60) + "min " + \
                              str(callDetailsObj.duration % 60) + "s"

               # replace call type ID by string
               callType = int(callDetailsObj.callTypes.contents0)
               if callType == 1:
                    callType = "incoming"
               elif callType == 2:
                    callType = "outgoing"
               elif callType == 3:
                    callType = "missed"
               else:
                    callType = "unknown"

               self.table_row(     outfd,
                                   hex(callDetailsObj.obj_offset),
                                   date,
                                   dalvik.parseJavaLangString(callDetailsObj.name.dereference_as('StringObject')),
                                   dalvik.parseJavaLangString(callDetailsObj.formattedNumber.dereference_as('StringObject')),
                                   duration,               
                                   dalvik.parseJavaLangString(callDetailsObj.countryIso.dereference_as('StringObject')),
                                   dalvik.parseJavaLangString(callDetailsObj.geoCode.dereference_as('StringObject')),
                                   callType)

该插件接受以下命令行参数:

  • -o:用于指向 gDvm 对象的偏移量

  • -p:用于进程 ID(PID)

  • -c:用于指向 PhoneClassDetails 类的偏移量

如果知道并传递这些参数给插件,插件的运行时间将显著减少。否则,插件必须在 RAM 中自行查找这些值。

键盘缓存

现在,我们想查看默认键盘应用程序的缓存。假设在解锁屏幕后没有其他输入,并且智能手机受 PIN 保护,则该 PIN 等于最后的用户输入,可以在 Android 内存转储中找到该输入作为 UTF-16 Unicode 字符串。最后的用户输入的 Unicode 字符串是由com.android.inputmethod.latin进程中的RichInputConnection类创建的,并存储在名为mCommittedTextBeforeComposingText的变量中。这个变量就像一个键盘缓冲区,存储了屏幕键盘最后输入并确认的按键。为了恢复最后的用户输入,我们提供了一个 Volatility 插件,名为dalvik_app_lastInput,如下所示:

class dalvik_app_lastInput(linux_common.AbstractLinuxCommand):

     def __init__(self, config, *args, **kwargs):
          linux_common.AbstractLinuxCommand.__init__(self, config, *args, **kwargs)
          dalvik.register_option_PID(self._config)
          dalvik.register_option_GDVM_OFFSET(self._config)
          self._config.add_option('CLASS_OFFSET', short_option = 'c', default = None,
          help = 'This is the offset (in hex) of system class RichInputConnection.java', action = 'store', type = 'str')

     def calculate(self):

          # if no gDvm object offset was specified, use this one
          if not self._config.GDVM_OFFSET:
               self._config.GDVM_OFFSET = str(0x41b0)

          # use linux_pslist plugin to find process address space and ID if not specified
          proc_as = None     
          tasks = linux_pslist.linux_pslist(self._config).calculate()
          for task in tasks:
               if str(task.comm) == "putmethod.latin":                    
                    proc_as = task.get_process_address_space()
                    self._config.PID = str(task.pid)
                    break

          # use dalvik_loaded_classes plugin to find class offset if not specified
          if not self._config.CLASS_OFFSET:
              classes = dalvik_loaded_classes.dalvik_loaded_classes(self._config).calculate()
              for task, clazz in classes:
                   if (dalvik.getString(clazz.sourceFile)+"" == "RichInputConnection.java"):
                        self._config.CLASS_OFFSET = str(hex(clazz.obj_offset))
                        break

          # use dalvik_find_class_instance plugin to find a list of possible class instances
          instance = dalvik_find_class_instance.dalvik_find_class_instance(self._config).calculate()
          for sysClass, inst in instance:
               # get stringBuilder object
               stringBuilder = inst.clazz.getJValuebyName(inst, "mCommittedTextBeforeComposingText").Object.dereference_as('Object')
               # get superclass object
               abstractStringBuilder = stringBuilder.clazz.super.dereference_as('ClassObject')

               # array object of super class
               charArray = abstractStringBuilder.getJValuebyName(stringBuilder, "value").Object.dereference_as('ArrayObject')
               # get length of array object
               count = charArray.length
               # create string object with content of the array object
               text = obj.Object('String', offset = charArray.contents0.obj_offset,
               vm = abstractStringBuilder.obj_vm, length = count*2, encoding = "utf16")
               yield inst, text

     def render_text(self, outfd, data):
          self.table_header(outfd, [    ("InstanceClass", "13"),
                                        ("lastInput", "20")                                 
                                        ])
          for inst, text in data:

               self.table_row(     outfd,
                                   hex(inst.obj_offset),
                                   text)

实际上,这个插件不仅恢复 PIN 码,还恢复最后一次给出的任意用户输入;在许多情况下,这可能是数字证据中的一个有趣的证据。与前面的插件类似,它接受相同的三个命令行参数:gDvm offsetPIDclass file offset。如果这些参数中的任何一个或全部没有提供,插件也可以自动确定缺失的值。

在 Linux 上使用 Volatility

在接下来的部分,我们将介绍内存获取技术和使用 Volatility 进行 Linux 内存取证的示例用例。

内存获取

如果系统未虚拟化,因此无法从虚拟化层直接获取内存;即使在 Linux 系统中,我们首选的工具仍然是 LiME。

然而,与 Android 不同的是,工具的安装和操作要简单得多,因为我们直接在 Linux 系统上生成并运行 LiME;但正如你将在接下来的段落中注意到的,许多步骤是非常相似的。

首先,确定正在分析的系统上运行的确切内核版本。如果没有足够的文档支持,可以运行以下命令来获取内核版本:

user@forensic-target $ uname –a
Linux forensic-target 3.2.0-88-generic #126-Ubuntu SMP Mon Jul 6 21:33:03 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

提示

在企业环境中使用配置管理

企业环境中通常运行配置管理系统,能够显示目标系统的内核版本和 Linux 发行版。要求客户提供这些数据,甚至是提供一台具有相同内核版本和软件环境的系统,能够帮助你减少 LiME 模块与取证目标之间的兼容性风险。

在实验环境中,准备 LiME 内核模块进行内存采集。要编译该模块,请确保你拥有正确版本的目标内核源代码,然后在 LiME 的src目录中执行以下构建命令:

user@lab src $ make -C /usr/src/linux-headers-3.2.0-88-generic M=$PWD

这应在当前目录中创建lime.ko模块。

在目标系统上,可以使用这个内核模块将内存转储到磁盘,如下所示:

user@forensic-target $ sudo insmod lime.ko path=/path/to/dump.lime format=lime

注意

我们建议选择网络路径来写入镜像。这样,做出的本地系统更改会很少。也可以通过网络传输镜像。只需按照在 Android 上使用 Volatility部分中的描述操作。

Linux 上的 Volatility

Volatility 提供了各种配置文件。这些配置文件由 Volatility 用于解释内存转储。不幸的是,由于 Linux 内核、系统架构和内核配置的种类繁多,无法为所有版本的 Linux 内核提供配置文件。

提示

列出所有 Volatility 配置文件

可以通过vol.py --info命令获取所有可用配置文件的列表。

因此,可能需要创建你自己的配置文件,以便与取证目标完美匹配。Volatility 框架通过提供一个虚拟模块来支持这一步骤,该模块必须针对目标系统的内核头文件进行编译。这个模块可以在 Volatility 分发版中的tools/linux子目录找到。将其编译——类似于 LiME——但启用调试设置:

user@lab src $ make -C /usr/src/linux-headers-3.2.0-88-generic CONFIG_DEBUG_INFO=y M=$PWD

这将创建module.ko。无需加载此模块;我们所需要的是其调试信息。我们使用dwarfdump工具,该工具在大多数 Linux 发行版中作为安装包提供,用于提取这些调试信息:

user@lab $ dwarfdump -di module.ko > module.dwarf

创建我们配置文件的下一步是获取目标系统或具有相同架构、内核版本和内核配置的系统的System.map文件。System.map文件通常位于/boot目录中。通常,内核版本会包含在文件名中,因此务必选择适用于取证目标系统运行内核的System.map文件。

module.dwarfSystem.map放入一个压缩档案中,这将成为我们的 Volatility 配置文件,如下所示:

user@lab $ zip Ubuntu3.2.0-88.zip module.dwarf System.map

如示例所示,ZIP 文件的名称应反映发行版和内核版本。

注意

确保不要向压缩档案中添加额外的路径信息。否则,Volatility 可能无法加载配置文件数据。

将新配置文件复制到 Volatility 的 Linux 配置文件目录,如下所示:

user@lab $ sudo cp Ubuntu3.2.0-88.zip /usr/local/lib/python2.7/dist-packages/volatility-2.4-py2.7.egg/volatility/plugins/overlays/linux/

除了使用系统范围的配置文件目录外,你还可以选择一个新的目录,并将--plugins=/path/to/profiles选项添加到 Volatility 命令行。

最后,你需要获取新配置文件的名称以供进一步使用。因此,使用以下命令:

user@lab $ vol.py --info

输出应包含一行额外的内容,显示新的配置文件,如下所示:

Profiles
--------
LinuxUbuntu3_2_0-88x64 - A Profile for Linux Ubuntu3.2.0-88 x64

要使用此配置文件,请在所有后续调用vol.py时,作为命令行参数添加--profile=LinuxUbuntu3_2_0-88x64

重建 Linux 的数据

所有分析 Linux 内存转储的插件都有linux_前缀。因此,你应使用 Linux 版本的插件。否则,可能会出现错误消息,提示所选配置文件不支持该模块。

分析进程和模块

分析内存转储的一个典型第一步是列出所有正在运行的进程和加载的内核模块。

以下是如何通过 Volatility 从内存转储中提取所有正在运行的进程:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_pslist
Volatility Foundation Volatility Framework 2.4

Offset             Name                 Pid             Uid             Gid    DTB                Start Time
------------------ -------------------- --------------- --------------- ------ ------------------ ----------
0xffff8802320e8000 init                 1               0               0      0x000000022f6c0000 2015-08-16 09:51:21 UTC+0000
0xffff8802320e9700 kthreadd             2               0               0      ------------------ 2015-08-16 09:51:21 UTC+0000

0xffff88022fbc0000 cron                 2500            0               0      0x000000022cd38000 2015-08-16 09:51:25 UTC+0000
0xffff88022fbc1700 atd                  2501            0               0      0x000000022fe28000 2015-08-16 09:51:25 UTC+0000
0xffff88022f012e00 irqbalance           2520            0               0      0x000000022df39000 2015-08-16 09:51:25 UTC+0000
0xffff8802314b5c00 whoopsie             2524            105             114    0x000000022f1b0000 2015-08-16 09:51:25 UTC+0000
0xffff88022c5c0000 freshclam            2598            119             131    0x0000000231fa7000 2015-08-16 09:51:25 UTC+0000

如输出所示,linux_pslist插件通过描述活动进程来迭代内核结构,即它从init_task符号开始,迭代task_struct->tasks链表。该插件获取所有正在运行的进程的列表,包括它们在内存中的偏移地址、进程名称、进程 ID(PID)、进程的用户和组的数值 ID(UID 和 GID),以及启动时间。目录表基址DTB)可用于进一步分析,将物理地址转换为虚拟地址。空的 DTB 条目最有可能与内核线程相关。例如,在我们的示例输出中是kthreadd

分析网络信息

内存转储包含有关法医目标系统网络活动的各种信息。以下示例展示了如何利用 Volatility 推导出最近的网络活动信息。

地址解析协议ARP缓存用于将 MAC 地址映射到 IP 地址。在建立本地网络上的网络通信之前,Linux 内核会发送 ARP 请求以获取给定目标 IP 地址对应的 MAC 地址信息。响应会被缓存到内存中,以便重新使用并与该 IP 地址在本地网络上进一步通信。因此,ARP 缓存条目指示了法医目标系统与本地网络上的哪些系统进行了通信。

要从 Linux 内存转储中读取 ARP 缓存,请使用以下命令:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_arp
[192.168.167.22                            ] at 00:00:00:00:00:00    on eth0
[192.168.167.20                            ] at b8:27:eb:01:c2:8f    on eth0

该输出提取显示系统为目标地址192.168.167.20保留了一个缓存条目,对应的 MAC 地址是b8:27:eb:01:c2:8f。第一个条目很可能是由于一次不成功的通信尝试而产生的缓存条目,也就是说,192.168.167.22的通信伙伴没有对系统发出的 ARP 请求做出响应,因此,相应的 ARP 缓存条目保持其初始值00:00:00:00:00:00。可能是通信伙伴无法访问,或者它根本不存在。

注意

如果你在 ARP 缓存中看到本地子网的大部分系统显示出多个 MAC 地址为 00:00:00:00:00:00 的条目,那么这表明存在扫描活动,也就是说,系统尝试在本地网络上探测其他系统。

为了进一步的网络分析,可能值得将从 ARP 缓存中获取的 MAC 地址列表与本地子网中应该存在的系统进行对比。虽然这种方法并非万无一失(因为 MAC 地址可以伪造),但它可能有助于发现不明的网络设备。

注意

查找 MAC 地址的硬件供应商

MAC 地址的前缀揭示了相应网络硬件的硬件供应商。像www.macvendorlookup.com这样的网站提供了网络卡硬件供应商的相关信息。

如果我们查找示例中b8:27:eb:01:c2:8f MAC 地址的硬件供应商,它显示该设备是由树莓派基金会制造的。在标准的办公室或数据中心环境中,这些嵌入式设备很少使用,因此检查该设备是否为良性设备是非常值得的。

为了概览创建内存转储时的网络活动,Volatility 提供了模拟 linux_netstat 命令的方法,如下所示:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_netstat
TCP      192.168.167.21  :55622 109.234.207.112  :  143 ESTABLISHED           thunderbird/3746
UNIX 25129          thunderbird/3746
TCP      0.0.0.0         : 7802 0.0.0.0         :    0 LISTEN                      skype/3833

这三行只是该命令典型输出的一个小片段。第一行显示 thunderbird 进程(PID 为 3746)与 IMAP 服务器(TCP 端口 143)通过 109.234.207.112 IP 地址建立了一个活动的 ESTABLISHED 网络连接。第二行仅显示一个 UNIX 类型的套接字,用于进程间通信IPC)。最后一行显示 skype(PID 为 3833)正在等待 LISTEN 状态,准备接收来自 TCP 端口 7802 的传入连接。

Volatility 还可以用来将进程列表缩小到那些具有原始网络访问权限的进程。通常,这种访问仅对动态主机配置协议DHCP)客户端、网络诊断工具以及当然的恶意软件有用,目的是在网络接口上构造任意数据包,例如进行所谓的 ARP 缓存中毒攻击。以下展示了如何列出具有原始网络套接字的进程:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_list_raw
Process          PID    File Descriptor Inode 
---------------- ------ --------------- ------------------
dhclient           2817               5              15831

这里,仅检测到 DHCP 客户端拥有原始网络访问权限。

提示

Rootkit 检测模块

Volatility 提供了多种机制来检测典型的 rootkit 行为,例如中断钩取、网络栈的操作和隐藏的内核模块。我们建议熟悉这些模块,因为它们可以加速你的分析。此外,定期检查模块更新,以利用 Volatility 内置的新恶意软件检测机制。

一些通用的方法和启发式技术用于恶意软件检测,并已结合在linux_malfind模块中。该模块会查找可疑的进程内存映射,并生成可能恶意进程的列表。

使用 YARA 进行恶意软件狩猎

YARA 本身是一个工具,能够在任意文件和数据集中的匹配给定的模式。相应的规则,也称为签名,是在硬盘或内存转储中搜索已知恶意文件的好方法。

在本节中,我们将展示如何在获取的 Linux 机器内存转储中搜索给定的恶意软件。因此,您可以使用我们将在接下来的内容中讨论的两种不同程序:

  • 直接使用 YARA 帮助搜索内存转储

  • 使用linux_yarascan和 Volatility

第一个选项有一个很大的缺点;正如我们所知,内存转储包含的是通常连续的碎片化数据。这一事实使得如果您在搜索已知签名时遇到失败的风险,因为它们不一定按您搜索的顺序排列。

第二个选项—使用linux_yarascan—更具容错性,因为它使用 Volatility 并了解获取的内存转储的结构。借助这些知识,它能够解决碎片化问题并可靠地搜索已知签名。虽然我们在 Linux 上使用linux_yarascan,但该模块也可用于 Windows(yarascan)和 Mac OS X(mac_yarascan)。

该模块的主要功能如下:

  • 在内存转储中扫描给定进程以查找给定的 YARA 签名

  • 扫描完整的内核内存范围

  • 将包含符合给定 YARA 规则的正面结果的内存区域提取到磁盘

输入vol.py linux_yarascan –h即可查看完整的命令行选项列表

基本上,您可以通过多种方式进行搜索。使用此模块的最简单方法是通过在内存转储中搜索给定的 URL。可以通过输入以下命令来完成此操作:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_yarascan –-yara-rules="microsoft.com" --wide

Task: skype pid 3833 rule r1 addr 0xe2be751f
0xe2be751f  6d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00   m.i.c.r.o.s.o.f.
0xe2be752f  74 00 2e 00 63 00 6f 00 6d 00 2f 00 74 00 79 00   t...c.o.m./.t.y.
0xe2be753f  70 00 6f 00 67 00 72 00 61 00 70 00 68 00 79 00   p.o.g.r.a.p.h.y.
0xe2be754f  2f 00 66 00 6f 00 6e 00 74 00 73 00 2f 00 59 00   /.f.o.n.t.s./.Y.
0xe2be755f  6f 00 75 00 20 00 6d 00 61 00 79 00 20 00 75 00   o.u...m.a.y...u.
0xe2be756f  73 00 65 00 20 00 74 00 68 00 69 00 73 00 20 00   s.e...t.h.i.s...
0xe2be757f  66 00 6f 00 6e 00 74 00 20 00 61 00 73 00 20 00   f.o.n.t...a.s...
0xe2be758f  70 00 65 00 72 00 6d 00 69 00 74 00 74 00 65 00   p.e.r.m.i.t.t.e.
0xe2be759f  64 00 20 00 62 00 79 00 20 00 74 00 68 00 65 00   d...b.y...t.h.e.
0xe2be75af  20 00 45 00 55 00 4c 00 41 00 20 00 66 00 6f 00   ..E.U.L.A...f.o.
0xe2be75bf  72 00 20 00 74 00 68 00 65 00 20 00 70 00 72 00   r...t.h.e...p.r.
0xe2be75cf  6f 00 64 00 75 00 63 00 74 00 20 00 69 00 6e 00   o.d.u.c.t...i.n.
0xe2be75df  20 00 77 00 68 00 69 00 63 00 68 00 20 00 74 00   ..w.h.i.c.h...t.
0xe2be75ef  68 00 69 00 73 00 20 00 66 00 6f 00 6e 00 74 00   h.i.s...f.o.n.t.
0xe2be75ff  20 00 69 00 73 00 20 00 69 00 6e 00 63 00 6c 00   ..i.s...i.n.c.l.
0xe2be760f  75 00 64 00 65 00 64 00 20 00 74 00 6f 00 20 00   u.d.e.d...t.o...
Task: skype pid 3833 rule r1 addr 0xedfe1267
0xedfe1267  6d 00 69 00 63 00 72 00 6f 00 73 00 6f 00 66 00   m.i.c.r.o.s.o.f.
0xedfe1277  74 00 2e 00 63 00 6f 00 6d 00 2f 00 74 00 79 00   t...c.o.m./.t.y.
0xedfe1287  70 00 6f 00 67 00 72 00 61 00 70 00 68 00 79 00   p.o.g.r.a.p.h.y.
0xedfe1297  2f 00 66 00 6f 00 6e 00 74 00 73 00 2f 00 59 00   /.f.o.n.t.s./.Y.
0xedfe12a7  6f 00 75 00 20 00 6d 00 61 00 79 00 20 00 75 00   o.u...m.a.y...u.
0xedfe12b7  73 00 65 00 20 00 74 00 68 00 69 00 73 00 20 00   s.e...t.h.i.s...
0xedfe12c7  66 00 6f 00 6e 00 74 00 20 00 61 00 73 00 20 00   f.o.n.t...a.s...
0xedfe12d7  70 00 65 00 72 00 6d 00 69 00 74 00 74 00 65 00   p.e.r.m.i.t.t.e.
0xedfe12e7  64 00 20 00 62 00 79 00 20 00 74 00 68 00 65 00   d...b.y...t.h.e.
0xedfe12f7  20 00 45 00 55 00 4c 00 41 00 20 00 66 00 6f 00   ..E.U.L.A...f.o.
0xedfe1307  72 00 20 00 74 00 68 00 65 00 20 00 70 00 72 00   r...t.h.e...p.r.
0xedfe1317  6f 00 64 00 75 00 63 00 74 00 20 00 69 00 6e 00   o.d.u.c.t...i.n.
0xedfe1327  20 00 77 00 68 00 69 00 63 00 68 00 20 00 74 00   ..w.h.i.c.h...t.
0xedfe1337  68 00 69 00 73 00 20 00 66 00 6f 00 6e 00 74 00   h.i.s...f.o.n.t.
0xedfe1347  20 00 69 00 73 00 20 00 69 00 6e 00 63 00 6c 00   ..i.s...i.n.c.l.
0xedfe1357  75 00 64 00 65 00 64 00 20 00 74 00 6f 00 20 00   u.d.e.d...t.o...

一种更复杂但更实际的方式是搜索给定的 YARA 规则。以下 YARA 规则是用来确定系统是否感染了Derusbi恶意软件家族:

rule APT_Derusbi_Gen
{
meta:
  author = "ThreatConnect Intelligence Research Team"
strings:
  $2 = "273ce6-b29f-90d618c0" wide ascii
  $A = "Ace123dx" fullword wide ascii
  $A1 = "Ace123dxl!" fullword wide ascii
  $A2 = "Ace123dx!@#x" fullword wide ascii
  $C = "/Catelog/login1.asp" wide ascii
  $DF = "~DFTMP$$$$$.1" wide ascii
  $G = "GET /Query.asp?loginid=" wide ascii
  $L = "LoadConfigFromReg failded" wide ascii
  $L1 = "LoadConfigFromBuildin success" wide ascii
  $ph = "/photoe/photo.asp HTTP" wide ascii
  $PO = "POST /photos/photo.asp" wide ascii
  $PC = "PCC_IDENT" wide ascii
condition:
  any of them
}

如果我们将此规则保存为apt_derusbi_gen.rule,我们可以通过输入以下命令在获取的内存转储中进行搜索:

user@lab $ vol.py --profile=LinuxUbuntu3_2_0-88x64 --file=memDump.lime linux_yarascan --yara-file=apt_derusbi_gen.rule --wide

结果只会显示一个简短的预览,您可以通过使用--size选项来放大它。

如果您正在调查预定义的场景(例如,如果您已经知道系统已被已知的攻击组攻击),您可以将所有规则复制到一个规则文件中,并一次性在内存转储中搜索该文件中的所有规则。Volatility 和linux_yarascan将显示每个匹配的结果及其对应的规则编号。这使得扫描已知恶意行为在内存转储中变得更快。

有大量可用于 YARA 签名的来源,这些来源在野外可用,我们这里只提及一些最重要的来源,以帮助你开始恶意软件猎杀,具体如下:

摘要

在本章中,我们概述了如何使用 Volatility 框架进行内存取证。在示例中,我们展示了 Android 和 Linux 系统的内存获取技术,并展示了如何在这两个系统上使用 LiME。我们使用 Volatility 获取了有关正在运行的进程、加载的模块、可能的恶意活动和最近的网络活动的信息。后者对于通过网络追踪攻击者的活动非常有用。

在本章的最后一个示例中,我们演示了如何在这样的内存转储中搜索给定的恶意软件签名或其他高度灵活的基于模式的规则。这些 YARA 签名或规则有助于快速识别可疑活动或文件。

此外,我们演示了如何获取 Android 设备的键盘缓存和通话历史。

接下来该做什么

如果你想测试从本书中获得的工具和知识,我们给你以下两条建议:

  • 创建一个包含两台虚拟机的实验室——MetasploitMetasploitable。尝试入侵你的Metasploitable系统,并随后进行取证分析。你能重建这次攻击并收集所有的妥协指标吗?

  • 获取一些旧的硬盘,这些硬盘已经不再使用,但过去曾经经常使用。对这些硬盘进行取证分析,并尽量重建尽可能多的数据。你能重建这些硬盘上的历史操作吗?

如果你想增强对本书中一些主题的了解,以下几本书是非常好的选择:

  • 实用移动取证Satish BommisettyRohit TammaHeather MahalikPackt Publishing出版

  • 记忆取证的艺术:在 Windows、Linux 和 Mac 内存中检测恶意软件和威胁Michael Hale LighAndrew CaseJamie LevyAAron Walters 编写,Wiley India出版

  • 数字取证与调查手册Eoghan Casey 编写,Academic Press出版