場阿忍愚CTF(BurningCTF) Write Up

SITW#29で教えてもらっていたBurningCTFに参加した。ちびちびとしかできなくて一応100位以内には入ったけど、もう少しそれっぽい問題解きたかった。まだまだだなぁ。

初めて使ったライブラリ等あっていいきっかけになった。あと、典型的なものは解き方をストックしておくと別のCTFで役に立ちそうだ。次は何にしようかな。write up書いて残したいから何か締め切りがあるものがいい。

以下、解いた問題に関する記録(1つ解けてないのが混じってるけど)。練習と芸術は省略。

121-二進術-100

対話的に問題を解くことになるけど、問題文から1万回やらないといけないぽいのでプログラムで解いた(hdすると10 27な行があるからここ書き換えたらいいかもしれんけどなるべく機械的に解きたかったので試さなかった)。今回はexpectライブラリで解いてみた。

$ cat solve.rb
require "pty"
require "expect"

PTY.getpty("./121-calculation") do |i, o|
  n = 10000
  n.times do |j|
    i.expect(/((?:\d+)?\s*[-+*\/%]\s*\d+)\s*=\s*/, 10) do |line, question|
      o.puts(eval(question).to_s)
    end
    puts "#{j+1}/#{n}"
  end
  i.readline
  puts "flag: #{i.readline}"
end

$ ruby solve.rb
...
flag: FLAG_5c33a1b8860e47da864714e042e13f1e

IO#expectは期待する出力があるまで待つ処理らしいので、1万回実行した。初めて使ったけど普通に使う分には便利っぽい。

132-解読術-100

忍者といえば以下らしいという事をぐぐったので、

  • 忍者といえば「山」「川」の符丁
  • 忍者なら当時は神代文字が読めた

見比べながら読むと「やまといえば」という事に見えたので「かわ」と入力。

flag: かわ

133-解読術-200(これは解けてないのでダラダラと残してある作業記録)

とりあえず中身を見てみる。本当に公開鍵のようだ。

$ openssl rsa -inform pem -in Decrypt_RSA/public-key.pem -text -pubin
...

秘密鍵で暗号化してある場合に備えて念のため以下を実行。

$ openssl rsautl -decrypt -inkey Decrypt_RSA/public-key.pem \
  -in Decrypt_RSA/flag.txt
unable to load Private Key
6629:error:0906D06C:PEM routines:PEM_read_bio:no start line:/SourceCache/OpenSSL098/OpenSSL098-52.8.4/src/crypto/pem/pem_lib.c:648:Expecting: ANY PRIVATE KEY

とりあえず思いつくのはどれかの素因数分解アルゴリズムで見つけやすいnになってるというところだろうか。
以下など見ながら何パターンか試してみる。

まずはnを確認。上を加工してもいいけど、以下で直接。

$ ruby -r openssl -e 'p OpenSSL::PKey::RSA.new(File.open("Decrypt_RSA/public-key.pem")).n.to_i'
3107418240490043721350750035888567930037346022842727545720161948823206440518081504556346829671723286782437916272838033415471073108501919548529007337724822783525742386454014691736602477652346609

一応既知のものでないかnでぐぐってみるも特になし。(検索語が長過ぎると言われるので適当に切って検索)

何パターンか試してみる。

フェルマー

pとqが平方根に近いと速そう。

$ cat fermat.rb
def prime_division(n)
  x = Math.sqrt(n).to_i + 1
  y = Math.sqrt((x ** 2 - n).abs).to_i
  loop do
    w = x ** 2 - n - y ** 2
    if w == 0
      return x+y, x-y
    elsif w > 0
      y = y + 1
    elsif w < 0
      x = x + 1
    end
  end
end

p prime_division(ARGV[0].to_i)

$ ruby fermat.rb 3107418240490043721350750035888567930037346022842727545720161948823206440518081504556346829671723286782437916272838033415471073108501919548529007337724822783525742386454014691736602477652346609
(しばらく待ったけど解けないので違ってそう)
pari/gp

アルゴリズムと得意な素数のパターンはまだよくわかってない。

> factor(3107418240490043721350750035888567930037346022842727545720161948823206440518081504556346829671723286782437916272838033415471073108501919548529007337724822783525742386454014691736602477652346609)

残念ながら手元だとメモリ不足で死んでしまった。

msieve

アルゴリズムと得意な素数のパターンはまだよくわかってない。

$ msieve -n -e -v 3107418240490043721350750035888567930037346022842727545720161948823206440518081504556346829671723286782437916272838033415471073108501919548529007337724822783525742386454014691736602477652346609

残念ながらやってる範囲では終わらなかった。

もし素因数分解に成功していたら

以下で復号しようと思って準備はしていたがその夢はかなわなかった。残念。
試しに128bitの鍵を作ってpublic_encrypt→公開鍵の情報からmsieveで素因数分解→private_decryptで復号まではできたのだが、素因数分解アルゴリズムの事を調べきれないうちにおしまい。似たような事をする機会がまたありそうなので、その時にでも調べよう。

$ cat solve.rb
require 'openssl'

module RSAPrivateKeyAttrRecalculatable
  def p=(val)
    recalc_attributes do
      super
    end
  end

  def q=(val)
    recalc_attributes do
      super
    end
  end

  private

  def recalc_attributes
    res = yield
    if self.p && self.q
      self.d = e.mod_inverse( (p-1)*(q-1) )
      self.dmp1 = d % (p - 1)
      self.dmq1 = d % (q - 1)
      self.iqmp = q.mod_inverse(p)
    end
    res
  end
end

class OpenSSL::PKey::RSA
  prepend RSAPrivateKeyAttrRecalculatable
end

pubkey = OpenSSL::PKey::RSA.new(File.open("Decrypt_RSA/public-key.pem"))
pubkey.p = ARGV[0].to_i
pubkey.q = ARGV[1].to_i
puts "flag: #{pubkey.private_decrypt(File.read("Decrypt_RSA/flag.txt"))}"

opensslライブラリ便利だ。今回は以下の事を調べたので次回は調べなくていいのは楽。mod_inverseが用意されてるのがすごいうれしい。どこかで一度正面から突破してみたいな。

  • pとqが入ってれば秘密鍵とみなされる
  • p=やq=を呼んでもdなどは再計算されない
    • というのも、なんの分岐もないから
    • 一番嬉しいのはp=とq=をしたらよしなにしてくれる事だけどそこまではしてくれないようだ
    • newの時にpとqが指定できるでもよかったんだけど、それも見当たらなかった

以上から自分で再計算するとto_dem後にopensslコマンド叩くなり、private_decryptなりで復号できそうだ(上記)。

Module#prepend使うとこういうのが書きやすくていいので最近多用してる気がするな...

でも残念ながら素因数分解が(手元では)終わらなかったのでアプローチが間違ってるのかもしれない。とここまでが独力。

ひとさまのwrite upを見た後

pとqは既知だったらしい(上でぐぐってるけど、やり方がわるかったかなぁ...)。

$ ruby solve.rb 1634733645809253848443133883865090859841783670033092312181110852389333100104508151212118167511579 1900871281664822113126851573935413975471896789968515493666638539088027103802104498957191261465571
flag: FLAG_IS_WeAK_rSA

次はfactordbも使ってみよう。

161-電網術-50

  1. Wiresharkで161-problem.pcapを開く
  2. ftpでフィルタリングして99フレーム目にFLAG.tarを見ている事を確認
  3. 100フレーム目以降のパケットを生で見てFLAG.tarとして保存
  4. 保存したファイルを開くと RkxBR3tYVEluWDY5bnF2RmFvRXd3TmJ9Cg== という文字列が現れる
  5. $ ruby -r base64 -e 'print Base64.decode64("<4.の文字列>")'
flag: FLAG{XTInX69nqvFaoEwwNb}

162-電網術-75

  1. Wiresharkで162-basic.pcapを開く
  2. httpでフィルタリングして39フレーム目にBasic認証に成功したパケットを確認
  3. ヒントがbasicという事なのでベーシック認証に渡した文字列を確認(aHR0cDovL2J1cm5pbmcubnNjLmdyLmpw保存したファイルを開くと )
  4. $ ruby -r base64 -e 'print Base64.decode64("<3.の文字列>")'
  5. http://burning.nsc.gr.jp にアクセスして、ユーザ名に"http"、パスワードに"//burning.nsc.gr.jp"を入力(※Basic認証は:区切り)

5.ではまずパケットが接続してる先を見るべきだったかもしれない。

flag: flag={BasicIsNotSecure}

171-諜報術-100

web.archive.org。

flag: ソフトウェア開発エンジニア

173-諜報術-155

画像検索。

flag: twanzphobic.wordpress.com

174-諜報術-100

wgetしてgrep --text 123。

flag: tanakazakkarini123

181-記述術-100

大分非効率な感じのスクリプトで見つけた。もっと効率的な方法があるといいなぁ。

$ cat solve.rb
substr = nil
s = ARGF.read
s_len = s.length
(2..(s_len)).each do |n|
  candidates = (0..(s_len - n)).to_a.collect { |i| s[i..(i+n)]}
  candidates.each_with_index do |s, i|
    if candidates[(i+1)..-1].index(s)
      if substr.nil? || s.length > substr.length
        substr = s
        break
      end
    end
  end
  puts "#{n}: #{substr}"
end

$ ruby solve.rb
(8文字目くらいまで見て、条件を満たす範囲の続きの文字を回答)
flag: f_sz!bp_$gufl=b?za>is#c|!?cxpr!i><

182-記述術-200

  • 1. 'alert'がないので数値から組み立てると仮定して文字っぽいものを確認
$ irb
> 0x52.chr      # => "R"
> 0x54.chr      # => "T"
> 0b1001100.chr # => "L"
> 101.chr       # => "e"
> 0O000101.chr  # => "A"
> 1.chr         # => "\x01"
  • 2. 最初の3つくらいを1つずつブラウザで試す
window["map"] => undefined
window["eval"] => function eval() { [native code] }
window["eval"]["call"] ...
  • 3. chrっぽい事をして「荒痕(1)」なだけに "alert(1)" を組み立てるようなものにする
window["eval"]["call"]`${
    [ (0O000101), (0b1001100), (101), 0x52, 0x54 ]
    ["map"](x=>String["fromCodePoint"](x))["join"]("")["toLowerCase"]() +"(1)"
}`;

以下が出てくる。

flag: Flag={4c0bf259050d08b8982b6ae43ad0f12be030f191}

183-記述術-100

  • 1. 1文字ずつ入力すると何文字ありそうかわかる。長い...(ので総当りを諦める)
$ cat find_chars.rb
require "http"

((?a..?z).to_a + [?_, ?{, ?}]).each do |c|
  url = "http://210.146.64.36:30840/count_number_of_flag_substring/?str=#{c}"
  puts "#{c}: #{HTTP.get(url).to_s.lines.grep(/member/).first}"
  sleep 5
end

$ find_chars.rb
(この結果をsolve.rbに反映)
  • 2. 1文字ずつ先頭から見ていけば無駄な試行回数を減らせそうだなと思ったのでそうする
$ cat solve.rb
require "http"

# find_chars.rb の結果を参照(flagは除外)。
chars = [
  ?_ * 6,
  ?a * (10 - 1),
  ?d * 9,
  ?e * 4,
  ?f * (9 - 1),
  ?g * (3 - 1),
  ?h * 3,
  ?i * 7,
  ?j * 2,
  ?k * 4,
  ?l * (1 - 1),
  ?n * 6,
  ?o * 1,
  ?r * 8,
  ?s * 8,
  ?u * 3,
  ?x * 5,
].join.chars

class RetryError < StandardError; end

res = "flag={"
begin
  chars.uniq.each do |c|
    s = res + c
    # puts "try: #{s}"
    url = "http://210.146.64.36:30840/count_number_of_flag_substring/?str=#{s}"
    if md = /member of "[^&]+" are (\d+)/.match(HTTP.get(url).to_s)
      if md[1].to_i > 0
        res += c
        chars.delete_at(chars.index(c))
        puts "current: #{res}"
        raise RetryError
      end
    end
    sleep 5
  end
  if chars.empty?
    res += "}"
    puts "flag: #{res}"
  end
rescue RetryError
  retry
end

$ ruby solve.rb
...
flag: flag={afsfdsfdsfso_idardkxa_hgiahrei_nxnkasjdx_hfuidgire_anreiafn_dskafiudsurerfrandskjnxxr}

184-記述術-100

  • 1. とりあえず正直に手で解凍してしばらく様子を見る
$ bunzip2 < 184-flag.txt| bunzip2 | funzip | tar xOf - | gunzip -c |
  funzip | tar xOf - | bunzip2 | tar xOf - | tar xOf - | funzip |
  funzip | funzip | bunzip2 | funzip | tar xOf - | bunzip2 | tar xOf - |
  tar xOf - | tar xOf - | gunzip -c | bunzip2 | tar xOf - | ...
  • 2. 1.で特定の方式しか使われなかったのでサブコマンドでやろうとする(標準入力は複製されないんだったか)
$ bunzip2 < 184-flag.txt|
  (bunzip2; funzip; tar xOf -; gunzip -c) 2> /dev/null |
  (bunzip2; funzip; tar xOf -; gunzip -c) 2> /dev/null |
  ...
  • 3. コードを書いて自動でファイル判別→解凍を繰り返す
$ cat solve.rb
require "tempfile"

def extract(path)
  f = Tempfile.create("184")
  f.close
  case `file #{path}`
  when /bzip2/
    system("bunzip2 < #{path} > #{f.path}")
    return f.path
  when /gzip/
    system("gunzip -c < #{path} > #{f.path}")
    return f.path
  when /tar/
    system("tar xOf - < #{path} > #{f.path}")
    return f.path
  when /Zip archive data/
    system("funzip < #{path} > #{f.path}")
    return f.path
  else
    system("cat #{path}")
    return true
  end
end

f = Tempfile.create("184")
f.close
system("bunzip2 < 184-flag.txt > #{f.path}")
res = extract(f.path)
while res != true do
  res = extract(res)
end

$ ruby solve.rb

もう少し短く書けばいいのにと思ったけど頑張らなかった。なお、1024回くらい圧縮してあった。追記されるヘッダ分からもう少し多めの回数で圧縮されてるのかと思ってたけど、案外少なかった。

flag: flag={6aKuZrEqxvBZUIqBOXgMclLwpQCo8OXi}

192-超文書転送術-100

  1. 問題文の最後に記載されたURL(省略)/about に隠されたヒントを見逃すなとある
  2. HTMLの中身を見ても.hintクラスを含めて特にないので大文字の部分だけ抜き出して読む
  3. 実際にしでかしてみる(flag.txtをfindで探してcatした)
flag: flag={Update bash to the latest version!}

20x-兵法術-y

最初は自分で考えてたけど、途中から機械的に解く方針に変えて将棋所に解いてもらった。

flag(201-兵法術-50): 4769
flag(202-兵法術-100): 26355756444636
flag(203-兵法術-150): 545646455747
flag(204-兵法術-200): 26364656776656453635

202-兵法術-100の四と七が入れ替わってるのはなかなか強烈だった。でもまぁよく見ろって書いてあったし兵法の合計点と比較すると軽いイタズラか。