0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ClamAVでアップロードファイルをウィルス対策する

Posted at
clamav

ClamAV(clamd)を使って、アップロードファイルをサーバーサイドでウィルススキャンするためのアーキ

image 2.png

ClamAV

  • マルチプラットフォームでフリーのアンチウィルススイート
  • ClamAVにはコマンド版とデーモン版がある。デーモン版はTCPでファイル単位のウィルス検閲に対応している
  • clamavのdockerイメージを使ってTCPでウィルススキャンする

clamdホスト構築

clamavの公式イメージによると、

The development of this image will be discontinued. Since 0.104 Cisco provides official docker images for clamav. This image here will be on hold and supported as long as possible.

このイメージの開発は終了します。0.104 以降、Cisco は clamav 用の公式 docker イメージを提供しています。こちらのイメージは保留され、可能な限りサポートされます。
mkodockx/docker-clamav

一応ちゃんと動いてくれるが、現在開発停止していてデータベースの更新中に予期しない切断が発生するため、Ciscoの公式版を使ってくれとのこと。
clamav/clamav | dockerhub

Dockerfile
version: '3.2'
services:
  app: ...
    depends_on:
      - clamavd

  clamavd:
    container_name: clamavd
    hostname: clamavd
    image: clamav/clamav
    ports:
      - 3310

ウィルスデータベース

freshclam

ClamAVのウイルステータベースは、/var/lib/clamavディレクトリにmain.cvdおよびdaily.cvdというファイルで保存されている。
これらのデータベースは、freshclamコマンドで更新する。
ClamAVのウイルスデータベースを自動更新するには - @IT

clamav/clamav | dockerhubイメージではfreshclamデーモンが一日一回(--checks=1)ウィルスデータベースを更新するようにチューニングされている。
freshclamコマンド版を使ってcrontabでスケジュールすることもできるが、まあおまかせのデーモン版で。

sh
ps | grep clamav
PID   USER     TIME  COMMAND
   12 clamav    0:00 freshclam --checks=1 --daemon --foreground --stdout --user=clamav
   13 clamav    0:22 clamd --foreground

なお、かつて高頻度でダウンロードをかけるクライアントに対策するためfreshclamによるダウンロードに制限した経緯があるようだ。

Abuse of the download system has forced us to push people towards FreshClam.
Unfortunately a handful have ruined it for everyone. (Looking at you, handful of IPs that download the daily.cvd 3x a second)

ダウンロードシステムの乱用により、私たちは人々をFreshClamに追い込むことを余儀なくされました。
残念ながら、一握りがみんなのためにそれを台無しにしました。(あなたを見て、daily.cvdを1秒間に3回ダウンロードする一握りのIP)
Official docker images of clamav

freshclamの設定ファイルを見るとデフォルトは12回(2時間に一回)のようだ。

/etc/clamav/freshclam.conf
# Number of database checks per day.
# Default: 12 (every two hours)                             
#Checks 24   

ClamAVによるリアルタイムスキャンの設定(ClamAV 1.0版) | 稲葉サーバーデザイン

clamavdのクライアント実装

PINGコマンドで疎通を確認した後に、INSTREAMスキャンコマンドを投げる

<?php

class ClamAVClient
{
    // clamdホストのデフォルト
    const CLAMAVD_HOST = 'clamavd';
    const CLAMAVD_PORT = 3310;

    private $_clamavd_host;
    private $_clamavd_port;
    private $_raw_fact;
    private $_socket;

    public function __construct($host=self::CLAMAVD_HOST, $port=self::CLAMAVD_PORT)
    {
        $this->_clamavd_host = $host;
        $this->_clamavd_port = $port;
    }

    public function __destruct()
    {
        $this->closeSocket();
    }

    /**
     * tcpソケットを閉じます
     */
    public function closeSocket(){
        if( is_resource($this->_socket) ) fclose($this->_socket);
    }

    /**
     * tcpソケットを開きます
     * @return resource TCPセッションハンドル
     */
    private function openSocket(){
        $this->_socket = stream_socket_client("tcp://{$this->_clamavd_host}:{$this->_clamavd_port}", $errno, $errstr, 10);
        if (!$this->_socket) {
            throw new RuntimeException("ホストの接続に失敗しました。({$errno}){$errstr}");
        }
        return $this->_socket;
    }

    /**
     * ソケットを取得します
     * @return resource TCPセッションハンドル
     */
    private function getSocket(){
        return $this->_socket ?: $this->openSocket();
    }

    /**
     * clamavdホストにPINGをリクエストします
     * @note PINGに対するclamavdの正常応答はPONG
     * @return bool true: 正常応答 / false: 異常応答
     */
    public function ping(){

        $socket = $this->getSocket();

        stream_socket_sendto($socket, 'PING');
        $this->_raw_fact = stream_socket_recvfrom($socket, 1024);

        return $this->fact() === 'PONG';
    }

    /**
     * clamavdのレスポンステキストからファクトを抽出します
     * @param string $response_text
     * @return string ファクト
     */
    public function fact(){
        $fact = (strrchr($this->_raw_fact, ":")) ?: ": {$this->_raw_fact}";
        return trim(substr($fact, 1));
    }

    /**
     * clamavdにファイルスキャンをリクエストします
     * 
     * @return bool true: ウィルスなし / false: ウイルスあり
     */
    public function scan($filepath){

        // ファイルが存在するか確認
        if ( file_exists($filepath) === false ) {
            throw new RuntimeException("ファイルが存在しません。file({$filepath})");
        }

        // ファイル内容を読み込み
        $fileContent = file_get_contents($filepath);
        if ($fileContent === false) {
            throw new RuntimeException("ファイルの読み込みに失敗しました。file({$filepath})");
        }

        // PINGセッション
        if( ( new ClamAVClient() )->ping() === false ) {
            throw new RuntimeException("PINGに失敗しました。");
        }

        // スキャンセッション
        $socket = $this->getSocket();

        // ストリームスキャンのハンドシェイク開始 => zINSTREAM\0
        $res = stream_socket_sendto($socket, "zINSTREAM\0");

        // ファイルを8KBでチャンク送信する
        $chunkSize = 8192;
        $bytesSent = 0;
        while ( $bytesSent < strlen($fileContent) ) {
            $chunk = substr($fileContent, $bytesSent, $chunkSize);
            $chunkLength = pack('N', strlen($chunk));
            $res = stream_socket_sendto($socket, $chunkLength . $chunk);
            $bytesSent += $chunkSize;
        }

        // スキャンのハンドシェイク終了 => ゼロバイトを送信
        $res = stream_socket_sendto($socket, pack('N', 0));

        // スキャン結果を読み取り => 問題なければ"OK"文字列が入っている
		// ウィルス検出	=> "stream: Eicar-Signature FOUND"
		// ウィルス未検出	=> "stream: OK"
        $this->_raw_fact = stream_socket_recvfrom($socket, 1024);        
        if( empty( $this->_raw_fact ) ){
            throw new RuntimeException("診断結果が空です。");
		}

        echo "clamscan result ({$filepath}) : {$this->_raw_fact}";
        return $this->fact() === 'OK';
    }
}

検閲

テストにはEICAR テストファイルを使用する。
EICAR テストファイルとは?

sh
echo "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" > /path/to/virus.txt
php
try{
	$clamav = new ClamAVClient();
	$result = $clamav->scan('/path/to/virus.txt');
}catch(RuntimeException $e){
	echo "clamavスキャンで例外が発生 {$e->getMessage()}";	
}

if( $result === false ){
  	throw new RuntimeException("clamavスキャンで脅威を検出しました。fact({$clamav->fact()})"); //=> Eicar-Signature FOUND
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?