Дмитрий DarkByte Москин

Мой блог, да.
logo

Приватный чат

Опубликовано 27.07.2011 автором Дмитрий Москин
Недавно запустил проект приватного чата, который позволяет общаться двум и более людям в конфиденциальной обстановке. Все сообщения шифруются на стороне клиента, сервер выступает лишь транспортом для передачи шифрованных сообщений между пользователями. Перед общением, один пользователь генерирует ключ и любым доступным способом сообщает его остальным. Таким образом, даже сервер не знает ключ, который используется для шифрования сообщений, ему доступна только небольшая часть ключа, отвечающая за принадлежность участников к определённой комнате. Кроме того, шифрованные сообщения хранятся на сервере всего лишь 12 часов, после чего физически удаляются и затираясь новыми, уже не могут быть восстановлены.

Вместо использования готовых криптографических алгоритмов было решено написать свой алгоритм для шифрования сообщений. В итоге получился банальный xor сообщений с ключом длинной 36кб, что поначалу показалось мне довольно хорошим и простым решением, но потом выяснилось, что не всё так хорошо, как кажется :)

Для тех, кто не знает что такое xor и как оно работает, сделаю небольшое пояснение. Xor - логическая операция "сложение по модулю 2", во многих языках программирования обозначается символом "^". Предположим у нас есть оригинальное сообщение "A" и ключ шифрования "B", шифрованное сообщение обозначим как "C". Итак, процесс шифрования сообщения: A^B=C, процесс дешифровки: C^B=A. Но ещё можно сделать так: A^C=B, т.е. имея оригинальное сообщение, а так же то же самое сообщение в шифрованном виде, мы можем получить ключ шифрования.

В чате, вместе с сообщениями шифруется и ник пользователя, т.е. сервер фактически не знает даже ников тех, кто общается в чате. Так я думал. Но неожиданно вспомнил, что ник пользователя (и другие настройки), для удобства сохраняется в cookies, которые передаются на сервер при каждом запросе, но я об этом совсем забыл. И как раз в этом и заключается уязвимость используемого алгоритма шифрования (уязвимость уже исправлена, об этом написано ниже), т.к. перехватив трафик пользователя чата, можно получить зашифрованное сообщение и часть расшифрованного, которым как раз и является ник, передаваемый в cookie в открытом виде.

Просто исправить уязвимость было не интересно, значительно интереснее было проверить на практике, действительно ли можно расшифровать чужую переписку, перехватив трафик одного из пользователей. Итак, начнём. Имеется комната, в которой общаются два пользователя (A и B), позже к ним присоединяется третий (C), трафик которого прослушивается.

Пользователь C заходит в чат и ему присылаются последние сообщения, отправленные другими двумя пользователям:
newmsg(1311769818,'11685492ac740df1c3c6ab7e98');
newmsg(1311769828,'097044d0f2b3d1d3e6de80954a0d3e10e0410fbca78f');
newmsg(1311769836,'057c4086b86019e6d9efb9588106cade2e423d7196666ba3ca');
newmsg(1311769862,'0c7541d5f7b9d6d4e6278859880a3b16367b33');
newmsg(1311769875,'5c2519dfe13940bb81bde806e39a5a4e74136becd406f7cb7b462ce2039ce15fa65c07d035f3900acf57d89f1ff2cc76229c519e69c174ee1d2c71b41fd1fb30');
newmsg(1311769886,'097044d0f28ee1201ff6805c4a000c162575367e4a99');
newmsg(1311770043,'224a7e88830778455d5e3ae332bde5f6dd7e06bfaead5aa9181db740acf14b29c1f26b4c5f903256a0054a');
newmsg(1311770211,'1b7347b1ba3e417c646703da0b84dccfe497');

Но сообщения зашифрованы, и мы даже не знаем ников отправителей.

Теперь пользователь C отправляет в чат некоторое сообщение, мы ловим пакет:



Из всех данных, для нас полезны только nick и msg, key в данном случае является лишь идентификатором комнаты и к шифрованию никакого отношения не имеет.

Для получения небольшой части ключа из имеющихся данных, напишем аналог функции, которая используется для шифрования сообщений в чате
function xor(plant,enc)
{
var enc2=[];
var msglen=enc.length/2;
for(var i=0; i<msglen; i++)
enc2.push(parseInt(enc.substr(i*2,2),16));

var res=[];
var pad=msglen%128;
for(var i=0; i<plant.length; i++){
var x=plant[i].charCodeAt(0)
x==8470?x=1:x>900&&(x-=857);
res.push(x^enc2[i]^pad);
}
return res;
}

И вызвав её, с имеющимися у нас данными, мы получим начало ключа:
xor('The_lost_angel1','224a7e88830778455d5e3ae332bde5f6dd7e06bfaead5aa9181db740acf14b29c1f26b4c5f903256a0054a')

[93, 9, 48, 252, 196, 67, 32, 26, 41, 20, 127, 175, 124, 250, 255]

А теперь инициализируем переменную ключа чата полученным ключом и воспользуемся функцией чата для дешифрования сообщений



Часть сообщений успешно дешифровалась, теперь мы знаем, что пользователи A и B, это Alice и Bob. В принципе на этом можно было бы и завершить дешифрование, ибо кажется, что более длинный ключ уже не от куда получать, но можно попробовать определить недостающие части слов по смыслу и продолжить процесс получения ключа.

Например, ответ на сообщение "привет, ка" был "отлично,", возможно сообщение было "привет, как дела?", проверим догадку:
xor('Bob: привет, как дела?','097044d0f2b3d1d3e6de80954a0d3e10e0410fbca78f')

[93, 9, 48, 252, 196, 67, 32, 26, 41, 20, 127, 175, 124, 250, 255, 231, 214, 140, 197, 72, 102, 166]



Догадка была верна, сообщения стали более открытыми. Повторим попытку.

xor('Alice: отлично, как твои?','057c4086b86019e6d9efb9588106cade2e423d7196666ba3ca')

[93, 9, 48, 252, 196, 67, 32, 26, 41, 20, 127, 175, 124, 250, 255, 231, 214, 140, 197, 72, 102, 166, 151, 101, 236]



И опять удача, почти все сообщения были восстановлены. Но увы, восстановить длинное сообщение не удалось, ибо оно было одно и больше вариантов для подбора не осталось. Хотя можно использовать то сообщение, с которого всё начиналось и восстановить ещё несколько байт ключа, но сути это не меняет.

Данная уязвимость уже исправлена и теперь, даже имея оригинальное и шифрованное сообщение, полученный ключ применить к остальным сообщениям будет значительно труднее. Кроме того, теперь ник передаётся в открытом виде и не содержится в зашифрованном сообщении, поэтому получение оригинального сообщения так же становится нетривиальной задачей :)