Post

FEAT:🚩 Daily-AlpacaHack 「Alpaca Form」Medium

.Netプログラムの逆解析とRVA埋め込み

FEAT:🚩 Daily-AlpacaHack 「Alpaca Form」Medium

20260528-daily_alpaca-rev-medium-Alpaca_Form

  • Category: Rev
  • Description: “この問題はLinux環境だけでも解けます。”
  • Tech Stack: CSharp, .Net
  • Keyword: Hardcoded_Credentials
  • Flag: {*** REDACTED ***}

階層構造

1
2
3
4
5
6
7
.
├── AlpacaForm.deps.json
├── AlpacaForm.dll
├── AlpacaForm.exe
└── AlpacaForm.runtimeconfig.json

1 directory, 4 files

Solution Path

表層解析

1
2
3
4
5
$ file AlpacaForm.dll
AlpacaForm.dll: PE32+ executable (GUI) x86-64 Mono/.Net assembly, for MS Windows, 2 sections

$ file AlpacaForm.exe
AlpacaForm.exe: PE32+ executable (GUI) x86-64, for MS Windows, 6 sections

動的解析

デコンパイルする前に,実際に .exe を起動し,大まかな動作を確認します. 起動すると,GUI が表示され,ただ入力された Flag 文字列が正解であるかを判定するだけのようです.

静的解析

AlpacaForm.dll を,DnSpy1 に渡して,デコンパイルします. 難読化などが一切されていないため,非常にクリーンなコードが復元できました. 明らかにフラッグ判定を行う名前である IsValidFlag(String) を確認します.

PrivateImplementationDetails 型に埋め込まれた静的文字列と,入力した Flag 文字列を一文字ずつ u (0x75/117)XOR しています. なので,埋め込まれた静的文字列が分かれば,Flag を復元することが可能です.

{} AlpacaForm::AlpacaForm::IsValidFlag(String)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AlpacaForm.AlpacaForm
// Token: 0x06000003 RID: 3 RVA: 0x00002094 File Offset: 0x00000294
[NullableContext(1)]
private unsafe static bool IsValidFlag(string flag)
{
	ReadOnlySpan<byte> readOnlySpan = new ReadOnlySpan<byte>((void*)(&<PrivateImplementationDetails>.E7FA4689F40C30105EA7D7783F036683B8E200105687796846C09EC688C7C146), 34);
	if (flag.Length != readOnlySpan.Length)
	{
		return false;
	}
	for (int i = 0; i < flag.Length; i++)
	{
		if ((flag[i] ^ 'u') != (char)(*readOnlySpan[i]))
		{
			return false;
		}
	}
	return true;
}

PrivateImplementationDetails 型にアクセスしても,C# 表示では static field参照 として抽象化されているため,静的文字列が表示されません. そこで,表示を IL に切り替えることで,.data cil I_00004930 としてバイト配列が表示されました. 復元に必要な情報は入手したので,復元スクリプトを書きます.

{} -::<PrivateImplementationDetails>

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
// Token: 0x02000005 RID: 5
.class private auto ansi sealed '<PrivateImplementationDetails>'
	extends [System.Runtime]System.Object
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Nested Types
	// Token: 0x02000006 RID: 6
	.class nested assembly explicit ansi sealed '__StaticArrayInitTypeSize=34'
		extends [System.Runtime]System.ValueType
	{
		.pack 1
		.size 34

	} // end of class __StaticArrayInitTypeSize=34


	// Fields
	// Token: 0x04000005 RID: 5 RVA: 0x00004930 File Offset: 0x00002B30
	.field assembly static initonly valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=34' E7FA4689F40C30105EA7D7783F036683B8E200105687796846C09EC688C7C146 at I_00004930
	.data cil I_00004930 = bytearray (
		34 19 05 14 16 14 0e 18 10 10 07 1e 14 01 2a 12
		14 2a 13 19 14 18 1c 1b 12 1a 54 2a 1a 01 1a 01
		1a 08
	)

} // end of class <PrivateImplementationDetails>

Flag の復元

バイト列をPython配列に変換してもらう処理 (0x prefixの付加) は,gemini にやってもらいました. 入手したバイトと,uascii 変換したものを XOR します.すると Flag 文字列が復元できました.

Flag復元 Pythonスクリプト

1
2
3
4
5
6
7
8
9
10
11
data = bytes([
    0x34, 0x19, 0x05, 0x14, 0x16, 0x14, 0x0e, 0x18,
    0x10, 0x10, 0x07, 0x1e, 0x14, 0x01, 0x2a, 0x12,
    0x14, 0x2a, 0x13, 0x19, 0x14, 0x18, 0x1c, 0x1b,
    0x12, 0x1a, 0x54, 0x2a, 0x1a, 0x01, 0x1a, 0x01,
    0x1a, 0x08
])

flag = ''.join(chr(b ^ ord('u')) for b in data)

print(flag)

試しに,AlpacaForm.exe に復元した Flag を入力しても,正しく Correct と表示されました.

1
2
$ python3 exploit.py
{*** REDACTED ***}

Post-Mortem & Dead ends

.NETバイナリにおける ReadOnlySpan<byte> の最適化挙動 (RVA埋め込み)

C#において,コード内に固定値の初期化データを ReadOnlySpan<byte>byte[] で直書きすると,コンパイラはそれをコード領域ではなく,メタデータ内の特殊な静的領域(.data cil / PrivateImplementationDetails)に配置する最適化 (RVA埋め込み)2 を行います.

  • C#表示での抽象化: dnSpyやILSpyのデフォルトのC#ビューでは,コンパイラが自動生成したフィールド名しか見えず,生データが隠されます.
  • 解決策: .NETのRevにおいて 配列の中身がC#コード上で見えない ケースに遭遇した際は,IL (Intermediate Language) 表示 に切り替えます. または,バイナリの .text.data セクションを直接探すのが定石です.

References

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