@ mrim.pl: Написание скриптов,
работающих по протоколу MMP
by Digimortal
[ intr0 ]
Что такое Mail.Ru Агент, я думаю, знают все. Для тех кто не в курсе, это асикуподобный мессенджер с поддержкой разнообразных дополнительных возможностей, вроде отправки смс, голосового общения, игр и пр. В этой статье я достаточно подробно опишу основные моменты работы с протоколом, который испульзует Мэйл.ру Агент, что поможет тебе в написании различных тулз, работающих с этой системой (ни в коем случае не пишите смс-флудеры, МАгент-спаммеры или сборщики почтовых баз mail.ru!!! ;)) Как уже можно было понять из названия, примеры кода, приводящиеся в статье, будут написаны на самом классном скриптовом языке программирования :).
[ протокол MMP ]
Mail.Ru Агент использует собственный протокол - MMP, или mrim, который является частично открытым. На сайте agent.mail.ru выложено краткое описание протокола. В данном cписании присутствует только информация по основным возможностям мессенджера, но вооружившись сниффером пакетов, выяснить значения заголовков пакетов, например, для отправки смс, не составит труда. Помимо описания там присутствует С-хидер, с значениями полей, флагов и т.п. Я посчитал, что будет удобней объединить описание пакетов и этот заголовочный файл в один txt-файл, что и я сделал (смотри в ссылках). В дальнейшем думаю внести туда описания некоторых пакетов, которые не вошли в официальное описание.
MMP действует поверх установленного tcp-соединения. Клиент инициализирует соединение с сервером, и далее взаимодействие происходит путем обмена сообщениями, причем сообщения могут отпарвляться как клиентом так и сервером.
MMP является бинарным протоколом, кроме того, данные передаются не в общепринятом сетевом формате, а в little-endian'е, т.е. старший байт идет впереди. Основные типы данных, описанные в протоколе это:
- UL;
- LPS;
- UIDL.
Типом UL разработчики обозначили u_long или двойное слово, т.е. 4 байта.
LPS - это составной тип, в который кодируются текстовые строки. Он представляет собой идущий впереди UL, в котором содержится длина строки, и саму строку. Строки представлены в windows-1251 кодировке.
UIDL используется гораздо реже первых двух и в статье затрагиваться не будет. Представлен последовательностью из 8 символов из множества [a-z A-Z 0-9 _ - = +].
[ Структура пакетов ]
Рассмотрим теперь структуру пакетов протокола. Как и полагается, пакет состоит из заголовка и данных (данные могут и отсутствовать). Поля хидера:
Код:
<-4bytes->
,----------.
| magic | мэйджик
+----------+
| proto | версия протокола
+----------+
| seq | номер сообщения в текущем соединении (ответ будет иметь тот же номер)
+----------+
| msg | тип пакета
+----------+
| dlen | длина данных (без учета заголовка)
+----------+
| from | ip отправителя в inet_aton() формате
+----------+
| fromport | порт отправителя
+----------+
| | зарезервированные 16 байт, которые
+- -+ в текущих версиях протокола не используются
| reserved |
+- -+
| |
+- -+
| |
+==========+
| data | далее идут данные (если есть)...
'~~~~~~~~~~'
Самым важным для нас полем является "тип пакета". "Мэйджик" и "версия" у нас постоянны. "Номер сообщения" и "длина данных" подсчитуются по ходу дела, а правильный ip и port, как я понял, передавать вообще не обязательно, поэтому не спрашуй у меня, почему я заполняю их нулями (с значением поля seq тоже дозволены некоторые вольности).
Тип пакета - это команды (или ответы на них), которыми обмениваются между собой клиент и сервер. Команды могут иметь параметры, передающиеся как данные пакета. Значения этих команд и параметров иможно взять из того файла, что прикрепил к статье. Я сразу приведу здесь те, что будут использоваться в приводимых далее примерах:
Код:
my $CS_MAGIC = 0xDEADBEEF; ## Клиентский Magic
my $PROTO_VERSION = 0x1000A; ## Версия протокола
my $MRIM_CS_HELLO = 0x1001; ## C->S, empty
my $MRIM_CS_HELLO_ACK = 0x1002; ## S->C, UL mrim_connection_params_t
my $MRIM_CS_LOGIN2 = 0x1038; ## C->S, LPS login, LPS password, UL status, LPS uagent
my $MRIM_CS_LOGIN_ACK = 0x1004; ## S->C, empty
my $MRIM_CS_LOGIN_REJ = 0x1005; ## S->C, LPS reason
my $MRIM_CS_PING = 0x1006; ## C->S, empty
my $MRIM_CS_USER_STATUS = 0x100F; ## S->C, UL status, LPS user
my $STATUS_ONLINE = 0x00000001;
my $MRIM_CS_MESSAGE = 0x1008; ## S->C, UL flags, LPS to, LPS message, LPS rtf-message
my $MESSAGE_FLAG_NORECV = 0x00000004;
Пишем саб, формирующий пакет (правильнее будет сказать, формирующий его хидер - данные, которые ему передаются, должны быть уже заранее правильно сформированы):
Код:
sub make_mrim_packet
{
my ($msg, $data) = @_; ## получаем параметры
my ($magic, $proto, $seq, $from, $fromport) = ($CS_MAGIC, $PROTO_VERSION, $seq_real, 0, 0);
my $dlen = 0; ## длина данных равна 0 или
$dlen = length($data) if $data; ## если есть данные, то рассчитывается их длина
my $mrim_packet = pack("L11", $magic, $proto, $seq, $msg, $dlen, $from, $fromport, 0, 0, 0, 0);
## пакуем заголовок шаблоном "L"
$mrim_packet .= $data if $data; ## добавляем данные, если они есть
return $mrim_packet; ## возвращаем готовый к отправке пакет
}
[ Взаимодействие client <-> server ]
Для того, чтоб устаовить соединение с mrim-сервером, нужно прежде получить его ip и port. Для этого необходимо установить tcp-соединение с mrim.mail.ru:2042 или mrim.mail.ru:443. Проделав это, клиент получает рекомендуемый для соединения ip-адрес и порт. Итак, вот саб, возвращающий ip:port для коннекта:
Код:
sub get_host_port
{
my $sock1 = new IO::Socket::INET
(
PeerAddr => 'mrim.mail.ru',
PeerPort => 2042, ## как вариант можно использовать 443
PeerProto => 'tcp',
TimeOut => 10
);
sysread ($sock1, my $answ, 18);
close $sock1;
chomp $answ;
return split /:/, $answ;
}
Получив ip-адрес и порт, мы можем создавать соединение с сервером для прохождения авторизации, которая происходит следующим образом:
Код:
,---. ,---.
| C |---------MRIM_CS_HELLO------->| S |
| L |<------MRIM_CS_HELLO_ACK------| E |
| I | | R |
| E |--------MRIM_CS_LOGIN2------->| V |
| N |<------MRIM_CS_LOGIN_ACK------| E |
| T |(или <---MRIM_CS_LOGIN_REJ---)| R |
`---' `---'
Клиент посылает пакет "HELLO", в ответ получает "HELLO_ACK" с данными (UL) в которых содержится ожидаемая частота пинга (ping_period). Клиент должен пинговать сервер специальным пакетом через интервал, равный ping_period секунд. Сервер может изменять ping_period, посылая специальный пакет клиенту, но обычно ping_period равен 30 секундам. Напишем саб для отпавления HELLO-пакета и получения значения ping_period из него:
Код:
sub hello
{
print $sock make_mrim_packet($MRIM_CS_HELLO);
sysread ($sock, my $ack, 48); ## принимаем 48 байт (44 - хидер, 4 - данные)
my ($magic, $proto, $seq, $msg, $dlen, $from, $fromport, $r1, $r2, $r3, $r4, $data) = unpack ("L12", $ack);
$ping_period = $data; ## получаем значение ping_period
$seq_real++; ## $seq_real - это в моем коде счетчик seq
print "[+] connected..\n" if $data;
}
Заодно напишем и саб для осуществления пинга сервера. Он будет очень простым:
Код:
sub ping
{
print $sock make_mrim_packet($MRIM_CS_PING);
$seq_real++;
}
Теперь нужно переслать серверу пакет MRIM_CS_LOGIN2, который содержит информацию, необходимую для авторизации: LPS login, LPS password, UL status и LPS user_agent. login и password - это, понятно что, статус - тоже, а user_agent - это описание клиента, может быть любым. При удачной авторизации сервер отвечает нам пакетом MRIM_CS_LOGIN_ACK, при неудачной - MRIM_CS_LOGIN_REJ, в данных которого содержится причина отказа в авторизации. Теперь воплотим это в коде:
Код:
sub login
{
my $data = pack ("L", length($login)) . $login . ## упаковываем LPS-данные
pack ("L", length($password)) . $password .
pack ("L", $status) . ## а вот так UL
pack ("L", length($user_agent)) . $user_agent;
print "[~] try to login as $login:$password\n";
print $sock make_mrim_packet($MRIM_CS_LOGIN2, $data); ## посылаем пакет
$seq_real++; ## не забывая про счетчик сообщений
sysread ($sock, my $ack, 48); ## считываем ответ
my ($magic, $proto, $seq, $msg, $dlen, $from, $fromport, $r1, $r2, $r3, $r4, $data_ack) = unpack ("L12", $ack);
## и распаковываем его
if ($msg == $MRIM_CS_LOGIN_ACK) ## проверяем удачно ли прошла авторизация
{
print "[+] authorization succesfull\n";
}
elsif ($msg == $MRIM_CS_LOGIN_REJ)
{
print "[-] bad authorization:$data_ack\n";
}
else
{
print "[!] something wrong!\n";
}
}
[ Ложки нету =) ]
По сути, у нас есть уже все необходимое, чтоб вывести в онлайн наш mrim.pl. Но просто висящий в онлайне скрипт - это совсем неинтересно, и я решил добавить в статью код, отправяляющий сообщение на указанный адрес. Саб этот я упростил до безобразия, не сделав возможность устанавливать флаги, не сделав проверку на получение адресатом сообщения и еще многие вещи, которые можно было бы сделать (впрочем, практически во всех вышеприведенных сабах стоило бы доработать некоторые моменты).
Данные которые должен содержать пакет сообщения: UL flags, LPS to, LPS message, LPS rtf-message. Установив нужные флаги, можно указать тип сообщения (например, указать, что пересылаемые данные являются списком контактов). Я установил только флаг, означающий отсутствие необходимости присылать пакет о подтверждении доставки сообщения. Сообщение можно оформить и в rtf-формате, но мне это нафиг не нужно, потому в rtf-message отправляется ноль. Итак саб, отправляющий сообщение:
Код:
sub message
{
my ($to, $text) = @_; ## получаем мыло адресата и текст сообщения
my $data = pack ("L", $MESSAGE_FLAG_NORECV) . ## выставим нужный флаг
pack ("L", length($to)) . $to .
pack ("L", length($text)) . $text .
pack ("L", '0');
print $sock make_mrim_packet($MRIM_CS_MESSAGE, $data);
$seq_real++;
}
Теперь соберем все это воедино, добавив основной код:
Код:
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
## config ##
my $login = 'anti@mail.ru'; ## логин
my $password = 'g00dp4ss'; ## пасс
my $user_agent = 'mrim.pl'; ## описание агента
## constants ## ниже вставь константы из первой врезки с кодом
< ...constants here... >
## vars ##
my $seq_real = 0; ## счетчик комманд
my $ping_period = 30; ## интервал для пинга
my $status = $STATUS_ONLINE; ## статус
## main ##
my ($host, $port) = get_host_port(); ## берем хост:порт
print "[~] connecting to $host:$port..\n";
my $sock = new IO::Socket::INET ## коннектимся
(
PeerAddr => $host,
PeerPort => $port,
PeerProto => 'tcp',
TimeOut => 10
);
hello(); ## хеллоу и ..
login(); ## логинимся
sleep $ping_period; ## просто повисим в онлайне с полминуты
ping(); ## пинг
message('matrix@bk.ru', 'There is no spoon, Neo..');
## отправляем сообщение на matrix@bk.ru
sleep $ping_period; ## продолжаем еще некоторое время на-
ping(); ## ходиться в онлайне
sleep 10;
## subs ## а ниже добавь все сабы, которые присутствовали в статье
< ...subs here... >
## eof..
Запускаем:
Код:
D:\perl-mrim>perl mrim.pl
[~] connecting to 194.67.57.244:2041..
[+] connected..
[~] try to login as anti@mail.ru:g00dp4ss..
[+] authorization succesfull
Наблюдая за anti@mail.ru из официального mrim-клиента, видим как он выходит в онлайн, а затем получаем от него сообщение: "There is no spoon, Neo.." =)
[ outr0 ]
На этом все, т.к. приведенной информации уже вполне достаточно для того, чтоб ты мог начать писать свои тулзы, взаимодействующие с системой МАгент. Жду теперь интересных релизов по этой теме..
[ Links ]
http://agent.mail.ru - официальный сайт Mail.ru Агент
http://agent.mail.ru/dev-license.html - официальное описание протокола MMP
http://digimortal.0x48k.cc/articlz/mrim-packets.txt - отредактированное мной описание пакетов mrim
http://hellknights.void.ru - сайт моей тимы
http://0x48k.cc - форум DarkSide ResearcherZ
Специально для форума Античат..