JavaScriptとPHPでの大量の処理の違いに基づくボット保護

最近、かなり人気のあるいくつかのリソースで使用されているボット保護に対処する必要がありました。

一見、保護はjavascriptを介したCookieの通常の設定であるように思われ、15分で対処できます。 実際、少し調べてから、どこで何が行われ、どのパラメーターがどこに渡されるかが明らかになりました。小さな関数をjavascriptからphpに書き換えるだけで、問題は解決しません。

しかし、それはそれほど単純ではありませんでした。 そして、最終的には保護が破られましたが、15分もかかりませんでした。保護の原理自体は新しく、私にとって非常に興味深いものでした。



だから、まず最初に。



表面検査



保護は次のように機能します。

index.phpサイトのメインページのスクリプトは、訪問者のIPアドレスから計算されたハッシュがパラメーターの1つに含まれるCookieを想定しています。

Cookieが送信されない場合、index.phpは、必要なパラメーターを計算するjavascriptコードを含む別のページに訪問者をリダイレクトし、Cookieに書き込み、メインページに戻ります。



通常のphpボットがCURLを介してGETおよびPOST要求を実行してそのような保護を通過できるようにするには、ハッシュ計算をjavascriptからphpに書き換えてから、必要なcookieを要求ヘッダーに追加する必要があります。



検死



より詳細に。

Firefoxを起動し、javascriptを無効にし、Firebugを有効にします。

メインページのindex.phpをリクエストし、リクエストおよびレスポンスヘッダーを確認します。



リクエスト:



ゲット
  http://example.com 




このリクエストのヘッダーは、私たちには関係ありません。

また、応答ヘッダーは次のとおりです。



ステータス:302一時的に移動



接続キープアライブ

コンテンツタイプテキスト/ html

日付XXX GMT

場所
  http://example.com/govalidateyourself#98765:1234:11.22.33.44:/index.php 


サーバーYTS / 1.20.0

転送エンコードチャンク





その後、Firefoxはヘッダーで指定された場所に自動的に移動し、次の応答ヘッダーを受信します。



Accept-Rangesバイト

接続キープアライブ

コンテンツタイプテキスト/ html; 文字セット= utf-8

日付XXX GMT

最終変更日YYY GMT

サーバーYTS / 1.20.0

Set-Cookie addr = 1234:11.22.33.44; パス= /

転送エンコードチャンク





11.22.33.44は私のIPアドレスであり、1234は計算ロジックが不明な数値です。



ページ自体には、jsコードへのリンクが含まれています
  http://example2.com/validator/va.js 
碑文「No javascript」。

jsがなければ、彼らは私たちにそれ以上先へ進ませません。



すべての応答要求が記録されたら、javascriptをオンにし、Cookieをクリアして、もう一度やり直します。

ここで、検証ページのリクエスト後に何が起こるかに興味があります。



今回はサイトのメインページが読み込まれます。最後のリクエストのタイトルは次のとおりです。



テキスト/ html、application / xhtml + xml、application / xmlを受け入れる; q = 0.9、* / *; q = 0.8

Accept-Encoding gzip、deflate

Accept-Language ru-ru、ru; q = 0.8、en-us; q = 0.5、en; q = 0.3

接続キープアライブ

Cookie addr = 5678:11.22.33.44; 尿= aabbccdd; v = 1

ホストexample.com

リファラー
  http://example.com/govalidateyourself 


Firefoxのユーザーエージェント





今回の最後のサーバー応答からの定数1234は5678に変更され、IPアドレスは同じままでした。 どうやらこれは、サーバーによって割り当てられ、Cookieに保存されている要求IDです。 さて、あなたはそれを保存し、リクエスト中に変更せずにクッキーに書き込む必要があります。



しかし、urine = aabbccddパラメーターはすでに興味深いものです。 サーバーから送信されたのではないので、それは私たちから受信したことを意味し、これがva.jsの仕事であることがわかります。



中身を確認する時間です。 一見したところ、完全な沼地、それは入らない方が良い:



if(document.cookie==""){document.write("Cookies error")}else{function poo(a,b){var c=a.length,d=b^c,e=0,f;while(c>=4){f=a.charCodeAt(e)&255|(a.charCodeAt(++e)&255)<<8|(a.charCodeAt(++e)&255)<<16|(a.charCodeAt(++e)&255)<<24;f=(f&65535)*1540483477+(((f>>>16)*1540483477&65535)<<16);f^=f>>>24;f=(f&65535)*1540483477+(((f>>>16)*1540483477&65535)<<16);d=(d&65535)*1540483477+(((d>>>16)*1540483477&65535)<<16)^f;c-=4;++e}switch(c){case 3:d^=(a.charCodeAt(e+2)&255)<<16;case 2:d^=(a.charCodeAt(e+1)&255)<<8;case 1:d^=a.charCodeAt(e)&255;d=(d&65535)*1540483477+(((d>>>16)*1540483477&65535)<<16)}d^=d>>>13;d=(d&65535)*1540483477+(((d>>>16)*1540483477&65535)<<16);d^=d>>>15;return d>>>0}function coo(a){var b=a+"=";var c=document.cookie.split(";");for(var d=0;d<c.length;d++){var e=c[d];while(e.charAt(0)==" ")e=e.substring(1,e.length);if(e.indexOf(b)==0)return e.substring(b.length,e.length)}return null}var dt=new Date,expiryTime=dt.setTime(dt.getTime()+1000e5);var dt2=new Date,expiryTime=dt2.setTime(dt2.getTime()+2e4);var addr=window.location.hash.split(":")[2];var a=poo(addr,47).toString(16);for(var i=0,z="";i<8-a.length;i++)z+="0";a=z+a;a=a.substring(6)+a.substring(4,6)+a.substring(2,4)+a.substring(0,2);var refurl=window.location.hash.split(":")[3];document.cookie="urine="+a+"; expires="+dt.toGMTString()+"; path=/";if(!coo("v")){document.cookie="v=1; expires="+dt2.toGMTString()+"; path=/";setTimeout("window.location = refurl",300)}else if(coo("v")<3){var c=coo("v");c++;document.cookie="v="+c+"; expires="+dt2.toGMTString()+"; path=/";setTimeout("window.location = refurl",300)}else if(coo("v")>=3){document.write("Too many redirects from: "+document.referrer)}}
      
      







しかし、少しの忍耐、そしてフォーマット後にすべてが読みやすく、非常に理解しやすいように見えます。

2つの関数coo()とpoo()、および必要なCookieを書き込み、index.phpに送り返すコードがあります。



関数co()は特に重要ではありません。指定されたパラメーターの値をcookieから受け取り、単純な正規表現でphpに簡単に書き込まれます。



そして、ここにpoo()関数があり、これは尿パラメーターを考慮します:



 function poo( a, b ) { var c = a.length, d = b^c, e = 0, f; while( c >= 4 ) { f = a.charCodeAt( e ) & 255 | ( a.charCodeAt( ++e ) & 255 ) << 8 | ( a.charCodeAt( ++e ) & 255 ) << 16 | ( a.charCodeAt( ++e ) & 255 ) << 24; f = ( f & 65535 ) * 1540483477 + ( ( ( f >>> 16 ) * 1540483477 & 65535 ) << 16 ); f ^= f >>> 24; f = ( f & 65535 ) * 1540483477 + ( ( ( f >>> 16 ) * 1540483477 & 65535 ) << 16 ); d = ( d & 65535 ) * 1540483477 + ( ( ( d >>> 16 ) * 1540483477 & 65535 ) << 16 )^f; c -= 4; ++e } switch( c ) { case 3: d ^= ( a.charCodeAt( e + 2 ) & 255 ) << 16; case 2: d ^= ( a.charCodeAt( e + 1 ) & 255 ) << 8; case 1: d ^= a.charCodeAt( e ) & 255; d = ( d & 65535 ) * 1540483477 + ( ( ( d >>> 16 ) * 1540483477 & 65535 ) << 16 ) } d ^= d >>> 13; d = ( d & 65535 ) * 1540483477 + ( ( ( d >>> 16 ) * 1540483477 & 65535 ) << 16 ); d ^= d >>> 15; return d >>> 0 }
      
      







呼び出し中に、次のパラメーターがそれに転送されます。



 var a = poo( addr, 47 ).toString( 16 );
      
      







a-これは、尿パラメーターの既製の値です(8文字未満の場合にのみゼロが埋め込まれます)。

addrはIPアドレス11.22.33.44です。

47は定数です。



これですべてが明らかになりました。

この保護を突破するphpボットは、次のアルゴリズムに従って動作するはずです。



1. GETリクエストを行う
  http://example.com/index.php 
応答ヘッダーを受信するオプションを設定します。



 curl_setopt( $ch, CURLOPT_HEADER, 1 );
      
      







そして同時に、リダイレクトの場合に自動遷移をオンにします:



 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
      
      







この場合、curlは新しい場所自体に移動するため、2番目のリクエストをプログラムする必要はありません。 そして、両方の応答のヘッダーを取得します。最初のヘッダーはLocationで、2番目のヘッダーはリクエストIDを含む最初のCookieです。



2.ヘッダーを解析し、リクエストIDとIPアドレスを取得します(別のトリックを使用する場合、すぐにはわかりませんが、ここではお勧めします-非常に便利です)。

尿パラメーターを読み取り、Cookieに書き込み、index.phpに新しいGETリクエストを送信します。 保護が通過しました。



Cookieは次のように記述されます。



 $headers = array( "Cookie: " . $cookie_str, // "addr=5678:11.22.33.44; urine=aabbccdd; v=1" /*    / */ ); curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
      
      







だから、最後のタッチが残っている-尿の計算。



すくい



poo()関数をphpに書き換えるだけです。

始めるには、少しグーグルで、phpにないjs関数と演算子のペアの類似物を書きます。



 // php js functions function charCodeAt( $str, $i ) { return ord( substr( $str, $i, 1 ) ); } // char at function charAt( $str, $i ) { return $str{ $i }; } //unsigned shift right (js >>>) function zeroFill( $a, $b ) { $z = hexdec( 80000000 ); if( $z & $a ) { $a = ( $a >> 1 ); $a &= ( ~ $z ); $a |= 0x40000000; $a = ( $a >> ( $b - 1 ) ); } else { $a = ( $a >> $b ); } return $a; }
      
      







これですべての準備が整い、poo()を書き換えることができます。



 // function poo( $a, $b ) { $c = strlen( $a ); $d = $b ^ $c; $e = 0; $f = ''; while( $c >= 4 ) { $f = charCodeAt( $a, $e ) & 255 | ( charCodeAt( $a, ++$e ) & 255 ) << 8 | ( charCodeAt( $a, ++$e ) & 255 ) << 16 | ( charCodeAt( $a, ++$e ) & 255 ) << 24; $f = ( $f & 65535 ) * 1540483477 + ( ( ( zeroFill( $f, 16 ) ) * 1540483477 & 65535 ) << 16 ); $f ^= zeroFill( $f, 24 ); $f = ( $f & 65535 ) * 1540483477 + ( ( ( zeroFill( $f, 16 ) ) * 1540483477 & 65535 ) << 16 ); $d = ( $d & 65535 ) * 1540483477 + ( ( ( zeroFill( $d, 16 ) ) * 1540483477 & 65535 ) << 16 )^$f; $c -= 4; ++$e; } switch( $c ) { case 3: $d ^= ( charCodeAt( $a, $e + 2 ) & 255 ) << 16; case 2: $d ^= ( charCodeAt( $a, $e + 1 ) & 255 ) << 8; case 1: $d ^= charCodeAt( $a, $e ) & 255; $d = ( $d & 65535 ) * 1540483477 + ( ( ( zeroFill( $d, 16 ) ) * 1540483477 & 65535 ) << 16 ); } $d ^= zeroFill( $d, 13 ); $d = ( $d & 65535 ) * 1540483477 + ( ( ( zeroFill( $d, 16 ) ) * 1540483477 & 65535 ) << 16 ); $d ^= zeroFill( $d, 15 ); return zeroFill( $d, 0 ); }
      
      







保存、実行、中断-jsとphpのバージョンの結果が一致しません。

元気?

jsとphpにコードを追加して、計算の各行の後に結果を表示し、何が起こっているのかを見てください。



単純な算術php演算子は、javascriptとは異なり、大きな数値ではうまく機能しないことがわかりました。



たとえば、式



 ( 18220025198660 & 65535 ) * 1540483477 + ( ( ( 18220025198660 >>> 16 ) * 1540483477 & 65535 ) << 16 );
      
      







javascriptの場合は221886241596 36に等しく、phpの場合も同様です



 ( 18220025198660 & 65535 ) * 1540483477 + ( ( ( zeroFill( 18220025198660, 16 ) ) * 1540483477 & 65535 ) << 16 )
      
      







わずかに異なる数221886241596 00に等しくなります



いくつかの同様の式が連続して計算されると、エラーが累積し、まったく異なる結果が得られます。 一部の式では、phpはデフォルトで結果がint型であると想定し、最大値を40億に制限します(32ビットシステム)。



Perlには、多数の同様の問題があります。



PHPで正確な計算を行うには、BC Mathライブラリの関数を使用する必要があります。 これに加えて、float型にキャストを追加する必要があります。



試行錯誤の結果、javascriptと同じ結果が得られるコードが得られます。 しかし、これには追加の時間と労力が必要です。

コードは最適ではありません。明確にするために、計算は段階的に実行されます。



 // function poo( $a, $b ) { $c = strlen( $a ); $d = $b ^ $c; $e = 0; $f = ''; while( $c >= 4 ) { $f = charCodeAt( $a, $e ) & 255 | ( charCodeAt( $a, ++$e ) & 255 ) << 8 | ( charCodeAt( $a, ++$e ) & 255 ) << 16 | ( charCodeAt( $a, ++$e ) & 255 ) << 24; $f = bcadd( bcmul( $f & 65535, 1540483477 ), ( floatval( ( bcmul( ( zeroFill( $f, 16 ) ), ( 1540483477 & 65535 ) ) ) ) << 16 ) ); $xx = zeroFill( $f, 24 ); $f = floatval( $f ) ^ floatval( $xx ); // $f = floatval( $f ); $f1 = bcmul( $f & 65535, 1540483477 ); $f2 = ( floatval( ( bcmul( ( zeroFill( $f, 16 ) ), ( 1540483477 & 65535 ) ) ) ) << 16 ); $f = bcadd( $f1, $f2 ); $d1 = bcmul( $d & 65535, 1540483477 ); $d2 = ( floatval( ( bcmul( ( zeroFill( $d, 16 ) ), ( 1540483477 & 65535 ) ) ) ) << 16 ); $d = bcadd( $d1, $d2 ); $d = floatval( $d ) ^ floatval( $f ); $c -= 4; ++$e; } switch( $c ) { case 3: $d = floatval( $d ) ^ ( ( charCodeAt( $a, $e + 2 ) & 255 ) << 16 ); case 2: $d = floatval( $d ) ^ ( ( charCodeAt( $a, $e + 1 ) & 255 ) << 8 ); case 1: $d = floatval( $d ) ^ ( charCodeAt( $a, $e ) & 255 ); $d1 = bcmul( $d & 65535, 1540483477 ); $d2 = ( floatval( ( bcmul( ( zeroFill( $d, 16 ) ), ( 1540483477 & 65535 ) ) ) ) << 16 ); $d = bcadd( $d1, $d2 ); } $d = floatval( $d ) ^ zeroFill( $d, 13 ); $d1 = bcmul( floatval( floatval( $d ) & 65535 ), 1540483477 ); $dd21 = zeroFill( $d, 16 ); $dd22 = floatval( bcmul( $dd21, 1540483477 & 65535 ) ); $dd23 = floatval( $dd22 << 16 ); $d2 = $dd23; $d = bcadd( $d1, $d2 ); $d = floatval( $d ) ^ zeroFill( $d, 15 ); if( $d < 0 ) { $res = bindec( decbin( ~0 ) ) - abs( $d ) + 1; } else { $res = $d; } return $res; }
      
      







zeroFill()関数の場合、最初に追加します。



 $a = floatval( $a );
      
      







おわりに



私のボットは仕事を終えており、ここで説明する保護を自分の目的に使用できます。 たとえば、計算を行うコードを動的に変更するなど、修正すると、このようなハッキングはさらに難しくなります。 そして、誰もあなたを真剣に受け止めたくないなら、この保護は十分でしょう。



一般的に、ボットに対する最良の保護はキャプチャです。 最もトリッキーなjavascriptでさえ、 Mechanize Perlモジュールのようなものを使用してボットによって実行できます。



All Articles