Содержание статьи
Ты вряд ли пользуешься 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 года после долгих поисков формат все-таки покорился.
Хакер #159. Подделка контрольной суммы и ЭЦП с помощью коллизий
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
- Описание формата истории Mail.ru Агента до версии 5.4;
- обсуждение Mail.ru History Reader;
- мой блог;
- С++ класс для создания winhex.pos-файлов;
- три метода работы с занятыми файлами;
- различные способы получения списка процессов;
- уменьшение размера Си-программы на примере Visual Studio.
P.S.
К сожалению, формат журнала не позволяет привести здесь мои хардкорные изыскания полностью, поэтому поспеши заглянуть на диск. Надеюсь, пример кода читалки (exe’шник которой, кстати, с помощью небольшой оптимизации уместился всего в 2 килобайта безо всяких пакеров) поможет тебе в написании быстрого и крутого C-кода, а также в изучении hex-редакторов и других низкоуровневых вещей. Кстати, незатронутой осталась не менее увлекательная тема чтения истории ICQ-переписки, которая также хранится в файле mra.dbs. Спасибо компании Mail.Ru, во-первых, за разработку Mail.Ru Агента, во-вторых, за заметное развитие любимой аськи, и в-третьих, за интересный квест, о котором я тебе сегодня рассказал.