Как стать автором
Обновить

Пишем viewer почтовой базы MS Exchange (часть 1)

Время на прочтение19 мин
Количество просмотров13K

В одной большой-пребольшой стране жили маленькие-премаленькие люди. И все было прекрасно пока не появилась глубокая-преглубокая яма прямо в центре этой большой, а точнее пребольшой страны. Ну, надо сказать, появилась она не одна и, конечно, не сразу, но никто об этом уже не вспомнит и никому это уже не важно.

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

И самые пресамые приступили. Изучали и засыпали, засыпали и изучали, изучали, засыпали, засыпали, изучали, и повторялось это не то, чтобы много, а очень много раз.

Так получилось, что наши маленькие самые-пресамые герои ушли, а точнее им пришлось уйти, а яма осталась. Но пришли новые герои, такие же маленькие, но и такие же самые-самые, и начали делать тоже самое.

В общем, продолжалось это столько же долго, пока не остался один самый. И в один прекрасный день яма исчезла, правда сама, и появилась где-то в совершенно другом месте, где ее вновь начали изучать и закапывать…

Все совпадения случайны, все вымышленно и никогда ничего подобного не было и не могло быть в действительности! Правда-правда, ну, или почти правда.

Об одной такой «яме» речь и пойдет далее. Но т.к. Privacy Policy мы затронем только небольшой ее кусочек, до которого можно добраться использую только открытые источники и документированное API.

Материал будет почти эксклюзивный т.к. информации по теме крупицы.

Вы никогда не думали где MS Exchange Server хранит всю вашу почту, или как он с ней работает на самом низком уровне? Вот немного об этом я и собираюсь здесь написать.

Предупреждение: Не пытайтесь глубоко лезть в эту тему, и всей жизни не хватит. Я предупредил.

Введение


MS Exchange Server (далее просто Exchange) является одним из флагманов в линейке продуктов корпорации Microsoft. Про его основные функции можно прочитать в wiki или на официальном сайте. Если коротко, то это своеобразный «комбайн» для работы с почтой, календарями и другими пользовательскими данными, имеющий широкие возможности интеграции с различными продуктами MS (SharePoint, TFS, и т.д.).

Но в рамках этой статьи нас будет интересовать не то, что он предоставляет конечному пользователю, а то откуда он берет эти данные и какое для этого использует API. Мы будем пытаться самостоятельно прочитать базу почтовых ящиков на Mailbox'ой роли Exchange 2010 (Mailbox Server).

Exchange имеет несколько точек входа (CAS Server), через которые пользователь может получить доступ к своим данным и несколько протоколов которые он может для этого использовать, например, OWA, RPC (Outlook), POP3/IMAP4.

В независимости от способа получение доступа Exchange направляет все запросы на Mailbox'ую роль (до Exchange 2007 эта была единственная роль), на которой, помимо прочего, находятся базы пользовательских почтовых ящиков (mailbox databases), которые нас сегодня и будут интересовать. Физически эти базы размещены на жестком диске внутри файлов *.edb. Их можно найти в папке Mailbox\<имя базы> в директории, куда был установлен Exchange. Помимо этого там размещаются логи транзакций и прочие файлы, связанные с жизненным циклом баз, но нам они не понадобятся, самое основное для нас это *.edb.

Если немного покопаться, то можно выяснить, что для доступа к содержимому баз Exchange использует Extensible Storage Engine (ESE)! А если еще покопаться, то становится ясно, что реализация функций ESE находится в библиотеке ese.dll (или esent.dll). Это ядро всех операций осуществляемых Exchange'ом. ESE предоставляет обширный набор средств для работы с базой данный. Описание функций, констант, структур и всего что может понадобиться можно найти здесь. К сожалению, эта документацию давно не обновлялась, поэтому там нет ряда функций появившихся в Exchange 2010, но в рамках этого топика они нам не понадобятся. Найти ese.dll можно в папке Bin внутри основной директории Exchange.

Почитав описание ESE становится ясно, что данные хранятся в табличном виде, т.е. наборе таблиц с столбцами и строками. Ячейки таблицы могут быть разных типов, ну, и плюс все особенности баз: индексы, поиск и т.п.

Итого, мы знаем что Exchange хранит свои базы на Mailbox роли, в виде файлов с расширением EDB, а доступ к ним осуществляет благодаря ESE (ese.dll). Этого нам достаточно и мы можем приступать к кодированию.

Мы перечислим таблицы в базе, а также все колонки и столбцы. Конечно мы никогда не узнаем, что они означают т.к. об этом знает только MS, ну, и еще несколько человек. В этим самых колонках должна быть почти вся информация связанная с почтовыми ящиками, начиная от имен папок у пользователя, заканчивая письмами, но понять где что уже задача реверсинженеринга и здесь рассматриваться не будет.

Программирование


Подготовка

Для начала нам понадобятся:
  • Visual Studio 2008-2010
  • MS Exchange Server 2010 (подойдет и любой другой, но в данном топике мы будем говорить именно про 2010)
  • Знание C/C++
В Exchange создаем базу, которую и будем пытаться прочитать. Для этого можно воспользоваться Exchange Management Console (EMC). Описывать процедуру не буду т.к. в интернете много информации на эту тему. Создадим в этой базе одного пользователя (через тот же EMC), чтобы в ней было какое-нибудь содержимое и залогонимся этим пользователем в свой почтовый ящик, чтобы проверить, что все сделано правильно, например, через OWA. После этого идем в директорию Mailbox и ищем там папку с именем нашей базы, а в ней EDB файл. Перед тем как скопировать базу размонтируем ее через EMC. Все, база для экспериментов у нас есть. Копируем ее куда-нибудь, например, в директорию будущего проекта.

Из папки Bin копируем ese.dll, благодаря которой будем работать с базой.

В Visual Studio создаем консольный C++ проект. Здесь важный нюанс, т.к. Exchange 2010 (в отличии от всех предыдущих версий) имеет только 64 битный вариант, то и проект нам придется создавать с поддержкой x64, т.к. иначе мы просто не сможем загрузить ese.dll в наше адресное пространство. Поэтому для тестирования приложения нужна 64 битная версия ОС, можно конечно тестировать и на самом Exchange, но я для этой цели использую свою рабочую станцию с Windows 7. Также мы будем использовать unicode версию API поэтому в проекте лучше сделать unicode кодировкой по умолчанию.

И так, в вновь созданном проекте убеждаемся, что стоят поддержка x64 и Unicode (General — Use Unicode Character Set). Теперь подключаем основной для ESE заголовочный файл:
#include <esent.h>
Данный файл идет с SDK вместе c студией начиная с VS 2008.
В stdafx.h добавляем 2 дефайна, с версией JET (ESE), и указываем, что мы хотим использовать юникодную версию API:
#define JET_UNICODE
#define JET_VERSION 0x0600

Хорошо, теперь нужно определится, что мы хотим получить из базы. ESE представляет собой базу с таблицами, колонками и строками, именно это мы и попытаемся из нее извлечь: таблицы, колонки и строки. Для этого подготовим следующие структуры:
typedef struct tagDBColumnsInfo
{
    std::wstring sColumnName;        
    std::vector<std::wstring> sColumnValues;      
}SDBColumnInfo;
 
typedef struct tagDBTableInfo
{
    std::wstring sTableName;         
    std::vector<SDBColumnInfo> sColumnInfo;
}SDBTableInfo;
 
typedef struct tagDBTablesInfo
{
    std::wstring sDBName;            
    std::vector<SDBTableInfo> sTablesInfo;
}SDBTablesInfo;

Первым делом нужно загрузить саму DLL, делаем это, как всегда, через ::LoadLibrary(...).
Функция из ese.dll мы будем загружать динамически и нам понадобятся следующие функции:
  • JetInit
  • JetCreateInstanceW
  • JetBeginSessionW
  • JetAttachDatabaseW
  • JetOpenDatabaseW
  • JetCloseDatabase
  • JetDetachDatabaseW
  • JetTerm
  • JetSetSystemParameterW
  • JetOpenTableW
  • JetGetColumnInfoW
  • JetRetrieveColumns
  • JetMove
  • JetGetTableColumnInfoW
  • JetCloseTable
  • JetGetSystemParameter

Открытие базы

После того как мы успешно загрузили нужные нам функции начинаем непосредственно читать базу. Согласно MSDN необходимо указать database page size, через установку параметра JET_paramDatabasePageSize (esent.h). Здесь появляется сложность т.к. узнать эту величину имея только EDB файл нельзя, а указать нужно точно иначе база не откроется. Это можно сделать через eseutils (идет в комплекте с Exchange), но я пошел немного другим путем, и выяснил, что эта величина константа для одинаковых версий Exchange и всегда кратна 4096. Так экспериментально выяснилось, что для Exchange 2010 она равна 32768.

Ok, первым делом задаем величину page size:
JET_ERR jRes = _JetSetSystemParameter ( NULLNULL, JET_paramDatabasePageSize, 32768NULL );


JET_ERR — это просто long, который содержит код ошибки. Превратить этот код в текстовое описание можно функцией JetGetSystemParameter (аля ::FormatMessage(...)):
JetGetSystemParameter ( m_instance, m_sesid, JET_paramErrorToString,
     reinterpret_cast<JET_API_PTR *>(&jeterror), cBuff, MAX_BUFFER_SIZE );

Для удобства разбора error-кода я использую следующий макрос (m_cLog это мой внутренний класс логирования):
#define WRITE_TO_LOG_AND_RETURN_IF_ERROR( jeterror ) \
if ( jeterror ) { \
char cBuff[MAX_BUFFER_SIZE] = {0}; \
if ( m_instance )_JetGetSystemParameter ( m_instance, m_sesid, \
     JET_paramErrorToString, reinterpret_cast<JET_API_PTR *>(&jeterror), cBuff, MAX_BUFFER_SIZE ); \
m_cLog.write ( m_sEDBPath, cBuff, jeterror,  __FILE__, __LINE__ ); \
return jeterror ; }


Теперь нужно отключить callback'и специфические для Exchange, т.к. мы о них ничего не знаем:
jRes = _JetSetSystemParameter ( NULLNULL, JET_paramDisableCallbacks, trueNULL );


Далее создаем новый instance (JET_INSTANCE m_instance) для работы с базой:
jRes = _JetCreateInstance ( &m_instance, NULL );


Выполняем инициализацию созданного instance'а для начала работы с базой:
jRes = _JetInit ( &m_instance );


Начинаем новую сессию (JET_SESID m_sesid):
jRes = _JetBeginSession ( m_instance, &m_sesid, NULLNULL );


Подключаем наш EDB файл:
jRes = _JetAttachDatabase ( m_sesid, L"demo.edb", JET_bitDbReadOnly );


И открываем его:
jRes = _JetOpenDatabase ( m_sesid, L"demo.edb"NULL&m_dbid, JET_bitDbReadOnly );


Итого, если все функции вернули JET_errSuccess, то база открыта, а значит можно приступать к чтению содержимого.

Далее будет немного кода. Буду его приводить т.к. по этой теме его днем с огнем не сыщешь.

Перечисляем таблицы

Для перечисления напишем следующую функцию:
JET_ERR CJetDBReaderCore::EnumRootTables ( SDBTablesInfo &sDBTablesInfo )
{
    sDBTablesInfo.sDBName    = m_sEDBPath;
    JET_ERR jRes             = OpenTable ( ROOT_TABLE );
    if ( jRes == JET_errSuccess )
    {
        JET_COLUMNBASE        sNameInfo,
                                  sTypeInfo;
        if (    !ReadFromTable ( ROOT_TABLE, NAME_COLUMN, sNameInfo ) &&
                !ReadFromTable ( ROOT_TABLE, TYPE_COLUMN, sTypeInfo ) )
        {
            JET_RETRIEVECOLUMN sJetRC[2];
            sJetRC[0].columnid = sNameInfo.columnid;
            sJetRC[0].cbData = sNameInfo.cbMax;
            sJetRC[0].itagSequence = 1;
            sJetRC[0].grbit = 0;
            CHAR szName[MAX_BUFFER_SIZE];
            sJetRC[0].pvData = szName;
 
            sJetRC[1].columnid = sTypeInfo.columnid;
            sJetRC[1].cbData = sTypeInfo.cbMax;
            sJetRC[1].itagSequence = 1;
            sJetRC[1].grbit = 0;
            WORD wType;
            sJetRC[1].pvData = &wType;
 
            do 
            {
                jRes = GetColumns ( ROOT_TABLE, sJetRC, 2 );
                if ( jRes != JET_errSuccess ) return jRes;
                if ( wType == 1 )
                {
                    szName[sJetRC[0].cbActual] = 0;
 
                    SDBTableInfo sTableInfo;
                    std::string tmp (szName);
                    sTableInfo.sTableName.assign(tmp.begin(), tmp.end());
 
                    sDBTablesInfo.sTablesInfo.push_back ( sTableInfo );
                }
 
            } while( !TableEnd ( ROOT_TABLE ) );
        }
 
        jRes = CloseTable ( ROOT_TABLE );
    }
 
    return jRes;
}

Где:
  • ROOT_TABLE — «MSysObjects», назовем эту таблицу root'ой, т.к. она содержит список всех остальных таблиц в базе.
  • NAME_COLUMN — «Name», колонка содержащая имена всех таблиц.
  • TYPE_COLUMN — «Type», колонка содержащая тип таблицы.

Как видно в коде, сначала мы открываем root'овую таблицу, это делается через функцию JetOpenTable:
JET_ERR CJetDBReaderCore::OpenTable ( std::wstring sTableName )
{
    std::map<std::wstring,JET_TABLEID>::const_iterator iter = m_tables.find ( sTableName );
    if ( iter == m_tables.end() )
    {
        JET_TABLEID        tableid ( 0 );
        JET_ERR            jRes = _JetOpenTable ( m_sesid, m_dbid, sTableName.c_str()NULL
            0, JET_bitTableReadOnly, &tableid );
        WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
 
        m_tables[sTableName] = tableid;
    }
 
    return JET_errSuccess;
}

Далее получаем информацию о колонках внутри ReadFromTable, т.к. нам нужно ее Id, чтобы получить содержимое:
JET_ERR CJetDBReaderCore::ReadFromTable ( 
    std::wstring sTableName, 
    std::wstring sColumnName, 
    JET_COLUMNBASE &sColumnBase )
{
    std::map <std::wstring, JET_TABLEID>::const_iterator iter = m_tables.find ( sTableName );
    if ( iter != m_tables.end() )
    {
        JET_ERR    jRes = _JetGetColumnInfo ( m_sesid, m_dbid, sTableName.c_str()
            sColumnName.c_str()&sColumnBase, sizeof ( JET_COLUMNBASE  ), JET_ColInfoBase );
        WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
    }
 
    return JET_errSuccess;
}

Имея Id заполняем структуру JET_RETRIEVECOLUMN, имея которую делаем JetRetrieveColumns внутри GetColumns, для получения имени таблицы:
JET_ERR CJetDBReaderCore::GetColumns ( 
    std::wstring sTableName, 
    JET_RETRIEVECOLUMN *sJetRC, 
    INT nCount )
{
    std::map <std::wstring, JET_TABLEID>::const_iterator iter = m_tables.find ( sTableName );
    if ( iter != m_tables.end() )
    {
        JET_ERR    jRes = _JetRetrieveColumns ( m_sesid, iter->second, sJetRC, nCount);
        WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
    }
 
    return JET_errSuccess;
}

Все, список таблиц получили, переходим к получению содержимого колонок. Будем для каждой таблицы получать список колонок в ней, и по мере получения будем сохранять эту информацию в наши структуры.

Перечисляем колонки

Напишем следующую функцию:
JET_ERR CJetDBReaderCore::EnumColumns ( 
    SDBTableInfo &sTableInfo, 
    std::list<SColumnInfo> &sColumnsInfo )
{
    if ( !OpenTable ( sTableInfo.sTableName ) )
    {
        JET_COLUMNLIST sColumnInfo;
        GetTableColumnInfo ( sTableInfo.sTableName&sColumnInfo );
        MoveToFirst ( sTableInfo.sTableName );
 
        char szNameBuff[MAX_BUFFER_SIZE];
        do
        {
            SColumnInfo            ci;
            JET_RETRIEVECOLUMN    sJetRC[4];
 
            sJetRC[0].columnid = sColumnInfo.columnidcolumnname;
            sJetRC[0].cbData = sizeof(szNameBuff);
            sJetRC[0].itagSequence = 1;
            sJetRC[0].grbit = 0;
            sJetRC[0].pvData = szNameBuff;
 
            sJetRC[1].columnid = sColumnInfo.columnidcolumnid;
            sJetRC[1].cbData = sizeof(DWORD);
            sJetRC[1].itagSequence = 1;
            sJetRC[1].grbit = 0;
            sJetRC[1].pvData = &ci.dwId;
 
            sJetRC[2].columnid = sColumnInfo.columnidcoltyp;
            sJetRC[2].cbData = sizeof(DWORD);
            sJetRC[2].itagSequence = 1;
            sJetRC[2].grbit = 0;
            sJetRC[2].pvData = &ci.dwType;
 
            sJetRC[3].columnid = sColumnInfo.columnidcbMax;
            sJetRC[3].cbData = sizeof(DWORD);
            sJetRC[3].itagSequence = 1;
            sJetRC[3].grbit = 0;
            sJetRC[3].pvData = &ci.dwMaxSize;
 
            GetColumns ( sTableInfo.sTableName, sJetRC, 4 );
 
            ci.sName.assign ( reinterpret_cast<wchar_t*> ( sJetRC[0].pvData), sJetRC[0].cbActual / 2 );
 
            SDBColumnInfo sDBColumnInfo;
            sDBColumnInfo.sColumnName = ci.sName;
 
            sColumnsInfo.push_back ( ci );
            sTableInfo.sColumnInfo.push_back ( sDBColumnInfo );
        }
        while ( !TableEnd ( sTableInfo.sTableName ) );
 
        CloseTable ( sTableInfo.sTableName );
    }
 
    return JET_errSuccess;
}

Здесь мы опять открываем таблицу, но уже не root, а ту, что нашли на предыдущем шаге.

Далее нужно получить информацию о всех колонках, для этого получаем указатель на первую и идем до последней перебирая одну за другой:
JET_ERR CJetDBReaderCore::MoveToFirst ( std::wstring sTableName )
{
    std::map <std::wstring, JET_TABLEID>::const_iterator iter = m_tables.find ( sTableName );
    if ( iter != m_tables.end() ) // if already open
    {
        JET_ERR jRes = _JetMove ( m_sesid, iter->second, JET_MoveFirst, 0 );
        BOOL bIsEmpty = ( jRes == JET_errNoCurrentRecord );
        if ( bIsEmpty ) return jRes;  // Ingnore if empty
        WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes );
    }
 
    return NO_ERROR;
}

JET_ERR CJetDBReaderCore::GetTableColumnInfo ( 
    std::wstring sTableName, 
    JET_COLUMNLIST* pCl, 
    BOOL bReplaceOld  )
{
    JET_ERR jRes = JET_errSuccess;
    std::map <std::wstring, JET_TABLEID>::iterator iter = m_tables.find ( sTableName );
    if ( iter != m_tables.end() )
    {
        jRes = _JetGetTableColumnInfo ( m_sesid, iter->second, NULL, pCl,
             sizeof(JET_COLUMNLIST), JET_ColInfoList);
        WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
 
        if ( bReplaceOld ) // if you not need last time open table
        {
            jRes = CloseTable ( sTableName );
            m_tables[sTableName] = pCl->tableid;
        }
        else
        {
            jRes = _JetCloseTable ( m_sesid, pCl->tableid );
            WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
        }
    }
 
    return jRes;
}

Простите, первый вариант поста обрезался, и я не успел вовремя отреагировать, видимо есть какое-то ограничение на размер статьи, поэтому она будет разбита на две части.
Продолжение следует...


В следующей части мы дочитаем базу, сделаем выводы, и посмотрим на пример данных, которые можно «выдрать» из базы.

P.S. Данный код является адаптированной и уменьшенной версией для поста. Поэтому в коде есть некоторые недоработки, а точнее затычки на местах урезанного функционала. Пожалуйста, не обращайте на них внимания это не production, а я хотел показать рабочие примеры. Код полностью рабочий и написан так, чтобы его можно было разместить в интернете и при этом не «съесть» все место на странице. Спасибо за понимание.

P.P.S. Я понимаю, что ввиду специфики данная информация вряд ли окажется полезной для широкого круга лиц, но если она поможет, даже одному человеку, я буду рад, и время, потраченное на этот пост, окупится.

Ссылки по теме
  1. Extensible Storage Engine на MSDN
  2. Статья на русском языке (чуть ли не единственная в рунете статья про использование ESE API)
  3. Примеры использования функций ESE
Теги:
Хабы:
+20
Комментарии17

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн