Vancir 2018-07-24T20:55:57+08:00 vancirprince@gmail.com VulDeePecker-基于深度学习的漏洞检测系统 2018-07-24T00:00:00+08:00 Vancir http://localhost:4000/2018/07/24/vuldeepecker 首先需要找到适用于深度学习的软件程序的表示, 论文还有code gadgets来表示程序并将其转换成向量(code gadgets在向量中是许多行(不一定连续)的, 语义相关的代码). 并据此设计实现了VulDeePecker检测系统. 评估结果表明, 在使用第一类漏洞数据集时在误报率合理的条件下有着更小的漏报率.

论文的主要主要贡献

  • 开启了使用深度学习检测漏洞的先河

初步确定了: 软件程序的表示, 检测粒度, 特定神经网络的选用

  • 设计并实现了VulDeePecker. 并根据以下方面进行评估
    • VulDeePecker可以同时处理多类漏洞吗? -> VulDeePecker使用漏洞模式(vulnerability patterns, 由神经网络训练学得)
    • 人类的专业知识可以帮助提高VulDeePecker表现吗?
    • VulDeePecker同其他漏洞检测方法相比效率如何?
  • 提出了第一个用于评估VulDeePecker和其他漏洞检测系统的数据集

论文结构:

  • 第二节讲述一些初步的原则
  • 第三节讨论VulDeePeckerDSHEJI
  • 第四节描述VulDeePecker的实验评估结果
  • 第五节讨论VulDeePecker的局限以及未来研究的一些问题
  • 第六节讲述前人相关工作
  • 第七节对论文进行总结

第三节 VulDeePecker的设计原理

A. 定义code gadget

(Code gadget)一个code gadget由大量数据或控制上的语义相关的语句组成.

为了生成code gadget我们提出key point的启发式概念, 它是一个”镜头”, 可以从特定角度来表示程序. 直观上看, key point的启发式概念, 从某种程度上说, 就是漏洞的中心, 或者就是说暗示有漏洞存在的代码片段. 对于由库/API调用的不当使用而造成的漏洞, key point就是指这些库/API函数调用. 对于数组不当使用造成的漏洞, key point就是这些数组.

值得注意的是, 一种类型的漏洞, 也许会包含有多种类型的key points. 例如, buffer error漏洞可对应以下key point: 库/API调用, 数组和指针. 一个key point也可以存在于多种类型的漏洞.

论文中仅关注特定的key point - 库/API函数调用. 对应于库/API函数调用, code gadget可以通过程序的数据流/控制流分析进行生成, 这也已经有许多著名的算法[23], [50]以及可用的商业产品如CheckMarx可以用来生成.

B. 概览 VulDeePecker

VulDeePecker有两个阶段: 训练阶段和检测阶段. 训练阶段给的输入是大量的训练程序, (一些是存在一个或多个已知漏洞的, 而另一些则是安全的程序.)

训练阶段包含4个步骤

步骤1 提取库/API函数调用以及对应的程序切片

这也包含两个子步骤

  • 步骤 1.1: 从训练用的程序中提取库/API函数调用. (VulDeePecker目前仅关注与key point相关的漏洞)
  • 步骤 1.2: 从步骤1.1中提取出的库/API函数中, 为每一个参数(或变量)提取一个或多个程序切片. 本论文中, 一个程序切片表示程序的语句(例: 各行代码), 这些语句在是跟库/API函数调用的参数语义相关的. 注意, 程序切片这个概念最初只是为了表示程序中有关程序点或变量的语句而引入的. [55]

步骤2 生成训练用程序的code gadget和标记对应的真实值

  • 步骤 2.1: 汇编步骤1.2获取到的程序切片为code gadgets, 每个code gadget对应一个库/API函数调用. 一个code gadget并一定对应于一系列连续的代码行. 相反, 它包含的是语义相关的多行代码(例如, 继承了编码在那些程序切片中的语义关系)
  • 步骤 2.2: 标记code gadget的真实值. 标记为1表示存在漏洞, 0表示安全. 标记是可行的, 因为我们知道训练用的程序哪些是存在漏洞的哪些不是. 如果存在漏洞, 我们也知道漏洞所在的位置.

步骤3 将code gadget转变为向量表示

  • 步骤 3.1: 转换code gadget为确定的符号表示. 该步骤目的在于保留训练用程序的一些语义信息
  • 步骤 3.2: 对步骤3.1获得的符号表示, 将code gadget编码为向量, 作为训练BLSTM神经网络的输入. (通常情况下想要使用神经网络都得这么干)

步骤4 训练BLSTM神经网络

将code gadget编码为向量并标记好真实值后, 就是标准的训练BLSTM神经网络的过程

检测阶段

给定一个或多个目标程序, 我们提取它的库/API函数调用以及对应的程序切片, 并将程序切片汇编为code gadget. code gadget转化成符号表示再编码为向量输入给训练好的BLSTM神经网络. 神经网络会输出哪些向量, 也就是哪些code gadget是存在漏洞的(“1”)或不存在漏洞(“0”). 如果一个code gadget存在漏洞, 也就能确定下来目标程序中的漏洞位置.

步骤5 转化目标程序为code gadget和向量

  • 步骤 5.1: 从目标程序提取库/API函数调用
  • 步骤 5.2: 根据库/API函数调用的参数提取出程序切片
  • 步骤 5.3: 将程序切片汇编为code gadget
  • 步骤 5.4: 转化code gadget对应的符号表示
  • 步骤 5.5: 将code gadget的符号表示编码为向量

步骤6 检测

使用训练好的BLSTM神经网络对目标程序提取得到的code gadget对应的向量进行分类. 如果分类结果是1, 表明存在漏洞, 标记为0, 表明不存在漏洞.

C 步骤1 提取库/API函数调用和程序切片

步骤 1.1: 提取库/API函数调用

将库/API函数分为两类: 前向和后向. 前向函数调用指那些直接从外部输入(例如, 命令行, 其他程序, socket, 或文件)中接收一个或多个输入的函数调用. 例如 recv函数调用就是一个前向函数调用, 因为它直接从socket中接收数据. 后向函数调用就是那些不从程序运行所在的环境中直接接收任何外部输入的函数调用. 例如strcpy

对于前向函数调用, 受输入参数影响的语句是危险的, 因为它们有可能因为不合适(精心构造)的参数值而产生漏洞. 对与后向函数调用, 能够影响参数值的语句也是危险的, 因为它可能使得函数调用具有漏洞.

步骤 1.2: 提取程序切片

该步骤生成对应于从目标程序中提取到的库/API函数调用的参数的程序切片. 我们定义两类切片: 前向和后向切片

前向切片对应于受参数影响的语句, 而后向切片对应于能够影响参数的语句. 我们使用商业软件CheckMARX的数据依赖图功能来提取这两类切片. 它的基本思路是如下这样:

  • 对于每个在前向库/API函数调用中的参数, 会生成一个或多个的前向切片, 而这些前向切片则对应于与参数相关的切片在库/API函数调用时或调用之后进行分支的情况.
  • 对于每个在后向库/API函数调用中的参数, 会生成一个或多个的后向切片, 而这些后向切片则对应于有多个与参数相关的切片在库/API函数调用时或之前进行合并的情况

注意. 一个程序切片包含有多个属于不同的用户定义的函数里的语句. 也就是说, 切片可以超出用户定义函数的边界. Checkmarx使用chains来表示切片, 但切片是也可以用树进行表示[23], [50], 因为线性结构仅能表示一个独立的切片, 而一个库/API函数调用常常对应于多个切片.

D 步骤2 提取code gadget并标记真实值

步骤 2.1: 将程序切片汇编成code gadget

首先, 给定一个库/API函数调用以及对应的程序切片, 我们可以将属于同一个用户定义消息函数的语句, 根据它们在用户定义函数中的出现次序组合成一个片段里(会对语句去重)

步骤 2.2: 标记真实值

每一个code gadget都需要标记为1(存在训练数据集中已知的漏洞)或0(不存在)

E 步骤3 转化code gadget为向量

步骤 3.1 转化code gadget为它们的符号表示

这一步目的在于启发式地捕获一些程序中的语义信息用于训练神经网络.

首先, 移除非ASCII字符和注释, 因为这些跟漏洞没有关系.

然后, 一对一地映射用户定义的变量到符号名(比如, “VAR1”, “VAR2”), 这里需要注意的是, 当变量在不同的code gadget中出现时, 多个变量可以映射到同一个符号名中.

其次, 一对一地映射用户定义的函数名到符号名(比如: “FUN1”, “FUN2”). 同样, 在不同的code gadget中出现, 多个函数名也可以映射到同一个符号名中.

步骤 3.2: 将符号表示编码为向量

我们将code gadget的符号表示经过词法分析拆分成token序列, 包括标识符, 关键字, 运算符, 和符号.

例如 strcpy(VAR5, VAR2);可以由7个token进行表示

"strcpy", "(", "VAR5", ",", "VAR2", ")", ";"

这样可以得到大量的token. 为了将这些token转换成向量, 我们使用word2vec(广泛用于文本挖掘)工具[14]. 该工具基于词向量(distributed representation), 这是一种将token映射成一个整型数(这个整型数会在随后转换成固定长度的向量)的想法

因为code gadget也许含有不同数量的token, 对应的向量也就会有这不同的长度. 因为BLSTM值接收等长向量作为输入, 因此我们需要作出调整. 为了达到这个目的, 我们引入了一个参数 $\tau$ 作为对应于code gadget的固定长度的向量

  • 当一个向量短于 $\tau$ , 会有两种情况: 如果code gadget由后向切片生成, 或是由多个后向切片组合而成, 那么我们就会在向量的首部填充0, 否则在向量的尾部填充0
  • 当向量长于 $\tau$ , 也有两种情况: 如果code gadget由后向切片生成, 或由多个后向切片组合而成, 我们会删去向量的首部, 否则删除向量的尾部.

这能够确保每一个由后向切片生成的code gadget的最后一个语句是一个库/API函数调用, 而每一个由前向切片生成的code gadget则也是一个库/API函数调用. 结果就是, 每一个code gadget都由一个 $\tau-bits$的向量表示. 向量的长度与BLSTM每一层的隐藏结点数量有关, 这也是一个可以调整的参数来提高漏洞检测的准确率.

]]>
UNVEIL 一种大规模检测勒索软件的自动化方法 2018-06-27T00:00:00+08:00 Vancir http://localhost:4000/2018/06/27/unveil-detect-ransomware 摘要

尽管勒索软件早已不是什么新奇的概念(这种攻击甚至可以追溯到20世纪80年代), 但这种类型的恶意软件最近开始复苏变得流行起来. 事实上, 近几年以来已经报道了数起备受瞩目的勒索软件攻击, 例如对索尼的大规模攻击导致了索尼不得不推迟发布电影«刺杀金正恩».

勒索软件通常会锁定受害者桌面, 使用户无法访问系统, 或是加密, 覆写, 删除用户文件, 以此来勒索钱财. 然而, 虽然现今已经提出了许多通用的恶意软件检测系统, 但没有一个检测系统有尝试专门解决勒索软件的检测问题.

在本论文中, 我们提出了一种名为UNVEIL的新型动态分析系统, 专门用于检测勒索软件. 而分析的关键依据就是, 勒索软件要想成功实行攻击, 就必定会去篡改用户的文件或桌面. UNVEIL会生成一个伪造的用户环境, 并检测勒索软件与用户数据的交互时间, 同时还会跟踪系统桌面上发生的可能是由勒索软件行为造成的变化. 我们的评估结果显示, UNVEIL 显著地提高了现有技术水平, 并能够检测出以往反病毒公司未能检测出的在野勒索软件.

1 绪论

恶意软件目前仍是互联网上最重要的安全威胁之一. 近期, 一种称为勒索软件的特定恶意软件在网络犯罪中变得非常流行. 尽管勒索软件这个概念并不新奇 - 这种攻击早在20世纪80年代末就已经发生过 - 但勒索软件的成功也使得勒索软件近几年衍生出了越来越多的家系[7,20,21,44,46]. 例如, CryptoWall 3.0作为一个高利润的勒索软件家族在世界各地成为头条新闻, 估计造成了3.25亿美元的损失[45], 又如索尼遭受勒索软件攻击事件[27]引发了媒体的大量关注, 美国官方甚至宣称是朝鲜发起了这次攻击.

勒索软件有多种方式运作, 简单的可以锁定受感染计算机的桌面, 复杂一些可以对受害者所有文件进行加密. 与传统恶意软件相比, 勒索软件表现出了一定的行为差异. 例如, 传统恶意软件强调隐蔽性, 以便于在不受怀疑的情况下收集银行证书信息或用户按键记录. 而与之相反, 勒索软件完全不在意隐蔽性, 它攻击的先手就是公开地告知用户你已经被感染了.

现今, 基于行为的恶意软件检测一个重要推动因素就是动态分析技术. 这些系统在一个受控环境中运行捕获的恶意软件样本并记录其行为(例如系统调用, API调用和网络流量). 不幸的是, 专注于行为的恶意软件检测系统(例如, 用于键盘记录的可疑操作系统功能)可能无法检测勒索软件, 因为这类恶意代码和那些使用加密或压缩的良性应用程序活动有相似之处. 此外, 这些检测系统目前还不适合检测勒索软件的具体行为, 从反病毒扫描器对勒索软件家族的错误分类就可以证明这一点[10,39].

在本论文中, 我们提出了一种新的动态分析系统, 旨在分析和检测勒索软件攻击并对其行为进行建模. 在我们的方法中, 系统会创建一个伪造的执行环境, 并监控勒索软件与环境的交互. 密切地监控勒索软件与文件系统的进程交互可以使检测系统精确地表征加密勒索软件的行为.

与此同时, 系统会跟踪程序对计算机桌面的可疑修改, 以表明这是勒索软件样的行为. 我们关键依据就在于勒索软件要想成功, 就必须访问和篡改受害者的文件或桌面. 我们的自动化方法简称为UNVEIL, 该方法允许系统大规模检测许多恶意软件样本, 并可靠地检测并标记那些有着类似勒索软件行为的恶意软件样本. 此外, 该检测系统还能提供对勒索软件运行方式的见解, 并自动区分不同类别的勒索软件.

我们在Windows流行的开源恶意软件分析框架–Cuckoo沙盒[13]上实现了UNVEIL的原型. 系统使用定制的Windows内核驱动来实现, 该驱动可以为文件系统提供监视功能. 此外, 我们还添加了在沙盒外运行的组件, 以监视目标计算机系统的用户界面.

我们在长期的研究中分析了近期在野的148,233个恶意软件样本. 我们的大规模实验结果表明, UNVEIL能够在真实世界的实时数据反馈中正确检测多个家族中的13,637个勒索软件样本. 我们的评估结果还表明, 目前的恶意软件分析系统可能还没有一个准确的行为模型来检测不同类别的勒索软件攻击。

例如, 系统能够正确检测出7,572个还未被传统AV检测出的勒索软件样本, 这些样本都属于现代文件锁定类型的勒索软件家族. UNVEIL还能够发现一种尚未被任何安全公司报告过的新型勒索软件. 这个勒索软件在一些著名的反恶意软件公司提供的现代沙盒中也没有显示出任何恶意行为,而在UNVEIL分析时则显示出了大量的文件加密活动。

从我们方法的高检测率可以表明, UNVEIL可以填充当前恶意软件分析系统快速识别新型在野勒索软件的空白. 只需在分析环境中附加到文件系统驱动程序, UNVEIL就可以轻松地部署在任何恶意软件分析系统上.

总而言之, 本论文做出了如下贡献:

  • 我们提供了一种新型技术用于检测针对受害者计算机上存储文件进行锁定的勒索软件(也称为文件锁定器). 该检测技术基于监控系统范围的文件系统访问信息, 并结合部署的仿真用户环境来判定勒索软件。

  • 我们进行了大规模检测, 评估结果表明我们的方法确实可以有效地检测勒索软件
  • 我们自动检测了近期的148,223个恶意软件样本数据集并验证其中确有13,637个勒索软件. 另外, 我们还发现了一个尚未被报告的勒索软件样本. 评估结果表明, 我们的技术在实践过程中表现良好(达到了96.3%的真正率[TP]和0的假正率[FPs]), 并可用于自动识别提交给分析检测系统的勒索软件样本.

论文的其他部分结构如下. 在第2节中, 我们会简要介绍背景信息并解释不同类别的勒索软件攻击. 在第3节中, 我们描述了UNVEIL的架构并解释我们用于检测多类勒索软件攻击的方法. 在第4节中, 我们提供了有关动态分析环境的更多细节. 第5节会展示我们的评估结果. 第6节将会讨论该方法的局限性. 第7节这介绍相关工作, 最后在第8节总结本篇论文.

2 背景介绍

如图其他类型的恶意软件一般, 勒索软件使用了许多策略以躲避检测, 传播并攻击用户. 例如, 它可以执行多重感染或进程注入, 将用户信息泄露给第三方, 对文件加密以及与C&C服务器建立安全通信. 我们的检测方法中有假定勒索软件样本可以并会使用其他恶意软件样本可能使用的所有技术.

另外, 我们的系统也假定一次成功的勒索软件攻击会进行一项或多项以下活动.

显示持久桌面消息

勒索软件成功感染后, 恶意程序通常会给受害者显示一条信息. 这条勒索消息会通知用户他的电脑已经被锁定, 并提供如何付款以恢复访问权限的说明. 勒索消息可以有很多种方法生成, 比较流行的就是调用专门的API函数(例如, CreateDesktop())来创建一个新桌面, 并将其配置为受害者在受损系统之外的默认界面. 恶意软件作者也可以使用HTML或创建其他持久化窗口来显示勒索消息. 而显示持久桌面消息则是许多勒索软件攻击中的常见操作。

肆意加密或删除用户私人文件

加密式勒索软件攻击会列出并肆意加密其发现的任何私人文件, 通过扣留解密秘钥来限制访问. 加密秘钥可以由受害者主机上的恶意软件本地生成, 也可以在C&C服务器上远端生成后交付给受感染主机. 攻击者可以自己编写破坏函数或Windows API函数来删除用户的原始文件, 也可以使用加密后的文件覆写, 或通过Windows安全删除API进行文件删除.

基于某些属性选择性地加密或删除用户私人文件

为了躲避检测, 大部分的勒索软件样本都会选择性地加密用户私人文件. 最简单的方式就是, 勒索软件样本根据文件访问日期列出相关文件. 更复杂的情形就是, 恶意可以打开应用程序(例如, word.exe), 列出应用程序最近访问的文件. 勒索软件样本可以向任意Windows应用程序注入恶意代码来获得这类信息(例如, 直接读取进程内存).

在本次工作中, 测试的样本已经危害系统, 并能够在用户的文件或桌面上进行任意与勒索相关的操作, 我们在这种情景下展开的研究.

3 UNVEIL 的功能设计

本节将会讲述我们用于检测多类勒索软件攻击的技术. 读者可以参阅第4节了解原型的实现细节.

3.1 检测文件加锁器File Lockers

我们首先阐述为什么我们的系统需要在每个恶意软件运行时创建一个独特的人造用户环境. 随后我们会展示文件系统活动监视器的设计并解释UNVEIL如何使用文件系统监视器的输出来检测勒索软件.

3.1.1 生成人造用户环境

在真实环境中, 保护恶意软件分析环境免受指纹分析技术影响这并不重要. 复杂恶意软件的作者会利用分析系统内的静态特征, 并启动基于侦查的攻击[31]以对公共或私有恶意软件分析系统进行指纹识别. 分析系统的静态环境可视为恶意软件分析系统的阿喀琉斯之踵. 其中一个可以对恶意软件分析系统的有效性产生显著影响的静态特征就是, 那些可以有效用于指纹识别分析环境的用户数据. 也就是说, 即使是在一个比如虚拟化检测等传统技巧都不可行的裸机环境中, 一次不切实的用户环境查看都可能成为代码正在恶意软件分析系统中运行的迹象.

直观来看, 一个解决这样侦查攻击的可能方法就是, 在每次恶意软件运行时建立一个用户数据合法, 真实且不确定的用户环境. 这些自动生成的用户环境是一个”诱人目标”, 可以诱使勒索软件攻击用户数据, 同时也能避免被攻击者识别. 在实践中, 生成用户环境是一个不容忽视的问题, 尤其是在自动化过程中. 这是因为内容生成器不应允许恶意软件作者对分析环境内自动生成的用户内容进行指纹识别, 或是确定内容是否属于真实用户. 我们会在4.1节中详细说明我们是如何在每次勒索软件运行时自动生成一个人造但贴近现实的用户环境.

3.1.2 文件系统活动监视

UNVEIL中的文件系统监视器可以直接访问I/O请求中涉及的数据缓冲区, 从而使系统可以全面地查看所有文件系统的改动. 每一次I/O操作包括有进程名, 时间戳, 操作类型, 文件系统路径, 读/写请求的数据缓冲区指针及对应熵值. I/O请求均在尽可能最低的层中生成. 比方说, 在用户/内核模式中, 有多种方式可以去读, 写以及列举文件, 但所有这些函数最终都会转换为一系列的I/O请求. 无论何时一个用户线程调用I/O请求API, 都会生成一个I/O请求并传递给文件系统驱动. 图1展示的是Windows环境中UNVEIL的上层设计.

figure1.png

图 1: UNVEIL中I/O访问监视器的设计概览. 该模块监视了用户模式下进程的系统范围的文件系统访问, 这使得UNVEIL全面掌握与用户文件的交互信息

UNVEIL的监视器对所有指向用户模式进程生成的文件系统的I/O请求设置了回调函数. 我们注意到, 对于UNVEIL操作, 由于性能原因, 最好仅为每个I/O请求设置一个回调函数, 并且这也能保持对I/O操作完全的可见性. 在UNVEIL中, 用户模式进程与文件系统的交互被形式化为访问模式. 我们来考虑I/O请求记录中的访问模式, 其中访问踪迹(trace) $T$ 是序列 $t_i$ 的集合.

latex1.png

对于我们研究的文件锁定类的所有勒索软件样本, 我们根据经验观察到, 这些样本会产生一些具有独特重复模式的I/O访问记录. 这是因为这些样本都使用了单一的特定策略来拒绝用户文件访问. 在执行攻击时, 每个文件重复的I/O访问模式可以准确地反映出这种攻击策略. 因此, 这些I/O访问模式可以提取为特定勒索软件家族的独特I/O指纹. 要注意的是, 这里我们的方法主要考虑写入和删除请求. 我们将在3.1.2节中阐释为每个文件提取I/O访问模式的过程.

I/O 数据缓冲区熵值

在I/O访问记录中发现的每一个对文件的读写请求, UNVEIL都会计算对应数据缓冲区的熵值. 比较同一个文件的读写请求相应的熵值可以绝妙地衡量加密勒索软件的行为. 则是因为勒索软件的一个共同策略就是读取源文件数据, 加密数据并使用加密后的数据覆写原始数据. 我们的系统使用香农熵进行计算. 在一个特定情形中, 假定数据块 $d$ 中的字节均匀随机分布, 我们有以下公式:

latex2.png

构建访问模式

对每次执行, UNVEIL为勒索软件样本生成I/O访问记录后, 它会根据文件名和请求时间戳对I/O访问请求排序. 这方便系统提取给定的某次运行中每个文件的I/O访问序列, 并检查哪些进程访问了哪些文件. 关键的想法在于, 对每个文件的I/O访问请求进行排序后, 可以轻易地在恶意进程生成I/O请求时观察到重复请求.

系统用来检测勒索软件样本的特定检测标准就是, 在每个恶意软件运行时识别出I/O序列中对应的写入和删除操作.

一次成功的勒索软件攻击, 恶意进程通常意图在攻击期间的某个时刻加密, 覆写或删除用户文件. 在UNVEIL中, 这些I/O请求模式会引发警报, 并被检测为可疑的文件系统活动. 我们研究了不同勒索软件家族中不同文件锁定类的勒索软件样本. 我们的分析结果表明, 尽管这些攻击在其攻击策略(例如, 规避技术, 密钥生成, 密钥管理, 与C&C服务器建立通讯)方面可能大有不同, 但根据其访问请求, 它们主要可以分为三类.

图2显示的是我们在实验中研究的多个勒索软件家族的高级访问模式. 例如, 左侧显示的访问模式表示具有不同密钥长度和桌面锁定技术的Cryptolocker变体. 但其访问模式在家族变体方面并无变化. 我们也观察到, CryptoWall家族中的样本也具有相同的I/O活动. 虽然这些家族成员被识别为两个不同的勒索软件家族, 但由于它们使用相同的加密函数来加密文件(即 Windows CryptoAPI), 因此它们在攻击用户文件时具有相似的I/O模式

figure2

图 2: 勒索软件家族在I/O访问模式上的策略差异. (1) 攻击者使用加密版本覆写用户文件; (2) 攻击者读取, 加密和删除文件, 但不会从存储介质中抹除文件; (3) 攻击者读取, 创建一个新的加密版本, 并通过覆写内容安全抹除原始文件

又如, 在FileCoder家族中, 勒索软件首先创建一个新文件, 从受害者文件中读取数据, 生成原始数据的加密版本, 再将加密数据缓冲区写入新生成的文件, 并简单删除原始用户文件的符号链接(见图2.2). 在这类文件锁定类勒索软件中, 恶意软件不会擦除磁盘上原始文件的数据. 对于这样的攻击手段, 受害者有很大可能恢复数据而无需支付赎金. 然而在第三种方法(图2.3)中, 勒索软件会根据原始文件的数据创建一个新的加密文件, 然后使用标准Windows API或自动以覆盖来安全地删除原始文件的数据(例如, CrypVault家族).


译稿已发表在看雪论坛, 点击链接跳转: 传送门 余下的翻译全文, 请下载译稿PDF文件: UNVEIL 一种大规模检测勒索软件的自动化方法.pdf

]]>
编写YARA规则检测恶意软件 2018-04-14T00:00:00+08:00 Vancir http://localhost:4000/2018/04/14/creat-yara-rules 简介

我们都知道, 黑掉漏洞百出的代码比修补代码有趣得多. 但只会入侵的黑客并不一定能满足雇主的需求. 一些公司就希望安全研究人员能够基于他们收集和发现的恶意软件样本或泄露数据进行补丁.

本文适合人群: 新手和爱好者

阅读本文需要的知识

其实并不需要太多知识要求, 当你对恶意软件分析和逆向工程理解越深, 你就越有独特的方式捕获恶意软件. 不过这并不妨碍你写出惊人的yara规则出来. 我所见过的大部分规则都相当基础. 大部分看上去就像5分钟就能写好的python脚本. 编写规则, yara规则本身十分简单, 真正的技巧和细节都在分析部分 .

  • 熟悉GNU Linux
  • 熟悉C语言语法(不作要求, 但十分有用)
  • 正则表达式 (同上, 不作要求, 但很有用)

声明

我是自学yara规则, 学校并没有教我这些. 我学习yara大约有30个小时, 花费了我一个周末的时间.

大纲

我将介绍以下内容:

  1. 规则标识符
  2. Yara关键字
  3. 字符串
    1. 十六进制值
    2. 文本字符串
    3. 字符串修饰符
    4. 正则表达式
    5. 字符串集
    6. 匿名字符串
  4. 条件
    1. 布尔值
    2. 字符串实例计数
    3. 字符串偏移或虚拟地址
    4. 匹配长度
    5. 文件大小
    6. 可执行程序入口点
    7. 访问指定位置的数据
    8. 对多字符串应用同一条件
    9. 迭代字符串出现次数
  5. 引用其他规则
  6. Yara要点
    1. 全局规则
    2. 私有规则
    3. 规则标签
    4. 元数据
    5. 使用模块
    6. 未定义值
    7. 外部变量/参数值
    8. 文件包含

让我们现在开始吧.


Yara与C语言语法十分相像, 以下是一个简单的规则, 这个规则没有进行任何操作:

rule HelloRule 
{
condition:
false
}

规则标识符

规则标识符是上面简单规则示例中跟在rule后的词, 比如单词”dummy”也可以是一个规则标识符, 标识符命名有如下要求:

  • 是由英文字母或数字组成的字符串
  • 可以使用下划线字符
  • 第一个字符不能是数字
  • 对大小写敏感
  • 不能超出128个字符长度

Yara关键字

下面这些词不能用作规则标识符, 因为这些单词在yara语言里有特定用处

all, and, any, ascii, at, condition, contains entrypoint, false, filesize, fullword, for, global, in import, include, int8, nt16, int32, int8be, int16be int32be, matches, meta, nocase, not, or, of private, rule, strings, them, true, uint8, uint16 uint32, int8be, uint16be, uint32be, wide

通常yara有两部分: 字符串定义条件

rule HelloRule2    // This is an example
{
    strings:
        $my_text_string = "text here"
        $my_hex_string = { E2 34 A1 C8 23 FB }

    condition:
        $my_text_string or $my_hex_string
}

当发现有规则里定义的任意字符串, 规则就会生效. 如你所见, 你还可以在规则里添加注释.

十六进制字符串

通配符

十六进制字符串可以用通配符表示, 通配符符号用”?”表示

rule GambitWildcard
{
    strings:
       $hex_string = { EF 44 ?? D8 A? FB }

    condition:
       $hex_string
}

这个规则可以匹配下面的两个字符串

EF 44 01 D8 AA FB
EF 44 AA D8 AB FB

不定长通配符

不定长的字符串可以用下面这个方法表示

rule MarioJump
{
        strings:
           $hex_string = { F4 23 [4-6] 62 B4 }

        condition:
           $hex_string
}

这个规则可以匹配下面的两个字符串

F4 23 01 02 03 04 62 B4
F4 23 AA BB CC DD EE FF 62 B4

当然无限长的字符串也是可以的.

rule BuzzLightyear
{
        strings:
           $hex_string = { F4 23 [-] 62 B4 }

        condition:
           $hex_string
}

这个规则可以匹配下面的两个字符串

F4 23 AA FF 62 B4
F4 23 AA AA AA AA AA...FF FF 62 B4

有条件的字符串

你可以创建一个字符串应对多种情况

rule WorriedRabbit
{
    strings:
       $hex_string = { BA 21 ( DA BC | C6 ) A5 }

    condition:
       $hex_string
}

这个规则可以匹配下面的两个字符串

BA 21 DA BC A5
BA 21 C6 A5

混合

当然, 你也可以将上面这几种方法结合起来.

rule WorriedGabmitLightyearJump
{
    strings:
       $hex_string = { BA ?? ( DA [2-4] | C6 ) A5 }

    condition:
       $hex_string
}

这个规则可以匹配下面的三个字符串

BA 01 DA 01 02 03 04 A5
BA AA C6 A5
BA FF DA 01 02 A5

文本字符串

除开使用十六进制字符串, 我们也还可以使用文本字符串

rule KimPossible
{
    strings:
        $alert_string = "Whats the Sitch"

    condition:
       $alert_string
}

你也可以像C语言那样使用如下的转义符:

\" 双引号
\\ 反斜杠
\t 水平制表符
\n 换行符
\xdd 以十六进制表示的任何字节

修饰符

不区分大小写的字符串

Yara默认对大小写敏感, 但你可以使用修饰符将其关闭

rule ThickSkin
{
    strings:
        $strong_string = "Iron" nocase

    condition:
        $strong_string
}

宽字符串

wide修饰符可以用来搜寻以2字节表示1字符这种方式编码的字符串, 这种宽字符串在许多二进制文件中都有出现. 如果字符串”FatTony”以2字节表示1字符的方式编码并在二进制文件中出现, 我们就可以使用wide修饰符将其捕获. 因为”FatTony”也可能是”fattony”, 我们也可以添加nocase修饰符以免错过.

rule FatTony
{
    strings:
        $fat_villain = "FatTony" wide nocase

    condition:
        $fat_villain
}

[!]重要提示: 请记住, 该修饰符只是将字符串中字符的ASCII码和\x00交错起来组成宽字符, 它并不支持包含非英文字符的UTF-16字符串. 要想对既有ASCII字符和宽字符的字符串进行搜索, 请使用如下命令:

rule ASCIIFatTony
{
    strings:
        $fat_villain = "FatTony" wide ascii nocase

    condition:
        $fat_villain
}

字符串默认是ASCII编码, 所以如果你想单独用ascii搜索”FatTony”, 你并不需要添加ascii修饰符

rule ASCIIFatTony
{
    strings:
        $fat_villain = "FatTony"

    condition:
        $fat_villain
}

如果你想在不使用widenocase修饰符的情况下进行搜索, 上述这个规则可以生效.

Fullwords修饰符

该修饰符可用于匹配那些前后没有附加其他字符的单词(全词匹配).

rule ShadyDomain
{
    strings:
        $shady_domain = "faceebook" fullword

    condition:
       $shady_domain
}

这个规则可以匹配下面的三个字符串

www.faceebook.com
www.myportal.faceebook.com
https://secure.faceebook.com

但这个规则不能匹配以下的字符串:

www.myfaceebook.com
thefaceebook.com

两者区别在于匹配的全词前后可以附加特殊字符, 不能是普通字符.

正则表达式

yara允许使用正则表达式, 不过要用正斜杠而非双引号括起来使用(像Perl编程那样)

rule RegularShow
{
    strings:
        $re1 = /md5: [0-9a-fA-F]{32}/
        $re2 = /state: (on|off)/

    condition:
        $re1 and $re2
}

该规则将捕获任何状态下找到的所有md5字符串.

你也可以在正则表达式中使用文本修饰符, 如nocase,ascii,widefullword.

元字符

元字符是一个字符对计算机程序有特定含义(而非字面含义)的字符. 在正则表达式中, 有以下含义:

** 引用下一个元字符
^ 匹配文件的开头
$ 匹配文件的末尾
| 多选
() 分组
[] 方括号字符类

也可以使用以下量词:

* 匹配0次或多次
+ 匹配1次或多次
? 匹配0次或1次
{n} 只匹配n次
{n, } 至少匹配n次
{ ,m} 至多匹配m次
{n,m} 匹配n到m次

也可以使用以下的转义符:

\t 水平制表符 (HT, TAB)
\n 换行符 (LF, NL)
\r 回车符 (CR)
\f 换页符 (FF)
\a 响铃
\xNN 十六进制代码为NN的字符

也可以使用以下字符类:

\w 匹配单词字符 (单词可由字母数字加"_"组成)
\W 匹配非单词字符
\s 匹配空白符
\S 匹配非空白字符
\d 匹配一个十进制数字字符
\D 匹配一个非数字字符
\b 匹配单词边界
\B 匹配非单词边界

字符串集

如果你想要中列表中选择一定数量的字符串, 你可以执行以下操作:

rule MigosPresent
{
    strings:
        $m1 = "Quavo"
        $m2 = "Offset"
        $m3 = "Takeoff"

    condition:
        2 of ($m1,$m2,$m3)
}

如果$m1, $m2$m3任意存在两个, 那么就满足上述规则中的条件.

你还可以使用通配符来表示一个字符集. 像如下这样使用通配符*

rule MigosPresent
{
    strings:
        $m1 = "Quavo"
        $m2 = "Offset"
        $m3 = "Takeoff"

    condition:
        2 of ($m*)
}

要表示strings中的所有变量, 你可以使用关键字them

rule ThreeRappersPresent
{
    strings:
        $m1 = "Quavo"
        $m2 = "Offset"
        $m3 = "Takeoff"
        $q1 = "Cardi B"

    condition:
        3 of them // equivalent to 3 of ($*)
}

你可以使用任何返回数值的表达式. 以下是使用关键字anyall的一个示例

rule Squad
{
    strings:
        $m1 = "Quavo"
        $m2 = "Offset"
        $m3 = "Takeoff"
        $q1 = "Cardi B"

    condition:
        3 of them // equivalent to 3 of ($*)
        all of them
        any of ($*) and 2 of ($*)    // Fancy way of using any in a rule that requires 3.
}

带有of和for…of的匿名字符串

如果你没有专门引用字符串的事件, 你可以仅使用$来将它们全部引用.

rule AnonymousStrings
{
    strings:
        $ = "dummy1"
        $ = "dummy2"

    condition:
        1 of them
}

条件

Yara允许通过and, or, 和not等相关运算符来表示布尔表达式, 算术运算符(+,-,*,%)和位运算符(&, , «, », ~, ^)也可用于数值表达式中.

布尔运算

字符串标识符也可在条件中充当布尔变量, 其值取决于文件中相关字符串是否存在.

rule Example
{
    strings:
        $hero1a = "Batman"
        $hero1b = "Robin"
        $hero2a = "Edward"
        $hero2b = "Alphonse"

    condition:
        ($hero1a or $hero1b) and ($hero2a or $hero2b)
}

计数字符串实例

有时我们不仅需要知道某个字符串是否存在, 还需要知道字符串在文件或进程内存中出现的次数. 每个字符串的出现次数由一个变量表示, 变量名是用#代替$的字符串标识符. 例如:

rule Ransomware
{
    strings:
        $a = "encrypted"
        $b = "btc"

    condition:
        #a == 2 and #b > 2
}

这个规则会匹配任何包含两个字符串$a以及出现至少两次字符串$b的文件或进程.

字符串偏移(虚拟地址)

在大多数情况下, 当在条件中使用字符串标识符, 我们都只需知道关联的字符串是否在文件或进程内存内就行了. 但有时我们还是需要知道该字符串是否在文件的某个特定偏移处, 或是在进程地址空间的某个虚拟地址处. 在这种情况下, 我们就需要操作符at.

rule Offset
{
    strings:
        $a = "encrypted"
        $b = "btc"

    condition:
        $a at 100 and $b at 200
}

如果在文件的偏移100处(或者在一个正在运行的进程中, 位于虚拟地址100位置)发现了字符串$a, 我们的规则就能捕获到该字符串. 当然字符串$b也要在偏移200位置上才行. 你也可以使用十六进制表示而不一定要十进制.

rule Offset
{
    strings:
        $a = "encrypted"
        $b = "btc"

    condition:
        $a at 0x64 and $b at 0xC8
}

at操作符指定到一个具体的偏移量, 而你可以使用操作符in来指定字符串的位置范围.

rule InExample
{
    strings:
        $a = "encrypted"
        $b = "btc"

    condition:
        $a in (0..100) and $b in (100..filesize)
}

字符串$a必须在偏移0-100之间才能找到, 而$b则必须是在偏移100到文件末尾位置(才能找到).

你也可以使用@a[i]来取得字符串$ai个字符的偏移量或虚拟地址. 字符串索引以1开头 , 故第1个字符是@a[1], 第2个是@[a2]并依此类推, 而不是以@a[0]开始. 如果你提供的索引值大过字符串总共出现的次数. 那结果就将是值NaN(Not a Number, 非数字).

匹配长度

对于包含跳转的许多正则表达式和十六进制字符串, 匹配长度用一个变量表示. 如果你有一个正则表达式/fo*/, 可以匹配字符串fo, foofooo, 那么各个的匹配长度都是不同的.

在字符串标识符前加一个!得到匹配长度, 你就可以将匹配长度作为你条件的一部分. 跟你获取偏移时使用字符@类似, !a[1]是第一个匹配到的字符串$a的长度, 而!a[2]就是第二个匹配到的字符串的长度, 依此类推. !a!a[1]的缩写.

rule Hak5
{
    strings:
        $re1 = /hack*/    // Will catch on hacker, hacked, hack, hack*

    condition:
        !re1[1] == 4 and !re1[2] > 6
}

该规则可以匹配如下字符串:

We hack things. We are hackers.

第一个hackre1[1]且其长度等于4. 第二个hack长度则至少为6

文件大小

字符串标识符并不是唯一可以在条件中出现的变量(实际上, 可以不定义任何字符串来编写一个规则), 还可以使用其他变量. filesize就保存着正在扫描的文件的大小. 大小以字节为单位.

rule FileSizeExample
{
    condition:
       filesize > 200KB
}

我们可以使用后缀KB将文件大小设置为200KB, 它会自动将常量的值乘上1024, 后缀MB会可以将值乘以2^20. 这两个后缀都只能用于十进制常量

[!]重要提示: filesize仅在规则应用于文件的时候生效. 如果应用于正在运行的进程, 那么它会永远都匹配不了.

可执行程序入口点

如果我们正扫描的文件是一个PE或ELF文件, 那么变量entry_point会存有可执行文件的入口点偏移值. 而如果我们正扫描一个运行的进程, 那么entry_point会存有可执行文件入口点的虚拟地址. 变量entry_point的经典用法是用于搜索入口点的一些pattern, 以检测壳或简单的感染病毒. 目前使用entry_point的方式是通过导入PE和/或ELF的库并使用它们各自的功能. Yara的entrypoint函数自第3版开始就已经过时了. 以下是它在第3版之前的样子.

rule EntryPointExample1
{
    strings:
        $a = { E8 00 00 00 00 }

    condition:
       $a at entrypoint
}

rule EntryPointExample2
{
    strings:
        $a = { 9C 50 66 A1 ?? ?? ?? 00 66 A9 ?? ?? 58 0F 85 }

    condition:
       $a in (entrypoint..entrypoint + 10)
}

[!]重要提示: 再次强调, 不要使用yara的entrypoint, 请在导入PE或ELF文件后使用对应的pe.entry_pointelf.entry_point

访问指定位置的数据

如果你想从特定偏移位置读取数据, 并将其存为一个变量. 那么你可以使用以下任何一个方式:

int8(<offset or virtual address>)
int16(<offset or virtual address>)
int32(<offset or virtual address>)

uint8(<offset or virtual address>)
uint16(<offset or virtual address>)
uint32(<offset or virtual address>)

int8be(<offset or virtual address>)
int16be(<offset or virtual address>)
int32be(<offset or virtual address>)

uint8be(<offset or virtual address>)
uint16be(<offset or virtual address>)
uint32be(<offset or virtual address>)

数据存储默认以小端序, 如果你想要读取大端序的整形数, 请使用下面几个以be结尾的对应函数.

参数<offset or virtual address>可以是任何一个返回无符号整数的表达式, 包括可以是uintXX函数的返回值.

rule IsPE
{
  condition:
     // MZ signature at offset 0 and ...
     uint16(0) == 0x5A4D and
     // ... PE signature at offset stored in MZ header at 0x3C
     uint32(uint32(0x3C)) == 0x00004550
}

for…of: 对许多字符串应用同一个条件

要用for循环来检查一组字符串是否满足特定条件, 请使用如下语法:

for num of string_set : ( boolean_expression )

对每个string_set的字符串, 都会计算boolean_expression的值, 并且这些值必须至少有1个为真.

当然你也可以使用其他关键字, 如allany代替num来使用.

for any of ($a,$b,$c) : ( $ at elf.entry_point  )

$表示集合中的所有字符串. 本例中, 它是字符串$a, $b$c.

你也可以使用符号#@来引用每一个字符串的出现次数和首字符偏移量.

for all of them : ( # > 3 )
for all of ($a*) : ( @ > @b )

迭代字符串出现次数

如果你想对偏移迭代并测试条件. 你可以如下操作:

rule Three_Peat
{
    strings:
        $a = "dummy1"
        $b = "dummy2"

    condition:
        for all i in (1,2,3) : ( @a[i] + 10 == @b[i] )
}

这个规则说的是, $b出现前三个的字符串应当分别隔$a出现的前三个的字符串10个字节远. 另外一种写法如下:

for all i in (1..3) : ( @a[i] + 10 == @b[i] )

我们也可以使用表达式. 在本例中, 我们迭代每一次出现的$a(记住, #a代表$a的出现次数). 该规则指定, 每一次$a都应当出现在文件的前100个字节内.

for all i in (1..#a) : ( @a[i] < 100 )

你也可以指定字符串的某一次出现需要满足条件(而非全部).

for any i in (1..#a) : ( @a[i] < 100 )
for 2 i in (1..#a) : ( @a[i] < 100 )

引用其他规则

就像C语言中引用函数那样. 函数, 或是这里说的规则, 都必须在使用前进行定义.

rule Rule1
{
    strings:
        $a = "dummy1"

    condition:
        $a
}

rule Rule2
{
    strings:
        $a = "dummy2"

    condition:
        $a and Rule1
}

Yara要点

全局规则

Yara允许用户在所有规则中进行约束. 如果你希望所有规则都忽略掉那些超出特定大小限制的文件, 那么你可以对规则进行必要的修改, 或是编写一条像以下这样的全局规则:

global rule SizeLimit
{
    condition:
        filesize < 2MB
}

你可以根据需要定义各种全局规则. 这些规则会在其他规则之前运行.

私有规则

私有规则在匹配时没有任何输出. 当和其它规则成对引用时, 这样就可以使输出更为清楚. 比如为了判断文件是否恶意, 有这样一条私有规则, 要求文件必须是ELF文件. 一旦满足这个要求, 随后就会执行下一条规则. 但我们在输出里想看的并不是该文件它是不是ELF, 我们只想知道文件是否恶意, 那么私有规则就派上用场了. 要想创建一条私有规则, 只需要在rule前添加一个private即可.

private rule PrivateRule
{
    ...
}

规则标签

如果你只想查看ruleName类型的规则输出, 你可以对你的规则打上标签

rule TagsExample1 : Foo Bar Baz
{
    ...
}

rule TagsExample2 : Bar
{
    ...
}

元数据

Yara允许在规则中存储一些额外数据.

rule MetadataExample
{
    meta:
        my_identifier_1 = "Some string data"
        my_identifier_2 = 24
        my_identifier_3 = true

    strings:
        $my_text_string = "text here"
        $my_hex_string = { E2 34 A1 C8 23 FB }

    condition:
        $my_text_string or $my_hex_string
}

使用模块

一些模块由YARA官方发布, 比如PECukoo模块. 这些模块就如python那样导入即可, 不过在导入时模块名需要添加双引号

import "pe"
import "cuckoo"

一旦模块成功导入, 你就可以在函数前加模块名, 来使用这些功能.

pe.entry_point == 0x1000
cuckoo.http_request(/someregexp/)

未定义的值

一些值在运行时保留为undefined. 如果以下规则在ELF文件上执行并找到对应的字符串, 那么它的结果相当于TRUE & Undefined.

import "pe"

rule Test
{
  strings:
      $a = "some string"

  condition:
      $a and pe.entry_point == 0x1000
}

所以在用的时候要注意咯!

外部变量

外部变量允许你定义一些, 依赖于第三方提供值的规则.

rule ExternalVariable1
{
    condition:
       ext_var == 10
}

ext_var是一个外部变量, 它在运行时会分配有一个值, (见命令行的-d选项以及yara-python中compilematch方法的参数). 外部变量可以是int, strboolean类型

外部变量可以和操作符containsmatches一起使用. contains在字符串包含特定子串的情况下返回true. 而matches在字符串匹配给定的正则表达式时返回true.

rule ExternalVariable2
{
    condition:
        string_ext_var contains "text"
}

rule ExternalVariable3
{
    condition:
        string_ext_var matches /[a-z]+/
}

你也可以将matches操作符和正则表达式一起使用

rule ExternalVariableExample5
{
    condition:
        /* case insensitive single-line mode */
        string_ext_var matches /[a-z]+/is
}

/[a-z]+/is中的i表示匹配时不区分大小写. s表示是在单行(single line)模式

记住, 你必须在运行时定义好所有的外部变量. 你可以使用-d参数来指定.

文件包含

当然在yara里你可以使用类似C语言的导入方式(#include, 不过yara里并不使用#, 并且包含的文件需要加双引号)来包含其他文件. 你可以在包含时使用相对路径, 绝对路径. 如果是windows系统, 还可以是驱动设备的路径.

include "Migos.yar"
include "../CardiB.yar"
include "/home/user/yara/IsRapper.yar"
include "c:\\yara\\includes\\oldRappers.yar"
include "c://yara/includes/oldRappers.yar"

总结

好吧. 现在你应该知道如何写一些Yara规则了. 这里有一些恶意软件的仓库, 规则和工具, 可以让你来生成yara规则. 如果你安装了yarGem, 你只需要将它指向到恶意软件, 它就会为该恶意软件生成一个签名. 如果你想捕捉一个恶意软件家族, 你最好是将规则推广到整个家族去.

资源:

Yara:

xxd:

比较命令

awk ‘FNR==NR{a[$1];next}($1 in a){print}’ malcourse.strings zoo.conficker.strings > same-strings

恶意软件仓库

]]>
模糊测试-初学者入门指南 2018-04-07T00:00:00+08:00 Vancir http://localhost:4000/2018/04/07/fuzz-testing-beginners-guide 模糊测试简介

模糊测试(Fuzzing), 简而言之, 就是为了触发新的或不可预见的代码执行路径或bug而在程序中插入异常的, 非预期的, 甚至是随机的输入. 因为模糊测试涉及到为目标提供大量的测试样例, 因此至少也会实现部分自动化. 模糊测试可以也应当用于测试每个需要接受某种形式输入的接口. 实际上, 模糊测试最起码就应该拿来用于测试每个从潜在恶意来源(比如互联网或用户提供的文件)获取输入的接口.

模糊测试是对其他测试技术的补充. 由模糊测试揭露出的问题往往是开发人员不太可能构建的输入(例如, 在处理一些边界情况, 数据正确性和错误处理例程时的输入)触发的. 在常规自动化测试过程中, 模糊测试扩大了代码覆盖范围, 提高了代码覆盖率测试程度. 通过模糊测试使用的非预期输入通常会触发一些平时不会触发的执行流.

很多地方都需要进行模糊测试. 它是你系统开发生命周期(SDLC)的一部分, 在这部分里, 你需要确保你完成了改善目标所要的系统性工作, 或是你只是想解决一些bug也行. 要如何费心于模糊测试取决于你的最终目标和相关资源, 本文只是帮你如何从模糊测试中获取更多的回报.

开始之前

也许到现在你已经跃跃欲试了. 很多组织和个体经常急于根据博客文章中的思路或会议上看到的酷炫演示来进行一次模糊测试, 这虽不一定是坏事, 但我们经常可以看到在模糊测试系统的背后有着大量的工作投入, 这些模糊测试系统仅在作者分配去完成其他任务之前有在使用, 稍加改动就会破坏兼容性. 更糟糕的是这些模糊测试系统长年消耗硬件资源, 却经常没能得出什么结果来. 就如同软件开发项目的其他任何部分一样, 测试自动化和模糊测试与否, 都需要一定的规划, 维护和提交.

你想拿fuzz干什么?

这是一个简单的问题, 但是答案却并不一定如你所想那样显而易见. 如果你已经有了一个目标, 那很不错. 如果没有, 那你就得去找一个接受输入的接口. 接口可以是对外的, 像是网络连接, 可以是一些文件. 当然也完全可以是对内的, 像是一个你代码正在使用的实用程序库(utility)里的函数调用约定. 模糊测试就是为你所选择的接口创建输入, 并观察这些接口如何处理这些极端的输入. 你可以通过威胁建模(Threat modelling)和回执数据流图来发现目标所拥有的潜在接口.

在每个接口背后可以有许多软件层, 选择对哪个层进行模糊测试就显得至关重要, 因为输入要到达那个层, 就需要通过前面各层的所有检查.

举个例子, 我们来看一个接收带签名二进制数据的HTTP服务器. 我们有一个含JSON字符串的二进制数据, 字符串里是我们应用程序要用到的值. 在这个例子里, 我们就有4个潜在的层需要进行模糊测试:

  1. 服务器接收的HTTP消息
  2. 二进制数据的签名校验
  3. JSON字符串解析
  4. 我们处理实际值的代码

暂且假定我们的HTTP, 签名和JSON库都是鲁棒的(我们并不想以这些库为目标). 要对我们自己的代码进行模糊测试, 我们就需要生成这些实际值, 然后将这些实际值包装为JSON字符串, 对二进制数据签名, 创建一个HTTP消息并将其发送给目标. 除非我们已经有了可以复用的自动化测试代码, 否则单独构建这些测试样例需要相当长的时间. 在堆栈中进行模糊测试也会不断带来开销, 并且在更改某些层时也更容易被破坏.

在模糊测试中, 测试样例的吞吐量也相当关键. 你应当考虑下目标是否有一些可禁用或绕过的功能, 以减少开销并扩大模糊测试覆盖范围. 通常我们实现一个直接使用模糊值调用目标代码的小程序可以带来不少好处. 在上面这个例子里, 写一个直接将值传递给我们处理代码的程序, 就可以绕过发送网络消息, 好几次哈希计算, 加密检查, JSON转字符串以及解析这些步骤. 在一些优化更好的模糊测试环境里, 诸如不必要的日志记录, CRC校验, 文件I/O以及远程资源调用等功能都会在一个更适合模糊测试的模式(“fuzz-friendly mode”)下禁用. 我们可以用一些ifdef, 创建虚拟(Mock)函数或其他仅用于构建模糊测试的配置来实现一个对模糊测试友好的模式(“fuzz-friendly mode”). 当然, 当你在进行一些会改变目标行为的模糊测试优化时, 你必须能确保这些修改不会创建或隐含任何的bug.

不过, 在刚开始时, 不要太担心想着要一个高效的每秒将数千个测试用例注入进优化的模糊测试环境中去的策略. 开始模糊测试的一个非常有效的办法就是将随机(或位翻转)的数据发送到你找到的任何接口去. 如果这能很快地找到问题, 那么你就算是找到了你第一个目标接口了!

你想找寻的是什么?

很多时候当你进行模糊测试, 目标可能会崩溃, 这是很难避免的. 然而, 为了能充分利用你的劳动, 你就还需要找到其他的错误情况. 目标都有它自己的功能需求, 需求里定义了程序应该干什么, 你可以从跟这个点找到它不应该做的事情. 除此之外, 所有程序都可能存在逻辑缺陷, 可能导致内存泄露或CPU及内存消耗过多等问题. 根据底层技术, 目标也可能容易发生内存腐败, 命令注入或其他应当注意的问题类别.

起初, 所有可能的潜在问题类型及其影响都应该记录下来. 现有的检测工具和技术可以适用于不同的问题类型, 但有些检测工具和技术使用起来相当复杂, 或是执行开销高昂. 影响评估有助于你判断使用工具或某技术是否值得. 例如, 图像压缩中颜色值的错误计算可能影响很小, 但却难以检测. 如果你只是想找到这些问题, 那么一些能使用模糊的和无效的身份绕过验证的地方十分致命, 也相当容易被检测到.

在研究不同的工具和技术时, 还要考虑其他的自动化测试方法. 例如, 在很多情况下, 你会发现你的单元测试(unit test)一次又一次触发了一些错误, 但你可能因为没有用到单元测试而无法发觉.

如何进行模糊测试?

模糊测试是一项一人一机器就能执行的技术. 中等规模的模糊测试可以作为持续集成(CI)系统的一部分来执行, 针对不同的项目每天运行几次模糊测试. 大规模的模糊测试可以通过使用数百上千台机器在云端并行自动地模糊测试. 所有这些环境都有着最终系统必须满足的不同需求. 因为最初基本不会考虑到与另一些部件的可用性, 所以通常情况下不会使用大型fuzzer

和所有的测试相同, 测试规模越大, 自动化就越重要. 使用单个实例来fuzz你的程序非常简单. 你可用不断地将模糊输入注入到目标程序中, 直到触发bug, 然后修复bug, 如此不断重复即可. 但当你同时处理成百上千个实例时, 你就会知道为什么重复筛选等功能相当重要了. 在一个在CI中针对不同构建版本并行运行模糊测试的大型组织中, 你也可能会忽视自动问题报告, 最小化测试用例和补丁验证这些需要注意的问题.

准备开始

在这里, 你应该对模糊测试所需的三个部件建立一个粗略的需求规格说明: 测试用例生成方案, 测试用例注入方法和装置. 现在你终于可以开始真正的工作了.

互联网里有许多开源或商业性质的模糊测试解答方案. 有些仅仅实现了测试用例生成, 有些则结合了测试用例生成和注入, 还有一些则具备完整的含有装置和自动化的堆栈. 一般而言, 商业产品可用性更强, 并且通常可以为大部分测试用例提供完整的解答方案. 特别是对于希望快速开始对多个产品进行模糊测试的组织而言, 商业解决方案是真正的选择. 而对于因为乐趣和利润, 想解决bug的个人, 商业解决方案通常会超出预算.

无论你是决定使用已有的解决方案或是自己实现一个, 都总会遇上一些问题.

Fuzzer灵活性

特别当你的最终目标是能对多个不同目标使用同一个工具解决时, 务必要确保你要使用的解决方案足够灵活, 以涵盖所有的样例. 如果整个系统必须进行重构, 或者最坏可能需要构建另一个系统, 那么对目标模糊测试会浪费大量的时间. 不同的工具也会揭示出不同的问题, 以长远眼光来看, 总会有新的工具再次揭露出新的问题. 所以组件切换, 特别是装置切换, 是一个十分有价值的功能.

处理结果

如果你正在建立一个模糊测试系统, 但你并不是实际发现问题解决问题的开发人员之一, 那么请联系那些正准备处理你系统得出结果的人员. 他们想从模糊测试的的bug报告中得到什么呀的信息呢? 如果开发人员每天早上在收件箱里一眼看到满满的像下面这样的bug报告, 那他们是真的很少意识到自己想要的信息:

标题: 程序X出现了崩溃

描述: 附件里的数据使得程序X崩溃了

附件: fuzz-test-case-1337 (22MB)

默认情况下, 错误报告至少应该包含重现问题的全部信息. 例如这样但不限于这样: 配置信息, 使用的操作系统, 目标的版本或构建版本号, CPU和内存信息, 以及适用的编译器选项和调试标志.

模糊测试中使用的设置对于开发人员来说应该要很容易重现才行, 并且你应该为每次对目标模糊测试所做的优化进行解释. 举个例子, 开发人员可能并不想解决只有在CRC校验被关闭的情形下才能重现的问题, 除非你能解释清楚, 当启用CRC校验时如何构造输入也能重现bug.

自动化模糊测试还包括有: 相似问题分类, 测试用例最小化, 回归范围查找, 修复验证, 甚至可以提供像容器, 虚拟机, 映像一样配好的测试环境.

跟踪进展

在长时间运行模糊测试之后, 你可能没有发现任何新错误, 这表明可能是如下两种情况之一:

  1. 你的模糊测试工作非常出色, 目标的鲁棒性正在提高.
  2. 或者你的模糊测试卡住了, 一次又一次地重复相同的代码路径.

正如模糊测试简介那一节所述, 模糊测试需要不断的维护和提交, 以保持长时间有效. 你可以使用一些技术来帮助你确保你的模糊测试始终保持有效, 并且还能对目标发生的一些变化进行测试.

如果你正在使用基于代码覆盖率的fuzzer, 你也许已经覆盖到了该覆盖的范围. 只要你的代码覆盖率在模糊测试过程中持续上升, 就无需担心. 但如果你的代码覆盖率不再上升了, 那你可能就遇到了一些需要更加深入分析的问题.

你不能仅根据现已覆盖的代码行数获知多少信息. 例如, 目标可能有的代码行, 没有特别配置是无法执行的. 或是可能存在无法到达的代码, 使得无法完全覆盖等等情况. 使用工具来显示运行测试样例时, 哪些部分的代码有执行, 哪些没有执行是相当有帮助的. 将这次的结果与之前fuzzer跑出的结果, 或其他自动化测试的结果进行比对, 并检查之前出现bug的位置, 可以帮助确保你的模糊测试效果不会倒退, 并且依旧涵盖着所有相关的代码路径. 如果还是少了一些未经过的代码路径, 那么下一步就是分析如何让你的测试用例生成器生成能触发这些路径的测试数据了. 特别是对于那些基于模型的fuzzer, 你经常会发现说fuzzer并没有拿到模型实现所需要的信息或字段. 变异测试的模糊器缺少代码覆盖率的原因通常都是因为初始样本文件覆盖率过低, 或是对输入的验证过于严格所导致的. 对于后一种情况, 请考虑”fuzz-friendly mode”

没有代码覆盖反馈, 事情就会变得有些棘手. 如果你已经实现了”fuzz-friendly mode”, 那么你有一个简单的解决方案: 那就是制造bug. 在你fuzzer应该到达的位置添加适当的print函数, assert断言或aborts函数, 并根据实际这些位置的到达情况进行跟踪. 你也可以将类似的”bug”添加到之前存在bug的位置. 只要记住, 你进行检查的时候不应影响到你的fuzzer, 使得你的fuzzer过于注重你刚刚添加”bug”的那部分代码, 并且在最后的投入使用之前将这些检查移除掉. 你可以自动化测试可以使用旧的构建版本进行测试, 那你还可以用那些有已知bug的版本来验证你的fuzzer能否找到这些已知bug, 测试那些已发现的旧bug同样也是一个找到系统中有待提高之处的好办法.

]]>
SHMALL简单堆内存分配器(Simple Heap Memory ALLocator) 2018-04-05T00:00:00+08:00 Vancir http://localhost:4000/2018/04/05/simple-heap-allocator SHMALL是作者@CCareaga为OS爱好者而编写的一个简单堆分配器. CCareaga编写的这个堆分配器力图尽可能易于理解. 希望能帮到那些OS开发的初学者以及那些对malloc和free的简单函数实现感兴趣的人.

我在学习原repo的过程中, 将README里的解释以及代码中的注释翻译成了中文, 详情可见: Vancir/heap_allocator

Compiling


代码中包含两个头文件, 各自用于定义堆和链表.

编译命令:

$ gcc main.c llist.c heap.c -o heap_test 
$ ./heap_test

这会运行分配器的一个demo并输出一些信息.

Initialization

需要提供一块内存用以初始化堆空间. 在这里是用的malloc进行分配. heap_t结构中的bins也同样需要分配内存

调用函数init_heap必须要提供一个空白堆结构的地址. 函数init_heap会创建一个chunk, chunk包含有一个header(node_t结构)和一个footer(footer_t结构). 函数需要使用常量HEAP_INIT_SIZE来确定chunk的大小, 并将其添加到start参数中以便确定heap的终结位置.

Metadata and Design

每个chunk的内存都包含一个位于开头的node结构和一个位于结尾的footer结构. 无论chunk被释放与否, node结构都包含有chunk的size以及2个用于双向链表中的指针(next和prev). footer结构只包含一个指向header的指针(当要释放相邻chunk时会用到这个指针). 在堆末尾的chunk被称为wilderness chunk. 它是堆中最大的chunk并且它的最大最小值都有在heap.h中声明. 合并或扩展chunk时就可以通过伸缩wilderness chunk来实现. 被释放的chunk存储在bin里, 每个bin实际上只是一个由大小相近的node组成的双向链表. 堆结构包含一定数量的bin, 数量在heap.h里的BIN_COUNT有定义. 要确定哪个bin放哪种chunk, 使用函数get_bin_index, 将chunk的size作为索引进行确定.

这种一致的binning函数可以确保chunk可以以预定的方式来访问和存储. chunks按顺序插入到bin中, 所以chunk的插入操作并不是O(1), 但也变得更加方便查找最合适的chunk. 注意, 你可以自己定义binning函数, 尽管默认的binning函数的表现已经足够优秀. 确定一个可以帮助快速检索chunk的更复杂binning函数也许是十分值得的.

Allocation

函数heap_alloc取刚刚分配的heap结构地址以及一个size作为参数. 使用函数get_bin_index来确定对应size的chunk的位置, 当然也许并没有那个size的chunk存在. 如果没有在对应的bin中找到合适的chunk, 那么就会检查下一个bin, 直到找到一个合适的chunk, 或是在找的过程中到达了最后一个bin(也没有找到), 这种情况下会从wilderness堆块中取一块内存出来创建chunk. 如果找到的chunk过大, 那么就会将该chunk分割一小块回收进bin里. 在判定一个chunk是否需要分割的时候, 会减去chunk中不需要使用的元数据(overhead)的大小, 根据剩下的size来确定. 如果chunk分割后, chunk左边的部分内存大于或等于MIN_ALLOC_SZ, 那我们就应该继续分割chunk并将剩余部分放到合适的bin中. 一旦我们准备好返回我们找到的chunk, 我们返回chunk的next地址. 这是因为分配chunk时我们并没有用到nextprev, 因此chunk的使用者可以将数据写进next域, 而这不会对堆的内部工作产生丝毫影响.

Freeing

函数heap_free获取heap_alloc返回的指针. 通过减去合适的偏移量以获取node结构的正确地址. 函数heap_free并不是简单地将chunk放置在对应的bin里就行了, 还会检查chunk周边的其他chunk. 如果周边的chunk处于空闲状态那么我们就可以将这些chunk合并成一个更大的chunk. 为了合并这些chunk, 我们需要使用footer来获取前一个chunk和后一个chunk的node结构. 比方说, 我们有一个叫to_free的chunk. 我们减去sizeof(footer_t)可以获取前一个chunk的footer, 这个footer包含一个指向前一个chunk头部的指针. 而获取后一个chunk , 我们相反加上sizeof(footer_t)即可. 当合并完毕, 我们会重新计算合并后的chunk的大小并放置进bin里

]]>
检测Android虚拟机的方法和代码实现 2018-04-04T00:00:00+08:00 Vancir http://localhost:4000/2018/04/04/android-anti-emulator 刚刚看了一些关于Detect Android Emulator的开源项目/文章/论文, 我看的这些其实都是13年14年提出的方法, 方法里大多是检测一些环境属性, 检查一些文件这样, 但实际上检测的思路并不局限于此. 有的是很直接了当去检测qemu, 而其它的方法则是旁敲侧击比如检测adb, 检测ptrace之类的. 思路也很灵活. 最后看到有提出通过利用QEMU这样的模拟CPU与物理CPU之间的实际差异(任务调度差异), 模拟传感器和物理传感器的差异, 缓存的差异等方法来检测. 相比检测环境属性, 检测效果会提升很多.

下面我就列出各个资料中所提出的一些方法/思路/代码供大家交流学习.

QEMU Properties

public class Property {
	public String name;
	public String seek_value;
	
	public Property(String name, String seek_value) {
		this.name = name;
		this.seek_value = seek_value;
	}
}
/** 
 * 已知属性, 格式为 [属性名, 属性值], 用于判定当前是否为QEMU环境
 */
private static Property[] known_props = {new Property("init.svc.qemud", null),
        new Property("init.svc.qemu-props", null), new Property("qemu.hw.mainkeys", null),
        new Property("qemu.sf.fake_camera", null), new Property("qemu.sf.lcd_density", null),
        new Property("ro.bootloader", "unknown"), new Property("ro.bootmode", "unknown"),
        new Property("ro.hardware", "goldfish"), new Property("ro.kernel.android.qemud", null),
        new Property("ro.kernel.qemu.gles", null), new Property("ro.kernel.qemu", "1"),
        new Property("ro.product.device", "generic"), new Property("ro.product.model", "sdk"),
        new Property("ro.product.name", "sdk"),
        new Property("ro.serialno", null)};
/**
 * 一个阈值, 因为所谓"已知"的模拟器属性并不完全准确, 有可能出现假阳性结果, 因此保持一定的阈值能让检测效果更好
 */
private static int MIN_PROPERTIES_THRESHOLD = 0x5;
/**
 * 尝试通过查询指定的系统属性来检测QEMU环境, 最后跟阈值比较得出检测结果.
 *
 * @param context A {link Context} object for the Android application.
 * @return {@code true} if enough properties where found to exist or {@code false} if not.
 */
public boolean hasQEmuProps(Context context) {
    int found_props = 0;

    for (Property property : known_props) {
        String property_value = Utilities.getProp(context, property.name);
        // See if we expected just a non-null
        if ((property.seek_value == null) && (property_value != null)) {
            found_props++;
        }
        // See if we expected a value to seek
        if ((property.seek_value != null) && (property_value.indexOf(property.seek_value) != -1)) {
            found_props++;
        }

    }

    if (found_props >= MIN_PROPERTIES_THRESHOLD) {
        return true;
    }

    return false;
}

这些都是基于一些经验和特征来比对的属性, 这里的属性以及之后的一些文件呀属性啊之类的我就不再多作解释.

Device ID

private static String[] known_device_ids = {"000000000000000", // Default emulator id
        "e21833235b6eef10", // VirusTotal id
        "012345678912345"};
public static boolean hasKnownDeviceId(Context context) {
    TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

    String deviceId = telephonyManager.getDeviceId();

    for (String known_deviceId : known_device_ids) {
        if (known_deviceId.equalsIgnoreCase(deviceId)) {
            return true;
        }

    }
    return false;
}

Default Number

private static String[] known_numbers = {
        "15555215554", // 模拟器默认电话号码 + VirusTotal
        "15555215556", "15555215558", "15555215560", "15555215562", "15555215564", "15555215566",
        "15555215568", "15555215570", "15555215572", "15555215574", "15555215576", "15555215578",
        "15555215580", "15555215582", "15555215584",};
public static boolean hasKnownPhoneNumber(Context context) {
    TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

    String phoneNumber = telephonyManager.getLine1Number();

    for (String number : known_numbers) {
        if (number.equalsIgnoreCase(phoneNumber)) {
            return true;
        }

    }
    return false;
}

IMSI

private static String[] known_imsi_ids = {"310260000000000" // 默认IMSI编号
};
public static boolean hasKnownImsi(Context context) {
    TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    String imsi = telephonyManager.getSubscriberId();

    for (String known_imsi : known_imsi_ids) {
        if (known_imsi.equalsIgnoreCase(imsi)) {
            return true;
        }
    }
    return false;
}

Build类

public static boolean hasEmulatorBuild(Context context) {
    String BOARD = android.os.Build.BOARD; // The name of the underlying board, like "unknown".
    // This appears to occur often on real hardware... that's sad
    // String BOOTLOADER = android.os.Build.BOOTLOADER; // The system bootloader version number.
    String BRAND = android.os.Build.BRAND; // The brand (e.g., carrier) the software is customized for, if any.
    // "generic"
    String DEVICE = android.os.Build.DEVICE; // The name of the industrial design. "generic"
    String HARDWARE = android.os.Build.HARDWARE; // The name of the hardware (from the kernel command line or
    // /proc). "goldfish"
    String MODEL = android.os.Build.MODEL; // The end-user-visible name for the end product. "sdk"
    String PRODUCT = android.os.Build.PRODUCT; // The name of the overall product.
    if ((BOARD.compareTo("unknown") == 0) /* || (BOOTLOADER.compareTo("unknown") == 0) */
            || (BRAND.compareTo("generic") == 0) || (DEVICE.compareTo("generic") == 0)
            || (MODEL.compareTo("sdk") == 0) || (PRODUCT.compareTo("sdk") == 0)
            || (HARDWARE.compareTo("goldfish") == 0)) {
        return true;
    }
    return false;
}

运营商名

public static boolean isOperatorNameAndroid(Context paramContext) {
    String szOperatorName = ((TelephonyManager) paramContext.getSystemService(Context.TELEPHONY_SERVICE)).getNetworkOperatorName();
    boolean isAndroid = szOperatorName.equalsIgnoreCase("android");
    return isAndroid;
}

QEMU驱动

private static String[] known_qemu_drivers = {"goldfish"};
/**
 * 读取驱动文件, 检查是否包含已知的qemu驱动
 *
 * @return {@code true} if any known drivers where found to exist or {@code false} if not.
 */
public static boolean hasQEmuDrivers() {
    for (File drivers_file : new File[]{new File("/proc/tty/drivers"), new File("/proc/cpuinfo")}) {
        if (drivers_file.exists() && drivers_file.canRead()) {
            // We don't care to read much past things since info we care about should be inside here
            byte[] data = new byte[1024];
            try {
                InputStream is = new FileInputStream(drivers_file);
                is.read(data);
                is.close();
            } catch (Exception exception) {
                exception.printStackTrace();
            }

            String driver_data = new String(data);
            for (String known_qemu_driver : FindEmulator.known_qemu_drivers) {
                if (driver_data.indexOf(known_qemu_driver) != -1) {
                    return true;
                }
            }
        }
    }

    return false;
}

QEMU文件

private static String[] known_files = {"/system/lib/libc_malloc_debug_qemu.so", "/sys/qemu_trace",
        "/system/bin/qemu-props"};
/**
 * 检查是否存在已知的QEMU环境文件
 *
 * @return {@code true} if any files where found to exist or {@code false} if not.
 */
public static boolean hasQEmuFiles() {
    for (String pipe : known_files) {
        File qemu_file = new File(pipe);
        if (qemu_file.exists()) {
            return true;
        }
    }

    return false;
}

Genymotion文件

private static String[] known_geny_files = {"/dev/socket/genyd", "/dev/socket/baseband_genyd"};
/**
 * 检查是否存在已知的Genemytion环境文件
 *
 * @return {@code true} if any files where found to exist or {@code false} if not.
 */
public static boolean hasGenyFiles() {
    for (String file : known_geny_files) {
        File geny_file = new File(file);
        if (geny_file.exists()) {
            return true;
        }
    }

    return false;
}

QEMU管道

private static String[] known_pipes = {"/dev/socket/qemud", "/dev/qemu_pipe"};
/**
 * 检查是否存在已知的QEMU使用的管道
 *
 * @return {@code true} if any pipes where found to exist or {@code false} if not.
 */
public static boolean hasPipes() {
    for (String pipe : known_pipes) {
        File qemu_socket = new File(pipe);
        if (qemu_socket.exists()) {
            return true;
        }
    }

    return false;
}

设置断点

static {
    // This is only valid for arm
    System.loadLibrary("anti");
}
public native static int qemuBkpt();

public static boolean checkQemuBreakpoint() {
    boolean hit_breakpoint = false;

    // Potentially you may want to see if this is a specific value
    int result = qemuBkpt();

    if (result > 0) {
        hit_breakpoint = true;
    }

    return hit_breakpoint;
}

以下是对应的c++代码

void handler_sigtrap(int signo) {
  exit(-1);
}

void handler_sigbus(int signo) {
  exit(-1);
}

int setupSigTrap() {
  // BKPT throws SIGTRAP on nexus 5 / oneplus one (and most devices)
  signal(SIGTRAP, handler_sigtrap);
  // BKPT throws SIGBUS on nexus 4
  signal(SIGBUS, handler_sigbus);
}

// This will cause a SIGSEGV on some QEMU or be properly respected
int tryBKPT() {
  __asm__ __volatile__ ("bkpt 255");
}

jint Java_diff_strazzere_anti_emulator_FindEmulator_qemuBkpt(JNIEnv* env, jobject jObject) {
  
  pid_t child = fork();
  int child_status, status = 0;
  
  if(child == 0) {
    setupSigTrap();
    tryBKPT();
  } else if(child == -1) {
    status = -1;
  } else {

    int timeout = 0;
    int i = 0;
    while ( waitpid(child, &child_status, WNOHANG) == 0 ) {
      sleep(1);
      // Time could be adjusted here, though in my experience if the child has not returned instantly
      // then something has gone wrong and it is an emulated device
      if(i++ == 1) {
        timeout = 1;
        break;
      }
    }

    if(timeout == 1) {
      // Process timed out - likely an emulated device and child is frozen
      status = 1;
    }

    if ( WIFEXITED(child_status) ) {
      // 子进程正常退出
      status = 0;
    } else {
      // Didn't exit properly - very likely an emulator
      status = 2;
    }

    // Ensure child is dead
    kill(child, SIGKILL);
  }

  return status;
}

这里我的描述可能并不准确, 因为并没有找到相关的资料. 我只能以自己的理解来解释一下:

SIGTRAP是调试器设置断点时发生的信号, 在nexus5或一加手机等大多数手机都可以触发. SIGBUS则是在一个总线错误, 指针也许访问了一个有效地址, 但总线会因为数据未对齐等原因无法使用, 在nexus4手机上可以触发. 而bkpt则是arm的断点指令, 这是曾经qemu被提出来的一个issue, qemu会因为SIGSEGV信号而崩溃, 作者想利用这个崩溃来检测qemu. 如果程序没有正常退出或被冻结, 那么就可以认定很可能是在模拟器里.

ADB

public static boolean hasEmulatorAdb() {
    try {
        return FindDebugger.hasAdbInEmulator();
    } catch (Exception exception) {
        exception.printStackTrace();
        return false;
    }
}

isUserAMonkey()

public static boolean isUserAMonkey() {
    return ActivityManager.isUserAMonkey();
}

这个其实是用于检测当前操作到底是用户还是脚本在要求应用执行.

isDebuggerConnected()

/**
 * 你信或不信, 还真有许多加固程序使用这个方法...
 */
public static boolean isBeingDebugged() {
    return Debug.isDebuggerConnected();
}

这个方法是用来检测调试, 判断是否有调试器连接.

ptrace

private static String tracerpid = "TracerPid";
/**
 * 阿里巴巴用于检测是否在跟踪应用进程
 * 
 * 容易规避, 用法是创建一个线程每3秒检测一次, 如果检测到则程序崩溃
 * 
 * @return
 * @throws IOException
 */
public static boolean hasTracerPid() throws IOException {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/status")), 1000);
        String line;

        while ((line = reader.readLine()) != null) {
            if (line.length() > tracerpid.length()) {
                if (line.substring(0, tracerpid.length()).equalsIgnoreCase(tracerpid)) {
                    if (Integer.decode(line.substring(tracerpid.length() + 1).trim()) > 0) {
                        return true;
                    }
                    break;
                }
            }
        }

    } catch (Exception exception) {
        exception.printStackTrace();
    } finally {
        reader.close();
    }
    return false;
}

这个方法是通过检查/proc/self/statusTracerPid项, 这个项在没有跟踪的时候默认为0, 当有程序在跟踪时会修改为对应的pid. 因此如果TracerPid不等于0, 那么就可以认为是在模拟器环境.

TCP连接

public static boolean hasAdbInEmulator() throws IOException {
    boolean adbInEmulator = false;
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/net/tcp")), 1000);
        String line;
        // Skip column names
        reader.readLine();

        ArrayList<tcp> tcpList = new ArrayList<tcp>();

        while ((line = reader.readLine()) != null) {
            tcpList.add(tcp.create(line.split("\\W+")));
        }

        reader.close();

        // Adb is always bounce to 0.0.0.0 - though the port can change
        // real devices should be != 127.0.0.1
        int adbPort = -1;
        for (tcp tcpItem : tcpList) {
            if (tcpItem.localIp == 0) {
                adbPort = tcpItem.localPort;
                break;
            }
        }

        if (adbPort != -1) {
            for (tcp tcpItem : tcpList) {
                if ((tcpItem.localIp != 0) && (tcpItem.localPort == adbPort)) {
                    adbInEmulator = true;
                }
            }
        }
    } catch (Exception exception) {
        exception.printStackTrace();
    } finally {
        reader.close();
    }

    return adbInEmulator;
}

public static class tcp {

    public int id;
    public long localIp;
    public int localPort;
    public int remoteIp;
    public int remotePort;

    static tcp create(String[] params) {
        return new tcp(params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8],
                        params[9], params[10], params[11], params[12], params[13], params[14]);
    }

    public tcp(String id, String localIp, String localPort, String remoteIp, String remotePort, String state,
                    String tx_queue, String rx_queue, String tr, String tm_when, String retrnsmt, String uid,
                    String timeout, String inode) {
        this.id = Integer.parseInt(id, 16);
        this.localIp = Long.parseLong(localIp, 16);
        this.localPort = Integer.parseInt(localPort, 16);
    }
}

这个方法是通过读取/proc/net/tcp的信息来判断是否存在adb. 比如真机的的信息为0: 4604D20A:B512 A3D13AD8..., 而模拟器上的对应信息就是0: 00000000:0016 00000000:0000, 因为adb通常是反射到0.0.0.0这个ip上, 虽然端口有可能改变, 但确实是可行的.

TaintDroid

public static boolean hasPackageNameInstalled(Context context, String packageName) {
    PackageManager packageManager = context.getPackageManager();

    // In theory, if the package installer does not throw an exception, package exists
    try {
        packageManager.getInstallerPackageName(packageName);
        return true;
    } catch (IllegalArgumentException exception) {
        return false;
    }
}
public static boolean hasAppAnalysisPackage(Context context) {
    return Utilities.hasPackageNameInstalled(context, "org.appanalysis");
}
public static boolean hasTaintClass() {
    try {
        Class.forName("dalvik.system.Taint");
        return true;
    }
    catch (ClassNotFoundException exception) {
        return false;
    }
}

这个比较单纯了. 就是通过检测包名, 检测Taint类来判断是否安装有TaintDroid这个污点分析工具. 另外也还可以检测TaintDroid的一些成员变量.

eth0

private static boolean hasEth0Interface() {
    try {
        for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
            NetworkInterface intf = en.nextElement();
            if (intf.getName().equals("eth0"))
                return true;
        }
    } catch (SocketException ex) {
    }
    return false;
}

检测是否存在eth0网卡.

传感器

手机上配备了各式各样的传感器, 但它们实质上都是基于从环境收集的信息输出值, 因此想要模拟传感器是非常具有挑战性的. 这些传感器为识别手机和模拟器提供了新的机会.

比如在论文Rage Against the Virtual Machine: Hindering Dynamic Analysis of Android Malware中, 作者对Android模拟器的加速器进行测试, 作者发现Android模拟器上的传感器会在相同的时间间隔内(观测结果是0.8s, 标准偏差为0.003043)产生相同的值. 显然对于现实世界的传感器, 这是不可能的.

acc-cdf.png

于是我们可以先注册一个传感器监听器, 如果注册失败, 就可能是在模拟器中(排除实际设备不支持传感器的可能性). 如果注册成功, 那么检查onSensorChanged回调方法, 如果在连续调用这个方法的过程所观察到的传感器值或时间间隔相同, 那么就可以认定是在模拟器环境中.

QEMU任务调度

出于性能优化的原因, QEMU在每次执行指令时都不会主动更新程序计数器(PC), 由于翻译指令在本地执行, 而增加PC需要额外的指令带来开销. 所以QEMU只在执行那些从线性执行过程里中断的指令(例如分支指令)时才会更新程序计数器. 这也就导致在执行一些基本块的期间如果发生了调度事件, 那么也没有办法恢复调度前的PC, 也是出于这个原因, QEMU仅在执行基本块后才发生调度事件, 绝不会执行的过程中发生.

sche-point.png

如上图, 因为调度可能在任意时间发生, 所以在非模拟器环境下, 会观察到大量的调度点. 而在模拟器环境中, 只能看到特定的调度点.

SMC识别

因为QEMU会跟踪代码页的改动, 于是存在一种新颖的方法来检测QEMU–使用自修改代码(Self-Modifying Code, SMC)引起模拟器和实际设备之间的执行流变化.

memory.png

ARM处理器包含有两个不同的缓冲Cache, 一个用于指令访问(I-Cache), 而另一个用于数据访问(D-Cache). 但如ARM这样的哈佛架构并不能保证I-Cache和D-Cache之间的一致性. 因此CPU有可能在新代码片已经写入主存后执行旧的代码片(也许是无效的).

这个问题可以通过强迫两个缓存一致得到解决, 这有两步:

  1. 清理主存, 以便将D-Cache中新写入的代码移入主存
  2. 使I-Cache无效, 以便它可以用主存的新内容重新填充.

在原生Android代码中, 可以使用cacheflush函数, 该函数通过系统调用完成上述操作.

diff.png

识别代码, 使用一个具有读写权限的内存, 其中包含两个不同函数f1和f2的代码, 这两个函数其实很简单, 只是单纯在一个全局字符串变量的末尾附加各自的函数名称, 这两个函数会在循环里交错执行, 这样就可以通过结果的字符串推断出函数调用序列.

如前所述, 我们调用cacheflush来同步缓存. 在实际设备和模拟器上运行代码得到的结果是相同的–每次执行都会产生一致的函数调用序列.

接下来我们移除调用cacheflush, 执行相同的操作. 那么在实际设备中, 我们每次运行都会观察到一个随机的函数调用序列, 这也如前所述的那样, 因为I-Cache可能包含一些旧指令, 每次调用的时候缓存都不同步所导致的.

而模拟器环境却不会发生这样的情况, 而且函数调用序列会跟之前没有移除cacheflush时完全相同, 也就是每次函数调用前缓存都是一致的. 这是因为QEMU会跟踪代码页上的修改, 并确保生成的代码始终与内存中的目标指令匹配, 因此QEMU会放弃之前版本的代码翻译并重新生成新代码.

结语

看到这里会不会已经觉得检测方法够多了. 可是我还只是看了13年14年的资料. 有关近几年的资料还未涉及.

最后我就把这些检测方法整合在一张思维导图里供大家一览, 欢迎大家和我交流带带我

参考链接

]]>
恶意软件"TSCookie"介绍 2018-03-06T00:00:00+08:00 Vancir http://localhost:4000/2018/03/06/tscookie 2018年1月17日左右, 社交媒体上开始出现一些关于恶意邮件的报道, 这些邮件声称来自日本的教育部, 文化部, 体育部和科技部. 这些邮件里包含有指向恶意软件”TSCookie”的URL链接(趋势科技将其称为为PLEAD恶意软件, 因为PLEAD取自趋势科技过往捕获到的一次APT攻击活动, 故本文中我们将该恶意软件命名为”TSCookie”). TSCookie在2015年在野外被发现, 并且怀疑黑客组织”BlackTech”与此次攻击活动有关. JPCERT/CC证实称, 使用恶意软件的敌对团伙已经对日本组织进行了针对性的攻击. 本文将介绍我们在分析TSCookie后的成果.

TSCookie概述

下图描述了TSCookie的执行流程:

1.png

TSCookie本身只是用作一个下载器, 通过从C&C服务器下载模块来扩展功能. 我们所检查的样本下载了一个具有传出信息和其他功能的DLL文件(以下简称”TSCookieRAT”). 下载的模块仅在内存上运行

TSCookie和TSCookieRAT的行为将在下面的章节中详细解释.

TSCookie行为

TSCookie使用HTTP协议与C&C服务器进行通信, 并下载用于加载模块的”模块”和”加载程序”. 恶意软件的资源中有一个加密的DLL文件. 当恶意软件被执行时, DLL文件被加载进内存并执行. DLL文件会执行一些主要功能, 例如与C&C服务器进行通信. (在某些情况下, 主要功能部分并未经过加密并且存储在恶意软件中, 还有一些样本会启动另一个进程并注入解密后的DLL文件.) 恶意软件的配置信息包括有C&C服务器信息且同时使用RC4进行加密. 有关配置的详细信息, 请参阅附录A.

以下是TSCookie在开始时发送的HTTP GET请求的示例. 出站消息被编码包含在Cookie头信息中.

GET /Default.aspx HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Date: Thu, 18 Jan 2018 10:20:55 GMT
Pragma: no-cache
Accept: */*
Cookie: 1405D7CD01C6978E54E86DA9525E1395C4DD2F276DD28EABCC3F6201ADAA66F55C15352D29D0FFE51BC9D431EB23E8E58959653D9366E372B5CFCC49BB
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Win32)
Host:[host name]:443

包含在Cookie头中的数据经RC4加密(密钥是Date标头值). 数据格式请参考附录B表B-1.

通过这个HTTP GET请求获得的数据使用一个8字节值进行RC4加密, 这个8字节值由配置中的一个固定值(附录A, 表A-1)和发送数据中的一个值(在附录B表B-1中, “根据系统信息生成的4字节值”)组成. 这些数据还包括有模块的加载程序.

TSCookie随后下载一个模块. 以下是下载模块的HTTP POST请求示例.

POST /Default.aspx HTTP/1.1
Connection: Keep-Alive
Date: Thu, 18 Jan 2018 10:30:55 GMT
Content-Type: application/x-www-form-urlencoded
Accept: */*
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Win32)
Content-Length: 34
Host: [host name]:443

[data]

发送的数据也同样是由RC4加密的(密钥是Date标头值). 数据格式请参考附录B表B-2. 通过该HTTP POST请求获得的数据也经过RC4加密, 使用的密钥与HTTP GET请求中密钥相同. 下载下来的模块可以先载入到内存中, 然后调用由HTTP GET请求获得的加载程序来得到执行.

TSCookieRAT行为

TSCookie在加载TSCookieRAT时提供一些参数比如C&C服务器信息. 程序一旦执行后, 感染主机的信息将通过HTTP POST请求发送到外部服务器.(HTTP头信息格式与TSCookie相同.)

数据从开头到0x14(密钥为Date标头值)都有经RC4加密, 紧跟着的是感染主机的信息(主机名, 用户名, 操作系统版本等等). 数据格式请参考附录C表C-1.

下图是发送感染主机信息(已解码)的示例.

2.png

之后, TSCookieRAT发送一个HTTP GET请求.(HTTP头信息依旧与TSCookie相同.) 通过该请求, C&C服务器发出命令, 而TSCookieRAT执行下列功能. (关于接收到的数据, 请参阅附录C, 表C-2, 关于命令列表, 请参阅附录D, 表D-1.)

  • 执行任意shell命令
  • 发送驱动器信息
  • 发送系统信息
  • 进行文件操作
  • 从Internet Explorer,Edge,Firefox,Chrome,Outlook处收集密码

命令执行的结果以第一个HTTP POST请求同样的格式发送出去(发送感染主机信息). 从C&C服务器发出的命令并未经过编码. 以下是执行列举进程和模块信息的命令时, 发送数据(已解码)的示例。

3.png

TSCookie解码工具

JPCERT/CC制作了一个工具, 用于解码和提取TSCookie的配置信息. 你可以访问Github使用该工具:

JPCERTCC/aa-tools · GitHub https://github.com/JPCERTCC/aa-tools/blob/master/tscookie_decode.py

4.png

结语

使用TSCookie的敌对团伙一直在利用各种类型的恶意软件对日本组织进行攻击. 由于这次攻击行动很可能持续下去. JPCERT/CC将继续谨慎地观察这一趋势.

附录E列出了为本文所检查的样本的散列值. 附录F中还列出了与TSCookie相关的一些目标主机. 请确保您的设备没有与这些主机通信.

如有任何疑问,请联系global-cc [at] jpcert.or.jp

Shusei Tomonaga

参考文章

[1] piyolog: Summary on Ministry of Education, Culture, Sports, Science and Technology Scam in January 2018 (Japanese)

http://d.hatena.ne.jp/Kango/20180119/1516391079

[2] Trend Micro: Following the Trail of BlackTech’s Cyber Espionage Campaigns

https://documents.trendmicro.com/assets/appendix-following-the-trail-of-blacktechs-cyber-espionage-campaigns.pdf

[3] Trend Micro: Following the Trail of BlackTech’s Cyber Espionage Campaigns

https://blog.trendmicro.com/trendlabs-security-intelligence/following-trail-blacktech-cyber-espionage-campaigns/

附录 A: TSCookie 配置信息

表A: 配置信息清单

偏移值 描述 备注
0x000 主机1标志 如果为0x01则进行通讯
0x004 主机1的端口号1  
0x008 主机1的端口号2  
0x010 主机1  
0x100 主机2标志  
0x104 主机2的端口号1  
0x108 主机2的端口号2  
0x110 主机2  
0x200 主机3标志  
0x204 主机3的端口号1  
0x208 主机3的端口号2  
0x210 主机3  
0x300 主机4标志  
0x304 主机4端口号1  
0x308 主机4端口号2  
0x310 主机4  
0x400 代理服务器  
0x480 代理端口号  
0x484 代理配置标志  
0x500 ID号  
0x604 固定值 4字节的RC4密钥 (0x925A765D)
0x89C 暂停时间  

附录 B: TSCookie 发出/接收的数据

表 B-1: Cookie头中包含的数据的格式

偏移 长度 内容
0x00 4 根据系统信息生成的4字节 (*)
0x04 4 0x10050014
0x08 4 0x10001
0x0C 4 0xAB1
0x10 4 0x04
0x14 4 根据系统信息生成的4字节
0x18 - 随机数据

(*) 表示是用固定值(0x925A765D)加密的RC4值

表 B-2: HTTP POST的数据格式

偏移 长度 内容
0x00 4 根据系统信息生成的4字节
0x04 4 0x10050014
0x08 4 0x10001
0x0C 4 0xAAD
0x10 4 位于0x14后的数据长度
0x14 - 随机数据

附录 C: TSCookieRAT 发出/接收的数据

表 C-1: HTTP POST的数据格式

偏移 长度 内容
0x00 4 根据系统信息生成的4字节
0x04 4 0x10050014
0x08 4 0x10001
0x0C 4 0xAAD
0x10 4 位于0x14后的数据长度
0x14 - 感染主机信息(使用”根据系统信息生成的4字节值”作为密钥进行RC4加密

表 C-2: 接收到的数据格式

偏移 长度 内容
0x00 4 命令
0x04 4 位于0x8后的数据长度
0x08 - 参数

附录D:TSCookieRAT使用的命令

表 D-1: 命令清单

含义
0x912 配置暂停时间
0x930 列举进程和模块
0x932 终止
0x934 启动远程shell
0x935 执行远程shell指令
0x936 关闭远程shell
0x946 获取IP地址
0x950 执行文件 (windows会有显示)
0x951 执行文件 (windows不会显示)
0x952 发送消息
0x953 发送驱动消息
0x954 发送文件清单
0x955 发送文件大小
0x956 发送文件
0x957 关闭对象句柄
0x958 选择文件发送 (发送文件同时执行0x955, 0x956命令)
0x959 下载文件
0x95A 删除文件
0x95C 移动文件
0x95E -
0x960 -
0x96B 获取windows窗体名
0x96E 从Internet Explorer, Edge, Firefox, Chrome, Outlook收集密码

附录 E: 样本的SHA-256值

TSCookie

  • 6d2f5675630d0dae65a796ac624fb90f42f35fbe5dec2ec8f4adce5ebfaabf75
  • cdf0e4c415eb55bccb43a650e330348b63bc3cbb53f71a215c44ede939b4b830
  • 17f1996ad7e602bd2a7e9524d7d70ee8588dac51469b08017df9aaaca09d8dd9
  • 1fa7cbe57eedea0ebc8eb37b91e7536c07be7da7775a6c01e5b14489387b9ca8
  • e451a1e05c0cc363a185a98819cd2af421ac87154702bf72007ecc0134c7f417
  • 1da9b4a84041b8c72dad9626db822486ce47b9a3ab6b36c41b0637cd1f6444d6
  • 35f966187098ac42684361b2a93b0cee5e2762a0d1e13b8d366a18bccf4f5a91
  • 0683437aebd980c395a83e837a6056df1a21e137e875f234d1ed9f9a91dfdc7f
  • 0debbcc297cb8f9b81c8c217e748122243562357297b63749c3847af3b7fd646
  • 96306202b0c4495cf93e805e9185ea6f2626650d6132a98a8f097f8c6a424a33
  • 6b66c6d8859dfe06c0415be4df2bd836561d5a6eabce98ddd2ee54e89e37fd44
  • 06a9c71342eeb14b7e8871f77524e8acc7b86670411b854fa7f6f57c918ffd2b
  • 20f7f367f9cb8beca7ce1ba980fafa870863245f27fea48b971859a8cb47eb09
  • f16befd79b7f8ffdaf934ef337a91a5f1dc6da54c4b2bee5fe7a0eb38e8af39e
  • 12b0f1337bda78f8a7963d2744668854d81e1f1b64790b74d486281bc54e6647
  • 201bf3cd2a723d6c728d18a9e41ff038549eac8406f453c5197a1a7b45998673
  • 5443ee54a532846da3182630e2bb031f54825025700bcd5f0e34802e7345c7b2
  • 39d7d764405b9c613dff6da4909d9bc46620beee7a7913c4666acf9e76a171e4
  • afe780ba2af6c86babf2d0270156da61f556c493259d4ca54c67665c17b02023
  • 4a8237f9ecdad3b51ffd00d769e23f61f1e791f998d1959ad9b61d53ea306c09
  • 203c924cd274d052e8e95246d31bd168f3d8a0700a774c98eff882c8b8399a2f

TSCookieRAT

  • 2bd13d63797864a70b775bd1994016f5052dc8fd1fd83ce1c13234b5d304330d

附录 F: 与TSCookie相关的目标主机

  • 220.130.216.76
  • 60.244.52.29
  • 45.76.102.145
  • jpcerts.jpcertinfo.com
  • jpcert.ignorelist.com
  • twnicsi.ignorelist.com
  • twcertcc.jumpingcrab.com
  • okinawas.ssl443.org
  • apk36501.flnet.org
  • appinfo.fairuse.org
  • carcolors.effers.com
  • edu.microsoftmse.com
  • eoffice.etowns.org
  • epayplus.flnet.org
  • fatgirls.fatdiary.org
  • gethappy.effers.com
  • iawntsilk.dnset.com
  • inewdays.csproject.org
  • ktyguxs.dnset.com
  • lang.suroot.com
  • langlang.dnset.com
  • longdays.csproject.org
  • lookatinfo.dnset.com
  • newtowns.flnet.org
  • ntp.ukrootns1.com
  • office.dns04.com
  • savecars.dnset.com
  • splashed.effers.com
  • sslmaker.ssl443.org
]]>
the White Rabbit CrackMe 解答 2018-02-06T00:00:00+08:00 Vancir http://localhost:4000/2018/02/06/solve-white-habbit-crackme Crackme文件可以从此处下载: White Rabbit crackme!

因为crackme里稍微使用了混淆和一些像恶意程序的把戏, 所以可能会被一些杀毒软件标记为恶意程序, 所以也建议在虚拟机下运行.

这个crackme运行的截图如下:

1.png

OK, 首先要做的第一件事就是将其载入到IDA中(我这里使用的是刚刚发布的IDA 7的免费版本). 通过搜索字符串Password#1来看它的交叉引用以及前后都发生了些什么.

2.png

就这了! 我们可以看到它被sub_4034D0所引用. 现在我们将跟随到引用处, 来看看接下来发生什么

3.png

sub_403D90中有一些初始化操作, 随后在sub_404150的结果与可疑值0x57585384的比较后又一个分支跳转. 子分支中的sub_403990输出了一些提示语以及后续一些有关接受用户输入的内容.

我们首先来看初始化部分(sub_403D90):

4.png

函数取了两个参数, 内容看上去也非常清楚: 通过给出的标识符查找资源文件, 加载资源文件, 确定它的文件大小, 然后申请内存空间并将资源文件的数据复制进去. 该函数返回那个新申请的内存空间的指针, 并将资源文件的大小存储在第一个参数中.

现在我们唯一需要注意的就是图中的sub_406A70, 它取了3个参数(target pointer, source pointer 以及 data size)并且看起来非常像是memcpy(或memmove, 是哪个不重要, 因为内存区域没有重叠). 但是函数内的代码却包含有大量的分支, 难以分析. 所以我们不能确定它有没有在复制的过程中以某种方式修改了数据(比如, 解密数据). 最简便的检查方式就是在调试器里动态分析, 比较函数返回时, souretarget内存是否有区别.

我使用[x64Dbg](https://x64dbg.com/)来分析. 在启动调试器后我们打开crackme, 调试器会自动运行程序并暂停在入口点位置.

5.png

现在我们需要在我们感兴趣的函数返回处设下一个断点. 指令地址是0x00403DF9(给定.text段的基址是0x00401000). 你可以根据内存布局来了解真正.text段载入的基址(我这里是0x00281000). 因此我的实际断点地址应该是0x00283DF9.

现在我们用bp 0x00283DF9命令设下断点, 继续执行触发断点. 然后我们右键点击右侧面板ebxedi寄存器的值, 选择在数据窗口跟随. 现在我们就可以确认sub_406A70仅仅复制了内存as is, 我们可以放心地将该函数重命名为更易理解的memcpy. 同样我们也把sub_403D90重命名为loadResource

现在我们来分析sub_404150

6.png

映入眼帘的是一个常量0x82F63B78. 通过google搜索知道说这是一个用于CRC32计算的多项式值. 代码里看也有从输入缓冲区里对每个字节的值异或累加, 随后再移位/异或8次. 因此它确实是一个crc32c计算函数.

在重命名和初期的分析后, 我们再来看看改动后的代码

7.png

注意: 也许你会对lea/cmovnb指令有些许困惑. 不过很好解释: lpPasswordText的值实际上是如下的结构体:

struct {
    union {
        char static[16];
        char *dynamic;
    }
    size_t length;
    size_t size;
}

这可能就是栈上std::string的形式. 当字符串仅有static数组那么长时, 不会申请额外的内存空间(并且static缓冲区的地址用lea加载). 相反如果超出了缓冲区, cmovnb会获取dynamic域所分配的内存的指针. 最后, eax会获得指向真正字符串数据的指针, 不论其位置具体在哪.

因此, sub_401000读取键盘输入到std::string, std::string随后传递给crc32c函数. 现在我们知道说我们的password应该含有CRC32的0x57585384, 我们可以根据这个条件判断我们是否获取到了正确的password.

现在我们来假定password跟给出的CRC32值相匹配, 来继续往下分析:

8.png

有趣的第一点就是sub_403C90, 因为它同时取了password资源数据作为参数.

9.png

很显然这里是一个异或加密的操作. 它首先确定password的长度, 随后用相应的password字符对输入缓冲区的每一个字节进行异或.

随后生成一个临时文件名, 将解密的资源数据内容写入到该文件(在函数sub_403090里). 待一切完成, 却也再没有给出任何关于password的线索了. 我们来看一下sub_403D20, 该函数接收新创建的文件名并执行了一些操作.

10.png

OK, 现在事情已经越发清晰. crackme尝试设置新生成的文件作为桌面壁纸, 因此很显然这个文件应该是一个图片.

现在我们要提取crackme里的资源文件, 看看我们能否有所收获. 你可以使用任意的资源编辑软件, 例如: Resource Hacker

11.png

我们可以看到它的大小是6,220,854字节, 对于一个图像来说已经很大了, 据此我们猜测, 这是一个无压缩的BMP图像文件.

BMP格式已经是众所周知, 并且有文档说明. 文件起始于一个"BM"签名, 随后是4字节的文件大小(小端序), 接着是两个4字节的保留字(全0), 一个4字节存储着位图数据的起始位置, 再紧接着是40字节的位图信息头(起始的是该信息头所占用的字节数). 再下面就是各种关于BMP信息了, 我们现在也不知道.

由于我们得知了真正的文件大小值, 所以我们可以较准确地推测出文件的前18个字节.

资源文件里的字节:
24 22 5A 80 31 77 5F 64 61 5F 44 61 62 62 41 74 7A 66
期待的结果:
42 4D 36 EC 5E 00 00 00 00 00 36 00 00 00 28 00 00 00

现在我们逐个将实际资源文件里的字节和说期望的字节进行异或, 这样我们就可以恢复出部分key的内容. 如果幸运的话, 我们可以获得一个完整的key

66 6F 6C 6C 6F 77 5F 64 61 5F 72 61 62 62 69 74 7A 66

异或得到的结果是"follow_da_rabbitzf". 最后的这一个"f"也许是重复的下一个key的起始字母, 也许就是这个key的一部分. 最简单的检查方法就是将其输入到crackme里看看结果如何.

12.png

Yeah. 我们的结果是正确的. 我们再继续.

现在我们有一个超酷的桌面壁纸, 然后还有另外一个password需要破解出来. 我们再次搜索"Password#2"字符串并跟随到交叉引用处:

13.png

这看起来跟之前非常相似, 因此我们自己向下来到解密开始的部分:

14.png

有趣的部分在sub_403E10, 这里在写入数据到文件之前进行了解密:

15.png

这里根据password导出一个AES128的密钥(使用SHA256作为密钥导出算法)并用于解密资源数据.

没有必要去破解AES加密(恐怕就连NSA也无法破解), 我们只知道password的crc32值. 很显然不足以通过暴力破解的手段来获取它(我尝试过!). 但等等, 我们有一个壁纸啊! 或许在壁纸里会有某些隐藏的信息!

用图像编辑器打开并使用”颜色选择”工具:

16.png

这应该就是我们一直在寻找的key! 接下来继续:

17.png

但是事情还没结束. 现在我们在临时目录下有一个解密过的可执行文件, 但我们还是没有拿到flag. 我们还需要用IDA继续分析.

因为第二个可执行文件按并没有产生任何字符串信息, 也就难以下手. 我们就来看看导入表情况:

18.png

这里有一系列的按顺序导入的ws2_32.dll的函数, 这给我们两个线索:

  • 程序有在进行网络socket操作
  • 程序有隐藏些什么!

因此我们的第一步就是去到这些函数被调用的地方, 并将这些函数重命名为可读性更高更有意义的名称. 序号与之对应的函数名称可以很容易地通过google搜索找到.

现在我们知道了所有的网络操作都在sub_404480里, 因此接下来仔细看看这个函数. 该函数开始是一个标准流程(WSAStartup/socket/bind/listen), 所以没太多亮点, 有趣的部分在下图:

19.png

因此它等待接受一个连接, 从连接中读取4字节, 基于静态缓冲区buf和接收的数据在sub_404640中执行一些操作. 如果操作成功转型(函数返回非零值), 它就会将buf的内容发回给客户端随后关闭连接. 否则它会关闭连接监听新的连接. 所有的操作都是同步的, 所以在sub_404640成功执行前不会退出函数.

来看看sub_404640的内容:

20.png

看起来非常像是一个小的状态机, 成功转移到下一状态时返回1, 有如下几个转移:

  • 从 初始状态(0) 到 ‘Y’ 状态 (如果接收到9)
  • 从 ‘Y’ 状态 到 ‘E’ 状态 (如果接收到3)
  • 从 ‘E’ 状态 到 ‘S’ 状态 (如果接收到5)
  • 接收到其他的任何值, 都会将状态机重置为 初始状态(0)

因此, 我们可能需要按顺序发起3个连接, 连到"server", 更新状态机到下一状态.

但是我们仍有两个问题需要解决:

  1. 我们不知道需要连接到哪一个端口(函数需要取端口号作参数)
  2. 在每次成功转移状态后, 监听的套接字都会关闭

因此我们需要找到所有的函数被调用的地方, 然后跟踪看它启动了哪一个端口.

如同我们所预料的那样, 函数被调用了3次(因为有3次合法的状态转移), 并且幸运的是, 它都是在同一个步骤里被调用的:

21.png

在这里

22.png

所以, server一开始开启了端口1337, 随后是1338, 最后是1339. 因此我们首先需要连接到1337端口并发送9, 然后连接到1338端口, 发送3. 最后连接到1339端口, 发送5. 我们可以使用内置的telnet工具来完成这一操作.

完成上述操作后会打开一个简短视频的YouTube页面:

23.png

我们成功地拿到了flag. 收工回家!

]]>
Unicorn Engine简介 2018-01-26T00:00:00+08:00 Vancir http://localhost:4000/2018/01/26/unicorn-intro 什么是Unicorn引擎

Unicorn是一个轻量级, 多平台, 多架构的CPU模拟器框架. 我们可以更好地关注CPU操作, 忽略机器设备的差异. 想象一下, 我们可以将其应用于这些情景: 比如我们单纯只是需要模拟代码的执行而非需要一个真的CPU去完成那些操作, 又或者想要更安全地分析恶意代码, 检测病毒特征, 或者想要在逆向过程中验证某些代码的含义. 使用CPU模拟器可以很好地帮助我们提供便捷.

它的亮点(这也归功于Unicorn是基于qemu而开发的)有:

  • 支持多种架构: Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64).
  • 对Windows和*nix系统(已确认包含Mac OSX, Linux, *BSD & Solaris)的原生支持
  • 具有平台独立且简洁易于使用的API
  • 使用JIT编译技术, 性能表现优异

你可以在Black Hat USA 2015获悉有关Unicorn引擎的更多技术细节. Github项目主页: unicorn

尽管它不同寻常, 但它无法模拟整个程序或系统, 也不支持系统调用. 你需要手动映射内存并写入数据进去, 随后你才能从指定地址开始模拟.

应用的情景

什么时候能够用到Unicorn引擎呢?

  • 你可以调用恶意软件中一些有趣的函数, 而不用创建一个有害的进程.
  • 用于CTF竞赛
  • 用于模糊测试
  • 用于gdb插件, 基于代码模拟执行的插件
  • 模拟执行一些混淆代码

如何安装

安装Unicorn最简单的方式就是使用pip安装, 只要在命令行中运行以下命令即可(这是适合于喜爱用python的用户的安装方法, 对于那些想要使用C的用户, 则需要去官网查看文档编译源码包):

pip install unicorn

但如果你想用源代码进行本地编译的话, 你需要在下载页面中下载源代码包, 然后可以按照以下命令执行:

  • *nix 平台用户
$ cd bindings/python
$ sudo make install
  • Windows平台用户
cd bindings/python
python setup.py install

对于Windows, 在执行完上述命令后, 还需要将下载页面的Windows core engine的所有dll文件复制到C:\locationtopython\Lib\site-packages\unicorn位置处.

使用unicorn的快速指南

我们将会展示如何使用python调用unicorn的api以及它是如何轻易地模拟二进制代码. 当然这里用的api仅是一小部分, 但对于入门已经足够了.

 1 from __future__ import print_function
 2 from unicorn import *
 3 from unicorn.x86_const import *
 4 
 5 # code to be emulated
 6 X86_CODE32 = b"\x41\x4a" # INC ecx; DEC edx
 7 
 8 # memory address where emulation starts
 9 ADDRESS = 0x1000000
10 
11 print("Emulate i386 code")
12 try:
13     # Initialize emulator in X86-32bit mode
14     mu = Uc(UC_ARCH_X86, UC_MODE_32)
15 
16     # map 2MB memory for this emulation
17     mu.mem_map(ADDRESS, 2 * 1024 * 1024)
18 
19     # write machine code to be emulated to memory
20     mu.mem_write(ADDRESS, X86_CODE32)
21 
22     # initialize machine registers
23     mu.reg_write(UC_X86_REG_ECX, 0x1234)
24     mu.reg_write(UC_X86_REG_EDX, 0x7890)
25 
26     # emulate code in infinite time & unlimited instructions
27     mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
28 
29     # now print out some registers
30     print("Emulation done. Below is the CPU context")
31 
32     r_ecx = mu.reg_read(UC_X86_REG_ECX)
33     r_edx = mu.reg_read(UC_X86_REG_EDX)
34     print(">>> ECX = 0x%x" %r_ecx)
35     print(">>> EDX = 0x%x" %r_edx)
36 
37 except UcError as e:
38     print("ERROR: %s" % e)

运行结果如下:

$ python test1.py 
Emulate i386 code
Emulation done. Below is the CPU context
>>> ECX = 0x1235
>>> EDX = 0x788f

样例里的注释已经非常直观, 但我们还是对每一行代码做出解释:

  • 行号2~3: 在使用Unicorn前导入unicorn模块. 样例中使用了一些x86寄存器常量, 所以也需要导入unicorn.x86_const模块
  • 行号6: 这是我们需要模拟的二进制机器码, 使用十六进制表示, 代表的汇编指令是: “INC ecx” 和 “DEC edx”.
  • 行号9: 我们将模拟执行上述指令的所在虚拟地址
  • 行号14: 使用Uc类初始化Unicorn, 该类接受2个参数: 硬件架构和硬件位数(模式). 在样例中我们需要模拟执行x86架构的32位代码, 我们使用变量mu来接受返回值.
  • 行号17: 使用mem_map 方法根据在行号9处声明的地址, 映射2MB用于模拟执行的内存空间. 所有进程中的CPU操作都应该只访问该内存区域. 映射的内存具有默认的读,写和执行权限.
  • 行号20: 将需要模拟执行的代码写入我们刚刚映射的内存中. mem_write方法接受2个参数: 要写入的内存地址和需要写入内存的代码.
  • 行号23~24: 使用reg_write方法设置ECXEDX寄存器的值
  • 行号27: 使用emu_start方法开始模拟执行, 该API接受4个参数: 要模拟执行的代码地址, 模拟执行停止的内存地址(这里是X86_CODE32的最后1字节处), 模拟执行的时间和需要执行的指令数目. 如果我们像样例一样忽略后两个参数, Unicorn将会默认以无穷时间和无穷指令数目的条件来模拟执行代码.
  • 行号32~35: 打印输出ECXEDX寄存器的值. 我们使用函数reg_read来读取寄存器的值.

要想查看更多的python示例, 可以查看文件夹bindings/python下的代码. 而C的示例则可以查看sample文件夹下的代码.

参考链接

]]>
pyc文件结构 2018-01-21T00:00:00+08:00 Vancir http://localhost:4000/2018/01/21/pyc-structure

原文链接: The structure of .pyc files

简单来说, 一个pyc文件包含以下三块

  • 一个4字节的魔数(magic number)
  • 一个4直接的修改时间戳(modification timestamp)
  • 一个编排过的代码对象

对于各个版本的python解释器, magic number都各不相同, 对于python 2.5则是b3f20d0a

修改时间戳则是源文件生成.pyc文件的Unix修改时间戳, 当源文件改变的时候, 该值也会变化

整个文件剩下的部分则是在编译源文件产生的代码对象编排后的输出. marshal跟python的pickle类似, 它对python对象进行序列化操作. 不过marshal和pickle的目标不同. pickle目的在于产生一个持久的独立于版本的序列化, 而marshal则是为了短暂地序列化对象, 因此它的表示会随着python版本二改变.

而且, pickle被设计用于适用用户定义的类型, 而marshal这时用于处理python内部类型的复杂结构

marshal的特性给出了pyc文件的重要特征: 它对平台独立, 但依赖于python版本. 一个2.4版本的pyc文件不能在2.5版本下执行, 但是它可以很好地移植到不同操作系统里.

接下来的部分也简单: 对于两个长整数和一个marshalled的代码对象, 复杂点在于代码对象的结构. 它们包含有编译器禅师的各种信息, 其中内容最丰富的这是字节码本身.

所幸有了marshal和dis模块, 编写程序导出这些信息并不会很难.

import dis, marshal, struct, sys, time, types

def show_file(fname):
    f = open(fname, "rb")
    magic = f.read(4)
    moddate = f.read(4)
    modtime = time.asctime(time.localtime(struct.unpack('L', moddate)[0]))
    print "magic %s" % (magic.encode('hex'))
    print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
    code = marshal.load(f)
    show_code(code)
     
def show_code(code, indent=''):
    print "%scode" % indent
    indent += '   '
    print "%sargcount %d" % (indent, code.co_argcount)
    print "%snlocals %d" % (indent, code.co_nlocals)
    print "%sstacksize %d" % (indent, code.co_stacksize)
    print "%sflags %04x" % (indent, code.co_flags)
    show_hex("code", code.co_code, indent=indent)
    dis.disassemble(code)
    print "%sconsts" % indent
    for const in code.co_consts:
        if type(const) == types.CodeType:
            show_code(const, indent+'   ')
        else:
            print "   %s%r" % (indent, const)
    print "%snames %r" % (indent, code.co_names)
    print "%svarnames %r" % (indent, code.co_varnames)
    print "%sfreevars %r" % (indent, code.co_freevars)
    print "%scellvars %r" % (indent, code.co_cellvars)
    print "%sfilename %r" % (indent, code.co_filename)
    print "%sname %r" % (indent, code.co_name)
    print "%sfirstlineno %d" % (indent, code.co_firstlineno)
    show_hex("lnotab", code.co_lnotab, indent=indent)
     
def show_hex(label, h, indent):
    h = h.encode('hex')
    if len(h) < 60:
        print "%s%s %s" % (indent, label, h)
    else:
        print "%s%s" % (indent, label)
        for i in range(0, len(h), 60):
            print "%s   %s" % (indent, h[i:i+60])

show_file(sys.argv[1])

我们使用这段代码, 来处理一个极简单的python文件生成的pyc文件:

a, b = 1, 0
if a or b:
    print "Hello", a

产生的结果如下:

magic b3f20d0a
moddate 8a9efc47 (Wed Apr 09 06:46:34 2008)
code
   argcount 0
   nlocals 0
   stacksize 2
   flags 0040
   code
      6404005c02005a00005a0100650000700700016501006f0d000164020047
      65000047486e01000164030053
  1           0 LOAD_CONST               4 ((1, 0))
              3 UNPACK_SEQUENCE          2
              6 STORE_NAME               0 (a)
              9 STORE_NAME               1 (b)

  2          12 LOAD_NAME                0 (a)
             15 JUMP_IF_TRUE             7 (to 25)
             18 POP_TOP
             19 LOAD_NAME                1 (b)
             22 JUMP_IF_FALSE           13 (to 38)
        >>   25 POP_TOP

  3          26 LOAD_CONST               2 ('Hello')
             29 PRINT_ITEM
             30 LOAD_NAME                0 (a)
             33 PRINT_ITEM
             34 PRINT_NEWLINE
             35 JUMP_FORWARD             1 (to 39)
        >>   38 POP_TOP
        >>   39 LOAD_CONST               3 (None)
             42 RETURN_VALUE
   consts
      1
      0
      'Hello'
      None
      (1, 0)
   names ('a', 'b')
   varnames ()
   freevars ()
   cellvars ()
   filename 'C:\\ned\\sample.py'
   name '<module>'
   firstlineno 1
   lnotab 0c010e01

有很多内容我们都不明白, 但是字节码却很好地被反汇编并呈现了出来. python虚拟机是一个面向栈的解释器, 因此有许多操作都是load和pop, 并且当然也有很多jump和条件判断. 字节码的解释器则是在ceval.c中实现, 对于字节码的具体改变则会依赖于python的主版本. 比如PRINT_ITEM PRINT_NEWLINE在python 3中则被去掉了.

在反汇编的输出中, 最左的数字(1,2,3)是源文件中的行号, 而接下来的数字(0,3,6,9)这是指令中的字节偏移. 指令的操作数这是直接用数字呈现, 而在括号内的这是符号性的解释. >>所表示的行实际上是代码中某处跳转指令的目标地址.

我们这个样例非常简单, 它只是一个模块中单一的代码对象中的指令流程. 在现实中有着类和函数定义的模块会十分复杂. 那些类和函数本身就是const列表中的代码对象, 在模块中进行了足够深的嵌套.

一旦你开始在这个级别挖掘, 会发现有各种各样适用于代码对象的工具. 在标准库中有内置的compile函数, 以及compiler, codeopopcode模块. 在真实场景中会有很多第三方包比如codewalk, byteplaybytecodehacks等. PEP 339给出了有关编译和操作码的更多详细信息. 最后

]]>