nkhrlab~

280字超の記事

CTFサーバとの自動対話

CTFでは指示されたサーバに接続して問題を解くことがよくある.例えば次のようにnetcatを利用した接続の方法が示される問題はその典型である.

nc 192.168.61.74 6174

しかしながら,このコマンドを実際に端末で実行してサーバと対話的に通信しても問題が解けない場合がある.例えば,サーバとの接続に時間制限があり,しかもその時間が問題を解く人にとってあまりに短い場合がこれに当てはまる.

例えば,サーバから送られてくる1以上1000以下の2つの整数の掛け算20題を10秒以内に計算する問題が出題されたとしよう.この場合は,人が暗算で問題を解き,キーボードを使って解答を送信するという一連の操作20題分を10秒以内で行うことはほとんど不可能であると考えられる.

このような問題を解くにあたって,サーバとの対話的通信を自動的に行うクライアントを作成することが有効である.また,サーバからのデータの代わりに標準入力のデータをクライアントに与えることができれば,クライアントの動作の検証も容易である.

そこで,この記事では2つの整数の掛け算20題を計算する問題を出題するサーバスクリプト,これに対応したサーバとの自動対話および標準入力による動作確認が行えるクライアントスクリプトを紹介する.

サーバスクリプト

このスクリプトは時間制限を提供しない.*1

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 追記)