HackThisSite - Application Mission 14

Challenge

It's Windows only for a reason.

只在 Windows 上运行是有原因的。

app14.exe 是个 .NET(VB.NET)WinForms 程序,启动时创建并使用注册表键 HKEY_CURRENT_USER\valid,验证逻辑挂在 Validate 按钮的点击事件里。

Solution

  • file app14.exePE32 executable for MS Windows 4.00 (GUI), Intel i386 Mono/.Net assembly, 4 sections:是 .NET 程序集,托管代码可以直接反编译,不用脱壳。
  • ikdasm 导出 IL,只有两个类:app14.goes(窗体,按钮名 vla、文本框 txt)和 app14.Encrypt(加 / 解密)。
  • 字符串堆里直接摆着全部线索:HKEY_CURRENT_USER\validsomerandomvl585mfg9gffm`{f}kpwrnnopesorry
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
$ cd <hts-workspace> && uv run python challenges/hts-app/app14/analyze_app14.py
=== #US heap (UTF-16LE literals) ===
'Property can only be set to Nothing'
'WinForms_RecursiveFormCreate'
'WinForms_SeeInnerException'
'txt'
'vla'
'Validate'
'goes'
'HKEY_CURRENT_USER\\valid'
'somerandomvl'
'ydXX!if not txt is blah youfailed'
'585mfg9gf'
'nope'
'sorry'
'fm`{f}kpwrn'
'valid'
'app14.Resources'

=== raw scan for candidate literals ===
b'fm`{f}kpwrn' -> NOT FOUND
b'ydXX!' -> NOT FOUND
b'somerandomvl' -> NOT FOUND
b'585mfg9gf' -> NOT FOUND
b'nope' -> NOT FOUND
b'sorry' -> NOT FOUND
b'valid' -> 0x32c2

#US 堆里的字符串是 UTF-16LE,所以按 ASCII 字节搜 fm`{f}kpwrn 搜不到。)

Step 1: 反编译 vla_Click

1
$ ikdasm app14.exe > app14_ildasm.txt

按钮 vla 的点击处理是 app14.goes::vla_Click,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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
.method private instance void  vla_Click(object sender,
[mscorlib]System.EventArgs e) cil managed
{
// Code size 344 (0x158)
.maxstack 4
.locals init (int32[] V_0,
[mscorlib]System.Decimal V_1,
[Microsoft.VisualBasic]Microsoft.VisualBasic.VariantType V_2,
int32 V_3)
IL_0000: ldc.i4.s 12
IL_0002: newarr [mscorlib]System.Int32
IL_0007: stloc.0
IL_0008: call class app14.My.MyComputer app14.My.MyProject::get_Computer()
IL_000d: callvirt instance [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Registry()
IL_0012: ldstr "HKEY_CURRENT_USER\\valid"
IL_0017: ldstr ""
IL_001c: ldc.i4.0
IL_001d: box [mscorlib]System.Int32
IL_0022: callvirt instance object [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy::GetValue(string,
string,
object)
IL_0027: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToInteger(object)
IL_002c: stloc.2
IL_002d: ldloc.2
IL_002e: conv.r8
IL_002f: ldc.r8 1.5
IL_0038: add
IL_0039: ldloc.2
IL_003a: conv.r8
IL_003b: mul
IL_003c: ldc.r8 0.025
IL_0045: add
IL_0046: newobj instance void [mscorlib]System.Decimal::.ctor(float64)
IL_004b: stloc.1
IL_004c: ldarg.0
IL_004d: callvirt instance [System.Windows.Forms]System.Windows.Forms.TextBox app14.goes::get_txt()
IL_0052: callvirt instance string [System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
IL_0057: ldstr "somerandomvl"
IL_005c: ldc.i4.0
IL_005d: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string,
string,
bool)
IL_0062: ldc.i4.0
IL_0063: bne.un.s IL_008d

IL_0065: ldstr "ydXX!if not txt is blah youfailed"
IL_006a: call object app14.Encrypt::ParseandEncrypt(string)
IL_006f: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
IL_0074: call object app14.Encrypt::ParseandDecrypt(string)
IL_0079: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
IL_007e: ldc.i4.s 48
IL_0080: ldstr "585mfg9gf"
IL_0085: call [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object,
[Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle,
object)
IL_008a: pop
IL_008b: br.s IL_00a9

IL_008d: ldstr "ydXX!if not txt is blah youfailed"
IL_0092: call object app14.Encrypt::ParseandDecrypt(string)
IL_0097: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
IL_009c: ldc.i4.s 48
IL_009e: ldstr "585mfg9gf"
IL_00a3: call [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object,
[Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle,
object)
IL_00a8: pop
IL_00a9: ldloc.1
IL_00aa: call float64 [mscorlib]System.Convert::ToDouble([mscorlib]System.Decimal)
IL_00af: ldloc.2
IL_00b0: conv.r8
IL_00b1: ldc.r8 1.5
IL_00ba: add
IL_00bb: ldloc.2
IL_00bc: conv.r8
IL_00bd: mul
IL_00be: ldc.r8 0.025
IL_00c7: add
IL_00c8: bne.un IL_0157

IL_00cd: ldloc.2
IL_00ce: ldc.i4.0
IL_00cf: ceq
IL_00d1: call class app14.My.MyApplication app14.My.MyProject::get_Application()
IL_00d6: callvirt instance [Microsoft.VisualBasic]Microsoft.VisualBasic.ApplicationServices.AssemblyInfo [Microsoft.VisualBasic]Microsoft.VisualBasic.ApplicationServices.ApplicationBase::get_Info()
IL_00db: callvirt instance string [Microsoft.VisualBasic]Microsoft.VisualBasic.ApplicationServices.AssemblyInfo::get_DirectoryPath()
IL_00e0: callvirt instance int32 [mscorlib]System.String::get_Length()
IL_00e5: stloc.3
IL_00e6: ldloca.s V_3
IL_00e8: call instance string [mscorlib]System.Int32::ToString()
IL_00ed: callvirt instance int32 [mscorlib]System.String::get_Length()
IL_00f2: ldc.i4 0x1ca
IL_00f7: ceq
IL_00f9: or
IL_00fa: brfalse.s IL_0110

IL_00fc: ldstr "nope"
IL_0101: ldc.i4.s 64
IL_0103: ldstr "sorry"
IL_0108: call [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object,
[Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle,
object)
IL_010d: pop
IL_010e: br.s IL_0157

IL_0110: ldarg.0
IL_0111: callvirt instance [System.Windows.Forms]System.Windows.Forms.TextBox app14.goes::get_txt()
IL_0116: callvirt instance string [System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
IL_011b: ldarg.0
IL_011c: callvirt instance [System.Windows.Forms]System.Windows.Forms.TextBox app14.goes::get_txt()
IL_0121: callvirt instance string [System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
IL_0126: ldc.i4.0
IL_0127: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string,
string,
bool)
IL_012c: ldc.i4.0
IL_012d: ceq
IL_012f: ldc.i4.0
IL_0130: ceq
IL_0132: ldc.i4.0
IL_0133: and
IL_0134: ldc.i4.0
IL_0135: or
IL_0136: ldloc.2
IL_0137: ldc.i4.1
IL_0138: ceq
IL_013a: or
IL_013b: brfalse.s IL_0157

IL_013d: ldarg.0
IL_013e: callvirt instance [System.Windows.Forms]System.Windows.Forms.TextBox app14.goes::get_txt()
IL_0143: ldstr "fm`{f}kpwrn"
IL_0148: call object app14.Encrypt::ParseandDecrypt(string)
IL_014d: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
IL_0152: callvirt instance void [System.Windows.Forms]System.Windows.Forms.TextBox::set_Text(string)
IL_0157: ret
} // end of method goes::vla_Click

读法:

  • IL_0008IL_002c:从注册表 HKEY_CURRENT_USER\valid 取值(默认 0)存进 validV_2)。
  • IL_004cIL_0063:把文本框内容和 "somerandomvl" 比较。相等走 IL_0065(把 "ydXX!if not txt is blah youfailed" 先加密再解密,原地打转,等于原样弹出来),不等走 IL_008d(直接解密这句密文,弹出乱码)。两条路都只是干扰信息。
  • IL_00c8bne.un 永远不跳(比较的是同一个表达式 (valid+1.5)*valid+0.025),直接进 IL_00cd
  • IL_00cdIL_00fa(valid == 0) | (目录路径长度转字符串再取长度的结果 == 0x1ca) 为真就弹 nope/sorry。正常目录长度不是 0x1ca,所以这一支要求 valid != 0
  • IL_0110IL_013bCompareString(txt, txt) 恒等,再加 and 0 / or 0 与 0 的比较恒假,整段串起来等价于 valid == 1
  • IL_013dIL_0152真正的结局txt.Text = ParseandDecrypt("fm`{f}kpwrn")

也就是说 valid 注册表值必须是 1,程序才会把解密后的密码填进文本框。这个值由 goes_Load 在窗体加载时创建并写成 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IL_0000:  call       class app14.My.MyComputer app14.My.MyProject::get_Computer()
IL_0005: callvirt instance [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Registry()
IL_000a: callvirt instance [mscorlib]Microsoft.Win32.RegistryKey [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy::get_CurrentUser()
IL_000f: ldstr "valid"
IL_0014: callvirt instance [mscorlib]Microsoft.Win32.RegistryKey [mscorlib]Microsoft.Win32.RegistryKey::CreateSubKey(string)
IL_0019: pop
IL_001a: call class app14.My.MyComputer app14.My.MyProject::get_Computer()
IL_001f: callvirt instance [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Registry()
IL_0024: ldstr "HKEY_CURRENT_USER\\valid"
IL_0029: ldstr ""
IL_002e: ldstr "0"
IL_0033: callvirt instance void [Microsoft.VisualBasic]Microsoft.VisualBasic.MyServices.RegistryProxy::SetValue(string,
string,
object)
IL_0038: ret

Windows only for a reason 的 reason 就是这个注册表依赖:不想靠点击 GUI,直接把密文解密出来即可。

Step 2: Encrypt.ParseandDecrypt

app14.Encrypt::ParseandDecrypt 的 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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
.method public static object  ParseandDecrypt(string stringd) cil managed
{
// Code size 150 (0x96)
.maxstack 4
.locals init (string V_0,
int32 V_1,
int32[] V_2,
int32 V_3,
string V_4,
object V_5,
int32 V_6,
int32 V_7)
IL_0000: ldc.i4 0x80
IL_0005: newarr [mscorlib]System.Int32
IL_000a: stloc.2
IL_000b: ldc.i4.0
IL_000c: stloc.s V_6
IL_000e: ldloc.s V_6
IL_0010: ldc.i4.2
IL_0011: rem
IL_0012: ldc.i4.0
IL_0013: bne.un.s IL_001d

IL_0015: ldloc.2
IL_0016: ldloc.s V_6
IL_0018: ldloc.s V_6
IL_001a: ldc.i4.3
IL_001b: sub.ovf
IL_001c: stelem.i4
IL_001d: ldloc.s V_6
IL_001f: ldc.i4.2
IL_0020: rem
IL_0021: ldc.i4.1
IL_0022: bne.un.s IL_002c

IL_0024: ldloc.2
IL_0025: ldloc.s V_6
IL_0027: ldloc.s V_6
IL_0029: ldc.i4.4
IL_002a: add.ovf
IL_002b: stelem.i4
IL_002c: ldloc.s V_6
IL_002e: ldc.i4.1
IL_002f: add.ovf
IL_0030: stloc.s V_6
IL_0032: ldloc.s V_6
IL_0034: ldc.i4.s 127
IL_0036: ble.s IL_000e

IL_0038: ldc.i4.0
IL_0039: stloc.s V_6
IL_003b: ldstr ""
IL_0040: stloc.s V_4
IL_0042: ldc.i4.1
IL_0043: ldarg.0
IL_0044: callvirt instance int32 [mscorlib]System.String::get_Length()
IL_0049: stloc.s V_7
IL_004b: stloc.1
IL_004c: br.s IL_008a

IL_004e: ldarg.0
IL_004f: ldloc.1
IL_0050: ldc.i4.1
IL_0051: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.Strings::Mid(string,
int32,
int32)
IL_0056: stloc.0
IL_0057: ldloc.0
IL_0058: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.Strings::Asc(string)
IL_005d: ldloc.2
IL_005e: ldloc.s V_6
IL_0060: ldelem.i4
IL_0061: sub.ovf
IL_0062: stloc.3
IL_0063: ldloc.s V_4
IL_0065: ldloc.3
IL_0066: call char [Microsoft.VisualBasic]Microsoft.VisualBasic.Strings::Chr(int32)
IL_006b: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(char)
IL_0070: call string [mscorlib]System.String::Concat(string,
string)
IL_0075: stloc.s V_4
IL_0077: ldloc.s V_6
IL_0079: ldc.i4.1
IL_007a: add.ovf
IL_007b: stloc.s V_6
IL_007d: ldloc.s V_6
IL_007f: ldc.i4.s 127
IL_0081: ble.s IL_0086

IL_0083: ldc.i4.0
IL_0084: stloc.s V_6
IL_0086: ldloc.1
IL_0087: ldc.i4.1
IL_0088: add.ovf
IL_0089: stloc.1
IL_008a: ldloc.1
IL_008b: ldloc.s V_7
IL_008d: ble.s IL_004e

IL_008f: ldloc.s V_4
IL_0091: stloc.s V_5
IL_0093: ldloc.s V_5
IL_0095: ret
} // end of method Encrypt::ParseandDecrypt

算法很直白:建一张 128 项的移位表,偶数下标 arr[i] = i - 3,奇数下标 arr[i] = i + 4;然后对每个字符 out = Chr(Asc(ch) - arr[j])j 从 0 数到 127 后归零。ParseandEncrypt 与之逐字节相同,只把 IL_0061sub.ovf 换成 add.ovf

移植成 Python:

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
#!/usr/bin/env python3
"""Decrypt HTS app14's hard-coded password (port of Encrypt.ParseandDecrypt).

From the IL of app14.Encrypt::ParseandDecrypt: build a 128-entry shift table
where arr[i] = i - 3 for even i and arr[i] = i + 4 for odd i, then subtract
arr[j] (j cycling 0..127) from each character code.
"""
def make_table():
arr = [0] * 128
for i in range(128):
if i % 2 == 0:
arr[i] = i - 3
else:
arr[i] = i + 4
return arr


def parse_and_decrypt(s):
arr = make_table()
out = []
j = 0
for ch in s:
out.append(chr(ord(ch) - arr[j]))
j += 1
if j > 127:
j = 0
return "".join(out)


def parse_and_encrypt(s):
arr = make_table()
out = []
j = 0
for ch in s:
out.append(chr(ord(ch) + arr[j]))
j += 1
if j > 127:
j = 0
return "".join(out)


if __name__ == "__main__":
enc = "fm`{f}kpwrn"
print("ciphertext :", enc)
print("password :", parse_and_decrypt(enc))
print("round-trip check:", parse_and_encrypt(parse_and_decrypt(enc)))
1
2
3
4
$ cd <hts-workspace> && uv run python challenges/hts-app/app14/app14_decrypt.py
ciphertext : fm`{f}kpwrn
password : ihatethereg
round-trip check: fm`{f}kpwrn

Step 3: 动态验证

与其只信移植版,不如让 wine 加载真正的 app14.exe 程序集,用反射调用它自己的 Encrypt.ParseandDecrypt。wine 自带 .NET(wine-mono),直接可跑编译好的 driver(原生 mono driver.exe 加载不到版本匹配的 Microsoft.VisualBasic 8.0.0.0 程序集,会直接挂掉):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Reflection;

class Driver {
static void Main() {
var asm = Assembly.LoadFrom("app14.exe");
var t = asm.GetType("app14.Encrypt");
var dec = t.GetMethod("ParseandDecrypt");
var enc = t.GetMethod("ParseandEncrypt");
Console.WriteLine("ParseandDecrypt(\"fm`{f}kpwrn\") = [" + dec.Invoke(null, new object[]{"fm`{f}kpwrn"}) + "]");
Console.WriteLine("ParseandDecrypt(\"ydXX!if not txt is blah youfailed\") = [" + dec.Invoke(null, new object[]{"ydXX!if not txt is blah youfailed"}) + "]");
Console.WriteLine("ParseandEncrypt(\"ihatethereg\") = [" + enc.Invoke(null, new object[]{"ihatethereg"}) + "]");
}
}
1
2
3
4
5
$ mcs driver.cs
$ WINEDEBUG=-all wine driver.exe
ParseandDecrypt("fm`{f}kpwrn") = [ihatethereg]
ParseandDecrypt("ydXX!if not txt is blah youfailed") = [|_YQ `c?ibm?kgi?\\^?K[HU?dR^GHHQBG]
ParseandEncrypt("ihatethereg") = [fm`{f}kpwrn]

密文 fm`{f}kpwrn 经程序自身的函数解出 ihatethereg,反向加密又能还原回密文,双向自洽。

ihatethereg