Skip to content

Chapter 4

hal0taso edited this page Aug 7, 2017 · 1 revision

第4章ではLSMを用いてファイルのオープン要求をフックして、 ClamAV による オンアクセススキャンの実装に挑戦してもらいます。

↑の講義内容案を考えていた時点では、プログラムの実行要求を捕捉して execve() システムコールに渡されたプログラムのみをスキャン対象にしようと思っていました。 しかし、第3章の内容を試したところ、 execve() システムコールに渡せるような検体は 含まれていないことが判明したため、ファイルのオープン要求を捕捉して open() システム コールに渡されたファイルをスキャン対象にすることにしました。そのため、捕捉範囲が 当初の想定より広くなっており、それに伴い無限再帰ループに陥らせないための若干の修正が 必要になることが見込まれます。

キャンプ本番での枠は4時間しかないので、事前学習期間中にどこまで進められるかが 勝負になります。なお、カーネルモジュールを扱うため、第2章と同様、破棄しても 惜しくない Linux 環境を用意の上、実行するようにしてください。

方針としては、 AKARI のソースコードをベースにします。まずは http://akari.osdn.jp/1.0/chapter-3.html の内容を一通り試してみてください。その後、 必要な部分を切り貼りしながら、事前学習の第3章で作成した ex2.c と連携できるように 改変していきます。 ex2.c を改変する際には ccs-tools-1.8.5-20170102.tar.gz に 含まれている ccs-queryd.c というファイルを参考にします。

akari-1.0.36-20170712.tar.gz を展開すると、 akari/ というディレクトリが作成されます。 その中の、 test.c というのが第4章のテンプレートとして使用するファイルになります。

本来はローダブルカーネルモジュールからはLSMにアクセスすることができないのですが、 probe.c がLSMにアクセスするのに必要な処理を引き受けてくれているため、毎回 カーネルをソースコードからリビルドすることなく(コンパイルの時間を節約しながら) LSMを使うことができるようになっています。でも、アンロード処理の面倒は見ないため、 再ロードするためにシステムを再起動させる処理は必要になります。

作成に必要なものは揃っている筈ですが、まだ手順を確認していないので、とりあえず 第3章までをクリアしていただければと思います。

4-1

第4章の最初の事前課題について説明します。

カーネルモジュールについては、LSMインタフェースをローダブルカーネル モジュールから利用できるように設計されている AKARI または CaitSith を ベースにします。

カーネルモジュールを開発するための環境に対して、以下の操作を行うことで ソースコードをダウンロードしてください。(本番で使う CentOS 7 および Fedora 25 の Oracle VirtualBox イメージの中には既に含まれています。)

mkdir ~/SVN
cd ~/SVN
svn checkout https://svn.osdn.net/svnroot/tomoyo/
svn checkout https://svn.osdn.net/svnroot/akari/
svn checkout https://svn.osdn.net/svnroot/caitsith/

この操作により、 ~/SVN/tomoyo/ ~/SVN/akari/ ~/SVN/caitsith/ というディレクトリが 作成されます。 AKARI をベースにする場合は ~/SVN/akari/trunk/akari/ 配下を、 CaitSith をベースにする場合は ~/SVN/caitsith/trunk/caitsith-patch/caitsith/ 配下を 利用します。どちらをベースにしても構いません。以後、特に断りが無ければ akari と caitsith とを区別せずに akari と表記します。

第2章で Hello world. を出力するカーネルモジュールを作成してもらいました。 それにより、ローダブルカーネルモジュールを作成してコンパイルおよびロードする 手順については既に知っているものと考えます。

まずは、 akari/test.c を参照してください。この中にはLSMインタフェースを ローダブルカーネルモジュールから利用できるようにするための処理が書かれていますので、 このソースコードをコンパイルしてロードしてみてください。

cd /usr/src/kernels/`uname -r`/
cp -a ~/SVN/akari/trunk/akari/ .
make SUBDIRS=$PWD/akari
insmod akari/akari_test.ko

もし、 insmod がエラーとなる場合、ご利用の環境には対応できませんので、 他の環境を用意してください。 insmod がエラーとならなかった場合は、 ご利用の環境に対応できますので、アンロードしてください。

rmmod akari_test

AKARI は Linux 2.6.0 以降、 CaitSith は Linux 2.6.27 以降で動作できるように 作られていますが、LSMのインタフェース(利用可能なフック関数や、フック関数に 渡される引数の数/型)はカーネルバージョンにより異なります。そのため、カーネル バージョンにより記述内容を修正する必要があるということを意識しておいてください。

次に、 akari/lsm.c を参照してください。この中には、カーネルのバージョン毎に 異なる akari/lsm-*.c を使うための指定が行われています。例えば CentOS 7 の場合は 3.10 ですので akari/lsm-2.6.29.c を、 Fedora 25 の場合は( 2017/07/28 現在) 4.11 ですので akari/lsm-4.7.c を参照してください。

lsm-2.6.29.c あるいは lsm-4.7.c の中には、LSMインタフェースから呼び出して もらうためのたくさんのフック関数と、それらのフック関数を登録するための処理が 記述されています。ファイルは大きいですが、この講義ではこの中のごく一部だけを 利用します。処理の流れを説明する都合上、カーネルが 3.10 の場合に利用される lsm-2.6.29.c について先に説明します。

まず、 module_init() パラメータで指定されている ccs_init() という関数を参照 してください。複数のカーネルバージョンで動作できるようにするための条件分岐が ありますが、この講義で必要なのは以下の部分だけです。

---------- lsm-2.6.29.c ----------
static int __init ccs_init(void)
{
        struct security_operations *ops = probe_security_ops();
        if (!ops)
                goto out;
        ccsecurity_exports.d_absolute_path = probe_d_absolute_path();
        if (!ccsecurity_exports.d_absolute_path)
                goto out;
        ccs_update_security_ops(ops);
        return 0;
out:
        return -EINVAL;
}
---------- lsm-2.6.29.c ----------

次に ccs_update_security_ops() という関数を参照してください。 たくさんの行がありますが、この講義で必要とするのは以下の部分だけです。

---------- lsm-2.6.29.c ----------
static void __init ccs_update_security_ops(struct security_operations *ops)
{
        swap_security_ops(file_open);
}
---------- lsm-2.6.29.c ----------

swap_security_ops() はマクロとして以下のように定義されています。

---------- lsm-2.6.29.c ----------
#define swap_security_ops(op)                                           \
        original_security_ops.op = ops->op; smp_wmb(); ops->op = ccs_##op;
---------- lsm-2.6.29.c ----------

つまり、 swap_security_ops(file_open); という行は original_security_ops.file_open = ops->op; smp_wmb(); ops->op = ccs_file_open; のように展開されます。

original_security_ops は以下のように定義されています。

---------- lsm-2.6.29.c ----------
static struct security_operations original_security_ops /* = *security_ops; */;
---------- lsm-2.6.29.c ----------

struct security_operationsinclude/linux/security.h で定義されている 構造体で、これがLSMインタフェースと呼ばれているものです。カーネルの中から この構造体に指定されているフック関数を呼び出し、フック関数の中で (パーミッションのチェックなどを行うことにより)アクセスの可否を判断します。

次に、 ccs_file_open() という関数を参照してください。

---------- lsm-2.6.29.c ----------
static int ccs_file_open(struct file *f, const struct cred *cred)
{
        int rc = ccs_open(f);
        if (rc)
                return rc;
        while (!original_security_ops.file_open);
        return original_security_ops.file_open(f, cred);
}
---------- lsm-2.6.29.c ----------

この関数では、 ccs_open() という関数を呼び出し、 ccs_open()0 を返却した 場合には original_security_ops.file_open() という関数も呼び出しています。

次に、 ccs_open() という関数を参照してください。

---------- lsm-2.6.29.c ----------
static int ccs_open(struct file *f)
{
        return ccs_open_permission(f);
}
---------- lsm-2.6.29.c ----------

この講義では、 ccs_open_permissoin() に相当する部分から ClamAV による オンアクセススキャンを行い、ウィルスが検出された場合にはアクセスを拒否 (負の値を返却)し、それ以外の場合にはアクセスを許可( 0 を返却)する という処理を実装することを目指します。

次に、カーネルが 4.11 の場合に利用される lsm-4.7.c について説明します。 module_init() パラメータで指定されている ccs_init() という関数を参照 してください。この講義で必要なのは以下の部分だけです。

---------- lsm-4.7.c ----------
static int __init ccs_init(void)
{
        int idx;
        struct security_hook_heads *hooks = probe_security_hook_heads();
        if (!hooks)
                goto out;
        for (idx = 0; idx < ARRAY_SIZE(akari_hooks); idx++)
                akari_hooks[idx].head = ((void *) hooks)
                        + ((unsigned long) akari_hooks[idx].head)
                        - ((unsigned long) &probe_dummy_security_hook_heads);
        ccsecurity_exports.d_absolute_path = probe_d_absolute_path();
        if (!ccsecurity_exports.d_absolute_path)
                goto out;
        for (idx = 3; idx < ARRAY_SIZE(akari_hooks); idx++)
                add_hook(&akari_hooks[idx]);
        return 0;
out:
        return -EINVAL;
}
---------- lsm-4.7.c ----------

akari_hookssecurity_hook_list 型の配列です。 security_hook_listinclude/linux/lsm_hooks.h で定義されている構造体で、フック関数のリストを 保持するために使われます。この講義では以下の1行だけを利用します。

---------- lsm-4.7.c ----------
static struct security_hook_list akari_hooks[] = {
        MY_HOOK_INIT(file_open, ccs_file_open),
}
---------- lsm-4.7.c ----------

MY_HOOK_INIT() はマクロとして以下のように定義されています。

---------- lsm-4.7.c ----------
#define MY_HOOK_INIT(HEAD, HOOK)                                \
        { .head = &probe_dummy_security_hook_heads.HEAD,        \
                        .hook = { .HEAD = HOOK } }
---------- lsm-4.7.c ----------

つまり、 MY_HOOK_INIT(file_open, ccs_file_open) という行は

akari_hooks[0] = {
.head = &probe_dummy_security_hook_heads.file_open,
.hook = { .file_open = ccs_file_open },
}

のように展開されます。

add_hook() という関数は以下のように定義されており、LSMインタフェースが 使用しているリストに、このモジュールで使用するフック関数のエントリを 追加するという処理を行っています。

---------- lsm-4.7.c ----------
static inline void add_hook(struct security_hook_list *hook)
{
        list_add_tail_rcu(&hook->list, hook->head);
}
---------- lsm-4.7.c ----------

複数のフック関数を呼び出す部分をLSMインタフェース側が提供しているため、 ccs_file_open() 関数の中から original_security_ops.file_open() という 関数を呼び出す必要は無くなっています。

---------- lsm-4.7.c ----------
static int ccs_file_open(struct file *f, const struct cred *cred)
{
        return ccs_open_permission(f);
}
---------- lsm-4.7.c ----------

なお、この講義で使用する akari_hooks 配列の長さは1なので、それに合わせて ccs_init() 関数の処理を以下のように修正して使います。(配列の長さが1なので、 配列にしなくても構いません。)

---------- lsm-4.7.c ----------
static int __init ccs_init(void)
{
        struct security_hook_heads *hooks = probe_security_hook_heads();
        if (!hooks)
                goto out;
        akari_hooks[0].head = ((void *) hooks)
                        + ((unsigned long) akari_hooks[0].head)
                        - ((unsigned long) &probe_dummy_security_hook_heads);
        ccsecurity_exports.d_absolute_path = probe_d_absolute_path();
        if (!ccsecurity_exports.d_absolute_path)
                goto out;
        add_hook(&akari_hooks[0]);
        return 0;
out:
        return -EINVAL;
}
---------- lsm-4.7.c ----------

さて、 ccs_open_permissoin() に相当する部分を実装することを目指す訳ですが、 その前に、ここまでの内容を実際に動作確認してみましょう。フック関数が登録されて 実際に呼ばれていることを確認できるようにするために、とりあえず以下のように printk() でファイル名を出力するだけのフック関数に書き直して、コンパイルおよび ロードしてみてください。

---------- lsm-2.6.29.c ----------
static int ccs_open(struct file *f)
{
        printk(KERN_INFO "Opening '%s'\n", f->f_path.dentry->d_name.name);
        return 0;
}
---------- lsm-2.6.29.c ----------
---------- lsm-4.7.c ----------
static int ccs_file_open(struct file *f, const struct cred *cred)
{
        printk(KERN_INFO "Opening '%s'\n", f->f_path.dentry->d_name);
        return 0;
}
---------- lsm-4.7.c ----------

無事にコンパイルおよびロードできた場合、ファイルのオープン要求が発生する度に、 (ディレクトリ部分を除いた)ファイル名部分が表示されるようになる筈です。 きっと解らない部分があると思いますので、参考用に akari/test.c の改造結果例を 添付しておきます。( test-3.10.c が 3.10 用、 test-4.11.c が 4.11 用です。)

なお、フック関数の処理中に(そのフック関数が使用するコード/データを含む メモリ領域が)アンロードされるとクラッシュしてしまいます。アンロード時に クラッシュする可能性を排除するために、アンロード用の処理は実装されていません。 よって、同じモジュールを修正して再ロードする際には、システムを再起動することで アンロードするようにしてください。

4-2

第4章の次の事前課題について説明します。

オンアクセススキャンを行うためには、スキャン対象のファイルを識別できるようにする 必要があります。前回の事前課題では、ファイルのオープン要求が発生した際に (ディレクトリ部分を除いた)ファイル名部分が表示されるようにしましたが、 ディレクトリ部分が不明なままではファイルを識別できません。

そこで、今回は、ディレクトリ部分を含んだパス名を算出するという処理を実装します。 これにより、ファイルのオープンが要求された際に、絶対パス名が判明するため、 スキャン対象のファイルを識別できるようになる筈です。(実際には、名前空間という 機能が存在することにより、識別できない可能性があるのですが、この講義では 名前空間については扱いません。)

akari/realpath.c というファイルを参照してください。

この中には、指定されたパス( dentry 構造体のポインタと vfsmount 構造体のポインタを メンバとして含む path 構造体)を受け取って、パス名( char * )を計算するための処理が 書かれています。この講義で不要な処理を削っていくと、カーネル 3.10 および 4.11 で 使用するのは以下の部分だけになります。

---------- realpath.c ----------
/**
 * ccs_realpath - Returns realpath(3) of the given pathname but ignores chroot'ed root.
 *
 * @path: Pointer to "struct path".
 *
 * Returns the realpath of the given @path on success, NULL otherwise.
 *
 * This function uses kmalloc(), so caller must kfree() if this function
 * didn't return NULL.
 */
char *ccs_realpath(const struct path *path)
{
        char *buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
        char *name = NULL;
        char *pos;

        if (!buf)
                return NULL;
        pos = caitsith_exports.d_absolute_path(path, buf, PAGE_SIZE);
        if (!IS_ERR(pos))
                name = kstrdup(pos, GFP_KERNEL);
        kfree(buf);
        return name;
}
---------- realpath.c ----------

この関数に渡す path 構造体は、 ccs_open() 関数に渡されている file 構造体の f->f_path メンバーです。そのため、絶対パス名を表示するには ccs_open() 関数を以下のように書き換えることで実現できます。

---------- test.c ----------
static int ccs_open(struct file *f)
{
        char *pathname = ccs_realpath(path);
        if (pathname) {
                printk(KERN_INFO "Opening '%s'\n", pathname);
                kfree(pathname);
        }
        return 0;
}
---------- test.c ----------

さて、ここでクイズです。パス名の最大長は何バイトでしょうか?

マニュアルによると、パス名を渡すシステムコール( open() など)では PATH_MAX バイトが上限とあり、 PATH_MAX は 4096 バイト(4KB)と定義されています。 ということは、パス名の最大長は(文字列の終端を示すための \0 を含めて) 4096 バイトということになるのでしょうか?

実は「パス名の最大長に制限は無い」というのが正解です。 PATH_MAX バイトというのは パス名を文字列としてシステムコールに渡す際に渡すことができる上限であって、 相対パス名を使えば無限に長いパス名を作成/参照することが可能になります。

そのため、 PATH_MAX バイトよりも長いパス名を計算できるようにしたい場合には 以下のように割り当てるサイズを動的に変更していく必要があります。

---------- realpath.c ----------
char *ccs_realpath(const struct path *path)
{
        char *buf = kmalNULL;
        char *name = NULL;
        unsigned int buf_len = PAGE_SIZE / 2;

        while (1) {
                char *pos;

                buf_len <<= 1;
                kfree(buf);
                buf = kmalloc(buf_len, GFP_KERNEL);
                if (!buf)
                        break;
                pos = ccsecurity_exports.d_absolute_path(path, buf, buflen);
                if (IS_ERR(pos))
                        continue;
                name = kstrdup(pos, GFP_KERNEL);
                break;
        }
        kfree(buf);
        return name;
}
---------- realpath.c ----------

第2章の事前課題で、 kmalloc() を使って割り当て可能なメモリの上限がどれくらいかを 確認してもらいました。ユーザ空間では malloc() を用いてGBレベルのサイズでも (空きメモリさえあれば)割り当てることができましたが、カーネル空間では原則として 32KB以下しか割り当てできませんでした。(32KBより大きい場合、割り当てに 失敗する可能性が非常に高くなります。)そのため、上記のように割り当てるサイズを 動的に変更したとしても、無限に長いパス名を計算することはできないという制約事項が あります。なお、「無限に長いパス名を作成/参照しようとする悪意あるユーザによる アクセスを許した時点で負け」という考えのようで、この制約事項が解消される予定は ありません。

この講義では、オンアクセススキャンを行う対象となるファイルを open() システム コールに渡すことで読み込むため、結局は PATH_MAX バイトの制約を受けることに なってしまいますので、上記のように割り当てるサイズを動的に変更する必要は ありません。

ということで、ここまでの内容を実際に動作確認してみましょう。 無事にコンパイルおよびロードできた場合、ファイルのオープン要求が発生する度に、 ディレクトリ部分を含むファイル名が表示されるようになる筈です。 参考用に akari/test.c の改造結果例を添付しておきます。 ( test-3.10.c が 3.10 用、 test-4.11.c が 4.11 用です。)

Clone this wiki locally