Xcode7よりiOSアプリの実機でのデバッグが登録無しでも行えるようになったのを機に簡単なアプリを作ったのですが、その際にiOSアプリからWindwosのファイル共有(SMB)を使えないかな?と思ったのが事の始まりでした。そこでWireSharkを使ってSMBプロトコルのやり取りを調べたり、MSのドキュメントやWebの記事を読んだりして実装してみました。ここで紹介するのは、取り敢えずSMB2プロトコルを使って共有ファイルをダウンロードするだけの単純なものです。
※この記事の目的は、SMB2プロトコルによる通信を行う実験的なものであり、プロトコルの解説や安定して動作するアプリやライブラリの提供が目的ではありませんので、悪しからずご了承ください。
いきなりですが、私自身、SMBプロトコルを良く理解しているわけではありませんので、詳しくは[MS-SMB]や[MS-SMB2]などの公式ドキュメントをお確かめください。従って、この記事をそのまま鵜呑みになさらないようご注意く下さい。また本文中の間違い、勘違い等もご指摘頂けると幸いです。
SMBプロトコルは大きく分けてSMB1とSMB2以降の2つに分類出来るかと思います。この2つは通信時のパケットの構造が異なるので互換性はありません。但し、プロトコルとしてやっている内容は同じようなことです。SMB1は古くから存在していますが、今となってはMSがサポートを打ち切ったXPや2003Server世代までのものなので、今回はSMB2で実装しました。
SMBプロトコルは単純にSMBプロトコルで定義された情報のやり取りだけをしているわけではなく、他のプロトコルをトンネルさせるコンテナの様な動きをしたりします。例えば認証ではNTLMやKerberosといったプロトコルをgss-apiなどで包んでSMBパケットの付加情報としてやり取りしたり、公開されている共有フォルダの一覧を取得するのにDCE/RCPのパケットをSMBパケットの付加情報としてやり取りしたりと一筋縄ではいきません。
さすがに全てを理解して実装するのは無理ですので、ここはWireSharkで調べたパケットを頼りに意味はよく分からないけど、これさえ送っておけば大丈夫かも的なデータを作成して送受信しています。尚、Kerberos認証には対応していません(出来ません、難易度高過ぎですw)。
SMBにはDialect(方言)と呼ばれるものが存在します。簡単に言えばバージョンみたいなもんですが、こちらの記事にわかりやすい表がありますのでご覧ください。今回のPGでは2.02か2.10限定(Vista/7相当)で実装しました。3.0以降はややこしそうなので先送りという名の放置プレイになりそうです。
私の使っている環境にあるファイル共有サーバに対してしか実験はしていません。具体的にはWindows7/8.1/10、2008R2 Server、MacOS X 10.11、Samba4での動作は確認しましたが、これ以外での動作可否は不明です。また、SMB2プロトコルを使用しているので、SMB1しか対応していないサーバとは通信できません(XP/2003Server、Samba3以下)。それと、私の環境ではWindowsのドメインやActiveDirectoryは利用していないので、Kerberos認証しか受け付けない環境でも動作しないと思います。
(※2017/5/6 追記: macOSの共有ファイル機能について、SMBではオプション扱いのパケットの署名機能が既定値で"ON"になりました。ここで紹介しているサンプルプログラムでは署名を省略しているので、そのままではmacOSに接続できません。ここを参考にmacOS側の設定を変更することで接続できるようになります)。
ここでは、SMB2プロトコルを使って、クライアントがサーバーからファイルをダウンロードするまでの流れを簡単に記述します。SMBプロトコルは基本的にはHTTPのようにクライアントからの要求に対してサーバが応える形式です。
まずは、ネゴシエーションを行い、サーバの方言や機能を確認します。次にNTLMやKerberosを使った認証を行いセッションが開始されます。これで準備が整ったのでサーバが公開している共有フォルダーの一覧を取得して、後は任意の共有フォルダーからファイルの一覧を取得したり、ファイルをダウンロードしたりします。今回は実装していませんが、ファイルを書き込んだりも行います。ここでセッションはSessionIdで管理され、共有フォルダーはTreeIDで管理されます。各ファイルやフォルダーはFileIDと呼ばれる16byteのIDで管理されます。
まずはクライアントからNEGOTIATE要求を送信します。クライアントで利用可能な方言一覧や機能が設定されています。サーバはそれに応じたNEGOTIATE応答を返し、そこで方言が確定し相互に利用する機能が決定されます。
ここで一つ、よくわからなかったのは、Windowsのサーバ(7/2008R2)に対して要求を出す時に、いきなりSMB2で話かけると応答が来ない点です。最初はSMB1で話かけ、応答は何故かSMB2で返って来ます。続けてSMB2の要求を送って、SMB2の応答を取得してネゴシエーションします。Mac OSだといきなりSMB2で要求しても応答してくれるんですが、、、
最初の難関です。認証はSMB2的にはSESSION_SETUP要求と応答で事は進むのですが、実際はSESSION_SETUPパケットはコンテナの役割で、その内容物であるgssapiで包装された認証データが「本体」と言っても過言ではありません。今回はNTLM認証のみを使いますので、NTLM認証で必要なやり取りをSESSION_SETUPに載せて行います。詳細はNTLM認証については[MS-NLMP]やこちらのサイトを、gssapiについては、[MS-SPNG]等を参照してください。
誤解を恐れずに、もの凄く簡単に言うと、NTLM認証ではType1、Type2、Type3という3つのNTLM認証データの送受信で成立します。クライアントはNEGOTIATIONメッセージであるType1を送り、サーバからCHALLENGEメッセージであるType2が返り、それを元にハッシュ計算を行って作成したAUTHENTICATEメッセージであるType3を送り、最後にその結果が返るという、SMB2的には二往復のSESSION_SETUP要求、応答で認証が行われます。その際、Type1〜Type3のメッセージはgssapiで包まれています。SPNEGOやNTLMSSPやらいろんな用語が登場しますが、ま、そういうことです。
また、NTLM認証には、LM認証、NTLM認証、LMv2認証、NTLMv2認証があり、今回はNTLMv2認証のみを実装しています。
gssapiとは何ぞや?と言われても正直よくわかりません。ここに具体的な例が記載されているので、雰囲気は掴めるかもしれません。また、gssapiではASN.1 DERと呼ばれるデータ構造を使っています。DERはデータの種類を示す1byteのTagとデータのサイズを示す可変バイト(だいたい1..4bytes)に続いてサイズ分のデータが続くといった構造になっており、ファイルのような階層構造になっています。NTLM認証で使われる全体の書式は固定的なものなので、肝であるType1〜Type3のデータを作成、解読さえ出来れば認証処理を行えそうです。
クライアントはType2で返された8byteのServer Challengeや自分で生成した8byteの乱数的な値(Client Challenge)を元にユーザ名やパスワードを使っていろいろハッシュ計算を行います。そんな中、MICと呼ばれるハッシュ計算があるのですが、この計算結果を含めてType3を送るとMacOSから弾かれてしまいます。計算が間違ってるのかもしれませんが、WindowsやSambaでは通ります。この両者が単純にMICを無視してるだけなのかもしれませんが、今のところ謎です。オプションなので今は外してあります。
尚、認証中にサーバからSessionIDが返されます。以降の送受信の際には全てのSMB2の要求パケットにこのSessionIDを埋め込む必要がありますので、どこかに退避しておきます。
先にも少し書きましたが、macOSのファイル共有のSMBパケットへの署名がデフォルトで要求されるようです。macOS上で無効に設定することも出来ますが、新たに署名機能も実装してみました。署名は認証後のSMB通信で送信するパケットに対して行います。
署名の方法ですが、SMB2では後述のSMB2パケットのTransportの4バイトを除く全体に対して"Key"によるHMAC-SHA256ハッシュ計算を行い、PacketHeaderのFlagsに署名入りのビットを立て、Signature欄にハッシュ値前半の16byteを代入する事で行うようです(計算前に予めSignature欄を"0"で埋めておく必要があります)。ここで、ハッシュ計算で使用する"Key"が何なのか謎でしたが、いろいろ調べるとどうやらNT認証で使用したSessionKey(16byte)を使うようです。
次の難関です。サーバがどんな共有フォルダーを公開しているのかを知る必要があります。共有フォルダーとはWindows上で"ネットワークのファイルとフォルダーの共有"で設定したフォルダーのことです(sambaではsmb.configで設定)。SMBプロトコルでは、共有フォルダーは一意のTreeIDで管理されています。
まずは"$IPC"という名前の特殊な共有フォルダーに接続(TREE_CONNECT要求)し、そのTreeIDを受け取ります(TREE_CONNECT応答)。今後の"$IPC"に対するアクセスでは、このTreeIDが必要となるので退避しておきます。そして、ここから謎の処理が始まります。"$IPC"に対して"srvsvc"という名のPipeをオープン(CREATE要求)し、そのFileIDを取得(CREATE応答)します。次にこの"srvsvc"に対してIOCTL要求/応答を行って"bind"を実行します。更に続けてIOCTL要求/応答を行って"NetShareEnumAll"を実行し、共有フォルダ一覧を取得します。
サラッと書きましたが、"bind"って何?w 。"NetShareEnumAll"は、その名前から何となく理解できますが、"bind"についてはわかりません。もっとややこしいのは、この一連のIOCTL経由で行われている処理内容はDCE/RPCを使ったリモートプロシージャコールらしいということです。
DCE/RPCだけで一冊の本が出来上がりそうなくらい難解なものなので、詳しく中身を理解する事は諦めます。"bind"や"NetShareEnumAll"では、可変長の要素を含んだ構造体のやり取りになります。ここでも定型的なデータ書式になっているので、状況に応じて設定しないといけない項目だけを変更し、その他は謎は謎のままにしておきます。ここで利用される構造体については、このサイトが多少の理解の手助けになるかもしれません。
一覧を無事取得した後は、"srvsvc"を閉じ(CLOSE要求/応答)、"$IPC"を切断(TREE_DISCONNECT要求/応答)します。
あるファイルへのアクセスは、まず、そのファイルが属する共有フォルダーへ接続(TREE_CONNECT要求/応答)し、TreeIDを取得します。フォルダーのファイル一覧を取得したい場合は、そのフォルダーのパス名でオープン(CREATE要求/応答)してFileIDを取得し、先のTreeIDとFileIDを指定してフォルダー配下のファイル一覧を問い合わせ(QUERY_DIRECTORY要求/応答)します。この時、ワイルドカードも指定出来るようです。又、共有フォルダー直下の問い合わせを行う時にはパス名は""ブランクを指定します。
ファイルの情報を取得したい場合は、TreeIDとFileIDを指定して問い合わせ(QUERY_INFO要求/応答)を行い、ファイルを読み込みたい場合は同様にREAD要求/応答で行います。WRITEも同様です。
ファイルの処理が終わったら、それを閉じ(CLOSE要求/応答)、共有フォルダーを切断(TREE_DISCONNECT要求/応答)します。
SMB2プロトコルでは、要求毎に加算されていくMessageIdをパケットに書き込む必要があります。このMessageIdはSession単位で管理しています。同一Session中に同じMessageIdを使用したり、以前に送ったMessageIdより小さな値を設定したりすると失敗します。1から始めて普通に+1していけば良さそうなものですが、Multi-Credit(Large-MTU)と呼ばれる機能を利用してファイルの読み書き(READ/WRITE)を行ったり、問い合わせ(QUERY_DIRECTORY)を行う際には注意が必要です。
Dialect(方言)の2.1未満では、64KBを超える単位でデータを扱えなかったようです。2.1以降では、64KBを超えるサイズのデータを扱えるようになりました。従って、READ/WRITEなどで読み書きする際に64KBを超えるデータサイズ単位で送受する事が可能になり、速度の向上が見込めます。このサイズとMulti-Creditの使用有無は既述のNEGOTIATE要求/応答で決定されます。具体的にはサーバよりMaxReadSize、MaxWriteSize、MaxTransactSizeに上限値が返されて来ます。
64KBを超える単位でREAD/WRITEやQUERY_DIRECTORYを行う場合には、MessageIdを付番する際に単純に+1増加するだけではいけないようです。[MS-SMB2]に計算式がありますが、READ/WRITEを行う際に、64KB単位で何回分になるのかを計算してSMB2パケットのCreditChargeに設定し、次回の要求の際には、+1ではなくCreditCharge分加算するといった事をします。今回のREAD要求でMessageIdが10、CreditChargeが5だとした場合、次回のREAD要求ではMessageIdが11ではなく、15になるといった具合です。正直、この理解で合ってるかどうかはわかりませんが、だいたいこんな感じです。
SMB2パケットに似たような項目でCreditRequestという項目があり、ここもREAD/WRITEを行うファイルをCREATEする際には大きめの値を指定してOpenしないと、READ/WRITEでCreditChargeを正しく指定しても失敗します。予約的な意味合いなんでしょうか?謎です(いや、MS-SMB2には書いてあるんですけど、私の拙い英語力では正確に理解できなくて、、、)。
実際にTCPで送受信されるバイナリデータ列について、簡単に構造を示すと下記の通りです。詳細は[MS-SMB2]をご覧ください。数値項目についてですが、Transportを除くほとんどの項目はリトルエンディアンです。また、主にファイル名やユーザー名などの指定に使われる文字列の多くはUCS2(UTF16LE)になっています。また、構造体の項目によっては、4/8byteのアラインメントで揃えてやる必要があったりします。その際の基準となる位置は、Packetheaderの先頭をオフセット"0"としたり、Payload部の先頭を"0"としたり様々です。
SMBパケットは4byteのTransportから始まります。ここにはTransport自身の4byteを含まない、SMBパケット全体のサイズが指定されています。最初の1byteは0固定で、残りの3byteにビッグエンディアンで整数値が入っています。これは、SMB1/2で同様です。
64byte長のヘッダーです。全てのSMB2パケットに含まれます。最初の4byteは、'\xfe'、'S'、'M'、'B'のProtocolIDが代入されています。因みにSMB1のProtocolIDは、'\xff'、'S'、'M'、'B'となっています。
SMB2のコマンド毎に各々の固定(というか場合によっては最小)のサイズが決まっています。場合によっては、というのはMessageの構造体の最後の項目が可変長の場合や、後述のPayload/Bufferなどが存在しても、このMessageの構造体サイズには決められた固定値を設定する為です。
コマンド毎に必要な付加情報です。SMB2プロトコルで決められた構造体のデータであったり、他プロトコルのデータであったり、コマンドによって異なります。コマンドによっては、このPayload/Bufferが無いものもあります。
40
|
00
|
||||||||||||||||
01
|
00
|
||||||||||||||||
00
|
00
|
00
|
00
|
||||||||||||||
C2
|
C1
|
||||||||||||||||
01
|
00
|
||||||||||||||||
F4
|
F3
|
F2
|
F1
|
||||||||||||||
00
|
00
|
00
|
00
|
||||||||||||||
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
||||||||||
00
|
00
|
00
|
00
|
||||||||||||||
00
|
00
|
00
|
00
|
||||||||||||||
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
||||||||||
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
00
|
||
〜
|
|||||||||||||||||
〜
|
取り敢えず動かすということを前提に正常ケースでの実装となっていますので、エラー処理が甘いです。また、SMB2には今回未実装の機能がまだまだ残されています。
SMB2のプロトコルに関する箇所はObjective-Cで書かれています。これは、パケットの操作でデータのポインタを構造体のポインタにキャストして手間を省こうという手抜きの為です。今の処、A8Xやx86では怒られていませんが、好ましいやり方とは言えません。ワード単位のバイト境界アクセス違反もコンパイラがきっと賢いコードを吐いてくれてるだろうという前提です。実戦で使わない方が賢明でしょう。また、A8Xやx86がリトルエンディアンなのでいいのですが、ビッグエンディアンのCPUだと当然誤動作します。
ソースはMacOS Xアプリのxcodeプロジェクトです(iOSのプロジェクトでは何かと問題あるのかな?と思いMacOS版にしました)。元々は、iOS用のアプリとして作成されているので、画面を除くSMBプロトコル関連のファイルは、そのままiOSでも動きます。開発環境は、XcodeがVer7.3Ver8.3.2、iOSは9.310.3.2、MacOS XはVer10.11.4Ver10.12.5です。
不慣れなObjective-Cですので、突っ込みどころが満載かと思いますが、まぁ、未熟者と笑って許してやってください。致命的な箇所については、ご指摘頂けたら助かります。
MD4、MD5のソースは他から拝借したもので、各々Apacheライセンス2.0、GPLと表記されています。混ぜるな危険というご指摘があれば、これらのソースの配布を停止しますので、問題があればご指摘ください。
私個人としては、自由に使って頂いて結構ですし、細かいことは気にしないのですが、一応私の書いたコードはMITライセンスにしておきます。従って、これは無保証です、また一切の責任は負いません。もしご利用になりたい方がいらっしゃれば、自己責任の元、ご自由にご利用ください。
現時点(2017/5/24)での"AS IS"です: SMB2TextX.zip
まだまだ奥(闇?)が深そうなプロトコルですね。