Challenge
It’s Windows only for a reason.
只在 Windows 上运行是有原因的。
app14.exe 是个 .NET(VB.NET)WinForms 程序,启动时创建并使用注册表键 HKEY_CURRENT_USER\valid,验证逻辑挂在 Validate 按钮的点击事件里。
Solution
file app14.exe→PE32 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\valid、somerandomvl、585mfg9gf、fm`{f}kpwrn、nope、sorry。
$ 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 FOUNDb'ydXX!' -> NOT FOUNDb'somerandomvl' -> NOT FOUNDb'585mfg9gf' -> NOT FOUNDb'nope' -> NOT FOUNDb'sorry' -> NOT FOUNDb'valid' -> 0x32c2(#US 堆里的字符串是 UTF-16LE,所以按 ASCII 字节搜 fm`{f}kpwrn 搜不到。)
Step 1: 反编译 vla_Click
$ ikdasm app14.exe > app14_ildasm.txt按钮 vla 的点击处理是 app14.goes::vla_Click,IL 全文:
.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,109 collapsed lines
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_0008–IL_002c:从注册表HKEY_CURRENT_USER\valid取值(默认 0)存进valid(V_2)。IL_004c–IL_0063:把文本框内容和"somerandomvl"比较。相等走IL_0065(把"ydXX!if not txt is blah youfailed"先加密再解密,原地打转,等于原样弹出来),不等走IL_008d(直接解密这句密文,弹出乱码)。两条路都只是干扰信息。IL_00c8的bne.un永远不跳(比较的是同一个表达式(valid+1.5)*valid+0.025),直接进IL_00cd。IL_00cd–IL_00fa:(valid == 0) | (目录路径长度转字符串再取长度的结果 == 0x1ca)为真就弹nope/sorry。正常目录长度不是 0x1ca,所以这一支要求valid != 0。IL_0110–IL_013b:CompareString(txt, txt)恒等,再加and 0/or 0与 0 的比较恒假,整段串起来等价于valid == 1。IL_013d–IL_0152:txt.Text = ParseandDecrypt("fm`{f}kpwrn")。
也就是说 valid 注册表值必须是 1,程序才会把解密后的密码填进文本框。这个值由 goes_Load 在窗体加载时创建并写成 0:
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: popIL_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: retWindows only for a reason 就是这个注册表依赖。
Step 2: Encrypt.ParseandDecrypt
app14.Encrypt::ParseandDecrypt 的 IL:
.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: rem76 collapsed lines
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_0061 的 sub.ovf 换成 add.ovf。
移植成 Python:
#!/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 tablewhere arr[i] = i - 3 for even i and arr[i] = i + 4 for odd i, then subtractarr[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 = []18 collapsed lines
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)))$ cd <hts-workspace> && uv run python challenges/hts-app/app14/app14_decrypt.pyciphertext : fm`{f}kpwrnpassword : ihatethereground-trip check: fm`{f}kpwrnStep 3: 动态验证
让 wine 加载真正的 app14.exe 程序集,用反射调用它自己的 Encrypt.ParseandDecrypt。wine 自带 .NET(wine-mono),直接可跑编译好的 driver(原生 mono driver.exe 加载不到版本匹配的 Microsoft.VisualBasic 8.0.0.0 程序集,会直接挂掉):
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"}) + "]"); }}$ mcs driver.cs$ WINEDEBUG=-all wine driver.exeParseandDecrypt("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]