C/C++ Encrypted Functions

Die antispy™ C/C++ Function Encryption ist ein Zwei-Faktor Mechanismus um Funktionen vor Reverse-Engineering zu schützen. Funktionen sind in einer Binary mittels Pattern Matchings, Heuristiken, Signaturen, Strings im Normalfall schnell identifiziert und wiedergefunden.

In unserer Funktionsverschlüsselung wird der Code markiert und in einem Post-Processing verschlüsselt. Zur Laufzeit der Anwendung wird der Code kurzfristig entschlüsselt und danach wieder verschlüsselt. Kombiniert mit Anti-Debugging Techniken, Call-Corruption und String sowie Value Verschlüsselungen ist es beinahe unmöglich sensiblen Code mittels statischer oder dynamischer Analyse wiederherzustellen.

Implementierung

Im Gegensatz zu unseren anderen Features benötigt dieses Feature ein bisschen mehr Know-How und Erfahrung. Aktuell wird diese Technik nur unter Windows unterstützt.

Zunächst muss in Ihrer CMakeLists.txt folgendes ergänzt werden.

POST_PROCESS_PE(<executeable_name>)

Anschließend wird der zu verschlüsselnde Code im Quelltext markiert.

#include <antispy/encrypted_function.hpp>

ANTI_SPY_CRYPT_FUNC_START(void, encrypted_procedure)
{
    std::cout << "encrypted_procedure called." << std::endl;
}
ANTI_SPY_CRYPT_FUNC_END(encrypted_procedure)

ANTI_SPY_CRYPT_FUNC_START(int, encrypted_function, int a, int b)
{
    std::cout << "encrypted_function returning " << a + b << std::endl;
    return a + b;
}
ANTI_SPY_CRYPT_FUNC_END(encrypted_function)

int main(int argc, const char **argv)
{
    encrypted_procedure();
    const int res = encrypted_function(2, 3);
    std::cout << "encrypted_function returned " << res << std::endl;
    return 0;
}

Im Auge des Reverse Engineerers

Vor der Verschlüsselung sieht der Code im Dissassembler folgendermaßen aus.

push    offset aEncrypted_proc ; "encrypted_procedure called."
push    ds:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::basic_ostream<char,std::char_traits<char>> std::cout
call    sub_401EA0
add     esp, 8
mov     ecx, eax
push    offset sub_4039E0
call    ds:??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &))
retn

Der eigentliche Sinn dieser Funktion ist für einen erfahrenen Reverse-Engineerer innerhalb von Sekunden erkennbar.

Nach der Verschlüsselung sieht die Funktion allerdings so aus.

out     0DEh, al
mov     ebx, 2F0269Ah
shr     dword ptr [ebp+1Ah], cl
jz      short sub_401000
db      2Eh
lahf
aad     6Dh
pop     ds
add     esi, [eax-4Dh]
cli
bound   ebx, [edi]
or      esi, ecx
sbb     eax, 8658ABE0h
retf

Dieser Code hat keinen Bezug mehr zum Original und die erkannten Instructions sind zufällig. Das heißt, dass ein Reverse-Engineerer bei der statischen Analyse erstmal völlig im Dunkeln tappt und zu diesem Zeitpunkt nicht direkt darauf schließen kann ob dieser Code virtualisiert oder verschlüsselt ist.