こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

DEF CON CTF Qualifier 2021 writeup - threefactooorx

ここ最近、CTFへの参加ができておらず、久しぶりのWriteup投稿。

DEF CON CTFはCTF界のお祭りであるし、GW期間で時間が少し取れたので参加することにした。
とは言え、pwn問とrev問が多数を占めておりWeb問は1問のみ。 何とかその1問は解いたことで責務を果たした。

問題

This is the end of phishing. The Order of the Overflow is introducing the ultimate authentication factor, the most important one, the final one. To help the web transition to this new era of security, we are introducing a 3FA tool for testing your webpages completely isolated on our admin's browser.

http://threefactooorx.challenges.ooo:4017

f:id:graneed:20210503065624p:plain

添付ファイル:3factooorx.crx

Writeup

問題ページの確認

here!のリンクをクリックすると、HTMLファイルをアップロードできる画面に遷移。
f:id:graneed:20210503065704p:plain

適当に、以下のテスト用のHTMLファイルを作ってアップロードしてみる。

<html>
<head>
</head>
<body>
test
</body>
</html>

アップロード直後、以下の画面が表示された。
f:id:graneed:20210503065837p:plain

しばらくすると、adminがアップロードしたHTMLを開いたであろう画像が表示された。背景が緑になっているが、特にHTMLファイルで背景の色は指定していない。 f:id:graneed:20210503065946p:plain

なるほど、わからん。

添付ファイルの確認

次に、添付ファイルのcrxファイルを確認する。 crxファイルはChrome拡張機能のファイルである。 zipで展開できるので、中身のファイルを確認していく。 主となる機能は以下の2ファイルに実装されている。

background_script.js

// Put all the javascript code here, that you want to execute in background.
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.getflag == "true")
      sendResponse({flag: "OOO{}"});
  }
);

content_script.js

const OOO_0x5be3=['zxHJzxb0Aw9U','wxvJveW','ufLcwvu','zxHmAwW','nwztzLH1zW','suTTvvi','u1rLENa','venkzeS','tu5WAK8','yxbWzw5Kq2HPBa','Dg9YqwXS','tg9HzgvK','B29VCG','mJmWmtDou2Thvxm','wffquvC','DNPsDMm','yM9KEq','z2v0zMXHzW','uvHXswi','DhLvuhy','DLDuuwu','ExL6u1K','nti3mZq2Dg10wKns','D21Py1u','vvrHB0q','CgfYzw50tM9Kzq','EwrOqNC','y29UC29Szq','yxbWBhK','n0TyvuDIvG','CMv0DxjUicHMDq','zMXHzZOG','y29UC3rYDwn0BW','BNvyuxK','ugzQC3a','DhLWzq','Dg9tDhjPBMC','vg9OseO','EvjKD3y','i3rOAxjKzMfJDa','y2HPBgrmAxn0','y3rVCIGICMv0Dq','sgvSBg8GzNjVBq','AePgANC','xIbDFq','Bg9N','AhHRExK','vufnuhG','DgfNtMfTzq','q0Tkvge','yxr0CMLIDxrLCW','EwPxv2q','zM9nvuK','DgHPCMrMywn0BW','zMrtEee','vhDyuw8','sMHJzwS','u3HNt0i','EwrZBeW','C1L1z28','nJK2mJe2BMP6rvHn','mxfeExHPCa','mJGYndK3rMHnuvbH','ywrKzwroB2rLCW','AxDJugq','Dhj1zq','re9nq29UDgvUDa','z3jLzw4','veLrDKy','uufpBwy','rMzLtKO','y3vUwxe','CMvTB3zLze5Vza','zxiGAxmGB2jZzq','txnytfG','tvjVz2i','x19WCM90B19F','ywTUEu4','t2DZtuO','uxPjCNC','zKXRBvu','uuHTvgi','sKD4sLy','y0rOy1m','zw50','nZq4nZz3z2zkrLy','B1jkAeq','y3jLyxrLrwXLBq','DgvKoIa','B2jZzxj2zq','Aw5MBW','uMToB0q','qwvYv24','tuHTqMq','yxr0CMLIDxrLtG','yxjxAMW','rLbRruC','CxvLCNLtzwXLyW','zK5Vt2y','CM4GDgHPCYiPka','sw9gtLu','zdOG','z2v0rwXLBwvUDa','m2zH','se1LBLK','uxjjtMO','zxjYB3i','E30Uy29UC3rYDq','s29pwKm','twPzEve','CNrvvee','C2vUze1LC3nHzW','BgvUz3rO','DevlBvy','qNLjza','q29SB3i','Ee9ZDvq','xIHBxIbDkYGGkW','ChjVDg90ExbL','y2HHCNmGywrKzq','w3rVC3q9iJeIxq','DgfYz2v0','ExPKrhG','DNnYqvC','ze1vq0C','ExPyBu8','q2j1u2S','zgL2','Dwvkwue','s1nOC0C','AgD2s0i','BMn0Aw9UkcKG','Aw5Uzxjive1m','zMXHzW','wgjWvuO','CNvUDgLTzq','ENbZwLK','ywrKrxzLBNrmAq','B0PIB1y','ie9ptW','uxDhzMG','yLbSq2S','DhjHy2u','C2v0qxr0CMLIDq','CNzPBMCU','nJy5nJy2uu9rEgrz','yMLUza','yw1L','rhjVyMm','rLnVzfe','wwLAwNq','B29Y','yNbWu0i','qxr0CMLIDxrLia','wwH4t1m','DMfSDwu','mJG0ntzusMLsDLm','u1DMD1G','rg1uwe0','C3r5Bgu','ALbYBNK','Dg9Y','CwLLs20','su5qvvq','u3n0D04','s3njwxm'];function OOO_0x1e05(0x5b41c2,0x49f0ea){0x5b41c2=0x5b41c2-(-0xca-0x1a+-0x1a6f+0x70a);let 0x21ce5c=OOO_0x5be3[0x5b41c2];if(OOO_0x1e05['EgEdZZ']===undefined){var 0x4805d2=function(0x4d3468){const 0x241d4='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let 0x2d6d6b='';for(let _0x33c38d=0x1dbf0x1+-0x1d-0xf9+-0x39f4,0x42ef4e,0x12b97b,0x310f68=-0x2427+-0x85+0x24ac;0x12b97b=0x4d3468'charAt';~0x12b97b&&(0x42ef4e=0x33c38d%(-0x2f6+-0xfdd0x2+0x115a0x2)?_0x42ef4e(-0x371+-0x1b1c+-0x629-0x5)+0x12b97b:0x12b97b,_0x33c38d++%(-0x1-0xdd8+0x13e0x13+-0x10x256e))?0x2d6d6b+=String'fromCharCode'*0x33c38d&0x2db-0xd+0x804+0x1d21)):-0x5d5+-0x10c-0x20+-0x1bab){0x12b97b=0x241d4'indexOf';}return 0x2d6d6b;};OOO_0x1e05['twlaRA']=function(0x2c5f97){const 0x146068=0x4805d2(0x2c5f97);let 0x2b05c7=[]; (snip)

content_script.jsは難読化されている。

まずは使ってみることにする。
Chromeを起動して以下のURLにアクセスする。

chrome://extensions/

f:id:graneed:20210503070837p:plain
「パッケージ化されていない拡張機能を読み込む」のボタンを押下し、先ほどcrxファイルを展開したディレクトリを指定する。

すると、正常にChrome拡張機能として認識された。
f:id:graneed:20210503070719p:plain

拡張機能が有効な状態で問題サイトにアクセスると、背景が緑色になった。 f:id:graneed:20210503071333p:plain

まだ、どうすればフラグが取れるのか、どこにフラグがあるのかがさっぱりわからないため、腰を据えて難読化されたコードを読んでいくことにする。

コード解析

実際にChrome拡張機能を動かしながら変数に設定されている内容などを確認してコード内容の解析を進めつつ、コードの変数名などを変更しながら解析を進めるのが効率的である。crxファイルを展開したディレクトリ配下のファイルを更新した後は「更新」ボタンを押下すると反映される。

なお、最初にcontent_script.js全体にフォーマット(整形)をかけてからChromeで表示確認すると、しばらくフリーズした後にOutOfMemoryが発生してしまった。原因がよくわからないが、全体一気にフォーマットをかけず、解析しながら逐次フォーマットをかけていくことにした。

全体を眺めると、先頭のOOO_0x5be3配列内のランダム文字列用のような要素をもとに、いくつかの関数を組み合わせて文字列の変換処理(よく読んでいないが、シフトしたり置き換えたり切り出したりだと思われる)をして、有効なJavaScriptの変数や関数の文字列を生成している。この変換処理を追うのは面倒なので、コンソールで実行して、変換後の文字列を把握していくことにする。

コードの後半部分を例にする。
以下はフォーマットをかけたコードである。_0x5e7e08関数や_0x5e48be関数が多く使用されていることがわかる。

(snip)
setTimeout(function() {
    const _0x5e7e08 = function(_0x36bfd4, _0x49f224, _0x1aad0d, _0x9f80a3) {
        return OOO_0x3fd47c(_0x36bfd4, _0x1aad0d - 0x360, _0x1aad0d - 0x84, _0x9f80a3 - 0x139);
    }
      , _0x5e48be = function(_0x44622e, _0x4e1ee1, _0x481182, _0x3003e2) {
        return OOO_0x3fd47c(_0x44622e, _0x481182 - 0x360, _0x481182 - 0x25, _0x3003e2 - 0xdf);
    }
      , _0x5ebd2a = {};
    _0x5ebd2a[_0x5e7e08(0x21b, 0x26c, 0x239, 0x26f)] = function(_0xd7fd19, _0x3dc26b) {
        return _0xd7fd19 + _0x3dc26b;
    }
    ,
    _0x5ebd2a[_0x5e48be(0x1e1, 0x17f, 0x1aa, 0x1cb)] = _0x5e7e08(0x1f1, 0x1dc, 0x1d7, 0x1ab),
    _0x5ebd2a[_0x5e48be(0x210, 0x1de, 0x1de, 0x1d5)] = function(_0x3daa6a, _0x29e0c7) {
        return _0x3daa6a == _0x29e0c7;
    }
    ,
    _0x5ebd2a['DvvtZ'] = function(_0x43093f, _0x1a0c40) {
        return _0x43093f == _0x1a0c40;
    }
    ,
    _0x5ebd2a[_0x5e48be(0x1b9, 0x1c1, 0x1f1, 0x1ce)] = _0x5e7e08(0x279, 0x232, 0x237, 0x215),
    _0x5ebd2a[_0x5e7e08(0x1ca, 0x1e5, 0x1e6, 0x1b4)] = 'processed',
    _0x5ebd2a[_0x5e7e08(0x27b, 0x23d, 0x22c, 0x24e)] = _0x5e7e08(0x229, 0x1a9, 0x1f9, 0x227);
    const _0x10b2d5 = _0x5ebd2a
      , _0xd26915 = {};
    _0xd26915[_0x5e7e08(0x1c9, 0x1d1, 0x1c9, 0x215)] = _0x10b2d5[_0x5e48be(0x21d, 0x219, 0x22c, 0x25d)],
    chrome[_0x5e48be(0x281, 0x24c, 0x23f, 0x228)][_0x5e48be(0x1fd, 0x25f, 0x227, 0x1f2) + 'e'](_0xd26915, function(_0x336e82) {
        const _0x39523f = function(_0x1f7238, _0x1ec865, _0x491b86, _0x4c5d70) {
            return _0x5e7e08(_0x4c5d70, _0x1ec865 - 0x152, _0x491b86 - -0x35c, _0x4c5d70 - 0x192);
        }
          , _0x5773c7 = function(_0x2e0483, _0x4c6386, _0xfa0575, _0xa5f600) {
            return _0x5e7e08(_0xa5f600, _0x4c6386 - 0x9a, _0xfa0575 - -0x35c, _0xa5f600 - 0x1ab);
        };
        FLAG = _0x336e82[_0x39523f(-0x10d, -0xe8, -0x11f, -0x128)],
        console['log'](_0x10b2d5[_0x39523f(-0x134, -0xf8, -0x123, -0x14b)](_0x10b2d5[_0x5773c7(-0x1c8, -0x164, -0x1b2, -0x16c)], _0x336e82[_0x39523f(-0x151, -0x152, -0x11f, -0x146)]));
        nodesadded == 0x1 * -0x27a + 0x3 * -0x3f8 + -0x4cd * -0x3 && _0x10b2d5[_0x39523f(-0x157, -0x1ae, -0x17e, -0x1c2)](nodesdeleted, -0x1b66 + -0x14e * 0x8 + 0x25d9) && attrcharsadded == -0x2001 + -0x2 * 0x433 + 0x49 * 0x8e && _0x10b2d5['DvvtZ'](domvalue, -0xed7 * -0x1 + -0x18f0 + 0x12a5) && (document['getElement' + _0x39523f(-0xf2, -0x127, -0x132, -0x153)](_0x5773c7(-0x141, -0x1ab, -0x16f, -0x192) + _0x5773c7(-0x131, -0x15d, -0x10d, -0xc2))['value'] = _0x336e82[_0x5773c7(-0xe7, -0xda, -0x11f, -0x111)]);
        const _0x369bcb = document[_0x39523f(-0xfc, -0x141, -0x14d, -0x186) + 'ent'](_0x10b2d5[_0x39523f(-0x121, -0x14a, -0x16b, -0x199)]);
        _0x369bcb[_0x5773c7(-0x158, -0xd6, -0x115, -0xeb) + 'te']('id', _0x10b2d5['hxkyy']),
        document[_0x39523f(-0x19a, -0x187, -0x194, -0x17d)][_0x39523f(-0x18c, -0x15b, -0x19b, -0x15b) + 'd'](_0x369bcb);
    });
}, -0x2 * -0xc41 + -0x2443 * -0x1 + -0xef * 0x3f);

Chromeで途中でブレイクポイントを設定して、下のコンソール画面で関数を実行すると、変換後の文字列がわかる。 f:id:graneed:20210503073230p:plain

あとは地道に変換していく。
他にも、変数名を自分でわかりやすくしたり、コメントでメモしながら読み進める。

setTimeout(function() {
    const _0x5e7e08 = function(_0x36bfd4, _0x49f224, _0x1aad0d, _0x9f80a3) {
        return OOO_0x3fd47c(_0x36bfd4, _0x1aad0d - 0x360, _0x1aad0d - 0x84, _0x9f80a3 - 0x139);
    }
      , _0x5e48be = function(_0x44622e, _0x4e1ee1, _0x481182, _0x3003e2) {
        return OOO_0x3fd47c(_0x44622e, _0x481182 - 0x360, _0x481182 - 0x25, _0x3003e2 - 0xdf);
    }
      , data1 = {};
    data1['func_plus'] = function(_0xd7fd19, _0x3dc26b) {
        return _0xd7fd19 + _0x3dc26b;
    }
    ,
    data1['bppSB'] = 'flag: ',
    data1['func_equals'] = function(_0x3daa6a, _0x29e0c7) {
        return _0x3daa6a == _0x29e0c7;
    }
    ,
    data1['func_equals2'] = function(_0x43093f, _0x1a0c40) {
        return _0x43093f == _0x1a0c40;
    }
    ,
    data1['SxgOB'] = 'div',
    data1['hxkyy'] = 'processed',
    data1['xOsuT'] = 'true';
    const data2 = data1
      , data3 = {};
    data3['getflag'] = data2['xOsuT'], // true
    chrome['runtime']['sendMessage'](data3, function(_0x336e82) { // _0x336e82 = {flag: "OOO{}"
        const _0x39523f = function(_0x1f7238, _0x1ec865, _0x491b86, _0x4c5d70) {
            return _0x5e7e08(_0x4c5d70, _0x1ec865 - 0x152, _0x491b86 - -0x35c, _0x4c5d70 - 0x192); // return "flag"
        }
          , _0x5773c7 = function(_0x2e0483, _0x4c6386, _0xfa0575, _0xa5f600) {
            return _0x5e7e08(_0xa5f600, _0x4c6386 - 0x9a, _0xfa0575 - -0x35c, _0xa5f600 - 0x1ab); // return "bppSB"
        };
        FLAG = _0x336e82['flag'],
        console['log'](data2['func_plus'](data2['bppSB'], _0x336e82['flag'])); // "flag: OOO{}"
        nodesadded == 5 &&
          data2['func_equals'](nodesdeleted, 3) &&
          attrcharsadded == 23 &&
          data2['func_equals2'](domvalue, 2188) && 
          (document['getElementById']('thirdfactooor')['value'] = _0x336e82['flag']);
        const _0x369bcb = document['createElement'](data2['SxgOB']);
        _0x369bcb['setAttribute']('id', data2['hxkyy']),
        document['body']['appendChild'](_0x369bcb);
    });
}, 500);

ポイントは下から10行目あたり。拡張機能を有効にした状態で、以下の条件を全て満たすHTML画面を表示すると、idがthirdfactooorのHTML要素にflag文字列をセットするようだ。

  • nodesadded5
  • nodesdeleted3
  • attrcharsadded23
  • domvalue2188

これら変数がどのようにセットされるか確認するため、解析作業をさらに進める。

let nodesadded=0,nodesdeleted=0,attrcharsadded=0;
const OOO_0x2a2a96={};
OOO_0x2a2a96['attributes']=true,OOO_0x2a2a96['childList']=true,OOO_0x2a2a96['subtree']=true;
const config = OOO_0x2a2a96
  , callback = function(_0x3bfa58, _0x473b60) {
    const _0x3b1336 = function(_0x2b0d18, _0x1f44dd, _0x266f3f, _0x30da66) {
        return OOO_0x3e535c(_0x2b0d18, _0x266f3f - 0xbf, _0x266f3f - 0x1f0, _0x30da66 - 0x15);
    }
      , _0x50e5cb = function(_0x17a99c, _0x93b86b, _0x162dd1, _0x23a6d3) {
        return OOO_0x3fd47c(_0x17a99c, _0x162dd1 - 0xbf, _0x162dd1 - 0x1d2, _0x23a6d3 - 0x14f);
    }
      , _0x1f7375 = {};
    _0x1f7375['CbuSk'] = function(_0x43d068, _0x30a174) {
        return _0x43d068(_0x30a174);
    }
    ,
    _0x1f7375['rYDJT'] = function(_0x14246c, _0x74b35d) {
        return _0x14246c + _0x74b35d;
    }
    ,
    _0x1f7375['exLil'] = "return (function() ",
    _0x1f7375['fdSxA'] = "{}.constructor(\"return this\")( )",
    _0x1f7375['UTaoD'] = function(_0x2e864f, _0x11077e) {
        return _0x2e864f === _0x11077e;
    }
    ,
    _0x1f7375['STezp'] = '3fa',
    _0x1f7375['Pfjsp'] = function(_0x3bd23a, _0x3cdd5a) {
        return _0x3bd23a !== _0x3cdd5a;
    }
    ,
    _0x1f7375['yzXmO'] = 'tEKmV',
    _0x1f7375['foMUI'] = function(_0x495d1b, _0x3d1d64) {
        return _0x495d1b === _0x3d1d64;
    }
    ,
    _0x1f7375['MjYyQ'] = 'childList',
    _0x1f7375['UAMPx'] = 'ohliS',
    _0x1f7375['qieKm'] = function(_0x2c672f, _0x1daaf7) {
        return _0x2c672f === _0x1daaf7;
    }
    ,
    _0x1f7375['yyzSY'] = 'attributes',
    _0x1f7375['MRogb'] = 'QHmTb',
    _0x1f7375['ydslL'] = "Nodes deleted: ",
    _0x1f7375['TohHJ'] = function(_0x50b4fd, _0x2f2f3a) {
        return _0x50b4fd + _0x2f2f3a;
    }
    ;
    const _0x55a6f1 = _0x1f7375;
    for (const _0x8a010b of _0x3bfa58) {
        var _0x5b12b9 = document['getElementById']('3fa');
        if (!_0x5b12b9) {
            if (_0x55a6f1['Pfjsp']('tEKmV', _0x55a6f1['yzXmO'])) { // 常にfalse
                function _0x4c234d() {
                    const _0x21ada6 = function(_0x13829f, _0x5aff45, _0x1049e9, _0x1fc823) {
                        return _0x3b1336(_0x5aff45, _0x5aff45 - 0x1ab, _0x1fc823 - 0x69, _0x1fc823 - 0x137);
                    }
                      , _0x1fbfda = function(_0x513222, _0x3868ec, _0x373041, _0x1b0c78) {
                        return _0x50e5cb(_0x3868ec, _0x3868ec - 0x1eb, _0x1b0c78 - 0x69, _0x1b0c78 - 0x63);
                    }
                      , _0x6715fd = YxHqhw[_0x21ada6(0x4, 0x0, 0x2a, -0x2)](_0x4805d2, YxHqhw['rYDJT'](YxHqhw[_0x21ada6(-0x33, -0x77, -0x5c, -0x7d)] + YxHqhw[_0x1fbfda(0x8, -0x60, -0x44, -0x4a)], ');'));
                    _0x2a1d4f = _0x6715fd();
                }
            } else
                return;
        } else {
            if (_0x55a6f1['foMUI'](_0x8a010b['target'], _0x5b12b9) || _0x8a010b['target']['parentNode'] === _0x5b12b9 || _0x8a010b['target']['parentNode']['parentNode'] === _0x5b12b9) {} else
                return;
        }
        if (_0x8a010b['type'] === 'childList') {
            if (_0x55a6f1['Pfjsp'](_0x55a6f1['UAMPx'], _0x55a6f1['UAMPx'])) { // 常にfalse
                function _0x2cef4c() {
                    if (_0x29dfcf) {
                        const _0x393cac = _0x3997ca['apply'](_0x25082f, arguments);
                        return _0x3d93fd = null,
                        _0x393cac;
                    }
                }
            } else
                nodesadded += _0x8a010b['addedNodes']['length'],
                nodesdeleted += _0x8a010b['removedNodes']['length'];
        } else {
            if (_0x55a6f1['qieKm'](_0x8a010b['type'], _0x55a6f1['yyzSY'])) {
                if (_0x55a6f1['qieKm'](_0x55a6f1['MRogb'], _0x55a6f1['MRogb']))
                    attrcharsadded += _0x8a010b['attributeName']['length'];
                else {
                    function _0x2d4b10() {
                        const _0x5a2574 = function(_0x1f61f0, _0x4c614f, _0x253be8, _0x30331c) {
                            return _0x3b1336(_0x30331c, _0x4c614f - 0xfd, _0x1f61f0 - 0xe2, _0x30331c - 0x1a9);
                        }
                          , _0x1db321 = function(_0x1e9565, _0x300054, _0x57104f, _0x115259) {
                            return _0x50e5cb(_0x115259, _0x300054 - 0x3a, _0x1e9565 - 0xe2, _0x115259 - 0x73);
                        };
                        if (_0x55a6f1[_0x5a2574(0x11, 0x4c, -0x3a, 0x57)](_0x5ac0e8[_0x1db321(0x72, 0x8e, 0xbc, 0x36)], _0x3ee0cd) || _0x55a6f1[_0x1db321(0x11, -0x18, -0x3, 0x39)](_0x336fd7[_0x1db321(0x72, 0x90, 0x50, 0xb7)][_0x5a2574(0x12, 0x21, -0xd, -0x15)], _0x4a3300) || _0x55a6f1[_0x1db321(0x11, 0x9, 0x15, -0x27)](_0x2ae526['target'][_0x1db321(0x12, 0x49, -0x39, -0x1e)][_0x1db321(0x12, 0x3f, -0x36, 0x41)], _0x430442)) {} else
                            return;
                    }
                }
            }
        }
    }
    console['log'](_0x55a6f1['rYDJT']("Nodes added: ", nodesadded)),
    console['log'](_0x55a6f1['ydslL'] + nodesdeleted),
    console['log'](_0x55a6f1['TohHJ']("Attribute chars added: ", attrcharsadded));
}
  , observer = new MutationObserver(callback);
observer['observe'](document, config),
console['log']('The observer is observing.'),

上記のコード内で、特定の条件を満たした場合にnodesaddednodesdeleted、またはattrcharsadded変数への加算処理を実行している。 MutationObserverというAPIを利用してDOMを監視しており、上記のコードはノードの追加、削除、変更が発生した際に動作するようだ。 developer.mozilla.org

変数の加算処理を通るための条件は以下のとおり。

  • nodesadded変数: idが3faであるノードの配下に、新規にノードを追加すると加算される。
  • nodesadded変数: idが3faであるノードの配下から、ノードを削除すると加算される。
  • attrcharsadded変数: idが3faであるノードの配下のノードに属性を追加すると、その属性名の長さだけ加算される。

これまで解析した結果をもとに、条件を満たすHTMLを作成する。idが3faのノードの配下に5つノードを追加した後、3つノードを削除し、名前が23文字の属性を追加する。

<html>
<head>
<script>
function init(){
  let p = document.getElementById('3fa');
  var c1 = document.createElement('div');
  p.append(c1);
  var c2 = document.createElement('div');
  p.append(c2);
  var c3 = document.createElement('div');
  p.append(c3);
  var c4 = document.createElement('div');
  p.append(c4);
  var c5 = document.createElement('div');
  p.append(c5);
  p.removeChild(c1);
  p.removeChild(c2);
  p.removeChild(c3);
  c4.setAttribute("AAAAAAAAAAAAAAAAAAAAAAA","hogehoge");
}
</script>
</head>
<body onload="">
<textarea id="thirdfactooor" rows="4" cols="40"></textarea>
<div id="3fa"></div>
<script>init();</script>
</body>
</html>

HTMLを表示して、条件判定の部分にブレイクポイントを設定して確認する。
f:id:graneed:20210503080431p:plain
右側のWatch欄を見ると、3つの変数が条件を満たしていることがわかる。 残りのdomvalue変数は、別の場所で変数操作をしていたが、適当に属性の値を変更したところ数値が変わったので、特に真面目にコードを読まずに調整した。
最終的に以下のHTMLファイルになった。

<html>
<head>
<script>
function init(){
  let p = document.getElementById('3fa');
  var c1 = document.createElement('div');
  p.append(c1);
  var c2 = document.createElement('div');
  p.append(c2);
  var c3 = document.createElement('div');
  p.append(c3);
  var c4 = document.createElement('div');
  p.append(c4);
  var c5 = document.createElement('div');
  p.append(c5);
  p.removeChild(c1);
  p.removeChild(c2);
  p.removeChild(c3);
  c4.setAttribute("AAAAAAAAAAAAAAAAAAAAAAA","1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901");
}
</script>
</head>
<body onload="">
<textarea id="thirdfactooor" rows="4" cols="40"></textarea>
<div id="3fa"></div>
<script>init();</script>
</body>
</html>

全ての変数が条件を満たしていることを確認できた。
f:id:graneed:20210503080750p:plain

そして、thirdfactooorにフラグの値がセットされた。これは自分の端末で実行したためフラグ文字列のprefix/suffixしかないが、このHTMLファイルをアップロードすればよいはずだ。
f:id:graneed:20210503080933p:plain

早速、アップロードすると、フラグがセットされた画像が表示された。 f:id:graneed:20210503081121p:plain

フラグゲット。

OOO{themorefactorsthebetter_butyouneedatleastthree}

Canarytokensサービスを試してみた

ある調べものをしていたところ、Canarytokensというサービスがあることを知った。

canarytokens.org

Canarytokensはカナリアトークンを生成するサービスであり、無料で利用できるほか、githubソースコードやDockerfileが公開されているため自分でサービス稼働環境を構築できる。カナリアトークンとはターゲット(被害者)がWebページ、電子メール、ファイルなどのリソースにアクセスする操作をしたときに、そのアクセスした事実やターゲットの環境情報(IPアドレス、UserAgent、ユーザ名など)を攻撃者に通知するための仕掛けである。

このサービスを知った時は、アクセス解析で使われるようなWebページに極小の透明画像やiframeを埋め込むような方式を想像したが、ドキュメントを読んでいると他にも様々な方式をサポートしていることがわかった。そこで、実際にどういった仕掛けで、どういった情報が記録されるのか試してみることにした。

なお、なぜカナリアというかは、昔の炭鉱でガス発見の警報装置としてカナリアが使われていたことに由来していると思われる。
ja.wikipedia.org

ここから各方式の紹介に入るが、全ての方式に共通して、ターゲットがカナリアトークンに引っかかった時の通知先としてEメールアドレスを指定できる。今回は、以下の使い捨てのメールアドレス発行サービスを使用した。
✉ Guerrilla Mail - Disposable Temporary E-Mail Address

Web bug / URL token

URLが生成される。

f:id:graneed:20210224233209p:plain

f:id:graneed:20210224233228p:plain

生成されたURLにアクセスすると真っ白な画面が表示される。 HTMLソースを確認すると、ブラウザ情報を記録するようなフィンガープリント機能を持つJavaScriptが実装されていることを確認できる。

URLへアクセス後に以下の通知メールを受信した。
f:id:graneed:20210224233712p:plain

また、カナリアトークンごとにHistoryページが提供され、そこではもっと詳細な記録情報を閲覧できる。
f:id:graneed:20210224234011p:plain

DNS token

canarytokens.comのサブドメインが生成される。
サブドメインnslookupコマンドで問合せすると以下の情報が記録された。
f:id:graneed:20210225000024p:plain

Unique email address

Eメールアドレスが生成される。
メール送信すると、以下の情報が記録された。
f:id:graneed:20210225224509p:plain

Custom Image Web bug

画像をアップロードすると、URLが生成される。
imgタグ等でアクセスすると、アップロードした画像が表示され、アクセスした際に情報が記録される。
記録される情報は接続元IPアドレスとUserAgent。

見つからないようにするには、1px×1pxの透明画像を使用すると良いのだと思う。 最初に自分が想像していたカナリアトークンに一番近い。

Microsoft Word Document

Wordファイルが生成される。
開いても特にマクロは埋まっていない。ただ、ヘッダーとフッターが設定されているように見える。 f:id:graneed:20210225003453p:plain

WordファイルをZipファイルとして展開すると、footer2.xmlINCLUDEPICTURE "http://canarytokens.com/about/traffic/articles/whb0ezqg0ahskmqdwjjndemyg/contact.php"が埋め込まれており、HTTPリクエストが飛んだようだ。

記録される情報は接続元IPアドレスとUserAgent。
自分の環境ではUserAgentはMozilla/4.0 (compatible; ms-office; MSOffice 16)となった。

機密ファイルが社外で開かれていないかを確認するために使用できそうだ。

Acrobat Reader PDF Document

PDFファイルが生成される。
Acrbat Readerで開くと以下の警告ダイアログが表示される。
f:id:graneed:20210225005240p:plain

「許可」するとWebブラウザが立ち上がり、URLへのアクセスが発生した。 ただ、HTTPではなくDNSへのアクセスを見ているようで、記録される情報は接続元IPアドレス程度であった。

警告ダイアログが表示されてしまうと、ターゲットに気付かれないようにするのは少々厳しい。

Windows Folder

個人的に一番驚いた手法。
Zipファイルが生成され、Zipファイルを展開するとMy Documentsというフォルダが展開される。 My Documentsフォルダを開くと、エクスプローラーが固まるためしばらく待つと、フォルダの中にdesktop.iniファイルが確認できる。 desktop.iniの中身は以下のとおり。

[.ShellClassInfo]
IconResource=\\%USERNAME%.%COMPUTERNAME%.%USERDOMAIN%.INI.jomvcu020919h7fvr2k2er61h.canarytokens.com\resource.dll

フォルダをエクスプローラーで開くだけでDNS通信が発生していたようで、結果、以下の情報が記録された。
f:id:graneed:20210225010738p:plain

これもまた、機密のzipファイルが社外で開かれていないかを確認するために使用できそうだ。

Custom exe / binary

EXEファイルをアップロードすると、カナリアトークンが埋め込まれたEXEファイルが生成される。 ここでは7zのインストール用のEXEファイルで試すことにした。
f:id:graneed:20210225011116p:plain

面白いことに、生成されたEXEファイルをダウンロードすると同時に以下の情報が記録された。 f:id:graneed:20210225011640p:plain

Microsoft Defenderの仕業かな」と思って設定を確認したところ「サンプルの自動送信」がオンになっていた。 f:id:graneed:20210225012006p:plain

設定をオフにしてから再度EXEファイルを生成すると、ダウンロードすると同時に記録されることは無くなったため予想通り。サンプルの自動送信をすると、Microsoftサンドボックス環境みたいなところで実行されるのだろうか。

自分でEXEファイルを実行すると、以下の情報が記録された。思ったより情報量は少ない。
f:id:graneed:20210225012652p:plain

ターゲットにEXEを実行させる障壁が高そうだ。 そして折角EXEを実行させられるのであれば、もっと色々なことができそうだ。

Cloned Website

以下のようなJavaScriptが生成される。

if (document.domain != "https://example.com") {
    var l = location.href;
    var r = document.referrer;
    var m = new Image();
    m.src = "http://canarytokens.com/"+
            "fqcm9lekdo4ibfoh33ivslg05.jpg?l="+
            encodeURI(l) + "&amp;r=" + encodeURI(r);
}

自分が管理しているWebサイトにこのJavaScriptを埋め込んでおくと、自分のWebサイトをクローンされて、そこに誰かがアクセスした際に、クローン先のサイトのURLの通知をうけることができるようだ。

サイトの運用者側で、フィッシングサイトを見つけるのに役立ちそうだ。

SQL Server

カナリアトークンの生成画面は以下のとおり。 通知のトリガーとなるDML操作とテーブルを指定できる。 f:id:graneed:20210225013311p:plain

実行すると以下のようなSQLが生成される。

--create a stored proc that'll ping canarytokens
      CREATE proc ping_canarytoken
      AS
      BEGIN
          declare @username varchar(max), @base64 varchar(max), @tokendomain varchar(128), @unc varchar(128), @size int, @done int, @random varchar(3);

          --setup the variables
          set @tokendomain = 'h3q9kuxguigeeyscumxvia9s1.canarytokens.com';
          set @size = 128;
          set @done = 0;
          set @random = cast(round(rand()*100,0) as varchar(2));
          set @random = concat(@random, '.');
          set @username = SUSER_SNAME();

          --loop runs until the UNC path is 128 chars or less
          while @done <= 0
          begin
              --convert username into base64
              select @base64 = (SELECT
                  CAST(N'' AS XML).value(
                        'xs:base64Binary(xs:hexBinary(sql:column("bin")))'
                      , 'VARCHAR(MAX)'
                  )   Base64Encoding
              FROM (
                  SELECT CAST(@username AS VARBINARY(MAX)) AS bin
              ) AS bin_sql_server_temp);

              --replace base64 padding as dns will choke on =
              select @base64 = replace(@base64,'=','-')

              --construct the UNC path
              select @unc = concat('\\',@base64,'.',@random,@tokendomain,'\a')

              -- if too big, trim the username and try again
              if len(@unc) <= @size
                  set @done = 1
              else
                  --trim from the front, to keep the username and lose domain details
                  select @username = substring(@username, 2, len(@username)-1)
          end
          exec master.dbo.xp_fileexist @unc;
      END

      --add a trigger if data is altered
      CREATE TRIGGER TRIGGER1
        ON TABLE1
        AFTER INSERT
      AS
      BEGIN
      exec ping_canarytoken
      end

環境を作って試すところまでできていないが、SQL文を見ると、SUSER_SNAME()関数を実行してWindowsのユーザ名を取得し、サブドメイン名として送信しているようだ。

攻撃者の興味を引くようなテーブルを用意して、データベースの中身が窃取されていないかを確認するために使用できそうだ。

QR Code

その名のとおり、QRコードが生成される。
f:id:graneed:20210225013725p:plain

QRコードには以下のようなURLが埋め込まれている。 http://canarytokens.com/feedback/traffic/tags/iuixoweag4nucmn1vo76jk0mv/submit.aspx

アクセスすると記録される情報は接続元IPアドレスとUserAgent。

SVN

以下のコマンドが生成される。

svn propset svn:externals "extras http://dvztaffuww7mzjfe7tw0p75e9.canarytokens.com" .

自分が管理しているリポジトリにこのコマンドを実行することで、誰がか勝手にリポジトリをクローンしたことに気付けるようだ。

AWS keys

実行すると以下のAWSのアクセスキーが生成される。

[default]
aws_access_key_id = AKIAXYZDQCENVA3R6Y7J
aws_secret_access_key = 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
output = json
region = us-east-2

AWS CLIで使用してみる。

root@kali:~# aws configure --profile canary
AWS Access Key ID [None]: AKIAXYZDQCENVA3R6Y7J
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-2
Default output format [None]: json

root@kali:~# aws s3 ls --profile canary

An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

20分程度待つと以下の情報が記録された。 f:id:graneed:20210225015750p:plain

Fast Redirect

Web bug / URL tokenと似ているが、URLを生成する際にリダイレクト先のURLの指定ができる。
生成されたURLへアクセスすると、指定したURLにリダイレクトされる。
記録される情報は接続元IPアドレスとUserAgent。

Slow Redirect

Fast Redirectと同様に、URLを生成する際にリダイレクト先のURLの指定ができる。
記録される情報はWeb bug / URL tokenと同じで、接続元IPアドレスとUserAgentに加え、ブラウザ情報なども含まれる。

Slack API key

実行すると以下のSlackのAPIキーが生成される。

# Slack API key
slack_api_key = xoxp-905439787527-905447633015-1784132156549-acecf32171dd971f2ee2561cd0bd0eae

curlコマンドでAPIを実行してみる。

root@kali:~# curl 'https://slack.com/api/auth.test' \
> -d 'token=xoxp-905439787527-905447633015-1784132156549-acecf32171dd971f2ee2561cd0bd0eae'
{"ok":true,"url":"https:\/\/ctorgworkspace.slack.com\/","team":"ct.org","user":"marge.haskell.bridge","team_id":"TSMCXP5FH","user_id":"USMD5JM0F","is_enterprise_install":false}

10分程度待つと以下の情報が記録された。 f:id:graneed:20210225020148p:plain

まとめ

カナリアトークンの使い道としては、生成したURL等を何らかの媒体でばら撒いてハニーポットとして使うほか、機密ファイルの漏洩有無の確認、Webサイトのクローン先のサイトの検出など、多岐にわたる。こちらのサービスと方式のパターンを知っておくと、どこかで役に立ちそうである。実際に何か目的があって使う場合は、自分で環境を立てる方がよさそうだ。

UTCTF 2020 writeup - Wasm Fans Only

某CTFチームのAdvent Calendar 2020向けの記事です。

例年のごとく年末の締めくくりとして今年開催のWeb系のWriteupを読み漁っていたところ、Web Assemblyの問題にいくつか遭遇した。Web Assemblyは比較的Rev問に近く苦手意識があるものの解けるようになりたいと思っていた。そんな折にChromeのWeb Assemblyのデバッグ機能が強化された記事を見かけた。

developers.google.com

これで少し手が出しやすくなってきた気がしたので、今年の過去問をサンプルに解いてみることにした。

問題

UTCTF 2020のWASM Fans Onlyを解く。

この大会は2020年3月開催であったにもかかわらず、嬉しいことにまだ問題サーバが生きている。
問題サーバはこちら。
https://wasmfans.ga/

また、githubに問題ファイルが公開されているのも嬉しい。
元のCのソースコードも公開されており、答え合わせをしながら解析できるため、初心者にピッタリ。
github.com

またWriteupもあるのも嬉しい。
https://ctftime.org/writeup/18640

なお、後のネタバレになってしまうが、実は問題サーバのホスト名(wasmfans.ga)がキーになっているため、もし上記のgithubリポジトリから落としてきて自分のサーバで試したい場合は、wasmfans.gaのホスト名で自サーバにアクセスできるよう/etc/hostsやC:\Windows\System32\drivers\etc\hostsを設定すること。

Writeup

HTML/JavaScript解析

まずURLにアクセスすると以下の画面が表示される。
f:id:graneed:20201221232702p:plain

UsernameとPasswordを適当に入力してLog inボタンを押下するとTry again!。
なおPasswordの項目IDはflagである。
f:id:graneed:20201221233402p:plain

Log inボタンを押下するとページ内のcheckFlag関数を呼び、checkFlag関数はModule._verify_flag();を呼んでいる。
f:id:graneed:20201221233058p:plain

_verify_flagverifyFlag.jsファイルに定義されており、更にWeb Assemblyのverify_flag関数を呼んでいる。 f:id:graneed:20201221233147p:plain

早速ここからWeb Assemblyの世界へ突入するが、その前にもう少しverifyFlag.jsファイルを見てみる。

verifyFlag.jsは非常に大きいファイルで正直わけわからん状態であるが、エラーメッセージの「Try again!」で検索すると、意味がわかるロジックが見つかる。これら3つの関数はWeb Assemblyから呼ばれる関数なので覚えておく。 f:id:graneed:20201221233726p:plain

wasmのざっくり静的解析

verifyFlag.wasmファイルをverify_flagで検索するとすぐに関数が見つかる。
ザっと目につく処理を書き出してみる。

  1. 0x01826~0x018e8
    変数の宣言と初期化?

  2. 0x018ec~0x01985
    ループ処理で何かを55とXORしている
    f:id:graneed:20201221235418p:plain

  3. 0x019ac
    $env.getStringをcallしている
    f:id:graneed:20201221234925p:plain

  4. 0x019b8~0x01a23
    変数の宣言と初期化?

  5. 0x01a26~0x1abc
    ループ処理で何かの変数を96とXORしている
    f:id:graneed:20201221235443p:plain

  6. 0x01ae3
    $env.getStringをcallしている
    f:id:graneed:20201221235535p:plain

  7. 0x01b26~0x01bf3 変数の宣言と移送か何かをしている

  8. 0x01c3e~0x01de0

    • $func23を呼んでから巨大な処理ブロック内へ。
    • $func23の返り値が0の場合は$label6を抜けて$env.loseを呼ぶ。その後$label9を抜けて終了。→NG
    • $func23の返り値が0でない場合は$aes_encrypt_blockを呼ぶ。その後ループ内で何らかの配列を1文字ずつ比較。
      • 一致しない場合は$env.loseを呼ぶ。→NG
      • ループを抜けた=全ての変数が一致したら$env.winを呼ぶ。→OK
    • $func23の解説は省略。フラグ文字列の文字数とprefixとsuffixのチェックである。

8のうち、ポイントとなる処理だけ残して以下に転記した。

    call $func23
    local.set $var121
    block $label9
      block $label6
        local.get $var121
        i32.eqz
        br_if $label6
        (snip)
        local.get $var133
        local.get $var128
        local.get $var125
        call $aes_encrypt_block
        local.get $var2
        local.get $var122
        i32.store offset=12
        block $label7
          loop $label10
            i32.const 16
            (snip)
            local.get $var159
            local.get $var160
            i32.and
            local.set $var161
            block $label8
              local.get $var161
              i32.eqz
              br_if $label8
              call $env.lose
              br $label9
            end $label8
            (snip)
          end $label10
          unreachable
        end $label7
        local.get $var2
        i32.load offset=136
        local.set $var165
        local.get $var165
        call $env.win
        br $label9
      end $label6
      call $env.lose
    end $label9

何となく処理の流れは掴めたので、あとはChromeのDebuggerで変数の値を確認しながら実際に動かしてみる。

wasmの動的解析

※この解析には冒頭で紹介したChromeのWeb Assemblyのデバッグ機能を使いたいためChromeのCanary版を使用している。(2020/12/22現在)

verify_flagの先頭にブレイクポイントを設定し、usernameにaaaaaaaaaaaaaaaa、Passwordにbbbbbbbbbbbbbbbbを入力して実行する。
f:id:graneed:20201222004404p:plain

そして、Chromeの新機能を発動するべく右側のペインのenv.memoryで右クリックしてInspect memoryを選択する。
f:id:graneed:20201222004505p:plain

下側のペインにメモリの状態が表示された!ASCII表示もあるぞ!
これまで右側のペインで縦に並んだ変数1つずつしか見れなかったのに、これは嬉しい。
f:id:graneed:20201222004558p:plain

また、変数にカーソル当てると、変数の中がホバーで表示されるようになった!
「え、今までそうじゃなかったの?」と疑問に思う人がいるかもしれないが、2020/12/22現在の安定板のChromeではできない。
f:id:graneed:20201222005231p:plain

このホバーで表示された変数をコピーし、先ほどのメモリインスペクタのアドレス欄に貼り付ける。 (10進数を貼り付けると、自動的に16進数に変換してくれる。) すると、何かのバイトデータが格納されていることがわかる。読める、読めるぞ。
f:id:graneed:20201222005635p:plain

上図は最初の変数の初期化が終わるあたりまで処理を進めた後の状態であるが、次に55とXORをとっているループ処理を何周か進めてみると、JavaScriptのDOMへのアクセスっぽい文字列が出てきた。
f:id:graneed:20201222010230p:plain

ループを最後まで進めると、HTMLのflag項目(Passwordのテキストボックスに入力した文字列)を取得するためのJavaScript呼出し文字列であるdocument.getElementById("flag").valueが出現した。
f:id:graneed:20201222010413p:plain

そして、その次の$env.getStringにこのJavaScript文字列を渡し、Passwordに入力した文字列を取得しメモリに格納された。
f:id:graneed:20201222010947p:plain

同じように次の$env.getStringまで処理を進めると、window.location.hostnameの文字列を渡してホスト名wasmfans.gaをメモリに格納していることを確認できた。
f:id:graneed:20201222011240p:plain

$func23まで進めると、返り値0が返ってきてそのままlose処理へ。

$func23を突破するため、24文字かつprefixがutflag{、suffixが}の条件を満たす文字列をPassword項目に入力してリトライ。
f:id:graneed:20201222012546p:plain

無事に突破して$aes_encrypt_blockの呼出しまで進め、渡している変数を確認する。
ここでも変数のホバー表示とメモリインスペクションが活躍する。
f:id:graneed:20201222012639p:plain

渡している3つの変数は以下のとおり。

  • Password項目に入力したutflag{AAAAAAAAAAAAAAAA}のA×16文字の部分。
    f:id:graneed:20201222013051p:plain
  • いつの間にかメモリに格納されていたnasmfans.gaという文字列。どうやら静的解析の7の処理で生成していたようだ。AESの鍵にあたる。
    f:id:graneed:20201222013028p:plain
  • 0x00。たぶんAESのIV。
    f:id:graneed:20201222013137p:plain

そろそろゴールは近い。

入力文字列をAES暗号化したデータとどのデータと比較しているかだが、ステップ実行しながら変数とメモリインスペクションを確認していくと、以下のバイトデータと比較していそうなことがわかる。 f:id:graneed:20201222013652p:plain

残念ながらメモリインスペクションからコピペができないため、手動で書き出す。
0f ae f8 59 84 b1 28 67 28 18 88 17 64 d3 25 2a

あとはAES復号するだけ。

from Crypto.Cipher import AES

encrypted = [0x0f, 0xae, 0xf8, 0x59, 0x84, 0xb1, 0x28, 0x67, 0x28, 0x18, 0x88, 0x17, 0x64, 0xd3, 0x25, 0x2a]
key = []

for c in 'nasmfans.ga':
    key.append(ord(c))
while len(key) != 16:
    key.append(0)

cipher = AES.new(bytes(key), AES.MODE_ECB)
print(cipher.decrypt(bytes(encrypted)))

実行する。

root@kali:/mnt/hgfs/CTF/Contest/wasm# python3 solve.py
b'fPRv38aICAz31Ix7'

フラグ文字列感は無いが、試しに画面入力してみる。prefixとsuffixは忘れずに。

f:id:graneed:20201222020602p:plain

初めてのWeb Assembly問ということで、元のCのソースやWriteupがあるにもかかわらず時間をかけたが、おかげで多少慣れた。直近のChromeデバッグ機能追加が無ければ心が折れたかもしれない。今後の機能強化も期待したい。