All Articles

HITCON CTF 2024 の ClamAV 問を解く

もくじ

AntiVirus(Rev)

It seems to be hard to reverse-engineer the anti-virus signature???

問題バイナリとして以下のスクリプトと cbc ファイルが与えられます。

#!/bin/sh
docker run -v /home/ctf/clamav/:/test/ --rm -it clamav/clamav clamscan --bytecode-unsigned -d/test/print_flag.cbc /test/sample.exe
ClamBCafhmlmengff|aa```c``a```|ah`cnbac`cecnb`c``biaabp`clamcoincidencejb:4096
PRINT_FLAG.{Fake,Real};Engine:56-255,Target:0;0;0:4d5a
Teddaaahdabahdacahdadahdaeahdafahdagahebheebgeebfeebaddc``daheb`eaacb`bbadb`baacb`bb`bb`baae`badahb`baaaae`badahb`daadb`aahdclhaahdb`bah
Eaeacabbbe|aebgefafdf``adbce|aecgefefkf``aebbe|amcgefdgfgifbgegcgnfafmfef``
G`am`@`bheBlbAeBbmBhoAdAcBdcAfBnoBknAfB`lB`lBioAbB`iB`lBioBjbBbmBhlBhoBbcB`iB`iBioB`iBhoBhoBioB`iBho`bgeBedBoeAfAgBdeBedAaAbBgfBegBmoBmoBnoBkoBhoBioBjoBfoBgoBdoBeoBcoB`oBaoBfgBlbAoBnbBdbBjbBibBjbBmjBebBebBfbBgbB`bBabBbbBcbBlaBmaBnaBicBiaBiaBjaBaaBdaBeaBfaBgaB`aBaaBbeBbaBldBmdBndBkdBhdBidBjdBodBddBedBfdBadB`dBadBbdBcdBlcBmcBncBicBhcBicBjcBkcBdcBecBfcBgjBacBacBbcBnmBlfBmfBnfBofBhfBifBjfBhfBdfAeBkmBgfB`fBagBbfBcfBleBmeBneBoeBhdBieBjeBkeBdeBeeBfeBgeB`eBadBbeBceBlhBmhBnhBohBhiBihBjhBkhBdhBehBfhBghB`hBahBbhBahBlgBmgBngBogBhgBigBjgBkgBdgBegBfgBgaBagBagBbgBoeBljBmjBnjBahBhjBijBjjBkjBdjBejBfjBiaBajBajBbjBekBmiBmiBniBaaBiiBiiBjiBmhBeiBeiBfiBkbBecBibBmcB`cBffBbgBigBnfBgfBggBcfBcgBobAdBlaAcBmfBnfBefBjfAjBfbBcaBeaBkbBfaBjaBfaAm@BdaBlcBnaAdAjBjaBkdBfdBndBndBkgBfeBodB`dBngB`gBffBffBneBhdBceBcnBddBomBiiBjkBljBjhBnjBokBliBdkBhkBckBlkBdkBbmBbdAoBieBmgBefBmgBofBhdBgfBkfBegBhfBhfBefB`dAbBhdBjkBnjBckBkkBgkBklBklBdmBbiBkiBhiBeoBcoBmaBaoBboBlbBmbBnbBobBhbBemBjbBkbBdbBebBfbBgbB`bBliBcmBhmBonBnnBmnAjBioBbnBanB`nBaeBdbBclBbeBiiB`eBgbBkeBmlBheBkcBlkBckBbkAbBjgBaoBikBbgBmdBlhAiBikBfhBdgBbkBklAjBmdBnlBbgBbkBabBkfBemBdmBklBigBokBfoBoiBhgBmmBliBciBbiBmjBoaBefBefBffBgfB`fBafBbfBcfBleBmeBneBgeBieBieBjeBmdBeeBeeBfeBbjB`eBaeBbeBceBlhBmhBnhBkgBhhBihBjhBkhBdhBehBfhBghB`hBahBbhBchBlgBmgBng`bfeB`eBbeBidBndBdeBoeBfdBldBadBgdBnbBbeBefBafBlf@`bfeB`eBbeBidBndBdeBoeBfdBldBadBgdBnbBfdBafBkfBef@`bad@Aa`bad@Aa`bad@Ab`bad@Ab`bad@Ac`bad@Ac`bad@Ad`bad@Ad`
A`b`bLbnab`dab`dabadab`eabad`b`b`aa`b`b`b`b`b`b`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`ah`b`d`b`d`b`b`bad`bad`ah`aa`b`d`b`b`b`d`b`b`Fbhbag
Bbadaddbbaeac@db`baeabbad@dAbd``hbad@aC``ddaaafeab`baeClhadTaaafabaa
Bb`bagabbaeAl`Aodb`d`bbAah`Tbaaf
Bb`bahabbad@d@db`baiabbabadClhadb`baj`bbabadClhadb`d`bb@haaTbaad
Baaakiab`dbjaClhahb`d`bbbjaaaTaaakadae
Bb`dalbbaaalb`damh`alB`bhb`danj`amB`bhb`baon`anbad`bbAh`abbadb`abbabb`abadbaacbbadb`aaoahbbagbbaab`dbcah`alB`bhb`dbdaj`bcaB`bhb`bbean`bdabadbfadbbaeac@dbadbgacbbadbfabeaahbhagbbgaaabiaeaahbbabhab`dbjaa`alAahb`d`bb@h`Taabiaacaf
Bb`bbkaabbaeAj`Aodb`d`bbAah`Tbaaf
Bb`dblabb`blab`bbman`blaTcab`bbmaE
Abb`bLcmbabad`b`b`b`dab`dab`dab`dab`dab`dab`dab`dab`dab`dabadabadabadabadabadabadabadaah`ah`aa`b`d`b`d`ah`b`d`b`d`b`b`bad`ah`aa`b`d`b`d`b`d`b`d`b`b`ah`b`d`b`d`b`d`aa`b`d`b`d`aa`b`d`aa`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`b`b`aa`ah`ah`ah`b`d`b`d`ah`ah`ah`b`d`b`b`b`d`b`d`b`d`aa`ah`ah`ah`b`d`b`d`ah`ah`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`b`b`b`d`b`d`b`d`b`d`ah`b`b`b`b`aa`b`d`b`d`b`d`b`d`b`d`b`b`ah`b`d`b`d`b`d`aa`b`d`b`d`aa`b`d`aa`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`aa`aa`aa`aa`b`d`b`d`aa`b`d`b`d`aa`b`d`b`d`b`b`bad`bad`ah`b`b`b`b`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`b`b`bad`bad`ah`ah`b`d`b`d`b`b`b`d`b`d`b`d`aa`ah`ah`ah`b`d`b`d`ah`ah`ah`b`d`b`d`b`b`aa`ah`ah`ah`b`d`ah`ah`ah`b`d`b`d`b`d`b`d`ah`b`b`b`b`aa`b`d`b`d`b`d`b`d`ah`ah`b`d`aa`Fcelabme
Bahbcan`aaahbdak`bcaAgaaabeaeab`baa@dTaabeableaa
Bb`dbfa`aaab`d`bb@hakTbaab
Bb`dbgabbakbgaahbhan`bgab`dbiah`bgaB`bhb`dbjaj`biaB`bhb`bbkan`bjabadblacbbad`bkaahbmagbblaaabnakaahbmaBooab`dboao`bhab`db`bo`bhab`d`bbboaajb`d`bb@haib`d`bbb`bafb`d`bb@haeTaabnaacbmb
Bb`dbabbbaibabb`dbbbbbajbbbb`bbcbn`babahbdbn`bbbb`dbebo`bcbb`dbfbi`Hl`kkkkoohbebb`dbgbk`bfbAahaabhbeab`dbgb@hTaabhbadb`a
Bb`dbibi`Hnkoookoohbebb`dbjbk`bibAahaabkbeab`dbjb@hTaabkbaeak
Bb`dblbl`bebB`ahaabmbeab`dblbBfahTaabmbafaj
BTbaag
Baabnbnab`bbcbBfadTaabnbaiah
Baabobeab`bbcbBfadTaabobbgbbhe
Baab`ceab`bbcbAfdTaab`cbebbhe
Baabaceab`bbcb@dTaabacbdbble
Bb`dbbci`Hlbkokkoohbebb`dbcck`bbcAahaabdceab`dbcc@hTaabdcalao
Bb`dbeci`Hnkookkoohbebb`dbfck`becAahaabgceab`dbfc@hTaabgcaman
Baabhceab`bbcbBbadTaabhcbibble
Baabiceab`bbcbAddTaabicbfbble
Baabjceab`bbcbAndTaabjcbhbble
Bb`dbkci`Hd`hiiiedhbebb`dblck`bkcAahaabmceab`dblc@hTaabmcbaabfa
Bb`dbnci`Hd`jiiiedhbebb`dbock`bncAahaab`deab`dboc@hTaab`dbbabea
Bb`dbadi`Hl`jkkkoohbebb`dbbdk`badAahaabcdeab`dbbd@hTaabcdbcabda
Baabddeab`bbcbAhdTaabddbjbble
Bb`dbedi`Hh``bbbjkhbebb`dbfdk`bedAahaabgdeab`dbfd@hb`dbhdo`bcbb`dbido`bdbb`d`bbbhdahb`d`bbbidagTaabgdbleblb
Baabjdeab`bbcbAidTaabjdbkbble
Bb`dbkdi`H```h`hedhbebb`dbldk`bkdAahaabmdeab`dbld@hTaabmdbgabma
Bb`dbndi`H``hiaiedhbebb`dbodk`bndAahaab`eeab`dbod@hTaab`ebhabla
BTbabia
Baabaenab`bbcbBcadTaabaebkabja
Baabbeeab`bbcbBcadTaabbebnabie
Baabceeab`bbcbAbdTaabcebnabie
Bb`dbdei`E``haahbebb`dbeek`bdeAahaabfeeab`dbee@hTaabfeblebab
Bb`dbgei`H```h`hedhbebb`dbhek`bgeAahaabieeab`dbhe@hb`dbjeo`bcbb`dbkeo`bdbb`d`bbbjeahb`d`bbbkeagTaabiebleblb
Bb`bblea`bcbAadaabmeeab`bbleBdadTaabmeb`bboa
Bahbneh`bdbAfaahboei`bdbAbaahb`fl`bneboeb`dbafo`bleb`dbbfo`b`fb`d`bbbafahb`d`bbbbfagTbablb
Bahbcfi`bdbAfaahbdfh`bdbAbaahbefl`bcfbdfb`dbffo`befb`d`bbBdahahb`d`bbbffagTbablb
Bb`bbgfa`bcbAadb`dbhf`abgfb`dbifi`E``haahbhfb`dbjfk`bifAahaabkfeab`dbjf@hTaabkfbcbbbb
Bahblfh`bdbAbaahbmfi`bdbAfaahbnfl`blfbmfb`dbofo`bgfb`db`go`bnfb`d`bbbofahb`d`bbb`gagTbablb
Bahbagi`bdbAbaahbbgh`bdbAfaahbcgl`bagbbgb`dbdgo`bgfb`dbego`bcgb`d`bbbdgahb`d`bbbegagTbablb
Bb`bbfga`bcbAadbad`bbAf`bbabadbggbbbbabggbadbhgcbbadbggbfgahbiggbbhgahbjgb`bdbbigb`dbkgo`bfgb`dblgo`bjgb`d`bbbkgahb`d`bbblgagTbablb
Bb`bbmga`bcbAadbad`bbAf`baabadbngbbbaabngbadbogcbbadbngbmgahb`hgbbogahbahm`b`hbdbb`dbbho`bmgb`dbcho`bahb`d`bbbbhahb`d`bbbchagTbablb
Bb`bbdha`bcbAadbad`bbAf`b`abadbehbbb`abehbadbfhcbbadbehbdhahbghgbbfhahbhha`bghbdbb`dbiho`bdhb`dbjho`bhhb`d`bbbihahb`d`bbbjhagTbablb
Bahbkhm`bdbbdab`dblho`bcbb`dbmho`bkhb`d`bbblhahb`d`bbbmhagTbablb
Bahbnha`bdbbdab`dboho`bcbb`db`io`bnhb`d`bbbohahb`d`bbb`iagTbablb
Bahbaib`bdbbdab`dbbio`bcbb`dbcio`baib`d`bbbbiahb`d`bbbciagTbablb
Bahbdia`bdbBooab`dbeio`bcbb`dbfio`bdib`d`bbbeiahb`d`bbbfiagTbablb
Bb`bbgia`bcbAadb`dbhio`bgib`dbiio`bdbb`d`bbbhiahb`d`bbbiiagTbablb
Bb`dbjibbagbjib`dbkibbahbkiahblin`bjib`bbmin`bkib`bbnia`bmiAadaaboiiab`bbniB`bdb`db`jo`blib`dbajo`bnib`dbbjo`blib`d`bbb`jajb`d`bbbajaib`d`bbbbjabTaaboiacbge
Bb`dbcjbbaebcjb`dbdjbbafbdjb`bbejn`bcjahbfjn`bdjb`dbgjo`bejb`dbhji`Hl`kkkkoohbgjb`dbijk`bhjAahaabjjeab`dbij@hTaabjjbnbbjc
Bb`dbkji`Hnkoookoohbgjb`dbljk`bkjAahaabmjeab`dblj@hTaabmjbobbec
Bb`dbnjl`bgjB`ahaabojeab`dbnjBfahTaabojb`cbdc
BTbabac
Baab`knab`bbejBfadTaab`kbccbbc
Baabakeab`bbejBfadTaabakbldbje
Baabbkeab`bbejAfdTaabbkbndbje
Baabckeab`bbej@dTaabckbodble
Bb`dbdki`Hlbkokkoohbgjb`dbekk`bdkAahaabfkeab`dbek@hTaabfkbfcbic
Bb`dbgki`Hnkookkoohbgjb`dbhkk`bgkAahaabikeab`dbhk@hTaabikbgcbhc
Baabjkeab`bbejBbadTaabjkbjdble
Baabkkeab`bbejAddTaabkkbmdble
Baablkeab`bbejAndTaablkbkdble
Bb`dbmki`Hd`hiiiedhbgjb`dbnkk`bmkAahaabokeab`dbnk@hTaabokbkcb`d
Bb`db`li`Hd`jiiiedhbgjb`dbalk`b`lAahaabbleab`dbal@hTaabblblcboc
Bb`dbcli`Hl`jkkkoohbgjb`dbdlk`bclAahaabeleab`dbdl@hTaabelbmcbnc
Baabfleab`bbejAhdTaabflbidble
Bb`dbgli`Hh``bbbjkhbgjb`dbhlk`bglAahaabileab`dbhl@hb`dbjlo`bejb`dbklo`bfjb`d`bbbjladb`d`bbbklacTaabilblebfe
Baablleab`bbejAidTaabllbhdble
Bb`dbmli`H```h`hedhbgjb`dbnlk`bmlAahaaboleab`dbnl@hTaabolbadbgd
Bb`db`mi`H``hiaiedhbgjb`dbamk`b`mAahaabbmeab`dbam@hTaabbmbbdbfd
BTbabcd
Baabcmnab`bbejBcadTaabcmbedbdd
Baabdmeab`bbejBcadTaabdmbcebke
Baabemeab`bbejAbdTaabembcebke
Bb`dbfmi`E``haahbgjb`dbgmk`bfmAahaabhmeab`dbgm@hTaabhmbleb`e
Bb`dbimi`H```h`hedhbgjb`dbjmk`bimAahaabkmeab`dbjm@hb`dblmo`bejb`dbmmo`bfjb`d`bbblmadb`d`bbbmmacTaabkmblebfe
Bb`bbnma`bejAadbad`bbAf`aobadbombbaobombadb`ncbbadbombnmahbangbb`nb`bbbno`banb`bbcna`bnmbbnb`dbdno`bcnb`dbeno`bfjb`d`bbbdnadb`d`bbbenacTbabfe
Bahbfna`bfjBooab`dbgno`bejb`dbhno`bfnb`d`bbbgnadb`d`bbbhnacTbabfe
Bahbinb`bfjbdab`dbjno`bejb`dbkno`binb`d`bbbjnadb`d`bbbknacTbabfe
Bahblna`bfjbdab`dbmno`bejb`dbnno`blnb`d`bbbmnadb`d`bbbnnacTbabfe
Bahbonm`bfjbdab`db`oo`bejb`dbaoo`bonb`d`bbb`oadb`d`bbbaoacTbabfe
Bb`bbboa`bejAadbad`bbAf`anbadbcobbanbcobadbdocbbadbcobboahbeogbbdoahbfoa`beobfjb`dbgoo`bbob`dbhoo`bfob`d`bbbgoadb`d`bbbhoacTbabfe
Bb`bbioa`bejAadbad`bbAf`ambadbjobbambjobadbkocbbadbjobioahblogbbkoahbmom`blobfjb`dbnoo`biob`dbooo`bmob`d`bbbnoadb`d`bbbooacTbabfe
Bb`bc``aa`bejAadbad`bbAf`albadca`abbalca`abadcb`acbbadca`ac``aahcc`agbcb`aahcd`ab`bfjcc`ab`dce`ao`c``ab`dcf`ao`cd`ab`d`bbce`aadb`d`bbcf`aacTbabfe
Bb`bcg`aa`bejAadb`dch`a`acg`ab`dci`ai`E``haahch`ab`dcj`ak`ci`aAahaack`aeab`dcj`a@hTaack`abbebae
Bahcl`ah`bfjAbaahcm`ai`bfjAfaahcn`al`cl`acm`ab`dco`ao`cg`ab`dc`aao`cn`ab`d`bbco`aadb`d`bbc`aaacTbabfe
Bahcaaai`bfjAbaahcbaah`bfjAfaahccaal`caaacbaab`dcdaao`cg`ab`dceaao`ccaab`d`bbcdaaadb`d`bbceaaacTbabfe
Bb`bcfaaa`bejAadaacgaaeab`bcfaaBdadTaacgaabdebee
Bahchaai`bfjAfaahciaah`bfjAbaahcjaal`chaaciaab`dckaao`cjaab`d`bbBdahadb`d`bbckaaacTbabfe
Bahclaah`bfjAfaahcmaai`bfjAbaahcnaal`claacmaab`dcoaao`cfaab`dc`bao`cnaab`d`bbcoaaadb`d`bbc`baacTbabfe
Bb`dcababbaccabab`dcbbabbadcbbaahccban`cabab`bcdban`cbbab`bcebaa`cdbaAadaacfbaiab`bcebaB`bdb`dcgbao`ccbab`dchbao`cebab`dcibao`ccbab`d`bbcgbaafb`d`bbchbaaeb`d`bbcibaabTaacfbabmbbge
Bb`dcjbabbabcjbaahckban`cjbaahclbam`ckbabmaah`fbclbablab`dcmbaa`bgaAahaacnbaiab`dcmbabfab`d`bbcmbaakTaacnbaabble
BTbable
BTbable
BTbable
BTbable
BTcab`bAadE

cbc ファイルは ClamAV のバイトコードシグネチャです。

バイトコードシグネチャの作成と解析方法については以下の記事でまとめています。

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

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

さすがに当然かとは思いますが、printsrc で出力可能なソースコードの情報はバイトコードシグネチャから削除されているようです。

しかし、info で出力した Logical シグネチャ部分の Hex 文字列が 4d5a(MZ) であることから、Target は任意のファイルであるものの、EXE ファイルを検出するためのバイトコードシグネチャである可能性がありそうです。

$ clambc --info print_flag.cbc
Bytecode format functionality level: 6
Bytecode metadata:
        compiler version: 0.105.0
        compiled on: (1719557581) Fri Jun 28 06:53:01 2024
        compiled by:
        target exclude: 0
        bytecode type: logical only
        bytecode functionality level: 0 - 0
        bytecode logical signature: PRINT_FLAG.{Fake,Real};Engine:56-255,Target:0;0;0:4d5a
        virusname prefix: (null)
        virusnames: 0
        bytecode triggered on: files matching logical signature
        number of functions: 2
        number of types: 25
        number of global constants: 13
        number of debug nodes: 0
        bytecode APIs used:
         read, seek, setvirusname
         
$ clambc --printsrc print_flag.cbc
# not printed

そこで、printbcir による逆アセンブル結果を解析することにします。

clambc --printbcir print_flag.cbc > dump.txt

ダンプされた逆アセンブル結果を見ると、Func0 と Func1 の 2 つの関数が定義されていました。

逆アセンブル結果は 1000 行以上あるので全文は載せませんが、まずは Func0 から解析していくことにします。

Func0 の解析

逆アセンブルした Func0 は以下の実装になっていました。

0 から 6 までの 7 つのブロックが定義されています。

------------------------------------------------------------------------
FUNCTION ID: F.0 -> NUMINSTS 40
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
0    0  OP_BC_GEPZ          [36 /184/  4]  4 = gepz p.3 + (30)
0    1  OP_BC_CALL_API      [33 /168/  3]  5 = seek[3] (31, 32)
0    2  OP_BC_MEMSET        [40 /200/  0]  0 = memset (p.4, 33, 34)
0    3  OP_BC_ICMP_EQ       [21 /108/  3]  6 = (5 == 35)
0    4  OP_BC_BRANCH        [17 / 85/  0]  br 6 ? bb.2 : bb.1

1    5  OP_BC_CALL_API      [33 /168/  3]  7 = setvirusname[4] (p.-2147483636, 36)
1    6  OP_BC_COPY          [34 /174/  4]  cp 37 -> 0
1    7  OP_BC_JMP           [18 / 90/  0]  jmp bb.6

2    8  OP_BC_CALL_API      [33 /168/  3]  8 = seek[3] (38, 39)
2    9  OP_BC_CALL_API      [33 /168/  3]  9 = read[1] (p.4, 40)
2   10  OP_BC_CALL_DIRECT   [32 /163/  3]  10 = call F.1 (4, 41)
2   11  OP_BC_COPY          [34 /174/  4]  cp 42 -> 1
2   12  OP_BC_JMP           [18 / 90/  0]  jmp bb.4

3   13  OP_BC_ICMP_ULT      [25 /129/  4]  11 = (26 < 43)
3   14  OP_BC_COPY          [34 /174/  4]  cp 26 -> 1
3   15  OP_BC_BRANCH        [17 / 85/  0]  br 11 ? bb.4 : bb.5

4   16  OP_BC_COPY          [34 /174/  4]  cp 1 -> 12
4   17  OP_BC_SHL           [8  / 44/  4]  13 = 12 << 44
4   18  OP_BC_ASHR          [10 / 54/  4]  14 = 13 >> 45
4   19  OP_BC_TRUNC         [14 / 73/  3]  15 = 14 trunc ffffffffffffffff
4   20  OP_BC_COPY          [34 /174/  4]  cp -2147483640 -> 2
4   21  OP_BC_COPY          [34 /174/  4]  cp 2 -> 16
4   22  OP_BC_GEP1          [35 /179/  4]  17 = gep1 p.16 + (15 * 65)
4   23  OP_BC_LOAD          [39 /196/  1]  load  18 <- p.17
4   24  OP_BC_SHL           [8  / 44/  4]  19 = 12 << 46
4   25  OP_BC_ASHR          [10 / 54/  4]  20 = 19 >> 47
4   26  OP_BC_TRUNC         [14 / 73/  3]  21 = 20 trunc ffffffffffffffff
4   27  OP_BC_GEPZ          [36 /184/  4]  22 = gepz p.3 + (48)
4   28  OP_BC_GEP1          [35 /179/  4]  23 = gep1 p.22 + (21 * 65)
4   29  OP_BC_LOAD          [39 /196/  1]  load  24 <- p.23
4   30  OP_BC_ICMP_EQ       [21 /106/  1]  25 = (18 == 24)
4   31  OP_BC_ADD           [1  /  9/  0]  26 = 12 + 49
4   32  OP_BC_COPY          [34 /174/  4]  cp 50 -> 0
4   33  OP_BC_BRANCH        [17 / 85/  0]  br 25 ? bb.3 : bb.6

5   34  OP_BC_CALL_API      [33 /168/  3]  27 = setvirusname[4] (p.-2147483638, 51)
5   35  OP_BC_COPY          [34 /174/  4]  cp 52 -> 0
5   36  OP_BC_JMP           [18 / 90/  0]  jmp bb.6

6   37  OP_BC_COPY          [34 /174/  4]  cp 0 -> 28
6   38  OP_BC_TRUNC         [14 / 73/  3]  29 = 28 trunc ffffffffffffffff
6   39  OP_BC_RET           [19 / 98/  3]  ret 29
------------------------------------------------------------------------

以下は、最初のブロックの変数と定数を整形したコードです。

0    0  OP_BC_GEPZ          [36 /184/  4]  v4 = gepz p.3 + (0x0)
0    1  OP_BC_CALL_API      [33 /168/  3]  v5 = seek[3] (0x0, 0x2)
0    2  OP_BC_MEMSET        [40 /200/  0]  v0 = memset(p.4, 0x0, 0x400)
0    3  OP_BC_ICMP_EQ       [21 /108/  3]  v6 = (v5 == 0x18c)
0    4  OP_BC_BRANCH        [17 / 85/  0]  br v6 ? bb.2 : bb.1

seek の第 2 引数は SEEK_END なので、ファイルの末尾までデータを読み飛ばし、そのポジションを v5 に確認しているものと思われます。

/**
\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

この v5 は後に 0x18c と比較され、一致する場合は BB2 に、一致しない場合は BB1 に飛ばされるようです。

memset は bytecodeapi.h に定義されていないようでしたが、標準ライブラリの ` void *memset(void *s, int c, sizet n);と同じく 0x400 バイト分のメモリ領域を初期化してp.4` のポインタが指すアドレスに割り当てている可能性が高そうです。

BB1 のブロックは、何かしらの検出を返した後、終了ブロックである BB6 にジャンプする実装でした。

そのため、BB1 は Fail の処理であり、ファイルの末尾の位置が 0x18c に一致する必要があることがわかります。

1    5  OP_BC_CALL_API      [33 /168/  3]  7 = setvirusname[4] (p.-2147483636, 36)
1    6  OP_BC_COPY          [34 /174/  4]  cp 37 -> 0
1    7  OP_BC_JMP           [18 / 90/  0]  jmp bb.6

以下は、BB2 のブロックの変数と定数を置き換えたものです。

最初の seek では、position 0 と SEEK_SET を引数としているので、おそらくファイルの読み取り位置を先頭に戻し、直後の read[1] (p.4, 0x18c) で 0x18c バイト分の全データを read できるようにしているものと思われます。

read されたデータは p.4 のポインタに格納され、その値は v10 = call F.1 (v4, 0x18c) で Func1 の第 1 引数に使用されているようです。

2    8  OP_BC_CALL_API      [33 /168/  3]  v8 = seek[3] (0x0, 0x0)
2    9  OP_BC_CALL_API      [33 /168/  3]  v9 = read[1] (p.4, 0x18c)
2   10  OP_BC_CALL_DIRECT   [32 /163/  3]  v10 = call F.1 (v4, 0x18c)
2   11  OP_BC_COPY          [34 /174/  4]  cp 0x0 -> v1
2   12  OP_BC_JMP           [18 / 90/  0]  jmp bb.4

最後に、cp 0x0 -> v1 で v1 に 0 を格納して BB4 にジャンプしています。

BB4 と BB3 はループ処理を実装しているので、この v1 がカウンタになりそうです。

以下は BB3 と BB4 の変数定数を置き換えたコードです。

3   13  OP_BC_ICMP_ULT      [25 /129/  4]  v11 = (v26 < 0x18c)
3   14  OP_BC_COPY          [34 /174/  4]  cp v26 -> v1
3   15  OP_BC_BRANCH        [17 / 85/  0]  br v11 ? bb.4 : bb.5

4   16  OP_BC_COPY          [34 /174/  4]  cp v1 -> v12
4   17  OP_BC_SHL           [8  / 44/  4]  v13 = v12 << 0x20
4   18  OP_BC_ASHR          [10 / 54/  4]  v14 = v13 >> 0x20
4   19  OP_BC_TRUNC         [14 / 73/  3]  v15 = v14 trunc ffffffffffffffff
4   20  OP_BC_COPY          [34 /174/  4]  cp -2147483640 -> v2
4   21  OP_BC_COPY          [34 /174/  4]  cp v2 -> v16
4   22  OP_BC_GEP1          [35 /179/  4]  v17 = gep1 p.16 + (v15 * 65)
4   23  OP_BC_LOAD          [39 /196/  1]  load  v18 <- p.17
4   24  OP_BC_SHL           [8  / 44/  4]  v19 = v12 << 0x20
4   25  OP_BC_ASHR          [10 / 54/  4]  v20 = v19 >> 0x20
4   26  OP_BC_TRUNC         [14 / 73/  3]  v21 = v20 trunc ffffffffffffffff
4   27  OP_BC_GEPZ          [36 /184/  4]  v22 = gepz p.3 + (0x0)
4   28  OP_BC_GEP1          [35 /179/  4]  v23 = gep1 p.22 + (v21 * 65)
4   29  OP_BC_LOAD          [39 /196/  1]  load  v24 <- p.23
4   30  OP_BC_ICMP_EQ       [21 /106/  1]  v25 = (v18 == v24)
4   31  OP_BC_ADD           [1  /  9/  0]  v26 = v12 + 0x1
4   32  OP_BC_COPY          [34 /174/  4]  cp 0x0 -> v0
4   33  OP_BC_BRANCH        [17 / 85/  0]  br v25 ? bb.3 : bb.6

このループはカウンタの値が 0x18c になるまで繰り返されることがわかります。

カウンタが 0x18c に到達した場合には、正しい検出を返す BB5 にジャンプできます。

5   34  OP_BC_CALL_API      [33 /168/  3]  27 = setvirusname[4] (p.-2147483638, 51)
5   35  OP_BC_COPY          [34 /174/  4]  cp 52 -> 0
5   36  OP_BC_JMP           [18 / 90/  0]  jmp bb.6

一方で、ループ処理内の v25 = (v18 == v24) の比較に失敗した場合には、カウンタを加算せずにループを途中で終了するようです。

ここで比較対象になっている v18 の方は、以下の箇所でハードコードされたポインタアドレス?から取り出された 1 バイト分の値である可能性が高そうです。

4   20  OP_BC_COPY          [34 /174/  4]  cp -2147483640 -> v2
4   21  OP_BC_COPY          [34 /174/  4]  cp v2 -> v16
4   22  OP_BC_GEP1          [35 /179/  4]  v17 = gep1 p.16 + (v15 * 65)
4   23  OP_BC_LOAD          [39 /196/  1]  load  v18 <- p.17

もう一方の v24 は p.3 から取り出したもののようですが、これは冒頭で見た通り v4 = gepz p.3 + (0x0) で使用されています。

つまり、実質 read したバイトデータを保存した後に Func1 に受け渡された p.4 のアドレスに合致するはずです。

4   27  OP_BC_GEPZ          [36 /184/  4]  v22 = gepz p.3 + (0x0)
4   28  OP_BC_GEP1          [35 /179/  4]  v23 = gep1 p.22 + (v21 * 65)
4   29  OP_BC_LOAD          [39 /196/  1]  load  v24 <- p.23
4   30  OP_BC_ICMP_EQ       [21 /106/  1]  v25 = (v18 == v24)

以上の点から、この関数では read した 0x18c のデータのポインタアドレスを Func1 に渡した後、その領域内のデータとハードコードされたポインタアドレスに存在するデータを 1 バイトずつ比較し、0x18c 回のチェックに成功した場合にファイルを検出できるシグネチャであることがわかります。

このハードコードされたデータは、あとでデバッグトレースを出力することで抽出したいと思います。

Func1 の解析

Func0 の実装を概ね把握できたので、次は Func1 を解析します。

Func1 は合計で 93 ものブロックがある大きな関数です。

すでに確認した通り、Func1 は v10 = call F.1 (v4, 0x18c) で呼び出されるため 2 つの引数を受け取っており、第 2 引数は常に 0x18c が与えられるようです。

Func0 内で Func1 に read したデータを保存したメモリ領域のアドレスが与えられていることから、この中では何らかの形で入力値が加工されている可能性があります。

この関数の冒頭部分を読んでみます。

0    0  OP_BC_TRUNC         [14 / 71/  1]  v19 = 0x18c trunc ffffffff
0    1  OP_BC_AND           [11 / 56/  1]  v20 = v19 & 0x7
0    2  OP_BC_ICMP_EQ       [21 /108/  3]  v21 = (0x18c == 0x0)
0    3  OP_BC_BRANCH        [17 / 85/  0]  br v21 ? bb.92 : bb.1

1    4  OP_BC_ZEXT          [16 / 84/  4]  v22 = 0x18c zext ffffffff
1    5  OP_BC_COPY          [34 /174/  4]  cp 0x0 -> v11
1    6  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

2    7  OP_BC_COPY          [34 /174/  4]  cp v11 -> v23
2    8  OP_BC_TRUNC         [14 / 71/  1]  v24 = v23 trunc ffffffffffffffff
2    9  OP_BC_SHL           [8  / 44/  4]  v25 = v23 << 0x20
2   10  OP_BC_ASHR          [10 / 54/  4]  v26 = v25 >> 0x20
2   11  OP_BC_TRUNC         [14 / 73/  3]  v27 = v26 trunc ffffffffffffffff
2   12  OP_BC_GEP1          [35 /179/  4]  v28 = gep1 p.0 + (v27 * 65)
2   13  OP_BC_LOAD          [39 /196/  1]  load  v29 <- p.28
2   14  OP_BC_ICMP_SGT      [27 /136/  1]  v30 = (v29 > 0xFF)
2   15  OP_BC_SEXT          [15 / 79/  4]  v31 = v24 sext v8
2   16  OP_BC_SEXT          [15 / 79/  4]  v32 = v24 sext v8
2   17  OP_BC_COPY          [34 /174/  4]  cp v31 -> v10
2   18  OP_BC_COPY          [34 /174/  4]  cp 0x0 -> v9
2   19  OP_BC_COPY          [34 /174/  4]  cp v32 -> v6
2   20  OP_BC_COPY          [34 /174/  4]  cp 0x0 -> v5
2   21  OP_BC_BRANCH        [17 / 85/  0]  br v30 ? bb.3 : bb.45

3   22  OP_BC_COPY          [34 /174/  4]  cp v9 -> v33
3   23  OP_BC_COPY          [34 /174/  4]  cp v10 -> v34
3   24  OP_BC_TRUNC         [14 / 73/  3]  v35 = v33 trunc ffffffffffffffff
3   25  OP_BC_TRUNC         [14 / 71/  1]  v36 = v34 trunc ffffffffffffffff
3   26  OP_BC_SEXT          [15 / 79/  4]  v37 = v35 sext v20
3   27  OP_BC_LSHR          [9  / 49/  4]  v38 = 0xffbbbb0c >> v37
3   28  OP_BC_AND           [11 / 59/  4]  v39 = v38 & 0x1
3   29  OP_BC_ICMP_EQ       [21 /109/  4]  v40 = (v39 == 0x0)
3   30  OP_BC_BRANCH        [17 / 85/  0]  br v40 ? bb.4 : bb.16

4   31  OP_BC_LSHR          [9  / 49/  4]  v41 = 0xffbfffbe >> v37
4   32  OP_BC_AND           [11 / 59/  4]  v42 = v41 & 0x1
4   33  OP_BC_ICMP_EQ       [21 /109/  4]  v43 = (v42 == 0x0)
4   34  OP_BC_BRANCH        [17 / 85/  0]  br v43 ? bb.5 : bb.11

5   35  OP_BC_OR            [12 / 64/  4]  v44 = v37 | 0x10
5   36  OP_BC_ICMP_EQ       [21 /109/  4]  v45 = (v44 == 0x16)
5   37  OP_BC_BRANCH        [17 / 85/  0]  br v45 ? bb.6 : bb.10

6   38  OP_BC_JMP           [18 / 90/  0]  jmp bb.7

このコードを読むと、入力値とハードコードされたデータを組み合わせて計算を行い、条件に応じて様々なブロックにジャンプする構成になっています。

ブロックの数自体が 93 もある点や入力パターンが最大で 0x18c ある点から、静的解析で入力値を逆算するのは諦めました。

Func0 の確認結果から、read したデータが保存されているメモリ領域の値は、最終的にシステムが用意している何らかのバイト値と一致しているか否かを検証しています。

つまり、入力バイトを総当たりすることで最終的な結果を求めることができそうです。

Func1 の入出力をトレースする

ここからは入力バイトの総当たりで正しい Flag を抽出することを目指します。

バイトコードシグネチャのデバッグトレースを行うために、まずは MZ から始まる 0x18c バイトのファイルを以下のコードで生成します。

data = b"MZ" + b"\x00" + b"\x41"*(0x18c-3)

with open("print_flag.exe","wb") as f:
    f.write(data)

その後、以下の記事と同じ手順で libclamav のコードを変更したものをビルドし、clamscan --bytecode-unsigned -d print_flag.cbc print_flag.exe コマンドでスキャンを行います。

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

これでバイトコードシグネチャを使用してスキャンを行っている間のデバッグトレースを参照します。

Func1 の実行が完了する前までのコードは読む必要が無いので、OP_BC_RET を探します。

すると、OP_BC_RET が呼び出される 3 箇所のうち、以下の箇所が Func1 の RET であることを特定できます。

image-20240819212556848

先ほど確認した v25 = (v18 == v24) によるループ処理内の比較は Func1 の終了後から呼び出されるため、このコード以降に存在する OP_BC_ICMP_EQ の前後のトレースを抽出します。

すると、以下のように 1 つずつ値を比較していき、比較に失敗したらそれ以降の比較が行われなくなるコードが存在していることがわかりました。

image-20240819212658714

上記からこの 1185 = (1136 == 1184)v25 = (v18 == v24) によるループ処理内の比較と対応していると予想できます。

いくらかの試行錯誤の後、以下のスクリプトで MZ を除いた先頭から 1 バイトずつの値を総当たりで特定しつつ、print_flag.exe を更新していくコードを作成しました。

import subprocess

data = [0 for i in range(0x18c)]
data[0] = ord("M")
data[1] = ord("Z")
target = 2

def bruteforce(target):
    global data

    for n in range(0x0,0xFF+1):
        data[target] = n
        ba = bytes(data)

        with open("print_flag.exe","wb") as f:
            f.write(ba)

        # clamscan --bytecode-unsigned -d print_flag.cbc print_flag.exe
        command = ["clamscan", "--bytecode-unsigned", "-d", "print_flag.cbc", "print_flag.exe"]
        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        outputs = result.stdout.decode("utf-8").splitlines()

        is_find = False
        counter = 0
        for i,line in enumerate(outputs):
            if "1185 = (1136 == 1184)" in line:
                r = int(outputs[i+3][-1])
                if r == 1:
                    if counter == target:
                        is_find = True
                        print(hex(n), line, counter, target)
                        return n
                    counter += 1
                else:
                    break
    return -1

for x in range(0x18c-2):
    r = bruteforce(target)
    if r >= 0:
        target += 1
    else:
        exit()

これを実行すると、しばらく時間がかかりますが 1 バイトずつ正しい値が特定されていき、最終的に Flag を取得するための EXE ファイルを生成できます。

image-20240820205414624

なお、最悪計算量は 0xFF*0x18c ですが、毎回 EXE ファイルの生成と clamscan の実行を行い、出力されたトレースの解析が必要なために総当たりにかなりの時間がかかります。

Func1 によるバイト値の変換の法則性を見抜ければよさそうですが、詳しく Func1 の実装を読み解くよりは総当たりの実行を待つ方が早そうでしたのでこのまま実行しています。

総当たりの完了まで数時間ほど待機すると、すべての検証にパスする EXE ファイルを特定できます。

これを実行すると、正しい Flag を取得することができました。

image-20240821204345132

まとめ

結構 Solver の少ない問題でしたが、ClamAV のバイトコードシグネチャと libclamav のデバッグトレースについて勉強した後だと非常にスムーズに解くことができました。