JPHACKSに参加した話(プロダクト紹介)
12/09追記
JPHACKS 2020 Advent Calendar 2020が生えていたのでこの記事の公開日である4日に入れておきます。
JPHACKSとは
結構大きめのハッカソンイベントです。2020年は以下のように開催されました。
- 10/31~11/6日の間に開発、7日に発表
- 決勝に進む上位16チームが13日に発表され、27日までさらに開発を進め、28日に発表
- フルリモートで開催
僕は現在elabという研究室に所属して、かつGiveryでバイトをしているのですが、elabの教授である江崎先生とGiveryはこのJPHACKSの主催です。参加しない理由が無いといういう理由で、研究室の同期を誘って出場しました。
この記事は、僕たちelab4b(elab for beginners)が作成した「SATORI」というプロダクトについてです。JPHACKSでの流れとかエピソードとかそういうところも書きたいところですが、文章量が異常になりそうなため、プロダクトについてちょこっと(ちょこっととは言っていない)紹介するだけに留めます。
SATORIについて
オンライン試験に特化したカンニング対策プラットフォームです。COVID-19の影響で多くの大学生がオンラインで試験を受けた経験があると思います。人によっては大学院院試がオンラインであったり、TOEFLを始めとした資格試験、コーディング試験などもオンライン試験として挙げられます。ただ、これらの試験には問題点があり、特に大学の試験についてはTwitterでも一時期話題になったことでしょう。
大学でのオンライン試験や院試の問題点
例えば、僕らの大学で行われたオンライン試験の一部では、以下のような環境での試験でした。
- 全員バーチャル背景をOFFにした状態でZOOMでカメラON
- カメラは手元/横顔/PC画面が映るような横からの視点でZOOMに参加する
- マイクON/OFFは試験によって異なる
- ZOOMは録画されます
受験者の立場では、このようなオンライン試験では以下のような問題点があると考えられます。
- カンニングが対面での試験以上に行いやすい環境での試験であったこと
- プライバシーの問題。部屋が他の人に見られるのが嫌だ。
- マイクONの場合、咳払いがブロードキャストされるのがすごい嫌だ、という人もいました。
- 大人数が同時にZOOMでカメラをONにすることで、ネットワーク負荷が非常に大きくなる
- 僕の実家は回線が非常に弱く、1:1でも両者がカメラONにするとダメです。多分そこで試験を受けるのは厳しいでしょう。
- 4月頃とかはZOOM使っているとブラウザが重くなるっていう現象起きませんでしたか?僕の環境では起こっていたのですが、これが試験中に行われると試験の続行が怪しいレベルでつらいです。
また、試験監督の立場だと、以下のような問題があります。
- 対面の試験に比べて、カンニング行為の検出が難しい
- 試験後に不正が無いか見直せるのはオンライン試験ならではの利点だが、見る量多すぎて無理
TOEFLの問題点
専用ソフトをインストールします。リモートアクセスを許可するソフトのため、試験官が自分のPCを操作します。これが不快だというチームメンバーがいました。僕も聞いていて正直もう少しマシな方法が無いものかと思いました。
また、この方法を運用するには多くの人数を試験官として割り当てる必要があります。
コーディング試験の問題点
現在ほとんどのコーディング試験がカンニング対策をしていないでしょう。もちろん出題される問題には一定の新規性があり、同じ問題を探して答えをコピペ、みたいなことはほぼ不可能です。では、以下のシナリオはどうでしょう?
- 受験者Aさんが別のサービスのチャット機能を利用し、十分に実力のあるBさんに「競プロ勉強中なのですが、この問題はどうやったら解けますか?」みたいなことを聞きます。
- Bさんは勉強熱心なAさんに感心し、その問題の解き方や実際にACするコードを返信します。
- Aさんはシメシメとそのコードをコピペして無事ACします。
このようなことは無いと言い切れますか?現在既にWebテストは替え玉で受験されることがある1ことを考えると、オンライン試験でも同様のことが起こるのは時間の問題だと思います(し、もう起こっているかもしれません)
SATORIの目標
では、上に挙げた問題点を全て克服できるようなツールは作成可能でしょうか?SATORIはそれらの多くを克服したプロダクトです。これからさらに良くなっていくかもしれません。
競合について
実は結構最近出ています。以下のようなものが割と検索でヒットするイメージです。
これらとの大きな違いは「処理をローカルで行って必要最低限のデータのみサーバーに送る」ため、「プライバシーが最大限守られる」「ネットワーク負荷がかからない」ことです。詳細を詳しく調べ切れてはいませんが、例えばCheckPointZはあらゆる操作を記録するみたいですので、恐らくキーロガーも導入することになるでしょう。また、SATORIにも通じるところがありますが、いずれの手法も何かしらの抜け道は存在すると思った方がよいです(開発者がそれを言うのか)。ここに挙げている手法すら対策されていない可能性もあります。
SATORIの構成要素
ざっくり現在の技術スタックは以下のようになっています。
electronとして動作するクライアントが様々な処理を行い、アラートデータのみをAPIサーバーへ送信、DBに保存します。試験監督は管理画面にアクセスすることで統計情報を取得することが出来ます。
正直本体がクライアントで、Web側はチームの1名に任せてしまったので申し訳ない...と思いつつクライアントの方に手を伸ばしていました。そこまで強くWebサイドに触れていないということもあり、この記事ではクライアントサイドのみに焦点を当てていこうと思います。
SATORIの検出能力と技術
さて、そもそもオンライン試験でのカンニングとしてどのような行為が考えられるでしょうか?そしてそれは「カンニング行為として検出すべきか」についても同時に考えていきます。
スマホを含む別端末や本の参照
対面だと一瞬でバレるタイプのやつです。一応類似する手法として「他の人の回答を見る」も存在しますが、オンライン試験を考えた時にこのケースは通常存在しないでしょう。
これらの動作の特徴としては、「一定以上の時間、パソコンの画面から目を離す」という点があります。つまり視線検出や顔向き検出が役に立ちそうです。現時点ではOpenCVにあるFacemark APIをJSで利用し、顔の特徴点から顔向き推定を行っています。結構簡易的ですが、「一定時間以上横方向を向いていた場合、怪しいと考える」としています。
想定される問題点として、「複数の画面を利用している」「Webカメラの位置決め打ちでいいのか」があります。前者については大学院入試で「複数の画面の利用を禁止する」という運用がなされていたため、それを活用(複数画面があると警告が出るようになっています)しています。後者については、「1画面であることが担保される状態で、ノートPCも含めWebカメラの位置はほぼ同じだろう」というやや強めの仮定を置いてしまっているため、運用でカバーor改善が必要です。
なお、上に挙げている競合は全てこれは実装していると思われます。また、この検出は対面でのテストでも使えるという汎用性を持っています。
チャット機能の利用
コーディング試験で例に挙げていたものもここに入ります。僕らの考えた「取り出すべき行動の特徴」として、以下の2つが挙げられました。
- チャット機能を利用するためには必ず別のウィンドウ(ブラウザならタブ)を利用してチャット画面を開く必要がある
- 貰った情報を利用するために、特にコーディング試験ではコピー&ペーストをする可能性が高い
これらのことから、「アクティブウィンドウの変化の検知」「クリップボードの内容変化の検知」を行っています。electronに入れるということでnpmからclipboardyとactive-winを引っ張ってきて、利用しています。本当はイベント検知出来たほうがいいんでしょうけど...
const activeWin = require('active-win'); // 現在アクティブなウィンドウのウィンドウタイトルを取得。この方法ならブラウザのタブ変化も検知可能です const current_active_app_title = (async() => { active_app_title = (await activeWin()).title; })();
const clipboardy = require('clipboardy'); // 現在のクリップボードの内容を取得 const current_clipboard = clipboardy.readSync();
これの結果、「あらゆるブラウザの検索」等も検知できてしまいますが、それをカンニングとみなすかどうかは試験によって異なってくると思います。例えばTOEFLならネット辞書使えるのでアウト、コーディング試験ならAPIとか言語仕様とか調べていいでしょ、みたいな。ここは運用に任せたいかなと思っている箇所です。
(説明能力を考えるとこのアラートログから実際の挙動をシミュレーション出来ると望ましいでしょう)
会話
カメラから見える受験者は普通に受験しているようにも見えます。ただし、受験者は画面外の誰かと会話をしていて2、受験者がヒントや答えを得ている、といったパターンです。
テスト前に受験者の音声データを録音し、その人と同一人物の音声か判定する、ということをしています。
ここがかなり闇で、「speaker dialization」という問題に分類されそうです。ケプストラムの類似度を取ってクラスタリングみたいなことをすることを考えると、RNNが一般的な手法として挙がります。元々「ローカルで実行する」という都合SVMを使って頑張っていたのですが、精度が出ないのでpyanote-audioを頼ってみたりしました。結構微妙な箇所です。
学習における特徴として、学習データを極限まで削っているということが考えられます。これは「テスト前にユーザーの声紋を録る時間が長いと体験がよくない」ということがあり、日本語においてケプストラムをクラスタリングすることを考えると各母音を平均化した曖昧母音を学習データにするとよいらしい(他のメンバー談)のですが、これを得るための最低情報は「あいうえおを各5秒間喋ってもらう」が考えられ、現時点ではこの手法を採用しています。
このモジュールはPythonで動くため、electronから別プロセスを立てて動かす必要があります。python-shellを利用して、Python Programを立てています。
const ps = require('python-shell'); const pyshell = new ps.PythonShell(script_path); pyshell.on('message', (message) => { // pythonでのstdoutがmessageに渡される });
なりすまし
そもそも別の人が受験しているパターン。さて、どのようなケースが考えられるでしょうか。
- そもそも現地で受験者が入れ替わるパターン。対面試験なら学生証チェックで落とせるやつです。これはdlibを使って検出が出来ています。3これも競合は全て実装していると思われます。
- 受験者がsshサーバーで受験を行い、外部から他の人がリモートアクセスすることで他の人が試験問題を解く場合。このように、監督側には正規の受験者が受験しているように見えますが、実際には他の人が問題を解いている場合を今後「実質的ななりすまし」と呼んでいきます。
- sshならプロセス監視すればそれっぽい名前がヒットするでしょう。systeminformationを使うと分かります。
- 一般的なリモートアクセスの場合、GUIでの操作になることが想定されます。実際に検証してみたところ、そのプロセスの通信量は1MB/s前後になることが多いです。この数値は試験中であることを考慮するとかなり大きい(ZOOMや動画視聴に相当するため)可能性が高いため、パケットキャプチャーを行ってリアルタイムにポート毎の通信量を測定します。閾値を超えた場合、該当ポートからプロセスを割り出す、みたいなことをしていますが、例えばChrome Remote Desktopの場合はUDPクライアントになっているのか現時点でポートからプロセスが割り出せないみたいです。
パケットキャプチャーについてはcapを利用してます。nodeでもパケットキャプチャー出来るんだ!と見つけた瞬間これでやってしまいました。結局Mac/Linux環境ではtcpdumpにroot権限が必要なため、electronとプロセスを分ける方が望ましいです。今回はsudo-promptを利用して、直接nodeを実行しています。
const AsyncLock = require('async-lock'); const Cap = require('cap').Cap; const decoders = require('cap').decoders; const PROTOCOL = decoders.PROTOCOL; const c = new Cap(); const filter = ""; const bufSize = 10 * 1024 * 1024; const buffer = Buffer.alloc(65535); const linkType = c.open(device.name, filter, bufSize, buffer); c.setMinBytes && c.setMinBytes(0); // UDP/TCP問わず、port毎にネットワークトラフィックを記録していきます const lock = new AsyncLock({ timeout: 1000 }); let portTraffics = {}; c.on('packet', (nbytes, _) => { if (linkType === 'ETHERNET') { const eth = decoders.Ethernet(buffer); const internet = eth.info.type === PROTOCOL.ETHERNET.IPV4 ? decoders.IPV4(buffer, eth.offset) : eth.info.type === PROTOCOL.ETHERNET.IPV6 ? decoders.IPV6(buffer, eth.offset) : null; if (internet === null) { return; } const saddr = internet.info.srcaddr; const daddr = internet.info.dstaddr; if (saddr !== ipAddr && daddr !== ipAddr) { return; } const trans = internet.info.protocol === PROTOCOL.IP.TCP ? decoders.TCP(buffer, internet.offset) : internet.info.protocol === PROTOCOL.IP.UDP ? decoders.UDP(buffer, internet.offset) : null; if (trans === null) { return; } const sport = trans.info.srcport; const dport = trans.info.dstport; lock.acquire("portTraffics", () => { if (saddr === ipAddr) { portTraffics[sport] = portTraffics[sport] ? portTraffics[sport] + nbytes : nbytes; } else { portTraffics[dport] = portTraffics[dport] ? portTraffics[dport] + nbytes : nbytes; } }); } });
今後の課題: そもそも与えられる情報は本当なのか?
実用化を踏まえたうえで考えなければならない大きな問題として、「与えられる情報は本当か否か考える必要がある」というものがあります。
例えばWebカメラだと「監督から見ると普通に受験しているように見える。ただし、実際には前日に撮影した動画をカメラとして流し込んでいる」みたいなことがありえます。こうするだけで持ち込みや別端末を利用したカンニングが可能になるわけです。
また、このソフトがVMにインストールされた場合どうなるでしょうか。当然VMの外部へのアクセスは基本的には出来ません。そのため、ホスト側で自由にカンニングが出来てしまうため、自身がVMにインストールされていないかどうか調べる必要があります。
デバイス情報やキーロガー、パケットキャプチャーについても同様で、OS側の処理に割り込むことで騙すことが出来そうです。例えばパケットキャプチャーについては、優先度がどうなるか不明ですが、BPFを用いてtcpdumpよりも前にパケットデータを取り出して、tcpdumpにデータを渡さない(DROPすれば出来る?)、みたいなことが出来れば理論上は検証を騙せる可能性があります。この辺あまり詳しくないですが。
ただ、共通点としてこれらのことには行った痕跡が存在します。例えば疑似カメラなら「カメラの詳細情報を調べると実在しないものである」であったり、VMなら「MACアドレス等ハードウェア情報がVM固有のものである」等。このような手法についても考察と対策を深め、同時に新しく想定される手法を考えていく必要があります。