All Articles

libclamav でバイトコードシグネチャのデバッグトレースを有効化する方法

先日、SECCON 2022 の Devil Hunter という問題をテーマに ClamAV のバイトコードシグネチャの作成と解析方法をまとめた記事を作成しました。

参考:CTF で学ぶ ClamAV シグネチャの作成と解析

この記事の中では扱いませんでしたが、バイトコードシグネチャの解析手法として、libclamav にパッチを当てることで実行されるバイトコードを追跡する神解法が存在します。

参考:hxp | SECCON CTF 2022 Quals

本記事では、上記の Writeup を参考に、libclamav にパッチを当てることでバイトコードシグネチャの動作を解析する手法についてまとめます。

なお、libclamav にパッチを当てる際には、ソースコードから ClamAV を自身でビルドする必要があります。

ClamAV のビルド方法については以下の記事でまとめています。

参考:ClamAV をソースコードからビルドして OnAccessScan をセットアップするまでのまとめ

もくじ

libclamav をカスタマイズして条件分岐時のオペランドをダンプする

この手法を利用するため、libclamav の bytecodevm.c 内の `DEFINEICMPOP(OPBCICMP_EQ, res = (op0 == op1));` の行を以下のように修正します。

// DEFINE_ICMPOP(OP_BC_ICMP_EQ, res = (op0 == op1));
DEFINE_ICMPOP(OP_BC_ICMP_EQ, printf("%d: %x == %x\n", bb_inst, op0, op1);res = (op0 == op1));

この変更によって、OP_BC_ICMP_EQ が呼び出される際に比較される 2 つの値をダンプすることができます。

実際にこの変更を行った環境で、clamscan によるスキャンを実施してみます。

以下は Fake Flag をスキャンした場合の結果です。

image-20240818150736162

また、以下は正解の Flag をスキャンした結果です。

image-20240818150754132

libclamav にシンプルなパッチを当てただけで、このバイトコードシグネチャがスキャン対象のテキストを加工した結果をハードコードされた整数値と比較していることを簡単に特定することができます。

バイトコードシグネチャのデバッグトレースを有効化する

libclamav では cli_byteinst_describe(inst, &bbnum); を使用して実行中のバイトコードをトレース可能な TRACE_INST が用意されていますが、この機能はデフォルトで無効化されています。

有効化するためには CL_DEBUG のフラグを立て、TRACE_INST を含むセクションの #if 0#if CL_DEBUG に変更します。

参考:clamav/libclamav/bytecode_vm.c at patch-libclamav · kash1064/clamav

[+] #define CL_DEBUG 1

***

[-] #if 0 /* too verbose, use #ifdef CL_DEBUG if needed */
[+] #if CL_DEBUG /* too verbose, use #ifdef CL_DEBUG if needed */
#define CHECK_UNREACHABLE                                \
    do {                                                 \
        cli_dbgmsg("bytecode: unreachable executed!\n"); \
        return CL_EBYTECODE;                             \
    } while (0)
#define TRACE_PTR(ptr, s) cli_dbgmsg("bytecode trace: ptr %llx, +%x\n", ptr, s);
#define TRACE_R(x) cli_dbgmsg("bytecode trace: %u, read %llx\n", pc, (long long)x);
#define TRACE_W(x, w, p) cli_dbgmsg("bytecode trace: %u, write%d @%u %llx\n", pc, p, w, (long long)(x));
#define TRACE_EXEC(id, dest, ty, stack) cli_dbgmsg("bytecode trace: executing %d, -> %u (%u); %u\n", id, dest, ty, stack)
#define TRACE_INST(inst)                                                   \
    do {                                                                   \
        unsigned bbnum = 0;                                                \
        printf(""); \
        cli_byteinst_describe(inst, &bbnum);                               \
        printf("\n");                                                      \
    } while (0)

これで再ビルドした ClamAV を使用してスキャンを行うと、以下のように実行時のバイトコードトレースを参照できるようになります。

image-20240818223620961

しかし、以下のコードを見るとわかる通り、cli_byteinst_describe で出力可能なトレースは clambc コマンドで逆アセンブルしたものと同じように、オペランドは変数で表されており、実際の値を参照することはできません。

void cli_byteinst_describe(const struct cli_bc_inst *inst, unsigned *bbnum)
{
    size_t j;
    char inst_str[256];
    const struct cli_apicall *api;

    if (inst->opcode > OP_BC_INVALID) {
        printf("opcode %u[%u] of type %u is not implemented yet!",
               inst->opcode, inst->interp_op / 5, inst->interp_op % 5);
        return;
    }

    snprintf(inst_str, sizeof(inst_str), "%-20s[%-3d/%3d/%3d]", bc_opstr[inst->opcode],
             inst->opcode, inst->interp_op, inst->interp_op % inst->opcode);
    printf("%-35s", inst_str);
    switch (inst->opcode) {
            // binary operations
        case OP_BC_ADD:
            printf("%d = %d + %d", inst->dest, inst->u.binop[0], inst->u.binop[1]);
            break;
        case OP_BC_SUB:
            printf("%d = %d - %d", inst->dest, inst->u.binop[0], inst->u.binop[1]);
            break;
        case OP_BC_MUL:
            printf("%d = %d * %d", inst->dest, inst->u.binop[0], inst->u.binop[1]);
            break;       
***

参考:clamav/libclamav/bytecode.c at main · Cisco-Talos/clamav

そこで、TRACE_INST に加えて TRACE_PTRTRACE_RTRACE_WTRACE_EXECTRACE_API のデバッグ出力を標準出力に返すように libclamav のコードを修正しました。

// #define TRACE_PTR(ptr, s) cli_dbgmsg("bytecode trace: ptr %llx, +%x\n", ptr, s);
// #define TRACE_R(x) cli_dbgmsg("bytecode trace: %u, read %llx\n", pc, (long long)x);
// #define TRACE_W(x, w, p) cli_dbgmsg("bytecode trace: %u, write%d @%u %llx\n", pc, p, w, (long long)(x));
// #define TRACE_EXEC(id, dest, ty, stack) cli_dbgmsg("bytecode trace: executing %d, -> %u (%u); %u\n", id, dest, ty, stack)

#define TRACE_PTR(ptr, s) printf("ptr %llx, +%x\n", ptr, s);
#define TRACE_R(x) printf("%u, read %llx\n", pc, (long long)x);
#define TRACE_W(x, w, p) printf("%u, write%d @%u %llx\n", pc, p, w, (long long)(x));
#define TRACE_EXEC(id, dest, ty, stack) printf("bytecode trace: executing %d, -> %u (%u); %u\n", id, dest, ty, stack)

#define TRACE_INST(inst)                                                   \
    do {                                                                   \
        unsigned bbnum = 0;                                                \
        printf(""); \
        cli_byteinst_describe(inst, &bbnum);                               \
        printf("\n");                                                      \
    } while (0)

// #define TRACE_API(s, dest, ty, stack) cli_dbgmsg("bytecode trace: executing %s, -> %u (%u); %u\n", s, dest, ty, stack)
#define TRACE_API(s, dest, ty, stack) printf("bytecode trace: executing %s, -> %u (%u); %u\n", s, dest, ty, stack)

参考:clamav/libclamav/bytecode_vm.c at patch-libclamav · kash1064/clamav

この修正を有効化すると、以下のようにメモリの read/write などのトレースを行うことができるようになります。

image-20240819002036877

上記の出力を見るとわかる通り、トレースした実行コードの直後に、メモリの read/write の情報が出力されます。

例えば以下の部分では、v640 と v1240 がそれぞれ 0x6cbfdd9f を read して OP_BC_ICMP_EQ による比較を行い、その結果(1) を @644 に格納しています。

OP_BC_ICMP_EQ       [21 /108/  3]  644 = (640 == 1240)
1444, read 6cbfdd9f
1444, read 6cbfdd9f
1444, write8 @644 1

また、以下の部分では、p.248 のポインタから 0x33547962(3Tyb)という 4 文字分の整数値を read し、v256 に書き込んだ上で Func2 の引数として関数呼び出しを行っていることを確認できます。

OP_BC_LOAD          [39 /198/  3]  load  256 <- p.248
530, read fffffffe00000019
ptr fffffffe00000019, +4
530, read 617f7db2ab69
530, write32 @256 33547962

OP_BC_CALL_DIRECT   [32 /163/  3]  260 = call F.2 (256)
bytecode trace: executing 2, -> 260 (32); 2

このようにデバッグトレースを有効化すると、前回の記事のように頑張って逆アセンブルコードを読まなくても、Flag のテキストが 4 文字ずつに分割され、32bit 整数値として Func2 に受け渡されることを簡単に特定することができます。

まとめ

デバッグトレースめちゃ便利でした。

今後バイトコードシグネチャ問がでても余裕で解けそうです。