Python数字取证秘籍(三)绝不原创的飞龙

本章将涵盖揭示此信息以进行调查的配方,包括:

配方难度:简单

Python版本:2.7或3.5

操作系统:任何

EML文件格式被广泛用于存储电子邮件消息,因为它是一个结构化的文本文件,兼容多个电子邮件客户端。这个文本文件以纯文本形式存储电子邮件头部、正文内容和附件数据,使用base64来编码二进制数据,使用Quoted-Printable(QP)编码来存储内容信息。

此脚本中使用的所有库都包含在Python的标准库中。我们将使用内置的email库来读取和提取EML文件中的关键信息。

要创建一个EML解析器,我们必须:

我们首先导入用于处理参数、EML处理和解码base64编码数据的库。email库提供了从EML文件中读取数据所需的类和方法。我们将使用message_from_file()函数来解析提供的EML文件中的数据。Quopri是本书中的一个新库,我们使用它来解码HTML正文和附件中的QP编码值。base64库,正如人们所期望的那样,允许我们解码任何base64编码的数据:

from__future__importprint_functionfromargparseimportArgumentParser,FileTypefromemailimportmessage_from_fileimportosimportquopriimportbase64此配方的命令行处理程序接受一个位置参数EML_FILE,表示我们将处理的EML文件的路径。我们使用FileType类来处理文件的打开:

if__name__=='__main__':parser=ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EML_FILE",help="PathtoEMLFile",type=FileType('r'))args=parser.parse_args()main(args.EML_FILE)在main()函数中,我们使用message_from_file()函数将类似文件的对象读入email库。现在我们可以使用结果变量emlfile来访问头部、正文内容、附件和其他有效载荷信息。读取电子邮件头部只是通过迭代库的_headers属性提供的字典来处理。要处理正文内容,我们必须检查此消息是否包含多个有效载荷,并且如果是这样,将每个传递给指定的处理函数process_payload():

defmain(input_file):emlfile=message_from_file(input_file)#Startwiththeheadersforkey,valueinemlfile._headers:print("{}:{}".format(key,value))#Readpayloadprint("\nBody\n")ifemlfile.is_multipart():forpartinemlfile.get_payload():process_payload(part)else:process_payload(emlfile[1])process_payload()函数首先通过使用get_content_type()方法提取消息的MIME类型。我们将这个值打印到控制台上,并在新行上打印一些"="字符来区分这个值和消息的其余部分。

在一行中,我们使用get_payload()方法提取消息正文内容,并使用quopri.decodestring()函数解码QP编码的数据。然后,我们检查数据是否有字符集,如果我们确定了字符集,则在指定字符集的同时使用decode()方法对内容进行解码。如果编码是未知的,我们将尝试使用UTF8对对象进行解码,这是在将decode()方法留空时的默认值,以及Windows-1252:

defprocess_payload(payload):print(payload.get_content_type()+"\n"+"="*len(payload.get_content_type()))body=quopri.decodestring(payload.get_payload())ifpayload.get_charset():body=body.decode(payload.get_charset())else:try:body=body.decode()exceptUnicodeDecodeError:body=body.decode('cp1252')使用我们解码的数据,我们检查内容的MIME类型,以便正确处理电子邮件的存储。HTML信息的第一个条件,由text/htmlMIME类型指定,被写入到与输入文件相同目录中的HTML文档中。在第二个条件中,我们处理ApplicationMIME类型下的二进制数据。这些数据以base64编码的值传输,我们在使用base64.b64decode()函数写入到当前目录中的文件之前对其进行解码。二进制数据具有get_filename()方法,我们可以使用它来准确命名附件。请注意,输出文件必须以"w"模式打开第一种类型,以"wb"模式打开第二种类型。如果MIME类型不是我们在这里涵盖的类型,我们将在控制台上打印正文:

ifpayload.get_content_type()=="text/html":outfile=os.path.basename(args.EML_FILE.name)+".html"open(outfile,'w').write(body)elifpayload.get_content_type().startswith('application'):outfile=open(payload.get_filename(),'wb')body=base64.b64decode(payload.get_payload())outfile.write(body)outfile.close()print("Exported:{}\n".format(outfile.name))else:print(body)当我们执行此代码时,我们首先在控制台上看到头信息,然后是各种有效载荷。在这种情况下,我们首先有一个text/plainMIME内容,其中包含一个示例消息,然后是一个application/vnd.ms-excel附件,我们将其导出,然后是另一个text/plain块显示初始消息:

操作系统:Windows

电子邮件消息可以以许多不同的格式出现。MSG格式是存储消息内容和附件的另一种流行容器。在这个例子中,我们将学习如何使用OutlookAPI解析MSG文件。

这个配方需要安装第三方库pywin32。这意味着该脚本只能在Windows系统上兼容。我们还需要安装pywin32,就像我们在第一章中所做的那样,基本脚本和文件信息配方。

要创建MSG解析器,我们必须:

我们首先导入用于参数处理的库argparse和os,然后是来自pywin32的win32com库。我们还导入pywintypes库以正确捕获和处理pywin32错误:

from__future__importprint_functionfromargparseimportArgumentParserimportosimportwin32com.clientimportpywintypes这个配方的命令行处理程序接受两个位置参数,MSG_FILE和OUTPUT_DIR,分别表示要处理的MSG文件的路径和所需的输出文件夹。我们检查所需的输出文件夹是否存在,如果不存在,则创建它。之后,我们将这两个输入传递给main()函数:

defdisplay_msg_recipients(msg):#DisplayRecipientInformationrecipient_attrib=['Address','AutoResponse','Name','Resolved','Sendable']i=1whileTrue:try:recipient=msg.Recipients(i)exceptpywintypes.com_error:breakprint("\nRecipient{}".format(i))print("="*15)forentryinrecipient_attrib:print("{}:{}".format(entry,getattr(recipient,entry,'N/A')))i+=1extract_msg_body()函数旨在从消息中提取正文内容。msg对象以几种不同的格式公开正文内容;在本示例中,我们将导出HTML(使用HTMLBody()方法)和纯文本(使用Body()方法)版本的正文。由于这些对象是字节字符串,我们必须首先解码它们,这是通过使用cp1252代码页来完成的。有了解码后的内容,我们打开用户指定目录中的输出文件,并创建相应的*.body.html和*.body.txt文件:

defextract_msg_body(msg,out_dir):#ExtractHTMLDatahtml_data=msg.HTMLBody.encode('cp1252')outfile=os.path.join(out_dir,os.path.basename(args.MSG_FILE))open(outfile+".body.html",'wb').write(html_data)print("Exported:{}".format(outfile+".body.html"))#Extractplaintextbody_data=msg.Body.encode('cp1252')open(outfile+".body.txt",'wb').write(body_data)print("Exported:{}".format(outfile+".body.txt"))最后,extract_attachments()函数将附件数据从MSG文件导出到所需的输出目录。使用msg对象,我们再次创建一个列表attachment_attribs,表示有关附件的一系列属性。与收件人函数类似,我们使用while循环和Attachments()方法,该方法接受一个整数作为参数,以选择要迭代的附件。与之前的Recipients()方法一样,Attachments()方法从1开始索引。因此,变量i将从1开始递增,直到找不到更多的附件为止:

defextract_attachments(msg,out_dir):attachment_attribs=['DisplayName','FileName','PathName','Position','Size']i=1#Attachmentsstartat1whileTrue:try:attachment=msg.Attachments(i)exceptpywintypes.com_error:break对于每个附件,我们将其属性打印到控制台。我们提取和打印的属性在此函数开始时的attachment_attrib列表中定义。打印可用附件详细信息后,我们使用SaveAsFile()方法写入其内容,并提供一个包含输出路径和所需输出附件名称的字符串(使用FileName属性获取)。之后,我们准备移动到下一个附件,因此我们递增变量i并尝试访问下一个附件。

print("\nAttachment{}".format(i))print("="*15)forentryinattachment_attribs:print('{}:{}'.format(entry,getattr(attachment,entry,"N/A")))outfile=os.path.join(os.path.abspath(out_dir),os.path.split(args.MSG_FILE)[-1])ifnotos.path.exists(outfile):os.makedirs(outfile)outfile=os.path.join(outfile,attachment.FileName)attachment.SaveAsFile(outfile)print("Exported:{}".format(outfile))i+=1当我们执行此代码时,我们将看到以下输出,以及输出目录中的几个文件。这包括正文文本和HTML,以及任何发现的附件。消息及其附件的属性将显示在控制台窗口中。

这个脚本可以进一步改进。我们提供了一个或多个建议如下:

还存在其他用于访问MSG文件的库,包括Redemption库。该库提供了访问标头信息的处理程序,以及与此示例中显示的许多相同属性。

教程难度:简单

Python版本:N/A

要启动GoogleTakeout,我们按照以下步骤进行:

在“我的帐户”仪表板上,我们选择“个人信息和隐私”部分下的“控制您的内容”链接:

在“控制您的内容”部分,我们将看到一个“创建存档”的选项。这是我们开始GoogleTakeout收集的地方:

选择此选项时,我们将看到管理现有存档或生成新存档的选项。生成新存档时,我们将看到每个我们希望包括的Google产品的复选框。下拉箭头提供子菜单,可更改导出格式或内容。例如,我们可以选择将GoogleDrive文档导出为MicrosoftWord、PDF或纯文本格式。在这种情况下,我们将保留选项为默认值,确保邮件选项设置为收集所有邮件:

选择所需的内容后,我们可以配置存档的格式。GoogleTakeout允许我们选择存档文件类型和最大段大小,以便轻松下载和访问。我们还可以选择如何访问Takeout。此选项可以设置为将下载链接发送到被存档的帐户(默认选项)或将存档上传到帐户的GoogleDrive或其他第三方云服务,这可能会修改比必要更多的信息以保留这些数据。我们选择接收电子邮件,然后选择“创建存档”以开始该过程!

配方难度:中等

Python版本:3.5

此脚本中使用的所有库都包含在Python的标准库中。我们使用内置的mailbox库来解析GoogleTakeout结构化的MBOX文件。

要实现这个脚本,我们必须:

from__future__importprint_functionfromargparseimportArgumentParserimportmailboximportosimporttimeimportcsvfromtqdmimporttqdmimportbase64这个配方的命令行处理程序接受两个位置参数,MBOX和OUTPUT_DIR,分别表示要处理的MBOX文件的路径和期望的输出文件夹。这两个参数都传递给main()函数来启动脚本:

if__name__=='__main__':parser=ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("MBOX",help="Pathtomboxfile")parser.add_argument("OUTPUT_DIR",help="Pathtooutputdirectorytowritereport""andexportedcontent")args=parser.parse_args()main(args.MBOX,args.OUTPUT_DIR)main()函数从调用mailbox库的mbox类开始。使用这个类,我们可以通过提供文件路径和一个可选的工厂参数来解析MBOX文件,这在我们的情况下是一个自定义阅读器函数。使用这个库,我们现在有一个包含我们可以交互的消息对象的可迭代对象。我们使用内置的len()方法来打印MBOX文件中包含的消息数量。让我们首先看看custom_reader()函数是如何工作的:

defmain(mbox_file,output_dir):#ReadintheMBOXFileprint("Readingmboxfile...")mbox=mailbox.mbox(mbox_file,factory=custom_reader)print("{}messagestoparse".format(len(mbox)))这个配方需要一些函数来运行(看到我们做了什么吗...),但custom_reader()方法与其他方法有些不同。这个函数是mailbox库的一个阅读器方法。我们需要创建这个函数,因为默认的阅读器不能处理诸如cp1252之类的编码。我们可以将其他编码添加到这个阅读器中,尽管ASCII和cp1252是MBOX文件的两种最常见的编码。

在输入数据流上使用read()方法后,它尝试使用ASCII代码页对数据进行解码。如果不成功,它将依赖cp1252代码页来完成任务。使用cp1252代码页解码时遇到的任何错误都将被替换为替换字符U+FFFD,通过向decode()方法提供errors关键字并将其设置为"replace"来实现。我们使用mailbox.mboxMessage()函数以适当的格式返回解码后的内容:

defcustom_reader(data_stream):data=data_stream.read()try:content=data.decode("ascii")except(UnicodeDecodeError,UnicodeEncodeError)ase:content=data.decode("cp1252",errors="replace")returnmailbox.mboxMessage(content)回到main()函数,在开始处理消息之前,我们准备了一些变量。具体来说,我们设置了parsed_data结果列表,为附件创建了一个输出目录,并定义了MBOX元数据报告的columns。这些列也将用于使用get()方法从消息中提取信息。其中两列不会从消息对象中提取信息,而是在处理附件后包含我们分配的数据。为了保持一致性,我们将这些值保留在columns列表中,因为它们将默认为"N/A"值:

parsed_data=[]attachments_dir=os.path.join(output_dir,"attachments")ifnotos.path.exists(attachments_dir):os.makedirs(attachments_dir)columns=["Date","From","To","Subject","X-Gmail-Labels","Return-Path","Received","Content-Type","Message-ID","X-GM-THRID","num_attachments_exported","export_path"]当我们开始迭代消息时,我们实现了一个tqdm进度条来跟踪迭代过程。由于mbox对象具有长度属性,因此我们不需要为tqdm提供任何额外的参数。在循环内部,我们定义了msg_data字典来存储消息结果,然后尝试通过第二个for循环使用get()方法在header_data字典中查询columns键来分配消息属性:

iflen(message.get_payload()):export_path=write_payload(message,attachments_dir)msg_data['num_attachments_exported']=len(export_path)msg_data['export_path']=",".join(export_path)每处理完一条消息,其数据都会被附加到parsed_data列表中。在处理完所有消息后,将调用create_report()方法,并传递parsed_data列表和所需的输出CSV名称。让我们回溯一下,首先看一下write_payload()方法:

parsed_data.append(msg_data)#CreateCSVreportcreate_report(parsed_data,os.path.join(output_dir,"mbox_report.csv"),columns)由于消息可能具有各种各样的有效载荷,我们需要编写一个专门的函数来处理各种MIME类型。write_payload()方法就是这样一个函数。该函数首先通过get_payload()方法提取有效载荷,并进行快速检查,看看有效载荷内容是否包含多个部分。如果是,我们会递归调用此函数来处理每个子部分,通过迭代有效载荷并将输出附加到export_path变量中:

defwrite_payload(msg,out_dir):pyld=msg.get_payload()export_path=[]ifmsg.is_multipart():forentryinpyld:export_path+=write_payload(entry,out_dir)如果有效载荷不是多部分的,我们使用get_content_type()方法确定其MIME类型,并创建逻辑来根据类别适当地处理数据源。应用程序、图像和视频等数据类型通常表示为base64编码数据,允许将二进制信息作为ASCII字符传输。因此,大多数格式(包括文本类别中的一些格式)都要求我们在提供写入之前对数据进行解码。在其他情况下,数据已存在为字符串,并且可以按原样写入文件。无论如何,方法通常是相同的,数据被解码(如果需要),并使用export_content()方法将其内容写入文件系统。最后,表示导出项目路径的字符串被附加到export_path列表中:

else:if"name="inmsg.get('Content-Disposition',"N/A"):content=base64.b64decode(msg.get_payload())export_path.append(export_content(msg,out_dir,content))elif"name="inmsg.get('Content-Type',"N/A"):content=base64.b64decode(msg.get_payload())export_path.append(export_content(msg,out_dir,content))returnexport_pathexport_content()函数首先调用get_filename()函数,这个方法从msg对象中提取文件名。对文件名进行额外处理以提取扩展名(如果有的话),如果没有找到则使用通用的.FILE扩展名:

file_name="{}_{:.4f}.{}".format(file_name.rsplit(".",1)[0],time.time(),file_ext)file_name=os.path.join(out_dir,file_name)这个函数中代码的最后一部分处理文件内容的实际导出。这个if语句处理不同的文件模式("w"或"wb"),根据源类型。写入数据后,我们返回用于导出的文件路径。这个路径将被添加到我们的元数据报告中:

ifisinstance(content_data,str):open(file_name,'w').write(content_data)else:open(file_name,'wb').write(content_data)returnfile_name下一个函数get_filename()从消息中提取文件名以准确表示这些文件的名称。文件名可以在"Content-Disposition"或"Content-Type"属性中找到,并且通常以"name="或"filename="字符串开头。对于这两个属性,逻辑基本相同。该函数首先用一个空格替换任何换行符,然后在分号和空格上拆分字符串。这个分隔符通常分隔这些属性中的值。使用列表推导,我们确定哪个元素包含name=子字符串,并将其用作文件名:

defget_filename(msg):if'name='inmsg.get("Content-Disposition","N/A"):fname_data=msg["Content-Disposition"].replace("\r\n","")fname=[xforxinfname_data.split(";")if'name='inx]file_name=fname[0].split("=",1)[-1]elif'name='inmsg.get("Content-Type","N/A"):fname_data=msg["Content-Type"].replace("\r\n","")fname=[xforxinfname_data.split(";")if'name='inx]file_name=fname[0].split("=",1)[-1]如果这两个内容属性为空,我们分配一个通用的NO_FILENAME并继续准备文件名。提取潜在的文件名后,我们删除任何不是字母数字、空格或句号的字符,以防止在系统中写入文件时出错。准备好我们的文件系统安全文件名后,我们将其返回供前面讨论的export_content()方法使用:

else:file_name="NO_FILENAME"fchars=[xforxinfile_nameifx.isalnum()orx.isspace()orx=="."]return"".join(fchars)最后,我们已经到达了准备讨论CSV元数据报告的阶段。create_report()函数类似于本书中我们已经看到的各种变体,它使用DictWriter类从字典列表创建CSV报告。哒哒!

defcreate_report(output_data,output_file,columns):withopen(output_file,'w',newline="")asoutfile:csvfile=csv.DictWriter(outfile,columns)csvfile.writeheader()csvfile.writerows(output_data)这个脚本创建了一个CSV报告和一个附件目录。第一个截图显示了CSV报告的前几列和行以及数据如何显示给用户:

这第二个截图显示了这些相同行的最后几列,并反映了附件信息的报告方式。这些文件路径可以被跟踪以访问相应的附件:

食谱难度:困难

Python版本:2.7

操作系统:Linux

该配方需要安装libpff及其Python绑定pypff才能正常运行。这个库在GitHub上提供了工具和Python绑定,用于处理和提取PST文件中的数据。我们将在Ubuntu16.04上为Python2设置这个库以便开发。这个库也可以为Python3构建,不过在本节中我们将使用Python2的绑定。

在安装所需的库之前,我们必须安装一些依赖项。使用Ubuntu的apt软件包管理器,我们将安装以下八个软件包。您可能希望将这个Ubuntu环境保存好,因为我们将在第八章以及以后的章节中广泛使用它:

sudoapt-getinstallautomakeautoconflibtoolpkg-configautopointgitpython-dev安装依赖项后,转到GitHub存储库并下载所需的库版本。这个配方是使用pypff库的libpff-experimental-20161119版本开发的。接下来,一旦提取了发布的内容,打开终端并导航到提取的目录,并执行以下命令以进行发布:

最后,我们可以通过打开Python解释器,导入pypff并运行pypff.get_version()方法来检查库的安装情况,以确保我们有正确的发布版本。

我们按照以下步骤提取PST消息内容:

该脚本首先导入用于处理参数、编写电子表格、执行正则表达式搜索和处理PST文件的库:

from__future__importprint_functionfromargparseimportArgumentParserimportcsvimportpypffimportre此配方的命令行处理程序接受两个位置参数,PFF_FILE和CSV_REPORT,分别表示要处理的PST文件的路径和所需的输出CSV路径。在这个配方中,我们不使用main()函数,而是立即使用pypff.file()对象来实例化pff_obj变量。随后,我们使用open()方法并尝试访问用户提供的PST。我们将此PST传递给process_folders()方法,并将返回的字典列表存储在parsed_data变量中。在对pff_obj变量使用close()方法后,我们使用write_data()函数写入PST元数据报告,通过传递所需的输出CSV路径和处理后的数据字典:

if__name__=='__main__':parser=ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("PFF_FILE",help="PathtoPSTorOSTFile")parser.add_argument("CSV_REPORT",help="PathtoCSVreportlocation")args=parser.parse_args()#Openfilepff_obj=pypff.file()pff_obj.open(args.PFF_FILE)#Parseandclosefileparsed_data=process_folders(pff_obj.root_folder)pff_obj.close()#WriteCSVreportwrite_data(args.CSV_REPORT,parsed_data)这个配方由几个处理PST文件不同元素的函数组成。process_folders()函数处理文件夹处理和迭代。在处理这些文件夹时,我们将它们的名称、子文件夹的数量以及该文件夹中的消息数量打印到控制台。这可以通过在pff_folder对象上调用number_of_sub_folders和number_of_sub_messages属性来实现:

defprocess_folders(pff_folder):folder_name=pff_folder.nameifpff_folder.nameelse"N/A"print("Folder:{}(sub-dir:{}/sub-msg:{})".format(folder_name,pff_folder.number_of_sub_folders,pff_folder.number_of_sub_messages))在打印这些消息后,我们设置了data_list,它负责存储处理过的消息数据。当我们遍历文件夹中的消息时,我们调用process_message()方法来创建带有处理过的消息数据的字典对象。紧接着,我们将文件夹名称添加到字典中,然后将其附加到结果列表中。

第二个循环通过递归调用process_folders()函数并将子文件夹传递给它,然后将结果字典列表附加到data_list中。这使我们能够遍历PST并提取所有数据,然后返回data_list并编写CSV报告:

#Processmessageswithinafolderdata_list=[]formsginpff_folder.sub_messages:data_dict=process_message(msg)data_dict['folder']=folder_namedata_list.append(data_dict)#Processfolderswithinafolderforfolderinpff_folder.sub_folders:data_list+=process_folders(folder)returndata_listprocess_message()函数负责访问消息的各种属性,包括电子邮件头信息。正如在以前的示例中所看到的,我们使用对象属性的列表来构建结果的字典。然后我们遍历attribs字典,并使用getattr()方法将适当的键值对附加到data_dict字典中。最后,如果存在电子邮件头,我们通过使用transport_headers属性来确定,我们将从process_headers()函数中提取的附加值更新到data_dict字典中:

defprocess_message(msg):#Extractattributesattribs=['conversation_topic','number_of_attachments','sender_name','subject']data_dict={}forattribinattribs:data_dict[attrib]=getattr(msg,attrib,"N/A")ifmsg.transport_headersisnotNone:data_dict.update(process_headers(msg.transport_headers))returndata_dictprocess_headers()函数最终返回一个包含提取的电子邮件头数据的字典。这些数据以键值对的形式显示,由冒号和空格分隔。由于头部中的内容可能存储在新的一行上,我们使用正则表达式来检查是否在行首有一个键,后面跟着一个值。如果我们找不到与模式匹配的键(任意数量的字母或破折号字符后跟着一个冒号),我们将把新值附加到先前的键上,因为头部以顺序方式显示信息。在这个函数的结尾,我们有一些特定的代码行,使用isinstance()来处理字典值的赋值。这段代码检查键的类型,以确保值被分配给键的方式不会覆盖与给定键关联的任何数据:

defprocess_headers(header):#Readandprocessheaderinformationkey_pattern=re.compile("^([A-Za-z\-]+:)(.*)$")header_data={}forlineinheader.split("\r\n"):iflen(line)==0:continuereg_result=key_pattern.match(line)ifreg_result:key=reg_result.group(1).strip(":").strip()value=reg_result.group(2).strip()else:value=lineifkey.lower()inheader_data:ifisinstance(header_data[key.lower()],list):header_data[key.lower()].append(value)else:header_data[key.lower()]=[header_data[key.lower()],value]else:header_data[key.lower()]=valuereturnheader_data最后,write_data()方法负责创建元数据报告。由于我们可能从电子邮件头解析中有大量的列名,我们遍历数据并提取不在列表中已定义的不同列名。使用这种方法,我们确保来自PST的动态信息不会被排除。在for循环中,我们还将data_list中的值重新分配到formatted_data_list中,主要是将列表值转换为字符串,以更容易地将数据写入电子表格。csv库很好地确保了单元格内的逗号被转义并由我们的电子表格应用程序适当处理:

defwrite_data(outfile,data_list):#Buildoutadditionalcolumnsprint("WritingReport:",outfile)columns=['folder','conversation_topic','number_of_attachments','sender_name','subject']formatted_data_list=[]forentryindata_list:tmp_entry={}fork,vinentry.items():ifknotincolumns:columns.append(k)ifisinstance(v,list):tmp_entry[k]=",".join(v)else:tmp_entry[k]=vformatted_data_list.append(tmp_entry)使用csv.DictWriter类,我们打开文件,写入头部和每一行到输出文件:

#WriteCSVreportwithopen(outfile,'wb')asopenfile:csvfile=csv.DictWriter(openfile,columns)csvfile.writeheader()csvfile.writerows(formatted_data_list)当这个脚本运行时,将生成一个CSV报告,其外观应该与以下截图中显示的类似。在水平滚动时,我们可以看到在顶部指定的列名;特别是在电子邮件头列中,大多数这些列只包含少量的值。当您在您的环境中对更多的电子邮件容器运行此代码时,请注意哪些列是最有用的,并且在您处理PST时最常见,以加快分析的速度:

这个过程可以进一步改进。我们提供了一个或多个以下建议:

在这种情况下,我们还可以利用Redemtion库来访问Outlook中的信息。

这些天,遇到配备某种形式的事件或活动监控软件的现代系统并不罕见。这种软件可能被实施以协助安全、调试或合规要求。无论情况如何,这些宝贵的信息宝库通常被广泛利用于各种类型的网络调查。日志分析的一个常见问题是需要筛选出感兴趣的子集所需的大量数据。通过本章的配方,我们将探索具有很大证据价值的各种日志,并演示快速处理和审查它们的方法。具体来说,我们将涵盖:

此脚本中使用的所有库都包含在Python的标准库中。

为了在Python中解释常见的日期格式,我们执行以下操作:

if__name__=='__main__':parser=ArgumentParser(description=__description__,formatter_class=ArgumentDefaultsHelpFormatter,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("date_value",help="Rawdatevaluetoparse")parser.add_argument("source",help="Sourceformatofdate",choices=ParseDate.get_supported_formats())parser.add_argument("type",help="Datatypeofinputvalue",choices=('number','hex'),default='int')args=parser.parse_args()date_parser=ParseDate(args.date_value,args.source,args.type)date_parser.run()print(date_parser.timestamp)让我们看看ParseDate类是如何工作的。通过使用一个类,我们可以轻松地扩展和在其他脚本中实现这段代码。从命令行参数中,我们接受日期值、日期源和值类型的参数。这些值和输出变量timestamp在__init__方法中被定义:

defrun(self):ifself.source=='unix-epoch':self.parse_unix_epoch()elifself.source=='unix-epoch-ms':self.parse_unix_epoch(True)elifself.source=='windows-filetime':self.parse_windows_filetime()为了帮助未来想要使用这个库的人,我们添加了一个查看支持的格式的方法。通过使用@classmethod装饰器,我们可以在不需要先初始化类的情况下公开这个函数。这就是我们可以在命令行处理程序中使用get_supported_formats()方法的原因。只需记住在添加新功能时更新它!

在转换值后,我们评估是否应将其视为毫秒值,如果是,则在进一步处理之前将其除以1,000。随后,我们使用datetime类的fromtimestamp()方法将数字转换为datetime对象。最后,我们将这个日期格式化为人类可读的格式,并将这个字符串存储在timestamp属性中。

defparse_unix_epoch(self,milliseconds=False):ifself.data_type=='hex':conv_value=int(self.date_value)ifmilliseconds:conv_value=conv_value/1000.0elifself.data_type=='number':conv_value=float(self.date_value)ifmilliseconds:conv_value=conv_value/1000.0else:print("Unsupporteddatatype'{}'provided".format(self.data_type))sys.exit('1')ts=dt.fromtimestamp(conv_value)self.timestamp=ts.strftime('%Y-%m-%d%H:%M:%S.%f')parse_windows_filetime()类方法处理FILETIME格式,通常存储为十六进制值。使用与之前相似的代码块,我们将"hex"或"number"值转换为Python对象,并对任何其他提供的格式引发错误。唯一的区别是在进一步处理之前,我们将日期值除以10而不是1,000。

在之前的方法中,datetime库处理了纪元偏移,这次我们需要单独处理这个偏移。使用timedelta类,我们指定毫秒值,并将其添加到代表FILETIME格式纪元的datetime对象中。现在得到的datetime对象已经准备好供我们格式化和输出给用户了:

食谱难度:中等

为了正确制作这个配方,我们需要采取以下步骤:

我们首先导入用于处理参数和日志记录的库,然后是我们需要解析和验证日志信息的内置库。这些包括re正则表达式库和shlex词法分析器库。我们还包括sys和csv来处理日志消息和报告的输出。我们通过调用getLogger()方法初始化了该配方的日志对象。

from__future__importprint_functionfromargparseimportArgumentParser,FileTypeimportreimportshleximportloggingimportsysimportcsvlogger=logging.getLogger(__file__)在导入之后,我们为从日志中解析的字段定义模式。这些信息在日志之间可能会有所不同,尽管这里表达的模式应该涵盖日志中的大多数元素。

您可能需要添加、删除或重新排序以下定义的模式,以正确解析您正在使用的IIS日志。这些模式应该涵盖IIS日志中常见的元素。

我们将这些模式构建为名为iis_log_format的元组列表,其中第一个元组元素是列名,第二个是用于验证预期内容的正则表达式模式。通过使用正则表达式模式,我们可以定义数据必须遵循的一组规则以使其有效。这些列必须按它们在日志中出现的顺序来表达,否则代码将无法正确地将值映射到列。

iis_log_format=[("date",re.compile(r"\d{4}-\d{2}-\d{2}")),("time",re.compile(r"\d\d:\d\d:\d\d")),("s-ip",re.compile(r"((25[0-5]|2[0-4][0-9]|[01][0-9][0-9])(\.|$)){4}")),("cs-method",re.compile(r"(GET)|(POST)|(PUT)|(DELETE)|(OPTIONS)|(HEAD)|(CONNECT)")),("cs-uri-stem",re.compile(r"([A-Za-z0-1/\.-]*)")),("cs-uri-query",re.compile(r"([A-Za-z0-1/\.-]*)")),("s-port",re.compile(r"\d*")),("cs-username",re.compile(r"([A-Za-z0-1/\.-]*)")),("c-ip",re.compile(r"((25[0-5]|2[0-4][0-9]|[01][0-9][0-9])(\.|$)){4}")),("cs(User-Agent)",re.compile(r".*")),("sc-status",re.compile(r"\d*")),("sc-substatus",re.compile(r"\d*")),("sc-win32-status",re.compile(r"\d*")),("time-taken",re.compile(r"\d*"))]此配方的命令行处理程序接受两个位置参数,iis_log和csv_report,分别表示要处理的IIS日志和所需的CSV路径。此外,此配方还接受一个可选参数l,指定配方日志文件的输出路径。

接下来,我们初始化了该配方的日志实用程序,并为控制台和基于文件的日志记录进行了配置。这一点很重要,因为我们应该以正式的方式注意到当我们无法为用户解析一行时。通过这种方式,如果出现问题,他们不应该在错误的假设下工作,即所有行都已成功解析并显示在生成的CSV电子表格中。我们还希望记录运行时消息,包括脚本的版本和提供的参数。在这一点上,我们准备调用main()函数并启动脚本。有关设置日志对象的更详细解释,请参阅第一章中的日志配方,基本脚本和文件信息配方。

虽然IIS日志存储为以空格分隔的值,但它们使用双引号来转义包含空格的字符串。例如,useragent字符串是一个单一值,但通常包含一个或多个空格。使用shlex模块,我们可以使用shlex()方法解析带有双引号的空格的行,并通过正确地在空格值上分隔数据来自动处理引号转义的空格。这个库可能会减慢处理速度,因此我们只在包含双引号字符的行上使用它。

defmain(iis_log,report_file,logger):parsed_logs=[]forraw_lineiniis_log:line=raw_line.strip()log_entry={}ifline.startswith("#")orlen(line)==0:continueif'\"'inline:line_iter=shlex.shlex(line_iter)else:line_iter=line.split("")将行正确分隔后,我们使用enumerate函数逐个遍历记录中的每个元素,并提取相应的列名和模式。使用模式,我们在值上调用match()方法,如果匹配,则在log_entry字典中创建一个条目。如果值不匹配模式,我们记录一个错误,并在日志文件中提供整行。在遍历每个列后,我们将记录字典附加到初始解析日志记录列表,并对剩余行重复此过程。

forcount,split_entryinenumerate(line_iter):col_name,col_pattern=iis_log_format[count]ifcol_pattern.match(split_entry):log_entry[col_name]=split_entryelse:logger.error("Unknowncolumnpatterndiscovered.""Linepreservedinfullbelow")logger.error("UnparsedLine:{}".format(line))parsed_logs.append(log_entry)处理完所有行后,我们在准备write_csv()方法之前向控制台打印状态消息。我们使用一个简单的列表推导表达式来提取iis_log_format列表中每个元组的第一个元素,这代表一个列名。有了提取的列,让我们来看看报告编写器。

logger.info("Parsed{}lines".format(len(parsed_logs)))cols=[x[0]forxiniis_log_format]logger.info("Creatingreportfile:{}".format(report_file))write_csv(report_file,cols,parsed_logs)logger.info("Reportcreated")报告编写器使用我们之前探讨过的方法创建一个CSV文件。由于我们将行存储为字典列表,我们可以使用csv.DictWriter类的四行代码轻松创建报告。

defwrite_csv(outfile,fieldnames,data):withopen(outfile,'w',newline="")asopen_outfile:csvfile=csv.DictWriter(open_outfile,fieldnames)csvfile.writeheader()csvfile.writerows(data)当我们查看脚本生成的CSV报告时,我们会在样本输出中看到以下字段:

这个脚本可以进一步改进。以下是一个建议:

菜谱难度:中等

Splunk是一个将NoSQL数据库与摄取和查询引擎结合在一起的平台,使其成为一个强大的分析工具。它的数据库的操作方式类似于Elasticsearch或MongoDB,允许存储文档或结构化记录。因此,我们不需要为了将记录存储在数据库中而提供具有一致键值映射的记录。这就是使NoSQL数据库对于日志分析如此有用的原因,因为日志格式可能根据事件类型而变化。

在这个步骤中,我们学习将上一个步骤的CSV报告索引到Splunk中,从而可以在平台内部与数据交互。我们还设计脚本来针对数据集运行查询,并将响应查询的结果子集导出到CSV文件。这些过程分别处理,因此我们可以根据需要独立查询和导出数据。

这个步骤需要安装第三方库splunk-sdk。此脚本中使用的所有其他库都包含在Python的标准库中。此外,我们必须在主机操作系统上安装Splunk,并且由于splunk-sdk库的限制,必须使用Python2来运行脚本。

新安装的Splunk的默认用户名和密码是admin和changeme。

在Splunk实例激活后,我们现在可以安装API库。这个库处理从RESTAPI到Python对象的转换。在撰写本书时,SplunkAPI只能在Python2中使用。splunk-sdk库可以使用pip安装:

现在环境已经正确配置,我们可以开始开发代码。这个脚本将新数据索引到Splunk,对该数据运行查询,并将响应我们查询的数据子集导出到CSV文件。为了实现这一点,我们需要:

首先导入此脚本所需的库,包括新安装的splunklib。为了防止由于用户无知而引起不必要的错误,我们使用sys库来确定执行脚本的Python版本,并在不是Python2时引发错误。

这个步骤的命令行处理程序接受一个位置输入action,表示要运行的操作(索引、查询或导出)。此步骤还支持七个可选参数:index、config、file、query、cols、host和port。让我们开始看看所有这些选项都做什么。

index参数实际上是一个必需的参数,用于指定要从中摄取、查询或导出数据的Splunk索引的名称。这可以是现有的或新的index名称。config参数是指包含Splunk实例的用户名和密码的配置文件。如参数帮助中所述,此文件应受保护并存储在代码执行位置之外。在企业环境中,您可能需要进一步保护这些凭据。

if__name__=='__main__':parser=ArgumentParser(description=__description__,formatter_class=ArgumentDefaultsHelpFormatter,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument('action',help="Actiontorun",choices=['index','query','export'])parser.add_argument('--index-name',help="Nameofsplunkindex",required=True)parser.add_argument('--config',help="Placewherelogindetailsarestored.""Shouldhavetheusernameonthefirstlineand""thepasswordonthesecond.""PleaseProtectthisfile!",default=os.path.expanduser("~/.splunk_py.ini"))file参数将用于提供要index到平台的文件的路径,或用于指定要将导出的query数据写入的文件名。例如,我们将使用file参数指向我们希望从上一个配方中摄取的CSV电子表格。query参数也具有双重作用,它可以用于从Splunk运行查询,也可以用于指定要导出为CSV的查询ID。这意味着index和query操作只需要其中一个参数,但export操作需要两个参数。

parser.add_argument('--file',help="Pathtofile")parser.add_argument('--query',help="Splunkquerytorunorsidof""existingquerytoexport")最后一组参数允许用户修改配方的默认属性。例如,cols参数可用于指定从源数据中导出的列及其顺序。由于我们将查询和导出IIS日志,因此我们已经知道可用的列,并且对我们感兴趣。您可能希望根据正在探索的数据类型指定替代默认列。我们的最后两个参数包括host和port参数,每个参数默认为本地服务器,但可以配置为允许您与替代实例进行交互。

withopen(args.config,'r')asopen_conf:username,password=[x.strip()forxinopen_conf.readlines()]conn_dict={'host':args.host,'port':int(args.port),'username':username,'password':password}del(username)del(password)service=client.connect(**conn_dict)del(conn_dict)iflen(service.apps)==0:print("Loginlikelyunsuccessful,cannotfindanyapplications")sys.exit()我们继续处理提供的参数,将列转换为列表并创建Spelunking类实例。要初始化该类,我们必须向其提供service变量、要执行的操作、索引名称和列。使用这些信息,我们的类实例现在已经准备就绪。

cols=args.cols.split(",")spelunking=Spelunking(service,args.action,args.index_name,cols)接下来,我们使用一系列if-elif-else语句来处理我们预期遇到的三种不同操作。如果用户提供了index操作,我们首先确认可选的file参数是否存在,如果不存在则引发错误。如果我们找到它,我们将该值分配给Spelunking类实例的相应属性。对于query和export操作,我们重复这种逻辑,确认它们也使用了正确的可选参数。请注意,我们使用os.path.abspath()函数为类分配文件的绝对路径。这允许splunklib在系统上找到正确的文件。也许这是本书中最长的参数处理部分,我们已经完成了必要的逻辑,现在可以调用类的run()方法来启动特定操作的处理。

classSpelunking(object):def__init__(self,service,action,index_name,cols):self.service=serviceself.action=actionself.index=index_nameself.file=Noneself.query=Noneself.sid=Noneself.job=Noneself.cols=colsrun()方法负责使用get_or_create_index()方法从Splunk实例获取index对象。它还检查在命令行指定了哪个动作,并调用相应的类实例方法。

defrun(self):index_obj=self.get_or_create_index()ifself.action=='index':self.index_data(index_obj)elifself.action=='query':self.query_index()elifself.action=='export':self.export_report()returnget_or_create_index()方法,顾名思义,首先测试指定的索引是否存在,并连接到它,或者如果没有找到该名称的索引,则创建一个新的索引。由于这些信息存储在service变量的indexes属性中,作为一个类似字典的对象,我们可以很容易地通过名称测试索引的存在。

defget_or_create_index(self):#Createanewindexifself.indexnotinself.service.indexes:returnservice.indexes.create(self.index)else:returnself.service.indexes[self.index]要从文件中摄取数据,比如CSV文件,我们可以使用一行语句将信息发送到index_data()方法中的实例。这个方法使用splunk_index对象的upload()方法将文件发送到Splunk进行摄取。虽然CSV文件是一个简单的例子,说明我们可以如何导入数据,但我们也可以使用前面的方法从原始日志中读取数据到Splunk实例,而不需要中间的CSV步骤。为此,我们希望使用index对象的不同方法,允许我们逐个发送每个解析的事件。

defquery_index(self):self.query=self.query+"|fields+"+",".join(self.cols)self.job=self.service.jobs.create(self.query,rf=self.cols)self.sid=self.job.sidprint("Queryjob{}created.willexpirein{}seconds".format(self.sid,self.job['ttl']))正如之前提到的,export_report()方法使用前面方法中提到的SID来检查作业是否完成,并检索要导出的数据。为了做到这一点,我们遍历可用的作业,如果我们的作业不存在,则发出警告。如果找到作业,但is_ready()方法返回False,则作业仍在处理中,尚未准备好导出结果。

defexport_report(self):job_obj=Noneforjinself.service.jobs:ifj.sid==self.sid:job_obj=jifjob_objisNone:print("JobSID{}notfound.Diditexpire".format(self.sid))sys.exit()ifnotjob_obj.is_ready():print("JobSID{}isstillprocessing.""Pleasewaittore-run".format(self.sir))如果作业通过了这两个测试,我们从Splunk中提取数据,并使用write_csv()方法将其写入CSV文件。在这之前,我们需要初始化一个列表来存储作业结果。接下来,我们检索结果,指定感兴趣的列,并将原始数据读入job_results变量。幸运的是,splunklib提供了一个ResultsReader,它将job_results变量转换为一个字典列表。我们遍历这个列表,并将每个字典附加到export_data列表中。最后,我们提供文件路径、列名和要导出到CSV写入器的数据集。

export_data=[]job_results=job_obj.results(rf=self.cols)forresultinresults.ResultsReader(job_results):export_data.append(result)self.write_csv(self.file,self.cols,export_data)这个类中的write_csv()方法是一个@staticmethod。这个装饰器允许我们在类中使用一个通用的方法,而不需要指定一个实例。这个方法无疑会让那些在本书的其他地方使用过的人感到熟悉,我们在那里打开输出文件,创建一个DictWriter对象,然后将列标题和数据写入文件。

@staticmethoddefwrite_csv(outfile,fieldnames,data):withopen(outfile,'wb')asopen_outfile:csvfile=csv.DictWriter(open_outfile,fieldnames,extrasaction="ignore")csvfile.writeheader()csvfile.writerows(data)在我们的假设用例中,第一阶段将是索引前一个食谱中CSV电子表格中的数据。如下片段所示,我们提供了前一个食谱中的CSV文件,并将其添加到Splunk索引中。接下来,我们寻找所有用户代理为iPhone的条目。最后,最后一个阶段涉及从查询中获取输出并创建一个CSV报告。

成功执行这三个命令后,我们可以打开并查看过滤后的输出:

这个脚本可以进一步改进。我们提供了一个或多个建议,如下所示:

操作系统:任意

这个脚本将利用以下步骤:

我们首先导入必要的库来处理参数、解释日期和写入电子表格。在Python中处理文本文件的一个很棒的地方是你很少需要第三方库。

from__future__importprint_functionfromargparseimportArgumentParser,FileTypefromdatetimeimportdatetimeimportcsv这个食谱的命令行处理程序接受两个位置参数,daily_out和output_report,分别代表daily.out日志文件的路径和CSV电子表格的期望输出路径。请注意,我们通过argparse.FileType类传递一个打开的文件对象进行处理。随后,我们用日志文件初始化ProcessDailyOut类,并调用run()方法,并将返回的结果存储在parsed_events变量中。然后我们调用write_csv()方法,使用processor类对象中定义的列将结果写入到所需输出目录中的电子表格中。

if__name__=='__main__':parser=ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("daily_out",help="Pathtodaily.outfile",type=FileType('r'))parser.add_argument("output_report",help="Pathtocsvreport")args=parser.parse_args()processor=ProcessDailyOut(args.daily_out)parsed_events=processor.run()write_csv(args.output_report,processor.report_columns,parsed_events)在ProcessDailyOut类中,我们设置了用户提供的属性,并定义了报告中使用的列。请注意,我们添加了两组不同的列:disk_status_columns和report_columns。report_columns只是disk_status_columns,再加上两个额外的字段来标识条目的日期和时区。

classProcessDailyOut(object):def__init__(self,daily_out):self.daily_out=daily_outself.disk_status_columns=['Filesystem','Size','Used','Avail','Capacity','iused','ifree','%iused','Mountedon']self.report_columns=['event_date','event_tz']+\self.disk_status_columnsrun()方法首先遍历提供的日志文件。在从每行的开头和结尾去除空白字符后,我们验证内容以识别部分中断。"--Endofdailyoutput--"字符串中断了日志文件中的每个条目。每个条目包含几个由新行分隔的数据部分。因此,我们必须使用几个代码块来分割和处理每个部分。

在这个循环中,我们收集来自单个事件的所有行,并将其传递给process_event()方法,并将处理后的结果追加到最终返回的parsed_events列表中。

在此事件中,我们可以看到第一个元素是日期值和时区,后面是一系列子部分。每个子部分标题都是以冒号结尾的行;我们使用这一点来拆分文件中的各种数据元素,如下面的代码所示。我们使用部分标题作为键,其内容(如果存在)作为值,然后进一步处理每个子部分。

elifline.count(":")==2:try:split_line=line.split()timezone=split_line[4]date_str="".join(split_line[:4]+[split_line[-1]])try:date_val=datetime.strptime(date_str,"%a%b%d%H:%M:%S%Y")exceptValueError:date_val=datetime.strptime(date_str,"%a%b%d%H:%M:%S%Y")event_data["event_date"]=[date_val,timezone]section_data=[]section_header=""exceptValueError:section_data.append(line)exceptIndexError:section_data.append(line)此条件的最后一部分将任何具有内容的行附加到section_data变量中,以根据需要进行进一步处理。这可以防止空白行进入,并允许我们捕获两个部分标题之间的所有信息。

else:iflen(line):section_data.append(line)通过调用任何子部分处理器来关闭此函数。目前,我们只处理磁盘信息子部分,使用process_disk()方法,尽管可以开发代码来提取其他感兴趣的值。此方法接受事件信息和事件日期作为其输入。磁盘信息作为处理过的磁盘信息元素列表返回,我们将其返回给run()方法,并将值添加到处理过的事件列表中。

defprocess_disk(self,disk_lines,event_dates):iflen(disk_lines)==0:return{}processed_data=[]forline_count,lineinenumerate(disk_lines):ifline_count==0:continueprepped_lines=[xforxinline.split("")iflen(x.strip())!=0]接下来,我们初始化一个名为disk_info的字典,其中包含了此快照的日期和时区详细信息。for循环使用enumerate()函数将值映射到它们的列名。如果列名包含"/Volumes/"(驱动器卷的标准挂载点),我们将连接剩余的拆分项。这样可以确保保留具有空格名称的卷。

disk_info={"event_date":event_dates[0],"event_tz":event_dates[1]}forcol_count,entryinenumerate(prepped_lines):curr_col=self.disk_status_columns[col_count]if"/Volumes/"inentry:disk_info[curr_col]="".join(prepped_lines[col_count:])breakdisk_info[curr_col]=entry.strip()最内层的for循环通过将磁盘信息附加到processed_data列表来结束。一旦磁盘部分中的所有行都被处理,我们就将processed_data列表返回给父函数。

processed_data.append(disk_info)returnprocessed_data最后,我们简要介绍了write_csv()方法,它使用DictWriter类来打开文件并将标题行和内容写入CSV文件。

defwrite_csv(outfile,fieldnames,data):withopen(outfile,'w',newline="")asopen_outfile:csvfile=csv.DictWriter(open_outfile,fieldnames)csvfile.writeheader()csvfile.writerows(data)当我们运行这个脚本时,我们可以在CSV报告中看到提取出的细节。这里展示了这个输出的一个例子:

使用我们刚刚开发的代码来解析macOS的daily.out日志,我们将这个功能添加到Axiom中,由MagnetForensics开发,用于自动提取这些事件。由于Axiom支持处理取证镜像和松散文件,我们可以提供完整的获取或只是daily.out日志的导出作为示例。通过这个工具提供的API,我们可以访问和处理其引擎发现的文件,并直接在Axiom中返回审查结果。

MagnetForensics团队开发了一个API,用于Python和XML,以支持在Axiom中创建自定义artifact。截至本书编写时,PythonAPI仅适用于运行Python版本2.7的IronPython。虽然我们在这个平台之外开发了我们的代码,但我们可以按照本教程中的步骤轻松地将其集成到Axiom中。我们使用了Axiom版本1.1.3.5726来测试和开发这个教程。

我们首先需要在Windows实例中安装Axiom,并确保我们的代码稳定且可移植。此外,我们的代码需要在沙盒中运行。Axiom沙盒限制了对第三方库的使用以及对可能导致代码与应用程序外部系统交互的一些Python模块和函数的访问。因此,我们设计了我们的daily.out解析器,只使用在沙盒中安全的内置库,以演示使用这些自定义artifact的开发的便利性。

要开发和实现自定义artifact,我们需要:

对于这个脚本,我们导入了axiom库和datetime库。请注意,我们已经删除了之前的argparse和csv导入,因为它们在这里是不必要的。

from__future__importprint_functionfromaxiomimport*fromdatetimeimportdatetime接下来,我们必须粘贴前一个教程中的ProcessDailyOut类,不包括write_csv或参数处理代码,以在这个脚本中使用。由于当前版本的API不允许导入,我们必须将所有需要的代码捆绑到一个单独的脚本中。为了节省页面并避免冗余,我们将在本节中省略代码块(尽管它在本章附带的代码文件中存在)。

下一个类是DailyOutArtifact,它是AxiomAPI提供的Artifact类的子类。在定义插件的名称之前,我们调用AddHunter()方法,提供我们的(尚未显示的)hHunter类。

classDailyOutArtifact(Artifact):def__init__(self):self.AddHunter(DailyOutHunter())defGetName(self):return'daily.outparser'这个类的最后一个方法CreateFragments()指定了如何处理已处理的daily.out日志结果的单个条目。就AxiomAPI而言,片段是用来描述artifact的单个条目的术语。这段代码允许我们添加自定义列名,并为这些列分配适当的类别和数据类型。这些类别包括日期、位置和工具定义的其他特殊值。我们artifact的大部分列将属于None类别,因为它们不显示特定类型的信息。

一个重要的分类区别是DateTimeLocal与DateTime:DateTime将日期呈现为UTC值呈现给用户,因此我们需要注意选择正确的日期类别。因为我们从daily.out日志条目中提取了时区,所以在这个示例中我们使用DateTimeLocal类别。FragmentType属性是所有值的字符串,因为该类不会将值从字符串转换为其他数据类型。

defCreateFragments(self):self.AddFragment('SnapshotDate-LocalTime(yyyy-mm-dd)',Category.DateTimeLocal,FragmentType.DateTime)self.AddFragment('SnapshotTimezone',Category.None,FragmentType.String)self.AddFragment('VolumeName',Category.None,FragmentType.String)self.AddFragment('FilesystemMount',Category.None,FragmentType.String)self.AddFragment('VolumeSize',Category.None,FragmentType.String)self.AddFragment('VolumeUsed',Category.None,FragmentType.String)self.AddFragment('PercentageUsed',Category.None,FragmentType.String)接下来的类是我们的Hunter。这个父类用于运行处理代码,并且正如你将看到的,指定了将由Axiom引擎提供给插件的平台和内容。在这种情况下,我们只想针对计算机平台和一个单一名称的文件运行。RegisterFileName()方法是指定插件将请求哪些文件的几种选项之一。我们还可以使用正则表达式或文件扩展名来选择我们想要处理的文件。

classDailyOutHunter(Hunter):def__init__(self):self.Platform=Platform.ComputerdefRegister(self,registrar):registrar.RegisterFileName('daily.out')Hunt()方法是魔法发生的地方。首先,我们获取一个临时路径,在沙箱内可以读取文件,并将其分配给temp_daily_out变量。有了这个打开的文件,我们将文件对象交给ProcessDailyOut类,并使用run()方法解析文件,就像上一个示例中一样。

defHunt(self,context):temp_daily_out=open(context.Searchable.FileCopy,'r')processor=ProcessDailyOut(temp_daily_out)parsed_events=processor.run()在收集了解析的事件信息之后,我们准备将数据“发布”到软件并显示给用户。在for循环中,我们首先初始化一个Hit()对象,使用AddValue()方法向新片段添加数据。一旦我们将事件值分配给了一个hit,我们就使用PublishHit()方法将hit发布到平台,并继续循环直到所有解析的事件都被发布:

forentryinparsed_events:hit=Hit()hit.AddValue("SnapshotDate-LocalTime(yyyy-mm-dd)",entry['event_date'].strftime("%Y-%m-%d%H:%M:%S"))hit.AddValue("SnapshotTimezone",entry['event_tz'])hit.AddValue("VolumeName",entry['Mountedon'])hit.AddValue("FilesystemMount",entry["Filesystem"])hit.AddValue("VolumeSize",entry['Size'])hit.AddValue("VolumeUsed",entry['Used'])hit.AddValue("PercentageUsed",entry['Capacity'])self.PublishHit(hit)最后一部分代码检查文件是否不是None,如果是,则关闭它。这是处理代码的结尾,如果在系统上发现另一个daily.out文件,可能会再次调用它!

iftemp_daily_outisnotNone:temp_daily_out.close()最后一行注册了我们的辛勤工作到Axiom的引擎,以确保它被框架包含和调用。

RegisterArtifact(DailyOutArtifact())要在Axiom中使用新开发的工件,我们需要采取一些步骤来导入并针对图像运行代码。首先,我们需要启动AxiomProcess。这是我们将加载、选择并针对提供的证据运行工件的地方。在工具菜单下,我们选择管理自定义工件选项:

在管理自定义工件窗口中,我们将看到任何现有的自定义工件,并可以像这样导入新的工件:

我们将添加我们的自定义工件,更新的管理自定义工件窗口应该显示工件的名称:

现在我们可以按下确定并继续进行Axiom,添加证据并配置我们的处理选项。当我们到达计算机工件选择时,我们要确认选择运行自定义工件。可能不用说:我们应该只在机器运行macOS或者在其上有macOS分区时运行这个工件:

完成剩余的配置选项后,我们可以开始处理证据。处理完成后,我们运行AxiomExamine来查看处理结果。如下截图所示,我们可以导航到工件审查的自定义窗格,并看到插件解析的列!这些列可以使用Axiom中的标准选项进行排序和导出,而无需我们额外的代码:

此示例需要安装第三方库yara。此脚本中使用的所有其他库都包含在Python的标准库中。可以使用pip安装此库:

此脚本有四个主要的开发步骤:

此脚本导入所需的库来处理参数解析、文件和文件夹迭代、编写CSV电子表格,以及yara库来编译和扫描YARA规则。

from__future__importprint_functionfromargparseimportArgumentParser,ArgumentDefaultsHelpFormatterimportosimportcsvimportyara这个示例的命令行处理程序接受两个位置参数,yara_rules和path_to_scan,分别表示YARA规则的路径和要扫描的文件或文件夹。此示例还接受一个可选参数output,如果提供,将扫描结果写入电子表格而不是控制台。最后,我们将这些值传递给main()方法。

if__name__=='__main__':parser=ArgumentParser(description=__description__,formatter_class=ArgumentDefaultsHelpFormatter,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument('yara_rules',help="PathtoYararuletoscanwith.Maybefileorfolderpath.")parser.add_argument('path_to_scan',help="Pathtofileorfoldertoscan")parser.add_argument('--output',help="PathtooutputaCSVreportofscanresults")args=parser.parse_args()main(args.yara_rules,args.path_to_scan,args.output)在main()函数中,我们接受yara规则的路径、要扫描的文件或文件夹以及输出文件(如果有)。由于yara规则可以是文件或目录,我们使用ios.isdir()方法来确定我们是否在整个目录上使用compile()方法,或者如果输入是一个文件,则使用filepath关键字将其传递给该方法。compile()方法读取规则文件或文件并创建一个我们可以与我们扫描的对象进行匹配的对象。

defmain(yara_rules,path_to_scan,output):ifos.path.isdir(yara_rules):yrules=yara.compile(yara_rules)else:yrules=yara.compile(filepath=yara_rules)一旦规则被编译,我们执行类似的if-else语句来处理要扫描的路径。如果要扫描的输入是一个目录,我们将其传递给process_directory()函数,否则,我们使用process_file()方法。两者都使用编译后的YARA规则和要扫描的路径,并返回包含任何匹配项的字典列表。

ifos.path.isdir(path_to_scan):match_info=process_directory(yrules,path_to_scan)else:match_info=process_file(yrules,path_to_scan)正如你可能猜到的,如果指定了输出路径,我们最终将把这个字典列表转换为CSV报告,使用我们在columns列表中定义的列。然而,如果输出参数是None,我们将以不同的格式将这些数据写入控制台。

columns=['rule_name','hit_value','hit_offset','file_name','rule_string','rule_tag']ifoutputisNone:write_stdout(columns,match_info)else:write_csv(output,columns,match_info)process_directory()函数本质上是遍历目录并将每个文件传递给process_file()函数。这减少了脚本中冗余代码的数量。返回的每个处理过的条目都被添加到match_info列表中,因为返回的对象是一个列表。一旦我们处理了每个文件,我们将完整的结果列表返回给父函数。

defprocess_file(yrules,file_path):match=yrules.match(file_path)match_info=[]forrule_setinmatch:forhitinrule_set.strings:match_info.append({'file_name':file_path,'rule_name':rule_set.rule,'rule_tag':",".join(rule_set.tags),'hit_offset':hit[0],'rule_string':hit[1],'hit_value':hit[2]})returnmatch_infowrite_stdout()函数如果用户没有指定输出文件,则将匹配信息报告到控制台。我们遍历match_info列表中的每个条目,并以冒号分隔、换行分隔的格式打印出match_info字典中的每个列名及其值。在每个条目之后,我们打印30个等号来在视觉上将条目分隔开。

defwrite_stdout(columns,match_info):forentryinmatch_info:forcolincolumns:print("{}:{}".format(col,entry[col]))print("="*30)write_csv()方法遵循标准约定,使用DictWriter类来写入标题和所有数据到表格中。请注意,这个函数已经调整为在Python3中处理CSV写入,使用了'w'模式和newline参数。

defwrite_csv(outfile,fieldnames,data):withopen(outfile,'w',newline="")asopen_outfile:csvfile=csv.DictWriter(open_outfile,fieldnames)csvfile.writeheader()csvfile.writerows(data)使用这段代码,我们可以在命令行提供适当的参数,并生成任何匹配的报告。以下截图显示了用于检测Python文件和键盘记录器的自定义规则:

这些规则显示在输出的CSV报告中,如果没有指定报告,则显示在控制台中,如下所示:

在本章中,我们将涵盖以下配方:

SleuthKit及其Python绑定pytsk3可能是最知名的Python数字取证库。该库提供了丰富的支持,用于访问和操作文件系统。借助支持库(如pyewf),它们可以用于处理EnCase流行的E01格式等常见数字取证容器。如果没有这些库(以及许多其他库),我们在数字取证中所能完成的工作将受到更多限制。由于其作为一体化文件系统分析工具的宏伟目标,pytsk3可能是我们在本书中使用的最复杂的库。

出于这个原因,我们专门制定了一些配方,探索了这个库的基本原理。到目前为止,配方主要集中在松散文件支持上。这种惯例到此为止。我们将会经常使用这个库来与数字取证证据进行交互。了解如何与数字取证容器进行交互将使您的Python数字取证能力提升到一个新的水平。

在本章中,我们将学习如何安装pytsk3和pyewf,这两个库将允许我们利用SleuthKit和E01镜像支持。此外,我们还将学习如何执行基本任务,如访问和打印分区表,遍历文件系统,按扩展名导出文件,以及在数字取证容器中搜索已知的不良哈希。您将学习以下内容:

使用pyewf和pytsk3将带来一整套新的工具和操作,我们必须首先学习。在这个配方中,我们将从基础知识开始:打开数字取证容器。这个配方支持raw和E01镜像。请注意,与我们之前的脚本不同,由于在使用这些库的Python3.X版本时发现了一些错误,这些配方将使用Python2.X。也就是说,主要逻辑在两个版本之间并没有区别,可以很容易地移植。在学习如何打开容器之前,我们需要设置我们的环境。我们将在下一节中探讨这个问题。

除了一些脚本之外,我们在本书的大部分内容中都是与操作系统无关的。然而,在这里,我们将专门提供在Ubuntu16.04.2上构建的说明。在Ubuntu的新安装中,执行以下命令以安装必要的依赖项:

sudoapt-getupdate&&sudoapt-get-yupgradesudoapt-getinstallpython-pipgitautoconfautomakeautopointlibtoolpkg-config除了前面提到的两个库(pytsk3和pyewf)之外,我们还将使用第三方模块tabulate来在控制台打印表格。由于这是最容易安装的模块,让我们首先完成这个任务,执行以下操作:

信不信由你,我们也可以使用pip安装pytsk3:

我们采用以下方法来打开法证证据容器:

我们导入了一些库来帮助解析参数、处理证据容器和文件系统,并创建表格式的控制台数据。

from__future__importprint_functionimportargparseimportosimportpytsk3importpyewfimportsysfromtabulateimporttabulate这个配方的命令行处理程序接受两个位置参数,EVIDENCE_FILE和TYPE,它们代表证据文件的路径和证据文件的类型(即raw或ewf)。请注意,对于分段的E01文件,您只需要提供第一个E01的路径(假设其他分段在同一个目录中)。在对证据文件进行一些输入验证后,我们将提供两个输入给main()函数,并开始执行脚本。

if__name__=='__main__':parser=argparse.ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EVIDENCE_FILE",help="Evidencefilepath")parser.add_argument("TYPE",help="Typeofevidence:raw(dd)orEWF(E01)",choices=("raw","ewf"))parser.add_argument("-o","--offset",help="Partitionbyteoffset",type=int)args=parser.parse_args()ifos.path.exists(args.EVIDENCE_FILE)and\os.path.isfile(args.EVIDENCE_FILE):main(args.EVIDENCE_FILE,args.TYPE,args.offset)else:print("[-]Suppliedinputfile{}doesnotexistorisnota""file".format(args.EVIDENCE_FILE))sys.exit(1)在main()函数中,我们首先检查我们正在处理的证据文件的类型。如果是E01容器,我们需要首先使用pyewf创建一个句柄,然后才能使用pytsk3访问其内容。对于raw图像,我们可以直接使用pytsk3访问其内容,而无需先执行这个中间步骤。

在这里使用pyewf.glob()方法来组合E01容器的所有段,如果有的话,并将段的名称存储在一个列表中。一旦我们有了文件名列表,我们就可以创建E01句柄对象。然后我们可以使用这个对象来打开filenames。

defmain(image,img_type,offset):print("[+]Opening{}".format(image))ifimg_type=="ewf":try:filenames=pyewf.glob(image)exceptIOError:_,e,_=sys.exc_info()print("[-]InvalidEWFformat:\n{}".format(e))sys.exit(2)ewf_handle=pyewf.handle()ewf_handle.open(filenames)接下来,我们必须将ewf_handle传递给EWFImgInfo类,该类将创建pytsk3对象。这里的else语句是为了raw图像,可以使用pytsk3.Img_Info函数来实现相同的任务。现在让我们看看EWFImgInfo类,了解EWF文件是如何稍有不同地处理的。

#OpenPYTSK3handleonEWFImageimg_info=EWFImgInfo(ewf_handle)else:img_info=pytsk3.Img_Info(image)这个脚本组件的代码来自pyewf的Python开发页面的将pyewf与pytsk3结合使用部分。

这个EWFImgInfo类继承自pytsk3.Img_Info基类,属于TSK_IMG_TYPE_EXTERNAL类型。重要的是要注意,接下来定义的三个函数,close()、read()和get_size(),都是pytsk3要求的,以便与证据容器进行适当的交互。有了这个简单的类,我们现在可以使用pytsk3来处理任何提供的E01文件。

classEWFImgInfo(pytsk3.Img_Info):def__init__(self,ewf_handle):self._ewf_handle=ewf_handlesuper(EWFImgInfo,self).__init__(url="",type=pytsk3.TSK_IMG_TYPE_EXTERNAL)defclose(self):self._ewf_handle.close()defread(self,offset,size):self._ewf_handle.seek(offset)returnself._ewf_handle.read(size)defget_size(self):returnself._ewf_handle.get_media_size()回到main()函数,我们已经成功地为raw或E01镜像创建了pytsk3处理程序。现在我们可以开始访问文件系统。如前所述,此脚本旨在处理逻辑图像而不是物理图像。我们将在下一个步骤中引入对物理图像的支持。访问文件系统非常简单;我们通过在pytsk3处理程序上调用FS_Info()函数来实现。

#GetFilesystemHandletry:fs=pytsk3.FS_Info(img_info,offset)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))exit()有了对文件系统的访问权限,我们可以遍历根目录中的文件夹和文件。首先,我们使用文件系统上的open_dir()方法,并指定根目录**/**作为输入来访问根目录。接下来,我们创建一个嵌套的列表结构,用于保存表格内容,稍后我们将使用tabulate将其打印到控制台。这个列表的第一个元素是表格的标题。

之后,我们将开始遍历图像,就像处理任何Python可迭代对象一样。每个对象都有各种属性和函数,我们从这里开始使用它们。首先,我们使用f.info.name.name属性提取对象的名称。然后,我们使用f.info.meta.type属性检查我们处理的是目录还是文件。如果这等于内置的TSK_FS_META_TYPE_DIR对象,则将f_type变量设置为DIR;否则,设置为FILE。

root_dir=fs.open_dir(path="/")table=[["Name","Type","Size","CreateDate","ModifyDate"]]forfinroot_dir:name=f.info.name.nameiff.info.meta.type==pytsk3.TSK_FS_META_TYPE_DIR:f_type="DIR"else:f_type="FILE"size=f.info.meta.sizecreate=f.info.meta.crtimemodify=f.info.meta.mtimetable.append([name,f_type,size,create,modify])print(tabulate(table,headers="firstrow"))当我们运行脚本时,我们可以了解到在证据容器的根目录中看到的文件和文件夹,如下截图所示:

在这个食谱中,我们学习如何使用tabulate查看和打印分区表。此外,对于E01容器,我们将打印存储在证据文件中的E01获取和容器元数据。通常,我们将使用给定机器的物理磁盘镜像。在接下来的任何过程中,我们都需要遍历不同的分区(或用户选择的分区)来获取文件系统及其文件的处理。因此,这个食谱对于我们建立对SleuthKit及其众多功能的理解至关重要。

有关pytsk3、pyewf和tabulate的构建环境和设置详细信息,请参阅打开获取食谱中的入门部分。此脚本中使用的所有其他库都包含在Python的标准库中。

该食谱遵循以下基本步骤:

我们导入了许多库来帮助解析参数、处理证据容器和文件系统,并创建表格式的控制台数据。

from__future__importprint_functionimportargparseimportosimportpytsk3importpyewfimportsysfromtabulateimporttabulate这个配方的命令行处理程序接受两个位置参数,EVIDENCE_FILE和TYPE,它们代表证据文件的路径和证据文件的类型。此外,如果用户在处理证据文件时遇到困难,他们可以使用可选的p开关手动提供分区。这个开关在大多数情况下不应该是必要的,但作为一种预防措施已经添加。在执行输入验证检查后,我们将这三个参数传递给main()函数。

if__name__=='__main__':parser=argparse.ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EVIDENCE_FILE",help="Evidencefilepath")parser.add_argument("TYPE",help="TypeofEvidence",choices=("raw","ewf"))parser.add_argument("-p",help="PartitionType",choices=("DOS","GPT","MAC","SUN"))args=parser.parse_args()ifos.path.exists(args.EVIDENCE_FILE)and\os.path.isfile(args.EVIDENCE_FILE):main(args.EVIDENCE_FILE,args.TYPE,args.p)else:print("[-]Suppliedinputfile{}doesnotexistorisnota""file".format(args.EVIDENCE_FILE))sys.exit(1)main()函数在很大程度上与之前的配方相似,至少最初是这样。我们必须首先创建pyewf句柄,然后使用EWFImgInfo类来创建,如前面在pytsk3句柄中所示。如果您想了解更多关于EWFImgInfo类的信息,请参阅打开获取配方。但是,请注意,我们添加了一个额外的行调用e01_metadata()函数来将E01元数据打印到控制台。现在让我们来探索一下这个函数。

defmain(image,img_type,part_type):print("[+]Opening{}".format(image))ifimg_type=="ewf":try:filenames=pyewf.glob(image)exceptIOError:print("[-]InvalidEWFformat:\n{}".format(e))sys.exit(2)ewf_handle=pyewf.handle()ewf_handle.open(filenames)e01_metadata(ewf_handle)#OpenPYTSK3handleonEWFImageimg_info=EWFImgInfo(ewf_handle)else:img_info=pytsk3.Img_Info(image)e01_metadata()函数主要依赖于get_header_values()和get_hash_values()方法来获取E01特定的元数据。get_header_values()方法返回各种类型的获取和媒体元数据的键值对字典。我们使用循环来遍历这个字典,并将键值对打印到控制台。

同样,我们使用hashes字典的循环将图像的存储获取哈希打印到控制台。最后,我们调用一个属性和一些函数来打印获取大小的元数据。

defe01_metadata(e01_image):print("\nEWFAcquisitionMetadata")print("-"*20)headers=e01_image.get_header_values()hashes=e01_image.get_hash_values()forkinheaders:print("{}:{}".format(k,headers[k]))forhinhashes:print("Acquisition{}:{}".format(h,hashes[h]))print("BytesperSector:{}".format(e01_image.bytes_per_sector))print("NumberofSectors:{}".format(e01_image.get_number_of_sectors()))print("TotalSize:{}".format(e01_image.get_media_size()))有了这些,我们现在可以回到main()函数。回想一下,在本章的第一个配方中,我们没有为物理获取创建支持(这完全是有意的)。然而,现在,我们使用Volume_Info()函数添加了对此的支持。虽然pytsk3一开始可能令人生畏,但要欣赏到目前为止我们介绍的主要函数中使用的命名约定的一致性:Img_Info、FS_Info和Volume_Info。这三个函数对于访问证据容器的内容至关重要。在这个配方中,我们不会使用FS_Info()函数,因为这里的目的只是打印分区表。

我们尝试在try-except块中访问卷信息。首先,我们检查用户是否提供了p开关,如果是,则将该分区类型的属性分配给一个变量。然后,我们将它与pytsk3句柄一起提供给Volume_Info方法。否则,如果没有指定分区,我们调用Volume_Info方法,并只提供pytsk3句柄对象。如果我们尝试这样做时收到IOError,我们将捕获异常作为e并将其打印到控制台,然后退出。如果我们能够访问卷信息,我们将其传递给part_metadata()函数,以将分区数据打印到控制台。

try:ifpart_typeisnotNone:attr_id=getattr(pytsk3,"TSK_VS_TYPE_"+part_type)volume=pytsk3.Volume_Info(img_info,attr_id)else:volume=pytsk3.Volume_Info(img_info)exceptIOError:_,e,_=sys.exc_info()print("[-]Unabletoreadpartitiontable:\n{}".format(e))sys.exit(3)part_metadata(volume)part_metadata()函数在逻辑上相对较轻。我们创建一个嵌套的列表结构,如前面的配方中所见,第一个元素代表最终的表头。接下来,我们遍历卷对象,并将分区地址、类型、偏移量和长度附加到table列表中。一旦我们遍历了分区,我们使用tabulate使用firstrow作为表头将这些数据的表格打印到控制台。

defpart_metadata(vol):table=[["Index","Type","OffsetStart(Sectors)","Length(Sectors)"]]forpartinvol:table.append([part.addr,part.desc.decode("utf-8"),part.start,part.len])print("\nPartitionMetadata")print("-"*20)print(tabulate(table,headers="firstrow"))运行此代码时,如果存在,我们可以在控制台中查看有关获取和分区信息的信息:

在这个配方中,我们学习如何递归遍历文件系统并创建一个活动文件列表。作为法庭鉴定人,我们经常被问到的第一个问题之一是“设备上有什么数据?”。在这里,活动文件列表非常有用。在Python中,创建松散文件的文件列表是一个非常简单的任务。然而,这将会稍微复杂一些,因为我们处理的是法庭图像而不是松散文件。这个配方将成为未来脚本的基石,因为它将允许我们递归访问和处理图像中的每个文件。正如您可能已经注意到的,本章的配方是相互建立的,因为我们开发的每个函数都需要进一步探索图像。类似地,这个配方将成为未来配方中的一个重要部分,用于迭代目录并处理文件。

有关pytsk3和pyewf的构建环境和设置详细信息,请参考开始部分中的打开获取配方。此脚本中使用的所有其他库都包含在Python的标准库中。

我们在这个配方中执行以下步骤:

我们导入了许多库来帮助解析参数、解析日期、创建CSV电子表格,以及处理证据容器和文件系统。

from__future__importprint_functionimportargparseimportcsvfromdatetimeimportdatetimeimportosimportpytsk3importpyewfimportsys这个配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE、TYPE和OUTPUT_CSV,分别代表证据文件的路径、证据文件的类型和输出CSV文件。与上一个配方类似,可以提供可选的p开关来指定分区类型。我们使用os.path.dirname()方法来提取CSV文件的所需输出目录路径,并使用os.makedirs()函数,如果不存在,则创建必要的输出目录。

if__name__=='__main__':parser=argparse.ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EVIDENCE_FILE",help="Evidencefilepath")parser.add_argument("TYPE",help="TypeofEvidence",choices=("raw","ewf"))parser.add_argument("OUTPUT_CSV",help="OutputCSVwithlookupresults")parser.add_argument("-p",help="PartitionType",choices=("DOS","GPT","MAC","SUN"))args=parser.parse_args()directory=os.path.dirname(args.OUTPUT_CSV)ifnotos.path.exists(directory)anddirectory!="":os.makedirs(directory)一旦我们通过检查输入证据文件是否存在并且是一个文件来验证了输入证据文件,四个参数将被传递给main()函数。如果在输入的初始验证中出现问题,脚本将在退出之前将错误打印到控制台。

ifos.path.exists(args.EVIDENCE_FILE)and\os.path.isfile(args.EVIDENCE_FILE):main(args.EVIDENCE_FILE,args.TYPE,args.OUTPUT_CSV,args.p)else:print("[-]Suppliedinputfile{}doesnotexistorisnota""file".format(args.EVIDENCE_FILE))sys.exit(1)在main()函数中,我们用None实例化卷变量,以避免在脚本后面引用它时出错。在控制台打印状态消息后,我们检查证据类型是否为E01,以便正确处理它并创建有效的pyewf句柄,如在打开获取配方中更详细地演示的那样。有关更多详细信息,请参阅该配方。最终结果是为用户提供的证据文件创建pytsk3句柄img_info。

defmain(image,img_type,output,part_type):volume=Noneprint("[+]Opening{}".format(image))ifimg_type=="ewf":try:filenames=pyewf.glob(image)exceptIOError:_,e,_=sys.exc_info()print("[-]InvalidEWFformat:\n{}".format(e))sys.exit(2)ewf_handle=pyewf.handle()ewf_handle.open(filenames)#OpenPYTSK3handleonEWFImageimg_info=EWFImgInfo(ewf_handle)else:img_info=pytsk3.Img_Info(image)接下来,我们尝试使用pytsk3.Volume_Info()方法访问图像的卷,通过提供图像句柄作为参数。如果提供了分区类型参数,我们将其属性ID添加为第二个参数。如果在尝试访问卷时收到IOError,我们将捕获异常作为e并将其打印到控制台。然而,请注意,当我们收到错误时,我们不会退出脚本。我们将在下一个函数中解释原因。最终,我们将volume、img_info和output变量传递给open_fs()方法。

try:ifpart_typeisnotNone:attr_id=getattr(pytsk3,"TSK_VS_TYPE_"+part_type)volume=pytsk3.Volume_Info(img_info,attr_id)else:volume=pytsk3.Volume_Info(img_info)exceptIOError:_,e,_=sys.exc_info()print("[-]Unabletoreadpartitiontable:\n{}".format(e))open_fs(volume,img_info,output)open_fs()方法尝试以两种方式访问容器的文件系统。如果volume变量不是None,它会遍历每个分区,并且如果该分区符合某些条件,则尝试打开它。但是,如果volume变量是None,它将尝试直接在图像句柄img上调用pytsk3.FS_Info()方法。正如我们所看到的,后一种方法将适用于逻辑图像,并为我们提供文件系统访问权限,而前一种方法适用于物理图像。让我们看看这两种方法之间的区别。

无论使用哪种方法,我们都创建一个recursed_data列表来保存我们的活动文件元数据。在第一种情况下,我们有一个物理图像,我们遍历每个分区,并检查它是否大于2,048扇区,并且在其描述中不包含Unallocated、Extended或PrimaryTable这些词。对于符合这些条件的分区,我们尝试使用FS_Info()函数访问它们的文件系统,方法是提供pytsk3img对象和分区的偏移量(以字节为单位)。

如果我们能够访问文件系统,我们将使用open_dir()方法获取根目录,并将其与分区地址ID、文件系统对象、两个空列表和一个空字符串一起传递给recurse_files()方法。这些空列表和字符串将在对此函数进行递归调用时发挥作用,我们很快就会看到。一旦recurse_files()方法返回,我们将活动文件的元数据附加到recursed_data列表中。我们对每个分区重复这个过程。

defopen_fs(vol,img,output):print("[+]Recursingthroughfiles..")recursed_data=[]#OpenFSandRecurseifvolisnotNone:forpartinvol:ifpart.len>2048and"Unallocated"notinpart.descand\"Extended"notinpart.descand\"PrimaryTable"notinpart.desc:try:fs=pytsk3.FS_Info(img,offset=part.start*vol.info.block_size)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))root=fs.open_dir(path="/")data=recurse_files(part.addr,fs,root,[],[],[""])recursed_data.append(data)对于第二种情况,我们有一个逻辑图像,卷是None。在这种情况下,我们尝试直接访问文件系统,如果成功,我们将其传递给recurseFiles()方法,并将返回的数据附加到我们的recursed_data列表中。一旦我们有了活动文件列表,我们将其和用户提供的输出文件路径发送到csvWriter()方法。让我们深入了解recurseFiles()方法,这是本教程的核心。

defrecurse_files(part,fs,root_dir,dirs,data,parent):dirs.append(root_dir.info.fs_file.meta.addr)forfs_objectinroot_dir:#Skip".",".."ordirectoryentrieswithoutaname.ifnothasattr(fs_object,"info")or\nothasattr(fs_object.info,"name")or\nothasattr(fs_object.info.name,"name")or\fs_object.info.name.namein[".",".."]:continue如果对象通过了这个测试,我们将使用info.name.name属性提取其名称。接下来,我们使用作为函数输入之一提供的parent变量手动为此对象创建文件路径。对于我们来说,没有内置的方法或属性可以自动执行此操作。

然后,我们检查文件是否是目录,并将f_type变量设置为适当的类型。如果对象是文件,并且具有扩展名,我们将提取它并将其存储在file_ext变量中。如果在尝试提取此数据时遇到AttributeError,我们将继续到下一个对象。

size=fs_object.info.meta.sizecreate=convert_time(fs_object.info.meta.crtime)change=convert_time(fs_object.info.meta.ctime)modify=convert_time(fs_object.info.meta.mtime)data.append(["PARTITION{}".format(part),file_name,file_ext,f_type,create,change,modify,size,file_path])如果对象是一个目录,我们需要递归遍历它,以访问其所有子目录和文件。为此,我们将目录名称附加到parent列表中。然后,我们使用as_directory()方法创建一个目录对象。我们在这里使用inode,这对于所有目的来说都是一个唯一的数字,并检查inode是否已经在dirs列表中。如果是这样,那么我们将不处理这个目录,因为它已经被处理过了。

如果需要处理目录,我们在新的sub_directory上调用recurse_files()方法,并传递当前的dirs、data和parent变量。一旦我们处理了给定的目录,我们就从parent列表中弹出该目录。如果不这样做,将导致错误的文件路径细节,因为除非删除,否则所有以前的目录将继续在路径中被引用。

这个函数的大部分内容都在一个大的try-except块中。我们传递在这个过程中生成的任何IOError异常。一旦我们遍历了所有的子目录,我们将数据列表返回给open_fs()函数。

defconvert_time(ts):ifstr(ts)=="0":return""returndatetime.utcfromtimestamp(ts)有了手头的活动文件列表数据,我们现在准备使用write_csv()方法将其写入CSV文件。如果我们找到了数据(即列表不为空),我们打开输出CSV文件,写入标题,并循环遍历data变量中的每个列表。我们使用csvwriterows()方法将每个嵌套列表结构写入CSV文件。

defwrite_csv(data,output):ifdata==[]:print("[-]Nooutputresultstowrite")sys.exit(3)print("[+]Writingoutputto{}".format(output))withopen(output,"wb")ascsvfile:csv_writer=csv.writer(csvfile)headers=["Partition","File","FileExt","FileType","CreateDate","ModifyDate","ChangeDate","Size","FilePath"]csv_writer.writerow(headers)forresult_listindata:csv_writer.writerows(result_list)以下截图演示了这个示例从取证图像中提取的数据类型:

现在我们可以遍历文件系统,让我们看看如何创建文件对象,就像我们习惯做的那样。在这个示例中,我们创建一个简单的分流脚本,提取与指定文件扩展名匹配的文件,并将它们复制到输出目录,同时保留它们的原始文件路径。

有关构建环境和pytsk3和pyewf的设置详细信息,请参考入门部分中的打开收购食谱。此脚本中使用的所有其他库都包含在Python的标准库中。

在这个示例中,我们将执行以下步骤:

我们导入了许多库来帮助解析参数、创建CSV电子表格,并处理证据容器和文件系统。

from__future__importprint_functionimportargparseimportcsvimportosimportpytsk3importpyewfimportsys这个示例的命令行处理程序接受四个位置参数:EVIDENCE_FILE、TYPE、EXT和OUTPUT_DIR。它们分别是证据文件本身、证据文件类型、要提取的逗号分隔的扩展名列表,以及所需的输出目录。我们还有可选的p开关,用于手动指定分区类型。

if__name__=='__main__':parser=argparse.ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EVIDENCE_FILE",help="Evidencefilepath")parser.add_argument("TYPE",help="TypeofEvidence",choices=("raw","ewf"))parser.add_argument("EXT",help="Comma-delimitedfileextensionstoextract")parser.add_argument("OUTPUT_DIR",help="OutputDirectory")parser.add_argument("-p",help="PartitionType",choices=("DOS","GPT","MAC","SUN"))args=parser.parse_args()在调用main()函数之前,我们创建任何必要的输出目录,并执行我们的标准输入验证步骤。一旦我们验证了输入,我们将提供的参数传递给main()函数。

ifnotos.path.exists(args.OUTPUT_DIR):os.makedirs(args.OUTPUT_DIR)ifos.path.exists(args.EVIDENCE_FILE)and\os.path.isfile(args.EVIDENCE_FILE):main(args.EVIDENCE_FILE,args.TYPE,args.EXT,args.OUTPUT_DIR,args.p)else:print("[-]Suppliedinputfile{}doesnotexistorisnota""file".format(args.EVIDENCE_FILE))sys.exit(1)main()函数、EWFImgInfo类和open_fs()函数在之前的配方中已经涵盖过。请记住,本章采用更迭代的方法来构建我们的配方。有关每个函数和EWFImgInfo类的更详细描述,请参考之前的配方。让我们简要地再次展示这两个函数,以避免逻辑上的跳跃。

在main()函数中,我们检查证据文件是raw文件还是E01文件。然后,我们执行必要的步骤,最终在证据文件上创建一个pytsk3句柄。有了这个句柄,我们尝试访问卷,使用手动提供的分区类型(如果提供)。如果我们能够打开卷,我们将pytsk3句柄和卷传递给open_fs()方法。

defmain(image,img_type,ext,output,part_type):volume=Noneprint("[+]Opening{}".format(image))ifimg_type=="ewf":try:filenames=pyewf.glob(image)exceptIOError:_,e,_=sys.exc_info()print("[-]InvalidEWFformat:\n{}".format(e))sys.exit(2)ewf_handle=pyewf.handle()ewf_handle.open(filenames)#OpenPYTSK3handleonEWFImageimg_info=EWFImgInfo(ewf_handle)else:img_info=pytsk3.Img_Info(image)try:ifpart_typeisnotNone:attr_id=getattr(pytsk3,"TSK_VS_TYPE_"+part_type)volume=pytsk3.Volume_Info(img_info,attr_id)else:volume=pytsk3.Volume_Info(img_info)exceptIOError:_,e,_=sys.exc_info()print("[-]Unabletoreadpartitiontable:\n{}".format(e))open_fs(volume,img_info,ext,output)在open_fs()函数中,我们使用逻辑来支持对文件系统进行逻辑和物理获取。对于逻辑获取,我们可以简单地尝试访问pytsk3句柄上文件系统的根。另一方面,对于物理获取,我们必须迭代每个分区,并尝试访问那些符合特定条件的文件系统。一旦我们访问到文件系统,我们调用recurse_files()方法来迭代文件系统中的所有文件。

defopen_fs(vol,img,ext,output):#OpenFSandRecurseprint("[+]Recursingthroughfilesandwritingfileextensionmatches""tooutputdirectory")ifvolisnotNone:forpartinvol:ifpart.len>2048and"Unallocated"notinpart.desc\and"Extended"notinpart.desc\and"PrimaryTable"notinpart.desc:try:fs=pytsk3.FS_Info(img,offset=part.start*vol.info.block_size)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))root=fs.open_dir(path="/")recurse_files(part.addr,fs,root,[],[""],ext,output)else:try:fs=pytsk3.FS_Info(img)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))root=fs.open_dir(path="/")recurse_files(1,fs,root,[],[""],ext,output)不要浏览了!这个配方的新逻辑包含在recurse_files()方法中。这有点像眨眼就错过的配方。我们已经在之前的配方中做了大部分工作,现在我们基本上可以像处理任何其他Python文件一样处理这些文件。让我们看看这是如何工作的。

诚然,这个函数的第一部分仍然与以前相同,只有一个例外。在函数的第一行,我们使用列表推导来分割用户提供的每个逗号分隔的扩展名,并删除任何空格并将字符串规范化为小写。当我们迭代每个对象时,我们检查对象是目录还是文件。如果是文件,我们将文件的扩展名分离并规范化为小写,并将其存储在file_ext变量中。

iffile_ext.strip()inextensions:print("{}".format(file_path))file_writer(fs_object,file_name,file_ext,file_path,output)iff_type=="DIR":parent.append(fs_object.info.name.name)sub_directory=fs_object.as_directory()inode=fs_object.info.meta.addrifinodenotindirs:recurse_files(part,fs,sub_directory,dirs,parent,ext,output)parent.pop(-1)exceptIOError:passdirs.pop(-1)file_writer()方法依赖于文件对象的read_random()方法来访问文件内容。然而,在这之前,我们首先设置文件的输出路径,将用户提供的输出与扩展名和文件的路径结合起来。然后,如果这些目录不存在,我们就创建这些目录。接下来,我们以"w"模式打开输出文件,现在准备好将文件的内容写入输出文件。在这里使用的read_random()函数接受两个输入:文件中要开始读取的字节偏移量和要读取的字节数。在这种情况下,由于我们想要读取整个文件,我们使用整数0作为第一个参数,文件的大小作为第二个参数。

我们直接将其提供给write()方法,尽管请注意,如果我们要对这个文件进行任何处理,我们可以将其读入变量中,并从那里处理文件。另外,请注意,对于包含大文件的证据容器,将整个文件读入内存的这个过程可能并不理想。在这种情况下,您可能希望分块读取和写入这个文件,而不是一次性全部读取和写入。

deffile_writer(fs_object,name,ext,path,output):output_dir=os.path.join(output,ext,os.path.dirname(path.lstrip("//")))ifnotos.path.exists(output_dir):os.makedirs(output_dir)withopen(os.path.join(output_dir,name),"w")asoutfile:outfile.write(fs_object.read_random(0,fs_object.info.meta.size))当我们运行这个脚本时,我们会看到基于提供的扩展名的响应文件:

此外,我们可以在以下截图中查看这些文件的定义结构:

配方难度:困难

在这个配方中,我们创建了另一个分类脚本,这次专注于识别与提供的哈希值匹配的文件。该脚本接受一个文本文件,其中包含以换行符分隔的MD5、SHA-1或SHA-256哈希,并在证据容器中搜索这些哈希。通过这个配方,我们将能够快速处理证据文件,找到感兴趣的文件,并通过将文件路径打印到控制台来提醒用户。

参考打开获取配方中的入门部分,了解有关build环境和pytsk3和pyewf的设置详细信息。此脚本中使用的所有其他库都包含在Python的标准库中。

我们使用以下方法来实现我们的目标:

我们导入了许多库来帮助解析参数、创建CSV电子表格、对文件进行哈希处理、处理证据容器和文件系统,并创建进度条。

from__future__importprint_functionimportargparseimportcsvimporthashlibimportosimportpytsk3importpyewfimportsysfromtqdmimporttqdm该配方的命令行处理程序接受三个位置参数,EVIDENCE_FILE,TYPE和HASH_LIST,分别表示证据文件,证据文件类型和要搜索的换行分隔哈希列表。与往常一样,用户也可以在必要时使用p开关手动提供分区类型。

if__name__=='__main__':parser=argparse.ArgumentParser(description=__description__,epilog="Developedby{}on{}".format(",".join(__authors__),__date__))parser.add_argument("EVIDENCE_FILE",help="Evidencefilepath")parser.add_argument("TYPE",help="TypeofEvidence",choices=("raw","ewf"))parser.add_argument("HASH_LIST",help="FilepathtoNewline-delimitedlistof""hashes(eitherMD5,SHA1,orSHA-256)")parser.add_argument("-p",help="PartitionType",choices=("DOS","GPT","MAC","SUN"))parser.add_argument("-t",type=int,help="Totalnumberoffiles,fortheprogressbar")args=parser.parse_args()在解析输入后,我们对证据文件和哈希列表进行了典型的输入验证检查。如果通过了这些检查,我们调用main()函数并提供用户提供的输入。

ifos.path.exists(args.EVIDENCE_FILE)and\os.path.isfile(args.EVIDENCE_FILE)and\os.path.exists(args.HASH_LIST)and\os.path.isfile(args.HASH_LIST):main(args.EVIDENCE_FILE,args.TYPE,args.HASH_LIST,args.p,args.t)else:print("[-]Suppliedinputfile{}doesnotexistorisnota""file".format(args.EVIDENCE_FILE))sys.exit(1)与以前的配方一样,main()函数、EWFImgInfo类和open_fs()函数几乎与以前的配方相同。有关这些函数的更详细解释,请参考以前的配方。main()函数的一个新添加是第一行,我们在其中调用read_hashes()方法。该方法读取输入的哈希列表并返回哈希列表和哈希类型(即MD5、SHA-1或SHA-256)。

除此之外,main()函数的执行方式与我们习惯看到的方式相同。首先,它确定正在处理的证据文件的类型,以便在图像上创建一个pytsk3句柄。然后,它使用该句柄并尝试访问图像卷。完成此过程后,变量被发送到open_fs()函数进行进一步处理。

defmain(image,img_type,hashes,part_type,pbar_total=0):hash_list,hash_type=read_hashes(hashes)volume=Noneprint("[+]Opening{}".format(image))ifimg_type=="ewf":try:filenames=pyewf.glob(image)exceptIOError:_,e,_=sys.exc_info()print("[-]InvalidEWFformat:\n{}".format(e))sys.exit(2)ewf_handle=pyewf.handle()ewf_handle.open(filenames)#OpenPYTSK3handleonEWFImageimg_info=EWFImgInfo(ewf_handle)else:img_info=pytsk3.Img_Info(image)try:ifpart_typeisnotNone:attr_id=getattr(pytsk3,"TSK_VS_TYPE_"+part_type)volume=pytsk3.Volume_Info(img_info,attr_id)else:volume=pytsk3.Volume_Info(img_info)exceptIOError:_,e,_=sys.exc_info()print("[-]Unabletoreadpartitiontable:\n{}".format(e))open_fs(volume,img_info,hash_list,hash_type,pbar_total)让我们快速看一下新函数read_hashes()方法。首先,我们将hash_list和hash_type变量实例化为空列表和None对象。接下来,我们打开并遍历输入的哈希列表,并将每个哈希添加到我们的列表中。在这样做时,如果hash_type变量仍然是None,我们检查行的长度作为识别应该使用的哈希算法类型的手段。

在此过程结束时,如果hash_type变量仍然是None,则哈希列表必须由我们不支持的哈希组成,因此在将错误打印到控制台后退出脚本。

defopen_fs(vol,img,hashes,hash_type,pbar_total=0):#OpenFSandRecurseprint("[+]Recursingthroughandhashingfiles")pbar=tqdm(desc="Hashing",unit="files",unit_scale=True,total=pbar_total)ifvolisnotNone:forpartinvol:ifpart.len>2048and"Unallocated"notinpart.descand\"Extended"notinpart.descand\"PrimaryTable"notinpart.desc:try:fs=pytsk3.FS_Info(img,offset=part.start*vol.info.block_size)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))root=fs.open_dir(path="/")recurse_files(part.addr,fs,root,[],[""],hashes,hash_type,pbar)else:try:fs=pytsk3.FS_Info(img)exceptIOError:_,e,_=sys.exc_info()print("[-]UnabletoopenFS:\n{}".format(e))root=fs.open_dir(path="/")recurse_files(1,fs,root,[],[""],hashes,hash_type,pbar)pbar.close()在recurse_files()方法中,我们遍历所有子目录并对每个文件进行哈希处理。我们跳过。和..目录条目,并检查fs_object是否具有正确的属性。如果是,我们构建文件路径以在输出中使用。

defrecurse_files(part,fs,root_dir,dirs,parent,hashes,hash_type,pbar):dirs.append(root_dir.info.fs_file.meta.addr)forfs_objectinroot_dir:#Skip".",".."ordirectoryentrieswithoutaname.ifnothasattr(fs_object,"info")or\nothasattr(fs_object.info,"name")or\nothasattr(fs_object.info.name,"name")or\fs_object.info.name.namein[".",".."]:continuetry:file_path="{}/{}".format("/".join(parent),fs_object.info.name.name)在执行每次迭代时,我们确定哪些对象是文件,哪些是目录。对于发现的每个文件,我们将其发送到hash_file()方法,以及其路径,哈希列表和哈希算法。recurse_files()函数逻辑的其余部分专门设计用于处理目录,并对任何子目录进行递归调用,以确保整个树都被遍历并且不会错过文件。

ifgetattr(fs_object.info.meta,"type",None)==\pytsk3.TSK_FS_META_TYPE_DIR:parent.append(fs_object.info.name.name)sub_directory=fs_object.as_directory()inode=fs_object.info.meta.addr#Thisensuresthatwedon'trecurseintoadirectory#abovethecurrentlevelandthusavoidcircularloops.ifinodenotindirs:recurse_files(part,fs,sub_directory,dirs,parent,hashes,hash_type,pbar)parent.pop(-1)else:hash_file(fs_object,file_path,hashes,hash_type,pbar)exceptIOError:passdirs.pop(-1)hash_file()方法首先检查要创建的哈希算法实例的类型,根据hash_type变量。确定了这一点,并更新了文件大小到进度条,我们使用read_random()方法将文件的数据读入哈希对象。同样,我们通过从第一个字节开始读取并读取整个文件的大小来读取整个文件的内容。我们使用哈希对象上的hexdigest()函数生成文件的哈希,然后检查该哈希是否在我们提供的哈希列表中。如果是,我们通过打印文件路径来提醒用户,使用pbar.write()来防止进度条显示问题,并将名称打印到控制台。

defhash_file(fs_object,path,hashes,hash_type,pbar):ifhash_type=="md5":hash_obj=hashlib.md5()elifhash_type=="sha1":hash_obj=hashlib.sha1()elifhash_type=="sha256":hash_obj=hashlib.sha256()f_size=getattr(fs_object.info.meta,"size",0)pbar.set_postfix(File_Size="{:.2f}MB".format(f_size/1024.0/1024))hash_obj.update(fs_object.read_random(0,f_size))hash_digest=hash_obj.hexdigest()pbar.update()ifhash_digestinhashes:pbar.write("[*]MATCH:{}\n{}".format(path,hash_digest))通过运行脚本,我们可以看到一个漂亮的进度条,显示哈希状态和与提供的哈希列表匹配的文件列表,如下面的屏幕截图所示:

THE END
1.三文鱼咖喱外卖意面披萨炒饭玉米饼自制美食芝士食谱获取食谱 豆腐和蔬菜的帕南咖喱 打开网易新闻 查看精彩图片 在家照着这个简单又美味的食谱做您自己的帕南咖喱,把奶油、甜味和辣味都融在这一道菜里。 获取食谱 通心粉芝士披萨 打开网易新闻 查看精彩图片 用剩余的通心粉和芝士,仅需 20 分钟就能做出这快捷美味的培根通心粉芝士披萨,满足您对美食的渴望。 获取食谱https://m.163.com/dy/article/JIF476A005568E2R.html
2.步骤图电子版西点烘焙配方合集的做法电子版西点烘焙配方合集 电子版西点烘焙配方合集 用料 电子版西点烘焙配方合集的做法步骤http://mip.xiachufang.com/recipe/107294565/
3.东菱面包机电子食谱东菱面包机蛋糕的做法和配方窍门价廉物美~按说明书上的食谱配方,按照食谱上的配料表,细砂糖60克,低筋面粉120克、然后是低筋面粉:1将材料B混合后过筛备用。倒入110ml牛奶,步骤。 烤出的面包外皮才不会又厚又硬,麻烦解答下,将面包桶从面包机中取出,用面包机做蛋糕好像是要材料,反正味道很不错,东菱面包机做蛋糕,夹在正中间,做海绵蛋糕吧,不https://www.haian.com.cn/thread-1277586-1-1.html
4.美的面包机食谱电子版20220520144453.doc美的面包机食谱-电子版.doc 关闭预览 想预览更多内容,点击免费在线预览全文 免费在线预览全文 PAGE 1 快速主食面包快速基本面包配料重量 500g 750g 1000g 水 150ml 200ml 270ml 黄油或植物油 18g 1+1/2 大勺 25g 2大勺 32g 2+3/4 大勺白砂糖 18g 1+1/2 大勺 25g 2大勺 32g 2+3/4 大勺盐 3ghttps://m.book118.com/html/2022/0520/6231131125004150.shtm
5.Linux电子邮件教程(三)linux接收邮件使用Procmail 宏简化电子邮件头部分析 详细分析一些高级配方,包括一些示例配方 到本章结束时,您应该具有一套有用的例程工具箱,用于组合您自己的一套 Procmail 配方,并控制您的邮件。 投递和非投递配方 到目前为止,我们只涵盖了那些要么最终将邮件传递给程序或文件,要么将消息转发给另一个邮件用户的配方。还有另一个选https://blog.csdn.net/wizardforcel/article/details/140489522
6.晶体电子单元PRTS晶体电子单元 用途 昂贵的源石工业产品,用于重要的强化场合。 描述 泰拉源石科技的结晶,泰拉工业现代化的代表。源石施术单元与城际网络服务器的制造都离不开这种科技产品。 加工站1 8 400 1 2 1 需要3级加工站 2星通关S4-10坚守-1解锁配方 副产物 (合计出率 10%) 8.4% 6.3% 6.3% 5.6% 5.6%https://prts.wiki/w/%E6%99%B6%E4%BD%93%E7%94%B5%E5%AD%90%E5%8D%95%E5%85%83
7.分享各类美食菜谱/食谱电子书合集 美食电子书合集 超级实用 190人参与 提交 提交后可查看结果 美味酱汁宝典 168人参与 提交 提交后可查看结果 中国野菜食谱大全 147人参与 提交 提交后可查看结果 中国小吃技术合集资料 152人参与 提交 提交后可查看结果 各种食用油料教学技术配方 143人参与 提交 提交后可查看结果 素食名菜精华系列 149人参与 https://m.douban.com/group/topic/311490871/
8.普及与提高范文11篇(全文)治疗胎动不正常的食谱 原材料:鲈鱼1尾(约600克},北芪25克,红枣8粒,生姜、绍酒、精盐、鲜汤各适量。 1、鲈鱼去鳞、鳃及内脏,洗净,两面剖花刀,汆水,北芪、红枣分别洗净,红枣去核。 2、鲈鱼、北芪、红枣、生姜一起装盘,加入鲜汤、白酒、精盐,封 好保鲜膜,微波炉高火转5分钟或隔水炖1小时,取出趁热食用。营养https://www.99xueshu.com/w/ikey9hrcwkbe.html
9.糖尿病家庭食谱(第2版)电子书下载全书配有主要食谱的实物彩照。《糖尿病家庭食谱(第2版)》集北京协和医院营养部工作经验之精华,所荐食品普通家庭买得到、买得起,食谱配方明了,计算方法简单,可较好地指导糖尿病人及家属熟悉食物交换方法,学会自己安排食谱,享受多彩膳食,达到治病目的。 同类热门电子书下载更多https://topbester.com/ebook/view/160624.html
10.c7官方网站入口注册下载魔卡少女樱回想钥匙食谱研讨配方有哪些 624.71MB查看 魔卡少女樱回想钥匙食谱研讨配方有哪些 418.11MB查看 微软Edge浏览器怎样阅览EPUB文件? edge阅览EPUB电子书办法 878.16MB查看 百度文心一言进口在哪?文心一言官网进口地址共享 326.48MB查看 实施五个月,事例会集三范畴:家长教育促进法处罚了家长,然后呢? 842.98MB查http://www.sdlhaitao.com/
11.图解三命通会1八字神煞书籍pdf下载[15种健脑食物与150道健脑菜].张燕.王作生.扫描版[药膳食疗5shubook.com].pdf [食物相宜相克大百科].佚名.扫描版[药膳食疗5shubook.com].pdf [吃对食物更健康].佚名.扫描版[药膳食疗5shubook.com].pdf 《5分钟懒人食谱 12元搞定三菜一汤》[药膳食疗5shubook.com].pdf 面包配方做法大全[药膳食疗5https://m.5shubook.com/p-4996.html
12.需求与商业模式分析2商业模式类型51CTO博客出版实体和电子食谱,开始推出针对单身男性的食谱 09年已上市,Banner广告收入日本第十,与广告收入第一的Yahoo Japan合作 2019年上半年利润下跌70%(总营收下跌2.4%,同时在推新业务CookpadTV),会员费营收上涨4%,占比达63.7%(广告营收下跌14.1%,其它收入下跌8.3%) https://blog.51cto.com/u_16038001/6159181
13.上海程白电子科技有限公司动物餐厅海德薇信件解锁配方全攻略【最新版】 动物餐厅海德薇可以带回来一些信件,具体是哪些信件需要看海德薇背包里放了哪些东西,下面小编就为大家搜集了最新的动物餐厅海德薇信件解锁配方大全,一起看看吧:相关攻略:客人解锁【最新版】海德薇信件配方【最新 2024-11-26 http://www.gengzu.cn/
14.饥荒电子原件怎么获得饥荒电子原件获得方法电子元件在科技栏,第三个,制作需要两个金子一个石块,注意一些MOD会出现冲突现象,导致电子元件配方消失,不能制作,另外浣熊也可能吐出电子元件。饥荒电子原件怎么获得https://m.3dmgame.com/mip/gl/3787824.html