Post

FEAT:🚩 HTB「Bypass」Easy

Easy, Windows, .Net

FEAT:🚩 HTB「Bypass」Easy

20260404-htb-chall-rev-easy-Bypass

Challenge Scenario: クライアントが完全に制御しています。認証をバイパスし、キーを読み取ってフラグを取得してください。

ディレクトリ構造

1
2
3
4
[4.0K]  ./
└── [8.5K]  Bypass.exe

1 directory, 1 file

Surface Analysis

実行ファイルが windows .Net であることが判明したため,Windows環境で解析を行います. dnSpyEx 132bit 版を用いて静的解析します.

1
2
$ file Bypass.exe
Bypass.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows

Static Analysis & Dynamic Analysis

まずは,.exeを実行してみます.解析環境は隔離しているため,基本的な安全は確保しています. パスワードとユーザーネームを標準入力に渡すのみでした.

1
2
3
4
5
Enter a username: User
Enter a password: Pass
Wrong username and/or password
Enter a username:
Enter a password:

Entry Point である 0.0() から解析を行います. global::0.1() がデフォルトで false を返すことで無限ループになっているため,戻り値を true に改竄し,global::0.2() に進ませる必要があります. dnSpy のデバッガでを用いて F9if (flag2) にブレークポイントを設置し,flag, flag2 変数を true に改竄します.

0 class (Entry Point)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using System;

// Token: 0x02000002 RID: 2
public class 0
{
	// Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258
	public static void 0()
	{
		// ユーザーとの対話ロジック呼び出し
		bool flag = global::0.1();
		// flag (0.1()) は必ず false を返す
		bool flag2 = flag;
		// 通常であれば,else に移動し無限ループとなる
		if (flag2)
		{
			global::0.2();
		}
		else
		{
			Console.WriteLine(5.0);
			global::0.0();
		}
	}

	// Token: 0x06000003 RID: 3 RVA: 0x00002090 File Offset: 0x00000290
	// ユーザーとの対話ロジック
	public static bool 1()
	{
		// Enter a username: 
		Console.Write(5.1);
		// username 文字列読み込み
		string text = Console.ReadLine();
		// Enter a password: 
		Console.Write(5.2);
		// password 文字列読み込み
		string text2 = Console.ReadLine();
		return false;
	}

	// Token: 0x06000004 RID: 4 RVA: 0x000020C8 File Offset: 0x000002C8
	public static void 2()
	{
		string <<EMPTY_NAME>> = 5.3;
		// Please Enter the secret Key: 
		Console.Write(5.4);
		// secret Key を text にバインド
		string text = Console.ReadLine();
		
		// リソースから読み込まれた正解のキーと,ユーザーが入力した文字列を判定するロジック
		bool flag = <<EMPTY_NAME>> == text;
		if (flag)
		{
			// Nice here is the Flag:HTB{ + SuP3rC00lFL4g + }
			Console.Write(5.5 + global::0.2 + 5.6);
		}
		else // 不正解の場合は再帰呼び出しでループ
		{
			Console.WriteLine(5.7);
			global::0.2();
		}
	}

	// Token: 0x04000001 RID: 1
	public static string 0;

	// Token: 0x04000002 RID: 2
	public static string 1;

	// Token: 0x04000003 RID: 3
	public static string 2 = 5.8;
}

if に飛ばしたいため,0.1()Edit IL Instructions 機能を用いて書き換えてパッチします. 必ず,true になるようにしたいため,index 12ldc.i4.0 命令を,ldc.i4.1 命令に変更します.

0.1() IL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0	0000	nop
1	0001	ldsfld	string '5'::'1'
2	0006	call	void [mscorlib]System.Console::Write(string)
3	000B	nop
4	000C	call	string [mscorlib]System.Console::ReadLine()
5	0011	stloc.0
6	0012	ldsfld	string '5'::'2'
7	0017	call	void [mscorlib]System.Console::Write(string)
8	001C	nop
9	001D	call	string [mscorlib]System.Console::ReadLine()
10	0022	stloc.1
11	0023	ldc.i4.0 // idc.i4.o = false => idc.i4.1 = true
12	0024	stloc.2
13	0025	br.s	14 (0027) ldloc.2 
14	0027	ldloc.2
15	0028	ret

すると,Please Enter the secret Key: が新たに表示されるようになりました.

1
2
3
Enter a username: testn
Enter a password: testp
Please Enter the secret Key: 

この構文から予想するに,5.5:HTB{ + 0.2:FLAG + 5.6:{ という Flag 文字列連結である可能性が高いです.

1
Console.Write(5.5 + global::0.2 + 5.6);

public static string 2 = 5.8; では,0 class がロードされた際に,8番フィールド を読み取っています.しかし,global::5.0() がまだ実行されていなければ,これには何の変数も入りません. したがって,このプログラムが起動して最初に文字が表示されるまでの間のどこか にコンストラクタ呼び出しがあるはずです. また,.NET プログラムには,モジュールコンストラクタ と呼ばれる,どのクラスのどのメソッドよりも先に,モジュールがロードされた瞬間に一度だけ実行される特殊な関数 が存在します. <module> .cctor() に記載されており,実際に 5.0() が呼び出されていました. また,0 class.cctor() にも,5.8 (Correct Flagと思われるフィールド) を 0.2 にバインドする処理が記載されています.

0 class > .cctor()

1
2
3
4
5
6
7
// 0
// Token: 0x06000006 RID: 6 RVA: 0x00002132 File Offset: 0x00000332
// Note: this type is marked as 'beforefieldinit'.
static 0()
{
	0.2 = 5.8;
}

module > .cctor()

1
2
3
4
5
6
// <Module>  
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250  
static <Module>()  
{    
	5.0();  
}

static variablesIL のパッチでデバッグ処理を埋め込むことで正解の文字列無しに Flag を見ます.

0.2() IL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
0	0000	nop
1	0001	ldsfld	string '5'::'3'
2	0006	stloc.0
3	0007	ldsfld	string '5'::'4'
4	000C	call	void [mscorlib]System.Console::Write(string)
5	0011	nop
// パッチ
6	0012	ldsfld	string '5'::'5'
7	0017	ldsfld	string '0'::'2'
8	001C	ldsfld	string '5'::'6'
9	0021	call	string [mscorlib]System.String::Concat(string, string, string)
10	0026	call	void [mscorlib]System.Console::Write(string)
11	002B	nop
// ここまで
12	002C	call	string [mscorlib]System.Console::ReadLine()
13	0031	stloc.1
14	0032	ldloc.0
15	0033	ldloc.1
16	0034	call	bool [mscorlib]System.String::op_Equality(string, string)
17	0039	stloc.2
18	003A	ldloc.2
19	003B	brfalse.s	29 (005B) nop 
20	003D	nop
21	003E	ldsfld	string '5'::'5'
22	0043	ldsfld	string '0'::'2'
23	0048	ldsfld	string '5'::'6'
24	004D	call	string [mscorlib]System.String::Concat(string, string, string)
25	0052	call	void [mscorlib]System.Console::Write(string)
26	0057	nop
27	0058	nop
28	0059	br.s	36 (006E) ret 
29	005B	nop
30	005C	ldsfld	string '5'::'7'
31	0061	call	void [mscorlib]System.Console::WriteLine(string)
32	0066	nop
33	0067	call	void '0'::'2'()
34	006C	nop
35	006D	nop
36	006E	ret

Flag を入手することができました.

1
2
3
Enter a username:
Enter a password:
Please Enter the secret Key: Nice here is the Flag:<Flag>

.NET難読化+埋め込みリソース展開パターン

5 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Reflection;

// Token: 0x02000007 RID: 7
internal static class 5
{
	// Token: 0x0600000B RID: 11 RVA: 0x00002194 File Offset: 0x00000394
	public static void 0()
	{
		// 実行ファイル内の埋め込みリソース 0 を取得
		6 <<EMPTY_NAME>> = new 6(global::7.3(Assembly.GetExecutingAssembly().GetManifestResourceStream("0")));
		global::5.0 = <<EMPTY_NAME>>.6(); // Wrong username and/or password
		global::5.1 = <<EMPTY_NAME>>.6(); // Enter a username: 
		global::5.2 = <<EMPTY_NAME>>.6(); // Enter a password: 
		global::5.3 = <<EMPTY_NAME>>.6(); // ThisIsAReallyReallySecureKeyButYouCanReadItFromSourceSoItSucks 
		global::5.4 = <<EMPTY_NAME>>.6(); // Please Enter the secret Key: 
		global::5.5 = <<EMPTY_NAME>>.6(); // Nice here is the Flag:HTB{
		global::5.6 = <<EMPTY_NAME>>.6(); // }
		global::5.7 = <<EMPTY_NAME>>.6(); // Wrong Key
		global::5.8 = <<EMPTY_NAME>>.6(); // SuP3rC00lFL4g
		global::5.9 = <<EMPTY_NAME>>.6(); // This executable has been obfuscated by using RustemSoft Skater .NET Obfuscator Demo version. Please visit RustemSoft.com for more information.
		global::5.a = <<EMPTY_NAME>>.6(); // This executable has been obfuscated by using RustemSoft Skater .NET Obfuscator Demo version. Please visit RustemSoft.com for more information.
		global::5.b = <<EMPTY_NAME>>.6(); // This executable has been obfuscated by using RustemSoft Skater .NET Obfuscator Demo version. Please visit RustemSoft.com for more information.
		global::5.c = <<EMPTY_NAME>>.6(); // This executable has been obfuscated by using RustemSoft Skater .NET Obfuscator Demo version. Please visit RustemSoft.com for more information.
	}

	// Token: 0x04000008 RID: 8
	internal static string 0;

	// Token: 0x04000009 RID: 9
	internal static string 1;

	// Token: 0x0400000A RID: 10
	internal static string 2;

	// Token: 0x0400000B RID: 11
	internal static string 3;

	// Token: 0x0400000C RID: 12
	internal static string 4;

	// Token: 0x0400000D RID: 13
	internal static string 5;

	// Token: 0x0400000E RID: 14
	internal static string 6;

	// Token: 0x0400000F RID: 15
	internal static string 7;

	// Token: 0x04000010 RID: 16
	internal static string 8;

	// Token: 0x04000011 RID: 17
	internal static string 9;

	// Token: 0x04000012 RID: 18
	internal static string a;

	// Token: 0x04000013 RID: 19
	internal static string b;

	// Token: 0x04000014 RID: 20
	internal static string c;
}

6 class は,静的変数等のシリアライズされたデータを順番に読み取るためのラッパクラスです.

6 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.IO;
using System.Text;

// Token: 0x02000008 RID: 8
internal class 6
{
	// Token: 0x0600000C RID: 12 RVA: 0x0000224A File Offset: 0x0000044A
	public 6(Stream 0)
	{
		this.0 = new BinaryReader(0, Encoding.Unicode);
	}

	// Token: 0x0600000D RID: 13 RVA: 0x00002263 File Offset: 0x00000463
	public 6(byte[] 0)
		: this(new MemoryStream(0))
	{
	}

	// Token: 0x0600000E RID: 14 RVA: 0x00002271 File Offset: 0x00000471
	public string 6()
	{
		return this.0.ReadString();
	}

	// Token: 0x0600000F RID: 15 RVA: 0x0000227E File Offset: 0x0000047E
	public sbyte 7()
	{
		return this.0.ReadSByte();
	}

	// Token: 0x06000010 RID: 16 RVA: 0x0000228B File Offset: 0x0000048B
	public int 8()
	{
		return this.0.ReadInt32();
	}

	// Token: 0x06000011 RID: 17 RVA: 0x00002298 File Offset: 0x00000498
	public long 9()
	{
		return this.0.ReadInt64();
	}

	// Token: 0x06000012 RID: 18 RVA: 0x000022A5 File Offset: 0x000004A5
	public float a()
	{
		return this.0.ReadSingle();
	}

	// Token: 0x06000013 RID: 19 RVA: 0x000022B2 File Offset: 0x000004B2
	public double b()
	{
		return this.0.ReadDouble();
	}

	// Token: 0x04000015 RID: 21
	private readonly BinaryReader 0;
}

リソース 0AES (Rijndael/CBC) で暗号化されており,その中に文字列データが入っています. 7 class では,リソース 0 のストリームデータを読み込み,AES 復号化を行うことで静的解析での Flag 窃取を妨害していました.

7 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.IO;
using System.Security.Cryptography;

// Token: 0x02000009 RID: 9
public static class 7
{
	// Token: 0x06000014 RID: 20 RVA: 0x000022C0 File Offset: 0x000004C0
	// AES復号
	public static byte[] 2(byte[] 0)
	{
		byte[] array4;
		using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
		{
			rijndaelManaged.BlockSize = 128;
			rijndaelManaged.Mode = CipherMode.CBC;
			rijndaelManaged.GenerateKey();
			rijndaelManaged.GenerateIV();
			using (MemoryStream memoryStream = new MemoryStream(0))
			{
				byte[] array = new byte[rijndaelManaged.Key.Length];
				byte[] array2 = new byte[rijndaelManaged.IV.Length];
				memoryStream.Read(array, 0, array.Length);
				memoryStream.Read(array2, 0, array2.Length);
				using (ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor(array, array2))
				{
					using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Read))
					{
						byte[] array3 = new byte[memoryStream.Length - memoryStream.Position];
						cryptoStream.Read(array3, 0, array3.Length);
						array4 = array3;
					}
				}
			}
		}
		return array4;
	}

	// Token: 0x06000015 RID: 21 RVA: 0x000023D0 File Offset: 0x000005D0
	// ストリーム読み込み
	public static byte[] 3(Stream 0)
	{
		byte[] array = new byte[0.Length];
		0.Read(array, 0, array.Length);
		return 7.2(array);
	}

	// Token: 0x04000016 RID: 22
	public const int 0;
}

暗号設計 (難読化処理)

  • AES-128-CBC
  • Keyが平文で埋め込み
  • IVも平文
  • そのまま復号可能
1
2
3
RijndaelManaged  
BlockSize = 128  
Mode = CBC

Flags

Flag HTB{SuP3rC00lFL4g}

References & Memo

This post is licensed under CC BY 4.0 by the author.