Авторские статьи @ mrim.pl: Написание скриптов, работающих по протоколу MMP

Discussion in 'Статьи' started by Digimortal, 16 Apr 2007.

  1. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    @ 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 _ - = +].


    [ Структура пакетов ]

    Рассмотрим теперь структуру пакетов протокола. Как и полагается, пакет состоит из заголовка и данных (данные могут и отсутствовать). Поля хидера:

    Code:
    	 <-4bytes->
    	,----------.
    	|   magic  |	мэйджик
    	+----------+
    	|   proto  |	версия протокола
    	+----------+
    	|    seq   |	номер сообщения в текущем соединении (ответ будет иметь тот же номер)
    	+----------+
    	|    msg   |	тип пакета
    	+----------+
    	|   dlen   |	длина данных (без учета заголовка)
    	+----------+
    	|   from   |	ip отправителя в inet_aton() формате
    	+----------+
    	| fromport |	порт отправителя
    	+----------+
    	|          |    зарезервированные 16 байт, которые
    	+-        -+	в текущих версиях протокола не используются
    	| reserved |
    	+-        -+
    	|          |
    	+-        -+
    	|          |
    	+==========+
    	|   data   |	далее идут данные (если есть)...
    	'~~~~~~~~~~'
    
    Самым важным для нас полем является "тип пакета". "Мэйджик" и "версия" у нас постоянны. "Номер сообщения" и "длина данных" подсчитуются по ходу дела, а правильный ip и port, как я понял, передавать вообще не обязательно, поэтому не спрашуй у меня, почему я заполняю их нулями (с значением поля seq тоже дозволены некоторые вольности).

    Тип пакета - это команды (или ответы на них), которыми обмениваются между собой клиент и сервер. Команды могут иметь параметры, передающиеся как данные пакета. Значения этих команд и параметров иможно взять из того файла, что прикрепил к статье. Я сразу приведу здесь те, что будут использоваться в приводимых далее примерах:

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

    Code:
    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 для коннекта:

    Code:
    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-адрес и порт, мы можем создавать соединение с сервером для прохождения авторизации, которая происходит следующим образом:

    Code:
    		,---.                              ,---.
    		| 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 из него:

    Code:
    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;
    }
    
    Заодно напишем и саб для осуществления пинга сервера. Он будет очень простым:

    Code:
    	
    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, в данных которого содержится причина отказа в авторизации. Теперь воплотим это в коде:

    Code:
    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 отправляется ноль. Итак саб, отправляющий сообщение:

    Code:
    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++;
    
    }
    
    Теперь соберем все это воедино, добавив основной код:

    Code:
    #!/usr/bin/perl
    
    use strict;
    use warnings;
    use IO::Socket::INET;
    
    
    ## config ## 
    	
    my $login	= '[email protected]';	## логин
    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('[email protected]', 'There is no spoon, Neo..');
    				## отправляем сообщение на [email protected]
    sleep $ping_period;		## продолжаем еще некоторое время на-
    ping();				## ходиться в онлайне
    sleep 10;
    	
    ##  subs  ## а ниже добавь все сабы, которые присутствовали в статье
    
    		< ...subs here... >
    
    ## eof..	
    
    Запускаем:

    Code:
    D:\perl-mrim>perl mrim.pl
    [~] connecting to 194.67.57.244:2041..
    [+] connected..
    [~] try to login as [email protected]:g00dp4ss..
    [+] authorization succesfull
    
    Наблюдая за [email protected] из официального 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



    Специально для форума Античат..
     
    #1 Digimortal, 16 Apr 2007
    Last edited: 16 Apr 2007
    23 people like this.
  2. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    кста, посоветуйте хороший сниффер
     
  3. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    wireshark (он же ethereal)..
     
  4. KSURi

    KSURi tnega AOLPS

    Joined:
    6 Jun 2006
    Messages:
    458
    Likes Received:
    219
    Reputations:
    357
    Весьма познавательно)
    Насчет кода: я никак не пойму, зачем многие люди юзают sysread/write ? Ты вроде юзаешь IO::Socket, так почему нельзя обойти просто $sock->send/recv ? Тем более, что sysread/write могут вызывать проблемы в некоторых ситуациях...

    ЗЫ:
    Тем не менее, этот человек разобрался в протоколе довольно глубоко)
     
  5. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    KSURi упорно подражает Perl Underground)))
    шутко
    зачем ты придераешься к коду? ведь фишка статьи обьяснить работу протокола а не показать примеры красивого кода. Обычный пример.
    я думаю ты излишне строг.
     
  6. fucker"ok

    fucker"ok Elder - Старейшина

    Joined:
    21 Nov 2004
    Messages:
    580
    Likes Received:
    279
    Reputations:
    91
    Хорошо. Многим будет интересно. В своё время разобрался в этом протоколе и очень полезный опыт получил. Никогда до того бинарными протоколами не занимался.
     
  7. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    тут ты прав.. немного позже поправлю код.. (у меня почему-то уже вошло в привычку использовать IO::Socket тока для открытия сокета =/)..

    да нет, KSURi все верно говорит.. ) и Perl Underground - это тру ).. если не обращать внимание на "мелкие" погрешности в своем коде, то они со временем становятся привычкой, и каждый твой скрипт будет в итоге говном.. а код просто обязан быть красивым, имхо =)
     
    #7 Digimortal, 16 Apr 2007
    Last edited: 16 Apr 2007
    1 person likes this.
  8. KSURi

    KSURi tnega AOLPS

    Joined:
    6 Jun 2006
    Messages:
    458
    Likes Received:
    219
    Reputations:
    357
    Не совсем верно. Я взял себе за правило писать на более-менее хорошем тоне, а не подражать Perl Underground. Езин всего лишь толкает хороший тон в массы, он его не изобрел.

    А по поводу sysread/write:
    Т.е. при использовнии sysread и print (или syswrite и while <$sock>) могут получиться проблемы.

    За исследование протокола я уже сказал спасибо в виде +мах. Жду исследований на тему отправки смс ;)
     
  9. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    такс. стоп. где в этом пакете время пинга($ping_period)? не могу найти
     
  10. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    1E 00 00 00 - так вот оно )
    1Eh = 30d
     
  11. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    хех, я после того как запостил сам ужо сообразил)
     
  12. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    кстати, я тока щас обратил внимание на то, что зарезервированные поля похоже все же используются в текущей версии протокола:
    Code:
    
    EF BE AD DE 0D 00 01 00 00 00 00 00 02 10 00 00   я╛н▐............
    04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
    B8 21 E7 0E 44 65 86 BF A8 1F 86 BF 1E 00 00 00   ╕!ч.DeЖ┐и.Ж┐....
    
    EF BE AD DE 0D 00 01 00 01 00 00 00 04 10 00 00   я╛н▐............
    00 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00   ................
    1F 00 00 00 0A 00 00 2B 07 F8 00 00               .......+.°..
    
     
  13. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    А не знаете ещё сниферов, что б видеть трафф только одной проги, а не всех?
    и кста, не могу найти в пакете место где находится номер пакета (в коде $seq_real)
     
    #13 x-treem, 16 Apr 2007
    Last edited: 16 Apr 2007
  14. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    там же фильтры настраиваются.. : /

    а номер пакета с 9-го по 12-й байт включительно занимает..
     
    #14 Digimortal, 16 Apr 2007
    Last edited: 16 Apr 2007
  15. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    угу, спасибо
    вот ещё фишка
    когда в клиент мессдж идёт, надо выслать потверждение.
    знаешь как?
    что то я пока не разберусь
     
  16. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    гляди в описании пакетов, там все это расписано.. и если ты вникнешь в содержимое статьи, то все будет предельно ясно:
    Code:
    	[ Доставка сообщения (sc): MRIM_CS_MESSAGE_ACK = 0x1009 ]
    
    	UL msg_id 	## Номер пакета (Sequence) этого сообщения для отправителя
    	UL flags 	## Возможные значения описаны в MRIM_CS_MESSAGE
    	LPS from 	## Адрес отправителя
    	LPS message 	## текстовая версия сообщения
    	LPS rtf-message ## форматированная версия сообщения
    
    
    
    	[ Подтверждение получения сообщения (cs): MRIM_CS_MESSAGE_RECV = 0x1011 ]
    
    	LPS from
    	UL msg_id
    
    	~Отправляется получателем сообщения сразу после прихода MRIM_CS_MESSAGE_ACK, если флаги MRIM_CS_MESSAGE_ACK не содержали MESSAGE_FLAG_NORECV. from и msg_id должны быть скопированы из MRIM_CS_MESSAGE_ACK и имеют то же значение. 
    
    тоесть, получив сообщение (MRIM_CS_MESSAGE_ACK), ты должен вытащить из данных LPS from, UL msg_id и отправить их как данные в пакете MRIM_CS_MESSAGE_RECV. Все это можно сделать по аналогии с уже приведенными в статье примерами кода..
     
  17. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    мне сразу несколько приходит
    и хз откуда выдирать.
    точнее думаю с первого, но не могу догнать что за остальные пакеты.
     
  18. Digimortal

    Digimortal Banned

    Joined:
    22 Aug 2006
    Messages:
    471
    Likes Received:
    248
    Reputations:
    189
    это все один пакет )
    просто в примерах в статье я всегда знал сколько будет размер получаемого пакета и выставлял его в sysread, но в твоем случае размер пакета заранее неизвестен - ведь данные могут быть различной длины.. у тебя первое - это заголовок - из него можно взять длину данных 1A 01 00 00 = 11Ah = 282 байта, а затем принять эти 282 байта - это будут данные пакета.. из этих данных и брать нужные значения..
     
  19. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    наверное я идиот(
     
  20. x-treem

    x-treem Elder - Старейшина

    Joined:
    8 Nov 2006
    Messages:
    130
    Likes Received:
    16
    Reputations:
    0
    вот посмотри подалуйсто:

    сначало вроде всё понятно, но вот что такое
    это ведь не просто мусор?