内存取证新选择:像访问文件一样进行内存取证

–内存取证利器MemProcFs

盘古石取证 虎贲小队王小石(liyi05@qianxin.com)

在过去的几年里,使用离地(living off the land)和无文件攻击技术的威胁者数量急剧增加。攻击者不再像以前那样关心如何消除他们的足迹,相反,他们尽量少留下足迹以避免被发现。这使得信息安全专业人员的工作更加困难,因为使用内置工具和磁盘上缺乏可扫描的恶意文件意味着一些传统的安全解决方案可能毫无用处。缺乏日志记录可能使我们很难在事后检查的过程中重建威胁者是如何滥用内置两用工具的,例如各种命令和脚本解释器,因此获取和分析内存可能在这种情况下发挥关键作用。

一般我们进行内存取证使用的工具是Rekall(http://www.rekall-forensic.com/)和Volatility(https://www.volatilityfoundation.org/),尤其是Volatility,由于具有大量的插件和广泛的社区支持,已经成为进行内存取证分析的首选工具。但是Volatility主要是命令行工具,使用起来需要记住大量的命令,制作特定版本的操作系统Profile也比较繁琐。也有一些工具如Volatility Workbench(https://www.osforensics.com/tools/volatilityworkbench.html)提供了一个简陋的GUI界面,减少了取证人员操作的难度,但是遗憾的是只支持大部分的Volatility参数。

  • MemProcFs简介

今天我们介绍一个新的内存取证框架MemProcFS(https://github.com/ufrisk/MemProcFS),它提供了内存取证的一个新视角,即把内存各种Artifact以文件系统的形式展现出来,取证人员可以直接使用文本编辑器这样的工具直接打开分析结果进行提取和搜索,或者使用专用工具对一些Artifact(比如注册表文件)进行进一步分析。分析过程简单直接。

MemProcFS是Ulf Frisk(@UlfFrisk,https://twitter.com/UlfFrisk)开发的内存取证框架。“MemProcFS 是一种将物理内存视为虚拟文件系统中的文件的简便方法。简单的点击内存分析,无需复杂的命令行参数!通过安装的虚拟文件系统中的文件或通过功能丰富的应用程序库访问内存内容和工件以包含在您自己的项目中!”(https://github.com/ufrisk/MemProcFS/blob/master/README.md)。

  • 安装

MemProcFS的运行需要一些依赖的库。

首先是Python3.6以上版本。你可以从官方网页上获得合适的Python版本:https://www.python.org/downloads/windows/。或者你也可以像我一样使用Anaconda(https://www.anaconda.com/)带的Python,目前最新版本是Python 3.9。

然后需要安装LeechCore。这是一个Python的依赖库。使用pip工具安装该库如下图:

再次是安装下一步是安装Microsoft Visual C++ Redistributables for Visual Studio 2019。你可以从https://go.microsoft.com/fwlink/?LinkId=746572获得安装程序。这是一个标准的Windows安装程序,双击运行就可以了。

现在所有依赖的环境都可以了,下面可以下载MemProcFS了。请到MemProcFS的GitHub仓库,网址是https://github.com/ufrisk/MemProcFS/releases,搜索最新的版本,目前是5.2版本:

请下载如上图红框标注的版本,下载后解压缩,如图所示:

现在就可以使用这个框架了。

  • 示例

下面我们以一个例子演示一下MemProcFS框架的使用。

首先我们用WinPmem(https://github.com/Velocidex/WinPmem)工具对一台Windows 7计算机做一个内存镜像。使用命令:winpmem_mini_x64_rc2.exe memdump.dd,其中memdump.dd文件为生成的内存镜像文件: 运行完成后,可以看到生成了一个32G的内存镜像文件:

然后我们可以运行以下命令来使用MemProcFS分析刚刚制作的内存镜像,e:\Develop\memprocfs\MemProcFS.exe -device memdump.dd,如下图所示:

这里我们可以看到MemProcFS识别出来我们这个内存镜像的操作系统是: Windows 6.1.7601 (X64),并且在创建了一个虚拟文件系统,并挂载到了系统的M:\盘。在不指定挂载点的时候,MemProcFS缺省挂载到M:\盘。如果M:\已经存在,则会报错,如下图:

指定其他挂载点,比如Q:\,可以使用选项-mount Q。如下图所示:

好了,现在我们把使用内存镜像制作的虚拟文件系统挂载到了M:\,我们可以打开资源管理器,直接访问M:\,如下图:

我们以内存里的注册表分析为例,来显示如何进行内存分析。

我们打开M:\registry\hive_files,可以看到在内存里提取到的注册配置单元都在该目录下:

我们可以直接使用注册表配置单元工具进行分析,比如使用内置注册表编辑器Regedit进行分析。

需要注意的是注册表编辑器需要将对应的配置单元文件(.reghive)复制到本地某个目录,比如我们复制到F:\Reghives目录下,如图:

我们运行Regedit.exe,如图:

我们选择“文件”菜单->”加载配置单元“,如图:

我们选择加载SOFTWARE配置单元,如下图:

加载完成如下图:

可以看到在HKU下挂载了一个名为SOFT的注册表键,可以像正常注册表那样访问里面的键和值。

我们也可以使用取证软件比如Registry Explorer(https://ericzimmerman.github.io/#!index.md)加载分析,如图:

当我们完成内存分析工作时,只要在运行MemProcFs的窗口按Ctrk+C即可退出。如图:

  • MemProcFS能做什么

现在,我们看一下MemProcFS能解析哪些数据:

1.进程列表

进程列表有两个目录,一个是根目录下的name目录,里面是按进程名称列出的每个进程目录。如下图:

进程名字后面的数字是进程的pid。

另外一个目录是根目录下pid目录,里面是按进程 pid 列出的每个进程目录。如下图:

这些子目录里可以查看进程的相关信息,比如命令行:

2.系统信息

系统信息在根目录下的sys目录,如图所示:

这里面的内容包括:

比如操作系统版本如下图:

3.其他系统信息

Sys目录下还有一些子目录,代表不同的系统信息,比如证书、驱动、进程、网络、内核对象等,如下图:

比如我们可以看看users目录,下面是用户信息,如图:

  1. 取证相关信息
    取证相关信息在根目录forensic子目录。该目录包含与 MemProcFS 取证子系统相关的目录和文件。取证子系统是可以对内存转储进行的更全面的面向批处理的分析任务的集合。默认情况下,取证模式未启用。
    可以通过命令行参数-forensic或编辑文件forensic_enable.txt来启用取证模式。命令行参数和文件都采用 1-4 之间的数字来启动取证模式。

取证模式可能需要一些时间来分析。它将一次性读取完整的内存转储并并行执行多个分析任务。结果将保存到 SQLITE 数据库中。根据选择启动的数字 1-4,完成后可以在 sqlite 浏览器中打开 sqlite 数据库。完成后,时间线和 NTFS MFT 分析等将作为forensic目录的子目录提供。

  1. 其他杂项信息
    杂项信息在根目录线下的misc目录,该目录包含代表与全局上下文相关的各种杂项分析任务的子目录。
    其中包括:bitlocker相关信息、检测到恶意软件信息、内存地址和物理地址映射表、浏览器相关信息等。这个目录下也有一个全局进程列表文件,如下图:
  1. 其他
    在根目录下有一个conf目录,该文件包含与各个进程无关的全局内存进程文件系统状态和配置设置。
    另外还有一个VM目录,默认情况下,vm目录是隐藏的。如果检测到支持的虚拟机,它将出现。必须通过命令行参数启用 VM 检测。
    从上面的内容来看,MemProcFS提供了内存取证分析巨细靡遗的内容,可以使得我们可以细致分析目标系统内存的内容。
  • MemProcFS命令行选项

最后,MemProcFS提供了详细的选项,可以根据分析的要求采用不同的选项。具体命令行选项,可以参考https://github.com/ufrisk/MemProcFS/wiki/_CommandLine

一般常见的命令行包括以下:

• 将内存转储文件挂载为默认 M:
memprocfs.exe -device c:\temp\win10x64-dump.raw
• 将内存转储文件挂载为默认的 M:,但提供更冗余的信息:
memprocfs.exe -device c:\temp\win10x64-dump.raw -v
• 将内存转储文件挂载为默认 M: 并启动取证模式:
memprocfs.exe -device c:\temp\win10x64-dump.raw -forensic 1
• 将内存转储文件挂载为 S:
memprocfs.exe -mount s -device c:\temp\win10x64-dump.raw
• 使用 WinPMEM 驱动程序以只读模式挂载实时目标内存(实时内存取证分析):
memprocfs.exe -device pmem
• 使用相应的页面文件挂载内存转储:
• memprocfs.exe -device unknown-x64-dump.raw -pagefile0 pagefile.sys -pagefile1 swapfile.sys

  • 结语

MemProcFS是一个强大的内存取证框架,更重要的是提供了取证分析人员和内存分析人员更为直观的访问接口,也不需要相关人员记住大量纷繁复杂的命令选项。现在就让我们试一下吧!

原文已发表于盘古石取证公众号

“我知道你在任务栏上干了啥”

–Windows 10 1903版本引入的新取证痕迹

Leon(liyi05@qianxin.com)

进行网络安全调查时,取证人员/应急响应人员需要从不同的痕迹中获取信息,以确定目标机器上发生了什么。在执行这些调查时,有时候需要判断某个程序是否是以交互方式执行的。例如,是否有人使用远程桌面协议(RDP)连接到系统然后运行某个程序。除了事件日志外,还有许多其他痕迹可以提供有价值的信息,例如注册表项。有许多注册表键来帮助推断哪些程序已在系统上运行,比如:

•       UserAssist(系统上最近执行了哪些程序,位置HKCU \SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\)

•       RecentApps(Windows 10系统最近执行的文件,HKCU\Software\Microsoft\Windows\Current Version\Search\RecentApps)

•       Shimcache(跟踪可执行文件的文件名、文件大小、上次修改时间,HKLM\ SYSTEM\CurrentControlSet \Control\Session Manager\AppCompatCache)

•       Last-Visited MRU(跟踪应用程序用来打开OpenSaveMRU键中记录的文件的可执行文件和目录位置。HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU)

•       Windows Background Activity Moderator (BAM)(提供了在系统上运行的可执行文件以及上次执行日期/时间的完整路径,仅对Windows 10 1709以后版本有效。HKLM\SYSTEM\CurrentControlSet\Services\bam\UserSettings\{SID}

只要有过Windows使用经验的人,都不会对任务栏陌生。早期Windows任务栏单纯就是个显示运行的任务的指示器。后来任务栏引入JumpList,可以显示某个程序曾经打开过的文件列表,这个痕迹可以帮助取证调查人员获得用户的活动记录。但是如果执行了哪些程序,每个程序执行的次数这些信息就无从得知了。 Windows 10的1903版本引入了新的注册表痕迹,叫做“FeatureUsage”,顾名思义,就是关于系统一些功能的使用情况,比如账户在应用程序之间切换的频率,查看Windows时钟,单击开始菜单等等。

图 1 Windows 10 1903版本
图 1 Windows 10 1903版本

FeatureUsage痕迹位于每个用户的用户注册表配置单元(NTUSER.DAT)里,因此每个账户都可能存在独立的FeatureUsage痕迹。为什么我们要说”可能“?因为FeatureUsage对应的注册表键只有在该用户第一次以交互方式登录到系统才会建立。这里说的交互登录包括通过登录界面登录,或者通过RDP协议进行远程访问。这一系列的痕迹都在每个用户的以下键下面:

HKU\Software\Microsoft\Windows\CurrentVersion\Explorer\FeatureUsage

下面我们就来看看这一系列痕迹可以带给我们什么样的惊喜。

首次登录时间

在该注册表键下有一个值KeyCreationTime,值是64位的Windows FileTime值,这个值说明最初创建该键的时间,也就是这个用户第一次通过交互方式登录这个系统的时间。因此取证人员可以通过这个值确认该用户第一次交互登录本系统的时间。

下面是我的电脑当前用户的KeyCreationTime值:

图2 首次登录时间

我们将这个值用Winhex进行转换,如图:

图3 使用Winhex解析时间

可以看出,当前账户第一次登录系统的时间是2020/2/27 03:09:30 UTC。

在FeatureUsage键下有五个子键,这些键记录任务栏的各种活动指标。通过这些子键,取证人员可以对该账户的活动进行分析。

图4 FeatureUsage的子键

AppBadgeUpdated

此项提供运行中的应用程序的图标更新的次数(例如,用于向您通知未读的电子邮件或通知)。例如,下图的Telegram软件来了新消息时,注册表这个键下面的Telegram对应的值就会增加。

图5 AppBadgeUpdated示例

此外当某些程序新开窗口时,这个数值也会增加,比如Chrome,每当你打开一个新窗口,而不是创建新标签时,对应的值就会加一。

下面是我的当前用户的AppBadgeUpdated的值列表。

图6 我的AppBadgedUpdated子键

我们注意到,首先不是所有在任务栏上出现过的程序都在这个列表里,最典型的就是微信的客户端就没有对应的值。其次出现在这里的值大概有以下几种情形,对应上面标记的几块:

  1. 这是Chrome的Web Application,比如_crx_pioclpoplcdbaefihamjohnefbikjilc对应的是Chrome里的WebApplication Evernote插件。
  2. 安装的应用程序。注意这里应用程序的路径里含有Folder GUID,其中{6D809377-6AF0-444B-8957-A3773F02200E}是ProgramFilesX64,一般就是C:\Program Files,而{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}是ProgramFilesX86,一般就是C:\Program Files (x86)
  3. 也是安装的应用程序,但是路径给出了绝对路径
  4. 没有安装的应用程序,路径也是绝对路径
  5. 是UWP应用,也就是一般从Windows Store安装的应用

注意,这里的应用即使是已经卸载或者无法访问的应用也会保留在这里,因此取证调查人员可以通过这个注册表键发现调查对象曾经卸载过的应用。

AppLaunch

这个键提供了固定在任务栏上的应用程序运行的次数。

Windows 7开始,对运行的程序提供了一个功能,可以将一个程序永远固定在任务栏上,这样下次要使用的时候,不需要再展开开始菜单层层叠叠的去查找应用程序。AppLaunch这个子键就可以知道这些被固定在任务栏上的程序被执行了多少次。

下图是我的任务栏固定的应用程序列表(有点多😊):

图7 任务栏固定的应用程序

下面我们看一下对应的AppLaunch子键:

图8 AppLaunch子键

应用程序的类型与AppBadgeUpdated类似,但是没有Chrome Web Application,这很好理解,Chrome的Web Application依赖于Chrome执行,应该没有办法固定到任务栏。

只要是曾经固定在任务栏上的应用程序,在这个子键下都有对应的值,即使取消固定,这个值还保留,而且如果重新进行固定,再点击任务栏上的图标还是一样会增加。我们可以看到在AppBadgeUpdated没有出现的微信程序(WeChat)由于我把它固定到了任务栏,这次也出现了。

但是如果程序固定在任务栏上,但是你选择从开始菜单,或者直接通过搜索或者其他方式启动程序,则这个值不会增加。

该键的一个用途是突出显示固定在任务栏上的应用程序以及其运行频率。在调查中,这可能有助于证明某用户知道某个应用程序(因为任务栏上有显示),并有意识地从固定链接中运行了该应用程序一定次数。

AppSwitched

这个键记录了应用程序通过鼠标左键点击任务栏切换焦点的次数,当程序运行以后,用鼠标左键点击任务栏上应用程序图标一次,计数就会增加1。如果通过“Alt+Tab“进行应用程序切换,则不会影响此处的值。 与前面两个子键相比,这个子键记录的应用程序的条目数是最多的,在我的机器上找到280+的值,前面两个子键记录的条目都不超过30个,应该是在工具栏上出现过的程序都列出来了。下图是我的AppSwitched子键部分值的截图:

图 9AppSwitched子键

这个键的功能类似与UserAssist和RecentApps,可以用于查找Windows上运行过的GUI可执行程序,不过它显示的是应用程序运行后鼠标左键点击的次数,而非启动的次数。

ShowJumpView

AppSwitched子键显示的是鼠标左键单击任务栏上的应用程序的次数,ShowJumpView子键显示的就是鼠标右键单击任务栏上的应用程序的次数。与AppSwitched子键类似,也是只显示鼠标右键单击的次数,而非程序启动的次数。

同样这里似乎也记录了所有在任务栏出现过并且鼠标右键曾经点击过的程序,因此比AppBadgeUpdated和AppLaunch记录的应用程序数量要多。下面是我机器上部分ShowJumpView键的截图:

图10 ShowJumpView子键

TrayButtonClicked

我们知道:任务栏上除了应用程序以外还有一些系统定义的按钮,这些按钮包括开始按钮、时钟等。这些按钮的点击次数就记录在TryButtonClicked子键下。目前已知8种按钮的点击次数会记录在此键下:

图11 TryButtonClicked子键

这8个按钮在工具栏上的分布如下图:

图12 系统自定义按钮

注意:搜索框和搜索按钮一次只能显示二者之一。另外“显示桌面”按钮不像之前的Windows显示一个很大的按钮,现在只在主桌面任务栏最右边(如果任务栏在侧边,就是最下边),比较不显眼,因此我的机器上的ShowDesktopButton的点击次数不过区区六次。

调查人员可以通过这个键基于单击任务栏按钮的次数来进一步了解用户行为以及被调查对象与操作系统的交互方式。一个潜在的用途是用来确定帐户交互使用的频率。

结论

FeatureUsage注册表键提供了关于任务栏和任务栏上应用程序和系统按钮的丰富的记录,取证调查人员可以使用这些数据来进一步加强其他用户痕迹分析的结果。同时它也可以提供RDP使用的信息(首次使用的时间)以及是否被调查对象是否使用反取证技术来去除那些更广为人知的取证痕迹。在进行犯罪调查时,它可能会提供深入了解用户行为及其与Windows的交互方式的信息。

(原文已发表于盘古石取证公众号

扩展移动取证:分析桌面计算机

原文:Extended Mobile Forensics: Analyzing Desktop Computers by Oleg Afonin

翻译:李毅(liyi05@qianxin.com)

在移动取证方面,专家们正在分析智能手机本身可能对云数据的访问。 但是,将搜索扩展到用户的台式机和笔记本计算机可能(并且可能将)帮助访问存储在智能手机机身上和云端的信息。 在本文中,我们将列出所有可以为智能手机数据提供信息的相关痕迹。 这些信息适用于Apple iOS设备以及运行Google Android的智能手机。

桌面和笔记本计算机上的移动痕迹

由于容量大,计算机存储可能包含比智能手机更多的证据。 然而,与我们期待智能手机的带时间戳和地理标记的用户数据相比,这将是一种不同的证据。

用户的PC或者MAc可以给移动取证专家什么帮助?以下几类证据可以帮助我们从手机和云端获取数据。

  1. iTunes备份。这种类型的证据是iPhone特有的(或者更确切地说是Apple特有的),用户的计算机上发现的本地备份可以成为宝贵的证据来源。
  2. 保存的密码。通过即时提取存储在用户的Web浏览器(Chrome,Edge,IE或Safari)中的密码,可以构建用于破解加密的自定义词典。更重要的是,可以使用存储的凭据登录用户的iCloud或Google帐户并执行云提取。
  3. 电子邮见帐户。电子邮件帐户可用于重置用户的Apple或Google帐户(然后使用新凭据进行后续云提取)。
  4. 身份验证令牌。可用于访问用户的iCloud帐户中的同步数据(必须在用户的计算机上使用令牌; 在macOS上,可以提取可转移的不受限制的令牌)。还有Google Drive的令牌(可用于访问用户的Google Drive帐户中的文件)和Google帐户的令牌(可用于从用户的Google帐户中提取大量数据)。计算机本身也是一种痕迹,因为某些认证令牌被“固定”到特定的硬件并且不能被转移到另一个设备。如果计算机是“可信”设备,则可以将其用于绕过双因素身份验证。

iTunes备份

iPhone用户可以在Windows上安装iTunes(或者在Mac上使用内置的iTunes)。iTunes可以用作定期或者手动制作iPhone的本地备份、这些备份可以说是移动取证专家可以从用户计算机收集的证据中最宝贵的部分。

与Android的剥离式ADB备份相比,Apple的iPhone拥有更好的本地备份系统。 Apple在关于iOS设备的备份中对本地备份进行了全面描述。

根据Apple的说法,“iTunes备份几乎包含了设备所有的数据和设置。”我们一贯的经验是,本地备份几乎包含了恢复现有iPhone或设置新设备所需的一切。 将文件和设置传输到另一台设备既快捷又简单; 用户使用替换设备的体验与使用旧iPhone不会有太大的不同。 甚至密码(iOS Keychain)也保存在本地备份中。

加密和不加密备份

用户可以选择使用密码保护其本地备份。 受密码保护的备份是安全加密的。 为了解密这些备份,需要破解或恢复密码。 密码恢复攻击很慢,在高端GPU上每秒只能做100-150次尝试。

加密和未加密的iTunes备份之间存在巨大差异。 在某种程度上,对于专家而言,密码保护的备份比没有密码的备份更为理想。 Apple说以下有关备份加密的内容:

iTunes加密备份功能可对您的信息加锁和编码。 加密的iTunes备份可以包含未加密的iTunes备份不包含的信息:

  • 保存的密码
  • Wi-Fi设置
  • 上网历史
  • 健康数据

我们只能从加密备份中提取已保存的密码(iOS Keychain)。 未加密的iTunes备份仍包含Keychain; 但是,未受保护的备份中的Keychain的内容使用设备的特定安全加密密钥进行加密,允许您从抓取备份的设备上解密内容。

从iOS 11开始,iTunes备份密码可以重置; 要做到这一点,你需要iPhone机身和锁屏密码。 从iOS 13开始,您需要使用锁屏密码在iTunes指定备份密码,或者使用iOS Forensic Toolkit。

最后,有些东西永远不会存储在本地备份中。

iTunes备份不包括:

  • 来自iTunes和App Store的内容,或直接下载到Apple Books的PDF
  • 从iTunes同步的内容,如导入的MP3或CD,视频,书籍和照片
  • 已存储在iCloud中的数据,如iCloud Photos,iMessages,以及文本(SMS)和多媒体(MMS)消息
  • 面部识别或触摸ID设置
  • Apple Pay信息和设置
  • Apple Mail数据
  • 活动,健康和钥匙串数据(要备份此内容,您需要在iTunes中使用加密备份。)

更多关于这个主题:

保存的密码

用户的密码可以存储在他们的计算机上,由流行的Web浏览器缓存或保存在密码管理器应用程序中。 存储的密码可用于解锁BitLocker加密的硬盘驱动器,访问用户Google帐户中的iCloud数据和数据,破坏强加密或访问用户的全面位置历史记录。·

Windows

在Windows中,一些具有保存密码功能的最流行的Web浏览器包括Google Chrome,Mozilla Firefox,Microsoft Edge和Internet Explorer以及Opera Browser。 在所有这些浏览器中,存储的密码仅受到松散保护。 因此,可以使用Elcomsoft Internet Password Breaker轻松提取它们。 提取密码后,您可以使用它们构建自定义字典来攻击加密文档或执行云提取。

要从用户的iCloud帐户中提取数据,请在Elcomsoft Phone Breaker中输入保存的身份验证凭据。

要从用户的Google帐户中提取数据,请在Elcomsoft Cloud Explorer中使用相应的身份验证凭据。

更多关于这个主题:

macOS

macOS将密码存储在Keychain中。 原生和第三方密码都将存储在Keychain中,至少在谷歌Chrome中是这样。 因此,访问密码需要提取和解密Keychain。

您现在可以使用Elcomsoft Phone Breaker(有Mac版)从用户的iCloud帐户中提取数据。 要从用户的Google帐户中提取数据,请在Elcomsoft Cloud Explorer (有Mac版) 中使用相应的身份验证凭据。

更多关于这个主题:

邮件账户

认证电子邮件客户端(例如,Outlook或Windows Mail)或经过在线邮件服务(例如,Gmail,Hotmail等)认证的Web浏览器可用于请求重置其他用户的帐户密码。 请注意,这对于使用双因素身份验证的Apple ID帐户不起作用。 它也不适用于Google帐户。 但是,您仍可以为其他帐户请求密码重置,例如社交网络,聊天和即时消息帐户。

Lockdown记录

Lockdown记录或配对记录经常用于访问锁定的iOS设备。 通过使用从嫌疑人计算机中提取的现有Lockdown记录,专家可以使用iOS Forensic Toolkit和其他取证工具对iOS设备进行逻辑采集。 通过逻辑采集流程,专家可以提取本地备份,访问共享和媒体文件,甚至可以提取设备崩溃日志。

从用户的计算机提取Lockdown记录可以提供从锁定的iPhone提取数据的机会。 请注意,只有AFU(After First Unlock)状态下的设备才能进行逻辑提取; 这就是为什么始终保持设备开启非常重要的原因。

关于Lockdown记录我们写了多篇文章。 虽然其中一些已有几年历史,但从那以后几乎没有变化(特别是,Apple在iOS 11.3中添加了Lockdown过期,并且在macOS中使用访问控制保护Lockdown文件)。 我们推荐以下文章:

只有在设备未进入所谓的USB限制模式时,才能进行逻辑采集(即使使用有效的Lockdown记录)。 有关USB限制模式的更多信息参见:

身份验证令牌

这些可用于访问用户的iCloud帐户中的同步数据(必须在用户的计算机上使用令牌;在macOS上,可以提取可传输的不受限制的令牌)。 还有Google Drive的令牌(可用于访问用户的Google Drive帐户中的文件)和Google帐户(可用于从用户的Google帐户中提取大量数据)。

身份验证令牌是允许客户端(Web浏览器,iCloud for Windows,Elcomsoft Phone BreakerElcomsoft Cloud Explorer等)连接到服务器而不为每个请求提供登录名和密码的数据。 这些数据存储在小文件中(Web浏览器将它们存储在cookie中)。 这些文件可用于避免用户在当前和后续会话期间输入其登录名和密码。 这些非常相同的文件可用于云取证工具验证相应的云服务,而无需提供(或甚至不知道)用户的登录名和密码。

身份验证令牌不包含密码或其哈希值。 相反,它们是完全随机的数据串。 令牌不能用于破解密码。

身份验证令牌的使用是受限的。 我们在下面列出使用令牌可以访问和不能访问的数据类型。

Apple账号

iCloud认证令牌特别难获取。 它们是什么、使用什么工具创建、存储它们的位置,以及如何以及何时使用它们是我们经常被问到的问题。 我们有许多文章阐明了身份验证令牌:

Windows: Apple将iCloud身份验证令牌绑定到创建它们的设备上。 如果转移了,这种“绑定”令牌不能在另外的设备上使用。 因此,人们只能在创建它们的同一台计算机上使用这些令牌。

macOS: macOS计算机也是同样的设备绑定。 但是,除了“绑定”标记之外,我们还能够访问和解密不受限制的标记。 您需要使用Elcomsoft Phone Breaker Forensic Edition从macOS中提取不受限制的令牌。 您需要使用相同的工具来使用令牌从iCloud下载信息。

令牌适用于:访问同步数据,包括联系人,日历和便笺; Safari浏览历史记录和打开标签页; 钱包卡片; 通话记录; iCloud照片。 您还可以从iCloud Drive访问文件,包括许多第三方应用程序容器(1Password,WhatsApp,Viber等),获取FileVailt2加密驱动器的恢复令牌,访问iCloud邮件。

您不能使用令牌:更改用户的iCloud密码; 禁用双因素身份验证; 访问密码(iCloud Keychain),屏幕时间,健康状况(iOS 12及更新版本)和消息。 您无法使用令牌下载iCloud备份。

(为了在技术上100%正确,iCloud备份可以使用非2FA帐户中的有效令牌下载。但是,这些令牌的生命周期仅限于创建令牌后的一小时。换句话说 它太短了,它的取证意义非常有限。)

更多关于这个主题:

Google账户

如果您可以访问用户的计算机(Mac或PC),则可以从该计算机中提取二进制身份验证令牌(使用Elcomsoft Cloud Explorer)并使用它来绕过密码和双因素身份验证保护。 有几种类型的令牌,它们以不同的方式受到限制。

Google身份验证令牌是小XML文件。 在Windows计算机上,它们存储在用户的硬盘上。在macOS里,它们保存在Keychain中。用户使用Chrome浏览器(对于Drive令牌是Google Drive应用)登录任何Google服务后,即会创建令牌。专家可以使用该XML文件中的数据来验证正在进行的和后续的会话,而无需要求用户重新输入密码或确认双因素身份验证提示。

Google身份验证令牌具有自己的一组限制。虽然它们没有被“绑定”到特定的计算机上并且可以轻松地转移到不同的PC或Mac,但是**令牌不能用于攻击**,**重置或恢复用户的帐户密码**。 **但是,令牌可用于关闭用户Google帐户中的双因素身份验证**。

从Chrome浏览器中提取的令牌更具通用性,允许访问许多数据类别。 另一方面,Google Drive应用生成的令牌只能用于访问用户Google Drive中的文件。

您可以在Windows和macOS平台上使用Elcomsoft Cloud Explorer自动提取、传输和使用Google身份验证令牌。

更多关于这个主题:

结论

移动取证不只是关于iPhone或Android手机机身的取证。 可以从许多其他来源提取与移动设备的使用相关的更多证据。 我们描述了可能在用户的计算机上找到的痕迹,以及它们用于移动取证的目的。

即使您无法使用身份验证令牌访问备份或任何受保护的容器,Apple iCloud可以保留比您想象的更多的实时数据作为同步数据访问。

iOS越狱的取证意义

原文:Forensic Implications of iOS Jailbreaking

June 12th, 2019 by Oleg Afonin

翻译:李毅(liyi05@qianxin.com

取证社区使用越狱来访问iOS设备的文件系统、执行物理提取和解密设备。 设备越狱是获取很多其他提取方法无法获得的很多证据的最直接的底层访问的之一的方式。

从消极方面来说,越狱是一个带有风险和其他影响的过程。 各种因素,例如越狱工具、安装方法以及理解和遵循程序的能力将影响安装越狱的风险和后果。 在本文中,我们将讨论使用各种越狱工具和安装方法的风险和后果。

为什么越狱

为什么越狱?你应该越狱吗? 说到移动取证,与其他获取方法相比,越狱设备有助于提取一些额外的信息。 在讨论不同采集方法之间的差异之前,让我们快速了解一下iOS设备可用的提取方法。

逻辑获取

逻辑获取是一种最简单、最简洁、最直接的获取方法。 在逻辑获取期间,专家可以让iPhone(iPad、iPad Touch)备份内容。 除了备份(无论用户是否使用密码保护备份),逻辑获取还可以让专家提取媒体文件(图片和视频)、崩溃日志和共享文件。 正确执行逻辑获取会生成一整套数据,包括用户钥匙串的内容(使用密码保护的备份)。

要求:iOS设备必须运行中,不能处于USB Restricted模式;您必须能够将其连接到计算机并执行配对过程(iOS 11和12在这一步中将需要密码)。 或者,可以使用从用户的计算机提取的现有配对记录。

在逻辑获取过程中提取的备份可能会加密。 如果是这种情况,专家可能会重置备份密码,这是一个自行承担后果的操作(访问[iOS越狱和物理获取分步指南](https://blog.elcomsoft.com/2019/05/step-by-step-guide-to-ios-jailbreaking-and-physical-acquisition/)查看更多信息,查看章节“如果必须重置备份密码”)。设备越狱是重置备份密码的可行替代方案。 越狱后,您将能够提取备份中可用的所有相同信息,等等。 此外,您还可以以纯文本格式查看用户的备份密码(请参阅同一文章中的“从钥匙串中提取备份密码”)。

Over-the-air(iCloud)提取

如果您知道用户的Apple ID和密码并且可以访问其第二个身份验证因素(如果在用户的Apple帐户上启用了双因素身份验证),则可以远程提取设备数据。 通过利用二进制认证令牌,可以在没有密码的情况下访问一些信息。 然而,这种情况非常有限。

要求:您必须知道用户的Apple ID登录名和密码,并且可以访问第二个身份验证因素(如果在用户帐户上启用了2FA)。 如果您需要访问受保护的数据类别(iCloud Keychain,iCloud Messages,Health等),您必须知道其中一个已注册的设备的密码或系统密码。 要访问同步数据,您可以使用现有的身份验证令牌而不是login/password/2FA序列。

在[iCloud提取](https://www.elcomsoft.com/eppb.html)过程中,您可以访问以下部分或全部内容:iCloud备份,同步数据和媒体文件。 如果您知道用户设备的密码,则可以访问其iCloud Keychain,Health和Messages数据。

Apple不断改进iCloud保护,使得除了恢复实际的iOS设备以外的其他方式访问某些类型的数据变得困难甚至无法访问。 这种猫捉老鼠的游戏中,取证软件制造商试图攻克这些保护措施,而苹果试图阻止第三方工具访问iCloud数据。

请注意,某些iCloud数据(备份,媒体文件和同步数据)可以通过法院命令从Apple获得。 但是,即使您有权提交请求,Apple也不会提供任何加密数据,例如用户密码(iCloud Keychain)、Health和Messages。

物理获取

物理获取是最深入的提取方法。 今天,iOS设备的物理获取仅限于文件系统提取,而不是镜像和解密整个分区。 使用此方法,专家可以对文件系统进行镜像,访问沙盒化的应用程序数据,解密所有Keychain记录,包括具有this_device_only属性的记录,提取系统日志等。

与其他采集方法相比,[物理提取](https://www.elcomsoft.com/eift.html)还可以访问以下所有内容:

文件系统提取:

  • 具有未提交事务和WAL文件的App数据库
  • 不备份其内容的沙盒化应用程序的数据
  • 访问安全聊天和受保护的消息(例如,Signal,Telegram和许多其他类似软件)
  • 下载的电子邮件
  • 系统日志

Keychain:

  • 使用this_device_only属性保护的项目
  • 这包括iTunes备份的密码

物理提取是最危险和最苛刻的方法。 要求列表包括设备本身处于解锁状态; 将设备与计算机配对的能力; 以及具有已知漏洞的iOS版本,以便可以安装越狱。

越狱的风险

越狱iOS设备的风险很大程度上取决于越狱工具和安装程序。 然而,今天越狱的主要风险不在于设备“变砖”。 在早期版本的iOS(包括iOS 9)中开发的越狱中存在使设备无法启动的真正风险。 这些旧的越狱正在修补内核并试图通过修补操作系统的其他部分来绕过系统的内核补丁保护(KPP)。 这从来都不是完全可靠的; 在最糟糕的情况下,设备根本无法启动。

现代越狱(针对iOS 10及更新版本)不修改内核,也不需要处理内核补丁保护。 结果,越狱设备将始终以非越狱状态启动; 你必须在每次启动时重新申请越狱。

现代越狱的两个风险是:

  1. 将设备暴露在Internet上。通过允许设备在安装越狱时联机,您允许设备同步数据,下载到设备被扣押时不存在的信息。更糟糕的是,您将使设备容易受到任何可能正在等待的远程锁定或远程擦除命令的影响。为了物理提取而安装越狱的过程与用于研究或其他目的的越狱有很大不同。特别是,取证专家正在努力使设备保持离线状态,以防止数据泄漏、不必要的同步以及可能的远程锁定或擦除设备的远程设备管理问题。虽然不存在“一般”越狱的越狱指南和手册,但为了物理获取而安装越狱具有多种取证意义和一些重要的预防措施。策略:您可以通过遵循iOS越狱指南中的正确的越狱安装过程来减轻这种风险。
  2. 越狱无法安装。越狱利用操作系统中的漏洞链来获取超级用户权限,逃离沙箱并允许执行未签名的应用程序。由于多个漏洞被连续利用,越狱过程可能随时失败。

缓解:由于不同的越狱使用不同的代码甚至可能针对不同的漏洞,不同的越狱工具具有不同的成功率。 如果您选择的原始工具失败,请尝试使用其他越狱工具。

越狱的后果

不同的越狱带来不同的后果。 Meridian、盘古、太极、Chimera或Unc0ver等经典越狱需要执行一系列的事情才能符合越狱的预期。 由于越狱的预期目的是允许从第三方库安装未签名的应用程序,越狱需要禁用代码签名检查并安装第三方软件包管理器,如Cydia或Sileo。 这样的事情需要对操作系统的核心进行侵入式修改,不可避免地重新安装和修改系统分区并将文件写入数据分区。

最近出现了新一代的越狱。 无根越狱(例如RootlessJB)在设计上不会修改系统分区。 与任何经典越狱相比,无根越狱要小得多。 另一方面,无根越狱不会轻易允许执行未签名的代码或安装第三方软件包管理器。 但是,它们确实包含一个可用的SSH守护程序,从而可以执行文件系统提取。

由于无根越狱不会改变系统分区的内容,因此可以轻松地从设备中删除此类越狱,将系统返回到越狱前的状态并在之后接收OTA更新。 一般来说,使用经典越狱时,这项任务将非常困难(有时甚至是不可能的)。

我们在博客中有一篇文章,其中提供了有关无根越狱及其与经典越狱的区别的更多信息:iOS 12 Rootless Jailbreak

Cellebrite和GrayShift等公司不依赖公共越狱来进行提取。 相反,他们使用一组未公开的漏洞来直接访问iOS设备的文件系统。 直接使用漏洞具有许多好处,因为设备的文件系统通常在提取期间不受影响。 使用漏洞进行文件系统提取后,设备上剩下的唯一痕迹将是各种系统日志中的条目。

因此,让我们比较使用经典或无根越狱来提取文件系统的后果。


经典越狱无根越狱直接提权
文件系统重载
修改系统分区
修改启动镜像(Kernel)否(iOS 10以后)
系统日志里记录项
设备可以安装OTA更新
可以访问”/”w/受限
可以访问”/Var”
解密Keychain
结果可重复

文件系统重载

一般来说,我们希望在越狱设备时避免重新加载文件系统。 虽然重新加载文件系统会打开文件系统根目录的读/写访问权限,但由于使用读/写访问可能对系统分区进行不兼容的修改,因此可能会导致设备变砖。 无需重新加载文件系统,我们就可以提取用户数据并解密Keychain。

修改系统分区

这也是我们在进行取证提取时要避免的事情。 对系统分区进行的任何修改都可能使设备“变砖”或降低其稳定性。 修改后的系统分区可能会破坏OTA更新(仍可通过iTunes进行完全更新或恢复)。 经典越狱将文件写入系统分区,以确保可以安装和启动未签名的应用程序。 无需修改系统分区,我们仍然可以访问数据分区的完整内容并解密Keychian。

修改启动镜像

早期越狱(包括针对所有最高到iOS 9版本的越狱)用来修补内核以实现不受限制的越狱。 虽然“不受限制”意味着设备在重新启动之后无限期地越狱,但修改内核具有一些严重的缺点,包括稳定性受损以及越狱设备的一般不可靠性。 由于Apple引入了先进的内核补丁保护(KPP)机制,因此修补内核变得不那么有吸引力了。 针对iOS 10和所有较新版本的iOS的公开越狱都不再修补内核。

系统日志项

越狱的安装和操作在整个系统的各种日志文件中以条目的形式留下多条痕迹。 这几乎是不可避免的,应该加以考虑。

OTA兼容性

根据越狱的类型,越狱设备在您完成提取后可能会或可能不会支持无线(OTA)更新。 一些经典的越狱会对设备进行修改,即使在越狱被删除后也无法安装OTA更新。 一些越狱使得创建系统还原点成为可能(使用APFS机制),因此至少在理论上应该可以将设备回滚到越狱前的状态。 根据我们的经验,这不够可靠。 另一方面,无根越狱根本不改变系统分区,使OTA更新变得容易。

访问文件系统的根“/“

经典越狱提供对文件系统根目录的读/写访问,从而可以转储系统分区以及数据分区的内容。 无根越狱只提供对数据分区(“/ var”)内容的访问,这足以用于取证提取。

访问数据分区“/var“

所有类型的越狱都可以访问存储在数据分区中的用户数据。 提供完整的文件系统,包括已安装的应用程序及其沙盒数据,数据库,系统日志文件等。

访问Keychain

可以使用任何一种越狱来解密Keychain(Safari中的密码和自动填充条目以及已安装的应用程序)。 可以解密所有密钥链条目,包括受最高保护级别保护并标记为this_device_only的条目。

结果可重复

越狱本质上是不可靠的。 他们使用未记录的漏洞获取超级用户权限和对文件系统的安全访问。 越狱可能无法安装或者需要多次尝试。 由于越狱会修改设备的内容,我们可能不会认为结果是完全可重复的。 然而,与经典越狱相比,无根越狱具有更干净的痕迹。

结论

毫无疑问,越狱确实有相当大的取证意义。 通过谨慎选择和使用越狱,可以显著减少负面后果的数量和严重程度。 然而,即使在最坏的情况下,物理提取的好处也可能远远大于越狱的缺点。

使用Boost::Python在C++应用程序中嵌入Python:第四部分

翻译: Leon Lee(liyi@pansafe.com)
原文:在此

在这个教程的第2部分中,我介绍了用于从C++解析Python异常的代码。在第3部分中,我使用Python ConfigParser模块实现了一个简单的配置解析类。作为该实现的一部分,我提到对于任何规模的项目,人们都希望在类中捕获并处理Python异常,以便该类的客户不必了解Python的细节。从调用者的角度来看,这个类就像任何其他C++类一样。

处理Python异常的明显方法是在每个函数中处理它们。例如,我们创建的C++ ConfigParser类的get函数将变为:

std::string ConfigParser::get(const std::string &attr, const std::string &section){
    try{
        return py::extract(conf_parser_.attr("get")(section, attr));
    }catch(boost::python::error_already_set const &){
        std::string perror_str = parse_python_exception();
        throw std::runtime_error("Error getting configuration option: " + perror_str);
    }
}

错误处理代码保持不变,但现在main函数变为:

int main(){
    Py_Initialize();
    try{
        ConfigParser parser;
        parser.parse_file("conf_file.1.conf");
        ...
        // Will raise a NoOption exception 
         cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;
    }catch(exception &e){
        cout << "Here is the error, from a C++ exception: " << e.what() << endl;
    }
}

当Python异常被抛出时,它将被解析并重新打包为一个std::runtime_error,它在调用处被捕获并像正常的C++异常一样处理(即无需经历parse_python_exception严格的操作)。对于只有少数函数或使用嵌入式Python的一两个类的项目,这肯定会有效。但是,对于更大的项目,人们希望避免大量重复的代码,它将不可避免地带来的错误。

对于我的实现,我想总是以相同的方式处理错误,但我需要一种方法来调用具有不同签名的不同函数。我决定利用boost库的另一个强大的领域:仿函数库,特别是boost::bindboost::functionboost::function提供仿函数类包装器,boost::bind绑定函数的参数。然后,这两者一起启用函数及其参数的传递,这些函数及其参数可以在以后调用。正是医生所要求的!

要使用仿函数,函数需要知道返回类型。由于我们使用不同的签名包装函数,因此函数模板可以很好地完成这一操作:

template <class return_type>
return_type call_python_func(boost::function<return_type ()> to_call, const std::string &error_pre){
    std::string error_str(error_pre);

    try{
        return to_call();
    }catch(boost::python::error_already_set const &){
        error_str = error_str + parse_python_exception();
        throw std::runtime_error(error_str);
    }
}

此函数将仿函数对象作为调用boost::python函数的函数。每个调用boost::python代码的函数现在被分成两个函数:私有的核心函数调用Python功能,公开的包装的函数使用call_python_func函数。这是更新的get函数及其合作伙伴:

string ConfigParser::get(const string &attr, const string &section){
    return call_python_func<string>(boost::bind(&ConfigParser::get_py, this, attr, section),
                                    "Error getting configuration option: ");
}

string ConfigParser::get_py(const string &attr, const string &section){
    return py::extract<string>(conf_parser_.attr("get")(section, attr));
}

get函数将传入的参数与隐式this指针绑定到get_py函数,get_py·函数又调用boost::python`执行操作所需的函数。简单有效。

当然,这里有一个权衡。不是重复的try…catch块代码和Python错误处理,每个类声明的函数数量增加了一倍。出于我的目的,我更喜欢第二种形式,因为它更有效地利用编译器来发现错误,但长度可能会有所不同。最重要的一点是在理解Python的代码级别处理Python错误。如果你的整个应用程序需要理解Python,你应该考虑用Python重写而不是嵌入,也许根据需要使用一些C++的模块。

与往常一样,您可以通过克隆github repo来完成本教程。

使用Boost::Python在C++应用程序中嵌入Python:第三部分

翻译: Leon Lee(liyi@pansafe.com)
原文:在此

本教程的第2部分中,我介绍了一种方法,使用应用程序的C++代码处理嵌入的Python代码抛出的异常。这对于调试嵌入式Python代码至关重要。在本教程中,我们将创建一个简单的C++类,它利用Python功能来处理开发实际应用程序中经常令人烦恼的部分:配置解析。

为了不让C++精英们感到愤怒,我将以外交方式说出这一点:我在C++中使用复杂的字符串操作。STL stringsstringstreams极大简化了任务,但执行应用程序级任务,并以健壮的方式执行它们,总是导致我编写更多的代码。因此,我最近使用嵌入Python,特别是ConfigParser模块,重新编写了Granola Connect(Granola Enterprise中用于处理与Granola REST API通信的守护进程)的配置解析机制。

当然,字符串操作和配置解析只是一个例子。对于第3部分,我可以选择任何数量的C++难以处理而Python中很简单的任务(例如,Web连接),但是配置解析类是一个简单但完整的用于嵌入Python以供实际使用的示例。从Github repo中获取本教程的代码。

首先,让我们创建一个涵盖非常基本的配置解析的类定义:读取和解析INI样式的文件,提取给定名称和节的字符串值,并为给定的节设置字符串值。这是类声明:

class ConfigParser{
    private:
        boost::python::object conf_parser_;

        void init();
    public:
        ConfigParser();

        bool parse_file(const std::string &filename);
        std::string get(const std::string &attr,
                        const std::string &section = "DEFAULT");
        void set(const std::string &attr,
                 const std::string &value,
                 const std::string &section = "DEFAULT");
};

ConfigParser模块提供的功能远远超出本教程所涵盖的功能,但我们在此实现的子集应作为实现更复杂功能的模板。该类的实现相当简单; 首先,构造函数加载__main__模块,提取字典,将ConfigParser模块导入命名空间,并创建一个boost::python::object类型的成员变量来包含RawConfigParser对象:

ConfigParser::ConfigParser(){
    py::object mm = py::import("__main__");
    py::object mn = mm.attr("__dict__");
    py::exec("import ConfigParser", mn);
    conf_parser_ = py::eval("ConfigParser.RawConfigParser()", mn);
}

用以下config_parser_对象执行文件解析以及值的获取和设置:

bool ConfigParser::parse_file(const std::string &filename){
    return py::len(conf_parser_.attr("read")(filename)) == 1;
}

std::string ConfigParser::get(const std::string &attr, const std::string &section){
    return py::extract<std::string>(conf_parser_.attr("get")(section, attr));
}

void ConfigParser::set(const std::string &attr, const std::string &value, const std::string &section){
    conf_parser_.attr("set")(section, attr, value);
}

在这个简单的例子中,为了简洁起见,允许传播异常。在更复杂的环境中,您几乎肯定希望让C++类处理并将Python异常重新打包为C++异常。如果性能或其他问题成为问题,您可以稍后创建一个纯C++类。

要使用该类,调用代码可以简单地将其视为普通的C++类:

int main(){
    Py_Initialize();
    try{
        ConfigParser parser;
        parser.parse_file("conf_file.1.conf");
        cout << "Directory (file 1): " << parser.get("Directory", "DEFAULT") << endl;
        parser.parse_file("conf_file.2.conf");
        cout << "Directory (file 2): " << parser.get("Directory", "DEFAULT") << endl;
        cout << "Username: " << parser.get("Username", "Auth") << endl;
        cout << "Password: " << parser.get("Password", "Auth") << endl;
        parser.set("Directory", "values can be arbitrary strings", "DEFAULT");
        cout << "Directory (force set by application): " << parser.get("Directory") << endl;
        // Will raise a NoOption exception 
        // cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;
    }catch(boost::python::error_already_set const &){
        string perror_str = parse_python_exception();
        cout << "Error during configuration parsing: " << perror_str << endl;
    }
}

就是这样:一个包含块和注释的键值配置解析器只需要50行代码。这只是冰山一角。在几乎相同长度的代码中,您可以执行各种各样的事情,这些事情在C++中最为痛苦,更容易出错且耗时:配置解析、列表和集合操作、Web连接、文件格式操作(想想XML/JSON),以及无数其他已在Python标准库中实现的任务。

第4部分中,我将介绍如何使用仿函数和Python命名空间的类来更强大和通用地调用Python代码。

使用Boost::Python在C++应用程序中嵌入Python:第二部分

翻译: Leon Lee(liyi@pansafe.com)
原文:在此

第1部分中,我们了解了如何在C++应用程序中嵌入Python,包括从应用程序调用Python代码的几种方法。虽然我之前承诺在第2部分中完整实现一个配置解析器,但我认为看一下错误解析会更有建设性。一旦我们有一个很好的方法来处理Python代码中的错误,我将在第3部分中创建承诺的配置解析器。我们开始吧!

如果您获得了本教程git repo副本并且正在使用它,您可能已经体验过boost::python处理Python错误的方式– error_already_set异常类型。如果没有,以下代码将生成异常:

namespace py = boost::python;
...
Py_Initialize();
...
py::object rand_mod = py::import("fake_module");

…它的输出不是那么有用:

terminate called after throwing an instance of 'boost::python::error_already_set' 
Aborted

简而言之,boost::python处理的Python代码中发生的任何错误都会导致库抛出此异常; 遗憾的是,该异常并未封装有关错误本身的任何信息。要提取有关错误的信息,我们将不得不求助于使用Python C API和一些Python本身的机制。首先,捕捉错误:

try{
    Py_Initialize();
    py::object rand_mod = py::import("fake_module");
}catch(boost::python::error_already_set const &){
    std::string perror_str = parse_python_exception();
    std::cout << "Error in Python: " << perror_str << std::endl;
}

Above, we’ve called the parse_python_exception function to extract the error string and print it. As this suggests, the exception data is stored statically in the Python library and not encapsulated in the exception itself. The first step in the parse_python_exception function, then, is to extract that data using the PyErr_Fetch Python C API function:

这里,我们调用parse_python_exception函数来提取错误字符串并将其打印出来。如此所示,异常数据静态存储在Python库中,而不是封装在异常本身中。parse_python_exception函数的第一步是使用Python C API的PyErr_Fetch函数提取该数据:

std::string parse_python_exception(){
    PyObject *type_ptr = NULL, *value_ptr = NULL, *traceback_ptr = NULL;
    PyErr_Fetch(&type_ptr, &value_ptr, &traceback_ptr);
    std::string ret("Unfetchable Python error");
...

由于可能存在全部、部分或没有异常数据,我们使用回退值设置返回的字符串。接下来,我们尝试从异常信息中提取和字符串化类型数据:

...
if(type_ptr != NULL){
    py::handle<> h_type(type_ptr);
    py::str type_pstr(h_type);
    py::extract<std::string> e_type_pstr(type_pstr);
    if(e_type_pstr.check())
        ret = e_type_pstr();
    else
        ret = "Unknown exception type";
}
...

在这个块中,我们首先检查是否真有一个指向类型数据的有效指针。如果存在,我们构造一个boost::python::handle指向该数据,然后我们从中创建一个str对象。此转换应确保可以进行有效的字符串提取,但要进行双重检查,我们创建一个提取对象,检查对象,然后在有效的情况下执行提取。否则,我们使用回退字符串作为类型信息。

接着,我们对异常值执行非常类似的步骤:

...
if(value_ptr != NULL){
    py::handle<> h_val(value_ptr);
    py::str a(h_val);
    py::extract<std::string> returned(a);
    if(returned.check())
        ret +=  ": " + returned();
    else
        ret += std::string(": Unparseable Python error: ");
}
...

我们将值字符串附加到现有错误字符串。对于大多数内置异常类型,值字符串是描述错误的可读字符串。

最后,我们提取回溯数据:

    if(traceback_ptr != NULL){
        py::handle<> h_tb(traceback_ptr);
        py::object tb(py::import("traceback"));
        py::object fmt_tb(tb.attr("format_tb"));
        py::object tb_list(fmt_tb(h_tb));
        py::object tb_str(py::str("\n").join(tb_list));
        py::extract<std::string> returned(tb_str);
        if(returned.check())
            ret += ": " + returned();
        else
            ret += std::string(": Unparseable Python traceback");
    }
    return ret;
}

回溯类似于类型和值提取,除了将回溯对象格式化为字符串的额外步骤。为此,我们导入traceback模块。从traceback中,我们然后提取format_tb函数并使用traceback对象的句柄调用它。这会生成一个回溯字符串列表,然后我们将它们连接成一个字符串。也许不是最漂亮的输出,但它完成了工作。最后,我们如上所述提取C ++字符串类型,并将其附加到返回的错误字符串并返回整个结果。

在前面错误的上下文中,应用程序现在生成以下输出:

Error in Python: : No module named fake_module

As I mentioned above, in Part 3 I will walk through the implementation of a configuration parser built on top of the ConfigParser Python module. Assuming, of course, that I don’t get waylaid again.

一般来说,这个函数可以更容易地找到嵌入Python代码中问题的根本原因。需要注意的是:如果您正在为嵌入解释器配置自定义Python环境(尤其是模块路径),则该parse_python_exception函数本身可能在尝试加载traceback模块时抛出一个boost::error_already_set异常,因此您可能希望将对函数的调用包装到try...catch块中并解析结果中的类型和值指针。

如上所述,在第3部分中,我将介绍构建在ConfigParserPython模块之上的配置解析器的实现。当然,假设我没有再次中断。

使用Boost::Python在C++应用程序中嵌入Python:第一部分

翻译: Leon Lee(liyi@pansafe.com)
原文:在此

在本系列教程的简介中,我说了将Python代码集成到Granola代码库中的动机。简而言之,它可以使我使用Python语言和标准库的好处来完成在C++中通常很痛苦或笨拙的任务。当然,底线是我不必移植任何已有的C++代码。

今天,我们看一下使用boost::python在C++中嵌入Python并与Python对象交互的基本步骤。我已将此部分中的所有代码放在github仓库中,请随意检出代码并使用。

从Python的内核来说,嵌入Python非常简单,不需要任何C++代码–Python发行版提供的库中包括C绑定内容。我们将跳过所有这些,直接进入通过boost::python在C++中使用Python,它提供了类包装和多态行为,相比C绑定,更与实际Python代码一致、本教程后面的部分,我们将介绍一些无法通过boost::python做到的事情(特别是多线程和错误处理)

好了,要开始的话,首先需要下载并构建boost,或者在包管理器得到一份副本。如果你选择构建,你可以只构建boost::python库(可惜不只是头文件),但是如果你经常使用C++编程,我还是建议熟悉整个boost库。如果你已经同步了上面的git仓库,确保在Makefile里把路径指向你的boost安装目录。好了,我们继续。

首先,我们需要能够构建嵌入Python的应用程序。使用gcc这不是很困难,它只是将boost::python和libpython以静态或者共享库的方式包含进来。根据你构建boost的方式不同,你可能会遇到各种困难。在github上的教程代码里,我们使用静态的boost::python库(libboost_python.a)和Python库的动态版本(libpython.so)。

我在MiserWare的开发工作的一个软性要求是使我们所有支持操作系统(一些Windows和一系列不断变化的Linux发行版)的环境保持一致。因此Granola链接到固定的Python版本,安装的版本里包括了运行代码所需要的Python库文件。也许并不理想,但是它提供了一个我肯定我们的代码将在所有支持的操作系统上运行的环境。

让我们运行一些代码。可以想象,可能需要包含正确的头文件。

Py_Initialize();     
py::object main_module = py::import(“__ main__”);     
py::object main_namespace = main_module.attr(“__ dict__”);   
 

注意,你必须直接初始化Python解释器(第一行)。虽然boost::python极大的简化了嵌入Python的任务,但是它并不能处理你需要做的所有事情。正如前面提到的,我们将在接下来的教程里看到更多的缺陷。在初始化以后,__main__模块被导入,命名空间被解析,这将产生空白的运行环境,我们可以在上面调用Python代码,添加模块和变量。

boost::python::exec("print 'Hello, world'", main_namespace);
boost::python::exec("print 'Hello, world'[3:5]", main_namespace);
boost::python::exec("print '.'.join(['1','2','3'])", main_namespace);

exec函数在指定的命名空间内运行字符串参数中的代码。所有正常的、未导入的代码都可以。当然,由于不能导入模块和提取值,因此不是很有用。

boost::python::exec("import random", main_namespace);
boost::python::object rand = boost::python::eval("random.random()", main_namespace);
std::cout << py::extract<double>(rand) << std::endl;

这里我们在命名空间__main__里通过执行相应的Python语句来导入random模块,把这个模块带入这个命名空间。当模块可用后,我们可以在这个命名空间里使用函数、对象和变量。本例里,我们使用了eval函数,它返回传入的Python语句的运行结果,来创建一个boost::python对象来包含random模块的random()函数返回的随机值。最后,我们将值以C++ double类型提取并打印出来。

这可能看上去有点……软。通过将格式化的Python字符串传递给C++函数来调用Python?这不是以一种非常面向对象的方式来处理事务。幸运的是,有一种更好的办法。

boost::python::object rand_mod = boost::python::import("random");
boost::python::object rand_func = rand_mod.attr("random");
boost::python::object rand2 = rand_func();
std::cout << boost::python::extract(rand2) << std::endl;

在这个最后的例子里,我们导入了random模块,但这次我们使用的是boost::python的import函数,它把模块加载到boost python的对象中。接下来,random函数对象从random模块中提取出来并存储在boost::python对象中。调用该函数,返回一个包含随机数的Python对象。最后double值被提取和打印出来。通常,所有Python对象都可以以这种方式处理–函数、类、内置类型。

当你开始持有复杂的标准库对象和用户定义类的实例时,它开始变得有趣。接下来的教程,我将按部就班围绕ConfigParser模块构建一个真正的配置解析类讨论从C++代码解析Python异常。