ソフトウェア

ADHD検査アプリをリバースエンジニアリングした結果


自分が注意欠如多動症(ADHD)なのではないかと考えたveritas氏は受診した精神科医からオンラインのADHD検査を受けるように指示されました。検査で受けた奇妙な体験と技術的探求からveritas氏はADHD検査のウェブアプリをリバースエンジニアリングし、詳細をブログにて公開しました。

Reverse Engineering My ADHD Test
https://nullpt.rs/reverse-engineering-adhd-test

veritas氏は「突発的に始めた趣味が長続きしない」という自分の傾向を性格のせいだと思っていましたが、友人が自閉症と診断されたことをきっかけにADHD検査受けることを検討しました。


精神科医を受診したところ、オンラインのADHD検査を受けるように指示されました。検査が始まったら何が起こるのか分からないまま、veritas氏はメールで送られてきた「検査開始」リンクをクリックしたところ、2000年代初頭のFlashゲームのような画面が表示された空白のページに飛びました。タスクは単純なもので、「赤いハートが3つ表示された『ハートのエース』のカードが表示されたらスペースキーを押す」「それ以外の場合は何もする必要がない」というものでした。


指示通りにスペースキーを押していたもののタイマーもスコアも表示されず、ただ点滅するカードだけが表示されていました。やがて、気をそらそうとしているかのように他のクリップアートがカードの横に現れ始め、ついには赤ちゃんの泣き声やパトカーのサイレンの音が混ざり始めました。veritas氏にはタスクが永遠に終わらないように感じられ、果てしなく続く試験を終わらせようとスペースキーをひたすら連打したそうです。


ADHD検査の画面を録画したものの一部がこちら。


検査の途中で好奇心が湧いたveritas氏は「ページのソースコードと送信リクエストをすべて取得する」という妙案を思き、検査を受けながらDevToolsを開いてすべてのページ・スクリプトを保存しました。検査終了後、保存したスクリプトの調査を開始しました。まずはスペースキーの押下をどのように処理しているかを確認しました。

$(document).keyup(function (evt) {
    keyPressed = false;
    Context.LastPressTime = Context.CurrentTime;
    if (evt.keyCode == 32 || evt.which == 32) {
        Context.spacePressArray.clickTimes.push(adjustedTime);
        console.log('Pushing Space: ' + Context.CurrentTime + ', Adjsuted Time: ' + adjustedTime);
    }
    else if (evt.keyCode != 32 && evt.keyCode != 13 ) {
        Context.spacePressArray.badClickTimes.push(adjustedTime);
        console.log('Pushing Bad Press: ' + ', Adjsuted Time: ' + adjustedTime);
    }
}).keydown(function (evt) {
    if (!keyPressed) {
        adjustedTime = Context.CurrentTime - Context.Offset;
    }
    keyPressed = true;
})

コードを確認すると明らかにスペースキー押下だけではなく「間違った」キー、つまりスペースキー・エンターキー以外のキー押下も追跡されていました。veritas氏はさらに別のフックも見つけました。

onBlur = function (present_message){
    present_message = (typeof present_message !== 'undefined' ? present_message : true);
    if (!Context.canLooseFocus) {
        Context.LastinFocusTime = Date.now();
        InnerLog.log('Last in Focus Time: ' + Context.LastinFocusTime + ', Last Test Time in Focus: ' + Context.CurrentTime);
        Context.InFocus = false;
        adhd.prototype.addBeep();
        if (present_message) {
            this.outOfFocusAlert.htmlElement.style.visibility = "visible";
        }
    }
};

$(window).focus(function () {
    if (!Context.canLooseFocus) {
        this.outOfFocusAlert.htmlElement.style.visibility = "hidden";
        Context.LostFocusAggregateTime += (Date.now() - Context.LastinFocusTime);
        Context.InFocus = true;
        Context.spacePressArray.lostFocusAggregateTime = Context.LostFocusAggregateTime;
        adhd.prototype.removeBeep();
        InnerLog.log('Aggregate Not in Focus Time: ' + Context.LostFocusAggregateTime + ", at: " + Context.CurrentTime);
        if (Context.LostFocusAggregateTime < 0 || Context.CurrentTime < 0 || Context.startTime < 0) {
            alert(Context.MessageSomethingWentWrong);
        }
    }
});

上記のコードは「検査アプリがフォーカスを失ったとき」、つまり新しいタブを開いたときなどを検出しています。

興味深い点として、被験者が大人か子供かによってアセットが切り替わることも分かりました。子供は15FPS、大人は24FPSに制限されていました。また子供には鳥の鳴き声・ボウリングの音・サーベルの音・飛行機の音が聞こえ、大人には赤ちゃんの泣き声・車の衝突音・パトカーのサイレンの音が聞こえるようになっていました。

if (ageGroup == 'adults') {
    properties = lib.properties_adults;
}
else {
    properties = lib.properties_children;
}

lib.properties_children = {
    width: 800,
    height: 600,
    fps: 15,
    color: "#666666",
    manifest: [
        {src:"/js/adhd-html5/sounds/Birds.mp3", id:"Birds"},
        {src:"/js/adhd-html5/sounds/Jedi.mp3", id:"Jedi"},
        {src:"/js/adhd-html5/sounds/Bowling.mp3", id:"Bowling"},
        {src:"/js/adhd-html5/sounds/Gong.mp3", id:"Gong"},
        {src:"/js/adhd-html5/sounds/Plane.mp3", id:"Plane"},
        {src:"/js/adhd-html5/sounds/Plane68.mp3", id:"Plane68"},
        {src:"/js/adhd-html5/sounds/SaberSmall.mp3", id:"SaberSmall"},
        {src:"/js/adhd-html5/sounds/PlaneBoom105.mp3", id:"PlaneBoom105"},
        {src:"/js/adhd-html5/sounds/SaberOff3.mp3", id:"SaberOff3"},
        {src:"/js/adhd-html5/sounds/Saber111.mp3", id:"Saber111"},
        {src:"/js/adhd-html5/sounds/Saber111copy.mp3", id:"Saber111copy"},
        {src:"/js/adhd-html5/sounds/Saber211.mp3", id:"Saber211"},
        {src:"/js/adhd-html5/sounds/Saber211copy.mp3", id:"Saber211copy"},
        {src:"/js/adhd-html5/sounds/Saber33.mp3", id:"Saber33"},
        {src:"/js/adhd-html5/sounds/Saber33copy.mp3", id:"Saber33copy"},
        {src:"/js/adhd-html5/sounds/SaberOnLong3.mp3", id:"SaberOnLong3"},
        {src:"/js/adhd-html5/sounds/SaberOnLong3copy.mp3", id:"SaberOnLong3copy"},
        {src:"/js/adhd-html5/sounds/beep-01a.mp3", id:"Beep"}
    ]
};

lib.properties_adults = {
    width: 800,
    height: 600,
    fps: 24,
    color: "#999999",
    manifest: [
        {src:"/js/adhd-html5/sounds/arsp018wav.mp3", id:"arsp018wav"},
        {src:"/js/adhd-html5/sounds/assp022wav.mp3", id:"assp022wav"},
        {src:"/js/adhd-html5/sounds/assp023wav.mp3", id:"assp023wav"},
        {src:"/js/adhd-html5/sounds/babyCry.mp3", id:"babyCry"},
        {src:"/js/adhd-html5/sounds/car_brakes.mp3", id:"BIKE2WAV"},
        {src:"/js/adhd-html5/sounds/bottle_pop_2.mp3", id:"bottle_pop_2"},
        {src:"/js/adhd-html5/sounds/carbrake01wav.mp3", id:"carbrake01wav"},
        {src:"/js/adhd-html5/sounds/chsp016wav.mp3", id:"chsp016wav"},
        {src:"/js/adhd-html5/sounds/copcar.mp3", id:"copcar"},
        {src:"/js/adhd-html5/sounds/DogBarking02wav.mp3", id:"DogBarking02wav"},
        {src:"/js/adhd-html5/sounds/flasher_coat_mono.mp3", id:"flasher_coat_mono"},
        {src:"/js/adhd-html5/sounds/pouringliquid1.mp3", id:"pouringliquid1"},
        {src:"/js/adhd-html5/sounds/arguing_couple.mp3", id:"arguing_couple"},
        {src:"/js/adhd-html5/sounds/barking_dog.mp3", id:"barking_dog"},
        {src:"/js/adhd-html5/sounds/police_car.mp3", id:"police_car"},
        {src:"/js/adhd-html5/sounds/baby_crying.mp3", id:"baby_crying"},
        {src:"/js/adhd-html5/sounds/smoking.mp3", id:"smoking"},
        {src:"/js/adhd-html5/sounds/beep-01a.mp3", id:"Beep"}
    ]
};

最終的に集計されたデータには「スペースキーを押したタイミング」「誤ったキーを押してしまったタイミング」「ウィンドウがフォーカスを外れていた合計時間」しか含まれておらず、採点モデルはソースコード上には見当たりませんでした。検査アプリの公式サイトを見て回ったところ、採点方式は以下の4つの注意指標に基づいているとのこと。

・Attentiveness(A・注意力):指示通りに刺激を評価し反応する能力、注意を払ったり特定の詳細に気づいたりすることに困難を抱える場合に影響
・Timeliness(T・適時性):与えられた時間枠内で正しく反応する能力、環境の変化にタイムリーに反応したり時間制限のあるタスクをこなしたりすることに困難を抱える場合に影響
・Impulsiveness(I・衝動性):「禁止」されている時点で反応してしまう傾向、状況や行動の結果を考慮せずに衝動的に行動してしまう場合に影響
・Hyper-Reactivity(H・過剰反応):運動出力を効率的に調整したり不必要な行動を控えたりすることの困難さ、過剰もしくは不正確で望ましくない反応を伴う

検査結果はリバースエンジニアリングに備えてDevToolsを開いたためか、精神科医に「試験で不正行為をしようとしたことはありますか?」と尋ねられるほど通常の範囲をはるかに逸脱したものでした。疑いを否定するとveritas氏は再検査を受けることになりました。再検査では集中力を保ち、スペースキーを連打しないように注意を払ったとのこと。

再検査後、「精神科医の視点から見て、このテストはどのように評価されているのだろうか?」との疑問からソースコードをさらに詳しく調べていると、興味深い関数が見つかりました。

function sendParentReportToClinic(testId) {
	var clinicEmail = $('#clinicEmail').val();
	var data = {"test_id": testId, "clinic_email": clinicEmail};
	$.ajax({
		type: 'POST',
		async: true,
		dataType: 'json',
		data: data,
		url: location.protocol + '//' + location.host + '/api/tests/send_parent_report',
		success: function (data) {
			alert(data.data);
		},
		error: function(error) {
			var errorText = $.parseJSON(error.responseText).message;
			alert(errorText);
		}
	});
}

上記の関数は検査結果をクリニックに送信するものであり、必要なパラメーターは「クリニックのメールアドレス」「テストID」の2つだけです。バックエンドの処理がずさんなら、クリニックのアドレスとして任意のメールアドレスを指定し、自分のテストID(または任意のテストID)を入力すれば、レポートを自分自身に送信させることが可能だとveritas氏は考えました。そこで、リクエストをcURLコマンドに変換し実際に試してみました。

curl -X POST 'https://adhd-test/api/tests/send_parent_report' \
   -H 'Accept: application/json' \
   --data-urlencode 'test_id=1010101' \
   --data-urlencode '[email protected]'

コマンドを実行したところ、レポートではなくログインページのHTMLがレスポンスに含まれていました。ブラウザでエンドポイントにアクセスするとサインアップボタンが表示されました。サインアップに必要なのはメールアドレス・パスワード・名前・電話番号といった基本的な情報だけでした。プロバイダーが製品をテストするためのトライアルアカウントを作成できるようにしてあったためveritas氏はトライアルでサインアップし、ダッシュボードに移動しました。

トライアルでは2回の無料検査を受けることができたので、veritas氏は検証計画を立てました。1回目は最初の検査と同様にスペースキーを連打する検証、2回目は再検査と同様に普通に検査を受ける検証です。自分をクライアント「Null Ptr」として登録し検査招待状を送信した後、計画した通りに実行しました。するとすぐにレポートがダッシュボードに表示され、確認できる状態になりました。


スペースキーを連打する検証の結果、レポートには「信頼性が低い」と記されており、精神科医が不正行為を疑った理由が判明しました。また、標準値との比較グラフを見ても何かがおかしいのは明らかでした。


普通に検査を受ける検証の結果は概ね普通であり、信頼性についても問題ないものでした。


標準値との比較も概ね一致していたものの、唯一目立ったのは衝動性が平均よりも高いと判定されたことでした。


レポートは最後に以下の要約を提供していました。

According to comparisons to age and gender matched norms a significant deviation from the norm was detected in Null Ptr's I performance. This performance pattern may indicate attentional difficulties, and taken together with other findings, an increased likelihood for the existence of ADHD.


つまり、判定モデルは年齢と性別が一致する基準値と比較した上で、veritas氏の衝動性の高さからADHDである可能性が高いと予測しました。

精神科医の再診で正式な検査結果が出るのを待つ間、友人のHaze氏にも同じ検査を受けてもらいました。


18分間スペースキーを押し続けた結果、友人は以下の通り「正常」と判定されました。

According to the norm comparison table there is a low probability that Haze. has attention difficulties.


再診の際、veritas氏は精神科医からADHDと診断され衝動性が高いと言われました。驚きとともに、もっと早く気づくべきだったとveritas氏は述懐しています。veritas氏はADHDの診断が全ての行動を説明するものではないとしつつも、自身の行動を理解する助けになると述べています。

ブログはveritas氏が突発的に始めた趣味である天体望遠鏡で撮影された月の写真で締めくくられています。

・関連記事
子どもに「ADHDに関連する遺伝的傾向」があると両親が離婚しやすくなる可能性 - GIGAZINE

ヒトゲノムで起こり得る約90億通りの変異による影響を予測したデータベース「AlphaGenome Atlas」をGoogle DeepMindが発表 - GIGAZINE

「図形の描き方」でパーキンソン病を99%の精度で識別できる可能性 - GIGAZINE

赤ちゃんの声に母親が「1秒以内」に応じる割合は小児期のADHD診断と関連する可能性 - GIGAZINE

AIエージェントが24時間体制で本番障害の一次調査を自動化、人間の役割は「判断」のみに - GIGAZINE

健康的な食事をしているADHDの青少年は集中力の問題が少ないとの研究結果 - GIGAZINE

業務監視ツールや評価AIが病院に導入されたせいで患者へのケアが悪化していると看護師らが主張 - GIGAZINE

Googleの「AMIE」などのAI医療ツールが診断や治療判断で人間の医師と同等またはそれ以上のパフォーマンスを示す - GIGAZINE

in ソフトウェア,   ネットサービス, Posted by log1c_sh

You can read the machine translated English article The results of reverse engineering an AD….