CTFサーバとの自動対話
CTFでは指示されたサーバに接続して問題を解くことがよくある.例えば次のようにnetcatを利用した接続の方法が示される問題はその典型である.
nc 192.168.61.74 6174
しかしながら,このコマンドを実際に端末で実行してサーバと対話的に通信しても問題が解けない場合がある.例えば,サーバとの接続に時間制限があり,しかもその時間が問題を解く人にとってあまりに短い場合がこれに当てはまる.
例えば,サーバから送られてくる2つの整数の掛け算20題を10秒以内に計算する問題が出題されたとしよう.この場合は,人が暗算で問題を解き,キーボードを使って解答を送信するという一連の操作20題分を10秒以内で行うことはほとんど不可能であると考えられる.
このような問題を解くにあたって,サーバとの対話的通信を自動的に行うクライアントを作成することが有効である.また,サーバからのデータの代わりに標準入力のデータをクライアントに与えることができれば,クライアントの動作の検証も容易である.
そこで,この記事では2つの整数の掛け算20題を計算する問題を出題するサーバスクリプト,これに対応したサーバとの自動対話および標準入力による動作確認が行えるクライアントスクリプトを紹介する.
サーバスクリプト
require 'socket' gate = TCPServer.open(ARGV[0]) qnum = 20 random = Random.new while true do # マルチスレッドで複数クライアントに対応 Thread.start(gate.accept) do |sock| perfect = true for i in 1..qnum do # 問題生成 sock.write("Question " + i.to_s + " / " + qnum.to_s + "\n") question = (random.rand((1 + i * i / 4)..(i * i))).to_s question += " * " question += (random.rand((1 + i * i / 4)..(i * i))).to_s sock.write(question + "\n") # 答えがあっているかチェック msg = sock.gets if (msg == nil) || (msg.strip == "") || (msg.to_i != eval(question)) then sock.write("Incorrect \n\n") perfect = false break end sock.write("Correct\n\n") end if perfect then # 実際のCTFではここでフラグを表示することが多い sock.write("Perfect\n") end sock.close end end gate.close
リッスンポートを引数として与えて起動する.
$ ruby server.rb 6174
手元で試す場合はnetcatを利用して次のコマンドで接続できる.
$ nc localhost 6174
接続するとサーバから掛け算の問題が出題され,正しい答えを送信すると次の問題が出題される.後に出題される問題ほど大きな数が使われた出題が増える.
$ nc localhost 6174 Question 1 / 20 1 * 1 1 Correct Question 2 / 20 4 * 3 12 Correct Question 3 / 20 4 * 9 ...
クライアントスクリプト
この問題に対応するスクリプトは例えば次のようになる.
require 'socket' if ARGV.size == 2 then $sockin = TCPSocket.open(ARGV[0], ARGV[1]) $sockout = $sockin $interactive = false else $sockin = STDIN $sockout = STDOUT $interactive = true end def print_if_int(str) if $interactive then print(str) end end def print_if_not_int(str) if !$interactive then print(str) end end print_if_int("Interactive mode\n") while true response = $sockin.gets if response == nil break end print_if_not_int(response) if response =~ /^[0-9]/ then answer = eval(response) print_if_not_int(answer.to_s + "\n") $sockout.write(answer.to_s + "\n") end end $sockin.close
手元で試す場合は,サーバスクリプトが起動している状態で次のようにホスト名とポート番号を与えて起動すると,サーバスクリプトとの自動的な対話が行われる.
$ ruby client.rb localhost 6174
また,引数を与えずに起動すると,標準入力をサーバからの入力とみなして動作する.
$ echo -e "1 * 1\n13 * 77" | ruby client.rb Interactive mode 1 1001
*1:マルチスレッド化によって複数のクライアントからの接続を受け入れるように変更した.(2017-10-20 追記)