ローカルストレージに保存するデータの暗号化 ― Windows の場合

Gumblar による FFFTP への攻撃について

GumblarによるFFFTPへの攻撃について

FTPのアカウントを盗み、サイトを改竄するGumblarウイルスが猛威をふるっております。

このGumblarウイルスの亜種が、FFFTPを狙って攻撃していることが報告されております。 詳しくは以下のサイトを参照してください。

smilebanana
UnderForge of Lack

FFFTPはパスワードをレジストリに記録しております。簡単な暗号化をかけてありますが、FFFTPはオープンソースであるため、暗号の解除法はプログラムソースを解析すれば可能です。

Gumblarウイルスの亜種は、レジストリに記録されているパスワードを読み取り、サイト改竄に使用しているようです。

上記理由により、以下のいずれかの対策をお取りください。

●接続先のFTPサーバーがSSL等に対応している場合。

→SSL対応のFTPソフトへの切り替えをお薦めします。現在、FFFTPはSSL等に対応していません。なお、切り替えの際は、コントロールパネルの「プログラムの追加と削除」を使って、FFFTPをアンインストールしてください。

●接続先のFTPサーバーがSSL等に対応していない場合。

→パスワードをFFFTPに記憶させるのをやめ、接続時に毎回パスワードを入力するようにしてください。ただし、Gumblarウイルスは通信の傍受も行っていると考えられるため、FTPサーバーに接続した時点で、Gumblarウイルスにパスワードが盗まれる可能性があり、万全ではありません。

なお、UnderForge of Lackに記載されているレジストリの削除ですが、通常はFFFTPをコントロールパネルを使ってアンインストールした段階で削除されます。

とあったので,FFFTP のソースを読んでみました.

/*----- パスワードを暗号化する ------------------------------------------------
*
*	Parameter
*		char *Str : パスワード
*		kchar *Buf : 暗号化したパスワードを格納するバッファ
*
*	Return Value
*		なし
*----------------------------------------------------------------------------*/

static void EncodePassword(char *Str, char *Buf)
{
	unsigned char *Get;
	unsigned char *Put;
	int Rnd;
	int Ch;

	srand((unsigned)time(NULL));

	Get = (unsigned char *)Str;
	Put = (unsigned char *)Buf;
	while(*Get != NUL)
	{
		Rnd = rand() % 3;
		Ch = ((int)*Get++) << Rnd;
		Ch = (unsigned char)Ch | (unsigned char)(Ch >> 8);
		*Put++ = 0x40 | ((Rnd & 0x3) << 4) | (Ch & 0xF);
		*Put++ = 0x40 | ((Ch >> 4) & 0xF);
		if((*(Put-2) & 0x1) != 0)
			*Put++ = (rand() % 62) + 0x40;
	}
	*Put = NUL;
	return;
}


/*----- パスワードの暗号化を解く ----------------------------------------------
*
*	Parameter
*		char *Str : 暗号化したパスワード
*		kchar *Buf : パスワードを格納するバッファ
*
*	Return Value
*		なし
*----------------------------------------------------------------------------*/

static void DecodePassword(char *Str, char *Buf)
{
	unsigned char *Get;
	unsigned char *Put;
	int Rnd;
	int Ch;

	Get = (unsigned char *)Str;
	Put = (unsigned char *)Buf;
	while(*Get != NUL)
	{
		Rnd = ((unsigned int)*Get >> 4) & 0x3;
		Ch = (*Get & 0xF) | ((*(Get+1) & 0xF) << 4);
		Ch <<= 8;
		if((*Get & 0x1) != 0)
			Get++;
		Get += 2;
		Ch >>= Rnd;
		Ch = (Ch & 0xFF) | ((Ch >> 8) & 0xFF);
		*Put++ = Ch;
	}
	*Put = NUL;
	return;
}

今回問題となっている接続パスワード等は,上記 EncodePassword 関数でエンコードされてレジストリに記録されていたようです.

CryptProtectData API

最初に注意書き.




私はセキュリティの専門家ではありませんので,下記の内容を信用する前に社内外のセキュリティ専門家の方によく相談されることをおすすめします.




さて今回の件,「オープンソースなプロダクトの場合,ソースを読めばデコード方法が分かる」という点がやや気になるような気にならないような感じです.
まず出発点として,Microsoft が推奨している方法を見てみましょう.

Storing Passwords

Never store passwords in plaintext (unencrypted). Encrypting passwords significantly increases their security. For information about storing encrypted passwords, see CryptProtectData. For information about encrypting passwords in memory, see CryptProtectMemory. Store passwords in as few places as possible. The more places a password is stored, the greater the chance that an intruder might find it. Never store passwords in a Web page or in a Web-based file. Storing passwords in a Web page or in a Web-based file allows them to be easily compromised.

After you have encrypted a password and stored it, use secure ACLs to limit access to the file. Alternatively, you can store passwords and encryption keys on removable devices. Storing passwords and encryption keys on a removable media, such as a smart card, helps create a more secure system. After a password is retrieved for a given session, the card can be removed, thereby removing the possibility that an intruder can gain access to it.

上記文章にあるように,CryptProtectData API や CryptProtectMemory API *1 を使ってデータを暗号化した上で,保存先のアクセスコントロールリストにも気をつけろ,とあります.
実際,CryptProtectData でパスワード等を暗号化しているオープンソースなプロダクトに,Chromium (Google Chrome) があります.

// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/password_manager/encryptor.h"

#include <windows.h>
#include <wincrypt.h>
#include "base/string_util.h"

#pragma comment(lib, "crypt32.lib")

bool Encryptor::EncryptWideString(const std::wstring& plaintext,
                                  std::string* ciphertext) {
  return EncryptString(WideToUTF8(plaintext), ciphertext);
}

bool Encryptor::DecryptWideString(const std::string& ciphertext,
                                  std::wstring* plaintext){
  std::string utf8;
  if (!DecryptString(ciphertext, &utf8))
    return false;

  *plaintext = UTF8ToWide(utf8);
  return true;
}

bool Encryptor::EncryptString(const std::string& plaintext,
                              std::string* ciphertext) {
  DATA_BLOB input;
  input.pbData = const_cast<BYTE*>(
    reinterpret_cast<const BYTE*>(plaintext.data()));
  input.cbData = static_cast<DWORD>(plaintext.length());

  DATA_BLOB output;
  BOOL result = CryptProtectData(&input, L"", NULL, NULL, NULL,
                                 0, &output);
  if (!result)
    return false;

  // this does a copy
  ciphertext->assign(reinterpret_cast<std::string::value_type*>(output.pbData),
                     output.cbData);

  LocalFree(output.pbData);
  return true;
}

bool Encryptor::DecryptString(const std::string& ciphertext,
                              std::string* plaintext){
  DATA_BLOB input;
  input.pbData = const_cast<BYTE*>(
    reinterpret_cast<const BYTE*>(ciphertext.data()));
  input.cbData = static_cast<DWORD>(ciphertext.length());

  DATA_BLOB output;
  BOOL result = CryptUnprotectData(&input, NULL, NULL, NULL, NULL,
                                   0, &output);
  if(!result)
    return false;

  plaintext->assign(reinterpret_cast<char*>(output.pbData), output.cbData);
  LocalFree(output.pbData);
  return true;
}

Chromium Revision 8066 では,CryptProtectData API の pOptionalEntropy 引数および pPromptStruct 引数に NULL を渡しています.これは,同じコンピュータの同じユーザであれば,誰でも CryptUnprotectData API で復号できることを意味します.
同じコンピュータを使う別のユーザから復号は,(暗号化が行われた PC 環境の暗号化設定で期待される程度に) 防がれます.暗号化されたデータ列が流出した場合にも,復号は (暗号化が行われた PC 環境の暗号化設定で期待される程度に) 防がれます.
例として,以下のバイト列を Chromium と同じ方法で暗号化してみました.

const unsigned char original_password[] = {
  0x6b, 0x6f, 0x67, 0x61, 0x69, 0x64, 0x61, 0x6e,
};

手元に構築した仮想環境の Windows XP SP3 では,上記バイト列から以下のようなバイト列が生成されました.なお,同じ入力データであっても毎回異なる結果が返されますが,どの出力に対しても復号結果は同じになります.

const unsigned char encrypted_password[] = {
  0x01, 0x00, 0x00, 0x00, 0xd0, 0x8c, 0x9d, 0xdf,
  0x01, 0x15, 0xd1, 0x11, 0x8c, 0x7a, 0x00, 0xc0,
  0x4f, 0xc2, 0x97, 0xeb, 0x01, 0x00, 0x00, 0x00,
  0x22, 0xd6, 0xf5, 0x5e, 0x47, 0x15, 0xa1, 0x4d,
  0x97, 0xde, 0x34, 0xbf, 0xc8, 0xb9, 0x4c, 0x9c,
  0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x03, 0x66, 0x00, 0x00, 0xa8, 0x00,
  0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xf6, 0xbb,
  0xf7, 0x64, 0xd3, 0xe0, 0x27, 0x58, 0xcf, 0xd0,
  0xf1, 0xab, 0x21, 0x3f, 0x6b, 0xf8, 0x00, 0x00,
  0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0xa0, 0x00,
  0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x84,
  0x42, 0xa8, 0x82, 0xff, 0x44, 0xc2, 0x44, 0xeb,
  0xaa, 0xc8, 0x84, 0xd2, 0x0d, 0x18, 0x10, 0x00,
  0x00, 0x00, 0x56, 0x63, 0x8c, 0x93, 0x17, 0x8e,
  0xe0, 0x7d, 0x38, 0x77, 0x6f, 0xe1, 0xda, 0x64,
  0x85, 0xdc, 0x14, 0x00, 0x00, 0x00, 0x31, 0xd4,
  0xab, 0x9c, 0xeb, 0xc3, 0x17, 0x62, 0xa6, 0xcd,
  0xcc, 0x1c, 0x0e, 0x35, 0xfa, 0x13, 0x52, 0x0e,
  0x00, 0x3a, };

さてこの暗号化,同一ユーザーのプロセスからは簡単に復号できてしまうということで,何らかのアプリケーションが陥落した時点であまり意味がありません*2.とはいえ,Windows で同一ユーザーのプロセスからの攻撃に対処するのは大変です.これはもはや OS の領分,しかも歴史の長い OS ということで,改善しようにも互換性への影響が大きく,Integrity Level や UAC の導入で見たような大きな混乱と長期の努力がつきまといます.
Windows 環境での開発経験者向けに 3 行でまとめると,つまりはこうです.

  • 仮に,Integrity Level Low な対話型プロセス (ブラウザやそのプラグイン) に任意のコードを実行可能な脆弱性が存在するとして,
  • そのプロセスでは,レジストリやファイルからデータを読み出すための API と CryptUnprotectData API が実行可能とする
  • さて,レジストリやファイルから個人情報 (のように保管されている機密データ) を盗み出されないようにするには?

こういう要求の実現は,(今の Windows では) アプリケーションと言うより OS というより,アンチウィルスソフトの領分のようにも感じられます.
今日も現実しんどいです…*3

実験に使ったサンプルコード

#include <iostream>

#include <windows.h>
#include <wincrypt.h>
#include <iostream>

#pragma comment(lib, "crypt32.lib")

bool EncryptString(const std::string& plaintext,
                   std::string* ciphertext) {
  DATA_BLOB input;
  input.pbData = const_cast<BYTE*>(
    reinterpret_cast<const BYTE*>(plaintext.data()));
  input.cbData = static_cast<DWORD>(plaintext.length());

  DATA_BLOB output;
  BOOL result = CryptProtectData(
    &input, L"", NULL, NULL, NULL,
    CRYPTPROTECT_UI_FORBIDDEN, // remove if you want to use password
    &output);
  if (!result)
    return false;

  // this does a copy
  ciphertext->assign(reinterpret_cast<std::string::value_type*>(output.pbData),
                     output.cbData);
  LocalFree(output.pbData);
  return true;
}

bool DecryptString(const std::string& ciphertext,
                   std::string* plaintext){
  DATA_BLOB input;
  input.pbData = const_cast<BYTE*>(
    reinterpret_cast<const BYTE*>(ciphertext.data()));
  input.cbData = static_cast<DWORD>(ciphertext.length());

  DATA_BLOB output;
  BOOL result = CryptUnprotectData(
    &input, NULL, NULL, NULL, NULL,
    CRYPTPROTECT_UI_FORBIDDEN, // remove if you want to use password
    &output);
  if(!result)
    return false;

  plaintext->assign(reinterpret_cast<char*>(output.pbData), output.cbData);
  LocalFree(output.pbData);
  return true;
}

void dump_string(const std::string& label, const std::string& test);

int main() {
  std::string original_test = "kogaidan";
  dump_string("original_password", original_test);

  std::string encrypted_text;
  if (!EncryptString(original_test, &encrypted_text)) {
    std::cerr << "EncryptString failed" << std::endl;
    return 1;
  }

  dump_string("encrypted_password", encrypted_text);

  std::string derypted_text;
  if (!DecryptString(encrypted_text, &derypted_text)) {
    std::cerr << "DecryptString failed" << std::endl;
    return 1;
  }
  dump_string("decrypted_password", derypted_text);

  return 0;
}

#pragma region dump_string 
void dump_string(const std::string& label, const std::string& test) {
  std::cout << "const unsigned char " << label.c_str() << "[] = {\n";
  std::cout << std::hex;
  int count = 0;
  for (std::string::const_iterator i = test.begin(); i != test.end(); ++i) {
    if (count++ == 0) { std::cout << "  "; }
    std::cout << "0x";
    std::cout.width(2);
    std::cout.fill('0');
    std::cout << (*i & 0xff) << ", ";
    if (count >= 8) { std::cout << "\n"; count = 0; }
  }
  std::cout << std::dec;
  std::cout << "};\n";
  std::cout << std::endl;
}
#pragma endregion

参考

  • DPAPI / DPAPIによる暗号化 - EternalWindows
    • CryptProtectData の使用方法について解説されています.プロンプトを表示してパスワードを併用する方法もあわせて紹介されています.
  • http://www.forest.impress.co.jp/docs/news/20100130_346056.html:title=
    • FFFTP が保存するデータが狙われている件について色々

参考2

CryptProtectMemory API は Windows Vista 以降で利用可能*4で,今回のログオンセッションのみ復号可能,といった期限付きの暗号化を行うことができます.

pDataは、暗号化したいデータを指定します。 cbDataは、pbDataのサイズを指定します。 この値は、CRYPTPROTECTMEMORY_BLOCK_SIZE定数の倍数でなければなりません。 dwFlagsは、次に示す定数のいずれかを指定します。

定数 説明
CRYPTPROTECTMEMORY_SAME_PROCESS 暗号化を行ったプロセスだけがデータを復号化できる。 プロセスが終了するとデ−タを複合化することはできない。
CRYPTPROTECTMEMORY_CROSS_PROCESS 暗号化を行ったプロセスだけでなく、別のプロセスもデータを複合化できる。 システムをシャットダウンするとデ−タを複合化することはできない。
CRYPTPROTECTMEMORY_SAME_LOGON 暗号化を行ったプロセスだけでなく、別のプロセスもデータを複合化できる。 ただし、そのプロセスは暗号化を行ったプロセスと同じ ログオンセッションで動作している必要がある。 システムをシャットダウンするとデ−タを複合化することはできない。

参考3

CredUIPromptForCredentials (Vista 以降は CredUIPromptForWindowsCredentials推奨) も,パスワード管理に使えそうですが,ユーザー単位に秘密情報を格納するため,同一ユーザーのプロセスがどれかひとつ陥落した時点でやばげに見えます.

はじめに

アプリケーションが、データベースや FTP サイトなど保護されたリソースにアクセスするために、ユーザー提供の資格情報が必要な場合があります。しかし、ユーザーの ID とパスワードを取得し格納することは、システムにとってセキュリティ上のリスクにもなります。可能であれば、ユーザーが資格情報を提供しないようにする必要がありますが (たとえば、データベース用に統合された認証を使用するなど)、それは避けられない場合もあります。ユーザーからの資格情報のリクエストが必要で、アプリケーションは、Microsoft® Windows® XP または Microsoft® Windows Server 2003 上で実行している場合、オペレーティング システムはこのタスクを容易にする関数を提供します。

Stored User Names and Passwords

Windows XP と Windows Server 2003 は、「Stored User Names and Passwords」と呼ばれる機能 (図 1 を参照してください) を使用して、1 つの Windows ユーザー アカウントに 1 セットの資格情報を関連付け、Data Protection API (DPAPI) を使用して、それらの資格情報を格納します。

図 1. Windows XP の [Credential Management] ダイアログ ボックス

アプリケーションが Windows XP または Windows .NET 上で実行している場合、アプリケーションは、資格情報管理 API 機能を使用して、ユーザーに資格情報を確認します。これらの API の使用によって、一貫したユーザー インターフェイス (図 2 を参照してください) が提供され、オペレーティング システムによるこれらの資格情報のキャッシュを自動的にサポートします。

図 2. 標準の Windows XP の資格情報ダイアログ ボックス

ユーザーの資格情報をアプリケーションで、リクエスト、格納、使用することに関する問題は、Michael Howard and David LeBlanc による『プログラマのためのセキュリティ対策テクニック』でさらに詳しく説明されています。詳細情報については、その本を読むことをお勧めします。ここでは、Microsoft® Visual Basic® .NET と C# アプリケーションからの資格情報管理 API の使用方法を示します。

またも EternalWindows さんの記事を参考に.

*1:これらの API は Windows 2000 以降でのみ利用可能です

*2:仮に pOptionalEntropy 引数を併用したとしても今度は pOptionalEntropy の内容をどうやって隠すかという問題になります.pOptionalEntropy の内容をソースコードに書いてしまうのは,パスワードをソースコードに書いてしまうことと同じです.

*3:[http://niha28.sakura.ne.jp/b/log/100:title=元ネタ]

*4:Windows 2000 SP3 以降に関しては [http://msdn.microsoft.com/en-us/library/aa387693.aspx:title=RtlEncryptMemory]