FEAT:🚩 HTB「Bypass」Easy
Easy, Windows, .Net
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 1 の 32bit 版を用いて静的解析します.
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 のデバッガでを用いて F9 で if (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 12 の ldc.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 variables を IL のパッチでデバッグ処理を埋め込むことで正解の文字列無しに 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;
}
リソース 0 は AES (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