HackThisSite - Application Mission 14

posted
read
542 words · 2 min
contents

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。
Terminal window
$ 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

Terminal window
$ 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: 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 就是这个注册表依赖。

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: rem
76 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 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 = []
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.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 程序集,会直接挂掉):

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"}) + "]");
}
}
Terminal window
$ 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]