Ты вряд ли пользуешься Mail.Ru Агентом, но это бешено популярный сервис, который с каждым днем набирает обороты. По официальным данным месячная аудитория этого мессенджера в конце прошлого года составляла безумную цифру в 21,4 миллиона человек. Это легко объяснить, — продукт действительно удачный. Но сегодня я хочу рассказать о том, как был разреверсен файл с историей сообщений пользователя.

 

WARNING

Не забывай о статье 138 — «Нарушение тайны переписки, телефонных переговоров, почтовых, телеграфных или иных сообщений» УК РФ, а также о наличии в ней главы 28 — «Преступления в сфере компьютерной информации» (ст. 272, 273, 274).

 

История взлома

Эксперимент начался для меня еще в далеком 2008 году, когда друг попросил проверить переписку его девушки в Mail.ru Агенте. Тогда файл истории представлял из себя простой текстовик с названием emailhistory.txt и имел по сравнению с mra.dbs (файл, в котором в настоящее время хранится история переписки и данные о контактах) примитивную структуру. За пару часов был написан простой, но эффективный RTF-конвертер, который и делал всю грязную работу по вытягиванию переписки из Агента. Друг был в восторге. Далее, в ходе изучения программирования на компилируемых языках, я в качестве практики написал программу Mail.ru History Reader, описание которой попало на страницы ][ в августе 2009 года. Получив большое количество положительных отзывов, я опубликовал структуру формата тогдашнего файла истории (см. ссылки в боковом выносе) и исходники читалки. Однако Mail.ru Агент продолжал развиваться, и править балом стал новый продвинутый файл mra.dbs. После этого события ко мне посыпались тонны сообщений от различных людей с просьбами заняться им. В компании с SOLON7 мы ковыряли этот файл в HEX-редакторе, пытаясь найти структуры, ссылки на смещения и всевозможные изменения после запуска Mail.ru Агента. К концу 2010 года после долгих поисков формат все-таки покорился.

Идентификаторы начала переписки
Идентификаторы начала переписки

 

Rich Text Format (RTF)

RTF, использующийся в mra.dbs, представляет из себя формат хранения размеченных документов, предложенный еще в 1982 году бородатыми программистами из Microsoft и Adobe. Для его парсинга совершенно не обязательно изобретать велосипед, а достаточно лишь отправить сообщение EM_STREAMIN с флагом SF_RTF для записи и EM_STREAMOUT с флагом SF_TEXT для чтения:

EDITSTREAM es = { 0 };
es.pfnCallback = EditStreamCallback;
es.dwCookie = (DWORD_PTR)&lps;
SendMessage(hRich, EM_STREAMIN, SF_RTF, (LPARAM)&es); 

Этот нехитрый прием и использован в моей читалке.

 

Как добыть файл mra.dbs?

Ты, конечно, задашься вопросом: а где, собственно, хранится этот пресловутый mra.dbs, и как его добыть? Файл mra.dbs хранится в папке «%APPDATA%\Mra\Base\mra.dbs» (например «C:\Documents and Settings\user\Application Data\Mra\Base\mra.dbs»), и заполучить его при выключенном Агенте не так уж и сложно, достаточно лишь использовать функции ExpandEnvironmentStrings и CopyFile. Однако при включенном Агенте файл mra.dbs является занятым и система попросту не позволит его использовать. Для решения этой проблемы можно, например, временно отключить Агент (для этого действия тебе понадобятся привилегии отладчика, которые можно получить только с правами Администратора) или найти открытый хэндл файла в системе, а затем продублировать его в адресное пространство своего процесса. Также можно прочесть файл напрямую с диска (правда, для этого нужно знать, что такое кластер и как работать напрямую с драйвером файловой системы) или же написать собственный файловый драйвер (это практически нереально). Все бы хорошо, но на практике у всех вышеперечисленных методов есть свои недостатки. При перечислении хэндлов с помощью ZwQuerySystemInformation и их копировании к себе в процесс с помощью DuplicateHandle можно столкнуться с двумя проблемами. Первая заключается в том, что при вызове ZwQueryInformationFile поток может повиснуть, ожидая отклика от блокирующего именованного канала. Вторая — после копирования оба хэндла (наш и открывшего файл процесса) будут указывать на один FileObject, а следовательно — текущий режим ввода-вывода. Позиция в файле и другая связанная с файлом информация будут общими у двух процессов, поэтому даже чтение файла вызовет изменение позиции чтения и нарушение нормальной работы программы, открывшей файл. Конечно, можно приостановить на время все потоки процесса файла, а после копирования восстанавливать позиции чтения и запускать процесс владельца снова, но это связано с большими затратами времени и сил. Казалось бы, идеальным методом может являться прямое чтение с диска, но и здесь есть недостатки. Таким способом можно читать только файлы, которые открываются с доступом FILE_READ_ATTRIBUTES (кроме файлов подкачки), файл обязательно должен быть не сжат, не зашифрован (иначе мы прочитаем ерунду) и иметь свой кластер (маленькие файлы в NTFS могут целиком размещаться в MFT). Также следует учесть, что во время чтения файл может быть изменен (и мы получим в результате непонятно что). Поэтому разберем самый простой метод с временным отключением процесса Агента.

Итак, чтобы убить процесс Mail.ru Агента, для начала необходимо узнать его идентификатор (ProcessID). Сделать это можно разными способами: через ToolHelp API, через Native API (используя функцию ZwQuerySystemInformation), прошерстив список открытых хэндлов или по списку открытых процессом окон (GetWindowThreadProcessId). Самый легкий вариант — это использование ToolHelp API и поиск по имени exe-файла. Для этого достаточно вызвать функции CreateToolhelp32Snapshot > Process32First > Process32Next, а затем в теле цикла сверять значение поля szExeFile структуры PROCESSENTRY32 c magent.exe. Необходимый нам ProcessID находится в этой же структуре, поле th32ProcessID:

hProcessSnap = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
if( INVALID_HANDLE_VALUE != hProcessSnap)
{
 pe32.dwSize = sizeof( PROCESSENTRY32 );
 if( Process32First( hProcessSnap, &pe32 ) )
 {
 do
 {
  if(0 == lstrcmp(pe32.szExeFile,_TEXT("magent.exe")))
  {
  pid=pe32.th32ProcessID;
  break;
  }
 } while(Process32Next( hProcessSnap, &pe32 ));
 }
 CloseHandle( hProcessSnap );
}

После того как мы найдем PID, нам необходимо получить привилегии отладчика SeDebugPrivilege (OpenProcessToken > LookupPrivilegeValue > AdjustTokenPrivileges) и убить процесс (OpenProcess > TerminateProcess), а потом снова попытаться вызвать CopyFile. Привилегии можно получить и более элегантным путем — через Native API:

void GetPrivilege(IN ULONG Privilege)
{
BOOLEAN OldValue;
RtlAdjustPrivilege(Privilege, TRUE, FALSE, &OldValue);
}

Все, mra.dbs у нас в руках. Теперь перейдем к его потрошению :).

 

Раскрываем секреты mra.dbs

Файл mra.dbs представляет из себя дамп памяти Mail.ru Агента, поэтому открыть его для чтения при работающей программе не представляется возможным (для рядового программиста, но у нас свои секреты :), также задачу усложняет тот факт, что в памяти все числа хранятся в перевернутом виде. Однако давай немного углубимся в реверс-инжиниринг.

Структура записи сообщения
Структура записи сообщения

Итак, в недрах mra.dbs существует хеш-таблица, в которой описаны смещения на 4-байтные идентификаторы. Идентификаторы служат для определения начала записи различных структур и сегментов дампа, среди которых и находятся нужные нам записи истории переписки (обрати внимание на соответствующую иллюстрацию):

typedef struct _ids {
unsigned int id1;
unsigned int id2;
unsigned int count;
} _ids;

 

INFO

Хеш-таблица — это структура данных, реализующая интерфейс ассоциативного массива, она позволяет хранить пары «ключ-значение». Двусвязный список состоит из элементов данных, каждый из которых содержит ссылки как на следующий, так и на предыдущий элементы.

Начало истории характеризуется ключевым словом mrahistory_, за которым следует e-mail хозяина файла mra.dbs и e-mail контакта, с которым ведется переписка. В случае с историей идентификаторы образуют двусвязный список: первый ведет к первому отправленному сообщению, а второй — к последнему принятому сообщению. Количество сообщений можно узнать, изучив четыре байта после идентификаторов (структура _ids). Пройдя по смещению идентификатора (его можно узнать из хеш-таблицы) мы попадем на запись сообщения (снова все внимание на соответствующий рисунок):

Поиск хеш-таблицы
Поиск хеш-таблицы
struct _message{
unsigned int size;
unsigned int prev_id;
unsigned int next_id;
unsigned int xz1;
FILETIME time;
unsigned int type_mesage;
char flag_incoming;
char byte[3];
unsigned int count_nick;
unsigned int magic_num; // 0x38
unsigned int count_message;
unsigned int xz2; 
unsigned int size_lps_rtf; 
unsigned int xz3; 
}; 

Строки в дампе сохраняются в кодировке Unicode (wchar_t) различными способами:

  • с завершающим нулем в конце строки;
  • в структуре LPS (название структуры взято из описания формата протокола MMP), где первые четыре байта указывают на длину последующей строки;
  • в формате RTF.

Зная количество сообщений, нам не составит труда пробежаться по всей цепочке. Но откуда вообще узнать, где находится эта хеш-таблица, и как найти начало записей истории? Над поисками ответов к этим вопросам мы с SOLON7 провели немало бессонных ночей.

 

Типы сообщений mra.dbs

  • 2 — неавторизованные пользователи;
  • 4 — запросы авторизации;
  • 7 — обычные сообщения;
  • 10 — передача файлов;
  • 35 — записи в микроблог;
  • 46 — смена геоположения.

Немного магии

По смещению 0x10 от начала файла mra.dbs, как оказалось, и хранится адрес заветной хеш-таблицы. Пройдя по смещению первого индекса из хеш-таблицы, мы натыкаемся на структуру начальных данных. Возможно, там находится вообще вся информация, заложенная в mra.dbs. Идем дальше. По смещению 0x20 в этой структуре хранится количество записей истории или, проще говоря, количество переписок. Так как файл дампа постоянно расширяется, то по смещению 0x2C лежит идентификатор последней записанной истории, — это все, что нам нужно знать, чтобы начать искать идентификаторы переписок. В целом же алгоритм такой:

  • проходимся по идентификаторам записей истории с помощью цикла (начиная от последней добавленной записи);
  • если в этой записи от смещения 0x190 присутствует слово «mrahistory_», то это означает, что по смещению 0x24 лежат идентификаторы цепочки сообщений данной переписки.

Чтобы стало немного понятней, взгляни на этот код:

DWORD * offset_table=(DWORD *)(mra_base + *(DWORD*)(mra_base + 0x10));
DWORD end_id_mail=*(DWORD*)(mra_base+0x20+offset_table[1]);
DWORD count_emails=*(DWORD*)(mra_base+0x2C+offset_table[1]);
...
for(int i=0;i<count_emails;i++) {
_ids *mail_data=(struct _ids*)(mra_base+offset_table[end_id_mail]+4);
if(memmem(((unsigned char*)mail_data+0x190), mrahistory,...)) {
emails[k].id=(_ids*)((unsigned char*)mail_data+0x24);
...
}
end_id_mail=mail_data->id2;
}    

Интерфейс моей читалки
Интерфейс моей читалки
 

Кодим

Сейчас я покажу тебе лишь самые основные моменты. Итак, файл mra.dbs является дампом памяти, поэтому мы не будем извращаться и использовать функции для работы с файловыми смещениями, а сразу поместим его в память нашей программы. Для этого заюзаем ресурсы ОС Windows и создадим Memory Mapped файл:

CreateFile
CreateFileMap
MapViewOfFile
VirtualFree
CloseHandle
CloseHandle    

Так как нам не нужно сохранять внесенные изменения обратно в файл, то здесь вместо UnmapViewOfFile используется VirtualFree. Первое, что мы сделаем, это найдем все контакты из истории переписки. Хранить найденное добро будем в структуре emails:

typedef struct _emails{
wchar_t *email;
_ids *id;
};
...
struct _emails *emails;
...
emails=VirtualAlloc(NULL,count_emails*sizeof(struct _emails),..);
...        

После прохода по идентификаторам и поиска строки «mrahistory_» наша структура будет заполнена адресами идентификаторов. Заметь, при этом мы не скопировали даже байта и израсходовали всего лишь 16*count_emails байт (например, при 1 000 контактов мы используем всего ~15 килобайт памяти). Теперь, имея на руках идентификаторы начала переписки с конкретным пользователем, мы можем прочитать сообщения:

int id_message=emails[k].id->id1;
for(int i=0;i<emails[k].id->count_messages;i++)
{
_message *mes=(_message *)(mra_base+offset_table[id_message]);
wchar_t *str=(wchar_t *)((unsigned char *)mes+sizeof(_message));
...
id_message=mes->prev_id;
}

Дата сообщения хранится в формате FILETIME, для удобства ее можно перевести в удобочитаемый вид с помощью функции FileTimeToSystemTime. Формат RTF отлично воспринимается Rich Edit’ом и любыми другими стандартными редакторами типа WordPad. Но с этим можно и не заморачиваться, так как сообщения хранятся в неотформатированном виде сразу после ника, а их размер указан в структуре message. Это все, что тебе нужно знать, чтобы получить удобоваримый список мессаг из Агента.

 

WWW

 

P.S.

К сожалению, формат журнала не позволяет привести здесь мои хардкорные изыскания полностью, поэтому поспеши заглянуть на диск. Надеюсь, пример кода читалки (exe’шник которой, кстати, с помощью небольшой оптимизации уместился всего в 2 килобайта безо всяких пакеров) поможет тебе в написании быстрого и крутого C-кода, а также в изучении hex-редакторов и других низкоуровневых вещей. Кстати, незатронутой осталась не менее увлекательная тема чтения истории ICQ-переписки, которая также хранится в файле mra.dbs. Спасибо компании Mail.Ru, во-первых, за разработку Mail.Ru Агента, во-вторых, за заметное развитие любимой аськи, и в-третьих, за интересный квест, о котором я тебе сегодня рассказал.

 

Оставить мнение

Check Also

Espruino Pico. Учимся программировать USB-микроконтроллер на JavaScript и делаем из него токен авторизации

Несмотря на огромное количество устройств на базе микроконтроллеров, созданных на волне ус…