Clangの静的解析コードであるCheckerのひとつ、 MallocOverflowSecurityCheckerを試してみる。
1. Checkerの実行方法
どんなCheckerがあるかを確認するには-analyzer-checker-helpを使用する。
$ clang -cc1 -analyzer-checker-help OVERVIEW: Clang Static Analyzer Checkers List USAGE: -analyzer-checker <CHECKER or PACKAGE,...> CHECKERS: <snip> alpha.security.MallocOverflow Check for overflows in the arguments to malloc() <snip>
今回試すMallocOverflowSecurityCheckerを-analyzer-checkerで指定する。
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow \ -analyze <target>.c
2. MallocOverflowSecurityCheckerが対象とする対象
MallocOverflowSecurityChecker.cppに記載されているコメントによれば、以 下の環境と手順で発生するヒープ領域操作を対象としている。
2.1. 環境
* 外部から入力可能なunsigned int nがある * 'malloc (n * 4)'のようなヒープ領域確保がある
2.2. 手順
* 攻撃者はunsigned int nを'UINT_MAX/4 + 2'にする * unsigned intで値が丸められ8Byteしかヒープ領域が確保されない * 8Byteより後のヒープ領域に不正な値を書き込まれる
よって、malloc(sizeof(int) * n)などの呼び出しにおいて、nが不正な値でヒー プ領域操作の問題があるかどうかを検出する。
3. 実装
ASTCodeBodyを継承している。
class MallocOverflowSecurityChecker : public Checker<check::ASTCodeBody> {
<snip>
void checkASTCodeBody(const Decl *D, AnalysisManager &mgr,
BugReporter &BR) const;
ASTCodeBodyのドキュメント。
/// rief Check every declaration in the AST. /// /// An AST traversal callback, which should only be used when the checker is /// not path sensitive. It will be called for every Declaration in the AST and /// can be specialized to only be called on subclasses of Decl, for example, /// FunctionDecl. /// /// check::ASTDecl<FunctionDecl>
path sensitiveでない(前後関係やif文の分岐などによる遷移を見ない) Checkerに使用する。以下、ソースコードに#でコメントを記載していく。
3.1. checkASTCodeBody
ASTから得られるCFGを辿り、mallocの引数をCheckMallocArgumentで確認し、 問題がある場合はベクタに積んでいく。ベクタをOutputPossibleOverflowsで 精査し、問題がある場合はwarning出力する。
void MallocOverflowSecurityChecker::checkASTCodeBody(const Decl *D,
AnalysisManager &mgr,
BugReporter &BR) const {
# CFGを取得する(ASTは関数単位なので、CFGも関数単位になる)
CFG *cfg = mgr.getCFG(D);
if (!cfg)
return;
// A list of variables referenced in possibly overflowing malloc operands.
# ベクタの定義
SmallVector<MallocOverflowCheck, 2> PossibleMallocOverflows;
# CFGを辿る
for (CFG::iterator it = cfg->begin(), ei = cfg->end(); it != ei; ++it) {
CFGBlock *block = *it;
# CFGBlockを辿る
for (CFGBlock::iterator bi = block->begin(), be = block->end();
bi != be; ++bi) {
if (Optional<CFGStmt> CS = bi->getAs<CFGStmt>()) {
# 関数呼び出しかどうか
if (const CallExpr *TheCall = dyn_cast<CallExpr>(CS->getStmt())) {
// Get the callee.
const FunctionDecl *FD = TheCall->getDirectCallee();
if (!FD)
return;
// Get the name of the callee. If it's a builtin, strip off the prefix.
IdentifierInfo *FnInfo = FD->getIdentifier();
if (!FnInfo)
return;
# 関数がmalloc/_MALLOCかどうか
if (FnInfo->isStr ("malloc") || FnInfo->isStr ("_MALLOC")) {
# 引数が一つか
if (TheCall->getNumArgs() == 1)
# mallocの引数を確認
CheckMallocArgument(PossibleMallocOverflows, TheCall->getArg(0),
mgr.getASTContext());
}
}
}
}
}
# EvaluatedExprVisitorを用いて、ベクタに積まれた二項演算子と
# non-const valueを精査し、問題がある場合はwarningを出力する
OutputPossibleOverflows(PossibleMallocOverflows, D, BR, mgr);
}
CFGは以下の方法でdumpできる。
$ cat hello.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello, world\n");
if (argc < 1)
{
fprintf(stdout, "0");
}
else if (argc < 2)
{
fprintf(stdout, "1");
}
else
{
fprintf(stdout, "%d", argc);
}
puts("");
return 0;
}
$ clang -cc1 -analyzer-checker=debug.ViewCFG -analyze hello.c
Writing '/<path-to-cfg>/CFG-533571.dot'... done.
if文による分岐毎のブロックがツリー構造で出力される。出力したCFGは Graphvizで閲覧できる。
3.2. CheckMallocArgument
malloc引数を確認する処理。malloc(non-const value * const value)のパター ンを見つけてnon-const valueと二項演算子をベクタに積む。
void MallocOverflowSecurityChecker::CheckMallocArgument(
SmallVectorImpl<MallocOverflowCheck> &PossibleMallocOverflows,
const Expr *TheArgument,
ASTContext &Context) const {
/* Look for a linear combination with a single variable, and at least
one multiplication.
Reject anything that applies to the variable: an explicit cast,
conditional expression, an operation that could reduce the range
of the result, or anything too complicated :-). */
const Expr * e = TheArgument;
const BinaryOperator * mulop = NULL;
# 二項演算子を再帰的に辿る為にループを用いる、例えばa * b + cなどの
# 場合に根が*、ノードに+を持つ二項演算子が設定されている
for (;;) {
e = e->IgnoreParenImpCasts();
# 二項演算子の場合
if (isa<BinaryOperator>(e)) {
const BinaryOperator * binop = dyn_cast<BinaryOperator>(e);
BinaryOperatorKind opc = binop->getOpcode();
// TODO: ignore multiplications by 1, reject if multiplied by 0.
# malloc(lhs * rhs)かどうか(malloc(lhs * rhs + a)の場合は演算子
# の順序で*が大元の二項演算子となる)
if (mulop == NULL && opc == BO_Mul)
mulop = binop;
# 比較演算子や等価演算子がある場合はやめる
if (opc != BO_Mul && opc != BO_Add && opc != BO_Sub && opc != BO_Shl)
return;
const Expr *lhs = binop->getLHS();
const Expr *rhs = binop->getRHS();
# isEvaluatbleはconstな場合にtrueになる、右辺が定数の場合はfor文
# の先頭に戻り、左辺を辿っていく
if (rhs->isEvaluatable(Context))
e = lhs;
# malloc(lhs + rhs)あるいは malloc(lhs * rhs)で、かつlhsがconst
# な変数の場合
else if ((opc == BO_Add || opc == BO_Mul)
&& lhs->isEvaluatable(Context))
e = rhs;
# constでない変数同士の場合
else
return;
}
# 変数/メンバ変数の場合
else if (isa<DeclRefExpr>(e) || isa<MemberExpr>(e))
# これ以上展開する必要はないのでbreak
break;
# 二項演算子/変数/メンバでない場合(例えば関数呼び出し)
else
# チェックをやめる(諦める)
return;
}
# malloc(const value * ...)があったかどうか
if (mulop == NULL)
return;
// We've found the right structure of malloc argument, now save
// the data so when the body of the function is completely available
// we can check for comparisons.
// TODO: Could push this into the innermost scope where 'e' is
// defined, rather than the whole function.
# ベクタに二項演算子とnon-const valueを積む
PossibleMallocOverflows.push_back(MallocOverflowCheck(mulop, e));
}
3.3. OutputPossibleOverflows
ベクタに積まれたnon-const valueが比較演算子でレンジチェックされている かを確認する処理。チェックされていない場合は二項演算子にwarningを出力 する。
void MallocOverflowSecurityChecker::OutputPossibleOverflows(
SmallVectorImpl<MallocOverflowCheck> &PossibleMallocOverflows,
const Decl *D, BugReporter &BR, AnalysisManager &mgr) const {
// By far the most common case: nothing to check.
# ベクタが空かどうか
if (PossibleMallocOverflows.empty())
return;
// Delete any possible overflows which have a comparison.
# EvaluatedExprVisitorを用いてベクタの中身を精査する、
# malloc(non-const value * const value)の場合にnon-const valueの値が比
# 較演算子でチェックされている場合は、warning対象から外す(ベクタに積
# まれているDecRefExprの変数定義と比較演算子の左辺/右辺の変数定義を比
# 較する)、ただし0をもちいて比較している場合は対象から外す、unsigned
# int > 0などは無意味な為
# またfor文/while文/Do文の条件式での比較は見ない
CheckOverflowOps c(PossibleMallocOverflows, BR.getContext());
# ASTの根から再帰的に辿り、比較の二項演算子を見つけて処理をする(対
# 象となる二項演算子は*, +, -, <<ではない)
c.Visit(mgr.getAnalysisDeclContext(D)->getBody());
// Output warnings for all overflows that are left.
# ベクタを辿る
for (CheckOverflowOps::theVecType::iterator
i = PossibleMallocOverflows.begin(),
e = PossibleMallocOverflows.end();
i != e;
++i) {
# warningを出力する
BR.EmitBasicReport(D, "malloc() size overflow", categories::UnixAPI,
"the computation of the size of the memory allocation may overflow",
PathDiagnosticLocation::createOperatorLoc(i->mulop,
BR.getSourceManager()),
i->mulop->getSourceRange());
}
}
EvaluatedExprVisitorはVisit()にあたえられたStmtを再帰的に辿っていく。 Stmt/Exprの種類によって、Visit##Stmtが呼ばれる。これらは下位のStmtや引 数のStmtを辿るように定義されている。EvaluatedExprVisitorを継承したクラ スでVisit##Stmtを独自定義することで、任意の処理を追加できる(その処理 の中でスーパークラスのVisit##Stmtを呼べば再帰処理は続く)。
4. 実験
malloc(size * mul)というmalloc呼び出しの検出を試す。mulは常にconstで、 sizeをレンジチェックするかどうかで5パターン試す。
4.1. レンジチェックが正常な場合
sizeが一定値以上の場合はreturnする。
$ cat malloc1.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
unsigned int size = UINT_MAX / 4 + 2;
char *buffer;
if (size >= 10)
return 1;
buffer = (char *) malloc(4 * size);
if (buffer == NULL)
return 1;
free(buffer);
return 0;
}
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow -analyze malloc1.c
$ # warningは出力されない
4.2. レンジチェックがない場合
sizeのレンジチェックがない。
$ cat malloc2.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
unsigned int size = UINT_MAX / 4 + 2;
char *buffer;
buffer = (char *) malloc(4 * size);
if (buffer == NULL)
return 1;
free(buffer);
return 0;
}
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow -analyze malloc2.c
malloc2.c:10:30: warning: the computation of the size of the memory allocation may overflow
buffer = (char *) malloc(4 * size);
~~^~~~~~
1 warning generated.
$ # 期待通りwarningが出力される。
4.3. for文でレンジチェック
for文でレンジチェックするケースはないと思うが。
$ cat malloc3.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
unsigned int size;
char *buffer;
for (size = 0; size < 10; size++)
;
buffer = (char *) malloc(4 * size);
if (buffer == NULL)
return 1;
free(buffer);
return 0;
}
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow -analyze malloc3.c
malloc3.c:13:30: warning: the computation of the size of the memory allocation may overflow
buffer = (char *) malloc(4 * size);
~~^~~~~~
1 warning generated.
$ # for文のレンジチェックはレンジチェックとして扱われない
4.4. レンジチェックが不正な場合
sizeが10より小さい場合はreturnする。malloc引数の大きさを計算していない ことが分かる(静的解析では計算が困難であるけれどできればしたいところ)。
$ cat malloc4.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
unsigned int size = UINT_MAX / 4 + 2;
char *buffer;
if (size < 10)
return 1;
buffer = (char *) malloc(4 * size);
if (buffer == NULL)
return 1;
free(buffer);
return 0;
}
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow -analyze malloc4.c
$ # warning出力されない
4.5. レンジチェックが後にある場合
sizeが10以上の場合にreturnするが、malloc確保よりも後にある場合。path sensitiveなCheckerでない故に前後関係を把握していない。ただし、レンジ チェックがmallocよりも後にあること自体がおかしいので、検出しなくても良 いと思う。
$ cat malloc5.c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
unsigned int size = UINT_MAX / 4 + 2;
char *buffer;
buffer = (char *) malloc(4 * size);
if (buffer == NULL)
return 1;
if (size >= 10) {
free(buffer);
return 1;
}
free(buffer);
return 0;
}
$ clang -cc1 -analyzer-checker=alpha.security.MallocOverflow -analyze malloc4.c
$ # warning出力されない
5. まとめ
path sensitiveなCheckerではないので、それほど複雑な処理にはなっていな い。パターンマッチングに近い。いくつか期待値通りにならないコードも紹介 したが、ほぼありえないコードなので問題ではないだろう。const value * non-const valueがオーバフローするかどうかの計算もできると検出精度が上 がると思う。
ASTを辿るのにCFGを用いる方法とEvaluatedExprVisitorを用いる方法が実装さ れているので参考になる。