Rastreamento de exames: ExamCookie

Aprendi que o governo dinamarquês não apenas suspendeu o programa Digital Exam Monitor, que analisamos e contornamos completamente no artigo anterior , mas talvez desligue completamente esse sistema uma semana depois de os informarmos sobre o método de hacking. Não quero pensar que foi apenas por nossa causa que o governo dinamarquês abandonou a idéia de monitorar os exames, mas nosso trabalho foi claramente notado.

Neste artigo, descreveremos os detalhes técnicos de como funciona outra ferramenta de rastreamento de alunos: ExamCookie. Se você estiver interessado apenas em ignorar o sistema, role para baixo até a seção apropriada.

ExamCookie


Recentemente, essa ferramenta chegou às notícias devido a uma investigação sobre a violação do RGPD. Decidimos dar uma olhada no segundo maior concorrente do sistema de rastreamento escolar acima mencionado durante os exames: ExamCookie . Este é um sistema de rastreamento comercial usado por mais de 20 escolas dinamarquesas. Não há documentação no site além da descrição a seguir:

O ExamCookie é um software simples que monitora a atividade do computador do aluno durante o exame para garantir que as regras sejam seguidas. O programa proíbe os alunos de usar qualquer forma ilegal de assistência.

O ExamCookie salva toda a atividade no computador: URLs ativos, conexões de rede, processos, área de transferência e capturas de tela ao redimensionar a janela.

O programa funciona de maneira simples: ao entrar no exame, você o executa no seu computador e monitora sua atividade. Quando o exame é concluído, o programa é fechado e você pode removê-lo do computador.

Para iniciar o rastreamento, você precisa usar seu login UNI, que funciona em vários sites educacionais, ou inserir manualmente as credenciais. Como não usamos a ferramenta, não podemos dizer em que casos a entrada manual é usada. Talvez isso seja feito para estudantes que não possuem um login UNI, o que não consideramos possível.



Informações binárias


O programa pode ser baixado na página inicial do ExamCookie. É um aplicativo .NET x86. Para referência, o hash MD5 binário analisado 63AFD8A8EC26C1DC368D8FF8710E337D assinatura EXAMCOOKIE APS de 24 de abril de 2019. Como o último artigo mostrou, a análise do binário .NET dificilmente pode ser chamada de engenharia reversa, porque a combinação de código IL e metadados de fácil leitura fornece o código fonte perfeito.

Diferentemente do programa de monitoramento anterior, os desenvolvedores dessa ferramenta não apenas a excluíram do log de depuração, como também a ofuscaram. Pelo menos eles tentaram :-)

Ofuscação (risada às lágrimas)


Quando abrimos o aplicativo no dnSpy, notamos rapidamente um ponto de entrada ausente:

 // 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) { } 

Estranho, geralmente é assumido algum tipo de wrapper, ele altera os corpos dos métodos do construtor do módulo, que é executado no ponto de entrada real, vamos ver:

 // 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(); } 

Legal. É 2019, e as pessoas ainda usam o Confuser (Ex).

Reconhecemos instantaneamente esse código de descompressão e verificamos os cabeçalhos do assembler:

  [módulo: ConfusedBy ("Confuser.Core 1.1.0 + a36320377a")] 

No momento, pensamos que o código seria realmente ofuscado, pois o construtor acima descriptografa os corpos e os recursos do método. Mas, para nossa surpresa, o desenvolvedor da ofuscação decidiu ... não renomear os metadados:



Isso mata todo o burburinho da engenharia reversa. Como dissemos em um artigo anterior , gostaria de encontrar o problema real de uma ferramenta de vigilância de alta qualidade e protegida, cuja análise levará mais de cinco minutos.

De qualquer forma, descompactar qualquer binário protegido por confuser (ex) é muito simples: use o dumper de binários .NET ou o ponto de interrupção da instrução break em <MODULE> .ctor e faça o dump. O processo leva 30 segundos e esse empacotador sempre será o meu favorito, porque a proteção contra depuração nunca funciona .

Decidimos usar o MegaDumper: isso é um pouco mais rápido que o dumping manual:



Após descarregar o binário ExamCookie, a seguinte mensagem deve aparecer:



Agora você tem um diretório com todos os fragmentos do assembler que são carregados no processo correspondente, desta vez com corpos de método descriptografados.

Quem implementou essa ofuscação, graças a Deus, pelo menos ele criptografou as linhas:

 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]); } 

Sim, a boa e antiga criptografia de cadeia de caracteres Confuser (Ex), a melhor pseudo-segurança do mundo do .NET. É bom que o Confuser (Ex) tenha sido invadido com tanta frequência que as ferramentas de desofuscação estejam disponíveis na Internet para cada mecanismo, para não tocarmos em nada relacionado ao .NET. Execute o ConfuserExStringDecryptor do CodeCracker no despejo binário:



Ele converte o snippet anterior para isso:

 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]); } 

Essa é toda a proteção do aplicativo, quebrada em menos de um minuto ... Não publicaremos nossas ferramentas aqui, porque não as desenvolvemos e não temos código-fonte. Mas quem quiser repetir o trabalho pode encontrá-los no Tuts4You . Como não temos mais uma conta tuts4you, não podemos vincular aos espelhos.

Funcionalidade


Surpreendentemente, nenhuma "funcionalidade oculta" real foi encontrada. Conforme indicado no site, as seguintes informações são enviadas periodicamente ao servidor:

  • Lista de processos (a cada 5000 ms)
  • Aplicativo ativo (a cada 1000 ms)
  • Área de transferência (a cada 500 ms)
  • Captura de tela (a cada 5000 ms)
  • Lista de adaptadores de rede (a cada 20.000 ms)

O restante do aplicativo é muito chato, por isso decidimos pular todo o procedimento de inicialização e ir diretamente para as funções responsáveis ​​pela captura de informações.

Adaptador


Os adaptadores de rede são montados pela função .NET NetworkInterface.GetAllNetworkInterfaces() , exatamente como no artigo anterior :

 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; 

Aplicativo ativo


Isso está ficando interessante. Em vez de registrar todas as janelas abertas, o utilitário controla apenas o aplicativo ativo. A implementação é exagerada; portanto, apresentamos um pseudocódigo bonito:

 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); } 

Ótimo ... as pessoas ainda usam nomes de processos para diferenciá-los. Eles nunca param e não pensam: "Espere um minuto, você pode alterar os nomes dos processos como quiser", para que possamos contornar essa proteção com segurança.

Se você leu um artigo anterior sobre outro programa de rastreamento de exames, provavelmente reconhecerá esta implementação subpara os navegadores:

 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 ""; } 

E a cereja no bolo:

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

Esta é literalmente a mesma implementação que no artigo anterior. É difícil entender como os desenvolvedores ainda não perceberam o quão ruim é. Qualquer um pode editar o URL no navegador, isso nem vale a pena demonstrar.

Descoberta de máquina virtual


Ao contrário do que o site diz, iniciar em uma máquina virtual define um sinalizador. A implementação é ... interessante.

 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"; } } 

Bem, por algum motivo, eles escrevem um binário externo no disco e o executam, e depois confiam completamente nos resultados de E / S. Isso realmente acontece com bastante frequência, mas a transferência de um trabalho tão importante para outro processo desprotegido é mais ou menos. Vamos ver com qual arquivo estamos lidando:



Então agora usamos C ++? Bem, a interoperabilidade não é necessariamente ruim. E isso pode significar que agora precisamos realmente trabalhar em engenharia reversa (!!). Vejamos a AID:

 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; } 

Isso verifica a presença da porta de E / S do VMWare 'VX':

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

A seguir, é verificada a execução da instrução de extensão do pc virtual , que deve funcionar apenas quando iniciada em um ambiente virtualizado, se não levar a uma falha da máquina se for processada incorretamente;):

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

... nenhuma engenharia reversa real, apenas 30 segundos para renomear duas funções :(

Este programa simplesmente lê a chave do registro e executa duas verificações de hipervisor que parecem estranhas em comparação com o outro programa. Gostaria de saber onde eles copiaram? Ah, veja, um artigo intitulado "Métodos para descobrir máquinas virtuais (sic)" que explica esses métodos :). De qualquer forma, esses vetores de detecção podem ser contornados editando o arquivo .vmx ou usando uma versão aprimorada de qualquer hipervisor de sua escolha.

Protecção de dados


Como mencionado anteriormente, está em andamento uma investigação por não conformidade com o GDPR, e seu site afirma:

Os dados são criptografados e enviados para um servidor seguro do Microsoft Azure, que pode ser acessado apenas com as credenciais corretas. Após o exame, os dados são armazenados por até três meses.

Não sabemos ao certo como eles determinam a "segurança" do servidor, pois as credenciais são codificadas no aplicativo e armazenadas em texto completamente não criptografado nos recursos de metadados:

  Ponto de extremidade: https://examcookiewinapidk.azurewebsites.net
 Nome de usuário: VfUtTaNUEQ
 Senha: AwWE9PHjVc 

Não examinamos o conteúdo do servidor (isso é ilegal), mas podemos assumir que o acesso total é fornecido lá. Como a conta está codificada no aplicativo, não há isolamento entre os contêineres de dados do aluno.

Isenção de responsabilidade legal: nos reservamos o direito de publicar credenciais da API porque elas são armazenadas em um arquivo binário público e, portanto, não são obtidas ilegalmente. No entanto, usá-los com intenção maliciosa claramente viola a lei; portanto, recomendamos enfaticamente que os leitores não usem as credenciais mencionadas de forma alguma e não sejam responsáveis ​​por possíveis ações.

Bypass


Como esse aplicativo lembra muito o Digital Exam Monitor, atualizamos o código do ayyxam para dar suporte ao ExamCookie.

Lista de processos


A interface do processo .NET armazena em cache internamente os dados do processo usando a chamada de sistema ntdll!NtQuerySystemInformation . Esconder processos exige algum trabalho, porque as informações sobre o processo são indicadas em muitos lugares. Felizmente, o .NET recupera apenas um tipo específico de informação; portanto, você não precisa usar todos os métodos de latebros .

Código para ignorar a validação de processos ativos.

 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; } 

Buffer


ole32.dll!OleGetClipboard , que é muito suscetível a ganchos, é responsável pela implementação interna de buffers no .NET. Em vez de gastar muito tempo analisando estruturas internas, você pode simplesmente retornar S_OK , e o tratamento de erros do .NET fará o resto:

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

Isso ocultará todo o buffer da ferramenta de vigilância ExamCookie sem atrapalhar a funcionalidade do programa.

Screenshots


Como sempre, as pessoas adotam uma implementação .NET pronta para a função desejada. Para contornar essa função, nem precisamos alterar nada no código anterior. As capturas de tela são controladas pela função .NET Graphics.CopyFromScreen . É essencialmente um invólucro para a transmissão de blocos de bits, que chama gdi32!BitBlt . Como nos videogames para combater sistemas anti-fraude que capturam capturas de tela, podemos usar o gancho BitBlt e ocultar qualquer informação indesejada antes de capturar uma captura de tela.


Abertura de sites


O URL do grabber é completamente copiado do programa anterior, para que possamos reutilizar nosso código para contornar a proteção. No último artigo, documentamos a estrutura AutomationElement, que executa este gancho:

 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; } 

Descoberta de máquina virtual


A detecção preguiçosa de uma máquina virtual pode ser contornada de duas maneiras: 1) um patch de um programa que é liberado para o disco; ou 2) um redirecionamento do processo de criação de processo para um aplicativo fictício. Este último parece claramente mais simples :). Então, internamente Process.Start() chama CreateProcess , então apenas conecte e redirecione para qualquer aplicativo fictício que imprima o caractere '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); } 

Baixar


Todo o projeto está disponível no repositório do Github . O programa funciona injetando o binário x86 no processo correspondente.

Source: https://habr.com/ru/post/pt453536/


All Articles