こんにちは、愛甲です。今回は、2010年11月27~28日に当サイトで行われたNetAgent Security Contest 2010のLevel1~Level4までの解答を行いたいと思います。問題文とファイルも公開していますので、興味がある方、またはコンテストに参加できなかった方は、よろしければ挑戦してみてください。
-----
■Level1 Activation (FILE)
Level1は特定の日時にしか起動しないプログラムの解析です。問題文は右図で、ファイルは上記のリンク(FILE)からダウンロードできます。
解答を得るもっとも効率的な方法は、Windowsの設定時間を2010年8月14日に変更することでしょう。ただ、もちろんデバッガで解析し、時間取得部分を変更しても構いません。OllyDbg、もしくはIDAProを用いてEXEファイルを逆アセンブルすると、現在時刻の判定を行う以下のコードが見つかります。
// LEVEL1.exe
00401036 |. 52 PUSH EDX
00401037 |. FF15 00204000 CALL DWORD PTR DS:[<&KERNEL32.GetSystemTime>]
0040103D |. 0FB745 DC MOVZX EAX,WORD PTR SS:[EBP-24]
00401041 |. 3D DA070000 CMP EAX,7DA
00401046 |. 75 6D JNZ SHORT LEVEL1.004010B5
00401048 |. 0FB74D DE MOVZX ECX,WORD PTR SS:[EBP-22]
0040104C |. 83F9 08 CMP ECX,8
0040104F |. 75 64 JNZ SHORT LEVEL1.004010B5
00401051 |. 0FB755 E2 MOVZX EDX,WORD PTR SS:[EBP-1E]
00401055 |. 83FA 0E CMP EDX,0E
00401058 |. 75 5B JNZ SHORT LEVEL1.004010B5
GetSystemTimeで現在時間を取得し、年、月、日が、それぞれ2010(0x7DA)、8(0x8)、14(0xE)で比較されています。この部分を変更すれば、解答が得られます。
// LEVEL1.exe
00401036 |. 52 PUSH EDX
00401037 |. FF15 00204000 CALL DWORD PTR DS:[<&KERNEL32.GetSystemTime>]
0040103D |. 0FB745 DC MOVZX EAX,WORD PTR SS:[EBP-24]
00401041 |. 3D DA070000 CMP EAX,7DA
00401046 |. 90 NOP
00401047 |. 90 NOP
00401048 |. 0FB74D DE MOVZX ECX,WORD PTR SS:[EBP-22]
0040104C |. 83F9 08 CMP ECX,8
0040104F |. 90 NOP
00401050 |. 90 NOP
00401051 |. 0FB755 E2 MOVZX EDX,WORD PTR SS:[EBP-1E]
00401055 |. 83FA 0E CMP EDX,0E
00401058 |. 90 NOP
00401059 |. 90 NOP
すべてのJNZ命令をNOPに変更しました。
実行するとパスワード「TVDDFTT」が表示されます。ちなみに、このパスワードは「SUCCESS」をアルファベット順に1シフトした(プログラムとしては1加算した)文字列です。
// LEVEL1.exe
0040105A |. 0FBE45 F6 MOVSX EAX,BYTE PTR SS:[EBP-A]
0040105E |. 83C0 01 ADD EAX,1
00401061 |. 8845 F6 MOV BYTE PTR SS:[EBP-A],AL
00401064 |. 0FBE4D F7 MOVSX ECX,BYTE PTR SS:[EBP-9]
00401068 |. 83C1 01 ADD ECX,1
0040106B |. 884D F7 MOV BYTE PTR SS:[EBP-9],CL
(省略)
004010A0 |. 6A 00 PUSH 0
004010A2 |. 68 08214000 PUSH LEVEL1.00402108
004010A7 |. 8D4D EC LEA ECX,DWORD PTR SS:[EBP-14]
004010AA |. 51 PUSH ECX
004010AB |. 6A 00 PUSH 0
004010AD |. FF15 A8204000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>]
EBP-0xA以降の各文字に+1し、MessageBoxでその文字列を表示しています。EBP-0xAの文字列は「SUCCESS」です。
■Level2 Hidden File (FILE)
Level2はファイルシステムの解析に関する問題です。
NTFSで構築されたファイルシステムのイメージを解析し、ZIPファイルの情報を取り出します。Sleuthkit、Autopsyを用いると、イメージファイルを解析し、ファイルシステム内を探索できます。
削除されたファイルxyz.zipを発見、ファイルサイズは115319とあります。これがLevel2のパスワードです(左下図)。
Sleuthkit、Autopsyを使わずとも、Level2はZIPファイルを探す問題なので、ZIPファイルの先頭4バイトの識別子をイメージファイルから検索しても、ZIPファイルのデータが得られます。
ZIPファイルは「50 4B 03 04」から始まり、「50 4B 05 06」から始まるフッタで終了します(ZIPフォーマット)。これを利用してイメージファイルからZIPファイルを検索します。
$ hexdump -C Forensics.img | grep "50 4b 03 04"
00061000 50 4b 03 04 14 00 01 00 00 00 1a a2 6f 3d d4 cd |PK..........o=..|
00061b80 4c 1d 05 8e b4 59 62 55 4f 33 44 5c 50 4b 03 04 |L....YbUO3D\PK..|
003cd000 50 4b 03 04 14 00 00 00 08 00 0f a2 6f 3d ad 82 |PK..........o=..|
003cfa80 d4 cd 1f cb 53 0a 00 00 c9 0a 00 00 50 4b 03 04 |....S.......PK..|
$ hexdump -C Forensics.img | grep "50 4b 05 06"
0007d260 67 50 4b 05 06 00 00 00 00 04 00 04 00 dc 00 00 |gPK.............|
003cdab0 78 6c 73 50 4b 05 06 00 00 00 00 02 00 02 00 89 |xlsPK...........|
$ perl -e 'print 0x7d261 - 0x61000 + 22'
115319
00061000から始まり0007d260辺りで終わるZIPファイルが見つかりました。フッタのサイズは22バイトなので、差分に22を加算するとZIPファイルのサイズ(解答パスワード)となります。
■Level3 Crypto (FILE)
暗号化ツール(Crypt_cs.exe)と、暗号化されたファイル(password)があり、それらを解析し、復号して元のファイルを得る問題です。
解き方は複数ありますが、いずれにせよRSAであることを推測できるかどうかがカギです。C#で書かれた部分は.Net Reflectorを用いて逆コンパイルできますので、これでソースコードを復元します。button2がクリックされたときの処理が以下のソースコードです。
// button2_Click
private void button2_Click(object sender, EventArgs e)
{
this.label1.Text = "";
this.make_key();
FileInfo info = new FileInfo(this.textBox1.Text);
uint[] numArray = new uint[info.Length];
BinaryReader reader = new BinaryReader(File.OpenRead(this.textBox1.Text));
try
{
for (int i = 0; i < info.Length; i++)
{
byte bInData = 0;
byte num3 = reader.ReadByte();
fnCrypt_cp(1, ref numArray[i], ref num3, g_n, g_e);
fnCrypt_cp(0, ref numArray[i], ref bInData, g_n, g_d);
if (num3 != bInData)
{
goto Label_00B3;
}
}
}
ファイルから1バイト読み込んで、fnCrypt_cp関数を呼び出します。この時、g_nとg_eを使っています。nとeを用いていることから暗号化、つまり暗号化された値がnumArray[i]へ格納されます。
次の1行では、fnCrypt_cp関数の呼び出しにg_nとg_dを用いています。nとd、つまり復号、おそらくnumArray[i]からbInDataへ復号した値が格納されると推測できます。この時点でnum3とbInDataは同じ値になります(num3は平文、bInDataは暗号化→復号を経て平文となっているため)。
問題文からn=13511、e=2645が分かっているため、これらに対応するdを得られれば、passwordファイルを復号できます。g_e、g_n、g_dをセットしているのはmake_key関数です。
// make_key
private uint make_key()
{
uint num;
uint num2;
Random random = new Random();
while (this.j(num = (uint) random.Next(0x100)) != 0)
{
}
while (this.j(num2 = (uint) random.Next(0x100)) != 0)
{
}
uint num3 = num * num2;
uint y = this.l(num - 1, num2 - 1);
uint x = 1;
while (this.g(++x, y) != 1)
{
}
uint num6 = 1;
do
{
num6++;
}
while (this.w(this.w(num6, y) * this.w(x, y), y) != 1);
g_n = num3;
g_e = num6;
g_d = x;
return 0;
}
n=13511を素因数分解すると59と229になります。
$ factor 13511
13511: 59 229
つまり、make_key関数でいうところのnum=59、num2=229となった際のg_dを求めればよいわけです。g_n、g_e、g_dを作る過程で、j、l、g、wといった関数を呼び出していますので、これらのソースコードを読み解き、g_n、g_e、g_dを作成するmake_key関数を自作し、num=59、num2=229を適用した状態で実行するとg_dが求まります。
// makekey.c
#include <stdio.h>
int j(unsigned int x)
{
unsigned int i;
if (x < 10)
{
return 1;
}
for (i = 2; i != x; i++)
{
if ((x % i) == 0)
{
return 1;
}
}
return 0;
}
unsigned int l(unsigned int x, unsigned int y)
{
unsigned int num = x;
unsigned int num2 = y;
while (num != num2)
{
if (num < num2)
{
num += x;
}
else
{
num2 += y;
}
}
return num;
}
unsigned int g(unsigned int x, unsigned int y)
{
unsigned int num;
while (y != 0)
{
num = x % y;
x = y;
y = num;
}
return x;
}
unsigned int w(unsigned int x, unsigned int n)
{
while (n <= x)
{
x -= n;
}
return x;
}
int main(void)
{
unsigned int num = 59;
unsigned int num2 = 229;
unsigned int num6;
unsigned int num3 = num * num2;
unsigned int y = l(num - 1, num2 - 1);
unsigned int x = 1;
while (g(++x, y) != 1)
{
}
num6 = 1;
do
{
num6++;
}
while (w(w(num6, y) * w(x, y), y) != 1);
printf("n=%d, e=%d, d=%d\n", num3, num6, x);
return 0;
}
makekey.cをコンパイルし、実行します。
$ gcc makekey.c -o makekey
$ ./makekey
n=13511, e=2645, d=5
d=5と出力されます。nとeも共に問題文と同じ値を示しました。あとはn=13511とd=5を用いて、DLL内のfnCrypt_cp関数を呼び出せば、passwordファイルを復号できます。
// Ans_Level3.cpp
#include "stdafx.h"
#include <Windows.h>
typedef int (__stdcall *FUNC)(int flag,
unsigned int *c, unsigned char *p,
unsigned int n, unsigned int d);
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE hDll = LoadLibrary("Crypt_cp.dll");
if(hDll == NULL)
return -1;
FUNC f = (FUNC)GetProcAddress(hDll, "fnCrypt_cp");
unsigned int buff;
unsigned char plain;
FILE *fp = fopen("password", "rb");
FILE *fp2 = fopen("ans", "wb");
while(fread(&buff, 4, 1, fp) != 0){
f(0, &buff, &plain, 13511, 5);
fwrite(&plain, 1, 1, fp2);
}
fclose(fp);
fclose(fp2);
FreeLibrary(hDll);
return 0;
}
上記のプログラムを、Crypt_cp.dll、passwordの2つのファイルが存在するフォルダで実行するとansファイルが作成されます。出力されるのはpngファイルであり、このpngファイルを開くと、解答パスワードが表示されます。
以上から、解答パスワードは「RSA_rSA_RsA_RSa_rsa」となります。
■Level4 Character codes (FILE)
文字コードに関する問題です。
問題となるファイルをテキストエディタで開くと以下のように表示されます。
// nacontest.eml
Date: Thu, 14 Oct 2010 16:18:51 +0900
From: Yosuke HASEGAWA
Subject: Decode this garbled message.
To: naseccontest2010@example.jp
Content-Type: text/plain; charset=utf-7
Content-Transfer-Encoding: 7bit
(テキストエディタでは表示できないデータ列が続く)
Utf-7は7ビットしか使用しないデータ列(文字コード)であるため、すべてのバイトデータは0x00~0x7Fの範囲にあるはずですが、実際のデータは0x80以上の値も使われているため、まずは、すべてのバイトの最上位ビットを0(OFF)にします。
// off7.c
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int i, len;
if(argc != 2)
return 1;
len = strlen(argv[1]);
for(i=0; i < len; i++)
argv[1][i] &= 0x7f;
printf(&%s\n&, argv[1]);
return 0;
}
上記のプログラムを用意し、nacontest.emlへ適用します。
// terminal
$ gcc -Wall off7.c -o off7
$ ./off7 &`cat nacontest.eml`&
Date: Thu, 14 Oct 2010 16:18:51 +0900
From: Yosuke HASEGAWA <hasegawa@netagent.example.jp>
Subject: Decode this garbled message.
To: naseccontest2010@example.jp
Content-Type: text/plain; charset=utf-7
Content-Transfer-Encoding: 7bit
+qILmlOqCs4LcgsWCtYK9gkKBCg27gvGCyILJgu+TtYKtgsiCooK2lZq
Ou4mvgr6CwYK9gsaCdo6igtyCt4JCgQoNCg3wiZqTzYJ1gY2CiYKUgoG
Ci4KBgo2Cj4KOgoSCgYKJgnaB8IK8lHCKyYK1gr2C4ILMgsWCt4JCgQo
NsZGrguCC5oqjksGCxIKtgr6Cs4KigkmBCg0KDQoN-
+と-で囲われたBase64の文字列が表示されます。utf-7はutf-16をBase64にて変換したデータ列であるため、utf-16へ変換します。
// enc16le.pl
#!/usr/bin/perl
use Encode;
print encode('utf-16le', decode('utf-7', $ARGV[0]));
この際、utf-16le、utf-16-beの2タイプを用意します。
// enc16be.pl
#!/usr/bin/perl
use Encode;
print encode('utf-16be', decode('utf-7', $ARGV[0]));
上記2つのスクリプトへ、utf-7の文字列を渡します。
$ perl enc16le.pl +qILmlOqCs4LcgsWCtYK9gkKBCg27gvGCyILJgu
+TtYKtgsiCooK2lZqOu4mvgr6CwYK9gsaCdo6igtyCt4JCgQoNCg3wiZqTzYJ1
gY2CiYKUgoGCi4KBgo2Cj4KOgoSCgYKJgnaB8IK8lHCKyYK1gr2C4ILMgsWCt4
JCgQoNsZGrguCC5oqjksGCxIKtgr6Cs4KigkmBCg0KDQoN- > result_le
$ perl enc16be.pl +qILmlOqCs4LcgsWCtYK9gkKBCg27gvGCyILJgu
+TtYKtgsiCooK2lZqOu4mvgr6CwYK9gsaCdo6igtyCt4JCgQoNCg3wiZqTzYJ1
gY2CiYKUgoGCi4KBgo2Cj4KOgoSCgYKJgnaB8IK8lHCKyYK1gr2C4ILMgsWCt4
JCgQoNsZGrguCC5oqjksGCxIKtgr6Cs4KigkmBCg0KDQoN- > result_be
出力されたresult_leとresult_beをテキストエディタで表示します(左図)。
以上の結果から、utf-16leをutf-7へ変換したと考えられます。また、最終的にShift-JISで表示されている点から、Shift-JISで書かれた文章がutf-16leに誤認識されてutf-7へ変換されたと考えられます。
解答パスワードは「mitakamondai」です。
-----
以上でLevel1~Level4までの解答とさせていただきます。なおLevel5~Level8については来年1月に更新予定です。
また、この更新が「当ブログの今年最後の更新」となります。これまでアクセスいただいた読者の方々、本当に有難うございました。また来年も同じように更新していきますので、何卒よろしくお願いいたします。