2014年6月27日金曜日

パズル系CAPTCHAを自動で操作


 パズル型CAPTCHAとは何か
WEBでの認証時に、CAPTCHA(キャプチャ)として歪んだ文字・数字列の
入力を促されるものの、非常に読み辛く、何度も間違えてしまうという体験は誰にでもあるだろう。
また、その利用勝手の悪さにより、サイトを利用したいという意思のあったユーザの10%以上がサイトを離れるという、サービスの提供側にもデメリットがあるようだ。

利用ユーザとサービス提供者の不満を解消するために、人に優しいパズル型のCapy CAPTCHAというものが各種サイトで使われ始めている。


(パズル型CAPTCHAの簡易例)
右側の△はマウスで動かすことができる。
それを左側の穴が開いた▲のところまでドラッグすると認証成功となる。

1. 埋め込むための画像とそこへ移動させる穴の開いた画像が存在
▲   △

2. 左側へ右側の画像をドラッグ
▲ ← △

3. 穴に埋め込まれているかどうかで認証する


パズル型CAPTCHAは非常に直観的で分かりやすい。
またこれはシステム試験の自動化をはかるエンジニアにとってもメリットがある。
各種技術の発達と競うように人間でも解読が難しくなってきている文字列を読み込み、
入力することは難しくとも、このパズル型CAPTCHAであればなんとか突破できるのではないだろうか。



 パズル型CAPTCHAを攻略するためのツール選定
WEBテストの自動化ツールとしては種々のものがある。
(例) Capybaraを使ってブラウザ操作を自動化

ただしこららはHTTPのプロトコルに従った操作がメインであり、純粋な画像の認識操作はできない。そこで今回はSikuliを使うことにした。
SikuliはOpenCVライブラリを使用しているので画像認識系の処理に強い。
パズル型CAPTCHAを突破するのにうってつけである。
画像を認識し、その画像をどう操作するかを指示できるため、利用範囲はブラウザ内アプリに限らない。デスクトップ上のアイコン操作なども可能になる。

今回はSikuliを利用してスクリプトを記述し、パズル型CAPTCHAを自動で処理させてみる。



仮想的なWEB環境とSikuliを使った試験の自動化概要
1. 
ブラウザのお気に入りに登録しているCAPTCHA埋め込み試験用サイトのアイコンをクリック。
試験用サイトのトップページへ画面が遷移する。
※HTTPプロトコルだけに限らない点を確認するためにGUI操作をやらせてみる。

2.
サイトにはパズル型CAPTCHAが埋め込まれている。
認証のために、パズルを実施。

3. 
画像を重ね合わせただけでは画面は遷移しない作りにしているので、
その下段にある、ログインアイコンをクリックする。



ツールの作成
Sikuliのインストール方法、設定方法は公式WEBを参照のこと。

Sikuliではjruby、jythonが利用できる。
ここではjrubyを使う。文法はruby(cruby)と同じである。

ただし、Sikuli用の関数も使うのため、
詳細な利用方法はSikuliのマニュアルを参照すること。


自動クリック用関数
試験項目の1.3.用の関数を作る(緑色はSikuli関数である)。
def translate(picture, similarity)
  if exists(Pattern(picture).similar(similarity), 0)
    click(picture)
    sleep (2) # wait for loading
  end
end

アイコンをクリックする場合。
translate("icon.png", 0.8)

ログイン画像をクリックする場合。
translate("login.png", 0.8)


マウスを乗せると画像の色が変わったりするサイトもあるだろう。
そのために、similarityを下げておいた。
画像の合致度を正確に合わせたい場合は、
検索対象の画像とクリック画像を分ければいいだろう。
find("picture_link.png")
click("picture_hover.png")

また合致度のディフォルトはグローバルに指示もできる。
そうすれば個別に指定する必要はなくなる。
Settings.MinSimilarity = 0.80


CAPCH操作用関数
いよいよ、試験項目2.の関数を作る。
パズル用に用意されている画像は一つではないはずである。
色や形の違い、またその回転に違いが存在するとして、
その組み合わせを試す必要がある。

▲ ←
▲ ← 
▼ ←

最初は移動用の画像()と、移動後の穴あき画像(▲、▼)をそれぞれ用意していた。
しかし、移動画像をもとに合致度を下げて穴あき画像を認識させれば
大概うまくいくことが分かったのここでは移動前画像しか手元にないことを想定する。
画像の収集にも腐心しなくていいので楽だろう。

移動前の画像だけを取得していた場合、移動後の画像と
二つがマッチするのではないか、
という疑問を持つかもしれないが画像の検索範囲を絞れば問題は起きない。

 ----     ----
| ▲  | ← | △ |
 ----     ----

すべてのパターンを試しても、画像がマッチしないかもしれない。
その場合に備えてCAPTCHAをreloadして画像を変えて再試行する処理も入れる。


def captcha()
  # 数値はサイトの構成、GUI環境に合わせて変えること
  src_x=907; src_y=424; src_w=63; src_h=138
  dst_x=724; dst_y=424; dst_w=182; dst_h=138

  count=3

  colors = ["blue", "green", "yellow"]
  figures = ["pentagon", "square", "triangle"]
  rotations = ["0", "1", "2"]

  # windows環境であってもパスの区切りは'/'でないと認識できなかった
  path = "C:/Documents and Settings/test/test.sikuli"
  Dir.chdir(path)

  count.times do

    colors.each do | color |
      figures.each do | figure |
          rotations.each do | rotation |

            src_pic = "src_#{color}_#{figure}_#{rotation}.png"
            next if File.exists?(src_pic) == false

            dst_pic = src_pic

            src_pic_obj = Region(src_x, src_y, src_w, src_h).exists(Pattern(src_pic).similar(0.80), 0)
            dst_pic_obj = Region(dst_x, dst_y, dst_w, dst_h).exists(Pattern(dst_pic).similar(0.60), 0)

            if src_pic_obj && dst_pic_obj
              dragDrop(src_pic_obj, dst_pic_obj)
              translate("login2.png", 0.8)
              return
            end

        end
      end
    end

    translate("reload.png", 0.8)

  end
end

captcha()


 Sikuliを利用する上での注意点
Sikuliの実行環境では画像用に変数を利用するとエラーが出た。

コード内での画像変数利用例。
src_pic = "src_#{color}_#{figure}#_{rotation}.png"

静的に画像の有無を確かめているのだろう。
今回のように動的に画像を変更する場合はコンソールにエラーが出るが問題ない。
[error] Image: could not be loaded from C:/Documents and Settings/test.sikuli/src_

ただし、完全に変数にすると、
src_pic = "#{color}_#{figure}_#{rotation}.png"
というエラーがでて、ファイルがオープンできない、実行できない状態となった。
[error] EditorPaneTransferHandler: importData: Problem pasting text null

このあたり、次のバージョンでは改善してほしいところである。