SECCON2015 Writeup - EEIC Advent Calendar Day 6
この記事はeeic Advent Calendar 2015の6日目の記事になります。
目次
はじめに
去年初めてやったSECCONではまとまった時間が取れずに悔しい思いをしたので、今年こそはもう少し・・・!と思い、参戦してみました。 今回は、EEICのメンバー(@Tak_Yaz, @meryngii, @wakanapo_eeic, @show_me_tech)を適当に集めて、SECCON2015のオンライン予選に参加しました。
チームの結果としては、以下の問題を解いて1900点、1251チーム中80位という順位でした。
皆CTF初心者の集まりとしてはわりと頑張ったのではないでしょうか。 チームで解いたものは以下です。
- [ 50] Start SECCON CTF
- [100] SECCON WARS 2015
- [100] Unzip the file
- [100] Reverse-Engineering Android APK 1
- [400] Reverse-Engineering Hardware 1
- [100] Connect the server
- [300] Exec dmesg
- [300] Decrypt it
- [200] QR puzzle (Windows)
- [100] Steganography 1
- [100] Steganography 3
- [ 50] Last Challenge (Thank you for playing)
チームメンバのwriteupはこちら。
- Tak_Yaz: SECCONに参加しました。(Write Upその他)
ここでは、僕の解いた/解こうとしたものについて簡単にメモを残していこうかと思います。
やったこと
Try 1: Unzip the file
とりあえず暗号化されたファイルが置いてあったので、ブルートフォースで解読するコマンド、fcrackzipを回しました。 ただ、しばらく経っても終わらなかったので、同時にこの問題触れていた@meryngiiにバトンタッチ。@meryngiiがbacknumber08.txt, backnumber09.txtを見つけていたので、それを使って解錠してくれたようです。
Try 2: Fragment2
Unzipしながら僕はこの問題に移行しました。問題としてはpcapファイルがあるだけなので、パケットをwiresharkとかで見たら100点問題だしすぐわかんだろー、とか思って気楽に開いたら、なんと1パケットだけ。 ペイロードはバイナリだけど、"Flag is in header"とかいうメッセージと、ポート80からのレスポンスだからおそらくHTTP界隈なのだろうということは理解しました。HTTPかつバイナリのペイロード、と言ったらHTTP2かな、とか思ったのだけどよく知らないので、とりあえずIP/TCPのヘッダをじっと見てみる。(大会終わってからTwitter見た感じ、これがよくなかったっぽい。)
なにもわからない。ということで、時間使ってるうちに次の問題が開かれたのでそっちに移行。
Try 3: Connect the server
Reverse-Engineering Hardware 1という問題を開いたら、大量のRaspberry Piとそれにつながったブレッドボードの写真、あとgpioをつかってflagを表示するpythonスクリプトが...
解けるけどめんどくさそう、となったのでまず簡単そうなこちらから解いてみました。
Connect the serverはFQDNとポート番号が与えられていたので、とりあえずncで接続。するとこんなのが出てきた。
The server is connected via slow dial-up connection. Please be patient, and do not brute-force. login:
何入れていいのかよくわからないけど、カチャカチャいじる。 で、しばらく悩んでたら
The server is connected via slow dial-up connection. Please be patient, and do not brute-force. login: Login timer timed out. Thank you for your cooperation. HINT: It is already in your hands. Good bye.
となったので、あぁ、なるほどということでwiresharkでパケットキャプチャしたらなんかフラグが出てきました。
100点げっと。
Try 4: Start SECCON CTF
まだこれ誰もトライしていなかったので、とりあえず解いてみる。 暗号と復号化後の値が2つサンプルで書いてあるのが問題。
ex1 Cipher:PXFR}QIVTMSZCNDKUWAGJB{LHYEO Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ{} ex2 Cipher:EV}ZZD{DWZRA}FFDNFGQO Plain: {HELLOWORLDSECCONCTF} quiz Cipher:A}FFDNEVPFSGV}KZPN}GO Plain: ????????????????????? There is no bonus in this question
普通のシーザー暗号かなっておもったけど、カッコあるし違うじゃん。となり、PlainがABC順なのでマッピングテーブルをrubyでちゃちゃっとつくって解答。 50点。
Try 5: Reverse-Engineering Hardware 1
さて、400点問題、しかしただただ面倒なだけっぽい問題来ました。 こんな感じ。
大量の画像(ピンぼけ、影多数)からよーーーく見ると使われているICは74HC74ということでD-FFなのがわかります。で、プログラム中もDA, DBといっているのでまぁきっと読み間違いじゃないのだろうと確認。ただ、X1, X2, ..., X6とプログラム中にはあるけど、これは写真中のLEDの左からただ対応しているだけではないのだろうと推測し、きちんと線を追ってリバースエンジニアリングすることにしました。
つらい、、、、、つらい、、、、、、、、と無限に言いながら@wakanapo_eeicと頑張って書いた回路はこんな感じ。(これは最終版の正しいもの)
これの論理式を、もともとのgpio.pyへ突っ込んで、テスト。 間違い。
写真を見直すと、一箇所ピンを見間違えてた・・・。
修正。
間違い。
[1日目終了。とりあえず家帰って寝て翌朝。]
写真を見直すと、一箇所ピンを見間違えてた・・・。
修正。
間違い。
写真を見直すと、一箇所ピンを見間違えてた・・・。
修正。
これでフラグが出てきました。
SECCON{###FD80UY#!8880UY#!8}
これ、また間違いだったかと思ったよ・・・・・・・・・・・・・・
しかし、まったくもって筋肉な問題だったし、どういう意図だったのだろうか。。。
改変したgpio.pyはこんな感じです。
X1 = 0 X2 = 0 X3 = 0 X4 = 0 X5 = 0 X6 = 0 D1 = 0 D2 = 0 Q1 = D1 Q2 = D2 def x1(d1, d2, q1, q2): return 1 if q1 else 0 def x2(d1, d2, q1, q2): return 1 if q2 else 0 def x3(d1, d2, q1, q2): return 1 if ((not d1) and q2) else 0 def x4(d1, d2, q1, q2): return 1 if ((d1 and q1) or (not q2 and q1)) else 0 def x5(d1, d2, q1, q2): return 1 if (not (d2 or q2)) else 0 def x6(d1, d2, q1, q2): return 1 if (q1 and q2) or (not q1 and not q2) else 0 def encoder(x6,x5,x4,x3,x2,x1): v = 0 v = x1; v = 2*v + x2 v = 2*v + x3 v = 2*v + x4 v = 2*v + x5 v = 2*v + x6 return v c = '@' flag = "" def calculate(): global X1, X2, X3, X4, X5, X6, D1, D2, Q1, Q2 X1 = x1(D1, D2, Q1, Q2) X2 = x2(D1, D2, Q1, Q2) X3 = x3(D1, D2, Q1, Q2) X4 = x4(D1, D2, Q1, Q2) X5 = x5(D1, D2, Q1, Q2) X6 = x6(D1, D2, Q1, Q2) print(Q1, Q2, X3, X4, X5, X6) # print(D1, Q1, D2, Q2, X1, X2, X3, X4, X5, X6) def clock(): global D1, D2, Q1, Q2 Q1 = D1 Q2 = D2 for i in range(10) : if c == 'Y' : # 0x39 = 0x111001 D1 = 0 D2 = 1 # GPIO.output(DA, False) # GPIO.output(DB, True) else: if (i & 1) == 0 : # 偶数 D1 = 0 else : D1 = 1 if (i & 2) == 0 : # x % 4 <= 1 D2 = 0 else : D2 = 1 calculate() # time.sleep(0.1) c = chr(encoder(X6,X5,X4,X3,X2,X1)+32) flag = flag + c # GPIO.output(CLK, True) # GPIO.output(CLK, False) clock() calculate() # time.sleep(0.1) flag = flag + chr(encoder(X6,X5,X4,X3,X2,X1)+32) print "The flag is SECCON{"+flag+"}"
Try 6: Individual Elebin
これは、ひたすらいろんなbinを実行するという問題。 fileコマンドしたらこんな感じ。
$ ls 1.bin* 10.bin* 11.bin* 2.bin* 3.bin* 4.bin* 5.bin* 6.bin* 7.bin* 8.bin* 9.bin* $ for i in `seq 1 11`; do file $i.bin; done 1.bin: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), statically linked, stripped 2.bin: ELF 32-bit MSB executable, MC68HC11, version 1 (SYSV), statically linked, stripped 3.bin: ELF 32-bit LSB executable, NEC v850, version 1 (SYSV), statically linked, stripped 4.bin: ELF 32-bit MSB executable, Renesas M32R, version 1 (SYSV), statically linked, stripped 5.bin: ELF 64-bit MSB executable, Renesas SH, version 1 (SYSV), statically linked, stripped 6.bin: ELF 32-bit MSB executable, SPARC, version 1 (SYSV), statically linked, stripped 7.bin: ELF 32-bit LSB executable, Motorola RCE, version 1 (SYSV), statically linked, stripped 8.bin: ELF 32-bit LSB executable, Axis cris, version 1 (SYSV), statically linked, stripped 9.bin: ELF 32-bit LSB executable, Atmel AVR 8-bit, version 1 (SYSV), statically linked, stripped 10.bin: ELF 32-bit LSB executable, ARM, version 1, statically linked, stripped 11.bin: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, stripped
とりあえず1は普通に実行できたけど、2.binからは環境がない。しかし、
$ strings 2.bin B76_76 890123456789abcdef GCC: (GNU) 3.4.6 .shstrtab .text .rodata .data .bss .stack .comment
ということで、2.binはきっとB76_76なのだろうと推測がつく。でも、3.binからは
$ strings 3.bin 0123456789abcdef GCC: (GNU) 3.4.6 .shstrtab .text .rodata .data .bss .stack .comment
9.binにいたっては、
$ strings 9.bin l/}/d h/y/ /_?OA !P0@A h/y/ F/W/ O__OO?Q (/3' O__OO?Q H/Y/O__OPp h/w' h.y. .F/W/n/ 1/ /_-N-}-l- L/]/O]_O ,/=//_?Oq/`/ 0123456789abcdef .shstrtab .text .trampolines .data .bss .stack
あぁ、さすがに実行しないと許してくれないのね、と悲しい気持ちになりました。
んじゃぁ、qemuで実行してやろうじゃないの、ということで実行。
$ qemu-mips 11.bin qemu: uncaught target signal 11 (Segmentation fault) - core dumped zsh: segmentation fault (core dumped) qemu-mips 11.bin
実行できない・・・・
ぶっちゃけqemuたたくの初めてだしなーよくわかんねぇなー、ということで諦めて次の問題へ移行しました。
Try 7: Reverse-Engineering Hardware 2
これもマッチョ系か・・・ということでみてみると、今度は配線が綺麗。
利用しているICは74HC161と記述があったので、バイナリカウンタがただ並んでいるだけのように見えます。
回路を(@wakanapo_eeicが)カンバって起こしてみると、どうやらバイナリカウンタをサイクリックシフトしたものがプログラムの入力の1byteになっているようだということまで確認し、付属していたpythonのコードを改変したのが以下。
#!/usr/bin/env python # import RPi.GPIO as gpio import time import sys import struct rfd = open(sys.argv[1], 'rb') wfd = open(sys.argv[2], 'wb') len = int(sys.argv[3]) load = 4 clock = 10 reset = 9 p0=21 p1=20 p2=16 p3=12 p4=25 p5=24 p6=23 p7=18 pns = [p7,p6,p5,p4,p3,p2,p1,p0] q0=19 q1=13 q2=6 q3=5 q4=22 q5=27 q6=17 q7=26 class Counter(): def __init__(self): self.n = 0 def init(self): self.n = 0 def setValue(self, n): self.n = n def getPin(self, x): if (x == q0): return 1 if self.n & (1 << 1) else 0 if (x == q1): return 1 if self.n & (1 << 2) else 0 if (x == q2): return 1 if self.n & (1 << 3) else 0 if (x == q3): return 1 if self.n & (1 << 4) else 0 if (x == q4): return 1 if self.n & (1 << 5) else 0 if (x == q5): return 1 if self.n & (1 << 6) else 0 if (x == q6): return 1 if self.n & (1 << 7) else 0 if (x == q7): return 1 if self.n & (1 << 0) else 0 return -1 def clock(self): self.n += 1 def pulse(pin): pass # gpio.output(pin, gpio.LOW) # gpio.output(pin, gpio.HIGH) def init(): pass # gpio.setwarnings(False) # gpio.setmode(gpio.BCM) # gpio.setup([clock,load,reset], gpio.OUT) # gpio.output([clock,load,reset], gpio.HIGH) # pulse(reset) # gpio.setup(pns, gpio.OUT) # gpio.output(pns, gpio.LOW) # for q in [q7, q6, q5, q4, q3, q2, q1, q0]: # gpio.setup(q, gpio.IN) def setValue(n): pulse(reset) for i in range(n): pulse(clock) def a2v(a): return a[7]+2*a[6]+4*a[5]+8*a[4]+16*a[3]+32*a[2]+64*a[1]+128*a[0] # main init() counter = Counter() for i in range(len): # value = a2v([gpio.input(q7), gpio.input(q6), gpio.input(q5), # gpio.input(q4), gpio.input(q3), gpio.input(q2), # gpio.input(q1), gpio.input(q0)]) value = a2v([counter.getPin(q7), counter.getPin(q6), counter.getPin(q5), counter.getPin(q4), counter.getPin(q3), counter.getPin(q2), counter.getPin(q1), counter.getPin(q0)]) # file convert v = rfd.read(1) d = '' d += struct.pack('B', ord(v) ^ value) wfd.write(d) # setValue(value) counter.setValue(value) # time.sleep(0.1) # value+3 counter.clock() counter.clock() counter.clock() # pulse(clock) # pulse(clock) # pulse(clock) # gpio.cleanup()
で、入力に付属のencryptedを食わせて出力をみてみたら、どうやらgzipらしいということがわかった。
が、しかし。ここからが解けない。gzipコマンドをつかって修復を試してみも解凍できないし、この67バイトのバイナリは一体なんなのか・・・? というのがわからずに諦めて次の問題へ。
Try 8: Micro computer exploit code challenge
問題は以下。
サーバーURL 10000 * プログラムはサーバの10000番ポートで稼働中 * フラッグ "SECCON{...}" はマシンコードの 0x1800 にある * プログラムはGDBシミュレータの上で動いている (gdb-7.10 + special patch for read service) avr-elf.x
もともとAVRは使っていたので環境が整っている気がするぞラッキー、と思いながら、すでに始めていた@meryngiiを手伝い始めました。 avr-elf.xはAVRマイコン向けのバイナリだということがfileコマンドからもわかったので、avr-objdumpでアセンブラからスタート。
これは、そのうちのメインルーチン(唯一入力を受け取る部分)のproc関数です。
00001126 <proc>: 1126: cf 93 push r28 1128: df 93 push r29 112a: cd b7 in r28, 0x3d ; 61 112c: de b7 in r29, 0x3e ; 62 112e: 60 97 sbiw r28, 0x10 ; 16 1130: 0f b6 in r0, 0x3f ; 63 1132: f8 94 cli 1134: de bf out 0x3e, r29 ; 62 1136: 0f be out 0x3f, r0 ; 63 1138: cd bf out 0x3d, r28 ; 61 113a: 83 e2 ldi r24, 0x23 ; 35 113c: 98 e1 ldi r25, 0x18 ; 24 113e: 0e 94 70 08 call 0x10e0 ; 0x10e0 <puts> 1142: ce 01 movw r24, r28 1144: 01 96 adiw r24, 0x01 ; 1 1146: 0e 94 60 08 call 0x10c0 ; 0x10c0 <gets> 114a: 80 e3 ldi r24, 0x30 ; 48 114c: 98 e1 ldi r25, 0x18 ; 24 114e: 0e 94 70 08 call 0x10e0 ; 0x10e0 <puts> 1152: ce 01 movw r24, r28 1154: 01 96 adiw r24, 0x01 ; 1 1156: 0e 94 70 08 call 0x10e0 ; 0x10e0 <puts> 115a: 8c e1 ldi r24, 0x1C ; 28 115c: 98 e1 ldi r25, 0x18 ; 24 115e: 0e 94 70 08 call 0x10e0 ; 0x10e0 <puts> 1162: 80 e0 ldi r24, 0x00 ; 0 1164: 90 e0 ldi r25, 0x00 ; 0 1166: 60 96 adiw r28, 0x10 ; 16 1168: 0f b6 in r0, 0x3f ; 63 116a: f8 94 cli 116c: de bf out 0x3e, r29 ; 62 116e: 0f be out 0x3f, r0 ; 63 1170: cd bf out 0x3d, r28 ; 61 1172: df 91 pop r29 1174: cf 91 pop r28 1176: 08 95 ret
AVRの関数呼び出しのconvention ruleをググって、r24, r25が関数の引数/返り値だということを把握しました。また、in r28, 0x3d/in r29, 0x3eはスタックポインタだということもAVRの命令セット一覧からわかりました。
で、この関数はr28, r29をpushしたあと、スタックを0x10下げているので、getsのバッファ(0x10バイト、つまり16バイト)をこえると、17バイト目がr29, 18がr28となり、19/20バイト目がprocからの戻り番地になっていると考えられます。
しかし。そうなるように入力を与えてもプログラムカウンタを好きなところに飛ばせない。飛ばしたあとの処理も思いついていない上にこれではどうしようもないなぁ。というところで残り30分。
残り30分はいろいろパパっと他の問題見たり、@muscle_azosonが4042を解けそうになっていたのでそれを手伝ったりしましたが、得点にはつながらなかったです。
END
僕が絡んだ得点は結局550点でした。
おわりに
QRコードが死ぬほど出てたり、ハードウェアのマッチョ系問題があったりと初心者でも労力かければ解ける系問題が案外多いなぁと思いましたし、そこからある程度得点を稼ぐことができたように感じます。(こんなもんなのかな?)
一方、バイナリだとかネットワークだとかはなかなか知識が足りずに難しく、スッキリ解くことができなくて辛かったです。
またもうちょっと知識を付けてぜひ再戦したいですね。興味ある人いたらぜひ声かけて下さいな。”コンピューター”のいろんな知識がつくのでとてもおすすめです。