程序分析 clang系列学习 (三)

基于CSA的污点分析

  • 污点分析
  • 基于CSA的实现
    • 操作符集合的定义
      • Sources和Propagators
      • Sinks
      • Filters
    • Checker执行过程
  • 总结
  • 参考文献

污点分析

污点分析是数据流分析的一个特例(taint analysis is a special case of data flow analysis.)涉及到的一些概念如下:

  • x x x y y y 的数据流记为 x → y x \rightarrow y xy,任何引起数据拷贝的操作(operation),比如赋值参数传递都会造成数据流动。

  • 来自未知或不受信任的数据源的数据是污点数据(tainted data)。

  • 一个操作 o p op op 如果从污点变量 v v v 返回结果 r r r(记作 v → o p ( r ) v \rightarrow op(r) vop(r)),那么 o p op op 就会被认为是污点操作符(taint operator)。同时, r r r 也会被认为是污点变量。从这里可以推出,污点变量具有传递性,如果 x → o p ( y ) x \rightarrow op(y) xop(y) y → o p ( z ) y \rightarrow op(z) yop(z),那么 x → z x \rightarrow z xz

  • 污点操作符可以作为sources生成污点数据或者作为propagator污点数据。

  • 不是所有的操作符都是污点操作符,有的操作符可以从污点数据中生成非污点数据,这些操作符被称之为sanitizers或者filters

因此污点分析可以被视为一个通用的API定义,包含四组操作:

  • Sources是一组可以生成污点数据的操作。

  • Propagators是在污点数据中传播或转换污点数据的一组操作。任何作为赋值或参数传递的编程语言原语操作都被视为Propagators。如果编程语言使用引用或指针,则应考虑别名

  • Filters是一组检查数据是否安全或从受污染的数据生成安全数据的操作。

  • Sinks是使用数据的关键操作集。

污点分析的状态转换系统可定义为 L T S t = < τ , O , Q , S , T , E > LTS_t = <\tau,O, Q, S, T, E> LTSt=<τ,O,Q,S,T,E>,其中:

  • τ \tau τ 是一组感兴趣的值和变量类型。

  • ∑ = S o ∪ P ∪ F ∪ S i \sum = S_o \cup P \cup F \cup S_i =SoPFSi 是操作符集合。 S o S_o So 是Sources集合、 P P P 是Propagators集合、 F F F 是Filters集合、 S i S_i Si 是Sinks集合。

  • S = { t a i n t e d , u n t a i n t e d , e r r o r } S = \{tainted, untainted, error\} S={tainted,untainted,error} 是状态集合。

  • T T T 是状态转移函数。

    • T ( , o p ) = t a i n t e d T( _ , op) = tainted T(,op)=tainted 如果 o p ∈ S o op \in S_o opSo
    • T ( s , o p ) = s T(s, op) = s T(s,op)=s 如果 o p ∈ P ( s ∈ Q ) op \in P (s \in Q) opP(sQ)
    • T ( , o p ) = u n t a i n t e d T( _ , op) = untainted T(,op)=untainted 如果 o p ∈ F op \in F opF
    • T ( t a i n t e d , o p ) = e r r o r T(tainted , op) = error T(tainted,op)=error 如果 o p ∈ S i op \in S_i opSi
    • T ( e r r o r , ) = e r r o r T(error, _) = error T(error,)=error
  • E = { e r r o r } E = \{error\} E={error}

状态转换图如下图所示:

source
propagator
filter
source
sink
*
start
Non
tainted
untainted
error

其中 S o , P , F , S i S_o, P, F, S_i So,P,F,Si 中的操作符需要用户自己定义。

基于CSA的实现

论文代码github地址。但是这个project好像是基于llvm 4.0的,对于最新的llvm版本会出现编译错误。而llvm 12.0版本的已经集成到了clang中,相关代码在GenericTaintChecker.cpp类中。这个checker有预先定义的Sources、Sinks、Propagators集合,也支持用户自定义的Sources、Sinks、Propagators。但是Filters貌似必须用户自定义。

关于CSA Checker我就不详细介绍了,这里主要说下GenericTaintChecker。首先它关注2个event。

  • checkPreCall:处理函数调用之前的程序点。

  • checkPostCall:处理函数调用之后的程序点,定义污点传播操作。

代码里的污点分析主要应用在下面类别的漏洞检测任务上

  • 格式化字符串漏洞(CWE-134)

  • 命令注入漏洞(CWE-78)

  • 缓冲区大小(缓冲区大小可能受到用户输入影响,这里并不检测缓冲区溢出)

操作符集合的定义

Sources和Propagators

这里的污点分析所有的操作符都是函数调用操作,没有赋值也没有类成员函数。GenericTaintChecker 中定义了 TaintPropagationRule 类,这个类定义了一些污点传播的Rule,每个TaintPropagationRule 对象对应一个API(scanf, fgets 等等)的传播规则,它主要的成员变量有

	  // 源参数集合SrcArgs是一个unsigned int vector,每一个元素的值表示可能会引入污点的参数索引
	  // 举例来说,当SrcArgs = {0, 1} 时。表示该函数第0个和第1个参数可能会引入污点,并且只有
	  // 在这2个参数都被污染的情况下目标参数集合中的变量才会被污染。
    ArgVector SrcArgs;
    /// 目标参数集合,函数调用结束后,可能会成为污点变量的参数集合,值表示函数参数索引或者返回值。
    ArgVector DstArgs;
    /// 第一个可变参数索引(只有在有可变参数起效)
    unsigned VariadicIndex;
    /// 函数中的可变参数作为源参数集合或者目标参数集合或者都不是。
    VariadicType VarType;
    /// 特殊的函数指针,这里只有socket函数对应的Rule定义了这个函数指针,指向的时postSocket函数
    PropagationFuncType PropagationFunc;

read 函数(原型 int read(int handle,void *buf,int len);)举例,其对应的rule中 SrcArgs{0, 2}DstArgs{1, ReturnValueIndex}。在处理的时候只有第0个参数 handle 和第2个参数 len 均为污点变量的时候,checker才会将第1个参数 buf 和 返回值均标记为污点变量。

它的构造函数主要用到的是

TaintPropagationRule(ArgVector &&Src, ArgVector &&Dst,
                         VariadicType Var = VariadicType::None,
                         unsigned VarIndex = InvalidArgIndex,
                         PropagationFuncType Func = nullptr)
        : SrcArgs(std::move(Src)), DstArgs(std::move(Dst)),
          VariadicIndex(VarIndex), VarType(Var), PropagationFunc(Func) {}

Sources和Propagators对应的API列表的定义在 TaintPropagationRule 类的 getTaintPropagationRule 方法中。代码如下:

GenericTaintChecker::TaintPropagationRule
GenericTaintChecker::TaintPropagationRule::getTaintPropagationRule(
    const NameRuleMap &CustomPropagations, const FunctionData &FData,
    CheckerContext &C) {
  // TODO: Currently, we might lose precision here: we always mark a return
  // value as tainted even if it's just a pointer, pointing to tainted data.

  // Check for exact name match for functions without builtin substitutes.
  // Use qualified name, because these are C functions without namespace.
  TaintPropagationRule Rule =
      llvm::StringSwitch<TaintPropagationRule>(FData.FullName)
          // Source functions
          // TODO: Add support for vfscanf & family.
          .Case("fdopen", {{}, {ReturnValueIndex}})
          .Case("fopen", {{}, {ReturnValueIndex}})
          .Case("freopen", {{}, {ReturnValueIndex}})
          .Case("getch", {{}, {ReturnValueIndex}})
          .Case("getchar", {{}, {ReturnValueIndex}})
          .Case("getchar_unlocked", {{}, {ReturnValueIndex}})
          .Case("getenv", {{}, {ReturnValueIndex}})
          .Case("gets", {{}, {0, ReturnValueIndex}})
          .Case("scanf", {{}, {}, VariadicType::Dst, 1})
          .Case("socket", {{},
                           {ReturnValueIndex},
                           VariadicType::None,
                           InvalidArgIndex,
                           &TaintPropagationRule::postSocket})
          .Case("wgetch", {{}, {ReturnValueIndex}})
          // Propagating functions
          .Case("atoi", {{0}, {ReturnValueIndex}})
          .Case("atol", {{0}, {ReturnValueIndex}})
          .Case("atoll", {{0}, {ReturnValueIndex}})
          .Case("fgetc", {{0}, {ReturnValueIndex}})
          .Case("fgetln", {{0}, {ReturnValueIndex}})
          .Case("fgets", {{2}, {0, ReturnValueIndex}})
          .Case("fscanf", {{0}, {}, VariadicType::Dst, 2})
          .Case("sscanf", {{0}, {}, VariadicType::Dst, 2})
          .Case("getc", {{0}, {ReturnValueIndex}})
          .Case("getc_unlocked", {{0}, {ReturnValueIndex}})
          .Case("getdelim", {{3}, {0}})
          .Case("getline", {{2}, {0}})
          .Case("getw", {{0}, {ReturnValueIndex}})
          .Case("pread", {{0, 1, 2, 3}, {1, ReturnValueIndex}})
          .Case("read", {{0, 2}, {1, ReturnValueIndex}})
          .Case("strchr", {{0}, {ReturnValueIndex}})
          .Case("strrchr", {{0}, {ReturnValueIndex}})
          .Case("tolower", {{0}, {ReturnValueIndex}})
          .Case("toupper", {{0}, {ReturnValueIndex}})
          .Default({});

  if (!Rule.isNull())
    return Rule;
  assert(FData.FDecl);

  // Check if it's one of the memory setting/copying functions.
  // This check is specialized but faster then calling isCLibraryFunction.
  const FunctionDecl *FDecl = FData.FDecl;
  unsigned BId = 0;
  if ((BId = FDecl->getMemoryFunctionKind())) {
    switch (BId) {
    case Builtin::BImemcpy:
    case Builtin::BImemmove:
    case Builtin::BIstrncpy:
    case Builtin::BIstrncat:
      return {{1, 2}, {0, ReturnValueIndex}};
    case Builtin::BIstrlcpy:
    case Builtin::BIstrlcat:
      return {{1, 2}, {0}};
    case Builtin::BIstrndup:
      return {{0, 1}, {ReturnValueIndex}};

    default:
      break;
    }
  }

  // Process all other functions which could be defined as builtins.
  if (Rule.isNull()) {
    const auto OneOf = [FDecl](const auto &... Name) {
      // FIXME: use fold expression in C++17
      using unused = int[];
      bool ret = false;
      static_cast<void>(unused{
          0, (ret |= CheckerContext::isCLibraryFunction(FDecl, Name), 0)...});
      return ret;
    };
    if (OneOf("snprintf"))
      return {{1}, {0, ReturnValueIndex}, VariadicType::Src, 3};
    if (OneOf("sprintf"))
      return {{}, {0, ReturnValueIndex}, VariadicType::Src, 2};
    if (OneOf("strcpy", "stpcpy", "strcat"))
      return {{1}, {0, ReturnValueIndex}};
    if (OneOf("bcopy"))
      return {{0, 2}, {1}};
    if (OneOf("strdup", "strdupa", "wcsdup"))
      return {{0}, {ReturnValueIndex}};
  }

  // Skipping the following functions, since they might be used for cleansing or
  // smart memory copy:
  // - memccpy - copying until hitting a special character.

  auto It = findFunctionInConfig(CustomPropagations, FData);
  if (It != CustomPropagations.end())
    return It->second.second;
  return {};
}

getTaintPropagationRule 方法中凡是构造方法第一个参数为空列表的都是Sources函数,比如

.Case("fdopen", {{}, {ReturnValueIndex}})
.Case("fopen", {{}, {ReturnValueIndex}})
.Case("freopen", {{}, {ReturnValueIndex}})
.Case("getch", {{}, {ReturnValueIndex}})

这些函数(比如 fdopen)的返回值统统会被标记为污点,不论参数是什么。

而像下面例子这样的构造就属于Propagator,getline 函数只有在第2个参数被污点影响的前提下其第0个参数会成为污点变量。

.Case("getline", {{2}, {0}})

Sinks

这里作者做污点分析主要分析上面提到的3种漏洞检测。通过 checkPre 实现。

bool GenericTaintChecker::checkPre(const CallEvent &Call,
                                   const FunctionData &FData,
                                   CheckerContext &C) const {
  if (checkUncontrolledFormatString(Call, C))
    return true;

  if (checkSystemCall(Call, FData.Name, C))
    return true;

  if (checkTaintedBufferSize(Call, C))
    return true;

  return checkCustomSinks(Call, FData, C);
}

checkUncontrolledFormatString 检测格式化字符串,checkSystemCall 检测系统命令注入,checkTaintedBufferSize 检测缓冲区长度,checkCustomSinks 检测自定义的函数调用问题, checkSystemCall 函数如下

bool GenericTaintChecker::checkSystemCall(const CallEvent &Call, StringRef Name,
                                          CheckerContext &C) const {
  // TODO: It might make sense to run this check on demand. In some cases,
  // we should check if the environment has been cleansed here. We also might
  // need to know if the user was reset before these calls(seteuid).
  unsigned ArgNum = llvm::StringSwitch<unsigned>(Name)
                        .Case("system", 0)
                        .Case("popen", 0)
                        .Case("execl", 0)
                        .Case("execle", 0)
                        .Case("execlp", 0)
                        .Case("execv", 0)
                        .Case("execvp", 0)
                        .Case("execvP", 0)
                        .Case("execve", 0)
                        .Case("dlopen", 0)
                        .Default(InvalidArgIndex);

  if (ArgNum == InvalidArgIndex || Call.getNumArgs() < (ArgNum + 1))
    return false;

  return generateReportIfTainted(Call.getArgExpr(ArgNum), MsgSanitizeSystemArgs,
                                 C);
}

大概意思是如果函数调用的是列表中的函数,检测其第0个参数是否受污点影响,影响的话报错。

checkTaintedBufferSize 函数如下:

bool GenericTaintChecker::checkTaintedBufferSize(const CallEvent &Call,
                                                 CheckerContext &C) const {
  const auto *FDecl = Call.getDecl()->getAsFunction();
  // If the function has a buffer size argument, set ArgNum.
  unsigned ArgNum = InvalidArgIndex;
  unsigned BId = 0;
  if ((BId = FDecl->getMemoryFunctionKind())) {
    switch (BId) {
    case Builtin::BImemcpy:
    case Builtin::BImemmove:
    case Builtin::BIstrncpy:
      ArgNum = 2;
      break;
    case Builtin::BIstrndup:
      ArgNum = 1;
      break;
    default:
      break;
    }
  }

  if (ArgNum == InvalidArgIndex) {
    using CCtx = CheckerContext;
    if (CCtx::isCLibraryFunction(FDecl, "malloc") ||
        CCtx::isCLibraryFunction(FDecl, "calloc") ||
        CCtx::isCLibraryFunction(FDecl, "alloca"))
      ArgNum = 0;
    else if (CCtx::isCLibraryFunction(FDecl, "memccpy"))
      ArgNum = 3;
    else if (CCtx::isCLibraryFunction(FDecl, "realloc"))
      ArgNum = 1;
    else if (CCtx::isCLibraryFunction(FDecl, "bcopy"))
      ArgNum = 2;
  }

  return ArgNum != InvalidArgIndex && Call.getNumArgs() > ArgNum &&
         generateReportIfTainted(Call.getArgExpr(ArgNum), MsgTaintedBufferSize,
                                 C);
}

这里并不是检测缓冲区溢出漏洞,而是检测这些拷贝类函数长度参数(memcpy 第2个参数指定复制长度)是否受污点影响,影响的话报错。

Filters

这里代码里没有明确的filter函数,需要导入用户自定义的。

Checker执行过程

checker的主要关注点就是函数调用,因此这里不会处理非函数调用语句。所以主要的执行过程就是 checkPreCallcheckPostCall

checkPreCall 的代码:

void GenericTaintChecker::checkPreCall(const CallEvent &Call,
                                       CheckerContext &C) const {
  Optional<FunctionData> FData = FunctionData::create(Call, C);
  if (!FData)
    return;

  // Check for taintedness related errors first: system call, uncontrolled
  // format string, tainted buffer size.
  if (checkPre(Call, *FData, C))
    return;

  // Marks the function's arguments and/or return value tainted if it present in
  // the list.
  if (addSourcesPre(Call, *FData, C))
    return;

  addFiltersPre(Call, *FData, C);
}
  • checkPre 用来对 printf 输出类,execv 命令执行类,memcpy 类缓冲区拷贝函数进行sink检测,如果出现sink的情况报错并返回。

  • addSourcesPre 用来预处理当前函数调用,当 SrcArgs 集合对应的参数均受污点影响时,将 DstArgs 集合中所有的索引值添加到全局集合 TaintArgsOnPostVisit 中,这么处理时因为 checkPreCall不能获取函数的返回值,必须在 checkPostCall 中获取。

  • addFiltersPre 用来根据用户自定义的Filter净化参数,不过这里没有预先定义好的没我就没有深究了。

checkPostCall 的代码如下

void GenericTaintChecker::checkPostCall(const CallEvent &Call,
                                        CheckerContext &C) const {
  // Set the marked values as tainted. The return value only accessible from
  // checkPostStmt.
  propagateFromPre(Call, C);
}
  • propagateFromPre 将全局集合 TaintArgsOnPostVisit 对应的参数变量设置为污点变量并清空 TaintArgsOnPostVisit。实现污点传播。

因此该checker的检测规则可以简单总结为:

  • 对于sink类API(输出类、命令执行类、缓冲区读写类),只执行 checkPre 便退出。

  • 对于propagator类和source类API(赋值,输入读写),执行了 addFiltersPrepropagateFromPre

  • 对于filter类API,执行了 addFiltersPre

总结

这里作者基于CSA的API实现了一个简单的污点分析工具,不过只能分析跟函数调用有关的污点传播,并且filter需要用户自己定义。其实许多条件判断语句多多少少充当了filter的角色,这些判断语句多多少少应该被考虑在内。

本人水平有限,欢迎大佬们来补充指正。

参考文献

Arroyo M , Chiotta F , Bavera F . An user configurable clang static analyzer taint checker[C]// Computer Science Society. IEEE, 2017.

你可能感兴趣的:(静态代码检测,程序分析,程序分析工具,安全,程序分析)