🦠

危険なユーザーが利用するサービスを開発しているあなたに「clamav」を差し上げます🫴

に公開

危険なユーザーは存在しないだろうなと思いつつも、「万が一」を見越したサービス設計をしておきたい。そんな堅牢なアプリケーションを開発している人向けの記事になります。

S3 にアップロードされるファイルはすべてウイルススキャンしたい――そんな要件に応えるため、調査の結果ユースケースに合った OSS の ClamAV を採用し、実装しました。
本記事はその実装メモ(S3 → Lambda → ClamAV → タグ付け)です。
構築を進める中で サーバーレス(イベント駆動・従量課金・レイヤー運用)の勘所 も掴めたので、併せて記録しておきます。

clamavとは

Clam AntiVirus is an open source (GPL) anti-virus toolkit for UNIX. It provides a number of utilities including a flexible and scalable multi-threaded daemon, a command line scanner and advanced tool for automatic database updates.

(c.f: https://wiki.archlinux.org/title/ClamAV)
UNIX向けのオープンソースで、主にウィルススキャン目的で作成されたライブラリです。
Linux上で動くアプリケーション向けのanti-malware製品は元から少なかったみたいですね。そこで開発されたのがclamavと。

サーバーレスコンピューティングとは

サーバーレスコンピューティングは、サードパーティが管理するサーバーインフラストラクチャ上でアプリケーションを構築してデプロイできるアプリケーション開発モデルです。
(https://aws.amazon.com/jp/what-is/serverless-computing/)

RailsのようなサーバーサイドアプリをAWSで動かす場合、通常はEC2やECS Fargateで実行環境を用意・運用しますよね。

サーバーレスなら基盤の調達・保守はクラウドベンダー側が担ってくれるので、開発者はコードとイベント設計に集中できる利点があります。

常時serveする必要のないコンピューティングリソースなので、コンピューティング費用とメンテナンス費用の節約ができる優れものです。

イベントドリブンなサービスなので、非同期やステートレスな他サービスとの相性がいいです。

AWSにおけるサーバーレス

AWSでは、サーバーレスアーキテクチャの構築とデプロイを容易にするためのいくつかのツールを提供しています。その中でも代表的なのが、SAM(Serverless Application Model)とSLS(Serverless Framework)です。

SAM

AWS Serverless Application Model (AWS SAM) is an open-source framework for building serverless applications using infrastructure as code (IaC)

(c.f: https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html)

yaml形式でAWSのサービスを管理、デプロイできるフレームワークです。sam deploy コマンドを実行するだけでAWS CloudFormation経由で各種yamlに書かれたサービスをデプロイすることができます。ローカルでサーバレス環境の動作確認ができる点が個人的には嬉しい点だなと感じています。

SLS

Makes it easy to use AWS Lambda and other managed cloud services to build applications that auto-scale, cost nothing when idle, and result in radically low maintenance.

こちらもサーバーレス環境で動くlambda functionのデプロイを手軽にするためのサービスです。

serverless.yamlに数行記述して、sls deployするだけでcloudformationが動いて、lambda functionとlambdaの実行に必要な環境がセットアップされます。スッゲェですね。

https://www.serverless.com/framework/docs/providers/aws/guide/deploying

今回作成しようとしているclamda-layer内のymlファイルには下記のように定義されています。

$ cat serverless.yml 
service: clambda-av

provider:
  name: aws
  region: ap-northeast-1
  runtime: nodejs14.x
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObjectTagging
      Resource: "arn:aws:s3:::clambda-av-files/*"

functions:
  virusScan:
    handler: handler.virusScan
    memorySize: 2048
    events:
      - s3: 
          bucket: clambda-av-files
          event: s3:ObjectCreated:*
    layers:
      - {Ref: ClamavLambdaLayer}
    timeout: 120

package:
  exclude:
    - node_modules/**
    - coverage/**

layers:
  clamav:
    path: layer

sls deployを実行すると、—aws-profileの引数に指定したIAMユーザーでcloudformationが動かされます。

ここでは、S3への実行ロールやlambda functionが作成されています。

lambda functionは、同一ディレクトリにあるhandler.js内の、virusScanというhandlerが指定されています。

ちなみに、中身はこうなっています。処理は追って説明します。

$ cat handler.js
const { execSync } = require("child_process");
const { writeFileSync, unlinkSync } = require("fs");
const AWS = require("aws-sdk");

const s3 = new AWS.S3();

module.exports.virusScan = async (event, context) => {
  if (!event.Records) {
    console.log("Not an S3 event invocation!");
    return;
  }

  for (const record of event.Records) {
    if (!record.s3) {
      console.log("Not an S3 Record!");
      continue;
    }

    // get the file
    const s3Object = await s3
      .getObject({
        Bucket: record.s3.bucket.name,
        Key: record.s3.object.key,
      })
      .promise();

    // write file to disk
    writeFileSync(`/tmp/${record.s3.object.key}`, s3Object.Body);

    try {
      // scan it
      const scanStatus = execSync(
        `clamscan --database=/opt/var/lib/clamav /tmp/${record.s3.object.key}`
      );

      await s3
        .putObjectTagging({
          Bucket: record.s3.bucket.name,
          Key: record.s3.object.key,
          Tagging: {
            TagSet: [
              {
                Key: "av-status",
                Value: "clean",
              },
            ],
          },
        })
        .promise();
    } catch (err) {
      if (err.status === 1) {
        // tag as dirty, OR you can delete it
        await s3
          .putObjectTagging({
            Bucket: record.s3.bucket.name,
            Key: record.s3.object.key,
            Tagging: {
              TagSet: [
                {
                  Key: "av-status",
                  Value: "dirty",
                },
              ],
            },
          })
          .promise();
      }
    }

    // delete the temp file
    unlinkSync(`/tmp/${record.s3.object.key}`);
  }
};

Serverless Framework(SLS)によるLambda Functionの簡単デプロイ

Serverless Framework(SLS)を使うと、簡単なserverless.yamlの記述とsls deployの実行だけで、AWS CloudFormationが動き、Lambda functionとLambdaの実行に必要な環境がセットアップされます。これは非常に便利な機能です。

例えば、下記のようにserverless.ymlを定義します。

$ cat serverless.yml 
service: clambda-av

provider:
  name: aws
  region: ap-northeast-1
  runtime: nodejs14.x
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObjectTagging
      Resource: "arn:aws:s3:::clambda-av-files/*"

functions:
  virusScan:
    handler: handler.virusScan
    memorySize: 2048
    events:
      - s3: 
          bucket: clambda-av-files
          event: s3:ObjectCreated:*
    layers:
      - {Ref: ClamavLambdaLayer}
    timeout: 120

package:
  exclude:
    - node_modules/**
    - coverage/**

layers:
  clamav:
    path: layer

このsls deployコマンドを実行すると、—aws-profileの引数に指定したIAMユーザーでAWS CloudFormationが動きます。このプロセスにより、S3への実行ロールやLambda functionが作成されます。

Lambda functionでは、同一ディレクトリにあるhandler.js内の、virusScanというhandlerが指定されています。

$ cat handler.js
const { execSync } = require("child_process");
const { writeFileSync, unlinkSync } = require("fs");
const AWS = require("aws-sdk");

const s3 = new AWS.S3();

module.exports.virusScan = async (event, context) => {
  //...
};

CLIから実行するにはIAMが必要なので、下記のインストラクションで作成しておくと良いかもですね。

https://www.serverless.com/framework/docs/providers/aws/guide/credentials

Dockerコンテナ

https://dev.to/sutt0n/scanning-files-on-lambda-with-a-clamav-lambda-layer-475c でclamavのラムダ用レイヤーを作成していきます。

事前にIAMユーザーを作成して、~/.aws/credentialsに保存しておきましょう。

今回はdevという名前のIAMユーザーを作成しました。これでCLIからAWSのサービスへアクセスすることができます。

[dev]
aws_access_key_id = *******************
aws_secret_access_key = **********************

https://www.serverless.com/framework/docs/providers/aws/guide/credentials

必要に応じて、build.shに書いてあるdockerコマンドの前にsudoを付け足します。

clamav内のファイル

少し興味があるので、./build.shを叩いた時に走るコマンドを一つ一つ見ていきます。

build.sh


// build.sh

#!/bin/bash

rm -rf ./layer
mkdir layer

docker build --platform=linux/x86_64 -t clamav -f Dockerfile .
docker run --name clamav clamav
docker cp clamav:/home/build/clamav_lambda_layer.zip .
docker rm clamav
mv clamav_lambda_layer.zip ./layer

pushd layer
unzip -n clamav_lambda_layer.zip
rm clamav_lambda_layer.zip
popd

#!/bin/bashって何?

she-bang‘(shabang). This derives from the concatenation of the tokens sharp (#) and bang (!)

どのshellを使うのかを明示的に指定するシンボリックリンクを指している。シェルスクリプトのようなUnix-likeなOperating Systemsでは、she-bangはコマンドとして認識されるそうです。

なので、最初に#がついていても、「これからbin/bashで実行すれば良いんだな」、とシステムが解釈するようです。

https://medium.com/@codingmaths/bin-bash-what-exactly-is-this-95fc8db817bf

各種dockerコマンド

今度はdockerコマンドが書かれていますね。読み解いていきましょう。

docker build --platform=linux/x86_64 -t clamav -f Dockerfile  // 1 .
docker run --name clamav clamav // 2
docker cp clamav:/home/build/clamav_lambda_layer.zip . // 3
docker rm clamav // 4
  1. docker build --platform=linux/x86_64 -t clamav -f Dockerfile

    Dockerイメージをビルドしています。--platform=linux/x86_64でイメージをLinuxのx86_64(64ビット)を指定してビルドしています。-t clamavでイメージの名前をclamavにしています。-f Dockerfileはビルド時にカレントディレクトリのDockerfileが使っています。

  2. docker run --name clamav clamav

    このコマンドはDockerイメージを実行して、新たなコンテナを作成します。--name clamavは作成されるコンテナの名前を指定します。この例ではコンテナの名前がclamavとなります。最後のclamavは実行するイメージの名前を指定します。この例では名前clamavのイメージが実行されます。

  3. docker cp clamav:/home/build/clamav_lambda_layer.zip .

    このコマンドはDockerコンテナからファイルやディレクトリをホストのシステムにコピーします。clamav:/home/build/clamav_lambda_layer.zipはコピー元を指定します。この例ではclamavという名前のコンテナの/home/build/clamav_lambda_layer.zipというパスのファイルを指定しています。.はコピー先を指定します。この例ではカレントディレクトリにファイルがコピーされます。

  4. docker rm clamav

    このコマンドはDockerコンテナを削除します。clamavは削除するコンテナの名前を指定します。この例ではclamavという名前のコンテナが削除されます。

# Dockerfile

FROM amazonlinux:2

WORKDIR /home/build

RUN set -e

RUN echo "Prepping ClamAV"

RUN rm -rf bin
RUN rm -rf lib

RUN yum update -y
RUN amazon-linux-extras install epel -y
RUN yum install -y cpio yum-utils tar.x86_64 gzip zip

RUN yumdownloader -x \*i686 --archlist=x86_64 clamav
RUN rpm2cpio clamav-0*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 clamav-lib
RUN rpm2cpio clamav-lib*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 clamav-update
RUN rpm2cpio clamav-update*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 json-c
RUN rpm2cpio json-c*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 pcre2
RUN rpm2cpio pcre*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 libtool-ltdl
RUN rpm2cpio libtool-ltdl*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 libxml2
RUN rpm2cpio libxml2*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 bzip2-libs
RUN rpm2cpio bzip2-libs*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 xz-libs
RUN rpm2cpio xz-libs*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 libprelude
RUN rpm2cpio libprelude*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 gnutls
RUN rpm2cpio gnutls*.rpm | cpio -vimd

RUN yumdownloader -x \*i686 --archlist=x86_64 nettle
RUN rpm2cpio nettle*.rpm | cpio -vimd

RUN mkdir -p bin
RUN mkdir -p lib
RUN mkdir -p var/lib/clamav
RUN chmod -R 777 var/lib/clamav

COPY ./freshclam.conf .

RUN cp usr/bin/clamscan usr/bin/freshclam bin/.
RUN cp usr/lib64/* lib/.
RUN cp freshclam.conf bin/freshclam.conf

RUN yum install shadow-utils.x86_64 -y

RUN groupadd clamav
RUN useradd -g clamav -s /bin/false -c "Clam Antivirus" clamav
RUN useradd -g clamav -s /bin/false -c "Clam Antivirus" clamupdate

RUN mkdir -p opt/var/lib/clamav
RUN chmod -R 777 opt/var/lib/clamav

RUN LD_LIBRARY_PATH=./lib ./bin/freshclam --config-file=bin/freshclam.conf

RUN tclamav_lambda_layer.zip bin
RUN zip -r9 clamav_lambda_layer.zip lib
RUN zip -r9 clamav_lambda_layer.zip var
RUN zip -r9 clamav_lambda_layer.zip etc

Dockerfile内でコンテナに積んでいるパッケージ

  1. clamav: ClamAVは、ウイルススキャナーであり、特に電子メールサーバーにおけるメールゲートウェイスキャンに使用されます。多くの形式のマルウェアに対応しており、Phishing関連の脅威、ウイルス、トロイの木馬などを検出できます。
  2. clamav-lib: ClamAVのライブラリです。これはClamAVを使用する他のプログラムが依存するライブラリであり、ClamAVの機能を提供します。
  3. clamav-update: ClamAVのウイルスデータベースを更新するためのパッケージです。これにより、ClamAVは最新のマルウェアに対する保護を維持できます。
  4. json-c: JSON-Cは、JSONデータの解析と生成を可能にするCライブラリです。
  5. pcre2: PCRE(Perl Compatible Regular Expressions)は、Perlの正規表現と互換性のある正規表現ライブラリです。PCRE2はその後続版で、より多くの機能と改善されたパフォーマンスを提供します。
  6. libtool-ltdl: libtool-ltdlは、プログラムがプラグインをロードするために使用するライブラリで、動的ロードの抽象化を提供します。
  7. libxml2: libxml2は、XMLとHTMLドキュメントを解析、変換、保存するためのライブラリです。
  8. bzip2-libs: bzip2-libsは、bzip2圧縮アルゴリズムを使用するための共有ライブラリです。
  9. xz-libs: xz-libsは、LZMA圧縮アルゴリズムを使用するための共有ライブラリです。
  10. libprelude: libpreludeは、プレリュードIDS(侵入検知システム)によって使用されるフレームワークで、複数のIDS製品が結果を一元的に収集、共有、相関することを可能にします。
  11. gnutls: GnuTLSは、SSL, TLS, DTLSプロトコルを実装するためのセキュア通信ライブラリです。
  12. nettle: Nettleは、低レベルの暗号ライブラリで、ハッシュ関数、暗号、公開鍵暗号、その他の暗号操作を提供します。

また、yum-utilscpiotar.x86_64、`

gzipzipshadow-utils.x86_64`といったパッケージもインストールされています。これらは、パッケージ管理、ファイルの圧縮・解凍、ユーザーとグループの管理など、一般的なシステム管理作業を支援するツールを提供します。

なお、このDockerfileでは、各パッケージがインストールされるのではなく、yumdownloaderコマンドでダウンロードされ、その後rpm2cpiocpioコマンドで解凍されています。これは、パッケージの中にある特定のファイルだけを抽出し、それを新しいDockerイメージの中に含めるためのものです。

最終的に、これらのファイルはclamav_lambda_layer.zipという名前のzipファイルにパッケージ化されます。これは、AWS Lambdaで使用するためのレイヤとして使用される可能性があります。レイヤは、Lambda関数のデプロイメントパッケージからライブラリや他の依存関係を分離するための分散パッケージです。

zip -r9 hoge.zip dir/

  • r : これは "recursive" の略で、指定したディレクトリとそのすべてのサブディレクトリとファイルを圧縮する。
  • 9 : これは圧縮レベルを指定している。0から9までの値を指定できる。9は最高の圧縮レベルを意味していて、出力されるzipファイルのサイズを最小限にするために、最も高度な圧縮アルゴリズムを使用しているということになる。

たとえば、zip -r9 clamav_lambda_layer.zip lib というコマンドは、lib ディレクトリとその中のすべてのサブディレクトリとファイルを、最高レベルで圧縮した clamav_lambda_layer.zip という名前のzipファイルを作成する。

$ cd serverless-clamav-lambda-layer/
(.venv) SubarunoMacBook-puro-3:serverless-clamav-lambda-layer subaru$ ls
Dockerfile         README.md          freshclam.conf     handler.test.js    package.json
LICENSE            build.sh*          handler.js         package-lock.json  serverless.yml
(.venv) SubarunoMacBook-puro-3:serverless-clamav-lambda-layer subaru$ .venv
-bash: .venv: command not found
(.venv) SubarunoMacBook-puro-3:serverless-clamav-lambda-layer subaru$ npm install
⸨###############⠂⠂⠂⸩ ⠇ reify:buffer-alloc: http fetch GET 200 https://registry.npmjs.org/buffer-a

$ bash ./build.sh
[+] Building 7.0s (5/57)                                                                                                                                                             
 => [internal] load build definition from Dockerfile                                                                                                                            0.2s
 => => transferring dockerfile: 2.16kB                                                                                                                                          0.1s
 => [internal] load .dockerignore                                                                                                                                               0.1s
 => => transferring context: 2B                                                                                                                                                 0.0s
 => [internal] load metadata for docker.io/library/amazonlinux:2                                                                                                                6.3s
 => [auth] library/amazonlinux:pull token for registry-1.docker.io

実行後のディレクトリ

$ ls -a
./                 .git/              .npmcheckrc        LICENSE            build.sh*          handler.js         layer/             package-lock.json  serverless.yml
../                .gitignore         Dockerfile         README.md          freshclam.conf     handler.test.js    node_modules/      package.json
(.venv) SubarunoMacBook-puro-3:serverless-clamav-lambda-layer subaru$ ls layer/
bin/ etc/ lib/ var/

slsを実行するためのパッケージがないので、グローバルインストールしておきます。

$ npm install -g serverless

s3バケットは全世界で一意な名前にしないといけないので、初期値だとエラーが発生します。

Deploying clambda-av to stage dev (ap-northeast-1)Stack clambda-av-dev failed to deploy (108s)
Environment: darwin, node 16.13.0, framework 3.25.0 (local), plugin 6.2.2, SDK 4.3.2
Credentials: Local, "dev" profile
Docs:        docs.serverless.com
Support:     forum.serverless.com
Bugs:        github.com/serverless/serverless/issues

Error:
CREATE_FAILED: S3BucketClambdaavfiles (AWS::S3::Bucket)
clambda-av-files already exists

View the full error: https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stack/detail?stackId=arn%3Aaws%3Acloudformation%3Aap-northeast-1%3A061293269148%3Astack%2Fclambda-av-dev%2Fe373dfa0-17b7-11ee-b2bc-063a6e7d620f

作成に成功した場合

$ sls deploy  --aws-profile dev

Deploying clambda-av to stage dev (ap-northeast-1)Service deployed to stack clambda-av-dev (105s)

functions:
  virusScan: clambda-av-dev-virusScan (218 kB)
layers:
  clamav: arn:aws:lambda:ap-northeast-1:061293269148:layer:clamav:2

2 deprecations found: run 'serverless doctor' for more details

Need a faster logging experience than CloudWatch? Try our Dev Mode in Console: run "serverless dev"

実際にS3にファイルを保存してみる

作成されたS3を見てみましょう。

$ aws s3 cp ./virus.txt s3://clambda-av-files-test1
$ aws s3api get-object-tagging --bucket clambda-av-files-test1  --key virus.txt

紐づいているlambda

デプロイされたlambdaを見てみます。

S3のcreateObjectイベントが発火すると動くlambdaが出来上がっていますね。

cloudwatch logs

S3にファイルを入れた後のログを見てみます

/opt/var/lib/clamavなんてディレクトリはないよというエラーが出ています。

handler.jsのvirusScan関数の中にある、下記記述でエラーが発生しています。

     const scanStatus = execSync(
        `clamscan --database=/opt/var/lib/clamav /tmp/${record.s3.object.key}`
      );

execSync

node:child_processモジュールの中にある一つのメソッドです。

https://nodejs.org/api/child_process.html#child-process

下記のように呼び出して、Linuxコマンドをjsファイル内で使用できます。

LinuxとUnix

Unixという使い勝手のいいパッケージが昔はあったが、使いづらくね?というイメージを持ったフィンランドの大学生が開発したのがLinuxだそうです。

現在、Unixを使用するのはお金がかかるが、Linuxは無料ですね。MacOSモLinuxを採用しています。コマンドで打っているlsとかpwdとかの、あれです。

https://eng-entrance.com/unix_linux

関数ないで、コマンドを実行できる関数だそうです。同じような関数は他にもあります。

clamscanコンテナの中身を確認

そこで、さっきdocker buildで作成したclamavのコンテナの中身を見てみます。

clamavのコンテナを使って自作コンテナを作った場合のディレクトリ校正

$ docker run --name clamav -it clamav /bin/bash
bash-4.2# ls
bin					      clamav_lambda_layer.zip		    lib						  opt
bzip2-libs-1.0.6-13.amzn2.0.3.x86_64.rpm      etc				    libprelude-5.2.0-2.el7.x86_64.rpm		  pcre2-10.23-11.amzn2.0.1.x86_64.rpm
clamav-0.103.8-1.amzn2.0.2.x86_64.rpm	      freshclam.conf			    libtool-ltdl-2.4.2-22.2.amzn2.0.2.x86_64.rpm  usr
clamav-lib-0.103.8-1.amzn2.0.2.x86_64.rpm     gnutls-3.3.29-9.amzn2.0.1.x86_64.rpm  libxml2-2.9.1-6.amzn2.5.8.x86_64.rpm	  var
clamav-update-0.103.8-1.amzn2.0.2.x86_64.rpm  json-c-0.11-4.amzn2.0.4.x86_64.rpm    nettle-2.7.1-9.amzn2.x86_64.rpm		  xz-libs-5.2.2-1.amzn2.0.3.x86_64.rpmv

なんだ、存在してるっぽいな、、

bash-4.2# cd opt/var/lib/clamav/
bytecode.cvd   daily.cvd      freshclam.dat  main.cvd       
bash-4.2# cd opt/var/lib/clamav/
bytecode.cvd   daily.cvd      freshclam.dat  main.cvd       
bash-4.2# cd opt/var/lib/clamav/
bash-4.2# pwd
/home/build/opt/var/lib/clamav

cvd拡張子ってなんなんだろう

.cvd .dat

.cvdは、Bitdefender antivirus and Internet security softwareが使っている、ウィルスの定義が書かれたファイルを保存するための拡張ファイルだそう。

https://fileinfo.com/extension/cvd

.datは、バイナリーtextファイルと他のプログラムが書かれた拡張ファイルだそう。

https://www.indeed.com/career-advice/career-development/dat-file#:~:text=What is a DAT file,often automatically create these files.

ローカル環境

clamavを実行してみる

eicarが提供するウィルスに感染したファイルをclamscanに渡してみます。すると、サポートされていないdatabase ファイルだというエラーが出ます。

$  curl https://secure.eicar.org/eicar.com.txt | clamscan -
LibClamAV Error: cli_loaddbdir: No supported database files found in /usr/local/var/lib/clamav
ERROR: Can't open file or directory

----------- SCAN SUMMARY -----------
Known viruses: 0
Engine version: 1.1.0
Scanned directories: 0
Scanned files: 0
Infected files: 0
Data scanned: 0.00 MB
Data read: 0.00 MB (ratio 0.00:1)
Time: 0.025 sec (0 m 0 s)
Start Date: 2023:07:02 10:39:11
End Date:   2023:07:02 10:39:11
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    68  100    68    0     0     40      0  0:00:01  0:00:01 --:--:--    40
curl: (23) Failed writing body

どうやらfreshclamコマンドを実行して、clamavが読み込むdatabaseを更新する必要があるようです。

https://wiki.archlinux.org/title/ClamAV#Installation

ただ、freshclam.confを適したディレクトリに移動させないとfreshclam コマンドは実行できないようです。

freshclam.conf.sampleを探せ

freshclam.confを適切なディレクトリに置かないと、freshclamを実行したときに「ファイルが存在しません」というエラーが出てしまう。

$ freshclam
ERROR: Can't open/parse the config file /usr/local/etc/clamav/freshclam.conf

freshclam.conf.sampleは、clamscanパッケージをhomebrewを使ってインストールした時に、一緒にダウンロードされています。

私の場合、/usr/local/etc/clamav/ に合ったので、下記コマンドで適切なかしょに移動させます。

$ cp /usr/local/etc/clamav/freshclam.conf.sample /usr/local/etc/clamav/freshclam.conf

さらに、freshclam.conf内の記述を修正します。vimでfreshclam.confを開いて、Exampleという部分をコメントアウトしましょう。

これを、

# Comment or remove the line below.
Example

こう

# Comment or remove the line below.
#Example

https://www.eukhost.com/forums/forum/technical-support/tutorials-how-tos/15706-clamd-update-error-please-edit-the-example-config-file

freshclamを再度実行


SubarunoMacBook-puro-3:/ subaru$ freshclam
ClamAV update process started at Sun Jul  2 10:42:21 2023
daily database available for download (remote version: 26956)
ERROR: NULL X509 store
Time:   33.1s, ETA:    0.0s [========================>]   58.73MiB/58.73MiB
WARNING:  ******* RESULT 200, SIZE: 61579733 ******* 
Testing database: '/usr/local/var/lib/clamav/tmp.a5ad13a6dd/clamav-2d384fd99ebd7b280d6d77311b426b7a.tmp-daily.cvd' ...
Database test passed.
daily.cvd updated (version: 26956, sigs: 2038014, f-level: 90, builder: raynman)
main database available for download (remote version: 62)
ERROR: NULL X509 store
Time:  1m 06s, ETA:    0.0s [========================>]  162.58MiB/162.58MiB
WARNING:  ******* RESULT 200, SIZE: 170479789 ******* 
Testing database: '/usr/local/var/lib/clamav/tmp.a5ad13a6dd/clamav-96a1ce8ec33e6a66420cf68eb0b3a638.tmp-main.cvd' ...
Database test passed.
main.cvd updated (version: 62, sigs: 6647427, f-level: 90, builder: sigmgr)
bytecode database available for download (remote version: 334)
ERROR: NULL X509 store
Time:    0.2s, ETA:    0.0s [========================>]  285.12KiB/285.12KiB
WARNING:  ******* RESULT 200, SIZE: 291965 ******* 
Testing database: '/usr/local/var/lib/clamav/tmp.a5ad13a6dd/clamav-02300344bb5af1363a6247c0ba226a2d.tmp-bytecode.cvd' ...
Database test passed.
bytecode.cvd updated (version: 334, sigs: 91, f-level: 90, builder: anvilleg)

clamscanを再度実行

curlでeicarのウィルススキャンを取得し、clamscanに渡してみます。

ちゃんとInfected filesが1と表示されました。

$ curl https://secure.eicar.org/eicar.com.txt | clamscan -
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    68  100    68    0     0     77      0 --:--:-- --:--:-- --:--:--    77Loading:     1s, ETA:  15s [=>                       ]  540.00K/8.69M sigs       
Loading:    21s, ETA:   0s [========================>]    8.67M/8.67M sigs    
Compiling:   5s, ETA:   0s [========================>]       41/41 tasks 

stdin: Win.Test.EICAR_HDB-1 FOUND

----------- SCAN SUMMARY -----------
Known viruses: 8669911
Engine version: 1.1.0
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.00 MB
Data read: 0.00 MB (ratio 0.00:1)
Time: 27.327 sec (0 m 27 s)
Start Date: 2023:07:02 10:50:05
End Date:   2023:07:02 10:50:33

ちなみに、感染していないファイルを渡すと下記のような結果になります。

$ clamscan 節電電球のアイコン素材.png
Loading:    19s, ETA:   0s [========================>]    8.67M/8.67M sigs       
Compiling:   4s, ETA:   0s [========================>]       41/41 tasks 

/Users/subaru/Downloads/節電電球のアイコン素材.png: OK

----------- SCAN SUMMARY -----------
Known viruses: 8669911
Engine version: 1.1.0
Scanned directories: 0
Scanned files: 1
Infected files: 0
Data scanned: 0.01 MB
Data read: 0.01 MB (ratio 1.00:1)
Time: 25.055 sec (0 m 25 s)
Start Date: 2023:07:02 11:07:33
End Date:   2023:07:02 11:07:58

ローカルでやったことをコンテナ環境でできるようにする

ローカル環境では、下記の手順を踏んでclamscanでinfected fileをfileを検知することができました。

  1. clamscanパッケージをインストール
  2. freshman.confを移動
    1. /usr/local/etc/clamav/freshclam.conf.sampleを/usr/local/etc/clamav/freshclam.confにコピー
    2. Exampleをコメントアウト
  3. freshclamコマンドを実行

この手順をコンテナ環境でも実行できればいいということがわかりました。

そこで、コンテナ環境では現状どの手順まで進んでいるのかをおさらいします。

そのために、Dockerfileとコンテナの中身をみます。

clamav-docker

dockerコンテナ内でclamavを実行できるようにするための設定が書いてあります。

コンテナに入って直接clamavをインストールしちゃう作成んでいきます。

yum install -y clamavを実行します。

実行後、./bin/freshclamを実行してdatabaseを更新します。

clamscanが使えるようになったか、下記コマンドを実行しておきましょう。

使えるようになったので、コンテナ内で、必要なディレクトリをzipファイルにしちゃいます。

bash-4.2# zip -r9 layer_final.zip bin
  adding: bin/ (stored 0%)
  adding: bin/freshclam.conf (deflated 21%)
  adding: bin/clamscan (deflated 68%)
  adding: bin/freshclam (deflated 56%)
bash-4.2# zip -r9 layer_final.zip var
  adding: var/ (stored 0%)
  adding: var/spool/ (stored 0%)
  adding: var/spool/quarantine/ (stored 0%)
  adding: var/lib/ (stored 0%)
  adding: var/lib/clamav/ (stored 0%)
bash-4.2# zip -r9 layer_final.zip etc
  adding: etc/ (stored 0%)
  adding: etc/cron.d/ (stored 0%)
  adding: etc/cron.d/clamav-update (deflated 25%)
  adding: etc/logrotate.d/ (stored 0%)
  adding: etc/logrotate.d/clamav-update (deflated 41%)
  adding: etc/sysconfig/ (stored 0%)
  adding: etc/sysconfig/freshclam (deflated 49%)
  adding: etc/freshclam.conf (deflated 60%)
bash-4.2# zip -r9 layer_final.zip lib
  adding: lib/ (stored 0%)
  adding: lib/libpreludecpp.so.12 (deflated 67%)
  adding: lib/libpcre2-posix.so.1 (deflated 64%)
  adding: lib/libhogweed.so.2 (deflated 36%)
  adding: lib/libprelude.so.28.1.0 (deflated 84%)
  adding: lib/libnettle.so.4.7 (deflated 46%)
  adding: lib/libfreshclam.so.2 (deflated 65%)
  adding: lib/liblzma.so.5 (deflated 46%)
  adding: lib/libclamav.so.9.0.5 (deflated 59%)
  adding: lib/libfreshclam.so.2.0.1 (deflated 65%)
  adding: lib/libtasn1.so.6.5.3 (deflated 51%)
  adding: lib/libbz2.so.1 (deflated 54%)
  adding: lib/libtasn1.so.6 (deflated 51%)
  adding: lib/libpcre2-8.so.0 (deflated 61%)
  adding: lib/libclammspack.so.0.1.0 (deflated 45%)
  adding: lib/libltdl.so.7.3.0 (deflated 54%)
  adding: lib/libclammspack.so.0 (deflated 45%)
  adding: lib/libnettle.so.4 (deflated 46%)
  adding: lib/liblzma.so.5.2.2 (deflated 46%)
  adding: lib/libprelude.so.28 (deflated 84%)
  adding: lib/libbz2.so.1.0.6 (deflated 54%)
  adding: lib/libpcre2-posix.so.1.0.1 (deflated 64%)
  adding: lib/libjson-c.so.2 (deflated 56%)
  adding: lib/libpreludecpp.so.12.0.1 (deflated 67%)
  adding: lib/libpcre2-8.so.0.5.0 (deflated 61%)
  adding: lib/libxml2.so.2 (deflated 54%)
  adding: lib/libjson.so.0 (deflated 70%)
  adding: lib/libgnutls.so.28.43.3 (deflated 55%)
  adding: lib/libclamav.so.9 (deflated 59%)
  adding: lib/libltdl.so.7 (deflated 54%)
  adding: lib/libhogweed.so.2.5 (deflated 36%)
  adding: lib/libjson.so.0.1.0 (deflated 70%)
  adding: lib/libxml2.so.2.9.1 (deflated 54%)
  adding: lib/libjson-c.so.2.0.1 (deflated 56%)
  adding: lib/libgnutls.so.28 (deflated 55%)

そして、作成したzipファイルをローカル環境から取得します。

まずは、docker psを使ってコンテナの名前を確認します。clamav_new2という名前になってますね。

$ docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED       STATUS       PORTS     NAMES
7faeafc8af1a   clamav    "/bin/bash"   2 hours ago   Up 2 hours             clamav_new2

docker cpを使ってコンテナ内で作成したzipファイルを手元にコピーします。

$ docker cp clamav_new2:/home/build/layer_final.zip .

そして、build.shの中身も下記のように変更します。

/layerへlayer_final.zipをunzipするだけです。

$ cat build.sh 
#!/bin/bash

rm -rf ./layer
mkdir layer

mv layer_final.zip ./layer

pushd layer
unzip -n layer_final.zip
rm layer_final.zip
popd

./buildを実行後、デプロイしてみましょう

CLIで確認してみましょう。成功していたら、タグがついているはずです。

適当なtxtファイルを用意しておきます。今回はvirus.txtを作成しました。

$ aws s3 cp ./virus.txt s3://clambda-av-files-test1
$ aws s3api get-object-tagging --bucket clambda-av-files-test1  --key virus.txt
{
  "TagSet": [
    { "Key": "av-status",   "Value": "dirty" }
  ]
}

ウィルススキャンに成功してそうですね。

まとめ

これで危険なファイルを識別する基盤を作れるようになりました。
あとは、INFECTEDタグがついていた場合は別のS3バケットへ移す、クライアントへ表示させない、といった応用を効かせていけば冒頭のユースケースをクリアできそうな気がします。

参考文献

clamav: https://wiki.archlinux.org/title/ClamAV
clamd: https://docs.clamav.net/manual/Usage/Scanning.html#clamd
serverless flamework: https://github.com/oss-serverless/serverless/tree/main
マルウェアを検出した際の動作を確認するには?: https://eset-support.canon-its.jp/faq/show/17872?site_domain=default

Discussion