SITW#30

CTF勢がいるらしいけど、この問題面白いから解いてみてよという事で解いた。

  • とりあえず開いてみる: 開けない
  • 修復を試みてみる: 開けない
    • $ pngfix sample.png
    • $ optipng -fix sample.png
  • 本当にpng
    • $ file sample.png: 本当にpngではある
    • (実はこういうファイルは最初にページャで見る習慣があるけど「臼NG」で始まってるから疑ってはなかった)
  • ヒントを見ると「正方形の枠」とあるのでQRコードかサイズだと思った
    • 前者だとよくわかんないからまずは後者
    • ここでfile出力に戻ると500x468(500と468のどちらに合わせるべきだろう?)
    • 問題文のjpgは400x400なのでどっちとも違うけど500に合わせてみる
    • ここみたいにしてviで以下を修正
 0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
-0000010: 0000 01f4 0000 01d4 0806 0000 00cb d6df  ................
+0000010: 0000 01f4 0000 01f4 0806 0000 00cb d6df  ................

開いてみるとFLAG{QWERTYUIOP}が取れておしまい。

追記: Windowsだと修正前でも開けるらしい。なるほど、高さが足りなくてフラグが隠れるのね。面白い。じゃあ400の行は根拠としては有効ではないや。

dashでRails Guides

あ、dashでRails Guidesを見る事もできるようになってる。自分でできる人は自分で生成して見たらよくて、そうじゃない人はRailsチュートリアルの電子版を買ったらよさそう。という事で早速試してみた。

(bundle installに失敗するようなパッケージは自分で入れてくだされ(sqlite3コマンドが叩かれるのでこれだけ書いてる))
$ sudo apt-get install sqlite3
$ git clone https://github.com/yasslab/railsguides.jp.git
$ bundle install --path ./.bundle
$ cd guides
$ bundle exec rake guides:generate:dash GUIDES_LANGUAGE=ja

guides/output/dash/ja/ruby_on_rails_guides_local.ja.docset というディレクトリが生成されるのでそれをdashから開いておしまい。

いい感じになった。ありがたやありがたや。でも本日時点だと左側のメニューから見ると階層がフラットだから検索以外のユースケースだと読み辛い。PRとか送れるといいなぁ。

CTFスコアサーバ

CTFのスコアサーバを使って遊んでみる。

yoggy/ctf-scoreserver

サイトにあるままにセットアップしたら動いた。機能がすくないけどそれが強みっぽい。以下にアクセスして使う。

  • :4567/: ユーザ向け
  • :4567/admin/: 管理者向け

以下のような感じだった。

  • カテゴリが設定できない
  • 個人で参加
  • 添付ファイルがあれば public 以下に自分で置いて問題文からリンクする必要がありそう
  • このサーバを攻撃してくれみたいな問題は別途サーバを立ててリンクする必要がありそう

機能が大変にシンプルだがそれは入門者に対して逆に情報過多にならない配慮かもしれないと思った。

isislab/CTFd

こちらもサイトにあるままにセットアップしたら動いた。docker-compose.yml見るにmariadbとか立ててるけどインストールしてないけどはて?と思ったらCTFd/config.pyを見るにDBが選択できるようだ。デフォルトだとsqliteみたい。
アクセスが少なければsqliteでもいいかもしれない。デモサイトもあるので試しやすい。

docker-compose.ymlが既にあるのがいいなと思ったので試してみようかと思ったけど、こけるのでとりあえずは諦めた。もう少し追いかけて必要ならPR送るなりしよう。

$ sudo apt-get install docker-compose git
$ git clone https://github.com/isislab/CTFd.git
$ cd CTFd && sudo docker-compose build
Service "db" mounts volumes from "data:rw", which is not the name of a service or container.

以下のような感じだった。入門向けじゃなくなってくると使ってみたい気持ちになる気がした。

  • 見た目はすっきりしててとてもいい
  • チームごとに参加する必要がある
  • CSSをいじる機能がある
  • ファイルを添付できる
  • このサーバを攻撃してくれみたいな問題は別途サーバを立ててリンクする必要がありそう
  • カテゴリが設定できる
  • 開催期間が設定できる

PTCoreSec/CTF-Scoreboard

自分ではインストールしてないけどYoutubeの最後の方を見るに使いやすそうだが3年前が最後のコミットみたいなので今は動かないかもしれないと思って試さなかった。

他にも色々あるんだけどCTF for ビギナーズ 2015 札幌で使ってるものが知りたいな。シンプルかつ一番見やすそう。少なくともTDUCTFは違ってそうだ。

場阿忍愚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の四と七が入れ替わってるのはなかなか強烈だった。でもまぁよく見ろって書いてあったし兵法の合計点と比較すると軽いイタズラか。

SECCON 2015 Write Up

SECCON 2015オンライン予選にRoute9チームで参加してきた。結果は99位。みんなすごいなぁという感じで面白かった。

以下、解いた問題に関する記録。

Exec dmesg

問題文
秘密のメッセージをLinuxのisoイメージの中から見つけてください。
記録

以下のようにして解いた。

  1. 問題文がexec dmesgなのでDLしたイメージをvirtualboxで起動してdmesgすると、dmesg: applet not foundとか言われる
  2. /var/log/messageみたいなものは存在しない
  3. dmesgが該当するかは知らなかったが、busyboxが面倒を見てるコマンドかもしれないのでls -l /bin/dmesgするとbusyboxへのsymlink
  4. busybox lsなどはできるがbusybox dmesgだとapplet not foundのためbusyboxに細工がしてあると推測
  5. strings busybox すると確かにdmesgがない(lsとかcatとかはあるのでここでなにか不要なコマンドをdmesgに置き換えてみる事にした)
  6. 手元の環境でstrings busyboxするとdmesgが存在した
  7. 5.の時に辞書順に並んでるコマンド郡の中でdmesgっぽい位置にあるenfth*1にとりあえず狙いを定める
  8. hexdump busyboxして置き換えるコマンドの位置を調べる(0x68b52(=428882)の位置を書き換えることにする
  9. sudo cp /bin/busybox /bin/busybox.new*2
  10. echo -n "dmesg" | sudo dd of=busybox.new bs=1 seek=428882 conv=notrunc
  11. busybox.new dmesg
  12. 結果の中にSECCONで始まる文字列がないか見る: SECCON{elf32-i386}

5.の辺りで以下のようなので、0x68b50=428880。+ 2バイトかな?という感じ。

$ hexdump busybox
...
00068b50 e.enfth.dnsdomain
...

しぶい問題だった。しかし、置き換えた後で思ったけどその辺からbusybox取ってきても良かったんじゃないだろうか?でも実は更にgrepだけして即回答してる人もいるみたいなので上には上がいるなぁと思った。

4042

問題文
謎の文章が2005年に古代遺跡から発見された。
これは何を意味している?
記録
  1. 4042と2005とくれば「RFC4042の事だ...」という感じでチームメンバが疑い始める*3
  2. no-network.txtというファイル名からRFC4042中のnonetの事だろうなぁという事で「絶対RFC4042の事だ...」と思う
  3. no-network.txtの中身を3文字ずつにしてRFC4042中の関数を使って読んでみるとSECCON{A_GROUP_OF_NINE_BITS_IS_CALLED_NONET}が取れる

なお、以下のような感じで作った。pack("U")してるけど、UTF-8じゃないのでまぁ化けるよねという感じだが100点なのでそんなに頑張らなかった。一応iconv -f UCS-4 -t UTF-8して失敗するのでfrom辺りが間違ってるかpack("U")してるのが問題かだと思うんだけどそこまでとした。

def digits_to_utf9_no_nets(s)
  return s.delete("\n").chars.each_slice(3).collect {|n| Integer("0" + n.join) }
end

def utf9_nonets_to_ucs4(no_nets)
  res = ""
  while !no_nets.empty? do
    no_net = no_nets.shift
    ucs4 = no_net & 0xff
    while (!no_nets.empty? && (no_net & 0x100) != 0) do
      ucs4 <<= 8
      no_net = no_nets.shift & 0xff
      ucs4 |= no_net
    end
    res << [ucs4].pack("U")
  end
  return res
end

no_nets = digits_to_utf9_no_nets(File.read('no-network.txt'))
puts utf9_nonets_to_ucs4(no_nets)

*1:後で気付いたけど、enfthはdmesgの一文字ずらしか。ヒントだったらしい

*2:最初は直接書き変えようと思ったけどbusyboxだから失敗したら何もできなくなる可能性があるので、cpして書き換える事にした

*3:良く知ってるねみんな

再びMatsue.rbのメンバーでRails Girls Matsueのお手伝いをした

11/13(金)、14(土)に行われたRails Girls Matsueのお手伝いを再びMatsue.rbのメンバー何人かでしてきた。いろいろとよかった。松江では特にそうは思わなかったし、別に特定の地域についてそう感じた訳でもないんだけど、「皆楽しんで終わり。何も残りませんでした」というだけにならないように気をつけないとなと思いながら参加した。でもそれは杞憂で、特にLTを聞きながらよい回なんじゃないかと思った。その他よかった点。

  • 結束力は相変わらずですごい
  • 他県の方まで手伝ってくださるのがすごい
  • メンツが固定じゃない(新陳代謝がある)
  • 直前に気分が悪くなる事があったけど、元気をもらってしまった

そうじゃない点として松江に限った事ではない点ではちょっと思ったのが、ガイドの修正についてはもう少し頑張るべきかもしれない。「教える側がどこを目指していて、教えられる側をどこに導きたいのか」という気持ちとコーチにjoinするという点について誰か矛盾を抱えたままやってないかしらと感じたような気がした。

Windowsはなぁ」とか「これはちょっとバグがあるからこう回避」みたいなものには人は来ない気がする(ので根絶して忘れるのが一番)けど、それを放置して成せる何かを考えてみても思いつくものはなかった。「楽しんで終わり」以外は。という事を開催前に考えてたけど、ガイド以外でそれを感じる点はなかったから、まぁよかったという結論でいい感じ。ガイドについては全国で数人activeならなんとかならなくもないからなぁ。

松江Ruby会議のプロコンで負けた

これが自分の結果(以下にコードのみ原文まま)。再起で関数呼び出してるところが他の人と大きく異なる点だけど、解き方自体はあまり他の人と変わらなかったなぁ。0.09秒を切ってみたかったけど、それはあたわず...

def f(a,r=[*a])
 s=r.shift
 x=s.reverse
 r.delete_at(r.rindex(x)||999) ? (s+f(a,r)+x) : (s==x ? s : (r[0] ? f(a,r) : ""))
end
puts f(readlines[1..-1].map!(&:chomp!).sort!)

しかしスタッフ容赦なかった。ルール的には問題ないけど。

# ところで今更気付いたけど結果ページに松江テルサ別館ってラボの事な気がするのであった...しまった...orz