All Articles

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

今回は SECCON 2022 の Devil Hunter という問題をテーマに ClamAV のシグネチャの記法や解析方法についてまとめました。

参考:SECCON2022onlineCTF/reversing/devilhunter at main · SECCON/SECCON2022online_CTF

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

もくじ

問題の概要:Devil Hunter(Rev)

Clam Devil; Asari no Akuma

問題バイナリとして flag.cbc と check.sh が与えられます。

check.sh は、以下のように clamscan と flag.cbc を使用してスキャンを行った際に検出されるテキストが Flag になることがわかります。

#!/bin/sh
if [ -z "$1" ]
then
    echo "[+] ${0} <flag.txt>"
    exit 1
else
    clamscan --bytecode-unsigned=yes --quiet -dflag.cbc "$1"
    if [ $? -eq 1 ]
    then
        echo "Correct!"
    else
        echo "Wrong..."
    fi
fi

flag.cbc は以下のテキストでした。

ClamBCafhaio`lfcf|aa```c``a```|ah`cnbac`cecnb`c``beaacp`clamcoincidencejb:4096
Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
Teddaaahdabahdacahdadahdaeahdafahdagahebdeebaddbdbahebndebceaacb`bbadb`baacb`bb`bb`bdaib`bdbfaah
Eaeacabbae|aebgefafdf``adbbe|aecgefefkf``aebae|amcgefdgfgifbgegcgnfafmfef``
G`ad`@`bdeBceBefBcfBcfBofBnfBnbBbeBefBfgBefBbgBcgBifBnfBgfBnbBfdBldBadBgd@`bad@Aa`bad@Aa`
A`b`bLabaa`b`b`Faeac
Baa``b`abTaa`aaab
Bb`baaabbaeAc`BeadTbaab
BTcab`b@dE
A`aaLbhfb`dab`dab`daahabndabad`bndabad`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`aa`b`d`b`b`aa`ah`aa`aa`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`b`b`b`d`b`d`b`b`b`b`bad`b`b`bad`b`d`aa`b`b`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`d`b`d`aa`Fbcgah
Bbadaedbbodad@dbadagdbbodaf@db`bahabbadAgd@db`d`bb@habTbaab
Baaaiiab`dbbaBdbhb`d`bbbbaabTaaaiabac
Bb`dajbbabajb`dakh`ajB`bhb`dalj`akB`bhb`bamn`albadandbbodad@dbadaocbbadanamb`bb`aabbabaoAadaabaanab`bb`aAadb`dbbaa`ajAahb`d`bb@h`Taabaaagaa
Bb`bbcaabbabacAadaabdakab`bbca@dahbeabbacbeaaabfaeaahbeaBmgaaabgak`bdabfab`d`bb@h`Taabgaadag
Bb`bbhaabbabacAadaabiakab`bbha@db`d`bb@haab`d`bb@h`Taabiaagae
Bb`dbjabbaabjab`dbkah`bjaB`bhb`dblaj`bkaB`bhb`bbman`blabadbnadbbodad@dbadboacbbadbnabmab`bb`bgbboab`bbab`baacb`bb`dbbbh`bjaBnahb`dbcbj`bbbB`bhb`bbdbn`bcbb`bbebc`Add@dbadbfbcbbadagbebb`bbgbc`Addbdbbadbhbcbbadbfbbgbb`b`fbbabbhbb`dbiba`bjaAdhaabjbiab`dbibBdbhb`d`bbbibaaTaabjbaeaf
Bb`bbkbgbagaablbeab`bbkbHbj`hnicgdb`bbmbc`Add@dbadbnbcbbadagbmbb`bbobc`AddAadbadb`ccbbadbnbbobb`bbacgbb`caabbceab`bbacHcj`hnjjcdaabcck`blbbbcb`bbdcc`Add@dbadbeccbbadagbdcb`bbfcc`AddAbdbadbgccbbadbecbfcb`bbhcgbbgcaabiceab`bbhcHoigndjkcdaabjck`bccbicb`bbkcc`Add@dbadblccbbadagbkcb`bbmcc`AddAcdbadbnccbbadblcbmcb`bbocgbbncaab`deab`bbocHcoaljkhgdaabadk`bjcb`db`bbbdc`Add@dbadbcdcbbadagbbdb`bbddc`AddAddbadbedcbbadbcdbddb`bbfdgbbedaabgdeab`bbfdHcoalionedaabhdk`badbgdb`bbidc`Add@dbadbjdcbbadagbidb`bbkdc`AddAedbadbldcbbadbjdbkdb`bbmdgbbldaabndeab`bbmdHoilnikkcdaabodk`bhdbndb`bb`ec`Add@dbadbaecbbadagb`eb`bbbec`AddAfdbadbcecbbadbaebbeb`bbdegbbceaabeeeab`bbdeHdochfheedaabfek`bodbeeb`bbgec`Add@dbadbhecbbadagbgeb`bbiec`AddAgdbadbjecbbadbhebieb`bbkegbbjeaableeab`bbkeHdiemjoeedaabmek`bfebleb`bbnec`Add@dbadboecbbadagbneb`bb`fc`AddAhdbadbafcbbadboeb`fb`bbbfgbbafaabcfeab`bbbfHoimmoklfdaabdfk`bmebcfb`dbefo`bdfb`d`bbbef`Tbaag
Bb`dbffbb`bffaabgfn`bffTcaaabgfE
Aab`bLbaab`b`b`dab`dab`d`b`d`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`aa`b`d`b`d`Fbfaac
Bb`d`bb@habb`d`bbG`lckjljhaaTbaaa
Bb`dacbbaaacb`dadbbabadb`baen`acb`bafn`adb`bagh`afAcdb`bahi``agb`baik`ahBoodb`bajm`aiaeb`bakh`ajAhdb`bali`aeBhadb`baml`akalb`bana`afAadaaaoeab`banAddb`db`ao`anb`dbaao`amb`d`bbb`aabb`d`bbbaaaaTaaaoabaa
BTcab`bamE
Snfofdg`bcgof`befafcgig`bjc`ej`

この CBC ファイルは ClamAV のバイトコードシグネチャなので、この署名にマッチするテキストを特定することが Flag を取得する方法といえそうです。

参考:Bytecode Signatures - ClamAV Documentation

問題を解く前に、ClamAV のシグネチャに関するドキュメントを読み込んでおくことにします。

参考:Signatures - ClamAV Documentation

ClamAV のシグネチャには大きく以下の種類があるようです。

  • データベース形式(CDV/CLD)
  • Body-based なシグネチャ

ここからは、ClamAV の各シグネチャについてドキュメントの情報を整理します。

データベース形式のシグネチャ(CDV、CLD)

ClamAV では、CDV および CLD というデータベース形式のアーカイブファイルとしてシグネチャが配信されます。

CLD は、CDIFF という差分更新の仕組みで更新が行われた場合に作成されるファイルです。

参考:Terminology - ClamAV Documentation

参考:ClamAV® blog: ClamAV, CVDs, CDIFFs and the magic behind the curtain

CVD がシグネチャデータベースアーカイブが圧縮されているものであり、Cisco-Talos によりデジタル署名され、配布されています。

ClamAV を利用するマシンでは通常、freshclam モジュールによって CVD のダウンロードを行います。

CVD の拡張子は .cvd ですが、CVD または CLD データベースが CDIFF パッチファイルによって更新されると、拡張子は .cld になります。

なお、ClamAV では Cisco-Talos により配布されている CDV データベースに加えて、カスタムデータベースファイルを使用してスキャンを行うことも可能です。

Body-based シグネチャ

ClamAV ではデータベース形式のシグネチャに加え、Body-based シグネチャを利用することもできます。

Body-based シグネチャは、ハッシュではなくスキャン対象内の特定のバイトシーケンスを検知条件として定義するシグネチャです。

ClamAV で使用可能な Body-based シグネチャには主に以下の種類があります。

※ 拡張子の末尾に u が付与されているものは、PUA シグネチャが有効化されている場合にのみロードされます。

  • *.ndb / *.ndu: Extended signatures
  • *.ldb / *.ldu / *.idb: Logical Signatures
  • *.cdb: Container Metadata Signatures
  • *.cbc: Bytecode Signatures
  • *.pdb / *.gdb / *.wdb: Phishing URL Signatures

今回の問題で使用しているバイトコードシグネチャ(.cbc) も、この Body-based シグネチャの一種です。

拡張シグネチャ(Extended signatures)

*.ndb / *.ndu は拡張シグネチャを指します。

拡張シグネチャは以下のフォーマットで記述でき、Hex シグネチャに加えて TargetType、Virus offset、FLEVEL などを定義します。

MalwareName:TargetType:Offset:HexSignature[:min_flevel:[max_flevel]]

参考:Extended Signatures - ClamAV Documentation

MalwareName には任意の値を記載できますが、公式のシグネチャでは通常以下の命名規則に従って定義されます。

{platform}.{category}.{name}-{signature id}-{revision}

参考:Signatures - ClamAV Documentation

また、TargetType にはスキャン対象のファイルの種類を指定します。

任意のファイルをスキャン対象とする場合は 0 を指定します。

参考:ClamAV File Types and Target Types - ClamAV Documentation

例えば、TEST_EXTENDED_SIG という検出名でファイルを検出する拡張シグネチャは以下のように定義できます。

TEST_EXTENDED_SIG:0:*:48656c6c6f2c20436c616d4156

このシグネチャでは、sigtool --hex-dump などで Hexdump 化した文字列 Hello, ClamAV を任意の種類のファイルから検出できます。

image-20240730221051779

実際にこのシグネチャを使用して clamscan --database=TEST_EXTENDED_SIG.ndb test1.txt コマンドでスキャンを発行すると、Hello, ClamAV というテキストが含まれているファイルを TEST_EXTENDED_SIG という検出名で検出できました。

image-20240730221234979

論理シグネチャ(Logical signatures)

*.ldb / *.ldu / *.idb の拡張子を持つシグネチャは論理シグネチャです。

論理シグネチャは論理演算子を使用して複数の署名を組み合わせることができます。

論理シグネチャのフォーマットは以下の通りです。

SignatureName;TargetDescriptionBlock;LogicalExpression;Subsig0;Subsig1;Subsig2;...

TargetDescriptionBlock にはエンジンやターゲットファイルに関する情報をコンマ区切りのペアで記述します。

TargetDescriptionBlock には Engine 以外の項目も記述できますが、互換性の維持の観点から Engine の指定を最初に置くことが推奨されています。

Engine の指定は Engine:81-255 のようなフォーマットで行います。

特に特定のバージョンから追加された機能を使用するシグネチャの場合には、この Engine の指定が重要なようです。

ちなみに、この指定は FLEVEL の値の範囲で記述されます。FLEVEL の値は 81 がバージョン 0.99 と対応しています。

参考:ClamAV Versions and Functionality Levels - ClamAV Documentation

TargetDescriptionBlock で指定可能な他の値には、Target や FileSize、EntryPoint のオフセットなど様々な値があります。

Target ではスキャン対象のファイルを指定できます。指定方法は拡張シグネチャと同じで、0 が任意のファイルを指定する値です。

参考:ClamAV File Types and Target Types - ClamAV Documentation

続く LogicalExpression のセクションでは、以降のサブシグネチャ間の関係を定義する論理式を記述します。

サブシグネチャには最大 64 個の項目を定義でき、定義した順に 0, 1,2 … という値で参照されます。

少々実装がわかりづらいですが、このサブシグネチャの中には式や値が含まれます。

例えば、ドキュメントのサンプルと同じ以下のシグネチャでは、0&1 の論理式により、Subsig0(41414141::i) と Subsig1(424242424242::i) の両方にマッチした場合にのみ対象のファイルを検出するようなシグネチャを定義できます。

TEST_LOGICAL_SIG;Engine:81-255,Target:0;0&1;41414141::i;424242424242::i

::i は大文字小文字を区別しないことを指示するオプションです。

つまり、上記のシグネチャは、ファイル内に AAAA(または aaaa)BBBBBB(または bbbbbb) の両方が存在する場合にファイルを検出するシグネチャです。

実際に、このシグネチャを使って test1 から test4 までのテキストファイルの検出を試してみると、ァイル内に AAAA(または aaaa)BBBBBB(または bbbbbb) の両方が存在する場合にのみ検知が行われることを確認できます。

image-20240731220015849

test3 および test4 は、AAAABBBBBB のどちらか一方しか含まれていないファイルであるため、このシグネチャでは検出されません。

image-20240731220037513

サブシグネチャの記法は非常に多くあるので今回の記事では扱いません。

詳細については以下のドキュメントにまとまっています。

参考:Logical Signatures - ClamAV Documentation

コンテナメタデータシグネチャ(Container Metadata Signatures)

コンテナメタデータシグネチャは *.cdb の拡張子を持つファイルにより定義されます。

シグネチャのフォーマットは以下の通りです。

VirusName:ContainerType:ContainerSize:FileNameREGEX:FileSizeInContainer:FileSizeReal:IsEncrypted:FilePos:Res1:Res2[:MinFL[:MaxFL]]

ContainerType には、CL_TYPE_ZIPCL_TYPE_7Z など、ClamAV で独自に定義されているアーカイブファイルのタイプを指定します。

任意のファイルタイプを指定する場合は * を使用できるようです。

参考:ClamAV File Types and Target Types - ClamAV Documentation

コンテナメタデータシグネチャに関する情報はあまり多くありませんが、ファイルの種類やサイズなどを指定してアーカイブファイルを検出できるようなシグネチャみたいです。

例えば、ContainerType に CL_TYPE_ZIP のみを指定した以下のシグネチャを使うと、任意の ZIP ファイルを検出することができます。

TEST_CONTAINER_METADATA_SIG:CL_TYPE_ZIP:*:*:*:*:*:*:*:*

image-20240801191401840

さらに、ContainerSize のオプションでは、ZIP などのコンテナファイル自体のサイズをバイト単位で条件に指定できます。

ContainerSize の値を 80000000-90000000 に変更すると、testzip.zip は検出されなくなるものの、ファイルサイズが 88843043 の bigsizezip.zip は検出されます。

TEST_CONTAINER_METADATA_SIG:CL_TYPE_ZIP:80000000-90000000:*:*:*:*:*:*:*

image-20240801191920008

他にも、コンテナファイルのファイル名や、圧縮されたサイズ、暗号化の有無など様々な条件を指定してコンテナファイルを検出できます。

バイトコードシグネチャ(Bytecode Signatures)

Devil Hunter の問題バイナリとして与えられたような .cbc の拡張子を持つシグネチャはバイトコードシグネチャです。

参考:Bytecode Signatures - ClamAV Documentation

ClamAV では、コンテンツを解析する C コードを記述することでより複雑なパターンマッチングを実装できます。

この時 C コードで作成されたシグネチャは「bytecode」と呼ばれる中間言語にコンパイルされます。

この「bytecode」は ASCII 形式の .cbc ファイルとして生成され、.cvd / .cld のデータベースファイルとして配布できます。

バイトコードシグネチャの記述方法とコンパイルの詳細については後述します。

フィッシングシグネチャ(Phishing URL Signatures)

ClamAV では、メールなどに記載された HTML 内の表示リンクと、実際のリンクのアドレスを検査できます。

参考:Phishing Signatures - ClamAV Documentation

フィッシングシグネチャについてはかなり多くの情報が記載されていますが、今回は割愛します。

Hash-based シグネチャ

ClamAV は Hash-based シグネチャを使用してファイルハッシュチェックによりファイルを検出できます。

Hash-based シグネチャには以下の 2 つの種類があります。

  • *.hdb *.hsb *.hdu *.hsu: File hash signatures
  • *.mdb *.msb *.mdu *.msu: PE section hash signatures

ファイルハッシュシグネチャ(File hash signatures)

ファイルハッシュシグネチャは以下のフォーマットで定義されます。

HashString:FileSize:MalwareName

ファイルハッシュには MD5、SHA1、SHA256 などを使用でき、以下のように sigtool を使用することで特定のファイルのファイルハッシュシグネチャを作成することができます。

sigtool --md5 test1.txt > test.hdb
sigtool --sha1 test1.txt > test.hdb
sigtool --sha256 test1.txt > test.hdb

このコマンドで生成したファイルハッシュシグネチャを使用すると、静的マッチングでファイルを検出することができます。

image-20240802003159509

なお、sigtool で生成したファイルハッシュシグネチャには FileSize 欄に対象ファイルのサイズが記述されます。

しかし、ファイルサイズが不明でハッシュのみがわかっている場合には、以下のように FileSize をワイルドカードに置き換えることでも検出が可能です。

bf47ba8d5e3af20bd79fa2c9ed028c5a9501a00f:*:test1.txt:73

この記法を使用する場合、末尾に最小エンジンレベルを 73 以上に指定するための値を追記する必要があります。

PE セクションハッシュシグネチャ(PE section hash signatures)

ClamAV はファイルのハッシュだけでなく、PE ファイル内の特定のセクションのハッシュシグネチャを検出に利用することもできます。

PE セクションハッシュシグネチャも sigtool で作成することができます。

sigtool --mdb /path/to/32bit/PE/file

ただし、本記事執筆時点(2024 年 8 月)時点で最新の ClamAV でも 64bit PE バイナリのセクションハッシュシグネチャの作成はサポートしていないようです。

※ PE インポートテーブルハッシュシグネチャも同じく 32bit のみサポート

YARA ルール形式

ClamAV は YARA ルールを処理することができるため、YARA ルールが記述された .yar / .yara の拡張子を持つシグネチャを定義できます。

ただし、ClamAV で扱うことができる YARA ルールにはいくつか制限があるので注意が必要です。

詳しい制限や利用方法については本記事では割愛します。

参考:YARA Rules - ClamAV Documentation

許可ルールの構成

ClamAV では誤検知抑制のためにいくつかの許可ルールを構成することができます。

許可ルールの構成はファイルハッシュやシグネチャ単位で行うことができます。

特定のファイルの検出を抑制する許可ルールを作成することは簡単で、ファイルハッシュシグネチャと同じように sigtool で出力した行を追加するだけです。

SHA1 もしくは SHA256 ハッシュを許可ルールとして追加する場合、許可リストの拡張子として .sfp を使用します。

sigtool --sha256 ~/Downloads/eicar.com >> /var/lib/clamav/false-positives.sfp

sigtool の利用例

# シグネチャに使用する Hex 文字列を確認
echo -n "test" | sigtool --hex-dump

# ファイルハッシュシグネチャの作成
sigtool --md5 test1.txt > test.hdb
sigtool --sha1 test1.txt > test.hdb
sigtool --sha256 test1.txt > test.hdb

# 許可ルールの作成
sigtool --sha256 ~/Downloads/eicar.com >> /var/lib/clamav/false-positives.sfp

参考:Signatures - ClamAV Documentation

バイトコードシグネチャ(Bytecode Signatures)のチュートリアル

今回のテーマとしている問題の Devil Hunter を解くため、バイトコードシグネチャについて詳しくドキュメントを読み進めます。

参考:clamav-bytecode-compiler/docs/user/clambc-user.pdf at main · Cisco-Talos/clamav-bytecode-compiler

バイトコードコンパイラの準備

まずはバイトコードコンパイラを用意します。

バイトコードコンパイラのビルドに必要な clang と LLVM をインストールします。

clang と LLVM のバージョンはそろえる必要があり、バージョン 8 が推奨のようです。

試しに apt でインストールできる最新のバージョン 18 を使用してみたところビルドに失敗したので、Docker を利用してバージョン 8 の clang/LLVM 環境を用意することにします。

参考:clamav-docker/clamav-bytecode-compiler/README.md at main · Cisco-Talos/clamav-docker

任意のディレクトリをカレントディレクトリにセットした状態で以下のコマンドを実行します。

docker run -v `pwd`:/src -it clamav/clambc-compiler:stable /bin/bash

これで、clambc-compiler を実行できるようになりました。

image-20240803015241622

Logical signature bytecodes(Algorithmic detection bytecodes)

Logical signature bytecodes(別名 Algorithmic detection bytecodes) は、Logical signature(.ldb) と同等のシグネチャによってトリガーされるバイトコードシグネチャです。

ClamAV が公式に配布している CDV/CLV のシグネチャもバイトコードシグネチャに該当しています。

なお、ClamAV は既定では Cisco が正式に配布したシグネチャ以外のバイトコードシグネチャを「信頼できない」シグネチャとみなします。

そのため、独自に作成したバイトコードシグネチャを使用してスキャンを行う場合には、clamscan もしくは clamd で信頼できないバイトコードシグネチャの使用を許可するオプションを明示的に有効化する必要がある点に注意が必要です。

参考:ClamAV® blog: Brief Re-introduction to ClamAV Bytecode Signatures

Logical signature bytecodes を使用すると、Logical signature を使用するよりもより高速に動作する複雑な検出ロジックを定義することができます。

Algorithmic detection bytecodes は、大きく以下の要素で構成されます。

  • シグネチャと対応するマルウェア名
  • パターン定義 (for logical subexpressions)
  • シンプルな C 関数で記述された Logical signature(bool logical_trigger(void))
  • Logical signature にマッチした際にトリガーされるシグネチャ(int entrypoint(void)
  • (オプション) その他 entrypoint で使用する関数や定数

マルウェア名とターゲットの指定

バイトコードシグネチャでは、検出に使用するマルウェア名として必須の VIRUSNAME_PREFIX と、オプションの VIRUSNAMES を定義します。

VIRUSNAME_PREFIX に指定した名前は検出時に必ず使用されます。

また、オプションの VIRUSNAMES にカンマ区切りで定義した値は、VIRUSNAME_PREFIX 以降に付与されます。

// TESTMALWARE.001.A
// TESTMALWARE.001.B
VIRUSNAME_PREFIX("TESTMALWARE.001")
VIRUSNAMES("A","B")

このオプション部分については、バイトコードシグネチャの中で foundVirus("A"); のように foundVirus 関数の引数として与えられることで決定されます。

また、TARGET にはバイトコードシグネチャのスキャンターゲットとなる種類を示す整数値を指定する必要があります。

この整数値は、ここまで他のシグネチャで使用したものと同じく、以下のドキュメントに記載の値を使用します。

参考:ClamAV File Types and Target Types - ClamAV Documentation

例えば、以下の例ではターゲットとして HTML(normalized) を指定しています。

// HTML(normalized)
// HTML - Whitespace transformed to spaces, tags/tag attributes normalized, all lowercase.
TARGET(3)

なお、ターゲットとして HTML(normalized) を指定する場合、空白文字やタグの変換、またすべてのテキストが小文字で解釈されるなどの点に注意が必要です。(大文字のテキストを対象としたシグネチャが有効に動作しなくなります)

FLEVEL の指定

バイトコードシグネチャでも、最小要件となる FLEVEL の指定ができます。

バイトコードシグネチャ内で定義する場合は、FLEVEL の整数値ではなく、FUNC_LEVEL_098_5 のような指定をを行います。

// FUNC_LEVEL_098_5 = 78
FUNCTIONALITY_LEVEL_MIN(FUNC_LEVEL_098_5)

指定に使用可能な値は下記 Docs の FunctionalityLevel (bytecode enum) の列の値を使用できます。

参考:ClamAV Versions and Functionality Levels - ClamAV Documentation

Declarations と Definitions

バイトコードシグネチャの中では Declarations と Definitions を定義できます。

Declarations は変数宣言、Definitions は変数定義のように使用されます。

そのため、Declarations は常に Definitions の前に行う必要があります。

以下の例では、magic、trojan の 2 つの Declarations を定義しています。

// Declarations
SIGNATURES_DECL_BEGIN
DECLARE_SIGNATURE(magic)
DECLARE_SIGNATURE(trojan)
SIGNATURES_DECL_END

この Declarations と対応する Definitions は以下のように定義できます。

// Definitions 
SIGNATURES_DEF_BEGIN
DEFINE_SIGNATURE(magic,"61616161")
DEFINE_SIGNATURE(trojan,"74726f6a616e")
SIGNATURES_END

これで、magic と trojan という 2 つのグローバル変数が登録されるため、バイトコードシグネチャのロジックの中でこれらの値を利用できるようになります。

なお、シグネチャで特定の文字列を検出したい場合は、論理シグネチャ(Logical signatures) の場合と同じく Hexdump した文字列を指定する必要があります。

上記の例では、aaaa という文字列を対象としたいので、DEFINE_SIGNATURE(magic,"aaaa") ではなく DEFINE_SIGNATURE(magic,"61616161") を定義しています。

Logical signature 関数の定義

バイトコードシグネチャでは、シンプルな C 関数で記述された Logical signature(bool logical_trigger(void)) のパターンにマッチした場合にシグネチャ (int entrypoint(void) がトリガーされます。

そのため、まずは logical_trigger 関数を以下のように定義します。

// All bytecode triggered by logical signatures must have this function
bool logical_trigger(void)
{
    return count_match(Signatures.magic) > 1;
}

count_match 関数は、特定のパターンにマッチした回数をカウントして return する関数です。

上記の例の場合、magic で定義したパターンにマッチしたカウントを返却します。

// This is the bytecode function that is actually executed when the logical signature matched
int entrypoint(void)
{
    if (matches(Signatures.deadbeef)) { foundVirus ("A") ; }
    else { foundVirus("B"); }

    // success, return 0
    return 0;
}

シグネチャの定義

Logical signature にマッチした場合に呼び出されるバイトコードシグネチャの本体であるシグネチャ (int entrypoint(void) を定義します。

entrypoint の処理に成功した場合、この関数は常に 0 を返却するように定義することが推奨されています。

また、Malware の条件にマッチした場合には foundVirus 関数を使用します。

// This is the bytecode function that is actually executed when the logical signature matched
int entrypoint(void)
{
    if (matches(Signatures.trojan)) { foundVirus("A"); }
    else { foundVirus("B"); }

    // success, return 0
    return 0;
}

上記の例では、matches(Signatures.deadbeef) のパターンにマッチした場合にはオプションの VIRUSNAMES である A を使用し、マッチしない場合には B を使用して検出を行う実装になっています。

今回作成したシグネチャの全文は以下の通りです。

// TESTMALWARE.001.A
// TESTMALWARE.001.B
VIRUSNAME_PREFIX("TESTMALWARE.001")
VIRUSNAMES("A","B")
TARGET(0)

// FUNC_LEVEL_098_5 = 78
FUNCTIONALITY_LEVEL_MIN(FUNC_LEVEL_098_5)

// Declarations
SIGNATURES_DECL_BEGIN
DECLARE_SIGNATURE(magic)
DECLARE_SIGNATURE(trojan)
SIGNATURES_DECL_END

// Definitions 
SIGNATURES_DEF_BEGIN
DEFINE_SIGNATURE(magic,"61616161")
DEFINE_SIGNATURE(trojan,"74726f6a616e")
SIGNATURES_END

// All bytecode triggered by logical signatures must have this function
bool logical_trigger(void)
{
    return count_match(Signatures.magic) > 1;
}

// This is the bytecode function that is actually executed when the logical signature matched
int entrypoint(void)
{
    if (matches(Signatures.trojan)) { foundVirus("A"); }
    else { foundVirus("B"); }

    // success, return 0
    return 0;
}

バイトコードシグネチャのコンパイルとスキャン

ここまでに作成したバイトコードシグネチャをコンパイルしてスキャンを行います。

ディレクトリ構成は以下のようになっています。

$ tree
.
├── bytecodes
│   └── TESTCODE001.c
├── samplefiles
│   ├── TEST001.html
│   └── TEST001.txt
└── up_bytecodes.sh

まずは clambc-compiler のコンテナイメージを pull して起動します。

このとき、ボリュームディレクトリには c ファイルを格納している bytecodes ディレクトリを指定しています。

# clambc-compiler の Docker コンテナを pull して起動する
docker run -v ./bytecodes:/src -it clamav/clambc-compiler:stable /bin/bash

# 作成した TESTCODE001.c を TESTCODE001.cbc にコンパイルする
cd /src
clambc-compiler /src/TESTCODE001.c -o TESTCODE001.cbc -O2

上記の例では最適化オプションとして -O2 を指定しています。

最適化オプションには -O0 から -O3 までのいずれかを使用できますが、少なくとも -O1 以上の最適化オプションを使用することが推奨されているようです。

これでコンパイルされた CBC ファイルを使用することでスキャンが可能になります。

Cisco が配布していないバイトコードシグネチャを使用する場合は --bytecode-unsigned=yes のオプションを使用する必要があります。

また、意図した通りに検出が行われない場合などは --debug オプションを使用して調査を行います。

clamscan --bytecode-unsigned=yes --disable-cache -d ./bytecodes/TESTCODE001.cbc ./samplefiles/TEST001.txt

今回はターゲットファイルを HTML(normalized) に指定しているので、aaaa などの文字列を含む場合でも txt ファイルは検知されません。

image-20240810140541309

一方で、aaaa を 2 つ以上と trojan という文字列を含む HTML ファイルをスキャンした場合は、TESTMALWARE.001.A として検知できます。

image-20240810140618893

また、aaaa を 2 つ以上のみ含む場合には、if (matches(Signatures.trojan)) { foundVirus("A"); } の条件にはマッチしなくなるため TESTMALWARE.001.B として検出されます。

image-20240810140647040

参考:ClamAV® blog: Sample File Properties Collection Analysis Bytecode Signature Walkthrough

バイトコードシグネチャの活用

ここからは、バイトコードシグネチャの様々な記法を試してみたいと思います。

File Properties Collection 分析を使用する

libclamav が File Properties Collection の JSON を生成するように構成されている場合、バイトコードシグネチャでは生成された JSON オブジェクトを検出条件にすることができます。

参考:ClamAV® blog: Sample File Properties Collection Analysis Bytecode Signature Walkthrough

以下は、ClamAV のリポジトリにあるサンプルのシグネチャをカスタマイズしたものです。

VIRUSNAME_PREFIX("SUBMIT.filetype")
VIRUSNAMES("CL_TYPE_MSWORD", "CL_TYPE_MSPPT", "CL_TYPE_MSXL",
           "CL_TYPE_OOXML_WORD", "CL_TYPE_OOXML_PPT", "CL_TYPE_OOXML_XL",
           "CL_TYPE_MSEXE", "CL_TYPE_PDF", "CL_TYPE_MSOLE2", "CL_TYPE_UNKNOWN", "InActive")

/* Target type is 0, all relevant files */
TARGET(0)

/* JSON API call will require FUNC_LEVEL_098_5 = 78 */
/* PRECLASS_HOOK_DECLARE will require FUNC_LEVEL_098_7 = 80 */
FUNCTIONALITY_LEVEL_MIN(FUNC_LEVEL_098_7)

#define STR_MAXLEN 256

// Declarations
SIGNATURES_DECL_BEGIN
DECLARE_SIGNATURE(magic)
SIGNATURES_DECL_END

// Definitions 
SIGNATURES_DEF_BEGIN
DEFINE_SIGNATURE(magic,"73616d706c65")
SIGNATURES_END

// All bytecode triggered by logical signatures must have this function
bool logical_trigger(void)
{
    return matches(Signatures.magic);
}

int entrypoint()
{
    int32_t objid, type, strlen;
    char str[STR_MAXLEN];

    /* check is json is available, alerts on inactive (optional) */
    if (!json_is_active())
        foundVirus("InActive");

    /* acquire the filetype object */
    objid = json_get_object("FileType", 8, 0);
    if (objid <= 0) {
        debug_print_str("json object has no filetype!", 28);
        return 1;
    }
    type = json_get_type(objid);
    if (type != JSON_TYPE_STRING) {
        debug_print_str("json object filetype property is not string!", 44);
        return 1;
    }

    /* acquire string length, note +1 is for the NULL terminator */
    strlen = json_get_string_length(objid) + 1;
    /* prevent buffer overflow */
    if (strlen > STR_MAXLEN)
        strlen = STR_MAXLEN;

    /* acquire string data, note strlen includes NULL terminator */
    if (json_get_string(str, strlen, objid)) {
        /* debug print str (with '\n' and prepended message */
        debug_print_str(str, strlen);

        /* check the contained object's filetype */
        if (strlen == 14 && !memcmp(str, "CL_TYPE_MSEXE", 14)) {
            foundVirus("CL_TYPE_MSEXE");
            return 0;
        }
        if (strlen == 12 && !memcmp(str, "CL_TYPE_PDF", 12)) {
            foundVirus("CL_TYPE_PDF");
            return 0;
        }
        if (strlen == 19 && !memcmp(str, "CL_TYPE_OOXML_WORD", 19)) {
            foundVirus("CL_TYPE_OOXML_WORD");
            return 0;
        }
        if (strlen == 18 && !memcmp(str, "CL_TYPE_OOXML_PPT", 18)) {
            foundVirus("CL_TYPE_OOXML_PPT");
            return 0;
        }
        if (strlen == 17 && !memcmp(str, "CL_TYPE_OOXML_XL", 17)) {
            foundVirus("CL_TYPE_OOXML_XL");
            return 0;
        }
        if (strlen == 15 && !memcmp(str, "CL_TYPE_MSWORD", 15)) {
            foundVirus("CL_TYPE_MSWORD");
            return 0;
        }
        if (strlen == 14 && !memcmp(str, "CL_TYPE_MSPPT", 14)) {
            foundVirus("CL_TYPE_MSPPT");
            return 0;
        }
        if (strlen == 13 && !memcmp(str, "CL_TYPE_MSXL", 13)) {
            foundVirus("CL_TYPE_MSXL");
            return 0;
        }
        if (strlen == 15 && !memcmp(str, "CL_TYPE_MSOLE2", 15)) {
            foundVirus("CL_TYPE_MSOLE2");
            return 0;
        }

        foundVirus("CL_TYPE_UNKNOWN");
        return 0;
    }

    return 0;
}

上記のシグネチャでは、json_is_active() で File Properties Collection の JSON が生成されているかを確認し、生成されていない場合は InActive で検出を行います。

JSON が生成されている場合には FileType 要素の文字列を比較して対象ファイルの種類を検出できます。

if (strlen == 14 && !memcmp(str, "CL_TYPE_MSEXE", 14)) {
    foundVirus("CL_TYPE_MSEXE");
    return 0;
}

このシグネチャをコンパイルした cbc ファイルを使用したスキャンは以下のコマンドで実施できます。

clamscan を使用する場合は --gen-json オプションを使用する必要があります。

clamscan --gen-json --bytecode-unsigned=yes --disable-cache -d ./bytecodes/TESTCODE002.cbc  ./samplefiles/doc_sample.docx

このシグネチャを使用してサンプルの Word ファイルをスキャンすると SUBMIT.filetype.CL_TYPE_OOXML_WORD としてファイルを検出することができます。

image-20240812123022567

また、clamscan で --debug オプションを使用すると、生成された JSON オブジェクトをダンプできます。

今回の場合は、以下のような JSON をダンプすることができました。

{
  "Magic":"CLAMJSONv0",
  "RootFileType":"CL_TYPE_OOXML_WORD",
  "FileName":"doc_sample.docx",
  "FileType":"CL_TYPE_OOXML_WORD",
  "FileSize":29864,
  "FileMD5":"1d45f29f2c0523d334d4665acd30a208",
  "CoreProperties":{
    "Attributes":{
      "cp":"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
      "dc":"http://purl.org/dc/elements/1.1/",
      "dcterms":"http://purl.org/dc/terms/",
      "dcmitype":"http://purl.org/dc/dcmitype/",
      "xsi":"http://www.w3.org/2001/XMLSchema-instance"
    },
    "Title":{},
    "Keywords":{},
    "Created":{
      "Value":[
        "2024-07-26T03:53:00Z"
      ]
    },
    "Modified":{
      "Value":[
        "2024-07-26T03:53:00Z"
      ]
    }
  },
  "CorePropertiesFileCount":1,
  "CustomPropertiesFileCount":1,
  "ContainedObjects":[
    {
      "FileName":"app.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-05142ae220fd85d0de8aa5fdbb679e88.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":1105,
      "FileMD5":"133656865921af498aa28ec5b4f77b24"
    },
    {
      "FileName":".rels",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-1e11204e3c8bc451adce2bbf9684d61f.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":877,
      "FileMD5":"834bb9f139e2c89042bc5f73ca3681ac"
    },
    {
      "FileName":"core.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-16f795eae2e129a3bc2d6b6d045d7ec6.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":602,
      "FileMD5":"48d63fac37f1798301b4a380bc7fbd47"
    },
    {
      "FileName":"document.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-df7bee169e6486afa59bea2b33a0c6aa.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":16079,
      "FileMD5":"7caa4d90df6f35547e9a0212c52c3cfb"
    },
    {
      "FileName":"webSettings.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-e66fe6b6cdc5505ef6837c41f64a7dc9.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":976,
      "FileMD5":"e6ef4ee039cfbbe805db5fd64c9285d6"
    },
    {
      "FileName":"document.xml.rels",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-fcb2cd75df0a7781452fb1173c41b495.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":1962,
      "FileMD5":"a272a252c4514589d0f0b4095edbf65b"
    },
    {
      "FileName":"theme11.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-1196c9448d0bd47e20333d0bdd69f464.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":6808,
      "FileMD5":"d4c5d9b2fbc2334a7d960978173fcbc1"
    },
    {
      "FileName":"item3.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-c2d5f805bdde863a8c614f7b89a9ebda.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":219,
      "FileMD5":"5eca9e027b94e6cd1bc64f2a06dcee92"
    },
    {
      "FileName":"itemProps31.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-5c0ec7b9b208dd18bc16221dd74383a8.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":335,
      "FileMD5":"08962c42256ecf756d4c628af592ff6f"
    },
    {
      "FileName":"item3.xml.rels",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-5fef1f0b8a132ec493b0cb870a6ffc2d.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":293,
      "FileMD5":"14d033452b3fba1be7138b73fa7d2e4b"
    },
    {
      "FileName":"settings.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-99884602b93a2ddf9d3eeaa0e70f0967.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":6081,
      "FileMD5":"de6f78fd2ae424ff5fd54310e161a25b"
    },
    {
      "FileName":"fontTable.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-b2a1927c2e99604e8697311d48fd4e48.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":3025,
      "FileMD5":"aadd621b59bb8af6b1324ce4579db1d8"
    },
    {
      "FileName":"item22.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-bce3cc1fe0e193e1f97ad4aa8bded549.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":1131,
      "FileMD5":"1aa7d8c84bbb518b7eec09d8fa79bdf7"
    },
    {
      "FileName":"itemProps22.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-c1f279419e99fb98be3876f2ffaa58bc.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":614,
      "FileMD5":"bbb569ce2200d3b8e0f5af2fd0ee87f2"
    },
    {
      "FileName":"item22.xml.rels",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-68018783a654cc1de6c75725876934cf.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":293,
      "FileMD5":"1b52716de290d728812bdd805e6ee277"
    },
    {
      "FileName":"item13.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-1cb43cc91c383ab4dc962120b926aafb.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":306,
      "FileMD5":"217ee5ba5f9835428ff1ab7501faf018"
    },
    {
      "FileName":"itemProps13.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-1832e99cda5310119c2166934cca1c9c.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":341,
      "FileMD5":"f8fb694a3d90c965a676bdfec949186a"
    },
    {
      "FileName":"item13.xml.rels",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-395ccc11d1cb463e8e27d5075cd0f4ed.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":293,
      "FileMD5":"4c767529172a3f3e3f06c29757972fd2"
    },
    {
      "FileName":"styles.xml",
      "FilePath":"/tmp/20240812_032807-scantemp.57234350df/clamav-5e22fe74d8b0fa6e8b63533680ba5d43.tmp",
      "FileType":"CL_TYPE_TEXT_ASCII",
      "FileSize":51823,
      "FileMD5":"6092dcc046c92f52c15c83ef435e4f35",
      "Viruses":[
        "SUBMIT.filetype.CL_TYPE_OOXML_WORD"
      ]
    }
  ]
}

正規表現の利用

バイトコードシグネチャ内では POSIX 正規表現を利用することができます。

ここでは、seek によるスキャン位置の指定やループ処理なども使用することができるようです。

詳しくは公式ドキュメントを参照・・・。

int entrypoint(void) {
    REGEX_SCANNER;
    
    seek(0, SEEK_SET);

    for (;;) {
        REGEX_LOOP_BEGIN
        /* 
         * ! re2c
         * ANY = [^];
         * 
         * "eval(" [a-zA-Z_] [a-zA-Z_0-9]* ".unescape" {
         *     long pos = REGEX_POS;
         *     if (pos < 0)
         *         continue;
         *     debug("unescape found at: ");
         *     debug(pos);
         * }
         * ANY {
         *     continue;
         * }
         */
    }
    return 0;
}

バイトコードシグネチャを解析する

バイトコードシグネチャの概要情報を表示する

clambc --info コマンドを使用すると、コンパイルされたバイトコードシグネチャの概要情報を表示できます。

以下は、TESTCODE001.cbc の情報をダンプした例です。

image-20240812133304509

上記のダンプ結果から、Logical シグネチャの情報やバイトコードシグネチャ内の関数の数を把握することができます。

bytecode logical signature: TESTMALWARE.001.{A,B};Engine:79-255,Target:3;(0>1);61616161;74726f6a616e

バイトコードシグネチャのソースコードを参照する

clambc --printsrc コマンドを使用すると、以下のようにバイトコードシグネチャの元になったソースコードを参照できます。

image-20240812134504034

clambc のコードを見るとわかる通り、このソースコードはコンパイルされたバイトコードシグネチャ内の S から始まる行にエンコードされて埋め込まれています。

static void print_src(const char *file)
{
    char buf[4096];
    int nread, i, found = 0, lcnt = 0;
    FILE *f = fopen(file, "r");
    if (!f) {
        fprintf(stderr, "Unable to reopen %s\n", file);
        return;
    }
    do {
        nread = fread(buf, 1, sizeof(buf), f);
        for (i = 0; i < nread - 1; i++) {
            if (buf[i] == '\n') {
                lcnt++;
            }
            /* skip over the logical trigger */
            if (lcnt >= 2 && buf[i] == '\n' && buf[i + 1] == 'S') {
                found = 1;
                i += 2;
                break;
            }
        }
    } while (!found && (nread == sizeof(buf)));
    if (debug_flag)
        printf("[clambc] Source code:");
    do {
        for (; i + 1 < nread; i++) {
            if (buf[i] == 'S' || buf[i] == '\n') {
                putc('\n', stdout);
                continue;
            }
            putc(((buf[i] & 0xf) | ((buf[i + 1] & 0xf) << 4)), stdout);
            i++;
        }
        if (i == nread - 1 && nread != 1)
            fseek(f, -1, SEEK_CUR);
        i     = 0;
        nread = fread(buf, 1, sizeof(buf), f);
    } while (nread > 0);
    fclose(f);
}

このコードは (buf[i] & 0xf) | ((buf[i + 1] & 0xf) << 4) のコードで取り出されています。

参考:clamav/clambc/bcrun.c at main · Cisco-Talos/clamav

実際に以下の Python スクリプトを作成してバイトコードシグネチャ内に埋め込まれているソースコードのデコードが可能なことを確認します。

code = r"""Sobob`bdeedcedemdadldgeadbeednb`c`cacnbadSobob`bdeedcedemdadldgeadbeednb`c`cacnbbdSfeidbeeecendadmdedoe`ebeedfdidhehbbbdeedcedemdadldgeadbeednb`c`cacbbibSfeidbeeecendadmdedcehbbbadbblbbbbdbbib
deadbegdeddehbccibSSobob`bfdeendcdoeldedfeedldoe`cichcoeec`bmc`bgchcSfdeendcddeidodndadldiddeieoeldedfeedldoemdidndhbfdeendcdoeldedfeedldoe`cichcoeecibSSobob`bddefcflfafbgafdgifofnfcg
ceidgdndaddeeebeedceoeddedcdldoebdedgdidndSddedcdldadbeedoeceidgdndaddeeebeedhbmfafgfifcfibSddedcdldadbeedoeceidgdndaddeeebeedhbdgbgofjfafnfibSceidgdndaddeeebeedceoeddedcdldoeednddd
Sobob`bddefffifnfifdgifofnfcg`bSceidgdndaddeeebeedceoeddedfdoebdedgdidndSddedfdidndedoeceidgdndaddeeebeedhbmfafgfifcflbbbfcacfcacfcacfcacbbibSddedfdidndedoeceidgdndaddeeebeedhbdgbgofjfafnflbbbgcdcgcbcfcfffcaffcacfcefbbib
ceidgdndaddeeebeedceoeedndddSSobob`badlflf`bbfigdgefcfofdfef`bdgbgifgfgfefbgefdf`bbfig`blfofgfifcfaflf`bcgifgfnfafdgegbgefcg`bmfegcgdg`bhfaffgef`bdghfifcg`bffegnfcfdgifofnf
bfofoflf`blfofgfifcfaflfoedgbgifgfgfefbghbfgofifdfibSkgSbgefdgegbgnf`bcfofegnfdgoemfafdgcfhfhbceifgfnfafdgegbgefcgnbmfafgfifcfib`bnc`backcSmgSSobob`bdehfifcg`bifcg`bdghfef`bbfigdgefcfofdfef`bffegnfcfdgifofnf`bdghfafdg`bifcg`bafcfdgegaflflfig`befhgefcfegdgefdf`bgghfefnf`bdghfef`blfofgfifcfaflf`bcgifgfnfafdgegbgef`bmfafdgcfhfefdf
ifnfdg`befnfdgbgig`gofifnfdghbfgofifdfibSkgSifff`bhbmfafdgcfhfefcghbceifgfnfafdgegbgefcgnbdgbgofjfafnfibib`bkg`bffofegnfdffeifbgegcghbbbadbbibkc`bmgSeflfcgef`bkg`bffofegnfdffeifbgegcghbbbbdbbibkc`bmg
Sobob`bcgegcfcfefcgcglb`bbgefdgegbgnf`b`cSbgefdgegbgnf`b`ckcSmg"""

i = 0
while True:
    if i >= len(code):
        break
    else:
        if code[i] == "S" or code[i] == "\n":
            print()
            i += 1
        else:
            w = ((ord(code[i])) & 0xf) | (((ord(code[i+1])) & 0xf) << 4)
            print(chr(w), end="")
            i += 2

上記の Python スクリプトを実行すると clambc を使用した場合と同じくコンパイル元のソースコードを取得できることがわかりました。

image-20240812162154366

公式から配布されているバイトコードシグネチャや CTF の問題などで使用される場合には、clambc で簡単にソースコードを復元されないように、バイトコードシグネチャ内のソースコード部分が削除されたり置換されたりするようです。

実際に、Devil Hunter の問題バイナリでは、以下のコードで生成した偽のデータが埋め込まれており、clambc コマンドで元のソースを参照できないようになっていました。

fake = b"not so easy :P\n"
line = "S"
for c in fake:
    line += chr(0x60 + (c & 0xf))
    line += chr(0x60 + ((c>>4) & 0xf))
print(line)

参考:SECCON2022onlineCTF/reversing/devilhunter/builds/gen.py at main · SECCON/SECCON2022online_CTF

バイトコードシグネチャを逆アセンブルする

clambc --printsrc を使用できない場合、clambc --printbcir でバイトコードシグネチャを可読可能なテキストとして表示して解析を行うことができます。

例えば、ここまで使用してきた TESTCODE001.cbc を解析すると以下の結果を得ることができます。

$ clambc --printbcir ./bytecodes/TESTCODE001.cbc 

found 19 extra types of 83 total, starting at tid 69
TID  KIND                INTERNAL
------------------------------------------------------------------------
 65: DPointerType        i8*
 66: DPointerType        i16*
 67: DPointerType        i32*
 68: DPointerType        i64*
 69: DArrayType          [1 x i8]
 70: DArrayType          [2 x i8]
 71: DArrayType          [3 x i8]
 72: DArrayType          [4 x i8]
 73: DArrayType          [5 x i8]
 74: DArrayType          [6 x i8]
 75: DArrayType          [7 x i8]
 76: DPointerType        [64 x i32]*
 77: DPointerType        [18 x i8]*
 78: DPointerType        i32**
 79: DPointerType        i8**
 80: DFunctionType       i32 func ( i32 i32 )
 81: DArrayType          [18 x i8]
 82: DArrayType          [64 x i32]
------------------------------------------------------------------------
########################################################################
####################### Function id   0 ################################
########################################################################
found a total of 9 globals
GID  ID    VALUE
------------------------------------------------------------------------
  0 [  0]: i0 unknown
  1 [  1]: [18 x i8] unknown
  2 [  2]: [18 x i8] unknown
  3 [  3]: i32* unknown
  4 [  4]: i32* unknown
  5 [  5]: i8* unknown
  6 [  6]: i8* unknown
  7 [  7]: i8* unknown
  8 [  8]: i8* unknown
------------------------------------------------------------------------
found 4 values with 0 arguments and 4 locals
VID  ID    VALUE
------------------------------------------------------------------------
  0 [  0]: i32
  1 [  1]: i1
  2 [  2]: i32
  3 [  3]: i32
------------------------------------------------------------------------
found a total of 4 constants
CID  ID    VALUE
------------------------------------------------------------------------
  0 [  4]: 0(0x0)
  1 [  5]: 17(0x11)
  2 [  6]: 17(0x11)
  3 [  7]: 0(0x0)
------------------------------------------------------------------------
found a total of 8 total values
------------------------------------------------------------------------
FUNCTION ID: F.0 -> NUMINSTS 8
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
  0    0  OP_BC_LOAD          [39 /198/  3]  load  0 <- p.-2147483644
  0    1  OP_BC_ICMP_EQ       [21 /108/  3]  1 = (0 == 4)
  0    2  OP_BC_BRANCH        [17 / 85/  0]  br 1 ? bb.2 : bb.1

  1    3  OP_BC_CALL_API      [33 /168/  3]  2 = setvirusname[4] (p.-2147483640, 5)
  1    4  OP_BC_JMP           [18 / 90/  0]  jmp bb.3

  2    5  OP_BC_CALL_API      [33 /168/  3]  3 = setvirusname[4] (p.-2147483642, 6)
  2    6  OP_BC_JMP           [18 / 90/  0]  jmp bb.3

  3    7  OP_BC_RET           [19 / 98/  3]  ret 7
------------------------------------------------------------------------

TESTCODE001.c のコードは以下の通りでした。

バイトコードシグネチャの関数は entrypoint の 1 つのみですので、ダンプ結果内にも Function id 0 のみが存在しています。

// TESTMALWARE.001.A
// TESTMALWARE.001.B
VIRUSNAME_PREFIX("TESTMALWARE.001")
VIRUSNAMES("A","B")
TARGET(3)

// FUNC_LEVEL_098_5 = 78
FUNCTIONALITY_LEVEL_MIN(FUNC_LEVEL_098_5)

// Declarations
SIGNATURES_DECL_BEGIN
DECLARE_SIGNATURE(magic)
DECLARE_SIGNATURE(trojan)
SIGNATURES_DECL_END

// Definitions 
SIGNATURES_DEF_BEGIN
DEFINE_SIGNATURE(magic,"61616161")
DEFINE_SIGNATURE(trojan,"74726f6a616e")
SIGNATURES_END

// All bytecode triggered by logical signatures must have this function
bool logical_trigger(void)
{
    return count_match(Signatures.magic) > 1;
}

// This is the bytecode function that is actually executed when the logical signature matched
int entrypoint(void)
{
    if (matches(Signatures.trojan)) { foundVirus("A"); }
    else { foundVirus("B"); }

    // success, return 0
    return 0;
}

ここからは逆アセンブルされたコードを整理していきます。

この逆アセンブル結果については公開されている情報がほとんどないため、ClamAV のソースコードの情報を参照しながら手探りで読み進めます。

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

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

参考:clamav/libclamav/clambc.h at main · Cisco-Talos/clamav

BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_LOAD          [39 /198/  3]  load  0 <- p.-2147483644
0    1  OP_BC_ICMP_EQ       [21 /108/  3]  1 = (0 == 4)
0    2  OP_BC_BRANCH        [17 / 85/  0]  br 1 ? bb.2 : bb.1
1    3  OP_BC_CALL_API      [33 /168/  3]  2 = setvirusname[4] (p.-2147483640, 5)
1    4  OP_BC_JMP           [18 / 90/  0]  jmp bb.3
2    5  OP_BC_CALL_API      [33 /168/  3]  3 = setvirusname[4] (p.-2147483642, 6)
2    6  OP_BC_JMP           [18 / 90/  0]  jmp bb.3
3    7  OP_BC_RET           [19 / 98/  3]  ret 7

まず、最初の OP_BC_LOAD は何らかの値を変数(おそらく ID 0 の変数)にロードするようです。

続く OP_BC_ICMP_EQ は 2 つのオペランドを比較した結果を変数(おそらく ID 1 の変数) に格納します。

今回の場合は ID 4 の定数 0 と比較していそうです。

OP_BC_BRANCH ではその比較結果に応じて bb.2bb.1 のどちらにジャンプするかが決定されます。

VIRUSNAME などは p.-2147483640 のように表現されてしまっていてどちらがどの値かは判断ができませんが、ソースコードを見ると condition ? True : False の構造となっていることを確認できます。

// control operations (termination instructions)
case OP_BC_BRANCH:
    printf("br %d ? bb.%d : bb.%d", inst->u.branch.condition,inst->u.branch.br_true, inst->u.branch.br_false);
    (*bbnum)++;
    break;

シグネチャ変数が多いと読みづらいので、次は以下のコードから生成したバイトコードシグネチャを逆アセンブルしてみます。

int entrypoint(void)
{
    int a = 1;
    int b = 2;
    int c;

    c = a * count_match(Signatures.magic) + b * count_match(Signatures.trojan);
    if (c > 5) { foundVirus("A"); }
    else { foundVirus("B"); }

    // success, return 0
    return 0;
}

このコードから生成したバイトコードシグネチャを逆アセンブルすると以下の結果を得ることができます。

found 7 values with 0 arguments and 7 locals
VID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i32
1 [  1]: i32
2 [  2]: i32
3 [  3]: i32
4 [  4]: i1
5 [  5]: i32
6 [  6]: i32
------------------------------------------------------------------------
found a total of 5 constants
CID  ID    VALUE
------------------------------------------------------------------------
0 [  7]: 1(0x1)
1 [  8]: 5(0x5)
2 [  9]: 17(0x11)
3 [ 10]: 17(0x11)
4 [ 11]: 0(0x0)
------------------------------------------------------------------------
found a total of 12 total values
------------------------------------------------------------------------
FUNCTION ID: F.0 -> NUMINSTS 11
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_LOAD          [39 /198/  3]  load  0 <- p.-2147483642
0    1  OP_BC_LOAD          [39 /198/  3]  load  1 <- p.-2147483643
0    2  OP_BC_SHL           [8  / 43/  3]  2 = 1 << 7
0    3  OP_BC_ADD           [1  /  8/  0]  3 = 2 + 0
0    4  OP_BC_ICMP_SGT      [27 /138/  3]  4 = (3 > 8)
0    5  OP_BC_BRANCH        [17 / 85/  0]  br 4 ? bb.1 : bb.2

1    6  OP_BC_CALL_API      [33 /168/  3]  5 = setvirusname[4] (p.-2147483638, 9)
1    7  OP_BC_JMP           [18 / 90/  0]  jmp bb.3

2    8  OP_BC_CALL_API      [33 /168/  3]  6 = setvirusname[4] (p.-2147483640, 10)
2    9  OP_BC_JMP           [18 / 90/  0]  jmp bb.3

3   10  OP_BC_RET           [19 / 98/  3]  ret 11
------------------------------------------------------------------------

まず、変数 0 と 1 に magic と trojan のカウントを格納しています。

その後、変数 1 を 1 つ左シフトした結果(2 倍した結果) を変数 2 に格納し、さらにそれに変数 0 を加算します。

ここまでの計算は以下のコードと対応しています。

int a = 1;
int b = 2;
int c;
c = a * count_match(Signatures.magic) + b * count_match(Signatures.trojan);

その後、OP_BC_ICMP_SGT で計算結果(変数 3)の値が 5 より大きいか否かを比較し、条件分岐を行います。

このように、バイトコードシグネチャの逆アセンブル結果は VM コードのように読み進めることができます。

バイトコードシグネチャのデバッグ

バイトコードシグネチャの VM 実行は gdb を使ってある程度デバッグすることができます。

デバッグは以下のコマンドで実行できます。

gdb ~/clamav/build/clamscan/clamscan

# libclamav のロード
run --bytecode-unsigned=yes --disable-cache -d ./bytecodes/TESTCODE001.cbc ./samplefiles/TEST001.txt

# Breakpoint の設定と実行
b cli_vm_execute
run --bytecode-unsigned=yes --disable-cache -d ./bytecodes/TESTCODE001.cbc ./samplefiles/TEST001.txt

cli_vm_execute は bytecode_vm.c で定義されている関数で、バイトコードシグネチャ内のオペコードとオペランドを解釈して実行する処理を担っています。

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

image-20240814221102543

この関数のデバッグを進めると、以下のように各オペコードの処理時の実行コードにアクセスできるようになります。

image-20240814224438808

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

本記事では使用していませんが、バイトコードシグネチャのデバッグを行う際には、libclamav のソースコードを変更してデバッグトレースを出力する方法が非常に便利です。

詳しくは以下の記事にまとめています。

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

バイトコードシグネチャを解析して Devil Hunter を解く

ClamAV のシグネチャについて概ね整理できたところで、いよいよ Devil Hunter の問題を解きます。

Devil Hunter の問題バイナリは以下の cbc ファイルでした。

ClamBCafhaio`lfcf|aa```c``a```|ah`cnbac`cecnb`c``beaacp`clamcoincidencejb:4096
Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
Teddaaahdabahdacahdadahdaeahdafahdagahebdeebaddbdbahebndebceaacb`bbadb`baacb`bb`bb`bdaib`bdbfaah
Eaeacabbae|aebgefafdf``adbbe|aecgefefkf``aebae|amcgefdgfgifbgegcgnfafmfef``
G`ad`@`bdeBceBefBcfBcfBofBnfBnbBbeBefBfgBefBbgBcgBifBnfBgfBnbBfdBldBadBgd@`bad@Aa`bad@Aa`
A`b`bLabaa`b`b`Faeac
Baa``b`abTaa`aaab
Bb`baaabbaeAc`BeadTbaab
BTcab`b@dE
A`aaLbhfb`dab`dab`daahabndabad`bndabad`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`aa`b`d`b`b`aa`ah`aa`aa`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`b`b`b`d`b`d`b`b`b`b`bad`b`b`bad`b`d`aa`b`b`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`d`b`d`aa`Fbcgah
Bbadaedbbodad@dbadagdbbodaf@db`bahabbadAgd@db`d`bb@habTbaab
Baaaiiab`dbbaBdbhb`d`bbbbaabTaaaiabac
Bb`dajbbabajb`dakh`ajB`bhb`dalj`akB`bhb`bamn`albadandbbodad@dbadaocbbadanamb`bb`aabbabaoAadaabaanab`bb`aAadb`dbbaa`ajAahb`d`bb@h`Taabaaagaa
Bb`bbcaabbabacAadaabdakab`bbca@dahbeabbacbeaaabfaeaahbeaBmgaaabgak`bdabfab`d`bb@h`Taabgaadag
Bb`bbhaabbabacAadaabiakab`bbha@db`d`bb@haab`d`bb@h`Taabiaagae
Bb`dbjabbaabjab`dbkah`bjaB`bhb`dblaj`bkaB`bhb`bbman`blabadbnadbbodad@dbadboacbbadbnabmab`bb`bgbboab`bbab`baacb`bb`dbbbh`bjaBnahb`dbcbj`bbbB`bhb`bbdbn`bcbb`bbebc`Add@dbadbfbcbbadagbebb`bbgbc`Addbdbbadbhbcbbadbfbbgbb`b`fbbabbhbb`dbiba`bjaAdhaabjbiab`dbibBdbhb`d`bbbibaaTaabjbaeaf
Bb`bbkbgbagaablbeab`bbkbHbj`hnicgdb`bbmbc`Add@dbadbnbcbbadagbmbb`bbobc`AddAadbadb`ccbbadbnbbobb`bbacgbb`caabbceab`bbacHcj`hnjjcdaabcck`blbbbcb`bbdcc`Add@dbadbeccbbadagbdcb`bbfcc`AddAbdbadbgccbbadbecbfcb`bbhcgbbgcaabiceab`bbhcHoigndjkcdaabjck`bccbicb`bbkcc`Add@dbadblccbbadagbkcb`bbmcc`AddAcdbadbnccbbadblcbmcb`bbocgbbncaab`deab`bbocHcoaljkhgdaabadk`bjcb`db`bbbdc`Add@dbadbcdcbbadagbbdb`bbddc`AddAddbadbedcbbadbcdbddb`bbfdgbbedaabgdeab`bbfdHcoalionedaabhdk`badbgdb`bbidc`Add@dbadbjdcbbadagbidb`bbkdc`AddAedbadbldcbbadbjdbkdb`bbmdgbbldaabndeab`bbmdHoilnikkcdaabodk`bhdbndb`bb`ec`Add@dbadbaecbbadagb`eb`bbbec`AddAfdbadbcecbbadbaebbeb`bbdegbbceaabeeeab`bbdeHdochfheedaabfek`bodbeeb`bbgec`Add@dbadbhecbbadagbgeb`bbiec`AddAgdbadbjecbbadbhebieb`bbkegbbjeaableeab`bbkeHdiemjoeedaabmek`bfebleb`bbnec`Add@dbadboecbbadagbneb`bb`fc`AddAhdbadbafcbbadboeb`fb`bbbfgbbafaabcfeab`bbbfHoimmoklfdaabdfk`bmebcfb`dbefo`bdfb`d`bbbef`Tbaag
Bb`dbffbb`bffaabgfn`bffTcaaabgfE
Aab`bLbaab`b`b`dab`dab`d`b`d`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`aa`b`d`b`d`Fbfaac
Bb`d`bb@habb`d`bbG`lckjljhaaTbaaa
Bb`dacbbaaacb`dadbbabadb`baen`acb`bafn`adb`bagh`afAcdb`bahi``agb`baik`ahBoodb`bajm`aiaeb`bakh`ajAhdb`bali`aeBhadb`baml`akalb`bana`afAadaaaoeab`banAddb`db`ao`anb`dbaao`amb`d`bbb`aabb`d`bbbaaaaTaaaoabaa
BTcab`bamE
Snfofdg`bcgof`befafcgig`bjc`ej`

cbc ファイルの情報を調べる

まずは clambc --info で情報を調べます。

このバイトコードシグネチャのトリガーとなる logical signature は 534543434f4e7b(SECCON{) がシグネチャとして定義されたもののようです。

$ clambc --info flag.cbc

Bytecode format functionality level: 6
Bytecode metadata:
        compiler version: 0.105.0
        compiled on: (1668026257) Wed Nov  9 20:37:37 2022
        compiled by:
        target exclude: 0
        bytecode type: logical only
        bytecode functionality level: 0 - 0
        bytecode logical signature: Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
        virusname prefix: (null)
        virusnames: 0
        bytecode triggered on: files matching logical signature
        number of functions: 3
        number of types: 21
        number of global constants: 4
        number of debug nodes: 0
        bytecode APIs used:
         read, seek, setvirusname

残念ながらソースコードの情報は改ざんされているようで、clambc --printsrc を使用しても情報は取得できませんでした。

image-20240815015927581

そこで、clambc --printbc の出力結果を調べることにします。

$ clambc --printbc flag.cbc
found 21 extra types of 85 total, starting at tid 69
TID  KIND                INTERNAL
------------------------------------------------------------------------
65: DPointerType        i8*
66: DPointerType        i16*
67: DPointerType        i32*
68: DPointerType        i64*
69: DArrayType          [1 x i8]
70: DArrayType          [2 x i8]
71: DArrayType          [3 x i8]
72: DArrayType          [4 x i8]
73: DArrayType          [5 x i8]
74: DArrayType          [6 x i8]
75: DArrayType          [7 x i8]
76: DPointerType        [22 x i8]*
77: DPointerType        i8**
78: DArrayType          [36 x i8]
79: DPointerType        [36 x i8]*
80: DPointerType        [9 x i32]*
81: DFunctionType       i32 func ( i32 i32 )
82: DFunctionType       i32 func ( i32 i32 )
83: DArrayType          [9 x i32]
84: DArrayType          [22 x i8]
------------------------------------------------------------------------
########################################################################
####################### Function id   0 ################################
########################################################################
found a total of 4 globals
GID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i0 unknown
1 [  1]: [22 x i8] unknown
2 [  2]: i8* unknown
3 [  3]: i8* unknown
------------------------------------------------------------------------
found 2 values with 0 arguments and 2 locals
VID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i1
1 [  1]: i32
------------------------------------------------------------------------
found a total of 2 constants
CID  ID    VALUE
------------------------------------------------------------------------
0 [  2]: 21(0x15)
1 [  3]: 0(0x0)
------------------------------------------------------------------------
found a total of 4 total values
------------------------------------------------------------------------
FUNCTION ID: F.0 -> NUMINSTS 5
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_CALL_DIRECT   [32 /160/  0]  0 = call F.1 ()
0    1  OP_BC_BRANCH        [17 / 85/  0]  br 0 ? bb.1 : bb.2

1    2  OP_BC_CALL_API      [33 /168/  3]  1 = setvirusname[4] (p.-2147483645, 2)
1    3  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

2    4  OP_BC_RET           [19 / 98/  3]  ret 3
------------------------------------------------------------------------
########################################################################
####################### Function id   1 ################################
########################################################################
found a total of 4 globals
GID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i0 unknown
1 [  1]: [22 x i8] unknown
2 [  2]: i8* unknown
3 [  3]: i8* unknown
------------------------------------------------------------------------
found 104 values with 0 arguments and 104 locals
VID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: alloc i64
1 [  1]: alloc i64
2 [  2]: alloc i64
3 [  3]: alloc i8
4 [  4]: alloc [36 x i8]
5 [  5]: i8*
6 [  6]: alloc [36 x i8]
7 [  7]: i8*
8 [  8]: i32
9 [  9]: i1
10 [ 10]: i64
11 [ 11]: i64
12 [ 12]: i64
13 [ 13]: i32
14 [ 14]: i8*
15 [ 15]: i8*
16 [ 16]: i32
17 [ 17]: i1
18 [ 18]: i64
19 [ 19]: i32
20 [ 20]: i1
21 [ 21]: i8
22 [ 22]: i1
23 [ 23]: i1
24 [ 24]: i32
25 [ 25]: i1
26 [ 26]: i64
27 [ 27]: i64
28 [ 28]: i64
29 [ 29]: i32
30 [ 30]: i8*
31 [ 31]: i8*
32 [ 32]: i32
33 [ 33]: i32
34 [ 34]: i64
35 [ 35]: i64
36 [ 36]: i32
37 [ 37]: i32
38 [ 38]: i8*
39 [ 39]: i32
40 [ 40]: i8*
41 [ 41]: i64
42 [ 42]: i1
43 [ 43]: i32
44 [ 44]: i1
45 [ 45]: i32
46 [ 46]: i8*
47 [ 47]: i32
48 [ 48]: i8*
49 [ 49]: i32
50 [ 50]: i1
51 [ 51]: i1
52 [ 52]: i32
53 [ 53]: i8*
54 [ 54]: i32
55 [ 55]: i8*
56 [ 56]: i32
57 [ 57]: i1
58 [ 58]: i1
59 [ 59]: i32
60 [ 60]: i8*
61 [ 61]: i32
62 [ 62]: i8*
63 [ 63]: i32
64 [ 64]: i1
65 [ 65]: i1
66 [ 66]: i32
67 [ 67]: i8*
68 [ 68]: i32
69 [ 69]: i8*
70 [ 70]: i32
71 [ 71]: i1
72 [ 72]: i1
73 [ 73]: i32
74 [ 74]: i8*
75 [ 75]: i32
76 [ 76]: i8*
77 [ 77]: i32
78 [ 78]: i1
79 [ 79]: i1
80 [ 80]: i32
81 [ 81]: i8*
82 [ 82]: i32
83 [ 83]: i8*
84 [ 84]: i32
85 [ 85]: i1
86 [ 86]: i1
87 [ 87]: i32
88 [ 88]: i8*
89 [ 89]: i32
90 [ 90]: i8*
91 [ 91]: i32
92 [ 92]: i1
93 [ 93]: i1
94 [ 94]: i32
95 [ 95]: i8*
96 [ 96]: i32
97 [ 97]: i8*
98 [ 98]: i32
99 [ 99]: i1
100 [100]: i1
101 [101]: i64
102 [102]: i64
103 [103]: i1
------------------------------------------------------------------------
found a total of 72 constants
CID  ID    VALUE
------------------------------------------------------------------------
0 [104]: 0(0x0)
1 [105]: 0(0x0)
2 [106]: 7(0x7)
3 [107]: 0(0x0)
4 [108]: 0(0x0)
5 [109]: 36(0x24)
6 [110]: 32(0x20)
7 [111]: 32(0x20)
8 [112]: 0(0x0)
9 [113]: 1(0x1)
10 [114]: 1(0x1)
11 [115]: 1(0x1)
12 [116]: 0(0x0)
13 [117]: 1(0x1)
14 [118]: 0(0x0)
15 [119]: 125(0x7d)
16 [120]: 0(0x0)
17 [121]: 1(0x1)
18 [122]: 0(0x0)
19 [123]: 0(0x0)
20 [124]: 0(0x0)
21 [125]: 32(0x20)
22 [126]: 32(0x20)
23 [127]: 0(0x0)
24 [128]: 30(0x1e)
25 [129]: 32(0x20)
26 [130]: 4(0x4)
27 [131]: 0(0x0)
28 [132]: 4(0x4)
29 [133]: 4(0x4)
30 [134]: 36(0x24)
31 [135]: 1939767458(0x739e80a2)
32 [136]: 4(0x4)
33 [137]: 0(0x0)
34 [138]: 4(0x4)
35 [139]: 1(0x1)
36 [140]: 984514723(0x3aae80a3)
37 [141]: 4(0x4)
38 [142]: 0(0x0)
39 [143]: 4(0x4)
40 [144]: 2(0x2)
41 [145]: 1000662943(0x3ba4e79f)
42 [146]: 4(0x4)
43 [147]: 0(0x0)
44 [148]: 4(0x4)
45 [149]: 3(0x3)
46 [150]: 2025505267(0x78bac1f3)
47 [151]: 4(0x4)
48 [152]: 0(0x0)
49 [153]: 4(0x4)
50 [154]: 4(0x4)
51 [155]: 1593426419(0x5ef9c1f3)
52 [156]: 4(0x4)
53 [157]: 0(0x0)
54 [158]: 4(0x4)
55 [159]: 5(0x5)
56 [160]: 1002040479(0x3bb9ec9f)
57 [161]: 4(0x4)
58 [162]: 0(0x0)
59 [163]: 4(0x4)
60 [164]: 6(0x6)
61 [165]: 1434878964(0x558683f4)
62 [166]: 4(0x4)
63 [167]: 0(0x0)
64 [168]: 4(0x4)
65 [169]: 7(0x7)
66 [170]: 1442502036(0x55fad594)
67 [171]: 4(0x4)
68 [172]: 0(0x0)
69 [173]: 4(0x4)
70 [174]: 8(0x8)
71 [175]: 1824513439(0x6cbfdd9f)
------------------------------------------------------------------------
found a total of 176 total values
------------------------------------------------------------------------
FUNCTION ID: F.1 -> NUMINSTS 115
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_GEPZ          [36 /184/  4]  5 = gepz p.4 + (104)
0    1  OP_BC_GEPZ          [36 /184/  4]  7 = gepz p.6 + (105)
0    2  OP_BC_CALL_API      [33 /168/  3]  8 = seek[3] (106, 107)
0    3  OP_BC_COPY          [34 /174/  4]  cp 108 -> 2
0    4  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

1    5  OP_BC_ICMP_ULT      [25 /129/  4]  9 = (18 < 109)
1    6  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
1    7  OP_BC_BRANCH        [17 / 85/  0]  br 9 ? bb.2 : bb.3

2    8  OP_BC_COPY          [34 /174/  4]  cp 2 -> 10
2    9  OP_BC_SHL           [8  / 44/  4]  11 = 10 << 110
2   10  OP_BC_ASHR          [10 / 54/  4]  12 = 11 >> 111
2   11  OP_BC_TRUNC         [14 / 73/  3]  13 = 12 trunc ffffffffffffffff
2   12  OP_BC_GEPZ          [36 /184/  4]  14 = gepz p.4 + (112)
2   13  OP_BC_GEP1          [35 /179/  4]  15 = gep1 p.14 + (13 * 65)
2   14  OP_BC_CALL_API      [33 /168/  3]  16 = read[1] (p.15, 113)
2   15  OP_BC_ICMP_SLT      [30 /153/  3]  17 = (16 < 114)
2   16  OP_BC_ADD           [1  /  9/  0]  18 = 10 + 115
2   17  OP_BC_COPY          [34 /174/  4]  cp 116 -> 0
2   18  OP_BC_BRANCH        [17 / 85/  0]  br 17 ? bb.7 : bb.1

3   19  OP_BC_CALL_API      [33 /168/  3]  19 = read[1] (p.3, 117)
3   20  OP_BC_ICMP_SGT      [27 /138/  3]  20 = (19 > 118)
3   21  OP_BC_COPY          [34 /171/  1]  cp 3 -> 21
3   22  OP_BC_ICMP_EQ       [21 /106/  1]  22 = (21 == 119)
3   23  OP_BC_AND           [11 / 55/  0]  23 = 20 & 22
3   24  OP_BC_COPY          [34 /174/  4]  cp 120 -> 0
3   25  OP_BC_BRANCH        [17 / 85/  0]  br 23 ? bb.4 : bb.7

4   26  OP_BC_CALL_API      [33 /168/  3]  24 = read[1] (p.3, 121)
4   27  OP_BC_ICMP_SGT      [27 /138/  3]  25 = (24 > 122)
4   28  OP_BC_COPY          [34 /174/  4]  cp 123 -> 1
4   29  OP_BC_COPY          [34 /174/  4]  cp 124 -> 0
4   30  OP_BC_BRANCH        [17 / 85/  0]  br 25 ? bb.7 : bb.5

5   31  OP_BC_COPY          [34 /174/  4]  cp 1 -> 26
5   32  OP_BC_SHL           [8  / 44/  4]  27 = 26 << 125
5   33  OP_BC_ASHR          [10 / 54/  4]  28 = 27 >> 126
5   34  OP_BC_TRUNC         [14 / 73/  3]  29 = 28 trunc ffffffffffffffff
5   35  OP_BC_GEPZ          [36 /184/  4]  30 = gepz p.4 + (127)
5   36  OP_BC_GEP1          [35 /179/  4]  31 = gep1 p.30 + (29 * 65)
5   37  OP_BC_LOAD          [39 /198/  3]  load  32 <- p.31
5   38  OP_BC_CALL_DIRECT   [32 /163/  3]  33 = call F.2 (32)
5   39  OP_BC_SHL           [8  / 44/  4]  34 = 26 << 128
5   40  OP_BC_ASHR          [10 / 54/  4]  35 = 34 >> 129
5   41  OP_BC_TRUNC         [14 / 73/  3]  36 = 35 trunc ffffffffffffffff
5   42  OP_BC_MUL           [3  / 18/  0]  37 = 130 * 131
5   43  OP_BC_GEP1          [35 /179/  4]  38 = gep1 p.7 + (37 * 65)
5   44  OP_BC_MUL           [3  / 18/  0]  39 = 132 * 36
5   45  OP_BC_GEP1          [35 /179/  4]  40 = gep1 p.38 + (39 * 65)
5   46  OP_BC_STORE         [38 /193/  3]  store 33 -> p.40
5   47  OP_BC_ADD           [1  /  9/  0]  41 = 26 + 133
5   48  OP_BC_ICMP_ULT      [25 /129/  4]  42 = (41 < 134)
5   49  OP_BC_COPY          [34 /174/  4]  cp 41 -> 1
5   50  OP_BC_BRANCH        [17 / 85/  0]  br 42 ? bb.5 : bb.6

6   51  OP_BC_LOAD          [39 /198/  3]  load  43 <- p.7
6   52  OP_BC_ICMP_EQ       [21 /108/  3]  44 = (43 == 135)
6   53  OP_BC_MUL           [3  / 18/  0]  45 = 136 * 137
6   54  OP_BC_GEP1          [35 /179/  4]  46 = gep1 p.7 + (45 * 65)
6   55  OP_BC_MUL           [3  / 18/  0]  47 = 138 * 139
6   56  OP_BC_GEP1          [35 /179/  4]  48 = gep1 p.46 + (47 * 65)
6   57  OP_BC_LOAD          [39 /198/  3]  load  49 <- p.48
6   58  OP_BC_ICMP_EQ       [21 /108/  3]  50 = (49 == 140)
6   59  OP_BC_AND           [11 / 55/  0]  51 = 44 & 50
6   60  OP_BC_MUL           [3  / 18/  0]  52 = 141 * 142
6   61  OP_BC_GEP1          [35 /179/  4]  53 = gep1 p.7 + (52 * 65)
6   62  OP_BC_MUL           [3  / 18/  0]  54 = 143 * 144
6   63  OP_BC_GEP1          [35 /179/  4]  55 = gep1 p.53 + (54 * 65)
6   64  OP_BC_LOAD          [39 /198/  3]  load  56 <- p.55
6   65  OP_BC_ICMP_EQ       [21 /108/  3]  57 = (56 == 145)
6   66  OP_BC_AND           [11 / 55/  0]  58 = 51 & 57
6   67  OP_BC_MUL           [3  / 18/  0]  59 = 146 * 147
6   68  OP_BC_GEP1          [35 /179/  4]  60 = gep1 p.7 + (59 * 65)
6   69  OP_BC_MUL           [3  / 18/  0]  61 = 148 * 149
6   70  OP_BC_GEP1          [35 /179/  4]  62 = gep1 p.60 + (61 * 65)
6   71  OP_BC_LOAD          [39 /198/  3]  load  63 <- p.62
6   72  OP_BC_ICMP_EQ       [21 /108/  3]  64 = (63 == 150)
6   73  OP_BC_AND           [11 / 55/  0]  65 = 58 & 64
6   74  OP_BC_MUL           [3  / 18/  0]  66 = 151 * 152
6   75  OP_BC_GEP1          [35 /179/  4]  67 = gep1 p.7 + (66 * 65)
6   76  OP_BC_MUL           [3  / 18/  0]  68 = 153 * 154
6   77  OP_BC_GEP1          [35 /179/  4]  69 = gep1 p.67 + (68 * 65)
6   78  OP_BC_LOAD          [39 /198/  3]  load  70 <- p.69
6   79  OP_BC_ICMP_EQ       [21 /108/  3]  71 = (70 == 155)
6   80  OP_BC_AND           [11 / 55/  0]  72 = 65 & 71
6   81  OP_BC_MUL           [3  / 18/  0]  73 = 156 * 157
6   82  OP_BC_GEP1          [35 /179/  4]  74 = gep1 p.7 + (73 * 65)
6   83  OP_BC_MUL           [3  / 18/  0]  75 = 158 * 159
6   84  OP_BC_GEP1          [35 /179/  4]  76 = gep1 p.74 + (75 * 65)
6   85  OP_BC_LOAD          [39 /198/  3]  load  77 <- p.76
6   86  OP_BC_ICMP_EQ       [21 /108/  3]  78 = (77 == 160)
6   87  OP_BC_AND           [11 / 55/  0]  79 = 72 & 78
6   88  OP_BC_MUL           [3  / 18/  0]  80 = 161 * 162
6   89  OP_BC_GEP1          [35 /179/  4]  81 = gep1 p.7 + (80 * 65)
6   90  OP_BC_MUL           [3  / 18/  0]  82 = 163 * 164
6   91  OP_BC_GEP1          [35 /179/  4]  83 = gep1 p.81 + (82 * 65)
6   92  OP_BC_LOAD          [39 /198/  3]  load  84 <- p.83
6   93  OP_BC_ICMP_EQ       [21 /108/  3]  85 = (84 == 165)
6   94  OP_BC_AND           [11 / 55/  0]  86 = 79 & 85
6   95  OP_BC_MUL           [3  / 18/  0]  87 = 166 * 167
6   96  OP_BC_GEP1          [35 /179/  4]  88 = gep1 p.7 + (87 * 65)
6   97  OP_BC_MUL           [3  / 18/  0]  89 = 168 * 169
6   98  OP_BC_GEP1          [35 /179/  4]  90 = gep1 p.88 + (89 * 65)
6   99  OP_BC_LOAD          [39 /198/  3]  load  91 <- p.90
6  100  OP_BC_ICMP_EQ       [21 /108/  3]  92 = (91 == 170)
6  101  OP_BC_AND           [11 / 55/  0]  93 = 86 & 92
6  102  OP_BC_MUL           [3  / 18/  0]  94 = 171 * 172
6  103  OP_BC_GEP1          [35 /179/  4]  95 = gep1 p.7 + (94 * 65)
6  104  OP_BC_MUL           [3  / 18/  0]  96 = 173 * 174
6  105  OP_BC_GEP1          [35 /179/  4]  97 = gep1 p.95 + (96 * 65)
6  106  OP_BC_LOAD          [39 /198/  3]  load  98 <- p.97
6  107  OP_BC_ICMP_EQ       [21 /108/  3]  99 = (98 == 175)
6  108  OP_BC_AND           [11 / 55/  0]  100 = 93 & 99
6  109  OP_BC_SEXT          [15 / 79/  4]  101 = 100 sext 1
6  110  OP_BC_COPY          [34 /174/  4]  cp 101 -> 0
6  111  OP_BC_JMP           [18 / 90/  0]  jmp bb.7

7  112  OP_BC_COPY          [34 /174/  4]  cp 0 -> 102
7  113  OP_BC_TRUNC         [14 / 70/  0]  103 = 102 trunc ffffffffffffffff
7  114  OP_BC_RET           [19 / 95/  0]  ret 103
------------------------------------------------------------------------
########################################################################
####################### Function id   2 ################################
########################################################################
found a total of 4 globals
GID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i0 unknown
1 [  1]: [22 x i8] unknown
2 [  2]: i8* unknown
3 [  3]: i8* unknown
------------------------------------------------------------------------
found 18 values with 1 arguments and 17 locals
VID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i32 argument
1 [  1]: alloc i64
2 [  2]: alloc i64
3 [  3]: i64
4 [  4]: i64
5 [  5]: i32
6 [  6]: i32
7 [  7]: i32
8 [  8]: i32
9 [  9]: i32
10 [ 10]: i32
11 [ 11]: i32
12 [ 12]: i32
13 [ 13]: i32
14 [ 14]: i32
15 [ 15]: i1
16 [ 16]: i64
17 [ 17]: i64
------------------------------------------------------------------------
found a total of 8 constants
CID  ID    VALUE
------------------------------------------------------------------------
0 [ 18]: 0(0x0)
1 [ 19]: 181056448(0xacab3c0)
2 [ 20]: 3(0x3)
3 [ 21]: 255(0xff)
4 [ 22]: 8(0x8)
5 [ 23]: 24(0x18)
6 [ 24]: 1(0x1)
7 [ 25]: 4(0x4)
------------------------------------------------------------------------
found a total of 26 total values
------------------------------------------------------------------------
FUNCTION ID: F.2 -> NUMINSTS 22
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
0    1  OP_BC_COPY          [34 /174/  4]  cp 19 -> 1
0    2  OP_BC_JMP           [18 / 90/  0]  jmp bb.1

1    3  OP_BC_COPY          [34 /174/  4]  cp 1 -> 3
1    4  OP_BC_COPY          [34 /174/  4]  cp 2 -> 4
1    5  OP_BC_TRUNC         [14 / 73/  3]  5 = 3 trunc ffffffffffffffff
1    6  OP_BC_TRUNC         [14 / 73/  3]  6 = 4 trunc ffffffffffffffff
1    7  OP_BC_SHL           [8  / 43/  3]  7 = 6 << 20
1    8  OP_BC_LSHR          [9  / 48/  3]  8 = 0 >> 7
1    9  OP_BC_AND           [11 / 58/  3]  9 = 8 & 21
1   10  OP_BC_XOR           [13 / 68/  3]  10 = 9 ^ 5
1   11  OP_BC_SHL           [8  / 43/  3]  11 = 10 << 22
1   12  OP_BC_LSHR          [9  / 48/  3]  12 = 5 >> 23
1   13  OP_BC_OR            [12 / 63/  3]  13 = 11 | 12
1   14  OP_BC_ADD           [1  /  8/  0]  14 = 6 + 24
1   15  OP_BC_ICMP_EQ       [21 /108/  3]  15 = (14 == 25)
1   16  OP_BC_SEXT          [15 / 79/  4]  16 = 14 sext 20
1   17  OP_BC_SEXT          [15 / 79/  4]  17 = 13 sext 20
1   18  OP_BC_COPY          [34 /174/  4]  cp 16 -> 2
1   19  OP_BC_COPY          [34 /174/  4]  cp 17 -> 1
1   20  OP_BC_BRANCH        [17 / 85/  0]  br 15 ? bb.2 : bb.1

2   21  OP_BC_RET           [19 / 98/  3]  ret 13
------------------------------------------------------------------------

このシグネチャでは、ID 0 から 2 までの 3 つの関数が定義されているようです。

このうち、以下の ID 0 の関数が entrypoint のように見えます。

この中では、call F.1 () の結果を評価し、True の場合には foundVirus が返される実装になっています。

BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_CALL_DIRECT   [32 /160/  0]  0 = call F.1 ()
0    1  OP_BC_BRANCH        [17 / 85/  0]  br 0 ? bb.1 : bb.2

1    2  OP_BC_CALL_API      [33 /168/  3]  1 = setvirusname[4] (p.-2147483645, 2)
1    3  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

2    4  OP_BC_RET           [19 / 98/  3]  ret 3

そのため、call F.1 () が True を返す入力が正しい Flag になることを予想できます。

ID 1 の関数では何かしらの値を比較しているようです。

また、その中では call F.2 (32) により ID 2 の関数が実行されています。

Func2 の調査

ID 1 の関数がメインの処理のようですが、先にコード量の少ない ID 2 の関数を調べることにします。

以下は ID 2 の関数(以降 Func2)の逆アセンブル結果です。

########################################################################
####################### Function id   2 ################################
########################################################################
found a total of 4 globals
GID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i0 unknown
1 [  1]: [22 x i8] unknown
2 [  2]: i8* unknown
3 [  3]: i8* unknown
------------------------------------------------------------------------
found 18 values with 1 arguments and 17 locals
VID  ID    VALUE
------------------------------------------------------------------------
0 [  0]: i32 argument
1 [  1]: alloc i64
2 [  2]: alloc i64
3 [  3]: i64
4 [  4]: i64
5 [  5]: i32
6 [  6]: i32
7 [  7]: i32
8 [  8]: i32
9 [  9]: i32
10 [ 10]: i32
11 [ 11]: i32
12 [ 12]: i32
13 [ 13]: i32
14 [ 14]: i32
15 [ 15]: i1
16 [ 16]: i64
17 [ 17]: i64
------------------------------------------------------------------------
found a total of 8 constants
CID  ID    VALUE
------------------------------------------------------------------------
0 [ 18]: 0(0x0)
1 [ 19]: 181056448(0xacab3c0)
2 [ 20]: 3(0x3)
3 [ 21]: 255(0xff)
4 [ 22]: 8(0x8)
5 [ 23]: 24(0x18)
6 [ 24]: 1(0x1)
7 [ 25]: 4(0x4)
------------------------------------------------------------------------
found a total of 26 total values
------------------------------------------------------------------------
FUNCTION ID: F.2 -> NUMINSTS 22
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
0    1  OP_BC_COPY          [34 /174/  4]  cp 19 -> 1
0    2  OP_BC_JMP           [18 / 90/  0]  jmp bb.1

1    3  OP_BC_COPY          [34 /174/  4]  cp 1 -> 3
1    4  OP_BC_COPY          [34 /174/  4]  cp 2 -> 4
1    5  OP_BC_TRUNC         [14 / 73/  3]  5 = 3 trunc ffffffffffffffff
1    6  OP_BC_TRUNC         [14 / 73/  3]  6 = 4 trunc ffffffffffffffff
1    7  OP_BC_SHL           [8  / 43/  3]  7 = 6 << 20
1    8  OP_BC_LSHR          [9  / 48/  3]  8 = 0 >> 7
1    9  OP_BC_AND           [11 / 58/  3]  9 = 8 & 21
1   10  OP_BC_XOR           [13 / 68/  3]  10 = 9 ^ 5
1   11  OP_BC_SHL           [8  / 43/  3]  11 = 10 << 22
1   12  OP_BC_LSHR          [9  / 48/  3]  12 = 5 >> 23
1   13  OP_BC_OR            [12 / 63/  3]  13 = 11 | 12
1   14  OP_BC_ADD           [1  /  8/  0]  14 = 6 + 24
1   15  OP_BC_ICMP_EQ       [21 /108/  3]  15 = (14 == 25)
1   16  OP_BC_SEXT          [15 / 79/  4]  16 = 14 sext 20
1   17  OP_BC_SEXT          [15 / 79/  4]  17 = 13 sext 20
1   18  OP_BC_COPY          [34 /174/  4]  cp 16 -> 2
1   19  OP_BC_COPY          [34 /174/  4]  cp 17 -> 1
1   20  OP_BC_BRANCH        [17 / 85/  0]  br 15 ? bb.2 : bb.1

2   21  OP_BC_RET           [19 / 98/  3]  ret 13
------------------------------------------------------------------------

このコードには 3 つの BB セクションがあります。

最初のセクションはシンプルで、いくつかの定数の値をローカル変数にコピーしています。

0    0  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
0    1  OP_BC_COPY          [34 /174/  4]  cp 19 -> 1
0    2  OP_BC_JMP           [18 / 90/  0]  jmp bb.1

最後のセクションは ret 13 で ID 13 の変数を返却しています。

真ん中のセクションは以下の実装になっています。

br 15 ? bb.2 : bb.1 というコードが存在していることから、このブロックはループ処理を行うことがわかります。

また、ここで評価している ID 15 の変数には OP_BC_ICMP_EQ 15 = (14 == 25) の評価結果が対応しています。

ID 25 は 0x4 の定数なので、恐らく ID 14 の変数がカウンタの役割をして、4 回のループ処理が行われると予想できます。

1    3  OP_BC_COPY          [34 /174/  4]  cp 1 -> 3
1    4  OP_BC_COPY          [34 /174/  4]  cp 2 -> 4
1    5  OP_BC_TRUNC         [14 / 73/  3]  5 = 3 trunc ffffffffffffffff
1    6  OP_BC_TRUNC         [14 / 73/  3]  6 = 4 trunc ffffffffffffffff
1    7  OP_BC_SHL           [8  / 43/  3]  7 = 6 << 20
1    8  OP_BC_LSHR          [9  / 48/  3]  8 = 0 >> 7
1    9  OP_BC_AND           [11 / 58/  3]  9 = 8 & 21
1   10  OP_BC_XOR           [13 / 68/  3]  10 = 9 ^ 5
1   11  OP_BC_SHL           [8  / 43/  3]  11 = 10 << 22
1   12  OP_BC_LSHR          [9  / 48/  3]  12 = 5 >> 23
1   13  OP_BC_OR            [12 / 63/  3]  13 = 11 | 12
1   14  OP_BC_ADD           [1  /  8/  0]  14 = 6 + 24
1   15  OP_BC_ICMP_EQ       [21 /108/  3]  15 = (14 == 25)
1   16  OP_BC_SEXT          [15 / 79/  4]  16 = 14 sext 20
1   17  OP_BC_SEXT          [15 / 79/  4]  17 = 13 sext 20
1   18  OP_BC_COPY          [34 /174/  4]  cp 16 -> 2
1   19  OP_BC_COPY          [34 /174/  4]  cp 17 -> 1
1   20  OP_BC_BRANCH        [17 / 85/  0]  br 15 ? bb.2 : bb.1

ループ処理の中では、いくつかの変数を XOR やシフト演算する実装になっています。

OP_BC_TRUNCOP_BC_SEXT が少々わかりづらかったですが、おそらく TRUNC は i64 変数を i32 にコピーする際の bit の切り捨て、SEXT は i32 変数を i64 変数にコピーする際の符号拡張を意味しているだけであり、実際は単純な値のコピー処理と考えてよさそうです。

また、ポイントになるのは OP_BC_LSHR で論理右シフトされる変数 0 で、0 [ 0]: i32 argument と記載されている通りここには Func1 から受け取った 32 bit の引数が格納されます。

このような操作を C のコードに落とし込むと以下のようになりました。

uint32_t func2(uint32_t v0) {
    uint64_t v1 = 0xacab3c0; // v19 = 0xacab3c0
    uint64_t v2 = 0; // v18 = 0
    uint32_t v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14;

    for (int i = 0; i < 4; i++) {
        v3 = (uint32_t)v1;
        v4 = (uint32_t)v2;
        
        v5 = v3;
        v6 = v4;
        v7 = v6 << 3; // v20 = 0x3
        v8 = v0 >> v7;
        v9 = v8 & 0xFF; // v21 = 0xFF
        v10 = v9 ^ v5;
        v11 = v10 << 8; // v22 = 0x8
        v12 = v5 >> 24; // v23 = 0x18
        v13 = v11 | v12;

        v2 = (uint64_t)(v6 + 1); // v24 = 1
        v1 = (uint64_t)v13;
    }

    return v13;
}

どうやら、引数として受け取った 32bit の整数値を 8bit ごとに 4 等分し、各値を使用してシフト演算や論理演算を行った結果を返り値とする関数のようです。

Func1 の調査

Func2 の実装を読んだところで、Func1 のコードを見ていきます。

Func1 では非常に多くの変数と定数が定義されていますが、特に着目したのは以下の定数です。

30 [134]: 36(0x24)
31 [135]: 1939767458(0x739e80a2)
32 [136]: 4(0x4)
33 [137]: 0(0x0)
34 [138]: 4(0x4)
35 [139]: 1(0x1)
36 [140]: 984514723(0x3aae80a3)
37 [141]: 4(0x4)
38 [142]: 0(0x0)
39 [143]: 4(0x4)
40 [144]: 2(0x2)
41 [145]: 1000662943(0x3ba4e79f)
42 [146]: 4(0x4)
43 [147]: 0(0x0)
44 [148]: 4(0x4)
45 [149]: 3(0x3)
46 [150]: 2025505267(0x78bac1f3)
47 [151]: 4(0x4)
48 [152]: 0(0x0)
49 [153]: 4(0x4)
50 [154]: 4(0x4)
51 [155]: 1593426419(0x5ef9c1f3)
52 [156]: 4(0x4)
53 [157]: 0(0x0)
54 [158]: 4(0x4)
55 [159]: 5(0x5)
56 [160]: 1002040479(0x3bb9ec9f)
57 [161]: 4(0x4)
58 [162]: 0(0x0)
59 [163]: 4(0x4)
60 [164]: 6(0x6)
61 [165]: 1434878964(0x558683f4)
62 [166]: 4(0x4)
63 [167]: 0(0x0)
64 [168]: 4(0x4)
65 [169]: 7(0x7)
66 [170]: 1442502036(0x55fad594)
67 [171]: 4(0x4)
68 [172]: 0(0x0)
69 [173]: 4(0x4)
70 [174]: 8(0x8)
71 [175]: 1824513439(0x6cbfdd9f)

この中では、0x739e80a2 や 0x3aae80a3 など、合計 9 つの 32bit 整数値が定義されています。

この値は、何らかの形で Flag の検証に使用されそうです。

Func1 は 0 から 7 までの BB ブロックで構成されています。

最初のブロックのコードは以下です。

0    0  OP_BC_GEPZ          [36 /184/  4]  5 = gepz p.4 + (104)
0    1  OP_BC_GEPZ          [36 /184/  4]  7 = gepz p.6 + (105)
0    2  OP_BC_CALL_API      [33 /168/  3]  8 = seek[3] (106, 107)
0    3  OP_BC_COPY          [34 /174/  4]  cp 108 -> 2
0    4  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

OP_BC_GEPZ は bytecode_vm.c 内で以下のように定義されています。

GEPZ の GEP は恐らく GetElementPtr の略称で、LLVM の GEP 命令と同じくポインタアドレス計算を行っているものと思われます。

DEFINE_OP(OP_BC_GEPZ)
{
    int64_t ptr, iptr;
    int32_t off;
    READ32(off, inst->u.three[2]);

    // negative values checking, valid for intermediate GEP calculations
    if (off < 0) {
        cli_dbgmsg("bytecode warning: found GEP with negative offset %d!\n", off);
    }

    if (!(inst->interp_op % 5)) {
        // how do negative offsets affect pointer initialization?
        WRITE64(inst->dest, ptr_compose(stackid,
                                        inst->u.three[1] + off));
    } else {
        READ64(ptr, inst->u.three[1]);
        off += (ptr & 0x00000000ffffffffULL);
        iptr = (ptr & 0xffffffff00000000ULL) + (uint64_t)(off);
        WRITE64(inst->dest, iptr);
    }
    break;
}

LLVM についてはあまり知識がないですが、リファレンスを読むとポインタアドレスが指す値を取り出す処理のようです。

参考:The Often Misunderstood GEP Instruction — LLVM 20.0.0git documentation

次の 8 = seek[3] (106, 107) では、スキャン対象のデータの先頭から 7 バイト分をスキップしています。(ID 106 の定数には 7が、107 の定数には SEEK_SET を意味する 0 が格納されています。)

enum {
    /**set file position to specified absolute position */
    SEEK_SET = 0,
    /**set file position relative to current position */
    SEEK_CUR,
    /**set file position relative to file end*/
    SEEK_END
};

/**
\group_file
 * Changes the current file position to the specified one.
 * @sa SEEK_SET, SEEK_CUR, SEEK_END
 * @param[in] pos offset (absolute or relative depending on \p whence param)
 * @param[in] whence one of \p SEEK_SET, \p SEEK_CUR, \p SEEK_END
 * @return absolute position in file
 */
int32_t seek(int32_t pos, uint32_t whence);

参考:clamav/libclamav/bytecode_api.h at main · Cisco-Talos/clamav

すでに Logical シグネチャの設定を見た通り、今回のスキャン対象は SECCON{ というテキストを含むため、恐らくこの文字列を無視する処理と思われます。

最後の処理では定数 ID 8 の値(0) を変数 ID 2 にコピーし、BB2 にジャンプしています。

BB2 で実装されているコードは以下の通りです。

br 9 ? bb.2 : bb.3br 17 ? bb.7 : bb.1 の定義から、何らかの条件分岐によるループ処理が行われていることがわかります。

BB7 は Fail の処理のようですので、ここでは BB7 にジャンプしない分岐を特定する必要があります。

1    5  OP_BC_ICMP_ULT      [25 /129/  4]  9 = (18 < 109)
1    6  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
1    7  OP_BC_BRANCH        [17 / 85/  0]  br 9 ? bb.2 : bb.3

2    8  OP_BC_COPY          [34 /174/  4]  cp 2 -> 10
2    9  OP_BC_SHL           [8  / 44/  4]  11 = 10 << 110
2   10  OP_BC_ASHR          [10 / 54/  4]  12 = 11 >> 111
2   11  OP_BC_TRUNC         [14 / 73/  3]  13 = 12 trunc ffffffffffffffff
2   12  OP_BC_GEPZ          [36 /184/  4]  14 = gepz p.4 + (112)
2   13  OP_BC_GEP1          [35 /179/  4]  15 = gep1 p.14 + (13 * 65)
2   14  OP_BC_CALL_API      [33 /168/  3]  16 = read[1] (p.15, 113)
2   15  OP_BC_ICMP_SLT      [30 /153/  3]  17 = (16 < 114)
2   16  OP_BC_ADD           [1  /  9/  0]  18 = 10 + 115
2   17  OP_BC_COPY          [34 /174/  4]  cp 116 -> 0
2   18  OP_BC_BRANCH        [17 / 85/  0]  br 17 ? bb.7 : bb.1

分岐処理の部分を抜き出して定数を実際の値と置き換えました。

1    5  OP_BC_ICMP_ULT      [25 /129/  4]  v9 = (v18 < 0x24)
1    6  OP_BC_COPY          [34 /174/  4]  cp v18 -> v2
1    7  OP_BC_BRANCH        [17 / 85/  0]  br 9 ? bb.2 : bb.3

2    8  OP_BC_COPY          [34 /174/  4]  cp v2 -> v10

2   14  OP_BC_CALL_API      [33 /168/  3]  v16 = read[1] (p.15, 0x1)
2   15  OP_BC_ICMP_SLT      [30 /153/  3]  v17 = (v16 < 0x1)
2   16  OP_BC_ADD           [1  /  9/  0]  v18 = v10 + 0x1

2   18  OP_BC_BRANCH        [17 / 85/  0]  br v17 ? bb.7 : bb.1

これを見ると、v2 変数をカウンタとして 36(0x24) 回のループ処理が行われることがわかります。

read の呼び出しが行われていることから、恐らくスキャン対象の SEEK した位置から 1 文字ずつ読み出す処理を 36 回繰り返しているものと思われます。

p.15 が何を指しているかは不明ですが、read 関数の実装を見る限り読み出したデータの保存先のポインタを指しているようです。(p. が付与されている変数はポインタとして扱うことを意味しているのかも?)

/**
\group_file
 * Reads specified amount of bytes from the current file
 * into a buffer. Also moves current position in the file.
 * @param[in] size amount of bytes to read
 * @param[out] data pointer to buffer where data is read into
 * @return amount read.
 */
int32_t read(uint8_t* data, int32_t size);

36 文字を read してどこかに保存した後は、BB3 のブロックが呼び出されます。

ここでは、さらに 1 文字追加で read し、その文字が変数 ID 119 に格納されている 0x7d(}) に一致するかを検証しているようです。

3   19  OP_BC_CALL_API      [33 /168/  3]  19 = read[1] (p.3, 117)
3   20  OP_BC_ICMP_SGT      [27 /138/  3]  20 = (19 > 118)
3   21  OP_BC_COPY          [34 /171/  1]  cp 3 -> 21
3   22  OP_BC_ICMP_EQ       [21 /106/  1]  22 = (21 == 119)
3   23  OP_BC_AND           [11 / 55/  0]  23 = 20 & 22
3   24  OP_BC_COPY          [34 /174/  4]  cp 120 -> 0
3   25  OP_BC_BRANCH        [17 / 85/  0]  br 23 ? bb.4 : bb.7

ここまでの情報から、正しい Flag は SECCON{<36 文字の文字列>} であることがわかります。

さらに次のブロックでは、もう 1 文字分 read を行い、(おそらく)読み込みに失敗することを確認しています。

4   26  OP_BC_CALL_API      [33 /168/  3]  24 = read[1] (p.3, 121)
4   27  OP_BC_ICMP_SGT      [27 /138/  3]  25 = (24 > 122)
4   28  OP_BC_COPY          [34 /174/  4]  cp 123 -> 1
4   29  OP_BC_COPY          [34 /174/  4]  cp 124 -> 0
4   30  OP_BC_BRANCH        [17 / 85/  0]  br 25 ? bb.7 : bb.5

つまり、スキャン対象が } で終了することをチェックしているものと思われます。

BB5 のブロックでは、ID 26 の変数をカウンタとして再び何らかのループ処理が行われているようです。

ループ終了の条件分岐(OP_BC_ICMP_ULT 42 = (41 < 134))で使用されている定数 134 は 36(0x24) ですが、ループごとにカウンタに加算される定数 ID 133 は 4(0x4) なので、このループは 9 回呼び出されると思われます。

この中では、先に確認した Func2 の呼び出しも行われます。

5   31  OP_BC_COPY          [34 /174/  4]  cp 1 -> 26
5   32  OP_BC_SHL           [8  / 44/  4]  27 = 26 << 125
5   33  OP_BC_ASHR          [10 / 54/  4]  28 = 27 >> 126
5   34  OP_BC_TRUNC         [14 / 73/  3]  29 = 28 trunc ffffffffffffffff
5   35  OP_BC_GEPZ          [36 /184/  4]  30 = gepz p.4 + (127)
5   36  OP_BC_GEP1          [35 /179/  4]  31 = gep1 p.30 + (29 * 65)
5   37  OP_BC_LOAD          [39 /198/  3]  load  32 <- p.31
5   38  OP_BC_CALL_DIRECT   [32 /163/  3]  33 = call F.2 (32)
5   39  OP_BC_SHL           [8  / 44/  4]  34 = 26 << 128
5   40  OP_BC_ASHR          [10 / 54/  4]  35 = 34 >> 129
5   41  OP_BC_TRUNC         [14 / 73/  3]  36 = 35 trunc ffffffffffffffff
5   42  OP_BC_MUL           [3  / 18/  0]  37 = 130 * 131
5   43  OP_BC_GEP1          [35 /179/  4]  38 = gep1 p.7 + (37 * 65)
5   44  OP_BC_MUL           [3  / 18/  0]  39 = 132 * 36
5   45  OP_BC_GEP1          [35 /179/  4]  40 = gep1 p.38 + (39 * 65)
5   46  OP_BC_STORE         [38 /193/  3]  store 33 -> p.40
5   47  OP_BC_ADD           [1  /  9/  0]  41 = 26 + 133
5   48  OP_BC_ICMP_ULT      [25 /129/  4]  42 = (41 < 134)
5   49  OP_BC_COPY          [34 /174/  4]  cp 41 -> 1
5   50  OP_BC_BRANCH        [17 / 85/  0]  br 42 ? bb.5 : bb.6

Func2 呼び出し時の引数は ID 32 の変数ですが、ここに何が入ってくるのか全く分かりません。

しかし、遡った行にある OP_BC_GEPZ 30 = gepz p.4 + (127) で参照している p.4 は、入力文字の格納先のポインタを取得した際と同じもののようです。

そのため、問題のメタ的にも入力文字を 4文字(32bit) 分取り出した値が Func2 の引数として与えられているのではないかと予想できます。

この戻り値は、OP_BC_STORE store 33 -> p.40 の行で OP_BC_GEP1 38 = gep1 p.7 + (37 * 65) から取り出されたポインタアドレスに格納されているようです。

最後のブロックである BB6 では、この p.7 から取り出された値と冒頭で確認した 0x739e80a2 などの 9 つの整数値と順に比較を行い、すべての検証に成功した場合に 1 を返す処理を行っているようです。

{[ 省略 ]}
6  100  OP_BC_ICMP_EQ       [21 /108/  3]  92 = (91 == 170)
6  101  OP_BC_AND           [11 / 55/  0]  93 = 86 & 92
6  102  OP_BC_MUL           [3  / 18/  0]  94 = 171 * 172
6  103  OP_BC_GEP1          [35 /179/  4]  95 = gep1 p.7 + (94 * 65)
6  104  OP_BC_MUL           [3  / 18/  0]  96 = 173 * 174
6  105  OP_BC_GEP1          [35 /179/  4]  97 = gep1 p.95 + (96 * 65)
6  106  OP_BC_LOAD          [39 /198/  3]  load  98 <- p.97
6  107  OP_BC_ICMP_EQ       [21 /108/  3]  99 = (98 == 175)
6  108  OP_BC_AND           [11 / 55/  0]  100 = 93 & 99
6  109  OP_BC_SEXT          [15 / 79/  4]  101 = 100 sext 1
6  110  OP_BC_COPY          [34 /174/  4]  cp 101 -> 0
6  111  OP_BC_JMP           [18 / 90/  0]  jmp bb.7

ここまでの確認結果から、このバイトコードシグネチャでは SECCON{<36 文字>} の Flag が記録された任意のファイルをスキャンし、Flag 内の 36 文字を 4 文字ずつ 32bit の整数として取り出して Func2 による計算を行い、その結果がハードコードされた整数値と一致するかを比較していると考えられます。

Solver を作成して Flag を特定する

ここまでの確認結果を元に、Func2 の出力がハードコードされた値になる入力を特定する Solver を Z3Py で作成することを試みました。

以下のような Solver を作成してみたのですが、色々とカスタマイズしても SAT を返す値を特定することができません。(恐らく型の扱いを上手くできていないのではないかとは思うのですが原因がわかりませんでした)

from z3 import *

s = Solver()

v0 = BitVec(f"v0", 32)  # i32 argument
v1, v2 = BitVec("v1", 64), BitVec("v2", 64) # v18 = 0 v19 = 0xacab3c0

for i in range(4):
    v3 = Extract(31,0,v1)
    v4 = Extract(31,0,v2)

    v5 = v3
    v6 = v4
    v7 = v6 << 0x3  # v20 = 0x3
    v8 = v0 >> v7  # Extend v0 to 64 bits to match operations
    v9 = v8 & 0xFF  # v21 = 0xFF
    v10 = v9 ^ v5
    v11 = v10 << 0x8  # v22 = 0x8
    v12 = v5 >> 0x18  # v23 = 0x18
    v13 = v11 | v12

    v14 = v6 + 1  # v24 = 1
    v2 = v14  # v16
    v1 = v13  # v17

ans = v13
print(ans)

s.add(v1 == 0xacab3c0)
s.add(v2 == 0)
s.add(ans == 1939767458)

if s.check() == sat:
    m = s.model()
    print(m)

そこで、ctypes で実装した以下の Func2 関数を使用して総当たりで Flag を特定することにしました。

import ctypes

def func2(v0):
    v1 = ctypes.c_uint64(0xacab3c0)  # v19 = 0xacab3c0
    v2 = ctypes.c_uint64(0)  # v18 = 0
    
    v3 = ctypes.c_uint32(0)
    v4 = ctypes.c_uint32(0)
    v5 = ctypes.c_uint32(0)
    v6 = ctypes.c_uint32(0)
    v7 = ctypes.c_uint32(0)
    v8 = ctypes.c_uint32(0)
    v9 = ctypes.c_uint32(0)
    v10 = ctypes.c_uint32(0)
    v11 = ctypes.c_uint32(0)
    v12 = ctypes.c_uint32(0)
    v13 = ctypes.c_uint32(0)
    
    for i in range(4):
        v3.value = ctypes.c_uint32(v1.value & 0xFFFFFFFF).value
        v4.value = ctypes.c_uint32(v2.value & 0xFFFFFFFF).value
        
        v5.value = v3.value
        v6.value = v4.value
        
        v7.value = v6.value << 3  # v20 = 0x3
        v8.value = v0 >> v7.value
        v9.value = v8.value & 0xFF  # v21 = 0xFF
        v10.value = v9.value ^ v5.value
        v11.value = v10.value << 8  # v22 = 0x8
        v12.value = v5.value >> 24  # v23 = 0x18
        v13.value = v11.value | v12.value
        
        v2.value = ctypes.c_uint64(v6.value + 1).value  # v24 = 1
        v1.value = ctypes.c_uint64(v13.value).value
    
    return v13.value

ans = [0x739e80a2,0x3aae80a3,0x3ba4e79f,0x78bac1f3,0x5ef9c1f3,0x3bb9ec9f,0x558683f4,0x55fad594,0x6cbfdd9f]
flag = ["" for i in range(9)]
for a in range(0x21,0x7e):
    for b in range(0x21,0x7e):
        for c in range(0x21,0x7e):
            for d in range(0x21,0x7e):
                res = func2(
                    a << 24 | b << 16 | c << 8 | d
                )
                if res in ans:
                    flag[ans.index(res)] = chr(d) + chr(c) + chr(b) + chr(a)
                    print(flag)

print("SECCON{" + "".join(flag) + "}")

書き終わったあたりで ctypes ではなく普通に C で書いた方が早かったかもと思いつつ、この Solver を使用して正しい Flag を特定できました。

image-20240816215102126

この Flag を使用すると ClamAV のスキャンも突破できます。

image-20240816215138764

まとめ

ClamAV のバイトコードシグネチャについてはいつかちゃんとやろうと思いつつ 1 年くらい経ってしまっていたので無事に消化できてよかったです。