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

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

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

Эксперимент начался для меня еще в далеком 2008 году, когда друг попросил проверить переписку его девушки в Mail.ru Агенте. Тогда файл истории представлял из себя простой текстовик с названием email_history.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;

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

Начало истории характеризуется ключевым словом 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 Агента, во-вторых, за заметное развитие любимой аськи, и в-третьих, за интересный квест, о котором я тебе сегодня рассказал.

Эта статья была опубликована журнале «Хакер»