ClangのMallocOverflowSecurityCheckerを試してみる

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で閲覧できる。

CFG

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を用いる方法が実装さ れているので参考になる。

ダウンロード
実験コード
検出対象として使用した実験コード
malloc.tar.gz
GNU tar 1.5 KB