ある大きな、大きな国では、小さくてかわいい人が住んでいました。 そして、この大きな、またはかなり大きな国の真ん中に深く深い穴が現れるまで、すべてがうまくいきました。 さて、私は言わなければならない、彼女は一人ではなく、そしてもちろん、すぐには現れなかったが、誰もこれをもう覚えていないだろうし、これはもはや誰にとっても重要ではない。主なものはピットであり、それは大きく、非常に大きく、むしろ無限であり、より正確には誰も知らなかった。 しかし、大きな国の政府は、ピットで何かをしなければならないことを知っていて、勉強して眠りにつくことに決めました。 この美しい状態にあった人たちの中で最も威圧的で器用で勇気があり、賢くて最も賢い人は何と呼ばれていましたか。
そして、最も予言的なセット。 彼らは勉強し、眠りに落ち、眠りに落ち、勉強し、勉強し、眠りに落ち、眠りに落ち、勉強しました。そして、これは何度も繰り返されませんでした。
私たちのほとんどのほとんどのヒーローが去ったか、むしろ彼らは去らなければならなかったので、ピットが残っていました。 しかし、新しいヒーローは、同じくらい小さく、同じで、まったく同じで、同じことを始めました。
一般に、これは1つが残っている限り続きました。 そして、ある晴れた日、ピット自体は姿を消し、まったく別の場所のどこかに現れました。そこで彼らは再び勉強し、埋め始めました...
すべての偶然はランダムであり、すべてが架空のものであり、そのようなものはかつてなかったため、現実にはありえませんでした! 真実、真実、まあ、またはほとんど真実。そのようなピットの1つについては、後で説明します。 しかし、以来 プライバシーポリシーでは、その一部のみを対象とします。これには、
オープンソースと
文書化された APIのみを使用してアクセスできます。
材料はほとんど排他的です 穀物のトピックに関する情報。
MS Exchange Serverがすべてのメールを保存する場所を考えたことがありますか、それとも最下位レベルでどのように動作しますか? これについて少し説明しますので、ここに書きます。
警告:このトピックに深く入ろうとしないでください。あなたの人生は十分ではありません。 警告した。
はじめに
MS Exchange Server(以下、単にExchange)は、Microsoft製品ラインの主要製品の1つです。 主な機能については、
wikiまたは公式
Webサイトで読むことができます。 要するに、これはメール、カレンダー、その他のユーザーデータを操作するための一種の「ハーベスター」であり、さまざまなMS製品(SharePoint、TFSなど)と十分に統合されています。
ただし、この記事のフレームワークでは、エンドユーザーに提供するものではなく、このデータをどこから取得し、どのAPIを使用するかに関心があります。 Exchange 2010(
メールボックスサーバー )のメールボックスの役割でメールボックスデータベースを個別に読み取ろうとします。
Exchangeには、ユーザーがデータにアクセスできるいくつかのエントリポイント(
CASサーバー )と、これに使用できるいくつかのプロトコル(OWA、RPC(Outlook)、POP3 / IMAP4など)があります。
方法に関係なく、Exchangeへのアクセスを取得すると、すべての要求がメールボックスの
役割 (Exchange 2007より前はこれが唯一の役割でした)に転送されます。 物理的に、これらのデータベースは
* .edbファイル内のハードディスクにあります。 これらは、ExchangeがインストールされたディレクトリのMailbox \ <データベース名>フォルダーにあります。 また、データベースのライフサイクルに関連するトランザクションログやその他のファイルがそこに配置されますが、それらは必要ありません。最も基本的なものは* .edbです。
少し掘り下げると、Exchangeは
Extensible Storage Engine(ESE)を使用してデータベースのコンテンツにアクセスしていることがわかります。 そして、掘り下げると、ESE関数の実装がライブラリese.dll(またはesent.dll)にあることが明らかになります。 これは、
すべての Exchange
操作の中核です 。 ESEは、データベースを操作するための広範なツールセットを提供します。 関数、定数、構造、および必要なすべての説明は、
ここにあります 。 残念ながら、このドキュメントは長い間更新されていないため、Exchange 2010に登場した機能は多くありませんが、このトピックでは必要ありません。 ese.dllは、Exchangeメインディレクトリ内のBinフォルダーにあります。
ESEの説明を読んだ後、データが
表形式で保存されていることが明らかになります。 列と行を持つテーブルのセット。 テーブルセルは、さまざまな種類のほか、ベースのすべての機能(インデックス、検索など)を使用できます。
合計で、Exchangeはデータベースを.edb拡張子を持つファイルの形式でメールボックスの役割に格納し、ESE(ese.dll)のおかげでそれらにアクセスすることを知っています。 これで十分であり、コーディングを開始できます。
データベース内のテーブル、およびすべての列と列をリストします。 もちろん、それらが何を意味するのか
は決してわかりません。 このことを知っているのはMSだけです。 これらの列には、ユーザーのフォルダー名から文字で終わるメールボックスに関連する
ほぼすべての情報が含まれているはずですが、リバースエンジニアリングのタスクが既にどこにあるかを理解することはここでは考慮されません。
プログラミング
準備する
最初に必要なもの:
- Visual Studio 2008-2010
- MS Exchange Server 2010(他のいずれでも可能ですが、このトピックでは2010について説明します)
- C / C ++の知識
Exchangeで、ベースを作成し、それを読み取ろうとします。 これを行うには、
Exchange管理コンソール (
EMC )を使用できます。 手順については説明しません。 このトピックに関する多くの情報がインターネット上にあります。 このデータベースに1人のユーザーを(同じEMCを介して)作成して、コンテンツが含まれ、このユーザーがこのメールボックスにログインして、たとえばOWAを介してすべてが正しく行われていることを確認します。 その後、メールボックスディレクトリに移動し、データベースの名前のフォルダーとその中のEDBファイルを探します。 ベースをコピーする前に、EMCを使用してアンマウントします。 すべて、実験の基礎があります。 たとえば、将来のプロジェクトのディレクトリにコピーします。
Binフォルダーからese.dllをコピーします。これにより、データベースを操作できます。
Visual Studioでは、コンソールC ++プロジェクトを作成します。 ここに重要なニュアンスがあります Exchange 2010(以前のすべてのバージョンとは異なり)は64ビットバージョン
のみであるため、x64をサポートするプロジェクトを作成する必要があります。 そうしないと、単にアドレススペースにese.dllをロードできません。 したがって、アプリケーションをテストするには、64ビットバージョンのOSが必要であり、Exchange自体で確実にテストできますが、この目的のためにWindows 7でワークステーションを使用します。また、UnicodeバージョンのAPIを使用します。
したがって、新しく作成されたプロジェクトでは、x64およびUnicode(一般-Unicode文字セットを使用)がサポートされていることを確認します。 次に、ESEのメインヘッダーファイルを接続します。
#include <esent.h>
このファイルには、VS 2008以降のスタジオと共にSDKが付属しています。
stdafx.hで、JETバージョン(ESE)で2つの定義
を追加し、ユニコードバージョンの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
- ジェットターム
- JetSetSystemParameterW
- JetOpenTableW
- JetGetColumnInfoW
- JetRetrieveColumns
- Jetove
- JetGetTableColumnInfoW
- JetCloseTable
- JetGetSystemParameter
ベース開口部
必要な関数を正常にロードしたら、データベースの直接読み取りを開始します。 MSDNによると、
JET_paramDatabasePageSize (esent.h)パラメーターを設定して、
データベースのページサイズを指定する必要があります。 これが問題の出番です。 EDBファイルのみを持つこの値を見つけることはできませんが、データベースを開かない場合は正確に指定する必要があります。 これはeseutils(Exchangeに含まれています)を使用して実行できますが、少し異なるパスを使用して、この値は同じバージョンのExchangeで一定であり、常に4096の倍数であることがわかりました。Exchange2010では
32768であることが実験的にわかりました
まず、ページサイズを設定します。
JET_ERR jRes = _JetSetSystemParameter ( NULL 、 NULL 、JET_paramDatabasePageSize、 32768 、 NULL ) ;
JET_ERRは、エラーコードを含む単なるlongです。 JetGetSystemParameter関数(ala :: FormatMessage(...))を使用して、このコードをテキスト記述に変換できます。
JetGetSystemParameter ( m_instance、m_sesid、JET_paramErrorToString、
reinterpret_cast < JET_API_PTR * > ( & jeterror ) 、cBuff、MAX_BUFFER_SIZE ) ;
エラーコードの解析の便宜上、次のマクロを使用します(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; }
次に、Exchange固有の
コールバックを無効にする必要があります。 それらについては何も知りません:
jRes = _JetSetSystemParameter ( NULL 、 NULL 、JET_paramDisableCallbacks、 true 、 NULL ) ;
次に、データベースを操作するための新しい
インスタンス (JET_INSTANCE m_instance)を作成します。
jRes = _JetCreateInstance ( & m_instance、 NULL ) ;
作成されたインスタンスを初期化して、データベースの操作を開始します。
jRes = _JetInit ( & m_instance ) ;
新しいセッションの開始(JET_SESID m_sesid):
jRes = _JetBeginSession ( m_instance、 & m_sesid、 NULL 、 NULL ) ;
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 ;
する
{
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」、このテーブルルートを呼び出しましょう。 データベース内の他のすべてのテーブルのリストが含まれています。
- NAME_COLUMN- 「名前」、すべてのテーブルの名前を含む列。
- TYPE_COLUMN- 「タイプ」、テーブルのタイプを含む列。
コードでわかるように、最初にルートテーブルを開きます。これは、
JetOpenTable関数を使用して
行います。
JET_ERR CJetDBReaderCore :: OpenTable ( std :: wstring sTableName )
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 ( 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。 検索 ( 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構造体に入力し、GetColumns内で
JetRetrieveColumnsを実行してテーブル名を取得します。
JET_ERR CJetDBReaderCore :: GetColumns (
std :: wstring sTableName、
JET_RETRIEVECOLUMN * sJetRC、
INT nCount )
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 ( 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 ] ;
する
{
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 ;
}
ここで、テーブルを再び開きますが、ルートではなく、前の手順で見つけたテーブルを開きます。
次に、すべての列に関する情報を取得する必要があります。このため、最初のポインターを取得し、最後の列に1つずつ移動します。
JET_ERR CJetDBReaderCore :: MoveToFirst ( std :: wstring sTableName )
{
std :: map < std :: wstring 、JET_TABLEID > :: const_iterator iter = m_tables。 検索 ( sTableName ) ;
if ( iter ! = m_tables。end ( ) ) //既に開いている場合
{
JET_ERR jRes = _JetMove ( m_sesid、iter- > second、JET_MoveFirst、 0 ) ;
BOOL bIsEmpty = ( jRes == JET_errNoCurrentRecord ) ;
if ( bIsEmpty ) jResを返します。 //空の場合は無視します
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes ) ;
}
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 > :: イテレータ iter = m_tables。 検索 ( 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 ) //最後にテーブルを開く必要がない場合
{
jRes = CloseTable ( sTableName ) ;
m_tables [ sTableName ] = pCl- > tableid ;
}
他に
{
jRes = _JetCloseTable ( m_sesid、pCl- > tableid ) ;
WRITE_TO_LOG_AND_RETURN_IF_ERROR_2 ( jRes )
}
}
return jRes ;
}
申し訳ありませんが、投稿の最初のバージョンは切断され、私は時間内に応答することができませんでした。明らかに、記事のサイズには何らかの種類の制限があるため、2つの部分に分割されます。
継続するには...次のパートでは、ベースを読んで結論を導き、ベースから「引き裂かれた」可能性のあるデータの例を見ていきます。
PSこのコードは、投稿用に変更されたバージョンです。 したがって、コードにはいくつかの欠陥があり、機能が切り詰められている場所にはギャグがあります。 それらに注意を払ってはいけません、これは生産ではありませんが、
実際の例を示したかったのです。 コードは完全に機能しており、インターネット上に配置できると同時に、ページ上のすべてのスペースを「食べない」ように記述されています。 ご理解いただきありがとうございます。
PPS私は、詳細のために、この情報が広範囲の人々にとって有用であるとは思わないが、それが1人でも役立つならば、私はうれしいです、そして、このポストに費やされた時間は報われるでしょう。
関連リンク
- MSDNのExtensible Storage Engine
- ロシア語の記事 (ESE APIの使用に関するRuNetの記事はほぼ唯一)
- ESE機能の例