
Источник: https://habr.com/post/137961/
Защита работает следующим образом.
Скрипт главной страницы сайта index.php ожидает куку, в которой одним из параметров будет указан хеш, вычисленный из IP-адреса посетителя.
Если кука не передается, то index.php перенаправляет посетителя на другую страницу, содержащую javascript код, который вычисляет необходимый параметр, записывает его в куку и возвращает нас обратно на главную страницу.
Чтобы обычный php-бот, выполняющий GET и POST запросы через CURL, смог проходить через такую защиту, нужно переписать вычисление хеша с javascript на php и затем дописывать в заголовок запроса нужную куку.
Вскрытие
Теперь подробнее.
Запускаем Firefox, отключаем javascript и включаем Firebug.
Запрашиваем главную страницу index.php и смотрим заголовки запроса и ответа.
Запрос:
GET
http://example.com
Заголовки этого запроса не представляют для нас интереса.
А вот заголовки ответа:
Status: 302 Moved Temporarily
Connection keep-alive
Content-Type text/html
Date XXX GMT
Location
http://example.com/govalidateyourself#98765:1234:11.22.33.44:/index.php
Server YTS/1.20.0
Transfer-Encoding chunked
После чего Firefox автоматически переходит на указанный в заголовке Location, получая следующий заголовок ответа:
Accept-Ranges bytes
Connection keep-alive
Content-Type text/html; charset=utf-8
Date XXX GMT
Last-Modified YYY GMT
Server YTS/1.20.0
Set-Cookie addr=1234:11.22.33.44; path=/
Transfer-Encoding chunked
Где 11.22.33.44 — мой IP-адрес, 1234 — какое-то число, логика вычисления которого неизвестна.
Сама страница содержит ссылку на js-код
http://example2.com/validator/va.js
и надпись «No javascript».
Без js нас дальше не пустят.
После того как все запросы-ответы записаны, включаем javascript, очищаем cookie и делаем все заново.
Сейчас нас интересует то, что будет происходить после запроса страницы валидации.
На этот раз загружается главная страница сайта, и вот заголовок последнего запроса:
Accept text/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
Connection keep-alive
Cookie addr=5678:11.22.33.44; urine=aabbccdd; v=1
Host example.com
Referer
http://example.com/govalidateyourself
User-Agent какой-то Firefox
Константа 1234 из прошлого ответа сервера в этот раз изменилась на 5678, IP-адрес остался тем же. Судя по всему это ID запроса, присваиваемый сервером и хранящийся в cookie. Ну что ж, его надо сохранить и просто записывать в куки в неизменном виде во время запросов.
А вот параметр 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)}}
Но немного терпения, и после форматирования все выглядит читабельно и довольно понятно.
Есть две функции coo() и poo(), и код который пишет нужную нам куку и отправляет обратно на index.php.
Функция сoo() не представляет особого интереса, она получает значение указанного параметра из куки, и легко переписывается на php простым регулярным выражением.
А вот функция poo(), которая считает параметр urine:
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 — это и есть уже готовое значение параметра urine (дальше оно только дополняется нулями если содержит меньше 8 символов).
addr — наш IP-адрес 11.22.33.44.
47 — константа.
Теперь все выглядит понятно.
php-бот, пробивающий эту защиту, должен работать по следующему алгоритму.
1. Делаем GET-запрос
http://example.com/index.php
Cтавим опцию получать заголовки ответа:
curl_setopt( $ch, CURLOPT_HEADER, 1 );
И заодно включаем автоматический переход в случае редиректа:
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
В этом случае curl сам выполнит переход на новый location, и нам нет нужды программировать второй запрос. И мы получим заголовки обоих ответов, в первом заголовке будет Location, во втором — первая кука, содержащая ID запроса.
2. Парсим заголовки, получаем ID запроса и свой IP-адрес (если мы используем разные трюки то мы можем его сразу и не знать, а здесь его нам любезно подсказывают — очень удобно).
Считаем параметр urine, записываем в куку и отправляем новый GET-запрос на index.php. Защита пройдена.
Кука прописывается так:
$headers = array(
"Cookie: " . $cookie_str, // "addr=5678:11.22.33.44; urine=aabbccdd; v=1"
/* другие заголовки по желанию/необходимости */
);
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
Итак, остался последний штрих — вычисление urine.
Грабли
Нужно просто переписать функцию poo() на php.
Для начала немного гуглим и пишем аналоги для пары js-функций и операторов, которых нет в php:
// 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 будет равно 22188624159636, а аналогичное в php
( 18220025198660 & 65535 ) * 1540483477 + ( ( ( zeroFill( 18220025198660, 16 ) ) * 1540483477 & 65535 ) << 16 )
будет равно немного другому числу 22188624159600
Когда несколько подобных формул вычисляются подряд то ошибка накапливается, давая в итоге совсем другой результат. В некоторых выражениях php по умолчанию предполагает что результат является типом int и ограничивает максимальное значение до 4 млрд (на 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 );
Защита работает следующим образом.
Скрипт главной страницы сайта index.php ожидает куку, в которой одним из параметров будет указан хеш, вычисленный из IP-адреса посетителя.
Если кука не передается, то index.php перенаправляет посетителя на другую страницу, содержащую javascript код, который вычисляет необходимый параметр, записывает его в куку и возвращает нас обратно на главную страницу.
Чтобы обычный php-бот, выполняющий GET и POST запросы через CURL, смог проходить через такую защиту, нужно переписать вычисление хеша с javascript на php и затем дописывать в заголовок запроса нужную куку.
Вскрытие
Теперь подробнее.
Запускаем Firefox, отключаем javascript и включаем Firebug.
Запрашиваем главную страницу index.php и смотрим заголовки запроса и ответа.
Запрос:
GET
http://example.com
Заголовки этого запроса не представляют для нас интереса.
А вот заголовки ответа:
Status: 302 Moved Temporarily
Connection keep-alive
Content-Type text/html
Date XXX GMT
Location
http://example.com/govalidateyourself#98765:1234:11.22.33.44:/index.php
Server YTS/1.20.0
Transfer-Encoding chunked
После чего Firefox автоматически переходит на указанный в заголовке Location, получая следующий заголовок ответа:
Accept-Ranges bytes
Connection keep-alive
Content-Type text/html; charset=utf-8
Date XXX GMT
Last-Modified YYY GMT
Server YTS/1.20.0
Set-Cookie addr=1234:11.22.33.44; path=/
Transfer-Encoding chunked
Где 11.22.33.44 — мой IP-адрес, 1234 — какое-то число, логика вычисления которого неизвестна.
Сама страница содержит ссылку на js-код
http://example2.com/validator/va.js
и надпись «No javascript».
Без js нас дальше не пустят.
После того как все запросы-ответы записаны, включаем javascript, очищаем cookie и делаем все заново.
Сейчас нас интересует то, что будет происходить после запроса страницы валидации.
На этот раз загружается главная страница сайта, и вот заголовок последнего запроса:
Accept text/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
Connection keep-alive
Cookie addr=5678:11.22.33.44; urine=aabbccdd; v=1
Host example.com
Referer
http://example.com/govalidateyourself
User-Agent какой-то Firefox
Константа 1234 из прошлого ответа сервера в этот раз изменилась на 5678, IP-адрес остался тем же. Судя по всему это ID запроса, присваиваемый сервером и хранящийся в cookie. Ну что ж, его надо сохранить и просто записывать в куки в неизменном виде во время запросов.
А вот параметр 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)}}
Но немного терпения, и после форматирования все выглядит читабельно и довольно понятно.
Есть две функции coo() и poo(), и код который пишет нужную нам куку и отправляет обратно на index.php.
Функция сoo() не представляет особого интереса, она получает значение указанного параметра из куки, и легко переписывается на php простым регулярным выражением.
А вот функция poo(), которая считает параметр urine:
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 — это и есть уже готовое значение параметра urine (дальше оно только дополняется нулями если содержит меньше 8 символов).
addr — наш IP-адрес 11.22.33.44.
47 — константа.
Теперь все выглядит понятно.
php-бот, пробивающий эту защиту, должен работать по следующему алгоритму.
1. Делаем GET-запрос
http://example.com/index.php
Cтавим опцию получать заголовки ответа:
curl_setopt( $ch, CURLOPT_HEADER, 1 );
И заодно включаем автоматический переход в случае редиректа:
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
В этом случае curl сам выполнит переход на новый location, и нам нет нужды программировать второй запрос. И мы получим заголовки обоих ответов, в первом заголовке будет Location, во втором — первая кука, содержащая ID запроса.
2. Парсим заголовки, получаем ID запроса и свой IP-адрес (если мы используем разные трюки то мы можем его сразу и не знать, а здесь его нам любезно подсказывают — очень удобно).
Считаем параметр urine, записываем в куку и отправляем новый GET-запрос на index.php. Защита пройдена.
Кука прописывается так:
$headers = array(
"Cookie: " . $cookie_str, // "addr=5678:11.22.33.44; urine=aabbccdd; v=1"
/* другие заголовки по желанию/необходимости */
);
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
Итак, остался последний штрих — вычисление urine.
Грабли
Нужно просто переписать функцию poo() на php.
Для начала немного гуглим и пишем аналоги для пары js-функций и операторов, которых нет в php:
// 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 будет равно 22188624159636, а аналогичное в php
( 18220025198660 & 65535 ) * 1540483477 + ( ( ( zeroFill( 18220025198660, 16 ) ) * 1540483477 & 65535 ) << 16 )
будет равно немного другому числу 22188624159600
Когда несколько подобных формул вычисляются подряд то ошибка накапливается, давая в итоге совсем другой результат. В некоторых выражениях php по умолчанию предполагает что результат является типом int и ограничивает максимальное значение до 4 млрд (на 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 );