В ходе построения клиент-серверного приложения, требуется реализовать возможность защищенной передачи данных.В этом случае я использую следующий подход:
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 - но и тут есть методы подмены (((
решения по полной защите нет.