污点分析是数据流分析的一个特例(taint analysis is a special case of data flow analysis.)涉及到的一些概念如下:
从 x x x 到 y y y 的数据流记为 x → y x \rightarrow y x→y,任何引起数据拷贝的操作(operation),比如赋值、参数传递都会造成数据流动。
来自未知或不受信任的数据源的数据是污点数据(tainted data)。
一个操作 o p op op 如果从污点变量 v v v 返回结果 r r r(记作 v → o p ( r ) v \rightarrow op(r) v→op(r)),那么 o p op op 就会被认为是污点操作符(taint operator)。同时, r r r 也会被认为是污点变量。从这里可以推出,污点变量具有传递性,如果 x → o p ( y ) x \rightarrow op(y) x→op(y)、 y → o p ( z ) y \rightarrow op(z) y→op(z),那么 x → z x \rightarrow z x→z。
污点操作符可以作为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 ∑=So∪P∪F∪Si 是操作符集合。 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 是状态转移函数。
E = { e r r o r } E = \{error\} E={error}
状态转换图如下图所示:
其中 S o , P , F , S i S_o, P, F, S_i So,P,F,Si 中的操作符需要用户自己定义。
论文代码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)
缓冲区大小(缓冲区大小可能受到用户输入影响,这里并不检测缓冲区溢出)
这里的污点分析所有的操作符都是函数调用操作,没有赋值也没有类成员函数。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}})
这里作者做污点分析主要分析上面提到的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个参数指定复制长度)是否受污点影响,影响的话报错。
这里代码里没有明确的filter函数,需要导入用户自定义的。
checker的主要关注点就是函数调用,因此这里不会处理非函数调用语句。所以主要的执行过程就是 checkPreCall
和 checkPostCall
。
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(赋值,输入读写),执行了 addFiltersPre
和 propagateFromPre
。
对于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.