考试追踪:ExamCookie

我了解到,丹麦政府不仅暂停了我们在上一篇文章中分析并完全规避 Digital Exam Monitor程序,而且还可能在我们将黑客方法告知他们一周后完全关闭了该系统。 我不想认为纯粹是因为我们,丹麦政府拒绝了监督考试的想法,但是我们的工作明显受到关注。

在本文中,我们将概述另一个学童跟踪工具如何工作的技术细节:ExamCookie。 如果仅对绕过系统感兴趣,请向下滚动至相应部分。

考试饼干


最近,由于对违反GDPR进行了调查,因此该工具成为新闻 。 我们决定在考试期间看一下上述学校跟踪系统的第二大竞争对手: ExamCookie 。 这是由20多家丹麦学校使用的商业跟踪系统。 除以下描述外,该网站上没有任何文档:

ExamCookie是一款简单的软件,可以在考试过程中监控学生的计算机活动,以确保遵守规则。 该计划禁止学生使用任何非法形式的协助。

ExamCookie将所有活动保存在计算机上:调整窗口大小时,活动的URL,网络连接,进程,剪贴板和屏幕截图。

该程序的工作原理很简单:通过输入考试,您可以在计算机上运行它,并监视您的活动。 考试完成后,程序将关闭,您可以将其从计算机中删除。

要开始跟踪,您需要使用可在各种教育网站上使用的UNI登录名,或手动输入凭据。 我们没有使用该工具,因此无法说出在哪种情况下使用手动输入。 也许这是针对没有UNI登录名的学生完成的,我们认为这是不可能的。



二进制信息


该程序可以从ExamCookie主页下载。 这是一个x86 .NET应用程序。 作为参考,分析的二进制MD5哈希63AFD8A8EC26C1DC368D8FF8710E337D日期为2019年4月24日63AFD8A8EC26C1DC368D8FF8710E337D签名EXAMCOOKIE A​​PS。 如上一篇文章所示,.NET二进制文件的分析几乎不能称为反向工程,因为易于阅读的IL代码和元数据的组合提供了完美的源代码。

与以前的监视程序不同,此工具的开发人员不仅从调试日志中删除了它,而且对其进行了混淆。 至少他们尝试过:-)

混淆(笑声含泪)


当我们在dnSpy中打开应用程序时,我们很快注意到缺少一个入口点:

 // Token: 0x0600003D RID: 61 RVA: 0x00047BB0 File Offset: 0x00045FB0 [STAThread] [DebuggerHidden] [EditorBrowsable(EditorBrowsableState.Advanced)] [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] internal static void Main(string[] Args) { } 

奇怪的是,通常会采用某种包装器,它会从模块的构造函数更改方法的主体,然后运行到实际的入口点,让我们来看看:

 // Token: 0x06000001 RID: 1 RVA: 0x00058048 File Offset: 0x00055048 static <Module>() { <Module>.\u206B\u202B\u200B\u206F\u206C\u202D\u200D\u200E\u202D\u206B\u206F\u206F\u202C\u202A\u206B\u202E\u202A\u206C\u202A\u206C\u200B\u206A\u202D\u206C\u202C\u206C\u200F\u202C\u206C\u202C\u200C\u206A\u200C\u206C\u200B\u206B\u202B\u206E\u202C\u202B\u202E(); <Module>.\u206C\u200D\u200F\u200E\u200C\u200C\u200F\u200F\u206E\u206A\u206A\u200B\u202C\u206A\u206B\u200D\u206E\u200E\u202D\u206B\u202C\u206C\u202D\u206D\u200C\u200F\u206E\u200F\u206E\u206A\u202B\u206B\u200E\u206B\u202E\u206F\u206A\u202E\u202C\u202A\u202E(); <Module>.\u200B\u202D\u200F\u200F\u202A\u206D\u202C\u206B\u206E\u202A\u206F\u206C\u200D\u200C\u202D\u200F\u202B\u202C\u202B\u206D\u206D\u202D\u206E\u200D\u206D\u206A\u202A\u202C\u200C\u206F\u206B\u206E\u200D\u202E\u206F\u200C\u206B\u200E\u206D\u206A\u202E(); } 

好酷 现在是2019年,人们仍然使用Confuser(Ex)。

我们立即识别出该解压缩代码,并检查了汇编程序头文件:

  [模块:ConfusedBy(“ Confuser.Core 1.1.0 + a36320377a”)] 

目前,我们认为代码实际上会被混淆,因为上述构造函数会解密该方法的主体和资源。 但是,令我们惊讶的是,混淆开发人员决定...不重命名元数据:



这消除了反向工程的所有嗡嗡声。 正如我们在上一篇文章中所说,我想遇到一个真正受到保护的高质量监视工具的问题,对此分析将花费超过五分钟的时间。

无论如何,解压缩受confuser(ex)保护的任何二进制文件都非常简单:使用.NET二进制文件转储程序或<MODULE> .ctor中的break语句断点并进行转储。 这个过程需要30秒钟,而这个打包程序将始终是我的最爱,因为针对调试的防护根本无法实现

我们决定使用MegaDumper:这比手动转储快一点:



转储ExamCookie二进制文件后,应显示以下消息:



现在,您有了一个目录,其中包含所有装入到相应进程中的汇编程序片段,这一次具有解密的方法主体。

谁实施了这种混淆,感谢上帝,至少他加密了这句话:

 else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.SymbolicLink)) { Module1.DebugPrint(<Module>.smethod_5<string>(1582642794u), new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.Tiff)) { Module1.DebugPrint(<Module>.smethod_2<string>(4207351461u), new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.UnicodeText)) { Module1.DebugPrint(<Module>.smethod_5<string>(3536903244u), new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.WaveAudio)) { Module1.DebugPrint(<Module>.smethod_2<string>(2091555364u), new object[0]); } 

是的,良好的旧字符串加密Confuser(Ex),. NET领域中最佳的伪安全性。 Confuser(Ex)经常被黑客入侵,以至于每种机制都可以在Internet上使用去混淆工具,这很好,因此我们不会涉及与.NET相关的任何内容。 在二进制转储上,从CodeCracker运行ConfuserExStringDecryptor:



它将先前的代码段转换为此:

 else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.SymbolicLink)) { Module1.DebugPrint("ContainsData.SymbolicLink", new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.Tiff)) { Module1.DebugPrint("ContainsData.Tiff", new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.UnicodeText)) { Module1.DebugPrint("ContainsData.UnicodeText", new object[0]); } else if (System.Windows.Forms.Clipboard.ContainsData(DataFormats.WaveAudio)) { Module1.DebugPrint("ContainsData.WaveAudio", new object[0]); } 

这就是对应用程序的全部保护,不到一分钟就被破坏了……我们不会在这里发布我们的工具,因为我们没有开发它们,也没有源代码。 但是任何想重复工作的人都可以在Tuts4You上找到它们。 我们不再有tuts4you帐户,因此我们无法链接到镜像。

功能性


令人惊讶的是,没有发现真正的“隐藏功能”。 如网站上所示,以下信息会定期发送到服务器:

  • 进程列表(每5000 ms)
  • 活动的应用程序(每1000毫秒)
  • 剪贴板(每500毫秒)
  • 屏幕截图(每5000毫秒)
  • 网络适​​配器列表(每20,000毫秒)

该应用程序的其余部分非常无聊,因此我们决定跳过整个初始化过程,直接进入负责捕获信息的功能。

转接头


网络适​​配器由.NET NetworkInterface.GetAllNetworkInterfaces()函数组装,与上一篇文章中的完全相同:

 NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface networkInterface in allNetworkInterfaces) { try { // ... // TEXT FORMATTING OMITTED // ... dictionary.Add(networkInterface.Id, stringBuilder.ToString()); stringBuilder.Clear(); } catch (Exception ex) { AdapterThread.OnExceptionEventHandler onExceptionEvent = this.OnExceptionEvent; if (onExceptionEvent != null) { onExceptionEvent(ex); } } } result = dictionary; 

主动申请


这变得越来越有趣。 该实用程序仅注册活动的应用程序,而不是注册所有打开的窗口。 该实现过于夸张,因此我们提供了一个精美的伪代码:

 var whiteList = { "devenv", "ExamCookie.WinClient", "ExamCookie.WinClient.vshost", "wermgr", "ShellExperienceHost" }; // GET WINDOW INFORMATION var foregroundWindow = ApplicationThread.GetForegroundWindow(); ApplicationThread.GetWindowRect(foregroundWindow, ref rect); ApplicationThread.GetWindowThreadProcessId(foregroundWindow, ref processId); var process = Process.GetProcessById(processId); if (process == null) return; // LOG BROWSER URL if (IsBrowser(process)) { var browserUrl = UiAutomation32.GetBrowserUrl(process.Id, process.ProcessName); // SEND BROWSER URL TO SERVER if (ValidBrowserUrl(browserUrl)) { ReportToServer(browserUrl); } } else if (!whiteList.contains(process.ProcessName, StringComparer.OrdinalIgnoreCase)) { ReportToServer(process.MainWindowTitle); } 

很好...人们仍然使用流程名称来区分它们。 他们从未停止过,并且没有想到:“请稍等,您可以根据需要更改进程的名称”,因此我们可以安全地绕过此保护。

如果您阅读有关另一项考试跟踪程序的上一篇文章,则可能会认识到这种针对浏览器的低于标准的实现:

 private bool IsBrowser(System.Diagnostics.Process proc) { bool result; try { string left = proc.ProcessName.ToLower(); if (Operators.CompareString(left, "iexplore", false) != 0 && Operators.CompareString(left, "chrome", false) != 0 && Operators.CompareString(left, "firefox", false) != 0 && Operators.CompareString(left, "opera", false) != 0 && Operators.CompareString(left, "cliqz", false) != 0) { if (Operators.CompareString(left, "applicationframehost", false) != 0) { result = false; } else { result = proc.MainWindowTitle.Containing("Microsoft Edge"); } } else { result = true; } } catch (Exception ex) { result = false; } return result; } 

 private string GetBrowserName(string name) { if (Operators.CompareString(name.ToLower(), "iexplore", false) == 0) { return "IE-Explorer"; } else if (Operators.CompareString(name.ToLower(), "chrome", false) == 0) { return "Chrome"; } else if (Operators.CompareString(name.ToLower(), "firefox", false) == 0) { return "Firefox"; } else if (Operators.CompareString(name.ToLower(), "opera", false) == 0) { return "Opera"; } else if (Operators.CompareString(name.ToLower(), "cliqz", false) == 0) { return "Cliqz"; } else if (Operators.CompareString(name.ToLower(), "applicationframehost", false) == 0) { return "Microsoft Edge"; } return ""; } 

还有蛋糕上的樱桃:

 private static string GetBrowserUrlById(object processId, string name) { // ... automationElement.GetCurrentPropertyValue(/*...*/); return url; } 

这实际上与上一篇文章中的实现相同。 很难理解开发人员如何仍然没有意识到它有多糟糕。 任何人都可以在浏览器中编辑URL,这甚至不值得演示。

虚拟机发现


与网站所说的相反,从虚拟机开始设置一个标志。 实现很有趣。

 File.WriteAllBytes("ecvmd.exe", Resources.VmDetect); using (Process process = new Process()) { process.StartInfo = new ProcessStartInfo("ecvmd.exe", "-d") { CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true }; process.Start(); try { using (StreamReader standardOutput = process.StandardOutput) { result = standardOutput.ReadToEnd().Replace("\r\n", ""); } } catch (Exception ex3) { result = "-5"; } } 

好吧,由于某种原因,他们将外部二进制文件写入磁盘并执行它,然后完全依赖I / O结果。 这确实经常发生,但是将如此重要的工作转移到另一个不受保护的过程中是这样的。 让我们看看我们正在处理哪个文件:



那么现在我们使用C ++吗? 好吧,互操作性实际上不一定是坏的。 这可能意味着我们现在确实必须进行逆向工程(!!)。 让我们看一下IDA:

 int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // ecx BOOL v4; // ebx int v5; // ebx int *v6; // eax int detect; // eax bool vbox_key_exists; // bl char vpcext; // bh char vmware_port; // al char *vmware_port_exists; // ecx char *vbox_detected; // edi char *vpcext_exists; // esi int v14; // eax int v15; // eax int v16; // eax int v17; // eax int v18; // eax int v20; // [esp+0h] [ebp-18h] HKEY result; // [esp+Ch] [ebp-Ch] HKEY phkResult; // [esp+10h] [ebp-8h] if ( argc != 2 ) goto LABEL_20; v3 = strcmp(argv[1], "-d"); if ( v3 ) v3 = -(v3 < 0) | 1; if ( !v3 ) { v4 = (unsigned __int8)vm_detect::vmware_port() != 0; result = 0; v5 = (vm_detect::vpcext() != 0 ? 2 : 0) + v4; RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\ACPI\\DSDT\\VBOX__", 0, 0x20019u, &result); v6 = sub_402340(); LABEL_16: sub_404BC0((int)v6, v20); return 0; } detect = strcmp(argv[1], "-s"); if ( detect ) detect = -(detect < 0) | 1; if ( !detect ) { LABEL_20: phkResult = 0; vbox_key_exists = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\ACPI\\DSDT\\VBOX__", 0, 0x20019u, &phkResult) == 0; vpcext = vm_detect::vpcext(); vmware_port = vm_detect::vmware_port(); vmware_port_exists = "1"; vbox_detected = "1"; if ( !vbox_key_exists ) vbox_detected = "0"; vpcext_exists = "1"; if ( !vpcext ) vpcext_exists = "0"; if ( !vmware_port ) vmware_port_exists = "0"; result = (HKEY)vmware_port_exists; v14 = std::print((int)&dword_433310, "VMW="); v15 = std::print(v14, (const char *)result); v16 = std::print(v15, ",VPC="); v17 = std::print(v16, vpcext_exists); v18 = std::print(v17, ",VIB="); v6 = (int *)std::print(v18, vbox_detected); goto LABEL_16; } return 0; } 

这验证了VMWare I / O端口“ VX”的存在:

 int __fastcall vm_detect::vmware_port() { int result; // eax result = __indword('VX'); LOBYTE(result) = 0; return result; } 

接下来,检查虚拟pc扩展指令的执行,该指令仅在虚拟化环境中启动时才有效,如果在处理不正确时不会导致机器崩溃;):

 char vm_detect::vpcext() { char result; // al result = 1; __asm { vpcext 7, 0Bh } return result; } 

...没有真正的逆向工程,只需30秒即可重命名两个功能:(

该程序仅读取注册表项并运行两次与其他程序相比看起来很奇怪的虚拟机监控程序检查。 我想知道他们在哪里复制的? 哦,看,一篇标题为“发现虚拟(sic)机器的方法”的文章解释了这些方法:)。 在任何情况下,都可以通过编辑.vmx文件或使用您选择的任何虚拟机管理程序的增强版本来规避这些检测向量。

资料保护


如前所述,针对不符合GDPR的行为正在进行调查,其网站指出:

数据被加密并发送到安全的Microsoft Azure服务器,该服务器只能使用正确的凭据进行访问。 考试后,数据最多可以存储三个月。

我们不太确定它们如何确定服务器的“安全性”,因为凭据在应用程序中进行了硬编码,并以完整的明文形式存储在元数据资源中:

 端点:https://examcookiewinapidk.azurewebsites.net
用户名:VfUtTaNUEQ
密码:AwWE9PHjVc 

我们没有检查服务器的内容(这是非法的),但是我们可以假定那里提供了完全访问权限。 由于帐户是在应用程序中进行硬编码的,因此学生数据容器之间没有隔离。

法律免责声明:我们保留发布API凭证的权利,因为它们存储在公共二进制文件中,因此不会被非法获取。 但是,出于恶意目的使用它们显然违反了法律,因此,我们强烈建议读者不要以任何方式使用上述凭证,并且对任何可能的行为不承担任何责任。

绕过


由于此应用程序让人想起Digital Exam Monitor,因此我们刚刚更新了ayyxam代码以支持ExamCookie。

工艺清单


.NET进程接口使用ntdll!NtQuerySystemInformation系统调用在内部缓存进程数据。 要从中隐藏进程需要一些工作,因为有关该进程的信息在很多地方都已指明。 幸运的是,.NET只检索一种特定类型的信息,因此您不必使用所有的Latebros方法。

代码绕过活动过程的验证。

 NTSTATUS WINAPI ayyxam::hooks::nt_query_system_information( SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information, ULONG system_information_length, PULONG return_length) { // DONT HANDLE OTHER CLASSES if (system_information_class != SystemProcessInformation) return ayyxam::hooks::original_nt_query_system_information( system_information_class, system_information, system_information_length, return_length); // HIDE PROCESSES const auto value = ayyxam::hooks::original_nt_query_system_information( system_information_class, system_information, system_information_length, return_length); // DONT HANDLE UNSUCCESSFUL CALLS if (!NT_SUCCESS(value)) return value; // DEFINE STRUCTURE FOR LIST struct SYSTEM_PROCESS_INFO { ULONG NextEntryOffset; ULONG NumberOfThreads; LARGE_INTEGER Reserved[3]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; ULONG BasePriority; HANDLE ProcessId; HANDLE InheritedFromProcessId; }; // HELPER FUNCTION: GET NEXT ENTRY IN LINKED LIST auto get_next_entry = [](SYSTEM_PROCESS_INFO* entry) { return reinterpret_cast<SYSTEM_PROCESS_INFO*>( reinterpret_cast<std::uintptr_t>(entry) + entry->NextEntryOffset); }; // ITERATE AND HIDE PROCESS auto entry = reinterpret_cast<SYSTEM_PROCESS_INFO*>(system_information); SYSTEM_PROCESS_INFO* previous_entry = nullptr; for (; entry->NextEntryOffset > 0x00; entry = get_next_entry(entry)) { constexpr auto protected_id = 7488; if (entry->ProcessId == reinterpret_cast<HANDLE>(protected_id) && previous_entry != nullptr) { // SKIP ENTRY previous_entry->NextEntryOffset += entry->NextEntryOffset; } // SAVE PREVIOUS ENTRY FOR SKIPPING previous_entry = entry; } return value; } 

缓冲液


ole32.dll!OleGetClipboard非常容易受到钩子的影响,它负责.NET中缓冲区的内部实现。 您无需花费很多时间来分析内部结构,只需返回S_OK ,其余的工作便由.NET错误处理:

 std::int32_t __stdcall ayyxam::hooks::get_clipboard(void* data_object[[maybe_unused]]) { // LOL return S_OK; } 

这将在ExamCookie监视工具中隐藏整个缓冲区,而不会影响程序的功能。

屏幕截图


与往常一样,人们采用所需功能的现成.NET实现。 为了解决此功能,我们甚至不必更改先前代码中的任何内容。 屏幕截图由Graphics.CopyFromScreen .NET函数控制。 实际上,它是用于传输位块的包装器,该包装器称为gdi32!BitBlt 。 就像在视频游戏中对抗使用截图的反作弊系统一样,我们可以在捕获截图之前使用BitBlt钩子并隐藏所有不需要的信息。


开设网站


抓取程序URL完全从先前的程序复制而来,因此我们可以再次使用我们的代码来绕过保护。 在上一篇文章中,我们记录了AutomationElement结构,由此启动了以下挂钩:

 std::int32_t __stdcall ayyxam::hooks::get_property_value(void* handle, std::int32_t property_id, void* value) { constexpr auto value_value_id = 0x755D; if (property_id != value_value_id) return ayyxam::hooks::original_get_property_value(handle, property_id, value); auto result = ayyxam::hooks::original_get_property_value(handle, property_id, value); if (result != S_OK) // SUCCESS? return result; // VALUE URL IS STORED AT 0x08 FROM VALUE STRUCTURE class value_structure { public: char pad_0000[8]; //0x0000 wchar_t* value; //0x0008 }; auto value_object = reinterpret_cast<value_structure*>(value); // ZERO OUT OLD URL std::memset(value_object->value, 0x00, std::wcslen(value_object->value) * 2); // CHANGE TO GOOGLE.COM constexpr wchar_t spoofed_url[] = L"https://google.com"; std::memcpy(value_object->value, spoofed_url, sizeof(spoofed_url)); return result; } 

虚拟机发现


可以通过两种方式来规避对虚拟机的延迟检测:1)刷新到磁盘的补丁程序; 或2)将流程创建过程重定向到虚拟应用程序。 后者似乎更简单:)。 因此,在内部Process.Start()调用CreateProcess ,因此只需将其挂钩并将其重定向到任何显示字符'0'的虚拟应用程序即可。

 BOOL WINAPI ayyxam::hooks::create_process( LPCWSTR application_name, LPWSTR command_line, LPSECURITY_ATTRIBUTES process_attributes, LPSECURITY_ATTRIBUTES thread_attributes, BOOL inherit_handles, DWORD creation_flags, LPVOID environment, LPCWSTR current_directory, LPSTARTUPINFOW startup_information, LPPROCESS_INFORMATION process_information ) { // REDIRECT PATH OF VMDETECT TO DUMMY APPLICATION constexpr auto vm_detect = L"ecvmd.exe"; if (std::wcsstr(application_name, vm_detect)) { application_name = L"dummy.exe"; } return ayyxam::hooks::original_create_process( application_name, command_line, process_attributes, thread_attributes, inherit_handles, creation_flags, environment, current_directory, startup_information, process_information); } 

资料下载


整个项目可在Github存储库中找到 。 该程序通过将x86二进制文件注入相应的进程来工作。

Source: https://habr.com/ru/post/zh-CN453536/


All Articles