ClangのChrootCheckerを試してみる

ClangのChrootCheckerを試してみます。本Checkerはchrootの問題を検出する ことができます。

 

1. 実行方法

$ clang -cc1 -analyzer-checker=alpha.unix.Chroot \
  -analyze <target>.c

2. chrootの問題

chrootはchdir("/")を実行しないと特定のディレクトリにchrootを実行したプ ロセスを閉じ込めることができない問題があります。 chroot実行後にすぐさまchdir("/")を実行する必要があります。 chrootを使った後に、chdirを使わない場合、chdir("/")を使わない場合、 chdir("/")を使う場合の3種類を試してみました。 全て/home/<me>/src/で実行しています。-staticでビルドした独自シェルを使 う為、Ubuntu上で実行しました。

2.1. chrootの後にchdirを使わない場合

newpathディレクトリよりも上の階層を見ることができてしまいます。

$ cat chroot_invalid_without_chdir.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define SHELL "/bin/sh"
 
int main(int argc, char *argv[])
{
  char *command = SHELL;
 
  if (argc < 2) {
        fprintf(stderr, "usage: %s newroot [command]\n", argv[0]);
        return 1;
  }
 
  if (argc > 2)
        command = argv[2];
 
  if (chroot(argv[1]) < 0) {
        perror("chroot");
        return 1;
  }
 
  if (execv(command, argv) < 0) {
        perror("execvp");
        return 1;
  }
 
  return 0;
}
$ sudo ./chroot_invalid_without_chdir newpath
sh> pwd
(unreachable)/home/<me>/src
sh> cd ..
sh> pwd
(unreachable)/home/<me>

2.2. chrootの後にchdir("..")を使う場合

newpathディレクトリよりも上の階層を見ることができてしまいます。

$ cat chroot_invalid_with_chdir.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define SHELL "/bin/sh"
 
int main(int argc, char *argv[])
{
  char *command = SHELL;
 
  if (argc < 2) {
        fprintf(stderr, "usage: %s newroot [command]\n", argv[0]);
        return 1;
  }
 
  if (argc > 2)
        command = argv[2];
 
  if (chroot(argv[1]) < 0) {
        perror("chroot");
        return 1;
  }
 
  if (chdir("..") < 0) {
        perror("chdir");
        return 1;
  }
 
  if (execv(command, argv) < 0) {
        perror("execvp");
        return 1;
  }
 
  return 0;
}
$ sudo ./chroot_invalid_with_chdir newpath
sh> pwd
(unreachable)/home/<me>
sh> cd ..
sh> pwd
(unreachable)/home

2.3. chrootの後にchdir("/")を使う場合

newpathディレクトリよりも上の階層を見ることができません。

$ cat chroot_valid.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define SHELL "/bin/sh"
 
int main(int argc, char *argv[])
{
  char *command = SHELL;
 
  if (argc < 2) {
        fprintf(stderr, "usage: %s newroot [command]\n", argv[0]);
        return 1;
  }
 
  if (argc > 2)
        command = argv[1];
 
  if (chroot(argv[1]) < 0) {
        perror("chroot");
        return 1;
  }
 
  if (chdir("/") < 0) {
        perror("chdir");
        return 1;
  }
 
  if (execvp(command, argv) < 0) {
        perror("execvp");
        return 1;
  }
 
  return 0;
}
$ sudo ./chroot_valid newpath
sh> pwd
/
sh> cd ..
sh> pwd
/

3. ChrootCheckerの実装

eval::Callとcheck::PreStmt<CallExpr>を継承し、evalCallと checkPreStmt(const CallExpr *,...)を実装しています。

class ChrootChecker : public Checker<eval::Call, check::PreStmt<CallExpr> > {
<snip>
  bool evalCall(const CallExpr *CE, CheckerContext &C) const;
  void checkPreStmt(const CallExpr *CE, CheckerContext &C) const;

3.1. enum Kind

chrootとchdir("/")を実行したかどうかを確認する為に、以下のenumを定義し ています。NO_CHROOTはchrootが呼ばれていない状態、ROOT_CHANGEDはchroot が呼ばれたがchdir("/")が呼ばれていない状態。JAIL_ENTEREDはchrootの後に chdir("/")が呼ばれた状態となっております。

enum Kind { NO_CHROOT, ROOT_CHANGED, JAIL_ENTERED };

3.2. getTag/addGDM/FindGDM

ProgramStateManagerが管理するmapクラスにenum Kindの遷移を記録します。 以下のgetTagメソッド内のstaticな変数のアドレスをmapクラスのkeyとしてい ます。

  static void *getTag() {
    static int x;
    return &x;
  }

addGDMメソッドでmapクラスに状態を登録します(上書きします)。

  state = Mgr.addGDM(state, ChrootChecker::getTag(), (void*) ROOT_CHANGED);

FindGDMメソッドでkeyに対応する状態を取得します。

  const void *k = state->FindGDM(ChrootChecker::getTag());

3.3. evalCall

evalCallで補足した関数がchrootの場合はChrootメソッドを呼び、chdirの場 合はChdirメソッドを呼びます。

bool ChrootChecker::evalCall(const CallExpr *CE, CheckerContext &C) const {
  const FunctionDecl *FD = C.getCalleeDecl(CE);
  if (!FD)
    return false;
 
  ASTContext &Ctx = C.getASTContext();
  if (!II_chroot)
    II_chroot = &Ctx.Idents.get("chroot");
  if (!II_chdir)
    II_chdir = &Ctx.Idents.get("chdir");
 
  if (FD->getIdentifier() == II_chroot) {
    Chroot(C, CE);
    return true;
  }
  if (FD->getIdentifier() == II_chdir) {
    Chdir(C, CE);
    return true;
  }
 
  return false;
}

3.4. Chroot

ROOT_CHANGEDの状態を登録します。

void ChrootChecker::Chroot(CheckerContext &C, const CallExpr *CE) const {
  ProgramStateRef state = C.getState();
  ProgramStateManager &Mgr = state->getStateManager();
  
  // Once encouter a chroot(), set the enum value ROOT_CHANGED directly in 
  // the GDM.
  state = Mgr.addGDM(state, ChrootChecker::getTag(), (void*) ROOT_CHANGED);
  C.addTransition(state);
}

3.5. Chdir

enum Kindの状態を取り出します。取り出せない場合はchrootが呼ばれていな いことになります。取り出せた場合はchdirの引数が"/"かどうかを確認します。 "/"の場合はJAIL_ENTEREDの状態を登録します。

void ChrootChecker::Chdir(CheckerContext &C, const CallExpr *CE) const {
  ProgramStateRef state = C.getState();
  ProgramStateManager &Mgr = state->getStateManager();
 
  // If there are no jail state in the GDM, just return.
  const void *k = state->FindGDM(ChrootChecker::getTag());
  if (!k)
    return;
 
  // After chdir("/"), enter the jail, set the enum value JAIL_ENTERED.
  const Expr *ArgExpr = CE->getArg(0);
  SVal ArgVal = state->getSVal(ArgExpr, C.getLocationContext());
  
  if (const MemRegion *R = ArgVal.getAsRegion()) {
    R = R->StripCasts();
    if (const StringRegion* StrRegion= dyn_cast<StringRegion>(R)) {
      const StringLiteral* Str = StrRegion->getStringLiteral();
      if (Str->getString() == "/")
        state = Mgr.addGDM(state, ChrootChecker::getTag(),
                           (void*) JAIL_ENTERED);
    }
  }
 
  C.addTransition(state);
}

3.6. checkPreStmt(const CallExpr *,...)

chroot/chdir以外の関数の場合にenum Kindの値を取り出します。値が ROOT_CHANGEDの場合はchrootが呼ばれたのにchdir("/")が呼ばれる前に関数が 呼ばれたことになる為、warningを出力します。

// Check the jail state before any function call except chroot and chdir().
void ChrootChecker::checkPreStmt(const CallExpr *CE, CheckerContext &C) const {
  const FunctionDecl *FD = C.getCalleeDecl(CE);
  if (!FD)
    return;
 
  ASTContext &Ctx = C.getASTContext();
  if (!II_chroot)
    II_chroot = &Ctx.Idents.get("chroot");
  if (!II_chdir)
    II_chdir = &Ctx.Idents.get("chdir");
 
  // Ingnore chroot and chdir.
  if (FD->getIdentifier() == II_chroot || FD->getIdentifier() == II_chdir)
    return;
  
  // If jail state is ROOT_CHANGED, generate BugReport.
  void *const* k = C.getState()->FindGDM(ChrootChecker::getTag());
  if (k)
    if (isRootChanged((intptr_t) *k))
      if (ExplodedNode *N = C.addTransition()) {
        if (!BT_BreakJail)
          BT_BreakJail.reset(new BuiltinBug("Break out of jail",
                                        "No call of chdir(\"/\") immediately "
                                        "after chroot"));
        BugReport *R = new BugReport(*BT_BreakJail, 
                                     BT_BreakJail->getDescription(), N);
        C.emitReport(R);
      }
  return;
}

4. ChrootCheckerの実行結果

先ほどのchrootのコードを対象とします。

4.1. chrootの後にchdirを使わない場合

chrootの後にexecvが呼ばれていることを補足しています。generateSinkでは なく、addTransitionを呼んでいる為、execv以降のperrorでもwarningが出力 されます。気になるのはchrootが失敗した場合のperrorまで補足されている点 です。

$ ./ChrootChecker.sh chroot_invalid_without_chdir.c 
chroot_invalid_without_chdir.c:20:2: warning: No call of chdir("/") immediately after chroot
        perror("chroot");
        ^~~~~~~~~~~~~~~~
chroot_invalid_without_chdir.c:24:7: warning: No call of chdir("/") immediately after chroot
  if (execv(command, argv) < 0) {
      ^~~~~~~~~~~~~~~~~~~~
chroot_invalid_without_chdir.c:25:2: warning: No call of chdir("/") immediately after chroot
        perror("execvp");
        ^~~~~~~~~~~~~~~~
3 warnings generated.

4.2. chrootの後にchdir("..")を使う場合

先ほどと同様ですが、chdir("..")が補足されていません。本Checkerでは chrootとchdir("/")の間にchdirが呼ばれることは問題なしと捉えているよう です。

chroot_invalid_with_chdir.c:20:2: warning: No call of chdir("/") immediately after chroot
        perror("chroot");
        ^~~~~~~~~~~~~~~~
chroot_invalid_with_chdir.c:25:2: warning: No call of chdir("/") immediately after chroot
        perror("chdir");
        ^~~~~~~~~~~~~~~
chroot_invalid_with_chdir.c:29:7: warning: No call of chdir("/") immediately after chroot
  if (execv(command, argv) < 0) {
      ^~~~~~~~~~~~~~~~~~~~
chroot_invalid_with_chdir.c:30:2: warning: No call of chdir("/") immediately after chroot
        perror("execvp");
        ^~~~~~~~~~~~~~~~
4 warnings generated.

4.3. chrootの後にchdir("/")を使う場合

chrootが失敗した場合のperrorが補足されています。chrootが成功したかどう かを見ていない為にfalse positiveが発生しています。

chroot_valid.c:20:2: warning: No call of chdir("/") immediately after chroot
        perror("chroot");
        ^~~~~~~~~~~~~~~~
1 warning generated.

5. ChrootCheckerの改善

ChrootCheckerには、chrootが失敗しているにも関わらずROOT_CHANGEDに遷移 するという問題と、問題検出された後も経路を辿り冗長なwarningを出力して しまうという問題があります。

以下の改善を加えてみました。BindExprでchrootの戻り値を0の値を持つSVal に結びつけ、generateSinkで以降の経路を辿らないようにしています。

Index: lib/StaticAnalyzer/Checkers/ChrootChecker.cpp
===================================================================
--- lib/StaticAnalyzer/Checkers/ChrootChecker.cpp       (revision 202679)
+++ lib/StaticAnalyzer/Checkers/ChrootChecker.cpp       (working copy)
@@ -87,11 +87,13 @@
 void ChrootChecker::Chroot(CheckerContext &C, const CallExpr *CE) const {
   ProgramStateRef state = C.getState();
   ProgramStateManager &Mgr = state->getStateManager();
+  SValBuilder &svalBuilder = C.getSValBuilder();
+  SVal success = svalBuilder.makeZeroVal(svalBuilder.getContext().IntTy);
   
   // Once encouter a chroot(), set the enum value ROOT_CHANGED directly in 
   // the GDM.
   state = Mgr.addGDM(state, ChrootChecker::getTag(), (void*) ROOT_CHANGED);
-  C.addTransition(state);
+  C.addTransition(state->BindExpr(CE, C.getLocationContext(), success));
 }
 
 void ChrootChecker::Chdir(CheckerContext &C, const CallExpr *CE) const {
@@ -140,7 +142,7 @@
   void *const* k = C.getState()->FindGDM(ChrootChecker::getTag());
   if (k)
     if (isRootChanged((intptr_t) *k))
-      if (ExplodedNode *N = C.addTransition()) {
+      if (ExplodedNode *N = C.generateSink()) {
         if (!BT_BreakJail)
           BT_BreakJail.reset(new BuiltinBug(
               this, "Break out of jail", "No call of chdir(\"/\") immediately "

再度、対象コードにChrootCheckerを掛けてみます。

5.1. chrootの後にchdirを使わない場合

chrootが失敗した場合の経路とexecv以降の冗長なwarningが消えています。

chroot_invalid_without_chdir.c:24:7: warning: No call of chdir("/") immediately after chroot
  if (execv(command, argv) < 0) {
      ^~~~~~~~~~~~~~~~~~~~
1 warning generated.

5.2. chrootの後にchdir("..")を使う場合

同様です。

chroot_invalid_with_chdir.c:25:2: warning: No call of chdir("/") immediately after chroot
        perror("chdir");
        ^~~~~~~~~~~~~~~
chroot_invalid_with_chdir.c:29:7: warning: No call of chdir("/") immediately after chroot
  if (execv(command, argv) < 0) {
      ^~~~~~~~~~~~~~~~~~~~
2 warnings generated.

5.3. chrootの後にchdir("/")を使う場合

chrootが失敗した場合の経路が出力されません。

(何も出力されない)

本改善をcfe-commitsに投稿してみました。もっと良い改善方法が得られると 嬉しいですね。

 

(追記)改善方法についてご意見頂きました。BindExprやgenerateSinkを実行すると他のCheckerの挙動にも悪影響を及ぼすようです。chrootが成功したかどうかを判定して静的解析を継続する必要があるようです。

 

6. まとめ

chrootとchdir("/")をセットで実行しないとjailを突破される問題があります。 ChrootCheckerでは本問題を検出できます。ただし、chrootが失敗した経路と 問題検出した経路にて、冗長なwarningが出力されるようです。

ダウンロード
ソースコード
対象コードとchrootの実験用シェル
src.tar.gz
GNU tar 1.6 KB