たのしい難読化のお話
まえがき
20歳になりました
はじめに
これは香川大学工学部サークルSLPのアドベントカレンダー15日目の記事です。
SLP KBIT Advent Calendar 2017 - Adventar
みんなだいすき難読化のおはなし
難読化ってなんだろ?
難読化コード 難読化コード(Obfuscated code)とは、コンピュータプログラムにおいて、その内部的な動作の手続き内容・構造・データなどを人間が理解しにくい、あるいはそのようになるよう加工されたソースコードやマシンコードのこと。 wikipediaより引用
JavaScriptなどのスクリプト言語では、ソースコードが簡単に見えてしまうので、まずいよねってこと。
できるだけクライアントサイドの処理を減らすか、今回の難読化という方法が取られます。
難読化コードってどんなの?
難読化には様々な手法がありますが、おもにJavaScriptでは次の3つの手法が取られます。
- 変数名を変数名や値をわかりづらくする
- ダミーのコードを埋め込む
- Ajaxなどで動的にコードを生成する。
この内、3つ目のAjaxは、クライアントだけで完結するのは難しいです。
今回は1つ目にしぼって難読化コードを考えてみます。
変数名の難読化
JavaScriptの変数名には「_」や「$」が使用できるのはご存知かと思われますが、
じつは一部の2バイト文字も利用することができます。
var ぞば=5; console.log(ぞば)//->5
漢字も対応してますので「鬱」や「呪」とかの漢字を使い、 読む人の精神を攻撃するのもいいかもしれませんね
あとは、1つのオブジェクトに変数をまとめるのも難読化では用いられます。
var 呪 = {呪:{呪:{呪:{呪:'呪'}}}}; console.log(呪.呪.呪.呪.呪)
うわぁ・・・
ドン引きですね、こんなのがコード中にあると、一体どんな気持ちでコードを書いていたのか心配になります。
さて、JavaScriptではオブジェクトのキーへのアクセスに[]を使用することができます。
window.document.bodyにアクセスするときに、
var a = "document" var b = "body" window[a][b]
とも書くことができます。これはショートコーディングにも使われる機能です。
後述する「値の難読化」と組み合わせると恐ろしい効力を持ちます。
変数名の難読化の面白いアプローチとしては、読む相手が人間ということを利用します。 変数名と内容を一致させないというのも難読化としては有効になります。
値の難読化
変数名の難読化ができても、値がそのままだとちょっと読みづらい程度で終わってしまいます。
ソースコードに文字列や数字がそのまま書いてあるのではあまり良くありません。
しっかり値も隠しちゃいましょう
数字の難読化
ここでは整数の場合に絞って考えてみます。
0以外の整数が1つ以上あれば、すべての整数を表現することができます。
ここでは、3の場合で考えてみます。
3-3 = 0
3/3 = 1
が出せます。あとはビットシフトなどをつかえば全ての数字を表現することができます。
例えば15なら
(3<<3/3+3/3)+3
などの表現を使用することができます。
ホーナー法を使えば自動で変換もできますね。
ホーナー法 - Wikipedia
また、~[]が-1になることを利用して、ソースコードから数字を消し去ることもできます。
文字列の難読化
難読化のキモです。文字列をどう処理するかで難読化の質が変わってくると言っても過言ではありません。
主に
- decodeURIでパーセントエンコードされた文字を戻す
- 「true」、「false」などを文字列に変換して使う
- String.fromCharCodeで文字コードを文字に戻す
- 「\uxxxx」を使う
があります。
1つずつ解説していきます。
decodeURIでパーセントエンコードされた文字を戻す
パーセントエンコードとは「%E3」など、URLなどで用いるためのエンコード形式です。
普通は、2バイト文字のエンコードに使用しますが、実は英数字もエンコードできます。
例えば「Gatrin」は「%47%61%74%6C%69%6E」とかけます。
変換は、以下のコードを参考にしてください。
"%"+"Gatlin".split('').map(w=>w.charCodeAt(0).toString(16).toUpperCase()).shift('%').join('%')
「true」、「false」などを文字列に変換して使う
これが一番強力です。こいつをうまく使うとコードからアルファベットが消えます。
JavaScriptでは「""+」と書くことで文字列に変換することができます。
これをうまく使い、「constructor」などの一部の文字を生成することができます。
(""+{})[5]+(""+{})[1]+(""+undefined)[1]+(""+false)[3]+(""+true)[0]+(""+true)[1]+(""+true)[2]+(""+{})[5]+(""+true)[0]+(""+{})[1]+(""+true)[1]
これは数値の難読化と合わせると非常に強力な難読化手法になります。
String.fromCharCodeで文字コードを文字に戻す
これは簡単です。String.fromCharCodeを呼び出すと文字コードを文字似直すことができます。
String.fromCharCode自体を1つ前の型変換を利用して隠してしまうと、すべての文字を難読化できます。
「\uxxxx」を使う
JavaScriptの文字列では、\uから始めることによって文字コードに対応した文字を作ることができます。
ただし、"\"+"uxxxx"などで連結してもタダの「\uxxxx」という文字にしかなりません。
evalなどで連結した文字をコードとして実行してやることによって目的の文字として取り出すことができます。
難読化の自動化
さて、これらの難読化を手動でやるのはさすがに無理です。
ある程度自動化する必要があります。
コードの変換には2つ方法があって
- 構文解析を使う
- コードを文字列として難読化する
構文解析を使う
Esprimaという構文解析用のモジュールがあります。
構文解析木をなぞりながら変数名、値を難読化していきましょう。
Esprima: Parser
実際に作ってみたのがこちら
GitHub - pekko1215/mochiscript: なんどくかだー
node index.html 入力ファイル 出力ファイル
で実行できます。
難読化始めた初期に作ったので、String.fromCharCodeの難読化が不十分ですね
コードを文字列として難読化する
こっちは簡単です。コード自体を生成するJavaScriptのコードをつくり、
evalやsetTimeoutなどで実行します。
実際に作ってみたのがこちら
GitHub - pekko1215/TrieteliScript
これはソースを「3」と記号に置き換えて難読化します。
なんで3なのかというと3周年だからです
eval、setTimeroutの代わりに「(3).constructor.constructor(コード)()」を利用してます。
最後に
いかがでしたか?JavaScriptのような規則が比較的ゆるい言語はいろいろな書き方をすることができます。
上に上げた以外にもいろいろな難読化の手法があります。難読化コードは作るのも楽しいですが読むのも楽しいです。
たまにはWebページのソースコードを覗いてみるのもいいかもしれません(๑•﹏•๑`)ぷえ~っ
SECCONに参加してみたお話 (2/2)
SECCONに参加してみたお話
さて、今回はSECCON 2017 Online CTF 予選に参加してみた感想とちょっとしたWrite-upをお届けします。
完全に初心者ですので、所々間違えてるかもしれないのでコメントで教えてもらえるとありがたいです。
前回のあらすじ!!
Pythooooon!!!
Powerful_Shell(300 points)
300ポインツ!?
パワフルなシェルつったらもうあれしかないよね!
ということでワタクシ専用問題がありました。
まず、powerful_shell.ps1というファイルが配られました。
実行するとこんな感じ
— ヒデホヒ パチスロおじさん (@eakonnsamui) 2017年12月10日
起動すると
いいね! pic.twitter.com/gTSdsxdscs
— ヒデホヒ パチスロおじさん (@eakonnsamui) 2017年12月10日
うーん、面白い
パスワードが音ってのがスパイみたいでかっこいいですね
ルパン三世とかで出てきそう
何はともあれ、ISEで中を見てみましょうか
うーん、とてもクール(。-`ω-)
見た感じソースの生成をしてますね
こういうのは実行する前にかっさらっていくのが粋な解き方です
Write-Progress -Completed -Activity "Extracting Script";.([ScriptBlock]::Create($ECCON))
どう見てもこの部分で実行してるので、コメントアウトしてechoで出してみます ビンゴですね、こんなところに隠れていたのか、このお茶目さんめ( ˘•ω•˘ )
どうやら音色と数字が対応してるみたいですね
echoで数字を出しながら照らし合わせると、
HHJHHJHJKJHJHF
という文字が出てきました。
演奏すると「さくら」になっています
うーんクールだ
Correct. Move to the next stage.
とクールさでは引けを取らない文字が出てくるので次に行きましょう
iex([System.Text.Encoding]::ASCII.GetString($plain))
懲りずにコード生成していますね
またechoでかっさらってしまいましょう!
はい出ました大好物!
大好きです、難読化コード
サービス問題ですねこれは
見た感じ、「;」や「[」を変数名に使って読みづらくしてるみたいですが、プロは最後を見ます
パイプ実行してますね、何が入ってくるのかを見てみましょう。
「&${;}」をechoに変えるだけです
まっっっっったく懲りてませんねw
最後の「|${;}」を消して、echoをiexに変えるとコードが出てきます。
正解は自分で試してみてね(/・ω・)/
感想
面白かった、毎週やってほしい
あとLinuxないから解けないやつおおいww
SECCONに参加してみたお話 (1/2)
SECCONに参加してみたお話
さて、今回はSECCON 2017 Online CTF 予選に参加してみた感想とちょっとしたWrite-upをお届けします。
完全に初心者ですので、所々間違えてるかもしれないのでコメントで教えてもらえるとありがたいです。
高松会場で参加!!
香川住みなんで高松会場で参加!
高松駅から歩いてすぐなんで道に迷わずに行けました。
地方でも開催してくれるのはうれしいですね!('ω')ノ
ついたのが17時前後、いつもは人がまばらな高松駅ですが、この日ばかりは人でいっぱい!活気にあふれていました。
さて、会場に近づいてくると人、人、人であふれています!
す、すごい人だ・・・
てかみんな半袖で寒くないのか?(:3 」∠)
ここで先にきていた組と合流します。
てかなんで金魚すくいやってるのw
どうすんのこれw
とまあここまで嘘です
ちゃんと書きます
参加経緯
今回、チーム名はKBJRで、メンバーはB1が3人、B2が5人という大所帯で参加しました。
ワタクシはセキュリティについてはまったくのシロートで、のこりのB2 4人の補助や解いたやつを見てみたいなと思い参加しました。
結果
黄色・・・赤西先生(B2)
赤色・・・ワイ(B2)
えぇ・・・
みんな何かしらの用事があったみたいで参加できなかったみたいです(:3 」∠)
しょーがないね(/・ω・)/
黄色問題はここに投げます www.marron.work
残りの赤色問題を解説したいと思います(。-`ω-)
Vigenere3d(100 points)
この問題はpythooonのコードと実行例、結果が与えられる問題です。こんな感じ
----- Vigenere3d.py import sys def _l(idx, s): return s[idx:] + s[:idx] def main(p, k1, k2): s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}" t = [[_l((i+j) % len(s), s) for j in range(len(s))] for i in range(len(s))] i1 = 0 i2 = 0 c = "" for a in p: c += t[s.find(a)][s.find(k1[i1])][s.find(k2[i2])] i1 = (i1 + 1) % len(k1) i2 = (i2 + 1) % len(k2) return c print main(sys.argv[1], sys.argv[2], sys.argv[2][::-1]) ----- $ python Vigenere3d.py SECCON{**************************} ************** POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9
うーんグレードザッパー
ワタクシのパソコンにはpython実行環境は入ってますが、
pythonアレルギーなのでとりあえずnodeで実行できる形に書き換えました。
function _l(idx, s) { return s.substr(idx) + s.slice(0, idx) } const answet = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9" //65 function main(p, k1, k2) { s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}" t = [...Array(s.length).keys()].map((i) => { return [...Array(s.length).keys()].map((j) => { return _l((i + j) % s.length, s) }) }); i1 = 0 i2 = 0 c = "" p.split('').forEach((a, i) => { c += t[s.indexOf(a)][s.indexOf(k1[i1])][s.indexOf(k2[i2])] i1 = (i1 + 1) % k1.length i2 = (i2 + 1) % k2.length }) return c } console.log(main(process.argv[2], process.argv[3], [...process.argv[3]].reduceRight((p, c) => p + c)))
暗号化としてはよく見る形ですね
元の文字(平文)にパスワード[i1]とパスワード[i2]を足して新しい文字を作っています。
よくあるやつですね、母校の小学校の学内ページのパスワードもこの形式でした。
Excelでまとめてみます
パスワード[i1]をP(i1)と書きます
P(i1) + P(i2) = P(i2) + P(i1)
なので、色が同じところは平文に同じ数を足すことで暗号文がとれます
「N」と「}」箇所が同じ29になっているのが確認できます。
これを使って平文を解読していきます。
うーんマイルド
長くなってきたのでPoweful_Shellは次の記事で('ω')ノ
BetterDiscord用、Discordを小さくするプラグインを作った
BetterDiscord用、Discordを小さくするプラグインを作った
おはヨッココ~、今回はDiscordを小さくするプラグイン(まんま)を作ってみました
どういうことかというと
こんな感じ、
通話中とかだとわざわざ大きな画面じゃなくてもいいよねってことで
暴力的なまでに小さくすることができます。
DIscordもしょせんElectron、しかもBetterDiscordにはプラグインという便利な機能まで備わってるじゃないですか
もうこれは作るっきゃないてことでね
つくっちゃいました
1.動作の流れ
- 上のバーにボタンを追加し、それを押すと動作する。
- 画面を小さくし、チャンネル情報などを非表示にして調節する。
- もう一度押すと元に戻る
2.完成
んで、できたのがこれ
アイコンが少し見づらいのでどうにかしたいところ
BetterDiscordのインストールが学内LANだと失敗する(解決)
BetterDiscordのインストールが学内LANだと失敗する(解決)
おはヨッココ~、Skypeはそろそろ消す予定のヒデホヒです。
タイトルからだとわかりづらいんですが、うちの大学の学内LANだとBetterDiscordの初回起動が失敗するんですよね。
うちの大学、ゲーム関係のサイトに行くとブロック!!されちゃうんですよね
小学校じゃねえんだぞ!!
具体的に言うとこんな感じ
ガッツリエラー出てますね・・・
おや、このエラー形式どこかで・・・
そうです、Discordはelectron製なんですね
みたいところソースコードもありそうですし見に行きましょう。
といっても、このままじゃエラーメッセージが見切れて見えません。
electron製ならPowerShellとかで起動するとログが見えるはず!
Discordの実行ファイルは「C:\Users\ユーザ名\AppData\Local\Discord\app-?.?.???」フォルダにあります。
ということで、起動してみると
BetterDiscord.jsというファイルが悪さをしているようですね。
見に行ってみましょう。
var keys = Object.keys(data); keys.forEach(key => { var emotes = data[key].emotes; emotes.forEach(emote => { returnData[emote.code] = emote.id; }); });
エラーメッセージを見ると、ここのdataがnullかundefinedになっているみたいです
nullかundefinedのときは空オブジェクトにしてエラーが発生しないようにしてみましょう
data = data || {}; var keys = Object.keys(data); keys.forEach(key => { var emotes = data[key].emotes; emotes.forEach(emote => { returnData[emote.code] = emote.id; }); });
無事起動しました。あんまりソースいじるのはいいことじゃないんで、素直に家でやったほうがいいんですけどね
同じようなエラーが発生しているなら、回線ブロックされていないか調べてみてください😋
BetterDiscordのプラグインとテーマを複数PCで共有する
BetterDiscordのプラグインとテーマを複数PCで共有する
ども、最近Discordの便利さにやっと気が付きました。
BetterDiscordを使うとプラグインとテーマ自由に決められるんですね。すごく素敵です。
View post on imgur.com
imgur.comさて、メイン機で設定したはいいものの、ノートで同じ設定をまたやるのは面倒ですよね。
今回はシンボリックリンクを作成を使ってGoogleDriveでプラグインとテーマを同期したいと思います。
1.設定フォルダの場所を確認する
BetterDiscordの設定フォルダは「%appdata%\BetterDiscord」フォルダにあります。
中にpluginsフォルダとthemesフォルダがありますね
テーマを設定している人ならファイルがあるのを確認できると思います。
この2つのフォルダをGoogleDriveで管理すれば複数PCで同じテーマが適用できるわけです。
さて、GoogleDrive側も用意しましょう。
とはいえこちらは、ふたつのフォルダをいれておくところを考えるだけでOKです
今回は「GoogleDrive/discord/better_discord」フォルダにします。
2.フォルダを移動させる
まず、Discordを終了させときます。
このとき、タスクバーのアイコンを右クリックし、「Quit Discord」でアプリごと終了します。
次に、BetterDiscordフォルダのpluginsとthemesをGoogleDriveのフォルダに移動させます。
3.シンボリックリンクを作成する
エクスプローラでBetterDiscordのフォルダを開き、ファイル→Windows PowerShellを開く→PowerShellを管理者として開くを押してPowerShellを起動させます。
次に以下のコマンドを実行します。
$drivepath = 'pluginsなどを移動したフォルダのパス' New-Item -ItemType SymbolicLink -Name plugins -Target $drivepath'plugins' New-Item -ItemType SymbolicLink -Name themes -Target $drivepath'themes'
Discordを起動し、いつもと同じテーマとプラグインが適用されていたら成功です。
同期したい他のPCでは、同じようにBetterDiscordフォルダを開き、themeとpluginsを削除し、
同じPowerShellの作業を行います。
いかがでしたでしょうか、それでは楽しいDiscordライフを!
(๑˃̵ᴗ˂̵)
Discordのテキストチャットを読み上げてみた 2
Discordのテキストチャットをボイスチャットに読み上げるBotづくり
ワタクシのようなコミュ障はなかなかボイスチャットに入りにくいんですね
けどわざわざテキストチャットを呼んでもらうのは会話のテンポてきにどうなの?
っておもったので作ってみることにします。
使うもの
NodeJS
voice-text(VoiceText Web APIをいい感じにしてくれる)
github.com
eris(DiscordBotをいい感じにしてくれる)
www.npmjs.com
前回までのあらすじ!
激熱モンゴル
3.溶接作業の下準備
3.1 動作について
- 複数のボイスチャンネルに音声を流すことはできないので、Botが音声を流すチャンネルを指定できるように
しなければならない。 - VoiceTextは声の変更もできるので、できればDiscord上で変更できるようにしたい。
- Generalのメッセージではなく、テキストチャンネルを別個に作成して読み上げたい
3.2 Discord上での操作
Botにコマンドメッセージを送ることで、読み上げチャンネルやボイスを変更できるようにする。
@Bot名 join General でGeneralに入室
@Bot名 stop で退室
@Bot名 voice 3 でボイスの変更
などが考えられる。
3.3 テキストチャンネルの作成
読み上げを行う投稿を、Botが作成する。
4.レッツ溶接
実際のコードは以下のようになった
const Eris = require("eris"); const { VoiceText } = require('voice-text'); const { writeFileSync } = require('fs'); const Tokens = require('./tokens.js'); const voiceText = new VoiceText(Tokens.voiceText); const bot = new Eris(Tokens.discord); var connection = null; var textBuffer = []; const ChannelName = 'text_to_voice' var userVoice = {}; const VoiceTable = ['hikari', 'haruka', 'takeru', 'santa', 'bear', 'show'] bot.on("ready", () => { // When the bot is ready bot.guilds.forEach((guild) => { var flag = true; guild.channels.forEach((channel) => { if (channel.name === ChannelName) { flag = false; } }) if (flag) { var parent = guild.channels.find((channel) => { return channel.name === 'Text Channels' }) guild.createChannel(ChannelName, 0, '', parent.id); } }) console.log("Ready!"); // Log "Ready!" }); bot.on("messageCreate", (msg) => { // When a message is created if (msg.mentions.some((user) => { return user.id === bot.user.id })) { var text = msg.content; var arr = text.split(' ') var commands = []; commands.push({ alias: 'join', fn: (name) => { var channel = msg.channel.guild.channels.find((channel) => { return channel.name === name && channel.type === 2 }) if (!channel) { return false; } bot.joinVoiceChannel(channel.id).then((con) => { connection = con; connection.on('end', () => { if (textBuffer.length) { connection.play(getYomiageStream(textBuffer.shift())) } }) }); return true } }) commands.push({ alias: 'stop', fn: () => { if (connection) { leaveVoiceChannel(connection.id) textBuffer = [] return true; } return false; } }) commands.push({ alias: 'voice', fn: (num) => { if (!(num in VoiceTable)) { return false; } userVoice[msg.author.id] = VoiceTable[num] return true; } }) var command; if (!arr.some((word, i) => { var com = commands.find((command) => { return word === command.alias }) if (!com) { return } if (!com.fn(...arr.splice(i + 1))) msg.addReaction('😥'); return true; })) { msg.addReaction('😥') } else { return } } if (msg.channel.name !== ChannelName) { return; } if (!connection) { return } if (connection.playing) { var voice = getVoiceByUser(msg.author.id) textBuffer.push({ voice: voice, msg: msg.content }) } else { var voice = getVoiceByUser(msg.author.id) var stream = getYomiageStream({ voice: voice, msg: msg.content }) connection.play(stream) } }) function getVoiceByUser(id) { if (id in userVoice) { return userVoice[id]; } var voice = VoiceTable[Math.floor(Math.random() * VoiceTable.length)]; userVoice[id] = voice; return voice; } function getYomiageStream(obj) { return voiceText.stream(obj.msg, { speaker: obj.voice }) } bot.connect(); // Get the bot to connect to Discord
bot.guilds.forEach((guild) => { var flag = true; guild.channels.forEach((channel) => { if (channel.name === ChannelName) { flag = false; } }) if (flag) { var parent = guild.channels.find((channel) => { return channel.name === 'Text Channels' }) guild.createChannel(ChannelName, 0, '', parent.id); } })
この部分で、テキストチャンネルがなければ新規に作成している。
erisの音声再生はありがたいことにStreamに対応しているため、
text-voiceでstreamを取得し、そのまま流し込んでいる。
5.デモ
— ヒデホヒ パチスロおじさん (@eakonnsamui) 2017年11月29日
6.感想
erisもtext-voiceも両方ともstreamに対応してたので、普通に書いてるとややこしい音声の送受信がかなり直感的に書けた。