All Articles

clamscan でファイルスキャンを行うまでの動作に関するメモ書き(途中まで)

特に目的なく ClamAV のコードを読み進めており、Clamscan でファイルスキャンを行うまでの動作に関するメモ書きを公開します。

今回は、オープンソースの AntiVirus ソフトウェアである ClamAV の clamscan ツールについてまとめます。

前回は、以下の記事で clamdscan を使用して Eicar ファイルを検出するまでの動作を確認した結果をまとめてみたのですが、今回は libclamav の初期化やシグネチャのロードに関する動作を確認してみたく、Onetime-Scanner である clamscan を使用することにしました。

参考:ClamAV で Eicar テストファイルを検出するまでのスキャン動作を追ってみたメモ書き - かえるのひみつきち

もくじ

clamscan の概要

clamscan ツールは、libclamav を使用してファイルスキャンを行うためのコマンドラインツールです。

clamdscan とは異なり、clamscan はシステム内で clamd デーモンが稼働していない場合でもスキャンを行うことが可能です。

そのため、clamscan は基本的に実行時にシグネチャのロードが行われ、コマンドの実行後スキャンが行われるまでに少々時間を要します。

image-20251018151701287

参考:Scanning - ClamAV Documentation

この記事では、clamscan の実装を解析していくことで、ClamAV によるマルウェアスキャンの動作を理解することを目指します。

clamscan のオプション

今回使用する ClamAV 1.5.0 の clamscan には以下のように多数のオプションが用意されています。

$ clamscan --help

                       Clam AntiVirus: Scanner 1.5.0
           By The ClamAV Team: https://www.clamav.net/about.html#credits
           (C) 2025 Cisco Systems, Inc.

    clamscan [options] [file/directory/-]

    --help                -h             Show this help.
    --version             -V             Print version number.
    --verbose             -v             Be verbose.
    --archive-verbose     -a             Show filenames inside scanned archives.
    --debug                              Enable libclamav's debug messages.
    --quiet                              Only output error messages.
    --stdout                             Write to stdout instead of stderr. Does not affect 'debug' messages.
    --no-summary                         Disable summary at end of scanning.
    --infected            -i             Only print infected files.
    --suppress-ok-results -o             Skip printing OK files.
    --bell                               Sound bell on virus detection.

    --tempdir=DIRECTORY                  Create temporary files in DIRECTORY.
    --leave-temps[=yes/no(*)]            Do not remove temporary files.
    --force-to-disk[=yes/no(*)]          Create temporary files for nested file scans that would otherwise be in-memory only.
    --gen-json[=yes/no(*)]               Generate JSON metadata for the scanned file(s). For testing & development use ONLY.
                                         JSON will be printed if --debug is enabled.
                                         A JSON file will dropped to the temp directory if --leave-temps is enabled.
    --json-store-html-uris[=yes(*)/no]   Store html URIs in metadata.
                                         URIs will be written to the metadata.json file in an array called 'URIs'.
    --json-store-pdf-uris[=yes(*)/no]    Store pdf URIs in metadata.
                                         URIs will be written to the metadata.json file in an array called 'URIs'.
    --json-store-extra-hashes[=yes(*)/no] Store md5 and sha1 in addition to sha2-256 in metadata.
    --database=FILE/DIR   -d FILE/DIR    Load virus database from FILE or load all supported db files from DIR.
    --official-db-only[=yes/no(*)]       Only load official signatures.
    --fail-if-cvd-older-than=days        Return with a nonzero error code if virus database outdated.
    --log=FILE            -l FILE        Save scan report to FILE.
    --recursive[=yes/no(*)]  -r          Scan subdirectories recursively.
    --allmatch[=yes/no(*)]   -z          Continue scanning within file after finding a match.
    --cross-fs[=yes(*)/no]               Scan files and directories on other filesystems.
    --follow-dir-symlinks[=0/1(*)/2]     Follow directory symlinks (0 = never, 1 = direct, 2 = always).
    --follow-file-symlinks[=0/1(*)/2]    Follow file symlinks (0 = never, 1 = direct, 2 = always).
    --file-list=FILE      -f FILE        Scan files from FILE.
    --remove[=yes/no(*)]                 Remove infected files. Be careful!
    --move=DIRECTORY                     Move infected files into DIRECTORY.
    --copy=DIRECTORY                     Copy infected files into DIRECTORY.
    --exclude=REGEX                      Don't scan file names matching REGEX.
    --exclude-dir=REGEX                  Don't scan directories matching REGEX.
    --include=REGEX                      Only scan file names matching REGEX.
    --include-dir=REGEX                  Only scan directories matching REGEX.

    --bytecode[=yes(*)/no]               Load bytecode from the database.
    --bytecode-unsigned[=yes/no(*)]      Load unsigned bytecode.
                                         **Caution**: You should NEVER run bytecode signatures from untrusted sources.
                                         Doing so may result in arbitrary code execution.
    --bytecode-timeout=N                 Set bytecode timeout (in milliseconds).
    --statistics[=none(*)/bytecode/pcre] Collect and print execution statistics.
    --detect-pua[=yes/no(*)]             Detect Possibly Unwanted Applications.
    --exclude-pua=CAT                    Skip PUA sigs of category CAT.
    --include-pua=CAT                    Load PUA sigs of category CAT.
    --detect-structured[=yes/no(*)]      Detect structured data (SSN, Credit Card).
    --structured-ssn-format=X            SSN format (0=normal,1=stripped,2=both).
    --structured-ssn-count=N             Min SSN count to generate a detect.
    --structured-cc-count=N              Min CC count to generate a detect.
    --structured-cc-mode=X               CC mode (0=credit debit and private label, 1=credit cards only.
    --scan-mail[=yes(*)/no]              Scan mail files.
    --phishing-sigs[=yes(*)/no]          Enable email signature-based phishing detection.
    --phishing-scan-urls[=yes(*)/no]     Enable URL signature-based phishing detection.
    --heuristic-alerts[=yes(*)/no]       Heuristic alerts.
    --heuristic-scan-precedence[=yes/no(*)] Stop scanning as soon as a heuristic match is found.
    --normalize[=yes(*)/no]              Normalize html, script, and text files. Use normalize=no for yara compatibility.
    --scan-pe[=yes(*)/no]                Scan PE files.
    --scan-elf[=yes(*)/no]               Scan ELF files.
    --scan-ole2[=yes(*)/no]              Scan OLE2 containers.
    --scan-pdf[=yes(*)/no]               Scan PDF files.
    --scan-swf[=yes(*)/no]               Scan SWF files.
    --scan-html[=yes(*)/no]              Scan HTML files.
    --scan-xmldocs[=yes(*)/no]           Scan xml-based document files.
    --scan-hwp3[=yes(*)/no]              Scan HWP3 files.
    --scan-onenote[=yes(*)/no]           Scan OneNote files.
    --scan-archive[=yes(*)/no]           Scan archive files (supported by libclamav).
    --scan-image[=yes(*)/no]             Scan image (graphics) files.
    --scan-image-fuzzy-hash[=yes(*)/no]  Detect files by calculating image (graphics) fuzzy hashes.
    --alert-broken[=yes/no(*)]           Alert on broken executable files (PE & ELF).
    --alert-broken-media[=yes/no(*)]     Alert on broken graphics files (JPEG, TIFF, PNG, GIF).
    --alert-encrypted[=yes/no(*)]        Alert on encrypted archives and documents.
    --alert-encrypted-archive[=yes/no(*)] Alert on encrypted archives.
    --alert-encrypted-doc[=yes/no(*)]    Alert on encrypted documents.
    --alert-macros[=yes/no(*)]           Alert on OLE2 files containing VBA macros.
    --alert-exceeds-max[=yes/no(*)]      Alert on files that exceed max file size, max scan size, or max recursion limit.
    --alert-phishing-ssl[=yes/no(*)]     Alert on emails containing SSL mismatches in URLs.
    --alert-phishing-cloak[=yes/no(*)]   Alert on emails containing cloaked URLs.
    --alert-partition-intersection[=yes/no(*)] Alert on raw DMG image files containing partition intersections.
    --nocerts                            Disable authenticode certificate chain verification in PE files.
    --dumpcerts                          Dump authenticode certificate chain in PE files.

    --max-scantime=#n                    Scan time longer than this will be skipped and assumed clean (milliseconds).
    --max-filesize=#n                    Files larger than this will be skipped and assumed clean.
    --max-scansize=#n                    The maximum amount of data to scan for each container file (**).
    --max-files=#n                       The maximum number of files to scan for each container file (**).
    --max-recursion=#n                   Maximum archive recursion level for container file (**).
    --max-dir-recursion=#n               Maximum directory recursion level.
    --max-embeddedpe=#n                  Maximum size file to check for embedded PE.
    --max-htmlnormalize=#n               Maximum size of HTML file to normalize.
    --max-htmlnotags=#n                  Maximum size of normalized HTML file to scan.
    --max-scriptnormalize=#n             Maximum size of script file to normalize.
    --max-ziptypercg=#n                  Maximum size zip to type reanalyze.
    --max-partitions=#n                  Maximum number of partitions in disk image to be scanned.
    --max-iconspe=#n                     Maximum number of icons in PE file to be scanned.
    --max-rechwp3=#n                     Maximum recursive calls to HWP3 parsing function.
    --pcre-match-limit=#n                Maximum calls to the PCRE match function.
    --pcre-recmatch-limit=#n             Maximum recursive calls to the PCRE match function.
    --pcre-max-filesize=#n               Maximum size file to perform PCRE subsig matching.
    --disable-cache                      Disable caching and cache checks for hash sums of scanned files.
    --hash-hint                          The file hash so that libclamav does not need to calculate it.
                                         The type of hash must match the '--hash-alg'.
    --log-hash                           Print the file hash after each file scanned.
                                         The type of hash printed will match the '--hash-alg'.
    --hash-alg                           The hashing algorithm used for either '--hash-hint' or '--log-hash'.
                                         Supported algorithms are 'md5', 'sha1', 'sha2-256'.
                                         If not specified, the default is 'sha2-256'.
    --file-type-hint                     The file type hint so that libclamav can optimize scanning.
                                         E.g. 'pe', 'elf', 'zip', etc.
                                         You may also use ClamAV type names such as 'CL_TYPE_PE'.
                                         ClamAV will ignore the hint if it is not familiar with the specified type.
                                         See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
    --log-file-type                      Print the file type after each file scanned.
    --cvdcertsdir=DIRECTORY              Specify a directory containing the root
                                         CA cert needed to verify detached CVD digital signatures.
                                         If not provided, then clamscan will look in the default directory.
    --fips-limits                        Enforce FIPS-like limits on using hash algorithms for
                                         cryptographic purposes. Will disable MD5 & SHA1.
                                         FP sigs and will require '.sign' files to verify CVD
                                         authenticity.

Environment Variables:

    LD_LIBRARY_PATH                      May be used on startup to find the libclamunrar_iface
                                         shared library module to enable RAR archive support.
    CVD_CERTS_DIR                        Specify a directory containing the root CA cert needed
                                         to verify detached CVD digital signatures.
                                         If not provided, then clamscan will look in the default directory.

Pass in - as the filename for stdin.

(*) Default scan settings
(**) Certain files (e.g. documents, archives, etc.) may in turn contain other
   files inside. The above options ensure safe processing of this kind of data.

clamscan で Eicar ファイルを検出する

まずは、以下のコマンドで clamscan により Eicar テストマルウェアを検出するまでのデバッグログをファイルとして保存します。

clamscan のデバッグ情報は stdout ではなく stderr に出力されるようなので、2>&1 にて stderr を stdout にリダイレクトしてからパイプにつないでいます。

(出力先を stdout に変更する --stdout オプションはデバッグメッセージには影響しません。)

clamscan --debug --disable-cache --verbose --stdout eicar 2>&1 | tee logfile.txt

gdb でデバッグを行う場合には、以下のように gdb --args を使用できます。

gdb --args clamscan --debug --disable-cache --verbose --stdout eicar 2>&1

こうして出力されたデバッグログは、およそ 1500 行ほどの情報でした。

また、今回は summary を無効化するオプションを使用していないので、最終的には以下のような summary と共に Eicar ファイルが clamscan により検出されたことを示す出力が返されました。

image-20251019125033073

以降は、Eicar ファイルを clamscan で検出するまでの一連の流れを追っていきます。

scanmanager 関数の呼び出し

一番初めに出力されたデバッグメッセージは以下の一連の情報でした。

LibClamAV debug: searching for unrar, user-searchpath: /usr/local/lib
LibClamAV debug: unrar support loaded from /usr/local/lib/libclamunrar_iface.so.12.1.0
LibClamAV debug: Initialized 1.5.0 engine
LibClamAV debug: Initializing phishcheck module
LibClamAV debug: Phishcheck: Compiling regex: ^ *(http|https|ftp:(//)?)?[0-9]{1,3}(\.[0-9]{1,3}){3}[/?:]? *$
LibClamAV debug: Phishcheck module initialized
LibClamAV debug: Bytecode initialized in interpreter mode
LibClamAV debug: clean_cache_init: Caching disabled.
LibClamAV debug: clean_cache_init: Cache initialized successfully.
LibClamAV debug: Adding certificate to verifier store: X509 { serial_number: "0493F2B851C5D5BED1", signature_algorithm: sha512WithRSAEncryption, issuer: [organizationalUnitName = "Arbor", organizationName = "Cisco", commonName = "Cisco Software Identity Root CA RSA 4096 SHA512 2099"], subject: [organizationalUnitName = "Arbor", organizationName = "Cisco", commonName = "Cisco Software Identity Root CA RSA 4096 SHA512 2099"], not_before: Jan 24 18:45:25 2024 GMT, not_after: Jan 24 18:45:25 2099 GMT, public_key: PKey { algorithm: "RSA" } }
LibClamAV debug: Verifier created successfully

1 行目の出力は、メッセージの内容から *load_module(const char *name, const char *featurename) 内の以下の箇所であると考えられます。

/*
* Search in "<prefix>/lib" checking with each of the different possible suffixes.
*/
cli_dbgmsg("searching for %s, user-searchpath: %s\n", featurename, SEARCH_LIBDIR);

参考:others.c - libclamav - Cisco-Talos/clamav - Sourcegraph

また、このメソッドは同コード内の rarload 関数から呼び出されているようです。

image-20251018175708668

さらに、デバッガでここまでの一連のコールスタックを取得してみると、これらの libclamav の関数は clamscan の scanmanager 関数から呼び出されていることを確認できます。

image-20251018220749733

ここまでの一連の処理については詳しくは記載しませんが大まかには以下の動作になっているようです。

  • clamscan::main でコマンドライン引数を opts 構造体に保存し、optget 関数を使用してコマンドライン引数からいくつかのオプションを有効化する
struct optstruct {
    char *name;
    char *cmd;
    char *strarg;
    long long numarg;
    int enabled;
    int active;
    int flags;
    int idx;
    struct optstruct *nextarg;
    struct optstruct *next;

    char **filename; /* cmdline */
};
  • グローバル変数として定義されている s_info 構造体の変数 info のメモリ領域を memset で初期化する
struct s_info {
    unsigned int sigs;         /* number of signatures */
    unsigned int dirs;         /* number of scanned directories */
    unsigned int files;        /* number of scanned files */
    unsigned int ifiles;       /* number of infected files */
    unsigned int errors;       /* number of errors */
    unsigned long int blocks;  /* number of *scanned* 16kb blocks */
    unsigned long int rblocks; /* number of *read* 16kb blocks */
};
  • コマンドライン引数と共に scanmanager 関数を呼び出す
  • scanmanager 関数は、libclamav 初期化を行い、スキャン対象を決定してスキャンを行う

なお、上記の流れで呼び出される scanmanager 関数は、clamscan にてファイルスキャンを要求する実質的な主体として機能します。

そのため、scanmanager 関数から結果が返されると、clamscan の実行も終了します。

参考:clamscan.c - clamscan - Cisco-Talos/clamav - Sourcegraph

libclamav の初期化

スキャンオプションの取得

scanmanager 関数が呼び出されると、まずは clscanoptions 構造体の変数 options の初期化が行われます。

/* Initalize scan options struct */
memset(&options, 0, sizeof(struct cl_scan_options));

この構造体には、スキャン時に使用するスキャンオプションが保存されます。

clscanoptions 構造体には general や heuristic などのメンバが定義されており、これらのビットマスクを操作することでオプションの変更を行うようです。

/*** scan options ***/
struct cl_scan_options {
    uint32_t general;
    uint32_t parse;
    uint32_t heuristic;
    uint32_t mail;
    uint32_t dev;
};

1 つ目の general オプションは、主に以下のコードで操作が行われるようです。

/* general */
#define CL_SCAN_GENERAL_ALLMATCHES                  0x1  /* scan in all-match mode */
#define CL_SCAN_GENERAL_COLLECT_METADATA            0x2  /* collect metadata (--gen-json) */
#define CL_SCAN_GENERAL_HEURISTICS                  0x4  /* option to enable heuristic alerts */
#define CL_SCAN_GENERAL_HEURISTIC_PRECEDENCE        0x8  /* allow heuristic match to take precedence. */
#define CL_SCAN_GENERAL_UNPRIVILEGED                0x10 /* scanner will not have read access to files. */

/* set scan options */
if (optget(opts, "allmatch")->enabled) {
    options.general |= CL_SCAN_GENERAL_ALLMATCHES;
}

if (optget(opts, "heuristic-scan-precedence")->enabled)
    options.general |= CL_SCAN_GENERAL_HEURISTIC_PRECEDENCE;

/* TODO: Remove deprecated option in a future feature release */
if ((optget(opts, "algorithmic-detection")->enabled) && /* && used due to default-yes for both options */
    (optget(opts, "heuristic-alerts")->enabled)) {
    options.general |= CL_SCAN_GENERAL_HEURISTICS;
}

/* JSON check to prevent engine loading if specified without libjson-c  */
if (optget(opts, "gen-json")->enabled)
    options.general |= CL_SCAN_GENERAL_COLLECT_METADATA;

上記のうち、heuristic-scan-precedence のオプションは、ヒューリスティックにマッチングした場合にすぐにスキャンを停止することを指定するオプションです。

また、gen-json オプションはテストや開発目的で、スキャンしたファイルのメタデータを含む JSON 形式の情報の生成を指示するオプションです。

デバッグモードでこのオプションを使用した場合、以下のような JSON 形式の情報が表示されるようです。

image-20251019132207507

2 つ目の parse オプションは、エンジンが parse するファイルタイプを指定するフラグのようです。

/* parsing capabilities options */
#define CL_SCAN_PARSE_ARCHIVE                       0x1
#define CL_SCAN_PARSE_ELF                           0x2
#define CL_SCAN_PARSE_PDF                           0x4
#define CL_SCAN_PARSE_SWF                           0x8
#define CL_SCAN_PARSE_HWP3                          0x10
#define CL_SCAN_PARSE_XMLDOCS                       0x20
#define CL_SCAN_PARSE_MAIL                          0x40
#define CL_SCAN_PARSE_OLE2                          0x80
#define CL_SCAN_PARSE_HTML                          0x100
#define CL_SCAN_PARSE_PE                            0x200

また、heuristic オプションはいくつかのヒューリスティックアラートを有効化します。

/* heuristic alerting options */
#define CL_SCAN_HEURISTIC_BROKEN                    0x2    /* alert on broken PE and broken ELF files */
#define CL_SCAN_HEURISTIC_EXCEEDS_MAX               0x4    /* alert when files exceed scan limits (filesize, max scansize, or max recursion depth) */
#define CL_SCAN_HEURISTIC_PHISHING_SSL_MISMATCH     0x8    /* alert on SSL mismatches */
#define CL_SCAN_HEURISTIC_PHISHING_CLOAK            0x10   /* alert on cloaked URLs in emails */
#define CL_SCAN_HEURISTIC_MACROS                    0x20   /* alert on OLE2 files containing macros */
#define CL_SCAN_HEURISTIC_ENCRYPTED_ARCHIVE         0x40   /* alert if archive is encrypted (rar, zip, etc) */
#define CL_SCAN_HEURISTIC_ENCRYPTED_DOC             0x80   /* alert if a document is encrypted (pdf, docx, etc) */
#define CL_SCAN_HEURISTIC_PARTITION_INTXN           0x100  /* alert if partition table size doesn't make sense */
#define CL_SCAN_HEURISTIC_STRUCTURED                0x200  /* data loss prevention options, i.e. alert when detecting personal information */
#define CL_SCAN_HEURISTIC_STRUCTURED_SSN_NORMAL     0x400  /* alert when detecting social security numbers */
#define CL_SCAN_HEURISTIC_STRUCTURED_SSN_STRIPPED   0x800  /* alert when detecting stripped social security numbers */
#define CL_SCAN_HEURISTIC_STRUCTURED_CC             0x1000 /* alert when detecting credit card numbers */
#define CL_SCAN_HEURISTIC_BROKEN_MEDIA              0x2000 /* alert if a file does not match the identified file format, works with JPEG, TIFF, GIF, PNG */

そして、mail と dev オプションそれぞれ以下の機能を有効化します。

/* mail scanning options */
#define CL_SCAN_MAIL_PARTIAL_MESSAGE                0x1

/* dev options */
#define CL_SCAN_DEV_COLLECT_SHA                     0x1 /* Enables hash output in sha-collect builds - for internal use only */
#define CL_SCAN_DEV_COLLECT_PERFORMANCE_INFO        0x2 /* collect performance timings */

dboption の設定

scanmanager 関数の中では、unsigned int dboptions = 0 で初期化された dboptions に対してビットマスクの設定も行います。

dboptions にセットされるフラグは以下の通り定義されています。

/* db options */
// clang-format off
#define CL_DB_PHISHING          0x2
#define CL_DB_PHISHING_URLS     0x8
#define CL_DB_PUA               0x10
#define CL_DB_CVDNOTMP          0x20    /* obsolete */
#define CL_DB_OFFICIAL          0x40    /* internal */
#define CL_DB_PUA_MODE          0x80
#define CL_DB_PUA_INCLUDE       0x100
#define CL_DB_PUA_EXCLUDE       0x200
#define CL_DB_COMPILED          0x400   /* internal */
#define CL_DB_DIRECTORY         0x800   /* internal */
#define CL_DB_OFFICIAL_ONLY     0x1000
#define CL_DB_BYTECODE          0x2000
#define CL_DB_SIGNED            0x4000  /* internal */
#define CL_DB_BYTECODE_UNSIGNED 0x8000  /* Caution: You should never run bytecode signatures from untrusted sources. Doing so may result in arbitrary code execution. */
#define CL_DB_UNSIGNED          0x10000 /* internal */
#define CL_DB_BYTECODE_STATS    0x20000
#define CL_DB_ENHANCED          0x40000
#define CL_DB_PCRE_STATS        0x80000
#define CL_DB_YARA_EXCLUDE      0x100000
#define CL_DB_YARA_ONLY         0x200000

/* recommended db settings */
#define CL_DB_STDOPT (CL_DB_PHISHING | CL_DB_PHISHING_URLS | CL_DB_BYTECODE)

この dboptions は scanmanager 関数の中で Virus database をロードする cl_load 関数の呼び出し時に引数として使用されます。

if ((opt = optget(opts, "database"))->active) {
    while (opt) {
        if ((ret = cl_load(opt->strarg, engine, &info.sigs, dboptions))) {
            logg("!%s\n", cl_strerror(ret));

            ret = 2;
            goto done;
        }

        opt = opt->nextarg;
    }
} else {
    char *dbdir = freshdbdir();

    if ((ret = cl_load(dbdir, engine, &info.sigs, dboptions))) {
        logg("!%s\n", cl_strerror(ret));

        free(dbdir);
        ret = 2;
        goto done;
    }

    free(dbdir);
}

例えば、CL_DB_PHISHING が有効化されている場合はフィッシングシグネチャがロードされ、CL_DB_PUA が有効化されている場合には PUA シグネチャがロードされます。

参考:libclamav - ClamAV Documentation

clinit 関数と clengine_new 関数

scanmanager 関数では libclamav の初期化のため、以下の関数が順に呼び出されます。

int cl_init(unsigned int options);
struct cl_engine *cl_engine_new(void);

割り当てられたエンジンのリソースは int cl_engine_free(struct cl_engine *engine); で解放されます。

cl_engine_new で初期化されるエンジンは cl_engine 構造体として以下の通り定義されています。

struct cl_engine {
    uint32_t refcount; /* reference counter */
    uint32_t sdb;
    uint32_t dboptions;
    uint32_t dbversion[2];
    uint32_t ac_only;
    uint32_t ac_mindepth;
    uint32_t ac_maxdepth;
    char *tmpdir;
    uint32_t keeptmp;
    uint64_t engine_options;

    /* Limits */
    uint32_t maxscantime; /* Time limit (in milliseconds) */
    uint64_t maxscansize; /* during the scanning of archives this size
				           * will never be exceeded
				           */
    uint64_t maxfilesize; /* compressed files will only be decompressed
				           * and scanned up to this size
				           */
    uint32_t maxreclevel; /* maximum recursion level for archives */
    uint32_t maxfiles;    /* maximum number of files to be scanned
				           * within a single archive
				           */
    /* This is for structured data detection.  You can set the minimum
     * number of occurrences of an CC# or SSN before the system will
     * generate a notification.
     */
    uint32_t min_cc_count;
    uint32_t min_ssn_count;

    /* Roots table */
    struct cli_matcher **root;

    /* hash matcher for standard MD5 sigs */
    struct cli_matcher *hm_hdb;
    /* hash matcher for MD5 sigs for PE sections */
    struct cli_matcher *hm_mdb;
    /* hash matcher for MD5 sigs for PE import tables */
    struct cli_matcher *hm_imp;
    /* hash matcher for allow list db */
    struct cli_matcher *hm_fp;

    /* Container metadata */
    struct cli_cdb *cdb;

    /* Phishing .pdb and .wdb databases*/
    struct regex_matcher *allow_list_matcher;
    struct regex_matcher *domain_list_matcher;
    struct phishcheck *phishcheck;

    /* Dynamic configuration */
    struct cli_dconf *dconf;

    /* Filetype definitions */
    struct cli_ftype *ftypes;
    struct cli_ftype *ptypes;

    /* Container password storage */
    struct cli_pwdb **pwdbs;

    /* Pre-loading test matcher
     * Test for presence before using; cleared on engine compile.
     */
    struct cli_matcher *test_root;

    /* Ignored signatures */
    struct cli_matcher *ignored;

    /* PUA categories (to be included or excluded) */
    char *pua_cats;

    /* Icon reference storage */
    struct icon_matcher *iconcheck;

    /* Negative cache storage */
    struct CACHE *cache;

    /* Database information from .info files */
    struct cli_dbinfo *dbinfo;

    /* Signature counting, for progress callbacks */
    size_t num_total_signatures;

    /* Used for memory pools */
    mpool_t *mempool;

    /* crtmgr stuff */
    crtmgr cmgr;

    /* Callback(s) */
    clcb_pre_cache cb_pre_cache;
    clcb_pre_scan cb_pre_scan;
    clcb_post_scan cb_post_scan;
    clcb_virus_found cb_virus_found;
    clcb_sigload cb_sigload;
    void *cb_sigload_ctx;
    clcb_hash cb_hash;
    clcb_meta cb_meta;
    clcb_file_props cb_file_props;
    clcb_progress cb_sigload_progress;
    void *cb_sigload_progress_ctx;
    clcb_progress cb_engine_compile_progress;
    void *cb_engine_compile_progress_ctx;
    clcb_progress cb_engine_free_progress;
    void *cb_engine_free_progress_ctx;

    /* Used for bytecode */
    struct cli_all_bc bcs;
    unsigned *hooks[_BC_LAST_HOOK - _BC_START_HOOKS];
    unsigned hooks_cnt[_BC_LAST_HOOK - _BC_START_HOOKS];
    unsigned hook_lsig_ids;
    enum bytecode_security bytecode_security;
    uint32_t bytecode_timeout;
    enum bytecode_mode bytecode_mode;

    /* Engine max settings */
    uint64_t maxembeddedpe;      /* max size to scan MSEXE for PE */
    uint64_t maxhtmlnormalize;   /* max size to normalize HTML */
    uint64_t maxhtmlnotags;      /* max size for scanning normalized HTML */
    uint64_t maxscriptnormalize; /* max size to normalize scripts */
    uint64_t maxziptypercg;      /* max size to re-do zip filetype */

    /* Statistics/intelligence gathering */
    void *stats_data;
    clcb_stats_add_sample cb_stats_add_sample;
    clcb_stats_remove_sample cb_stats_remove_sample;
    clcb_stats_decrement_count cb_stats_decrement_count;
    clcb_stats_submit cb_stats_submit;
    clcb_stats_flush cb_stats_flush;
    clcb_stats_get_num cb_stats_get_num;
    clcb_stats_get_size cb_stats_get_size;
    clcb_stats_get_hostid cb_stats_get_hostid;

    /* Raw disk image max settings */
    uint32_t maxpartitions; /* max number of partitions to scan in a disk image */

    /* Engine max settings */
    uint32_t maxiconspe; /* max number of icons to scan for PE */
    uint32_t maxrechwp3; /* max recursive calls for HWP3 parsing */

    /* PCRE matching limitations */
    uint64_t pcre_match_limit;
    uint64_t pcre_recmatch_limit;
    uint64_t pcre_max_filesize;

#ifdef HAVE_YARA
    /* YARA */
    struct _yara_global *yara_global;
#endif
};

参考:libclamav - ClamAV Documentation

ウイルス検知時のコールバック関数の割り当て

エンジンの初期化の完了後は、clenginesetclcbvirus_found 関数によりエンジンがウイルスを検知した際に実行するコールバック関数を割り当てできます。

既定では以下の clamscanvirusfound_cb 関数がコールバック関数として割り当てられており、ファイルと検出名を表示するコードが実装されています。

static void clamscan_virus_found_cb(int fd, const char *virname, void *context)
{
    struct clamscan_cb_data *data = (struct clamscan_cb_data *)context;
    const char *filename;

    UNUSEDPARAM(fd);

    if (data == NULL)
        return;
    if (data->filename != NULL)
        filename = data->filename;
    else
        filename = "(filename not set)";
    logg("~%s: %s FOUND\n", filename, virname);
    return;
}

cl_engine_set_clcb_virus_found(engine, clamscan_virus_found_cb);

進捗バーの表示

続く以下のコードでは、出力先がターミナルであり、かついくつかのオプションがセットされていない場合にのみ、シグネチャのロードやエンジンのコンパイルなどの時間のかかる処理を進捗バー付きで実行します。

if (isatty(fileno(stdout)) &&
    !optget(opts, "debug")->enabled &&
    !optget(opts, "quiet")->enabled &&
    !optget(opts, "infected")->enabled &&
    !optget(opts, "no-summary")->enabled) {
    /* set progress callbacks */
    cl_engine_set_clcb_sigload_progress(engine, sigload_callback, &sigload_progress_ctx);
    cl_engine_set_clcb_engine_compile_progress(engine, engine_compile_callback, &engine_compile_progress_ctx);
#ifdef ENABLE_ENGINE_FREE_PROGRESSBAR
    cl_engine_set_clcb_engine_free_progress(engine, engine_free_callback, &engine_free_progress_ctx);
#endif
}

進捗バーは以下のように表示されます。

image-20251115183346232

エンジンオプションのセット

その後に呼び出されるコードでは、clenginesetstr 関数や clenginesetnum 関数を用いてエンジンに各種オプションがセットされます。

参考:libclamav - ClamAV Documentation

使用されるオプションは以下の通り定義されています。

/**
 * @brief Allocate a new scanning engine and initialize default settings.
 *
 * The engine should be freed with `cl_engine_free()`.
 *
 * @return struct cl_engine* Pointer to the scanning engine.
 */
extern struct cl_engine *cl_engine_new(void);

enum cl_engine_field {
    CL_ENGINE_MAX_SCANSIZE,        /* uint64_t */
    CL_ENGINE_MAX_FILESIZE,        /* uint64_t */
    CL_ENGINE_MAX_RECURSION,       /* uint32_t */
    CL_ENGINE_MAX_FILES,           /* uint32_t */
    CL_ENGINE_MIN_CC_COUNT,        /* uint32_t */
    CL_ENGINE_MIN_SSN_COUNT,       /* uint32_t */
    CL_ENGINE_PUA_CATEGORIES,      /* (char *) */
    CL_ENGINE_DB_OPTIONS,          /* uint32_t */
    CL_ENGINE_DB_VERSION,          /* uint32_t */
    CL_ENGINE_DB_TIME,             /* time_t */
    CL_ENGINE_AC_ONLY,             /* uint32_t */
    CL_ENGINE_AC_MINDEPTH,         /* uint32_t */
    CL_ENGINE_AC_MAXDEPTH,         /* uint32_t */
    CL_ENGINE_TMPDIR,              /* (char *) */
    CL_ENGINE_KEEPTMP,             /* uint32_t */
    CL_ENGINE_BYTECODE_SECURITY,   /* uint32_t */
    CL_ENGINE_BYTECODE_TIMEOUT,    /* uint32_t */
    CL_ENGINE_BYTECODE_MODE,       /* uint32_t */
    CL_ENGINE_MAX_EMBEDDEDPE,      /* uint64_t */
    CL_ENGINE_MAX_HTMLNORMALIZE,   /* uint64_t */
    CL_ENGINE_MAX_HTMLNOTAGS,      /* uint64_t */
    CL_ENGINE_MAX_SCRIPTNORMALIZE, /* uint64_t */
    CL_ENGINE_MAX_ZIPTYPERCG,      /* uint64_t */
    CL_ENGINE_FORCETODISK,         /* uint32_t */
    CL_ENGINE_DISABLE_CACHE,       /* uint32_t */
    CL_ENGINE_DISABLE_PE_STATS,    /* uint32_t */
    CL_ENGINE_STATS_TIMEOUT,       /* uint32_t */
    CL_ENGINE_MAX_PARTITIONS,      /* uint32_t */
    CL_ENGINE_MAX_ICONSPE,         /* uint32_t */
    CL_ENGINE_MAX_RECHWP3,         /* uint32_t */
    CL_ENGINE_MAX_SCANTIME,        /* uint32_t */
    CL_ENGINE_PCRE_MATCH_LIMIT,    /* uint64_t */
    CL_ENGINE_PCRE_RECMATCH_LIMIT, /* uint64_t */
    CL_ENGINE_PCRE_MAX_FILESIZE,   /* uint64_t */
    CL_ENGINE_DISABLE_PE_CERTS,    /* uint32_t */
    CL_ENGINE_PE_DUMPCERTS,        /* uint32_t */
};

ファイルスキャンの実行

libclamav とエンジンの初期化が完了した後は、ClamAV によるスキャンが行われます。

今回の場合は、直接 eicar ファイルを指定しているので、scan_files 関数によるスキャンが行われるようです。

if (optget(opts, "file-list")->enabled || opts->filename) {
    /* scan the files listed in the --file-list, or it that's not specified, then
     * scan the list of file arguments (including data from stdin, if `-` specified) */
    ret = scan_files(engine, opts, &options, dirlnk, filelnk);

#ifdef _WIN32
} else if (optget(opts, "memory")->enabled) {
    /* scan only memory */
    ret = scan_memory(engine, opts, &options);

#endif
} else {
    /* No list of files provided to scan, and no request to scan memory,
     * so just scan the current directory. */
    char cwd[1024];

    /* Get the current working directory.
     * we need full path for some reasons (eg. archive handling) */
    if (!getcwd(cwd, sizeof(cwd))) {
        logg(LOGG_ERROR, "Can't get absolute pathname of current working directory\n");
        ret = 2;
    } else {
        CLAMSTAT(cwd, &sb);
        scandirs(cwd, engine, opts, &options, 1, sb.st_dev);
    }
}

scan_files 関数

scan_files 関数は以下の引数を受け取り、ファイルのスキャンを実行します。

/**
* @brief Scan the files from the --file-list option, or scan the files listed as individual arguments.
*
* If the user uses both --file-list <LISTFILE> AND one or more files, then clam will only
* scan the files listed in the LISTFILE and emit a warning about not scanning the other file parameters.
*
* @param opts
* @param options
* @return int
*/
static int scan_files(struct cl_engine *engine, const struct optstruct *opts, struct cl_scan_options *options, unsigned int dirlnk, unsigned int filelnk)

今回のような単体のファイルスキャンの場合、ファイルスキャンために scanfile 関数が呼び出されます。

static void scanfile(const char *filename, struct cl_engine *engine, const struct optstruct *opts, struct cl_scan_options *options)

ファイルパスの取得

scanfile 関数でファイルのスキャンを行うため、まずは clirealpath 関数で realfilename に実際のスキャン対象のファイルパスを保存します。

ret = cli_realpath((const char *)filename, &real_filename);
if (CL_SUCCESS != ret) {
    logg(LOGG_DEBUG, "Failed to determine real filename of %s.\n", filename);
    logg(LOGG_DEBUG, "Quarantine of the file may fail if file path contains symlinks.\n");
} else {
    filename = real_filename;
}

今回の場合、実行時のコマンドライン上では filename を ”eicar” として渡しています。

ここから、cli_realpath 関数を呼び出し、ライブラリ関数 realpath を使用してスキャン対象のファイルの絶対パスを取得した後に、filename 変数のデータを取得した完全パスに置き換えます。

ファイルスキャン

スキャン対象のファイルパスを取得したら、アクセス権などいくつかのチェックを行った上で、スキャン対象のファイルのファイルディスクリプタを取得し、clscandescex 関数を呼び出します。

struct metachain {
    char **chains;
    size_t lastadd;
    size_t lastvir;
    size_t level;
    size_t nchains;
};

struct clamscan_cb_data {
    struct metachain *chain;
    const char *filename;
};


logg(LOGG_DEBUG, "Scanning %s\n", filename);

if ((fd = safe_open(filename, O_RDONLY | O_BINARY)) == -1) {
    logg(LOGG_WARNING, "Can't open file %s: %s\n", filename, strerror(errno));
    info.errors++;
    goto done;
}

data.chain    = &chain;
data.filename = filename;

ret = cl_scandesc_ex(
    fd,
    filename,
    &verdict,
    &alert_name,
    &info.bytes_scanned,
    engine, options,
    &data,
    hash_hint,
    hash_out,
    hash_alg,
    file_type_hint,
    file_type_out);

clscandescex 関数は、受け取ったファイルディスクリプタをスキャンし、その結果を変数 verdict_out に返します。

/**
 * @brief Scan a file, given a file descriptor.
 *
 * This callback variant allows the caller to provide a context structure that
 * caller provided callback functions can interpret.
 *
 * This extended version of cl_scanmap_callback allows the caller to provide
 * additional hints to the scanning engine, such as a file hash and file type.
 *
 * This variant also upgrades the `scanned` output parameter to a 64-bit integer.
 *
 * @param desc               File descriptor of an open file. The caller must provide this or the map.
 * @param filename           (Optional) Filepath of the open file descriptor or file map.
 * @param[out] verdict_out   A pointer to a cl_verdict_t that will be set to the scan verdict.
 *                           You should check the verdict even if the function returns an error.
 * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan
 *                           matches against a signature.
 * @param[out] scanned_out   The (exact) number of bytes scanned.
 * @param engine             The scanning engine.
 * @param scanoptions        Scanning options.
 * @param[in,out] context    (Optional) An application-defined context struct, opaque to libclamav.
 *                           May be used within your callback functions.
 * @param hash_hint          (Optional) A NULL terminated string of the file hash so that
 *                           libclamav does not need to calculate it.
 * @param[out] hash_out      (Optional) A NULL terminated string of the file hash.
 *                           The caller is responsible for freeing the string.
 * @param hash_alg           The hashing algorithm used for either `hash_hint` or `hash_out`.
 *                           Supported algorithms are "md5", "sha1", "sha2-256".
 *                           If not specified, the default is "sha2-256".
 * @param file_type_hint     (Optional) A NULL terminated string of the file type hint.
 *                           E.g. "pe", "elf", "zip", etc.
 *                           You may also use ClamAV type names such as "CL_TYPE_PE".
 *                           ClamAV will ignore the hint if it is not familiar with the specified type.
 *                           See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
 * @param[out] file_type_out (Optional) A NULL terminated string of the file type
 *                           of the top layer as determined by ClamAV.
 *                           Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
 *                           See also: https://docs.clamav.net/appendix/FileTypes.html#file-types
 * @return cl_error_t        CL_SUCCESS if no error occured.
 *                           Otherwise a CL_E* error code.
 *                           Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
 */
extern cl_error_t cl_scandesc_ex(
    int desc,
    const char *filename,
    cl_verdict_t *verdict_out,
    const char **last_alert_out,
    uint64_t *scanned_out,
    const struct cl_engine *engine,
    struct cl_scan_options *scanoptions,
    void *context,
    const char *hash_hint,
    char **hash_out,
    const char *hash_alg,
    const char *file_type_hint,
    char **file_type_out);

変数 verdictout は、列挙型 clverdict_t として以下の通り定義されています。

/**
 * @brief Scan verdicts for cl_scanmap_ex(), cl_scanfile_ex(), and cl_scandesc_ex().
 */
typedef enum cl_verdict_t {
    CL_VERDICT_NOTHING_FOUND = 0,    /**< No alerting signatures matched. */
    CL_VERDICT_TRUSTED,              /**< The scan target has been deemed trusted (e.g. by FP signature or Authenticode). */
    CL_VERDICT_STRONG_INDICATOR,     /**< One or more strong indicator signatures matched. */
    CL_VERDICT_POTENTIALLY_UNWANTED, /**< One or more potentially unwanted signatures matched. */
} cl_verdict_t;

clscandescex 関数の中では、受けとったファイルディスクリプタを fmapnew に渡して clfmapt 構造体(clfmap 構造体)の変数 map にマッピングしたあと、さらに scan_common 関数を呼び出します。

if (NULL == (map = fmap_new(desc, 0, sb.st_size, filename_base, filename))) {
    cli_errmsg("CRITICAL: fmap_new() failed\n");
    status = CL_EMEM;
    goto done;
}

status = scan_common(
    map,
    filename,
    verdict_out,
    last_alert_out,
    scanned_out,
    engine,
    scanoptions,
    context,
    hash_hint,
    hash_out,
    hash_alg,
    file_type_hint,
    file_type_out);

cl_fmap 構造体は以下のように定義されており、さまざま API から利用できるようにファイルマップを抽象化する目的で使用するようです。

struct cl_fmap {
    /* handle interface */
    void *handle;
    clcb_pread pread_cb;

    /* memory interface */
    const void *data;

    /* internal */
    uint64_t mtime;
    uint64_t pages;
    uint64_t pgsz;
    uint64_t paged;
    bool aging;           /** Indicates if we should age off memory mapped pages */
    bool dont_cache_flag; /** Indicates if we should not cache scan results for this fmap. Used if limits exceeded */
    bool handle_is_fd;    /** Non-zero if `map->handle` is an fd. This is needed so that `fmap_fd()` knows if it can
                              return a file descriptor. If it's some other kind of handle, then `fmap_fd()` has to return -1. */
    size_t offset;        /** File offset representing start of original fmap, if the fmap created reading from a file starting at offset other than 0.
                              `offset` & `len` are critical information for anyone using the file descriptor/handle */
    size_t nested_offset; /** Offset from start of original fmap (data) for nested scan. 0 for orig fmap. */
    size_t real_len;      /** Length from start of original fmap (data) to end of current (possibly nested) map.
                              `real_len == nested_offset + len`.
                              `real_len` is needed for nested maps because we only reference the original mapping data.
                              We convert caller's fmap offsets & lengths to real data offsets using `nested_offset` & `real_len`. */

    /* external */
    size_t len; /** Length of data from nested_offset, accessible via current fmap */

    /* real_len = nested_offset + len
     * file_offset = offset + nested_offset + need_offset
     * maximum offset, length accessible via fmap API: len
     * offset in cached buffer: nested_offset + need_offset
     *
     * This allows scanning a portion of an already mapped file without dumping
     * to disk and remapping (for uncompressed archives for example) */

    /* vtable for implementation */
    void (*unmap)(fmap_t *);
    const void *(*need)(fmap_t *, size_t at, size_t len, int lock);
    const void *(*need_offstr)(fmap_t *, size_t at, size_t len_hint);
    const void *(*gets)(fmap_t *, char *dst, size_t *at, size_t max_len);
    void (*unneed_off)(fmap_t *, size_t at, size_t len);
    void *windows_file_handle;
    void *windows_map_handle;

    /* flags to indicate if we should calculate a hash next time we calculate any hashes */
    bool will_need_hash[CLI_HASH_AVAIL_TYPES];

    /* flags to indicate if we have calculated a hash */
    bool have_hash[CLI_HASH_AVAIL_TYPES];

    /* hash values */
    uint8_t hash[CLI_HASH_AVAIL_TYPES][CLI_HASHLEN_MAX];

    uint64_t *bitmap;
    char *name; /* name of the file, e.g. as recorded in a zip file entry record */
    char *path; /* path to the file/tempfile, if fmap was created from a file descriptor */
};

scan_common 関数

clfmap として抽象化されたファイルは scancommon 関数にわたされます。

この関数は、cl_fmap のスキャンを開始するための関数です。

/**
 * @brief   The main function to initiate a scan of an fmap.
 *
 * @param map                 File map.
 * @param filepath            (optional, recommended) filepath of the open file descriptor or file map.
 * @param[out] verdict_out    A pointer to a cl_verdict_t that will be set to the scan verdict.
 *                            You should check the verdict even if the function returns an error.
 * @param[out] last_alert_out Will be set to a statically allocated (i.e. needs not be freed) signature name if the scan matches against a signature.
 * @param[out] scanned_out    (Optional) The number of bytes scanned.
 * @param engine              The scanning engine.
 * @param scanoptions         Scanning options.
 * @param[in,out] context     (Optional) An application-defined context struct, opaque to libclamav.
 *                            May be used within your callback functions.
 * @param hash_hint           (Optional) A NULL terminated string of the file hash so that
 *                            libclamav does not need to calculate it.
 * @param[out] hash_out       (Optional) A NULL terminated string of the file hash.
 *                            The caller is responsible for freeing this string.
 * @param hash_alg            The hashing algorithm used for either `hash_hint` or `hash_out`.
 *                            Supported algorithms are "md5", "sha1", "sha2-256".
 *                            Required only if you provide a `hash_hint` or want to receive a `hash_out`.
 * @param file_type_hint      (Optional) A NULL terminated string of the file type hint.
 *                            E.g. "pe", "elf", "zip", etc.
 *                            You may also use ClamAV type names such as "CL_TYPE_PE".
 *                            ClamAV will ignore the hint if it is not familiar with the specified type.
 * @param file_type_out       (Optional) A NULL terminated string of the file type
 *                            of the top layer as determined by ClamAV.
 *                            Will take the form of the standard ClamAV file type format. E.g. "CL_TYPE_PE".
 *                            The caller is responsible for freeing this string.
 * @return cl_error_t         CL_SUCCESS if no error occured.
 *                            Otherwise a CL_E* error code.
 *                            Does NOT return CL_VIRUS for a signature match. Check the `verdict_out` parameter instead.
 */
static cl_error_t scan_common(
    cl_fmap_t *map,
    const char *filepath,
    cl_verdict_t *verdict_out,
    const char **last_alert_out,
    uint64_t *scanned_out,
    const struct cl_engine *engine,
    struct cl_scan_options *scanoptions,
    void *context,
    const char *hash_hint,
    char **hash_out,
    const char *hash_alg,
    const char *file_type_hint,
    char **file_type_out)

この中では、cli_ctx 構造体として定義されている ClamAV のコンテキスト情報を初期化し、引数として受け取ったエンジンやスキャンオプションなどの情報をコンテキストとして登録します。

/* internal clamav context */
typedef struct cli_ctx_tag {
    char *target_filepath;   /* (optional) The filepath of the original scan target. */
    char *this_layer_tmpdir; /* Pointer to current temporary directory, MAY vary with recursion depth. For convenience. */
    uint64_t *scanned;
    const struct cli_matcher *root;
    const struct cl_engine *engine;
    uint64_t scansize;
    struct cl_scan_options *options;
    uint32_t scannedfiles;
    unsigned int corrupted_input;      /* Setting this flag will prevent the PE parser from reporting "broken executable" for unpacked/reconstructed files that may not be 100% to spec. */
    cli_scan_layer_t *recursion_stack; /* Array of recursion levels used as a stack. */
    uint32_t recursion_stack_size;     /* stack size must == engine->max_recursion_level */
    uint32_t recursion_level;          /* Index into recursion_stack; current fmap recursion level from start of scan. */
    evidence_t this_layer_evidence;    /* Pointer to current evidence in recursion_stack, varies with recursion depth. For convenience. */
    fmap_t *fmap;                      /* Pointer to current fmap in recursion_stack, varies with recursion depth. For convenience. */
    size_t object_count;               /* Counter for number of unique entities/contained files (including normalized files) processed. */
    struct cli_dconf *dconf;
    bitset_t *hook_lsig_matches;
    void *cb_ctx;
    cli_events_t *perf;
    struct json_object *metadata_json;            /* Top level metadata JSON object for the whole scan. */
    struct json_object *this_layer_metadata_json; /* Pointer to current metadata JSON object in recursion_stack, varies with recursion depth. For convenience. */
    struct timeval time_limit;
    bool limit_exceeded; /* To guard against alerting on limits exceeded more than once, or storing that in the JSON metadata more than once. */
    bool abort_scan;     /* So we can guarantee a scan is aborted, even if CL_ETIMEOUT/etc. status is lost in the scan recursion stack. */
} cli_ctx;

いくつかの初期化とチェックを経て、初期化したコンテキスト情報と共に climagicscan 関数を呼び出します。

/*
 * DO THE SCAN!
 */
status = cli_magic_scan(&ctx, file_type);

climagicscan 関数

climagicscan は、さまざま実際のシグネチャマッチングによるスキャンにかなり近い動作を担当しています。

この関数が呼び出された後、いくつかのチェックを経たあとに pre_hash callback が呼び出されます。

/*
 * Run the pre_hash callback.
 */
ret = cli_dispatch_scan_callback(ctx, CL_SCAN_CALLBACK_PRE_HASH);
if (CL_SUCCESS != ret) {
    status = ret;
    goto done;
}

prehash callback は、`clidispatchscancallbackの第 2 引数にCLSCANCALLBACKPREHASH` を指定することで呼び出されます。

clidispatchscan_callback 関数では、このように指定されたフラグに応じて、事前定義されたコールバック関数をセットして実行します。

/*
 * Determine which callback to use.
 */
switch (location) {
    case CL_SCAN_CALLBACK_PRE_HASH:
        callback = ctx->engine->cb_scan_pre_hash;
        break;
    case CL_SCAN_CALLBACK_PRE_SCAN:
        callback = ctx->engine->cb_scan_pre_scan;
        break;
    case CL_SCAN_CALLBACK_POST_SCAN:
        callback = ctx->engine->cb_scan_post_scan;
        break;
    case CL_SCAN_CALLBACK_ALERT:
        callback = ctx->engine->cb_scan_alert;
        break;
    case CL_SCAN_CALLBACK_FILE_TYPE:
        callback = ctx->engine->cb_scan_file_type;
        break;
    default:
        status = CL_EARG;
        cli_errmsg("dispatch_scan_callback: Invalid callback location\n");
        goto done;
}

// 中略

if (NULL == callback) {
    /*
     * Callback is not set.
     */
    if (location == CL_SCAN_CALLBACK_ALERT) {
        // Accept the alert.
        status = CL_VIRUS;
    } else {
        // Keep scanning.
        status = CL_SUCCESS;
    }
    goto done;
}

current_layer = (cl_scan_layer_t *)&ctx->recursion_stack[ctx->recursion_level];

/*
 * Call the callback function.
 */
// TODO: Add performance measurements around the new callback specific to each callback location.
// perf_start(ctx, PERFT_PRECB);
status = callback(
    current_layer, // current scan layer
    ctx->cb_ctx    // application context
);
// perf_stop(ctx, PERFT_PRECB);

しかし、ここで呼び出されるコールバック関数ですが、どうもクライアント側で定義する必要があるらしく、今回のコマンドで実行した clamscan から cbscanpre_hash が呼び出された場合には NULL == callback が True となり、実際には何のコールバックも呼び出されませんでした。

その後も同じような呼び出しをいくつか行い、ファイルタイプのチェックを行った後、最終的に scanraw 関数の呼び出しに到達します。

/*
 * Perform pattern matching for malware detections AND embedded file type recognition.
 * Embedded file type recognition may re-assign the current file as a new type, or
 * it may detect embedded files. E.g. ZIP entries in a PE file (i.e. self-extracting ZIP).
 */
if ((type != CL_TYPE_IGNORED) &&
    /* CL_TYPE_HTML: raw HTML files are not scanned, unless safety measure activated via DCONF */
    (type != CL_TYPE_HTML || !(SCAN_PARSE_HTML) || !(DCONF_DOC & DOC_CONF_HTML_SKIPRAW)) &&
    (!ctx->engine->sdb)) {

    cli_dbgmsg("cli_magic_scan: Performing raw scan to pattern match and/or detect embedded files\n");

    ret = scanraw(ctx, type, typercg, &dettype);

    // Evaluate the result from the scan to see if it end the scan of this layer early,
    // and to decid if we should propagate an error or not.
    if (result_should_goto_done(ctx, ret, &status)) {
        goto done;
    }
}

image-20251123212552823

scanraw 関数

この関数は、ファイルマップに対する ras scan を行う関数として実装されているようです。

/**
 * @brief Perform raw scan of current fmap.
 *
 * @param ctx           Current scan context.
 * @param type          File type
 * @param typercg       Enable type recognition (file typing scan results).
 *                      If 0, will be a regular ac-mode scan.
 * @param[out] dettype  If typercg enabled and scan detects HTML or MAIL types,
 *                      will output HTML or MAIL types after performing HTML/MAIL scans
 * @return cl_error_t
 */
static cl_error_t scanraw(cli_ctx *ctx, cli_file_t type, uint8_t typercg, cli_file_t *dettype)

実際のコードは 1000 行ほどありますが、スキャン自体はさらに cliscanfmap 関数を呼び出すことで行われます。

perf_start(ctx, PERFT_RAW);
ret = cli_scan_fmap(ctx,
                    type == CL_TYPE_TEXT_ASCII ? CL_TYPE_ANY : type,
                    false,
                    &ftoffset,
                    acmode,
                    NULL);
perf_stop(ctx, PERFT_RAW);

まとめ

雑に書きましたが、とりあえず疲れ切ったのでこの記事はここで終わりです。

続きの cliscanfmap 関数については こちらの記事 で書く予定です。