[ksnctf] Perfect Cipher

ksnctfにチャレンジ 第21問目です。

※ksnctfは常駐型のCTFサイトです。 ※問題のページはコチラです。

Perfect Cipher

乱数を利用してワンタイムパッドな暗号化が施されているため 元のデータを復元することはできないファイルを復号する問題です。

配布の暗号化ソースコードをみると  メルセンヌ・ツイスタを用いた乱数生成器 を利用してワンタイムパッド用の乱数表を作成していることが わかります。

メルセンヌ・ツイスタは非常に優秀な乱数生成器ですが、 一定量以上の生成器の出力を集めると暗号生成器の中身が 再現可能になって、以降どんな乱数が出力されるか計算可能に なります。 mt19937.cpp はじめ通常の実装では 連続する624個分の出力が集まればOKです。 mt19937.cpp では乱数を生成するために シード値などから精製した  テーブル mt[624] と インデックス mti をもちいて乱数を生成するため、生成された乱数から テーブル mt を復元 すればいいわけです。

genrand_int32() の Tempering という部分が テーブル mt から 乱数を作る計算なので この逆関数に当たる処理を 書きます。 逆関数は検索すれば JavaやPythonなどの実装が見つかったので それを 参考にC用に書きなおせました。

次に、 テーブルmt を復元するための 出力サンプルが必要です。 メルセンヌ・ツイスタで得た乱数を用いて  (元データ) xor (乱数) = (暗号データ) という計算をしており 乱数は hoge.key
暗号データは hoge.enc として保存しているだけなので  (元データ) xor (暗号データ) = (乱数) となり encrypt.cpp encrypt.enc のデータを XOR することで encrypt.key を復元することができます。

encrypt.key はまさに メルセンヌ・ツイスタで生成された 乱数表そのものでかつ 624個以上の乱数が記されています。

この乱数表を先術の Temperingの逆処理にまわして テーブルmt を復元すれば 最初にデータを暗号化したときと 全く同じ乱数表が得られます。

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include "mt19937ar.h"

extern unsigned long mt[624]; 
extern int mti;

unsigned int store[624];
unsigned int key[624];
int eti;

unsigned int un_bit_shift_left_xor(unsigned int v,int shift, unsigned int mask);
unsigned int un_bit_shift_right_xor(unsigned int v,int  shift);
unsigned int un_tempering(unsigned int k);
unsigned int emu_init();
unsigned int emu_genrand_int32();
void dummy_encrypt(const char *plain, const char *crypt, const char *key);
void decrypt(const char *plain, const char *crypt, const char *key);
void encrypt(const char *plain, const char *crypt, const char *key);
void getflag(const char *file);
void makekey();
int min(int a, int b) { return a<b ? a : b; }

int main()
{
    makekey();
    emu_init();

    encrypt("encrypt.cpp", "encrypt.enc", "encrypt.key");
    dummy_encrypt("flag.jpg", "flag.enc", "flag.key");

    decrypt("encrypt_dec.cpp", "encrypt.enc", "encrypt.key");
    decrypt("flag_dec.jpg", "flag.enc", "flag.key");

    getflag("flag_dec.jpg");
    return 0;
} 

void dummy_encrypt(const char *plain, const char *crypt, const char *key)
{
    FILE *fk = fopen(key, "wb");

    for (int i=0; i<80000; i+=4)
    {
        unsigned long k = emu_genrand_int32();
        fwrite(&k, 4, 1, fk);
    }
    fclose(fk);
}

void encrypt(const char *plain, const char *crypt, const char *key)
{
    FILE *fp = fopen(plain, "rb");
    FILE *fc = fopen(crypt, "wb");
    FILE *fk = fopen(key, "wb");

    fseek(fp, 0, SEEK_END);
    unsigned long length = (unsigned long)ftell(fp);
    fseek(fp, 0, SEEK_SET);

    fwrite(&length, 4, 1, fc);

    for (int i=0; i<length; i+=4)
    {
        unsigned long p;
        fread(&p, 4, 1, fp);
        unsigned long k = emu_genrand_int32();
        unsigned long c = p^k;
        fwrite(&c, 4, 1, fc);
        fwrite(&k, 4, 1, fk);
    }

    fclose(fp);
    fclose(fc);
    fclose(fk);
}

void decrypt(const char *plain, const char *crypt, const char *key)
{
    FILE *fp = fopen(plain, "wb");
    FILE *fc = fopen(crypt, "rb");
    FILE *fk = fopen(key, "rb");

    unsigned long length;
    fread(&length, 4, 1, fc);

    for (int i=0; i<length; i+=4)
    {
        unsigned long c;
        fread(&c, 4, 1, fc);
        unsigned long k;
        fread(&k, 4, 1, fk);
        unsigned long p = c^k;

        fwrite(&p, min(4,length-i), 1, fp);
    }

    fclose(fp);
    fclose(fc);
    fclose(fk);
}

void getflag(const char *file)
{
    int i;
    char c;
    FILE *fp;
    fp = fopen(file, "rb");
    for( i=0; !feof(fp); i++)
     {
        c=fgetc(fp);
         if( isgraph(c) )
         {
            if(i>=4218 && i<=(4218+42)) printf("%c",c);
        }
     }
    fclose(fp);
    printf("\n");
}

void makekey()
{
    int i;
    int blocks;

    unsigned long p,k,e;

    FILE *fp;
    FILE *fe;
    FILE *fk;

    fp = fopen("encrypt.cpp", "rb");
    fe = fopen("encrypt.enc", "rb");
    fk = fopen("sample.key", "wb");

    fread(&blocks, 4, 1, fe);

    for(i=0; i<blocks; i++)
    {
        fread(&p, 4, 1, fp);
        fread(&e, 4, 1, fe);
        k = p^e;
        fwrite(&k, min(4,blocks-i), 1, fk);
    }

    fclose(fp);
    fclose(fe);
    fclose(fk);
    return;
}

unsigned int un_bit_shift_left_xor(unsigned int v,int shift, unsigned int mask)
{
    int i;
    unsigned int r,tmp;

    i = 1;
    r = v;

    while( (i*shift) < 32 )
    {
        tmp = r << shift;
        r   = v ^ (tmp&mask);
        i++;
    }

    return r;
}

unsigned int un_bit_shift_right_xor(unsigned int v,int  shift)
{
    int i;
    unsigned int r,tmp;

    i = 1;
    r = v;

    while( (i*shift) < 32 )
    {
        tmp = r >> shift;
        r   = v ^ tmp;
        i++;
    }

    return r;
}

unsigned int un_tempering(unsigned int k)
{
    unsigned int x;
    x = k;
    x = un_bit_shift_right_xor(x, 18);
    x = un_bit_shift_left_xor(x, 15, 0xefc60000);
    x = un_bit_shift_left_xor(x, 7, 0x9d2c5680);
    x = un_bit_shift_right_xor(x, 11);

    return x;
}

unsigned int emu_init()
{
    FILE  *fk;
    int i;
    unsigned long k;

    fk = fopen("sample.key", "rb");

    for(i=0; i<624; i++)
    {
        fread(&k, 4, 1, fk);
        key[i] = k;
    }

    for(i=0;i<624;i++)  mt[i] = un_tempering(key[i]);
    mti = 624;    
    eti = 0;
}

unsigned int emu_genrand_int32()
{
    unsigned int ret;
    if(eti<624)
    {
        ret = key[eti];
        eti++;
        return ret; 
   }else{
        return genrand_int32();
    }
}

これがソースコードの全体です。 genrand_int32(); の代わりに 画像暗号化時の乱数生成を再現する emu_genrand_int32(); という関数を作って元の encrypt.cppの作業を 再現してみました。

使用する前に 配布されたデータの中の mt19937ar.cpp の 56 57 行目で

unsigned long mt[N]; /* the array for the state vector  */
int mti=N+1; /* mti==N+1 means mt[N] is not initialized */

static を除去します。(解析した値で mt mti を上書きするため)

static 除去が終わったら take21.cpp という名前で解凍したフォルダに ソースコードを保存して

$ g++ -o take21 take21.cpp mt19937ar.cpp 
$ ./take21

でFLAG取得まで自動でおわります。

ちなみに FLAGなどは正常にとれ、JPEGのヘッダは FF D8 からはじまり Adobe Exif などの文字も確認できるのですが 画像自体は破損扱いに なっています。 テーブルの生成の一部に不備があるのかもしれません…が とりあえずFLAGが取れたので良しとします。