为Clang Static Analyzer创建插件以搜索整数溢出


文章作者: 0x64rem


参赛作品


一年半以前,我有实现我的相位器的想法,这是大学论文的一部分。 我开始研究有关控制流程图,数据流程图,符号执行等的材料。 接下来是搜索工具,这是不同库(Angr,Triton,Pin,Z3)的示例。 最后没有什么具体的事情发生,直到今年夏天我参加了Digital Security的Summer of Hack 2019计划,在那里为我提供了Clang Static Analyzer的扩展作为该项目的主题。 在我看来,该主题将帮助我将理论知识摆在架子上,开始执行重要的工作,并从经验丰富的导师那里获得建议。 接下来,我将告诉您编写插件的过程如何进行,并描述实习期间的想法。


lang静态分析仪


对于开发,Clang提供了三个用于交互的界面选项:


  • LibClang是高级C接口,允许您与AST交互,但不能完全交互。 如果您需要与另一种语言(例如, bindings的实现)或稳定的接口进行交互,则是一个不错的选择。
  • Clang插件 -编译时调用的动态库。 允许您完全操纵AST。
  • LibTooling-一个用于基于Clang创建单独工具的库。 还提供了与AST交互的完全访问权限。 生成的代码可以在已检查项目的构建环境之外运行。

由于我们将扩展Clang Static Analyzer的功能,因此我们选择插件的实现。 您可以使用C ++或Python为该插件编写代码。


对于后者,可以使用绑定器来解析源代码,在生成的抽象语法树的节点上进行迭代,还可以访问节点的属性,并且可以将节点映射到源代码行。 这样的组合适合于简单的检查器。 有关更多详细信息,请参见llvm存储库


我的任务需要对代码进行详细的分析,因此选择了C ++进行开发。 接下来是对该工具的介绍。


Clang Staic Analyzer (以下称为 CSA)是用于基于符号执行对C / C ++ / Objective-C代码进行静态分析的工具。 可以通过在Clang前端中将-cc1和-analyze标志添加到build命令中来调用分析器,也可以通过单独的scan-build binar来调用分析器。 除了分析本身之外,CSA还可以生成可视的html报告。


# ,      clang' clang -cc1 --help #  CSA  №1 clang++ -cc1 -x c++ -load path/to/Checker.so -analyze -analyzer-checker=test.Me -analyzer-config $BUILD_OPTIONS Checker.cpp 

  #  CSA  №2 scan-build -load-plugin path/to/Checker.so -enable-checker test.Me $BUILD_COMMAND 

  #       DivideZero clang++ -cc1 -analyze -analyzer-checker=core.DivideZero -o reports div-by-zero-test.cpp 


CSA具有出色的库,可以使用AST(抽象语法树),CFG(控制流图)来解析源代码。 从结构中,您可以进一步了解变量的声明,变量的类型,二进制和一元运算符的使用,可以获得符号表达式等。 我的插件将使用AST类的功能,此选择将​​进一步合理。 以下是在插件的实现中使用的类的列表,该列表将帮助您初步了解CSA的功能:


  • Stmt-这包括二进制操作。


  • Decl-变量声明。


  • Expr-存储表达式的左,右部分及其类型。


  • ASTContext-有关树(当前节点)的信息。


  • 源管理器-有关与树的一部分相对应的实际代码的信息。


  • RecursiveASTVisitor,ASTMatcher-用于遍历树的类。


    我重复一遍,CSA为开发人员提供了详细检查代码结构的机会,并且上面列出的类只是可用代码的一小部分。 如果您不知道如何提取任何数据,我绝对建议您查看Clang版本的文档 。 很可能已经写了一些合适的东西。



整数溢出搜索


要开始实施插件,您需要选择它将解决的任务。 对于这种情况,llvm网站提供了潜在检查程序的列表 ;您还可以修改现有的稳定检查程序或alpha检查程序。 在检查可用检查器的代码期间,很明显,为了更成功地开发libclang,最好从头开始编写检查器,因此从一系列未实现的想法中进行选择 。 结果,选择了该选项以创建用于整数溢出检测的检查器。 Clang已经具有防止此漏洞的功能(指示使用了-ftrapv,-fwrapv等标志),它已内置在编译器中,并且这种精疲力尽被灌入警告中,因此在这里并不经常出现。 仍然存在UBSan ,但是它们是消毒剂,并不是每个人都使用它们,并且该方法用于在运行时识别问题,并且CSA插件在编译时起作用,分析源。


接下来是有关所选漏洞的资料集合。 整数溢出曾经很简单,但并不严重。 实际上,该漏洞很有趣,并可能产生令人印象深刻的后果。
整数溢出是一种漏洞,可能导致代码中的整数类型数据采用意外值。 上溢-如果变量变得比预期的大,则下溢-小于其原始类型。 可能由于程序员和编译器而出现此类错误。


在C ++中,在算术比较操作期间,将整数值转换为相同的类型,就位深度而言,更常见的是转换为较大的值。 这些鬼影无处不在,并且不断出现,它们可以是显性的或隐性的。 有几种发生鬼影的规则[1]:


  • 从带符号转换为带符号但更大的类型:只需添加高位。
  • 将有符号整数转换为相同容量的无符号整数:负数将转换为正数,并具有新的含义。 DirectVE中类似错误的一个示例是CVE-2014-2977
  • 将有符号整数转换为具有更大位容量的无符号整数:首先,位容量将扩展,然后,如果数字为负,则将错误地更改该值。 例如:0xff(-1)变为0xffffffff。
  • 一个无符号整数,其符号具有相同的位容量:数字可以更改值,具体取决于高位的值。
  • 一个无符号整数,其整数带有更大的符号:首先,一个无符号数字的容量增加,然后转换为有符号的整数。
  • 下转换:位只是被截断。 这会使无符号值变为负数,依此类推。 PHP中此类漏洞的一个示例。

即 漏洞的触发因素可能是优化过程中程序员或编译器引起的用户输入不安全,算术不正确,类型转换不正确。 当一段代码对一个版本的编译器无害,但随着新优化算法的发布,“爆炸”并导致意外行为时,也可以使用定时炸弹选项。 在历史上,SafeInt类已经出现了这种情况(非常具有讽刺意味)[5,6.5.2]。


整数溢出会打开一个广泛的向量:有可能迫使执行采用不同的路径(如果溢出影响条件语句),则导致缓冲区溢出。 为了清楚起见,您可以熟悉特定的CVE,并查看其原因和后果。 自然,最好在开源产品中查找整数溢出,这样您不仅可以阅读说明,还可以看到代码。


  • CVE-2019-3560 -Fizz中的整数溢出(一个为Facebook实现TLS的项目)可以利用局促的网络数据包利用此漏洞进行DoS攻击。
  • CVE-2018-14618-由于密码长度,整数溢出导致Curl中的缓冲区溢出。
  • CVE-2018-6092-在32位系统上,Chrome的WebAssembly中的漏洞允许通过特殊的HTML页面实现RCE。

为了避免重新发明轮子,考虑了在CppCheck静态分析器中检测整数溢出的代码。 他的方法如下:


  1. 确定表达式是否为二进制运算符。
  2. 如果是,则检查两个参数是否均为整数类型。
  3. 确定类型的大小。
  4. 通过计算检查该值是否可以超过其最大或最小限制。
    但是在这一阶段,它还没有给出明确的说明。 事实证明,有许多不同的故事,因此,信息的系统化变得更加困难。 一切都放在了CWE的列表中。 总共,站点上分配了9种类型的整数溢出:
    • 190-整数oveflow
    • 191-整数下溢
    • 192-整数强制错误
    • 193-一对一
    • 194-意外的标志扩展
    • 195-有符号到无符号转换错误
    • 196-未签名到已签名的转换错误
    • 197-数值截断错误
    • 198-使用不正确的字节顺序

我们考虑每个选项的原因,并理解溢出会发生在不正确的显式/隐式强制转换的情况下。 而且因为 任何类型转换都显示在抽象语法树的结构中,我们将使用AST进行分析。 在下图(图3)中,可以看到导致树中进行强制转换的任何操作都是一个单独的节点,并且在树中四处游荡,我们可以基于具有可能导致错误的转换的表检查所有类型转换。


标志g标志l签到取消签名取消签名取消签名
签收+--+------
取消签名+--------+


更具体地说,该算法听起来像这样:我们遍历Casts并查看IntegralCast(整数转换)。 如果找到合适的节点,请查看其后代以查找二进制运算或Decl(变量声明)。 在第一种情况下,您需要检查二进制操作使用的符号和位深度。 在第二种情况下,仅比较声明的类型。


检查器实施


让我们开始实施。 我们需要检查程序的框架,该框架可以是独立的库,也可以作为Clang的一部分进行组装。 在代码中,差异将很小。 如果您已经打算编写自己的插件,我建议您立即阅读一个小pdf文件: “ Clang静态分析器:Checker开发人员指南” ,尽管那里可能不再有用,但基本内容已在此处进行了描述,该库会定期更新,但是您可以立即抓住。


如果要将检查器添加到您的clang程序集,则需要:


  1. 用大约以下内容编写检查器本身:


     namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { //       string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void ento::registerSuperChecker(CheckerManager &mgr) { auto checker = mgr.registerChecker<SuperChecker>(); //       ,   4    //       ,  stand-alone    . AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); SuperChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } 

  2. 然后,在Clang的源代码中,您将需要更改文件CMakeLists.txtCheckers.td 。 住在这里${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
    ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
    在第一个中,您只需要在文件名中添加代码,在第二个中,您需要添加结构性描述:


      #Checkers.td def SuperChecker : Checker<"SuperChecker">, HelpText<"test checker">, Documentation<HasDocumentation>; 


如果不清楚,那么在Checkers.td文件中,有足够的示例说明如何执行操作。


最有可能的是您不想重建Clang,您将在库程序集(so / dll)中使用该选项。 然后在检查器的代码中应该是这样的:


 namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void initializationFunction(CheckerManager &mgr){ SuperChecker *checker = mgr.registerChecker<SuperChecker>(); //       ,   4    AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); TestChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } extern "C" void clang_registerCheckers (CheckerRegistry &registry) { registry.addChecker(&initializationFunction, "test.Me", "SuperChecker description", "doc_link"); } extern "C" const char clang_analyzerAPIVersionString [] = "8.0.1"; 

接下来,收集您的代码,您可以编写自己的汇编脚本,但是如果对此有任何问题(如作者所遇到的问题:)),则可以在clang源代码中使用Makefile并以一种奇怪的方式创建clangStaticAnalyzerCheckers命令。


接下来,调用检查器:


  • 用于内置检查器


     clang++ -cc1 -analyze -analyzer-checker=core.DivideZero test.cpp 

  • 对于外部


     clang++ -cc1 -load ${PATH_TO_CHECKER}/SuperChecker.so -analyze -analyzer-checker=test.Me -analyzer-config test.Me:UsrInp1="foo" test.Me:Inp1="bar" -analyzer-config test.Me:Inp2=123 test.cpp 

    在这一阶段,我们已经有了某种结果(图4),但是编写的代码只能检测到潜在的溢出。 这意味着大量误报。




要解决此问题,我们可以:


  • 来回浏览该图并检查变量的特定值,以防可能发生溢出。
  • 在AST遍历期间,立即保存变量的特定值并在必要时检查它们。
  • 使用污点分析。

为了证实更多的论据,值得一提的是,在分析Clang时, #include指令中指定的所有文件也会解析,结果,最终AST的大小会增加。 结果,在建议的选项中,只有一项对于特定任务是合理的:


  • 首先,它需要很多时间才能完成。 走进一棵树,搜索并计算所需的一切将花费很长时间,使用此类代码来分析大型项目可能会变得困难。 要在代码中遍历树,我们将使用clang::RecursiveASTVisitor 该类执行递归深度搜索。 这种方法的时间估计为 ,其中V是顶点集合,E是图形边的集合。
  • 第二个-您当然可以存储,但是我们不知道我们将需要什么,不需要什么。 此外,我们在分析中使用的树结构本身需要大量内存,因此在其他资源上花费此类资源是一个坏主意。
  • 第三是一个好主意,对于这种方法,您可以找到足够的研究和示例。 但是在CSA中,没有现成的污点。 有一个检查器 ,该检查器后来被添加到源代码中的alpha检查器列表(alpha.security.tai​​nt.TaintPropagation)中,在文件GenericTaintChecker.cpp中进行了描述。 该检查器很好,但是仅适用于C中已知的不安全I / O函数,它仅“标记”变量,这些变量是危险函数的参数或结果。 除了描述的选项外,还需要考虑全局变量,类字段等,以便正确还原“分布”模型。

实习的剩余时间花费在阅读GenericTaintChecker.cpp并尝试重新制作以满足您的需求。 到学期末,它并没有成功解决问题,但是它仍然是一项改进任务,已经超出了DSec的培训范围。 同样在开发过程中,很明显,识别危险功能是一项单独的任务,项目中并非总是危险位置来自某些标准功能,因此在检查器中添加了一个标志,以指示将被视为“中毒” /“标记”的功能列表在污染分析中。
此外,添加了检查以确定该变量是否为位字段。 通过标准的CSA工具,大小由类型确定,如果我们使用位字段,则其大小将具有整个字段的位类型的值,而不是变量声明中指定的位数。


结果如何?


目前,已经实现了一个简单的检查器,该检查器仅可以警告潜在的整数溢出。 改进的污点分析类,仍有很多工作要做。 之后,您需要使用SMT来确定溢出。 为此,Z3 SMT求解器是合适的,它已在5.0.0版中添加到了Clang组件中(根据发行说明判断)。 要使用求解器,必须使用CLANG_ANALYZER_BUILD_Z3=ON选项构建Clang,并且当直接调用CSA插件时,将-Xanalyzer -analyzer-constraints=z3标志。


GitHub结果存储库


参考文献:


  1. Howard M.,Leblanc D.,Viega J.“计算机安全的24个罪过”


  2. 如何在24小时内编写检查程序


  3. Clang静态分析器:Checker开发人员指南


  4. CSA检查器开发手册


  5. Dietz W.等。 了解C / C ++中的整数溢出


Source: https://habr.com/ru/post/zh-CN473412/


All Articles