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。
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 === '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_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:
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_0061 的 sub.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 """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