В этом случае я использую следующий подход:
1. Подписывание данных секретным ключом.
Это дает гарантию, что данные не будут подменены на промежуточных узлах до сервера.
Технология следующая:
a. Генерируем секретный ключ, о котором знает только приложения клиента и сервера
b. К передаваемым данным на сервер добавляем еще один параметр равный хэшу данных с секретным ключом. Пример на Flex:
var key:String = "0000-0000-0000-0000-0000"; var sreq:String = "method=setBalance&addMoney=1000"; //сам запрос //отсортируем параметры //чтобы последовательность параметром и на клиенте и на сервере была одинакова. Это гарантирует схождение хэшей. sreq = sreq.split("&").sort().join("&"); //добавим к параметрам время, чтобы нельзя было продублировать запрос var now:Date = new Date(); var sec = now.getTime().toString().substr(0, -3); //конечный запрос к серверу с дополнительным параметром = хэшу запроса и секретного ключа sreq = sreq + "×tamp="+sec+"&sig="+MD5.hash(sreq+key);c. Сервер принимает запрос и проверяет данные используя свой секретный ключ.
К сожалению нельзя использовать четкое совпадение времени клиента и сервера, т.к. оно может отличаться или сообщение может идти до нескольких секунд. Из-за этого на сервере допускается расхождение timestamp до 100 секунд. Серверный код на PHP:
$_TIME = $_SERVER['REQUEST_TIME'];//время запроса $_KEY = "0000-0000-0000-0000-0000";//тотже ключ, что и на клиенте $_TIMEOUT = 100; //допустимое отклонение времени // Проверим валидность данных //присланная подпись должна сойтись со сгенерированной на основе закрытого ключа $q = $_GET; //отсортируем также как в flex unset($q['timestamp'], $q['sig']); ksort($q); $req = ""; foreach($q as $k=>$v) { $req .= $k."=".$v."&"; } $req = substr($req, 0, -1); if($_GET['sig'] != md5($req.$_KEY)) { //подписи не сошлись. Данные были подменены. exit; } //Проверим расхождение времени if($_GET['timestamp'] + $_TIMEOUT < $_TIME) { //Расхождение более чем на 100 секунд. Прерываем запрос exit; }
К сожалению такой метод не гарантирует нам, что запрос не будет перехвачен на промежуточном узле и не продублирован N раз в течении этих 100 секунд. Для устранения этого служит следующий пункт:
2. Проверка запроса на дублирование.
Идею я позаимствовал из TCP/IP. Добавим к запросу еще один параметр = счетчику сообщений, а на сервере будем хранить значение последнего счетчика. На клиенте достаточно сгенерировать счетчик = текущее значение + 1, а на сервере принять, проверить что новое значение больше старого и обновить серверный счетчик. Это гарантирует нам, что если злоумышленник перехватит запрос и пошлет его заново, то сервер его отвергнет, т.к. значение будет меньше хранящегося на сервере.
Для реализации этого механизма на сервере нам уже понадобится БД, а на клиенте дополнительный запрос на получение счетчика.
Под катом можно увидеть полный код клиента и сервера:
Сторона клиента (Flex):
1. Класс реализующий передачу данных и их защиту:
package Lib { import com.adobe.crypto.MD5; import flash.events.*; import flash.net.URLLoader; import flash.net.URLRequest; import flash.net.URLRequestHeader; import flash.net.URLRequestMethod; import flash.net.URLVariables; public class HttpRequest { private var header:URLRequestHeader; private var request:URLRequest; private var loader:URLLoader; public var uid:int = 1; private var key:String = "0000-0000-0000-0000-0000"; public function HttpRequest(url: String) { loader = new URLLoader(); header = new URLRequestHeader("pragma", "no-cache"); request = new URLRequest(url); } public function getData(req: String, func, method:String = "get"):void { loader.addEventListener(Event.COMPLETE, func); var sreq:String = req + "&uid="+this.uid; sreq = sreq.split("&").sort().join("&"); //отсортируем параметры (чтобы хэши сошлись) var now:Date = new Date(); var sec = now.getTime().toString().substr(0, -3); sreq = sreq + "×tamp="+sec+"&sig="+MD5.hash(sreq+this.key); request.data = new URLVariables(sreq); if(method == "post") { request.method = URLRequestMethod.POST; } else { request.method = URLRequestMethod.GET; } request.requestHeaders.push(header); loader.load(request); } } }
2. Пример исопльзования:
public static function _get(req:String, func, method:String = "get", server:String = "http://server/listener.php"):void { var http:HttpRequest = new HttpRequest(server); http.getData(req, function complete(event:Event):void { var loader:URLLoader = URLLoader(event.target); //генерируем объект (подробнее) var xmlObj:XMLTree = new XMLTree(loader.data); var data:Object = xmlObj.getData(); func(data.response);//вызываем колбэк с данными }, method); } Functions._get("method=user", function(data:Object) { Alert.show(data.toString()); });
Серверная сторона (PHP):
$_TIME = $_SERVER['REQUEST_TIME']; $_KEY = "0000-0000-0000-0000-0000"; $_TIMEOUT = 100; $_TIME_BEFORE_EVENT = 3600*1; //db $_SERV = "localhost"; $_USR = "root"; $_PASW = ""; $_DB = "bookie"; // 1. Проверим валидность данных //присланная подпись должна сойтись с сгенерированной на основе закрытого ключа (в флэш аналогичная генерация) //когда данные подтверждены, можно двигаться дальше $q = $_GET; //отсортируем также как в flex unset($q['timestamp'], $q['sig']); ksort($q); $req = ""; foreach($q as $k=>$v) { $req .= $k."=".$v."&"; } $req = substr($req, 0, -1); if($_GET['sig'] != md5($req.$_KEY)) { XMLError(1, 'Erorr Sign'); } //2. Проверим расхождение времени if($_GET['timestamp'] + $_TIMEOUT < $_TIME) { XMLError(2, 'Request Expired'); } //далее нужна БД $link = mysql_connect($_SERV, $_USR, $_PASW); if(!$link) { XMLError(5, 'DB Server Unaviable'); } if(!mysql_select_db($_DB, $link)) { XMLError(6, 'DB Not Exist'); } //основные методы if($_GET['method'] == 'section') { checkRepeat($uid); XMLSelect('SELECT id, name, last_update, type_name, cnt_elem FROM section WHERE tid = '.intval($_GET['tid']).getFilter(), 'section'); } else { XMLError(4, 'Unknown Method'); } function checkRepeat($uid) { //будем контролировать только запросы на обновление, на просмотр пох $id = mysql_result(mysql_query("SELECT lastid FROM `user` WHERE id = ".$uid), 0); //Проверим на повтор //пришедший счетчик должен быть больше текущего значения. Не важно на сколько, т.к. верность данных мы уже проверили на шаге 1. if($_GET['lid'] <= $id) { //вопрос: а если запрос долго идет и счетчик уже увеличился? XMLError(3, 'Dublicate request'); } if(!mysql_query("UPDATE `user` SET lastid = lastid + 1 WHERE id = ".$uid)) echo mysql_error(); } function XMLSelect($sql, $name = 'element', $list = true) { $result = mysql_query($sql); if (!$result) XMLError(7, mysql_error()); header('Content-type: application/xml'); $ret = '<?xml version="1.0" encoding="utf-8"?>'. '<response>'; $keys = NULL; while ($row = mysql_fetch_assoc($result)) { $ret .= '<'.$name.'>'; if(!$keys) { $keys = array_keys($row); } if($list) { foreach($keys as $key) { $ret .= '<'.$key.'>'.$row[$key].'</'.$key.'>'; } } else { $ret .= $row[$keys[0]]; } $ret .= '</'.$name.'>'; } echo $ret.'</response>'; exit; } function XMLUpdate($sql, $uid = NULL) { if(!empty($uid)) checkRepeat($uid); $result = mysql_query($sql); if (!$result) XMLError(7, mysql_error()); header('Content-type: application/xml'); echo '<?xml version="1.0" encoding="utf-8"?>'. '<response>1</response>'; exit; } function XMLError($code, $str = 'Unknown Error') { header('Content-type: application/xml'); echo '<?xml version="1.0" encoding="utf-8"?>'. '<error>'. ' <error_code>'.$code.'</error_code>'. ' <error_msg>'.$str.'</error_msg>'. '</error>'; exit; }
Структура БД не имеет значения. Надеюсь общий смысл понятен.
А если я открою код Flex-приложения 16-ричным редактором, а лучше специальным анализатором Flash-файлов, и найду секретный ключ?
ОтветитьУдалитьСправедливое замечание. Но это свойственно любому приложению (exe, swf, ...).
ОтветитьУдалитьЧто-то у меня пока нет идей как это обойти...
Хотя може делать чтото типа этого:
1. Использовать первый запрос к апи в настройках приложения. Получить подписанные достоверные данные.
2. Получить секретный ключ от сервера на основе этих данных.
3. Дальше по предыдущей схеме.
Это в случае, если приложение запущена во вконтакте. Иначе нужна какая то система авторизации на сайте, где будет запускаться flash приложение, чтобы получить достоверные подписанные данные о пользователе на первом этапе.
ОтветитьУдалитьНо всё равно этот секретный ключ перехватывается.
ОтветитьУдалитьДаже если пытаться защитить его каким-либо алгоритмом, swf декомпилится, алгоритм узнается, и ключ получаем всегда... А потом шлём сколько угодно запросов, со счетчиком равным 262435623.
Не очень-то хорошо...
Я не прав?
Да уж, получается максимум что можно сделать - это усложнить взлом...
ОтветитьУдалитьА что, если генерировать уникальную подпись на сервере для каждого запроса, с привязкой к расположению приложения. Но это похоже тоже легко сломается, если подделать расположение приложения.
А что если замешать хеш типа sha256($Username:$_SERVER['REMOTE_ADDR']) и проверять при каждом подключении (запросе) клиента ключ, хранящийся на сервере + использовать SSL? ;)
ОтветитьУдалитьВ данном случае динамика обеспечена и даже в случае перехвата ключа... Сервер, проверяя ключ, по сути генерирует его снова
ОтветитьУдалитьчто то вроде:
function verifyKey($key)
{
$test_key = sha256($SESSION['Username'].":".$_SERVER['REMOTE_ADDR']);
if($key == $test_key)
//всё ok, наш клиент
else
//"что то не так"
}
и тем самым сравнивает источник (адрес источника) запроса с изначальным...
конечно это довольно медленно - выстраивать хеши при каждом запросе, но помоему в этой схеме есть толк...
Жду комментов
$_SERVER['REMOTE_ADDR'] - это Ip адрес клиента?
ОтветитьУдалитьТ.е. опять же нужна серверная регистрация, чтобы собирать ip пользователей приложения. А если у пользователя динамический ip?
Ктомуже злоумышленнику достаточно зарегаться на сервере (поснифать чужой трафик) и пользоваться этим ключем до скончания веков.
Если же $_SERVER['REMOTE_ADDR'] - это адрес расположения приложения, то достаточно узнать через whois ip адрес приложения и в декомпилированном приложении подставить этот ip.
:)
ОтветитьУдалить$_SERVER['REMOTE_ADDR'] - это ip клиента, для которого в данный момент выполняется скрипт.
Я беру пример где существует регистрация пользователей на сервере... А открытая она или закрытая - это уже другой вопрос)))
Я так понял что вопрос ставился о невозможности подмены запроса))))
Возьмём зарегистрированного раннее пользователя скажем "vasya" :) который в данный момент логинится на сервер с ip 92.252.203.22
сервер проверяет user/pass в случае успешного логина выдаёт клиенту (Flex) ключ сгенерированный как "vasya:92.252.203.22"...
Я ПОВТОРЯЮ ip юзера вовсе НЕ обязан храниться на сервере (в базе), а по поводу сниффа я не совсем понял... Ведь если запрос, помеченный ключём "vasya:92.252.203.22" придёт с ip скажем 92.252.210.24, то запрос будет отклонён :))))
Повторяю ля хеша берётся текущий адрес ЗАЛОГИНЕНОГО пользователя! Имя пользователя в хеше используется только для уникальности хеша, например, если "92.252.203.22" это интернет-шлюз какой нибудь организации, за которой 200 компов.
Способа "fuck the system" всего три:
1. Подмена IP злоумышленником в то время когда клиент работает с сервером (когда ключ выдан и действителен), но для этого нужно засниффить каким то образом IP клиента, более того... Нужно вообще знать, что ключ строится на основе IP ;)
2. Хак логина (например брутворс), но для предотвращения этого давно придумали CAPCHA.
3. Хак базы... ну что тут можно сказать... DB сервер не должен торчать "в инет" :))
В базе пароли должны быть зашифрованы алгоритмом, отличающимся от того, которым они шифруются при передаче от клиента к серверу ;)
Сергей, спасибо! Очень похоже на правду. Особенно, если использовать сочетаний сессий на сервере и ip клиента. У меня остался только вопрос: на сколько сложно подделать ip адрес клиента? Если это трудноосуществимая операция, то защита выглядит очень надежно.
ОтветитьУдалитьВ особо тяжелых случаях еще можно использовать одноразовые пароли etoken (http://ru.wikipedia.org/wiki/EToken) :) Для подтвержения пользователя.
ОтветитьУдалитьТеоретически существует возможность подменить адрес в заголовке TCP датаргаммы, но на пути от проги, которая это сделает есть ещё сетевая карта, которая формирует TCP аппаратно и вставляет в датаграммы установленный для неё (сетевухи) IP... Далее - провайдер.
ОтветитьУдалитьА вобще TCP - протокол, основанный на факте соединения (что подразумевает возможность передачи в оба конца) то есть если клиент обращается к серверу, то сервер на транспортном уровне (TCP) должен иметь возможность обратиться к клиенту, а так как в инет не могут торчать 2 кома с одним IP, то возможность подмены IP в TCP заголовке кажется весьма неправдоподобной :)
А вообще надо спецификацию TCP почитать...
Единственное. что может произойти, это случайность...
Скажем, если вышеупомянутый Вася и злоумышленник в данный момент используют один пул провайдера (92.252.203.22) то есть внешний IP у них в данный момент один..., но в этом случае (поидее) возможность сниффа со стороны злоумышленника - исключена.
Да и вообще снифф возможен только по схеме клиент-сниффер-сервер, либо если они в одной локалке (подключены к одному маршрутизатору, но тут всё зависит от конфигурации маршрутизатора)
Я не знаю как с таким количеством факторов рассчитать возможность подмены IP адреса в TCP заголовке в количественном исчислении)))) но мне она кажется очень маловероятной.
Этот комментарий был удален автором.
ОтветитьУдалить"Скажем, если вышеупомянутый Вася и злоумышленник в данный момент используют один пул провайдера (92.252.203.22) то есть внешний IP у них в данный момент один..., но в этом случае (поидее) возможность сниффа со стороны злоумышленника - исключена.
ОтветитьУдалить"
Ну или если взять какую нибудь маленькую организацию, используюшую свой интернет шлюз (прокси сервер) с внешним (интернет) ip-адресом 92.252.203.22, то сисадмин в этой конторе может снифить траффик сколько ему вздумается, и впринципе может теоретически пользоваться ключом клиента при условии что знает как конкретный Flex клиент взаимодействует с сервером (например декомпиляция клиента), но тут уж ничего не сделаешь... это скорее человеческий фактор :) От этого никак не защититься...
Сергей, спасибо за столь развернутые комментарии :) Теперь мне стало понятно, что возможность взлома при таком методе стремится к минимуму. Ваш метод заслуживает отдельной статьи :)
ОтветитьУдалитьДа право не стоит, Алексей :)
ОтветитьУдалитьЭто всё просто логика)))))
Я уверен что это всё давным давно используется...
Кстати по поводу сессий...
ОтветитьУдалитьЯ просто сразу внимания не обратил :)
В случае с сессиями по моему нет смысла в использовании хешированных ключей...
Потому что по сути сессия занимается тем же самым, только на более низком уровне реализации (сервер).
Этот как вы выразились "метод" - является скорее альтернативой сессиям...
Я допёр до этого тогда, когда мне пришлось искать способ избавиться от использования сессий при работе Flex приложения (для AIR) с сервером. т.к принудительно обновлять сессию по таймеру в Flex приложении после бездействия клиента скажем в течении 10 минут - показалось как то криво..., а вечные сессии - форменное б*ядство :)
К ключу лучше ешё прикрутить какое - нибудь рандомное значение, сгенерированное на сервере...
ОтветитьУдалитьХотя в этом случае всё немного усложняется т.к это (рандомное) значение надо хранить где то на сервере(для последующей проверки входящего ключа)... либо действительно в сессии, либо в базе...
ОтветитьУдалитьМожно как в статье: счетчик запросов, тогда исключится возможность повторного запроса.
ОтветитьУдалитьВот только вы забыли еще об одном виде хацкеров =)))
ОтветитьУдалитьЧто если, злоумышленник и клиент - одно и то же лицо? Ну например, захотел он подкрутить себе рейтинг в игре или пару баксов добавить себе на счет?
Что тогда? Есть ли способы защититься от декомпиляции?
Ни один из способов не является приемлемым. Мутки с ип вам позволят привязать лишь текущую сессию - грубо говоря, приложение не запустят в 2 окна. Что бы защитить соединения одного пользователя от дрыгих - достаточно обычного ключа (ид уникального) пользователя - все это в мд5 и на сервер. Большинство проблем создают именно пользователи-злоумышленники которые пишут ботов, и пытаются найти уязвимости на сервере - для СВОЕГО персонажа в игре например.
ОтветитьУдалитьЗащитить способов нет - но есть усложнить.
Вот потому клиент и оперирует только с конечной информацией.
ОтветитьУдалитьНа примере игры:
*Пользователь выбрал куда бить - send
*Сервак просчитал попадание, урон, остатки хп моба, записал в базу - показал это клиенту - load
*Пользователь выбрал куда бить снова - send
на стороне клиента ТОЛЬКО отображение информации, на стороне сервера - рассчеты, работа с данными.
декомпиляция клиента игры в итоге не даст никаких возможностей накрутки.
Cпасибо большое. Vobar.
В статье обсуждается защита данных от подмены. Один из способо - подпись данных закрытым ключем.
ОтветитьУдалитьТакже не всегда есть возможность обрабатывать такой поток данных в режиме реального времени на сервере.
Столкнулся с такой темой.. Думал зашифровать. Но потом возникла проблема, что как бы не шифровал, пользователь всегда может продублировать запрос и допустим потратив игровые деньги вернуть заново. Потом пришла идея, тоже с использованием времени и даты. Только забота в том, что время отправки с компа пользователя попадает в заголовок пакета. Вот и сравнивать. Расшифровываем, берем время(в php не знаю есть такая функция или нет, заголовки смотреть запроса) если время отправки уже имело место быть (тут бд понадобится) баним за дублирование запроса. Что имеем в итоге? 1. Используя шифрование данных (des, rsa, aes и подобные) защищаемся от подделки запроса. 2. Ни легальный пользователь, ни нелегальный, ни даже программа, не знает о том, что помимо шифра, сервер еще проверит и время отправки. Здесь поясню, мы программно не присваиваем время и дату, это сделает ОС при отправке данных в инет. 3. Защита конечно не гарантирует подделки, но усложняет ее хорошенько. Ну и недостаток метода это если в одну и ту же секунду программа легально отправит два разных запроса, это нужно учесть. Не более одного запроса в секунду. 4. RSA алгоритм как раз и решает проблему шифрования и подделки запроса, а точнее его невозможности (ну только если у вас нет 100 тыс лет жизни, что б дождаться взлома ключа), но при этом не убирает проблему дублирования
ОтветитьУдалитьпро дублирование в случае с базой данных это не проблема, перед тем как чтото туда вбивать или вычислять делается маленькая проверка на несколько полей, скажем логин + таймштамп (который он получит от сервера и с которым он сможет делать только 1 запрос после которого он получит новый штамп), и если есть несовпадение то запрос или просто убивается без предупреждения клиента или клиенту выдается нужное предупреждение + ко всему этому обновление штампа, задумка не сложная но довольно эффективная поскольку старые штампы в реальном времени убиваются тем самым и не бесконечны (поскольку обновляются при новом запросе)и можно к ним таймеры применять по необходимости...
ОтветитьУдалитьИ все таки самая большая проблема - это защита от подмены данных когда злоумышленник и клиент - одно и то же лицо.
ОтветитьУдалить1. дубли мы исключаем счетчиком + таймстамп
2. для каждого запроса на сервере генерится рандомное значение, которое используется для шифрования запроса клиентом. Также на клиенте генерится рандом для ответов сервера.
3. на клиенте и на сервере хранится секретный ключ, который также используется для подсчета контрольной суммы... вот защита этого ключа и есть проблема, потому как клиент у нас - swf_ка, и очень легко декомпилится. Думаю единственное решение - это обфускация кода. Только вот хорошие обфускаторы стоят хороших денег.
также можно отправлять по ssl - но и тут есть методы подмены (((
решения по полной защите нет.