Windowsで暗号プログラムを書いてみる(Hash編) | reverse-eg-mal-memoのブログ

reverse-eg-mal-memoのブログ

サイバーセキュリティに関して、あれこれとメモするという、チラシの裏的存在。
medium(英語):https://sachiel-archangel.medium.com/

最近はセキュリティエンジニアで、フォレンジック、マルウェア解析でどうにか生活できるようになりましたが。

昔は、しがないプログラマだったのですよねぇ。

そのせいか、昔取った杵柄でたまにプログラムを書く機会があるのですが。

今回も例に漏れず、色々あってなんとなくパスワード管理ツールを自作してみようなんて思ったわけですが(どんな状況なんだろうか?自分でもよくわからないw)。

 

割と細かいこと忘れてるんですよね。

 

そしてさらに、

 

その頃って暗号プログラム書いたことなかったんだよね。

 

そんなわけで、Windowsで暗号プログラムを書こうと思った際に、色々調べたり試してみたり、バグで泣いたりしたので、サンプルソースを置いておこうと思った次第です。

いや、前書いたソース掘ればいいっちゃいいんだけど・・・。「ブログに書いておけば大抵のところから見れる」という安直な理由だったりします。

 

もちろん、ソースがクソ過ぎてGithubになんて恥ずかしくて上げられやしないってのが大ですがね!

 

さて、Windowsには「CryptAPI」というのがあり、こちらを使えば割と簡単に暗号化ができます。

今回は、CryptAPTを使った暗号プログラムソースをメモっておきます。

 

 

ハッシュ関数

この前説いるのかしらw

ハッシュ関数は「メッセージダイジェスト」とも呼ばれ、与えられたデータを一方向に暗号化する関数です。

もっともよく知られている利用法の一つとして、パスワードの保存があります。

他にも、ファイルの改ざん検知電子署名など様々な分野で使われています。

 

ハッシュ関数にも色々種類があり、有名なものを挙げると、古いものではMD2MD5などがあり、比較的新しいものにSHA-1、新しいものにSHA-2SHA-3などがあります。

他にも、Windows特有のLMハッシュ、NTLMハッシュなどがあり、このあたりを詳しく調べたければWikipediaを見た方が早いとか、真面目に暗号の本を買った方がいいです。

規格化されているハッシュ関数は、ソースも公開されています。

以下にいくつか例を挙げておきます。

 

MD5 (RFC 1321)

https://www.rfc-editor.org/rfc/rfc1321.txt

 

SHA-1 (RFC 3174)

https://www.rfc-editor.org/rfc/rfc3174.txt

 

SHA-2 (RFC 6234)

https://www.rfc-editor.org/rfc/rfc6234.txt

https://www.rfc-editor.org/rfc/rfc4634.txt

 

このあたりのソースを流用して実装してもいいのですが、折角なので今回はCryptAPIを使って実装してみたいと思います。

理由は・・・

 

毎回コード書いたり、ソース移植しなくて良くて簡単だから。

 

ガッツリやりたい人はFIPSやRFC、暗号の本を読んで勉強してください!!

私は易き方向に逃げる!!(なんつー技術ブログだ・・・)

 

 

Hash計算時のCryptAPIのざっくりな流れ

大雑把なフローは以下の通り。
 

前提:

  • "WinCrypt.h"をincludeすること。
 

概略フロー:

  • Cryptコンテキストの取得(CryptAcquireContext)
  • ハッシュオブジェクトを生成(CryptCreateHash)
  • ハッシュ値を計算(CryptHashData)
  • ハッシュ計算結果の取得(CryptGetHashParam)
  • ハッシュオブジェクトの開放(CryptDestroyHash)
  • Cryptコンテキストの開放(CryptReleaseContext)

 

ポイント:

  • 利用するCryptAPIの種類と順番、概ねのパラメータ。
  • ハッシュオブジェクトを生成時、ハッシュアルゴリズム(SHA-1など)を指定する。

 

 

CryptAPIを使ったHash計算サンプルソース

一応、VisualStudio2017で動作することが確認されたソースです。

もし流用するのであれば、パラメータ等については、用途に合わせて変えてください。

MicrosoftのMSDNを参考にするとよいでしょう。

また、例によってタブが上手く入らなくてスペースでインデントしているので、適宜直してください。

 

関数の仕様:

static int Sha1(char *pData, int iSize, BYTE *pHashValue, int iHashValueSize);

第1引数で渡したデータをSHA-1ハッシュ計算し、第3引数の領域に格納し返す。

 

引数:

pData:ハッシュ化したいデータのバッファ

iSize:ハッシュ化したいデータのサイズ

pHashValue:ハッシュ化したデータの受取りバッファ

iHashValueSize:ハッシュ化したデータの受取りサイズ

 

返り値:

0:正常終了

負値:異常終了(詳細はヘッダのdefine参照)

 

制限:

大きなサイズのデータには未対応。

intのサイズ制限に引っかかるので、2GB以上は無理(DWORDにすれば拡張可能)。

また、CryptHashDataのサイズパラメータがDWORDのため、約4GB以上は関数の仕様的に無理だと思われる。

第3引数の領域はハッシュ値を格納するのに十分な領域を設定しておくこと。

(後で思ったけど、第4引数をintのポインタにしておけば、CRYPTHASH_ER_SIZESMALLの時に必要なサイズを返してあげることができるかな・・・と。といってもまあ、SHA-1が20バイトって変わらないから、必要性ほぼないかな。)

 

CryptHash.h

#pragma once

#define CRYPTHASH_SUCCESS 0
#define CRYPTHASH_ER_FATAL -1
#define CRYPTHASH_ER_ACQUIRECONTEXT -2
#define CRYPTHASH_ER_CREATEHASH -3
#define CRYPTHASH_ER_HASHDATA -4
#define CRYPTHASH_ER_GETHASHPARAM_SIZE -5
#define CRYPTHASH_ER_GETHASHPARAM_DATA -6
#define CRYPTHASH_ER_SIZESMALL -7
#define CRYPTHASH_ER_PARAM -8

class CryptHash
{
public:
    CryptHash();
    ~CryptHash();

    static int Sha1(char *pData, int iSize, BYTE *pHashValue, int iHashValueSize);
};

 

CryptHash.cpp

#include "stdafx.h"
#include "CryptHash.h"
#include <WinCrypt.h>


CryptHash::CryptHash()
{
}


CryptHash::~CryptHash()
{
}

int CryptHash::Sha1(char *pData, int iSize, BYTE *pHashValue, int iHashValueSize)
{
    HCRYPTPROV hProv = NULL;        // コンテキストハンドル
    HCRYPTHASH hHash = NULL;        // ハッシュハンドル
    DWORD dwHashSize = 0;
    DWORD dwSize = 0;
    int iRetCode = CRYPTHASH_ER_FATAL;

    if (pData == NULL || iSize <= 0 || pHashValue == NULL || iHashValueSize <= 0)
        return CRYPTHASH_ER_PARAM;

    try {

        // コンテキストの取得
        // PROV_RSA_AESはSHA1の計算に対応しているため。
        if (CryptAcquireContext(&hProv, NULL, NULL,
                                         PROV_RSA_AES, CRYPT_VERIFYCONTEXT) == false)
        {
            if (GetLastError() == NTE_BAD_KEYSET)
            {
                if (CryptAcquireContext(&hProv, NULL, NULL, 
                                                 PROV_RSA_AES, CRYPT_NEWKEYSET) == false)
                {
                    throw CRYPTHASH_ER_ACQUIRECONTEXT;
                }
            }
            else
            {
                throw CRYPTHASH_ER_ACQUIRECONTEXT;
            }
        }

        // ハッシュオブジェクトを生成
        if (CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash) == false)
        {
            throw CRYPTHASH_ER_CREATEHASH;
        }

        // ハッシュ値を計算
        if (CryptHashData(hHash, (BYTE *)pData, (DWORD)iSize, 0) == false)
        {
            throw CRYPTHASH_ER_HASHDATA;
        }

        // ハッシュ計算結果のサイズ取得
        // 注:SHA1だからサイズ分かっている(160ビット、20バイト)んだけど、
        // 関数の呼び出し方を知るために一応記述。

        dwSize = sizeof(dwHashSize);
        if (CryptGetHashParam(hHash,
                                        HP_HASHSIZE,
                                       (BYTE *)&dwHashSize,
                                       &dwSize,
                                       NULL) == false)
        {
            throw CRYPTHASH_ER_GETHASHPARAM_SIZE;
        }

        if ((int)dwHashSize > iHashValueSize)
        {
            throw CRYPTHASH_ER_SIZESMALL;
        }

        // ハッシュ計算結果の取得
        if (CryptGetHashParam(hHash,
                                        HP_HASHVAL,
                                       pHashValue,
                                       &dwHashSize,
                                       NULL) == false)
        {
            throw CRYPTHASH_ER_GETHASHPARAM_DATA;
        }

        iRetCode = CRYPTHASH_SUCCESS;
    }
    catch (int iError) {
        iRetCode = iError;
    }

    // 後処理
    if (hHash)
    {
        CryptDestroyHash(hHash);
        hHash = NULL;
    }
    if (hProv)
    {
        CryptReleaseContext(hProv, 0);
        hHash = NULL;
    }

    return 0;
}

 

やっぱりソースコードの掲載には向かねぇw

 

思いつきでコードを書いちゃったので、型とか色々モノ申すところがありそうなので、流用する人は適宜調整を。

第1引数が char * なのは、これを呼びだすプログラムでインプットがASCIIのテキストになっているからです。素直に BYTE * で、呼び出す側がキャストでいいよね(直せよって言われそう)。

可変長データはデータクラス作った方がよさそうだと思いつつ、今回面倒なのでやってないです

(掲載するソースが増えるけど見にくいし・・・)

 

データクラスにしておけば、デストラクタで領域開放でき、呼び出し側が意識しなくて良くなるので、第3引数は領域未確保でも動的に領域確保してデータを格納するように作れるかなと思う。

関数内でメモリアロケーションしていないのは、関数内でHeapAllocを使うか、VirtualAllocを使うか、newを使うか、mallocを使うかといったことによる制限を作りたくないため。

(関数内で勝手に領域確保してポインタを渡すと、「関数の仕様をちゃんと理解していないおバカにゃん」が領域開放漏れしてくれるので、個人的に好きじゃない。また、関数の呼び出し側が領域の開放方法も把握しなければならないので、分かりにくい。

データクラスにするなら、基底の領域確保関数を Virtual で作っておいて、実装を VirtualAlloc版と HeapAlloc版作って、呼び出す側が利用状況に応じて適したデータクラスを使い分ける、といったことを妄想したくなる。)

 

 

さいごに

とりあえず動くことは確認したんだけど、細かいバグとか指摘してくれると嬉しいなぁ。

AESも書く予定だけど、ちょっと待ってね。