Software protection system against cracking

Matrix

Software is a valuable asset, the result of hundreds of hours of design, programming and testing, which is why protecting software against cracking and, consequently, against illegal use is so important nowadays.

The aim of this work is to present various methods of cracking software, using all available techniques and modern tools, such as debuggers, disassemblers and decompilers, etc.

The final goal of the work is to create a sample security software, using the described methods to protect applications from being broken.

In developing of the application Microsoft Visual Studio 2003 version was used. Parasoft C++ Test was used for code quality analysis.

Chapter 1. Software cracking

1.1. Historical background

Software cracking has a long history, inseparably linked to the development of the first commercial applications for platforms such as the Apple II, Atari 800 and Commodore 64, where software developers, mainly gamers, struggled with the illegal use of their products, creating ever better security systems.

The first security systems mainly used methods that made it difficult to copy physical data media, such as tapes and floppy disks, by recording information on media that was impossible or difficult for an ordinary user to copy.

As technology developed and PCs became more popular, methods based on hardware security started to evolve towards purely software-based solutions using the latest encryption methods, detection of software cracking tools and innovative licensing systems, mostly based on public key infrastructures.

Hardware solutions are also used, but to a lesser extent, dongles based on LPT and USB interfaces may be an example here, but due to high implementation costs they are mainly used as an element of security of expensive and specialized applications.

Klucz sprzętowy HASP na interfejsie LPT
HASP dongle on LPT interface

1.2. Software cracking techniques

In this chapter we will discuss how to crack the software and remove security features from the application.

1.2.1. Software analysis

Analysis of the software, including its security, is the basis for any further work. The analysis usually uses disassemblers, i.e. programs that disassemble the application's binary code into the processor's assembler code for which the application was compiled.

In the case of Windows applications, we are dealing with code for processors in x86 architecture (both 32 and 64-bit). Analyzing software as a whole process is called reverse engineering.

The most popular disassembly applications today are the IDA Pro package, which is a combination of disassembly and debugger. IDA allows you to disassemble applications written not only for Windows (PE file format), but also for Linux (ELF file format).

The IDA disassembler and decompiler
The IDA disassembler and decompiler

Thanks to the built-in signature system, IDA can also automatically recognize popular libraries used in applications such as RTL, MFC, VCL. This method allows to identify byte signatures the names of most functions from these libraries, e.g. memcmp(), strlen(), thanks to which the analysis of application code is facilitated.

IDA is very powerful software and has support for plug-ins extending its capabilities, as well as built-in its own scripting language, resembling the C programming language in syntax.

In total, IDA is able to disassemble code compiled for more than 50 different processors with support for basic and extended instruction sets, including

  • Intel
  • AMD
  • ARM
  • Fujitsu FR
  • Hitachi HD
  • Motorola
  • Rockwell
  • Z80

Below is a sample code, written in C, which will be disassembled after compilation.

#include <stdio.h>

int main(int argc, char *argv[])
{
   // display text on the console
   printf("Hello World!");

   // exit with the 0 return code
   return 0;
}

The code was compiled with the Windows LCC compiler to an executable file. After disassembling the binary file in the IDA software, you can see the effect of compiling the C language source to x86 assembly code. Below is an excerpt showing the compiled main function:

.text:0040129C main proc near ; CODE XREF: start+66p
.text:0040129C 55                       push    ebp
.text:0040129D 89 E5                    mov     ebp, esp
.text:0040129F 68 94 90 40 00           push    offset aHelloWorld
.text:004012A4 E8 34 5B 00 00           call    printf
.text:004012A9 59                       pop     ecx
.text:004012AA 31 C0                    xor     eax, eax
.text:004012AC 5D                       pop     ebp
.text:004012AD C3                       retn
.text:004012AD main endp

The code that has been disassembled is commonly called deadlisting. You may notice that the disassembled fragment contains information such as the section name of the executable file (the executable file contains various sections that store data such as code, static data, resources, etc.) in which the selected fragment of code is located, in this case, the section name is .text, then next to the subsequent assembler instructions, their memory address and the instruction code stored in hexadecimal (as it is stored in the compiled file) are displayed.

As you can see from the above code chunk, a good knowledge of the syntax and instructions of the assembler and basic knowledge of the structure of the executable files are required for software analysis.

1.2.2. Protection analysis

Proprietary software protection systems usually require manual analysis in a disassembler, while the popularity of ready-made protection systems has contributed to the creation of automatic tools that can recognize the applied protection in the selected application.

Such tools are identifiers and their operation consists in scanning application files for known binary signatures of the most popular protection systems binary patterns.

This operation can be compared to antivirus scanners, which also use scanning for known binary signatures of computer viruses.

PEiD identifier
PEiD identifier

The PEiD identifier shown above is the one that detected that the program file was compressed by a popular ASPack application (an executable compression tool).

1.2.3. Software debugging

Analysis of software by disassembly does not always work, because only static code is visible, and often encrypted, so next to disassembly the second most common technique used to get more detailed information is debugging.

Application debugging consists of running the application code under the supervision of special software (debugger) and tracking step by step the performed operations. This technique is usually used to find hidden bugs in applications.

Nowadays, practically every integrated development environment (IDE) has a built-in debugger, allowing to find troublesome code fragments.

It should be noted here, that the debugging in such conditions takes place with simultaneous access to the source code of the application (with the optional possibility of previewing generated assembler code during the compilation process) so that debugging the application in such conditions we see code written in a high-level language (e.g. C++, C#, Delphi).

Visual Studio Debugger
Visual Studio Debugger

The above-mentioned ability to debug the code of the application being executed at the assembler level is, on the other hand, the most commonly used method when analyzing the code for the purpose of its cracking, because in most cases you do not have access to the source code.

In the past, the most popular tool used for code debugging was the famous SoftICE system debugger from Compuware, which allowed to debug both the code of system drivers, but also ordinary applications running in user mode.

The SoftICE debugger executing the !DUMP memory dump command
The SoftICE debugger executing the !DUMP memory dump command

Due to its huge capabilities, SoftICE was the main tool used to crack applications. However, the development of subsequent versions of Windows, especially Windows XP and Windows Vista, caused more and more problems with running the debugger itself, which had problems with working under new operating systems, but also with supporting new graphics cards (SoftICE was a system debugger and used direct access to the graphics card). As a result of these problems, Compuware stopped releasing new versions of the debugger.

With the collapse of the SoftICE debugger, other projects started to appear, which were supposed to take its place on the market, but the only one that made a name for itself was OllyDbg from Oleh Yuschuk.

OllyDbg has evolved from a simple tool into an advanced software tool that today has enormous capabilities and a whole range of plug-ins that make OllyDbg the most advanced debugger on the market ever.

OllyDbg debugger window
OllyDbg debugger window

The OllyDbg Debugger is currently used by ordinary programmers, people who analyze viruses in antivirus companies, people who search for software vulnerabilities (using a modified version of OllyDbg called Immunity Debugger, expanded to support Python scripts) and people who crack software.

Basics of using Immunity Debugger

1.2.4. Monitoring system changes

The changes made by applications in the operating system are often a guideline for understanding how protection systems work.

Most often, changes and access to the information contained in the file system or Windows registry allow you to discover where, for example, registration keys and other sensitive data are saved.

To track changes in the file system, currently the most popular application is Microsoft's FileMon (currently ProcMon), which allows you to log all changes and references to the entire file system through all running applications (or selected).

FileMon file system monitor
FileMon file system monitor

To track changes and access keys in the Windows registry, most often another Microsoft program is used, namely , which allows to monitor all or selected applications.

RegMon System Registry Monitor
RegMon System Registry Monitor

Programs such as FileMon and RegMon only allow you to track changes made to the file system and registry based on monitoring the entire system. For this purpose, they use file system or registry hooking driver on the kernel-mode level.

There is also a separate group of applications called API spies, which allow you to monitor the selected application and all functions that the monitored application uses.

KaKeeware API Functions Application Monitor
KaKeeware API Functions Application Monitor

Applications of this type work mostly by intercepting API calls to the operating system at the level of monitored software, through techniques such as API hooking and code injection directly into the application.

1.2.5. Modification of application files

The simplest way to crack the software and get an illegal copy of the program is to modify the application binary files. To manually modify binary files, hex-editors are used, i.e. programs that allow you to view (displaying the contents of the files byte byte byte in hexadecimal system) and edit their contents.

HIEW hex editor
HIEW hex editor

When cracking an application, modifications usually do not cover many areas of the code, but only the critical fragments previously recognized during the analysis, for example, responsible for checking the correctness of the entered serial number used for software registration.

1.2.6. Modification of application memory

Sometimes it happens that it is difficult or even impossible to modify a file on the disk, e.g. because of the applied protection of the application in the form of file checksums, where the change of any data in the application files would be automatically detected by the software and most often in such cases the application is closed after a warning message is displayed.

This type of protection, however, includes methods using the fact of access to the application memory. This technique consists in detecting a running application, e.g. by the title of its window, thanks to which it is possible to access the memory of its process, and then overwriting the application code in the memory from an external program.

This method is most often used for temporary modification of the code of the running application in order to bypass the verification procedures, e.g. when entering a serial number, where after entering a correct number, the application saves the so-called registration tag (a flag informing the application about a successful registration) to the Windows registry.

Below is an example program in C language, which after detecting the application by its window name overwrites one byte in the application memory:

#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  HWND hWindowHandle = NULL;
  DWORD dwProcessId = 0;
  HANDLE hProcessHandle = NULL;
  BYTE cBuffer[3] = { 0x90 };

  // find the application window with the specified title
  hWindowHandle = FindWindow(NULL, "Aplikacja v1.0");

  // check if this application is running
  if (hWindowHandle != NULL)
  {
    // read the process ID from the window handle
    GetWindowThreadProcessId(hWindowHandle, &dwProcessId);

    // open application process handle
    hProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, \
                                 1, dwProcessId);

    // store a single byte from the cBuffer[] array
   // at the 0x401000 application memory offset
    WriteProcessMemory(hProcessHandle, (LPVOID)0x401000, \
                       &cBuffer[0], sizeof(cBuffer), NULL);
  }
  else
  {
    printf("Window not found!");
  }

  // terminate the application with error code 0
  return 0;

}

In this case the changes are made in the memory of the running application, but the described method is not effective if the changes in the application are to be visible every time you launch it. For this purpose another technique is used, namely to create a loader application.

Its task is to run the proper application in suspended mode, then the changes are made to the application memory, and then the running is continued, so the changes will be visible in the application memory from the very beginning.

The example program presents the described technique:

#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  STARTUPINFO lpSi = { 0 };
  PROCESS_INFORMATION lpPi = { 0 };
  BYTE cBuffer[1] = { 0x90 };

  // initialize the STARTUPINFO structure
  GetStartupInfo(&lpSi);

  // run the process in suspend mode
  CreateProcess("C:\\aplikacja.exe", NULL, NULL, NULL, \
                TRUE, CREATE_SUSPENDED, NULL, NULL, \
                &lpSi, &lpPi);

  // store a single byte from the cBuffer[] array
  // at the 0x401000 application memory offset
  WriteProcessMemory(lpPi.hProcess, (LPVOID)0x401000, \
                     &cBuffer[0], sizeof(cBuffer), NULL);

  // resume the suspended process
  ResumeThread(lpPi.hThread);

  // terminate the application with error code 0
  return 0;

}

To work effectively, this method requires that instead of the application file, the loader program file is run, as this is the only guarantee of making appropriate changes to the application memory.

However, sometimes there are situations, when more advanced methods are required, for example, when we want to change something in application memory, but under some condition, then static changes made in the above examples are not enough.

In such situations, you use the built-in Windows debugging functions to help you create your own tools. This method consists in creating an application resembling a debugger, which will automatically load the indicated application and using the breakpoint system will be able to stop the application in the indicated place and, depending on the conditions, make appropriate changes in the application's memory or read some information that is available only under the existing conditions (e.g. temporarily decrypted data).

The example below presents the use of the described technique to run the application with a trap on a specific code address and waiting for the moment when the application reaches this part of the code, after which the state of the processor registers will be read and the value of one of them will be copied in text form to the system clipboard.

#include <windows.h>
#include <stdio.h>

#define BPX_AT (LPVOID)0x401361

int main(int argc, char *argv[])
{
  STARTUPINFO lpSi = { 0 };
  PROCESS_INFORMATION lpPi = { 0 };
  DEBUG_EVENT lpDe;
  CONTEXT lpCtx;
  BYTE cBreakpoint = 0xCC, cOriginal;
  DWORD dwWritten = 0;
  DWORD dwContinueStatus = DBG_CONTINUE;
  HANDLE hMem = NULL;

  // initialize the STARTUPINFO structure
  GetStartupInfo(&lpSi);

  // run the process in suspend mode
  CreateProcess("C:\\aplikacja.exe", NULL, NULL, NULL, \
                TRUE, DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS | \
                CREATE_SUSPENDED, NULL, NULL, &lpSi, &lpPi);

  // read the original byte value from he address
  // where breakpoint byte (0xCC) will be set
  ReadProcessMemory(lpPi.hProcess, BPX_AT, &cOriginal, 1, \
                    &dwWritten);

  // set the breakpoint at the provided address (int 3 instruction)
  WriteProcessMemory(lpPi.hProcess, BPX_AT, &cBreakpoint, 1, \
                     &dwWritten);

  // resume the suspended process
  ResumeThread(lpPi.hThread);

  while (1)
  {
    WaitForDebugEvent(&lpDe, INFINITE);

    dwContinueStatus = DBG_CONTINUE;

    switch (lpDe.dwDebugEventCode)
    {
    case EXCEPTION_DEBUG_EVENT:

      // was it a breakpoint?
      switch (lpDe.u.Exception.ExceptionRecord.ExceptionCode)
      {
      case EXCEPTION_BREAKPOINT:

        // was it our breakpoint on our memory address?
        if (lpDe.u.Exception.ExceptionRecord.ExceptionAddress == BPX_AT)
        {
          // important! - set the flag that tells how
          // much information will be read back to the
          // CONTEXT structure that holds the values of the
          // CPU registers at the time when the code hit our
          // 0xCC (int3) breakpoint
          lpCtx.ContextFlags = CONTEXT_ALL;

          // read the values of the CPU registers
          GetThreadContext(lpPi.hThread, &lpCtx);

          // restore the original code byte at the breakpoint
          // location (now it's filled with 0xCC byte)
          WriteProcessMemory(lpPi.hProcess, BPX_AT, &cOriginal, \
                             1, &dwWritten);

          // set the return address from the exception handler
          // in CPU EIP register
          lpCtx.Eip = (DWORD)BPX_AT;
          lpCtx.ContextFlags = CONTEXT_ALL;

          // set the CPU register values including the new EIP pointer
          SetThreadContext(lpPi.hThread, &lpCtx);

          // read the contents of the memory pointed by the EAX register
          // to the system clipboard
          if (OpenClipboard(NULL) == TRUE)
          {
            hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, 256);

            if (hMem != NULL)
            {
              sprintf((char *)GlobalLock(hMem), "%08X", lpCtx.Eax);

              SetClipboardData(CF_TEXT, hMem);
            }

            CloseClipboard();
          }
        }

       break;
    }

    break;

    case EXIT_PROCESS_DEBUG_EVENT:

      // close the thread and the process handles
      CloseHandle(lpPi.hThread);
      CloseHandle(lpPi.hProcess);

      ExitProcess(0);

    }

  ContinueDebugEvent(lpDe.dwProcessIdsId, lpDe.dwThreadId, dwContinueStatus);

  } // while(1)

  return 0;

}

This method is not often used due to its complexity and restrictions in the operating system itself (Windows Vista), which make it impossible to debug an application without appropriate rights, due to the possibility of manipulation on the application memory.

1.2.7. Unpacking applications

Modern application protection systems are able to deal with various forms of attack on the application code and sometimes the only way to break the application is to unpack the protected application.

The purpose of this operation is to restore a protected application file to its original form (without any protection), making it easier to analyze it and remove additional protection, if any.

The term "unpacking" is used here because security systems (exe-protector) use in connection with encryption of the application binary file also its compression. The compression is used to reduce the size of the application file after it has been protected because usually, the protection code added to the application file would increase the size of the application file, and by compressing it is possible to keep a comparable size of the file before it is protected or even achieve a significant degree of compression (depending on the compression algorithm used).

Unpacking protected applications can be divided into "manual" mode unpacking and automatic unpacking using specialized unpackers applications.

Manual unpacking is a labour-intensive process and requires knowledge of many aspects of protection systems used in the software. Manual unpacking of protected files uses a range of tools such as debuggers, disassemblers, and programs that rebuild certain elements of executable files that have been intentionally corrupted.

Manual unpacking requires a thorough analysis of the protected software and in practice debugging the entire protection code, so that you can see the vulnerabilities of the protection, understand how it works and remove it.

With the development of protections, tools such as the OllyDbg debugger have been equipped with add-ons that allow you to automatically control the debugger through scripting languages.

Nowadays, scripts allowing to bypass the most popular protections (or fragments of them) are published, thanks to which the reconstruction of binary files, protected by popular protection systems becomes automatic in practice.

These methods do not always work, mainly due to the complexity of protection systems (and options used during protection) or their updates, which often aim at stopping such attacks, but the automation of the process of removing the protections shows that there has been significant progress in the techniques of breaking the software.

Below is a sample script, written in the OllyScript language for the OllyDbg debugger, whose function is to intercept the operation of a selected application at a specific point (in the presented example it is to intercept the decrypted data in the memory) and then save it to disk:

; declaration of variables
var string_ptr
var file_name
var file_index
var file_size
var x

; set a breakpoint on the instruction, where
; the data is decrypted
bp 401020

; initialize the file_index variable to 0
mov file_index, 0

; once the trap is set, continue to run the application
again:
run

; the following code will be executed if the application encounters
; to the previously set trap, continue with the application
; (without stopping at OllyDbg)
cob

; the script allows you to read current registers
; processor, the indicator for the decrypted data finds
; in memory at the address indicated by [ebp-14].
mov x, ebp
sub x, 14
mov x, [x]

; save the pointer to the decrypted data to the variable
; string_ptr
mov string_ptr, x

; the decrypted data are strings that are completed
; in memory byte 0x00, finding this byte, you can specify
; the size of the string, so that it can be saved to disk
find string_ptr, #00#
cmp $RESULT, 0
je skip_file

; count the size of the marker string (the difference between the end
and its beginning
mov x, $RESULT
sub x, string_ptr

mov file_size, x

; prepare the name of the file in which it will be saved
; decrypted character string, dropped from memory
The eval function works like a C-language sprint.
eval    "C:\Test\{file_index}.txt"
mov file_name, $RESULT

; dump the memory of the decrypted data to a file
dm string_ptr, file_size, file_name

; display the log of the operation performed in OllyDbg
eval "{file_index} - VA = {string_ptr}, Size = {file_size}"

log $RESULT;

; increase the index used to create files
inc file_index

skip_file:

; continue with the application
jmp again

The OllyScript language syntax resembles the assembler language syntax, but it is equipped with additional functions that allow for a whole range of actions to automate manual operations in the OllyDbg debugger.

The above example is only a simple demonstration of the capabilities of OllyScript, the scripts used to bypass commercial security are more complex, and their size sometimes reaches tens of kilobytes of code.

Apart from OllyScript, there is a special version of the OllyDbg debugger called Immunity Debugger, which is equipped with a scripting system based on the Python language. Below is a sample script, searching for specific instruction in the given module:

#!/usr/bin/env python

__VERSION__ = '1.0'

import immlib

def main():

  imm = immlib.Debugger()

  cmd="pop ebx"

  res=imm.searchCommandsOnModule(0x7C9C1005,cmd)

  imm.Log("one module")

  for addy in res:

    imm.Log( str(addy))

  res=imm.searchCommands(cmd)

  imm.Log("all modules")

  for addy in res:

    imm.Log( str(addy) )

if __name__=="__main__":

  print "This module is to be used within Immunity Debugger only"

Scripts written in Python language are most often used when searching for software vulnerabilities, and scripts based on OllyScript are used to crack protections.

However, manual unpacking or using scripts can be too much of a waste of time, as the most popular protection systems have been created with the creation of unpacking tools that allow you to easily recreate the original form of protected files without any additional user intervention.

These tools are usually a combination of a debugger or an emulator, allowing you to track the operation of the protection code and dump or automatically rebuild the original application.

Often such tools use specially written system drivers to hide their presence from the security code or to bypass applied protections that cannot be bypassed in user mode.

Automatic unpacking programs do not always work either, but they are much more dangerous because, thanks to their simple operation, they can be used even by inexperienced users to distribute unprotected copies of software.

Chapter 2. Software protection methods

This chapter will discuss how to protect your software against cracking.

2.1. Protection against software analysis

The basic form of protection of an application against analysis is hiding its code from tools such as disassemblers and debuggers.

Apart from hiding the code, more modern methods are also used, consisting of a mutation of the original code, making it difficult to analyze it and understand its functioning. There are many methods of protecting software from analysis, and the following are used in modern protection systems.

2.1.1. Compression

Application binary file compression was already used in the times of MS-DOS, mainly due to the limited size of data carriers such as floppy disks on which programs were distributed.

The standard floppy disk presented below has a capacity that nowadays wouldn't allow saving a single MP3 file, that's why data compression was so valuable as well as the applications themselves.

Standard floppy disk with a capacity of 1.44 MB
Standard floppy disk with a capacity of 1.44 MB

Applications compressing executable binary files are called exe-packers. Their principle of operation is similar to ordinary data archiving applications, except that a small piece of code is added at the end to the code of the compressed binary file, which is responsible for decompressing the data and starting the application.

One of the first popular compressors for MS-DOS was a Polish program designed by Piotr Warezak - WWPack, which allowed to compress binary files in EXE and COM formats.

File compression provided only simple protection against code analysis, because, with the advent of compression tools, programs were introduced to extract compressed applications.

One of the most popular programs of this type was a tracer called CUP386, for DOS environment, which allowed to automatically track compressed application files and rebuild them (by dropping memory areas that were decompressed). CUP386 was able to automatically rebuild applications compressed by compressors like CUP386:

Compressing applications have evolved with new versions of Windows and the most popular ones are today:

The Linux-based system, apart from the UPX compressor, did not see many tools for compressing Executable and Linkable Format (ELF) executable files, and it can be said that in this area, any innovations were caused by the development of Windows and often their incompatibility, which forced the authors of compression tools to look for solutions to many complicated technical problems, resulting from system differences, for example between the Windows 9x family of operating systems and systems based on Windows NT, which differ in the way they load executable files and the way they handle some of their structures.

Currently, application binary file compression is only used to reduce the size of executable files, they are not used to secure applications, but the data compression itself is used by protection systems as an addition to the whole.

Binary file compressors are also often used to protect malware, which bypasses detection by antivirus programs that rely on scanning files for known signatures (sets of bytes taken from widespread viruses).

The compression of binary files does not change the way they work, but only causes the antivirus software to have to be updated with a new set of signatures.

Malware compression was also used against antivirus programs that could not cope with decompression code (using emulation), but nowadays antivirus programs are more and more advanced and use other instruments to detect malware, such as behavioral detection, based on observation of programs' behavior in the operating system and changes made by them.

2.1.2. Encryption

Already in the times of MS-DOS, in addition to executable file compressors, more complicated programs began to appear, whose main purpose was to protect the software from breaking. Such programs are called exe-protectors.

The main difference between the compressors and protectors was that the protectors contained, in addition to the compression code, procedures that encrypted data and those that made it difficult to rebuild a protected application to its original form.

The development of this type of security software was mainly related to the popularity of shareware programs, which were more and more willingly protected by their authors in order to prevent them from being broken and, as a result, the availability of a pirated version on the market.

Over time, simple security systems evolved into advanced tools, equipped with a whole range of security methods, integrated licensing systems, based on public key infrastructure (RSA encryption, elliptical curves, etc.).

The most popular security systems include:

  • PELock
  • Asprotect
  • Obsidium
  • SVKP
  • ExeCryptor
  • ActiveMARK
  • StarForce

The protection methods used in protection systems include the following:

  • Detection of tools used to crack the protection
  • Encryption and virtualization of selected code fragments
  • Rebuilding application data structures (import table)
  • Protection of original application data structures against restoration
  • Dynamic memory protection of the protected application
  • Protection against modification of application files

2.1.3. Virtualization

Code virtualization consists in transforming instructions written in the original, compiled form into pseudo-code of your own processor. Code virtualization is currently one of the most popular methods used to make it difficult to understand the operation of critical code fragments, such as procedures for checking application registration numbers or some secret algorithms processing data.

Code virtualization makes the analysis of the code require the knowledge of the form of instructions into which the original code was processed, the analysis of the virtual machine, which is responsible for the interpretation of new instructions, and the analysis of the code subjected to the process of virtualization, which in addition can still be mutated at the level of pseudocode.

The idea of virtualization and using pseudocode instead of assembler code for one platform is not new and it is used by Java, Visual Basic and those from the .NET family, i.e. C#, VB#, J#, where the source code is compiled into a form of pseudocode, which is run, interpreted and executed by a suitable virtual machine.

IL pseudocode of the .NET virtual machine
IL pseudocode of the .NET virtual machine

The purpose of using the pseudocode for the solutions described is to make the compiled applications easily portable between different operating systems and processors.

The use of pseudocode instead of the code suitable for the installed processor is associated with performance degradation, because the virtual machine interpreting the pseudocode is much slower than the native code.

Therefore, the pseudo-code compilation for the current processor (Just In Time compilation, or JIT for short) is used. This technique allows to apply advanced optimization algorithms depending on the available processor and use its full capabilities with support for extensions such as multimedia extensions of x86 family processors, i.e. MMX, SSE, SSE2 etc.

Thanks to this technique, applications written in interpreted languages show higher performance than if they were compiled directly into the code of the selected processor, where they are compiled into only one form, backward compatible with older processors, which reduces its speed.

However, the virtualization of the code in order to secure it is slightly different from the applications described above, and usually performance plays a secondary role here, but the emphasis is placed on the greatest possible complexity of the virtual machine and the form of instructions and their mutation.

Nowadays, code virtualization is offered by most security systems, such as the Virtual Machine:

  • StarForce - is a gaming security package that was the first to apply the transformation of executable code to pseudocode, which has gained recognition among game publishers for its high level of security.
  • ExeCryptor - a security package for all kinds of commercial Windows applications, using partial virtualization of application code as one of the security techniques.
  • Themida - an advanced application security system for Windows, which offers the transformation of application code into the code of one of the selected virtual processors.
  • VMProtect - application security package and system drivers, which is based on the transformation of application code into pseudocode.

These tools are suitable for securing Windows applications written in any programming language, provided that the executable file contains code compatible with x86 processors.

For these packages, virtualization is typically used to secure a small number of procedures and functions in a given application, as this results in the performance degradation described.

When writing an application's source code, programmers most often mark code fragments with special markers that allow security packages to find code fragments that are to be transformed into pseudocode.

An example of marking code to be transformed in an application for the Delphi environment:

function Test(i: integer): integer;
begin
   // a marker indicating the beginning of the code to be
   // transformed into a pseudocode...
   {$I VM_START.inc}

   Result := i * 2;

   // marker to indicate the end of the code
   {$I VM_END.inc}

End;

However, there are much more specialized protection packages that allow you to create applications in specially designed languages that will be entirely compiled into a pseudocode, but this type of software is rare.

Code virtualization is, and most likely will be, the most common method of protecting software from breaking, and it can be expected that significant progress will be made in this area, as this method effectively makes it difficult or even impossible to understand how the code works and does not allow for simple modification.

2.2. Protection against modification of files and memory

Protecting against modifications made to application files or their memory is a key element of protection against breakage, as modifying essential software components can be an easy way to bypass the protection applied.

2.2.1. Checksums

Checksums are the basic form of protection for application files to determine whether the original content has been modified. Checksums are usually calculated from compiled program files, and then placed in additional files distributed with applications (e.g. databases) or in the structure of the controlled files themselves (in a specific file location that is omitted when calculating the checksum).

When starting an application, the checksum is calculated from the current data and compared to the original checksum. If the checksums match, this indicates that nothing has been modified, but if the checksums do not match, this may indicate an intentional modification (e.g. to remove a security feature), made on the application files, and usually, under these circumstances, the application is closed.

It should be noted here that a change in the checksum of an executable binary file may also be caused by damage to the data carrier (e.g. as a result of physical damage) or, less frequently, by a file virus, which is rare in recent years.

The CRC32 algorithm is mainly used to calculate checksums, which allows to calculate a 32-bit check value from any data buffer. Below is a fragment of CRC32 algorithm, written in C language.

static const unsigned int table[256] = {
  0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,
  0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,
  0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,
  ...
  0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D
};

unsigned int crc32(unsigned char *data, unsigned int size, unsigned int crc)
{
  while(size > 0)
  {
    size--;

    crc = (((crc >> 8) & 0xFFFFFF) ^ table[(crc ^ *data++) & 0xFF]);
  }

  return(crc);

}

Due to the small size of the final check value, it is possible to create two different data sets for which the CRC32 checksum will be the same (so-called collision).

It is possible to imagine a situation when the CRC32 checksum is used to verify an executable file where some bytes have been replaced, as a result of which the checksum will not match, but filling the file with the relevant data will make the checksums match.

Due to this weakness, more complicated algorithms are most often used, such as hash functions, which create longer checksums, for which generating two sets of data giving the same result would be a very time-consuming process.

The hash function algorithms:

Hash algorithm name

Year

The length of the hash in bits

MD2

1992

128

MD4

1990

128

MD5

1992

128

SHA-0

1993

160

SHA-1

1995

160

SHA-2

2004

224 / 256 / 384 / 512

RIPEMD

1996

160

Whirlpool

2000

512

The biggest weakness of checksums is the possibility to change them or to change the code that is responsible for their calculation, so often several checksums are used, which are additionally checked not only from the main application file, but also additional dynamic libraries attached to the whole application are responsible for their calculation and verification.

2.2.2. Memory monitoring

Changes made to the files can be detected by using checksums, in the same way, the application code can be verified, uploaded to memory. For memory monitoring, an additional thread in the running application is most often used, which, running in the background, is responsible for scanning selected fragments of the application code and verifying their correctness using checksums.

While the implementation of checksums for application files is relatively simple for an average programmer, the creation of application memory monitoring code already requires more knowledge in the field of software analysis, as it requires knowledge of addressing executable files in memory, knowledge of the location of code fragments to be monitored and additional aspects such as, for example, relocations, which make the checksums different each time, depending on which area of memory the application was loaded by the operating system.

2.2.3. Error correction codes

Error correction codes are used to correct corrupted data, such as transmission data. The use of error correction codes can also be used in software protection, to repair a deliberately modified application code in a file and in memory. There are several versions of correction codes:

  • Hamming – used for the correction of 1-bit data corruption
  • Redd-Solomon – used to correct more data
  • Turbo – used for correction of data transmitted via satellite

The use of correction codes involves attaching additional information to the original data to correct the damaged fragments.

Correction codes have not found widespread use in security systems due to low performance in case of correction of large amounts of modified data and due to easier implementation of checksums, which work better as an element of protection against unwanted modifications introduced into the application.

2.3. Detection of tools to assist in cracking applications

Detection of tools used to break software is one of the most popular methods to protect software from breaking.

These methods are used to prevent the application from running in the presence of selected tools. Currently, there is a whole range of different methods to detect virtually all available applications that are used to break software security, but these applications have also experienced extensions or special versions that are not detected by popular methods.

This fact shows that the detection of software cracking tools is only the first line of resistance and you should not base the whole protection on just one protection method.

2.3.1. Debugger detection

Debuggers are detected by different methods depending on their type, i.e. system or user mode debuggers. System debuggers are most often detected by the drivers they use or by a thorough check of the system structures that are modified in the presence of the debugger to allow tracking of the application and its operations.

Below is a method that allows detecting the SoftICE debugger by checking the presence of its loaded driver in the operating system:

//
// function to check the presence of the SoftICE debugger
// by detecting its controller in memory
//
BOOL IsSoftICE()
{
  // check to see if you can open the SICE driver
  if (CreateFile("\\\\.\\SICE",GENERIC_READ,FILE_SHARE_READ \
                | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL) \
      != INVALID_HANDLE_VALUE)
  {
    // the driver is detected in the system, return it to TRUE
    return TRUE;
  }

  // no driver was detected
  return FALSE;
}

Some system debuggers provide undocumented features available through their own API system, which is also used to detect their presence.

Below is another method that allows you to detect the SoftICE debugger by calling the number 3 interrupter, normally used in debuggers to set traps in the code, but with a special set of codes stored in 16-bit registers of the SI and DI processors:

    mov    si,'FG'
        mov     di,'JM'
        int     3

Executing the above code without the presence of the SoftICE debugger will result in an exception in the code (which can be safely captured), but in the presence of the debugger there will be no exception, because the SoftICE debugger controls interrupt number 3 all the time and if special values are detected in the processor registers, it allows the application to continue running.

Some methods used to detect system debuggers are based on techniques that are safely performed only on specific operating systems (e.g. calling interrupts or accessing system structures) and their performance in more restrictive environments may cause the application to be suspended.

More compatible methods can be used to detect debuggers running in user mode, such as detecting a debugger by its main window title or process name.

User mode debuggers are characterized by the fact that they use a common set of WinApi functions and are operated under the responsibility of the operating system itself, so it is possible to detect different debuggers by using information that the operating system itself leaves behind.

Below is a piece of code in the assembler, which allows you to detect active debuggers, using the code stripping features built into Windows:


        cmp     dword ptr fs:[30h],0            ; is it an NT family system?
        jns     _windows_nt

        cmp     dword ptr fs:[20h],0            ; detect debugger for
        jne     _debugger_detected              ; Windows 95, 98, ME

        jmp     _continue                       ; continue

_windows_nt:

        mov     eax,dword ptr fs:[30h]          ; pointer to the system
                                                ; process PEB structure

        movzx   eax,byte ptr [eax+2]            ; debugger presence is marked
        or      al,al                           ; within PEB structure field
        jne     _debugger_detected

_continue:

This section refers to two system structures (depending on the current operating system) in which the presence of a debugger is indicated and on this basis the application is continued or interrupted.

When detecting debuggers, it should be mentioned that the presence of a debugger does not always indicate bad intentions, but the vast majority of security systems based on debugger detection treat the presence of a debugger as an attempt to break the security system.

2.3.2. Detection of system monitoring tools

Monitoring tools such as FileMon or RegMon can be detected by the names of the drivers they use (just like system debuggers are detected) or by the titles of their windows.

BOOL IsRegMon()
{
  HWND hWindow = NULL;

  hWindow = FindWindowEx(NULL,NULL,NULL,"Registry Monitor - Sysinternals: www.sysinternals.com");

  // return TRUE if a window handle is found
  // with the title given or FALSE if not
  return (hWindow != NULL) ? TRUE : FALSE;

}

Methods based on detecting monitoring applications by searching for their windows are commonly used in integrated security systems. However, their effectiveness is very low, because people who break the software use modified versions of this type of application, where, for example, the title of the monitoring application window is changed compared to the original.

Often methods based on window detection based on the exact title of the application are replaced by more sophisticated methods, such as checking the position and size of characteristic lights in the monitoring application windows (e.g. the position of buttons or edit fields).

Chapter 3. Working principle of the software protection system

This chapter will present a sample application to protect software against cracking.

For its creation we used selected techniques, discussed in previous chapters, and a sample license system based on license keys, the content of which is used to decrypt code fragments, marked with special markers.

Without a proper license key, one does not have access to encrypted code fragments.

Next, I will present the stages of its creation and functioning.

3.1. Background information

The application to protect software against cracking was written in C++ using Microsoft Visual Studio environment and in assembler language for x86 platform. The operation of the created application can be divided into three stages, which will be discussed in detail.

3.1.1. Graphical interface of the protection application

The security application is equipped with a graphical user interface, consisting of a dialog box, allowing you to select an executable file for security. The whole thing is based on WinApi functions, without using additional libraries.

Software protection graphical interface
Software protection graphical interface

The graphical interface allows you to select an executable file for protection and informs about the next steps of protecting the file. After protecting the file, it is possible to start it quickly by selecting the "Test" option.

3.1.2. Loader program code

The loader is the part of the application that comes with the protected file and is responsible for protecting the software from being broken. The code of the loader was created in assembler language [Wójcik 2004] for Intel processors.

3.1.3. Handling of the executable file structure

The CPELib class, written in C++, is responsible for handling the format of the executable files. Its main task is to provide access to the internal structures of the executable file and to create new executable files with attached security code.

The CPELib class uses the knowledge of books on building executable files and information available on the Internet.

3.1.4. Protecting the executable files

To support the protection of executable files, CEncryptExe class was used, which uses CPELib class and loader code to create a protected copy of the software.

3.2 Protecting an executable file

The CEncryptExe class provides the EncryptExe method, which is responsible for securing the selected executable file. The only parameter of this method is the pointer to the structure describing information such as file path to be protected, output file path, license key name and others.

typedef struct _ENCRYPTEXE_PARAMS {

  const TCHAR *lpszInputFilename;
  const TCHAR *lpszOutputFilename;
  ENCRYPTEXE_MSGBOX epMsgLicenseNotFound;
  const char *lpszLicenseName;
  const char *lpszLicenseKey;

} ENCRYPTEXE_PARAMS, *PENCRYPTEXE_PARAMS;

3.2.1. Checking the input parameters

The first step of the EncryptExe method is to verify the input parameters in order to avoid errors in case of their absence.

DWORD CEncryptExe::EncryptExe(PENCRYPTEXE_PARAMS lpEncryptParams)
{
DWORD dwResult = CEncryptExe::ERR_SUCCESS;
...

/////////////////////////////////////////////////////////////////
//
// check input parameters
//
/////////////////////////////////////////////////////////////////

if ( (lpEncryptParams == NULL) || (lpEncryptParams->lpszInputFilename == NULL) )
{
  LOG(_T("Please enter valid input parameters!"));

  return CEncryptExe::ERR_INVALID_PARAMS;
}

3.2.2. Accessing the input file

The next step is to access the input file to read it later.

/////////////////////////////////////////////////////////////////
//
// open input file
//
/////////////////////////////////////////////////////////////////

#ifdef UNICODE
  hFile = _wfopen(lpEncryptParams->lpszInputFilename, _T("rb") );
#else
  hFile = fopen(lpEncryptParams->lpszInputFilename, _T("rb") );
#endif

  if (hFile != NULL)
  {
    LOG(_T("%s file has been successfully opened."), \
    lpEncryptParams->lpszInputFilename);
  }
  else
  {
    LOG(_T("Cannot open %s file"), \
    lpEncryptParams->lpszInputFilename);

    return CEncryptExe::ERR_FILE_INPUT;
  }

The input file is opened as a binary file in the read mode. The functions used to open the file are compatible with international character encoding in Unicode mode depending on the project compilation settings.

If it is not possible to open the file (e.g. when the file is already opened by another application), the EncryptExe method terminates the operation with the appropriate error code that can be used for later analysis of the error.

3.2.3. Reading file contents

After accessing the file, its size is checked to avoid the file being empty, then the memory area where its contents are loaded is allocated.

/////////////////////////////////////////////////////////////////
//
// check input file size
//
/////////////////////////////////////////////////////////////////

fseek(hFile, 0, SEEK_END);

dwFile = ftell(hFile);

fseek(hFile, 0, SEEK_SET);

if (dwFile != 0)
{
  LOG(_T("Input file size is %lu bytes."), dwFile);
}
else
{
  LOG(_T("Input file is empty (0 bytes)!"));

  fclose(hFile);

  return FALSE;
}

/////////////////////////////////////////////////////////////////
//
// allocate the memory to read the contents of the input file
//
/////////////////////////////////////////////////////////////////

lpFilePtr = new BYTE[dwFile];

if (lpFilePtr == NULL)
{
  LOG(_T("Cannot allocate the memory for reading the input file!"));

  fclose(hFile);

  return FALSE;
}

/////////////////////////////////////////////////////////////////
//
// read the input file
//
/////////////////////////////////////////////////////////////////

if (fread(lpFilePtr, 1, dwFile, hFile) == dwFile)
{
  LOG(_T("File successfully read."));
}
else
{
  LOG(_T("Cannot read the input file!"));

  fclose(hFile);

  delete [] lpFilePtr;

  return CEncryptExe::ERR_FILE_INPUT_READ;
}

// close file handle
fclose(hFile);

3.2.4. Reading an executable file

After reading the contents of the file into the allocated memory, it is successively loaded through the CPELib class as an executable file, giving access to its internal structures and data.

/////////////////////////////////////////////////////////////////
//
// use CPELib class to read the input file
//
/////////////////////////////////////////////////////////////////

if ( m_PeLib.LoadFile(lpFilePtr, dwFile) != m_PeLib.PERR_SUCCESS)
{
  LOG(_T("Input file is invalid (invalid Portable Executable format)!"));

  delete [] lpFilePtr;

  return CEncryptExe::ERR_FILE_INPUT_INVALID;

}

When a file is loaded using the CPELib class, its format and structural correctness are verified, as there are times when executable files contain corrupted or incomplete data, preventing further protection actions.

3.2.5. Creating a new file in memory

Based on the input file, a copy of the file is created in memory, containing the same data structures, which will be used to create a protected version of the file.

/////////////////////////////////////////////////////////////////
//
// create the output file in memory, that is based on the input file
//
/////////////////////////////////////////////////////////////////

lpRebuilded = m_PeLib.NewFile(lpOepRVA, (m_PeLib.pNT->OptionalHeader.SectionAlignment), (m_PeLib.pNT->OptionalHeader.FileAlignment), FALSE);

if (lpRebuilded == NULL)
{
  LOG(_T("Cannot allocate memory for the output file!"));

  delete [] lpFilePtr;

  return FALSE;
}

3.2.6. License key checksum

The license system uses a system of keys without which fragments of the code of the original application cannot be decrypted. To encrypt these fragments, a sample algorithm is used, which uses a checksum calculated from the contents of the key as a key.

/////////////////////////////////////////////////////////////////
//
// calculate a checksum from the contents of the license key,
// it will be used to encrypt code sections
//
/////////////////////////////////////////////////////////////////

for (i = 0, dwLicenseKey = 0; i < strlen(lpEncryptParams->lpszLicenseKey); i++)
{
  dwLicenseKey += (BYTE)lpEncryptParams->lpszLicenseKey[i];
}

The control value used as an encryption key is the sum of all the bytes of the license file and is only meant to illustrate the mechanism of operation of the security used in finished software protection systems, where much more complex verification and encryption schemes of license data are used.

3.2.7. Code and data encryption

The next step in protecting the file is to search for code fragments, marked with special markers, and encrypt the code between them with the key that is the checksum of the license file. Below is an example of locating encryption markers in the source code of the application:

#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include "EncryptExe.h"

int main()
{
  ENCRYPT_START

  printf("License key is present!\n");

  ENCRYPT_END

  return 0;
}

Apart from encrypting the marked code fragments, whole sections containing the code and data are also encrypted in order to make it impossible to read the content of the entire file easily for third parties.

/////////////////////////////////////////////////////////////////
//
// - find and encrypt code fragments between
//   ENCRYPT_START and ENCRYPT_END
// - also encrypt entire code section using RC6 algorithm
//
/////////////////////////////////////////////////////////////////

// allocate memory for maximum number of possible code encryption markers
lpEncryptionMacros = new ENCRYPTEXE_MACRO[ENCRYPTEXE_MACROS_COUNT];

// generate random encryption keys for the RC6 algorithm
for (i = 0; i < sizeof(cEncryptionKey); i++)
{
  cEncryptionKey[i] = (BYTE)rand();
}

for (i = 0, lpCurrentSection = m_PeLib.pSections; i < m_PeLib.dwSections; i++, lpCurrentSection++)
{
  bEncrypted = FALSE;

  // encrypt code and data sections only, ignore other type of sections
  for (j = 0; j < sizeof(szSections) / 4; j++)
  {
    // is it a valid executable section name?
    if (strncmp((const char *)&lpCurrentSection->Name[0], \
                szSections[j], 8) == 0)
    {
      // does the section has any data inside?
      if ( (lpCurrentSection->VirtualAddress != 0) && \
           (lpCurrentSection->SizeOfRawData != 0))
      {
        // read the offset to the section data or code within the file
        lpData = m_PeLib.RVA2Offset(lpCurrentSection->VirtualAddress);
        dwData = lpCurrentSection->SizeOfRawData;

        // find the code between the
        // ENCRYPT_START and ENCRYPT_END
        // encryption markers
        dwEncryptionMacros += FindEncryptionMacros(lpData, \
                                                   lpCurrentSection->VirtualAddress, dwData, \
                                                   &lpEncryptionMacros[dwEncryptionMacros]
        );

        // encrypt entire section
        rc6_crypt(cEncryptionKey, lpData, dwData, TRUE);

        bEncrypted = TRUE;
      }
    }
  }

  // copy the section to the new output file
  lpSection = m_PeLib.CopySection(lpCurrentSection, FALSE);

  // was the section encrypted?
  if (bEncrypted == TRUE)
  {
    // if it was, set a special marker within section structure
    // to inform the loader program about the encryption
    lpSection->NumberOfLinenumbers = 1;

    // add write access flags to the encrypted section characteristics
    lpSection->Characteristics |= IMAGE_SCN_MEM_WRITE;
  }
}

Encrypted sections of code and data are marked in the section header structure with a special marker, which is read when starting a protected application and if set, the whole section is decrypted.

3.2.8. Adding a loader

The loading program is written in assembler language and its task is, among others, to handle encrypted code fragments, verify license data and detect tools used to break the software. The loading program is added at the end of the secured file, in a new section called .code.

/////////////////////////////////////////////////////////////////
//
// insert the loader program code into the protected file
// add information about encrypted code fragments
//
/////////////////////////////////////////////////////////////////

DWORD dwLoaderCode = sizeof(cLoaderCode) + dwEncryptionMacros * sizeof(ENCRYPTEXE_MACRO);

lpLoaderCode = new BYTE[dwLoaderCode];

memcpy(lpLoaderCode, cLoaderCode, sizeof(cLoaderCode));
memcpy(&lpLoaderCode[sizeof(cLoaderCode)], lpEncryptionMacros, dwEncryptionMacros * sizeof(ENCRYPTEXE_MACRO));

lpCurrentSection = m_PeLib.InsertSection(".code", lpLoaderCode, dwLoaderCode, 0xE00000E0);

delete [] lpEncryptionMacros;
delete [] lpLoaderCode;

lpLoaderCode = m_PeLib.RVA2OffsetNew(lpCurrentSection->VirtualAddress);

After adding the loader you need to update some of its data, e.g. the number of encrypted code fragments, the name of the license key file you are looking for, the original code entry point of the protected application, and the location of important structures of the original file, which must be corrected by the loader itself when running.

/////////////////////////////////////////////////////////////////
//
// update the loader program
//
// - set the original entrypoint for the application
// - set the address of the application import table
// - set the global encryption keys
// - save the number of ENCRYPT_START and ENCRYPT_END markers
//
/////////////////////////////////////////////////////////////////

ReplaceValue(lpLoaderCode, sizeof(cLoaderCode), "OEP1", m_PeLib.pNT->OptionalHeader.AddressOfEntryPoint);
ReplaceValue(lpLoaderCode, sizeof(cLoaderCode), "IAT1", m_PeLib.pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
ReplaceValue(lpLoaderCode, sizeof(cLoaderCode), "MAR1", dwEncryptionMacros);
ReplaceValue(lpLoaderCode, sizeof(cLoaderCode), "MAR2", dwEncryptionMacros);

memcpy(&lpLoaderCode[LDR_ENCRYPTION_KEY], cEncryptionKey, sizeof(cEncryptionKey));

// name of the license key
COPY_STRING_TO_LOADER(16, LDR_LICENSE_FILE, lpEncryptParams->lpszLicenseName);

// strings for the information box
COPY_STRING_TO_LOADER(128, LDR_MSG_LICENSE_CPT, lpEncryptParams->epMsgLicenseNotFound.lpszMsgCaption);
COPY_STRING_TO_LOADER(128, LDR_MSG_LICENSE_TXT, lpEncryptParams->epMsgLicenseNotFound.lpszMsgText);
SET_LOADER_DWORD(LDR_MSG_LICENSE_ICO, lpEncryptParams->epMsgLicenseNotFound.uType);

The loader uses Windows WinApi functions in order to use them, the loader comes with a special table called an import table, which contains information on what Windows functions the loader will be able to use.

The loader import table is placed in a different location in the protected file each time, so some of its elements need to be adjusted depending on its current location.

/////////////////////////////////////////////////////////////////
//
// update import table structure for the loader
//
/////////////////////////////////////////////////////////////////

lpLoaderImports = (PIMAGE_IMPORT_DESCRIPTOR)lpLoaderCode;

while(lpLoaderImports->OriginalFirstThunk != 0)
{
  lpLoaderImports->FirstThunk += lpCurrentSection->VirtualAddress;
  lpLoaderImports->OriginalFirstThunk += lpCurrentSection->VirtualAddress;
  lpLoaderImports->Name += lpCurrentSection->VirtualAddress;
  lpLoaderApis = (PDWORD)m_PeLib.RVA2OffsetNew(lpLoaderImports->OriginalFirstThunk);

  while (*lpLoaderApis != 0)
  {
    *lpLoaderApis += lpCurrentSection->VirtualAddress;
    lpLoaderApis++;
  }

  lpLoaderImports++;

  dwLoaderImports += sizeof(IMAGE_IMPORT_DESCRIPTOR);
}

After updating the loader import table structure, it is set as the default import table for the application, replacing the original protected file import table:

m_PeLib.SetDirectory(IMAGE_DIRECTORY_ENTRY_IMPORT, lpCurrentSection->VirtualAddress, dwLoaderImports);

3.2.9. Setting the new entrypoint

After starting the application, the control is passed to the address of the entrypoint address, this is the place where the application starts. In normal applications, this address usually indicates the code that initializes the various data needed for the program to continue running.

After protecting the file, the entrypoint address must be set to the beginning of the loader code, which will pass the control to the original entrypoint of the application only after it has done its job.

/////////////////////////////////////////////////////////////////
//
// set a new application entrypoint to the loader offset
//
/////////////////////////////////////////////////////////////////

m_PeLib.pNewNT->OptionalHeader.AddressOfEntryPoint = lpCurrentSection->VirtualAddress + LDR_ENTRY;

3.2.10. Completion of the protection process

After encrypting the code and data and adding the loader, the code is executed which is responsible for updating the newly created executable file and saving it to the output file (or overwriting the input file).

/////////////////////////////////////////////////////////////////
//
// update the PE EXE file structure to reflect the changes
//
/////////////////////////////////////////////////////////////////

dwRebuilded = m_PeLib.CloseNewFile();

/////////////////////////////////////////////////////////////////
//
// create an output file or overwrite existing file
//
/////////////////////////////////////////////////////////////////

if (lpEncryptParams->lpszOutputFilename != NULL)
{
  lpszNewFile = (TCHAR *)lpEncryptParams->lpszOutputFilename;
}
else
{
  lpszNewFile = (TCHAR *)lpEncryptParams->lpszInputFilename;
}

#ifdef UNICODE

hFile = _wfopen(lpszNewFile, _T("wb+"));

#else

hFile = fopen(lpszNewFile, _T("wb+"));

#endif

// check file handle
if (hFile != NULL)
{
  LOG(_T("Created %s output file."), lpszNewFile);
}
else
{
  delete [] lpFilePtr;
  delete [] lpRebuilded;

  LOG(_T("Cannot create an output file %s!"), lpszNewFile);

  return CEncryptExe::ERR_FILE_OUTPUT_CREATE;
}

///////////////////////////////////////////////////////////////////
//
// save output file contents
//
/////////////////////////////////////////////////////////////////

// save output file
if (fwrite(lpRebuilded, 1, dwRebuilded, hFile) == dwRebuilded)
{
  LOG(_T("Output file size is %lu bytes."), dwRebuilded);

  dwResult = CEncryptExe::ERR_SUCCESS;
}
else
{
  LOG(_T("Cannot write to the %s output file!"), lpszNewFile);

  dwResult = CEncryptExe::ERR_FILE_OUTPUT_WRITE;
}

When a newly created file is saved, the previously allocated memory is released (to prevent memory leaks) and the open file handles are closed.

/////////////////////////////////////////////////////////////////
//
// release the memory and close the input file handle
//
/////////////////////////////////////////////////////////////////

delete [] lpFilePtr;
delete [] lpRebuilded;

fclose(hFile);

If the file protection process was performed successfully, the ERR_SUCCESS value will be returned.

/////////////////////////////////////////////////////////////////
//
// ERR_SUCCESS - it means success, everything else indicates an error
//
/////////////////////////////////////////////////////////////////

return dwResult;

}

3.3. Loader program and its functions

The loader takes control as soon as the protected file is started. The following subsections will show the next steps of its operation.

3.3.1. Initialization

When the application is launched, the operating system sets the state of the processor registers and passes the control to the application input point. For executable files, it is important that the initial CPU registers state before jumping to the original application entrypoint is preserved only for the ESP stack register.

For dynamic library files, it is required to retain additional ESI, EDI, EBP and EBX registries because the input procedure for dynamic libraries is the stdcall procedure and these registers retain important information that cannot be modified by the loader code.

;///////////////////////////////////////////////////////////////
;
; the beginning of the loader code, here it begins
; the execution of the protected application
;
;///////////////////////////////////////////////////////////////

_loader_entrypoint:

        ;int    3               ; breakpoint (for debugging purposes)

        push    esi             ; \
        push    edi             ;  > save critical CPU registers original values
        push    ebx             ; /

3.3.2. Relative addressing

Although the location of the loader code is known for the protected file, programming a normal code that directly refers to the memory cells through fixed addresses would require the correction of any such instruction in the loader code.

Using relative addressing, relative to some fixed base, makes it very easy to write code that is independent of the location in the memory. This method is called „delta offset” addressing and was originally used in computer viruses, but has found widespread use in software protection systems.

;///////////////////////////////////////////////////////////////
;
; calculate the relative offset used to access various loader parts
;
; so-called delta offset
;
;///////////////////////////////////////////////////////////////

        call _delta
_delta:
        mov     eax,dword ptr[esp]      ; eax = offset _delta
        sub     esp,-4

3.3.3. A common interface

The loader program has a lot of data and functions spread over the whole code, to make it easier, a common interface has been created, based on the data structure in which the most frequently used elements are stored, such as e.g. WinApi procedure addresses:

;///////////////////////////////////////////////////////////////
;
; main interface for the loader program
;
;///////////////////////////////////////////////////////////////

LDR_INTERFACE struct

        lpDelta                 dd ?    ; delta offset
        hModuleBase             dd ?    ; current application image base
        lpIAT                   dd ?    ; pointer to the loader import table
        lpcEncryptionKey        dd ?    ; pointer to the encryption/decryption key

        ; RC6 sections
        dwLicensePresent        dd ?    ; license key presence flag
        dwLicenseKey            dd ?    ; license key checksum

        lpPEHeader              dd ?    ; PE header memory pointer
        lpSectionTable          dd ?    ; section table
        dwSectionCount          dd ?    ; number of sections

        bWindows9x              dd ?    ; is it Windows 9x
        bWindowsNT              dd ?    ; is it Windows NT
        bWindowsVista           dd ?    ; is it Windows Vista or newer OS

LDR_INTERFACE ends

LDR_INTERFACE interface is updated right after running the protected application.

3.3.4. Operating system detection

Some protection elements work only properly on one operating system, so the loader detects the Windows version and saves information to the LDR_INTERFACE interface structure.

;///////////////////////////////////////////////////////////////
;
; get information about the Windows version
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _get_os_version         ; update the data within
                                        ; LDR_INTERFACE structure

3.3.5. Debuggers detection

Before performing any actions, the loader calls procedures to detect system debuggers and those operating in user mode. If the debugger is detected, the protected application is terminated immediately, without any warning message.

;///////////////////////////////////////////////////////////////
;
; run debugger detection code
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _antidebug_detect       ; detect active debuggers
        test    eax,eax                 ; 0 none detect, != 0 debugger detected
        jne     _exit                   ; terminate application in case of
                                        ; a debugger detection

3.3.6. License key

The license system is based on keys, the content of which is used to create keys that decrypt the marked code fragments. The next part of the loading program is responsible for finding the license key in the directory of the running application, reading it and calculating the checksum, which is the decryption key.

;///////////////////////////////////////////////////////////////
;
; check the presence of the license key file
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _verify_license         ; check the license key

In the absence of a key, a warning message will be displayed to warn you that the application has limited functionality, as without the key, encrypted code fragments will be unavailable.

3.3.7. Decryption of code and data sections

All sections containing the code and data have been encrypted while protecting the file, so the next step is to decrypt them, for which the following call is responsible:

;///////////////////////////////////////////////////////////////
;
; decrypt the code and data sections (it's been encrypted with
; an RC6 encryption algorithm)
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _decrypt_sections       ; decrypt sections

3.3.8. Handling of encrypted code fragments

Encrypted code fragments are only decrypted if the correct license key is present in the protected application directory, but they must be connected to the loading program code to work properly.

Execution of the code marked with ENCRYPT_START macro results in temporary decryption and passing the control to this part of the code. Execution of the code marked with ENRYPT_END macro results in re-encrypting the block between the two macros.

Thanks to this technique, the code is decrypted only for a moment necessary for its execution, which significantly reduces the possibility of rebuilding the code of such a protected application. Without the license key, code fragments between the macros are omitted.

;///////////////////////////////////////////////////////////////
;
; initialize encryption markers
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _initialize_markers     ; initialize markers

3.3.9. Application import table resolving

The original application import table was replaced by an import table for the loader during the file protection process. For the application to function properly, you need to fill in its own import table by loading all libraries listed in it and downloading the addresses of functions used by the application.

;///////////////////////////////////////////////////////////////
;
; resolve an original application import table (where WinApi function
; pointers are placed)
;
;///////////////////////////////////////////////////////////////

        push    esi                     ; LDR_INTERFACE
        call    _resolve_imports        ; fill out the application import table
        test    eax,eax                 ; 0 success, != 0 failure
        jne     _exit                   ; exit on error

If an error occurs when filling in the original import table of an application, it usually means that the application is using functions that are not available for the current operating system, for example, it is using functions contained in Windows Vista, when starting the application in Windows 2000. In this case, the application cannot be continued, because the WinApi function addresses would be set to 0 and most likely the application would cause an exception (error).

3.3.10. Return to the application entrypoint

After all steps have been taken, decrypting the code and data sections, setting procedures for encryption macros, the loader passes the control to the original application entry point:

;///////////////////////////////////////////////////////////////
;
; return to the original application entry point
;
;///////////////////////////////////////////////////////////////

        mov     eax,'1PEO'              ; original entrypoint address (as a RVA pointer)
        add     eax,[esi].hModuleBase   ; add current application imagebase
                                        ; to form a VA (Virtual Address) pointer

        pop     ebx                     ; \
        pop     edi                     ;  > restore critical CPU registers
        pop     esi                     ; /  original values

        jmp     eax                     ; jump to the original application entrypoint

;///////////////////////////////////////////////////////////////
;
; on any error exit the application without any warning
;
;///////////////////////////////////////////////////////////////

_exit:

        push    1                       ; error code
        call    [edi].lp_ExitProcess    ; terminate the application

Conclusion

The aim of this work was to present modern methods of cracking the protections used in software and to present methods of protecting software against cracking.

The final result is a sample application for software protection, using the discussed issues. The license system used is based on concepts used in commercial protection systems and can be successfully used to protect software against breaking.

The structure of the created protection application is mainly based on the C++ code, which allows for its easy expansion or complete transfer to any platform with a C+++ language compiler. The loading program, created in an assembler and saved in binary form, allows to create independent modules for different platforms and processors.

The methods of cracking the software are constantly evolving, so it is important that the protection software is constantly improved. The created protection application can be freely developed in such directions as code virtualization, improved licensing system, based on strong asymmetric encryption.

Additional improvements may include support for other executable file formats, e.g. for Linux or MacOS. Security support for 64-bit and .NET-based applications would also be a forward-looking solution.

Literature

While writing this work, I didn't once reach for any book, but my promoter insisted that I have to add something there, so here's a list of books I've never read or bought and a couple of pages to get him off the hook.

  1. [Błaszczyk 2004] Błaszczyk A.: Win32ASM.Asembler w Windows. wyd. Helion, Gliwice, 2004.
  2. [Eckel 2008] Eckel B., Flenov M.: C++. Thinking in C++. Edycja polska. C++. Elementarz hakera, wyd. Helion, Gliwice 2008.
  3. [Eilam 2005] Eilam E.. Reversing: Secrets of Reverse Engineering, wyd. Wiley Publishing, Indianapolis 2005.
  4. [Ferguson 2004] Ferguson N., Schneier B.: Kryptografia w praktyce, wyd. Helion, Gliwice 2004.
  5. [Sweeney 2002] Sweeney P.: Error Control Coding: From Theory to Practice, wyd. John Wiley & Sons, Chichester 2002.
  6. [Karbowski 2005] Karbowski M.: Podstawy kryptografii, wyd. Helion, Gliwice, 2005.
  7. [Wójcik 2004] Wójcik B.: When and how to use an assembler. Assembly programming basics, Software 2.0, 2002 nr 5, str. 20 – 26.
  8. [WWW1] opis formatu Portable Executable
  9. http://msdn.microsoft.com/msdnmag/issues/02/02/PE/
  10. [WWW2] dokumentacja WinApi http://msdn.microsoft.com/
  11. [WWW3] opis standardu Unicode http://unicode.com

  12. [WWW4] konwencja wywoływania funkcji WinApi http://msdn.microsoft.com/en-us/library/zxk0tw93.aspx

About the Author

— the author is interested in expensive watches beyond his financial reach, has a black belt in yoga, spends his time between watching Rick & Morty and pilates lessons on God knows what, apart from that he's an advocate of closed-source software, an opponent of hardened vegetable fats and an eager fan of Pepe the Frog 🐸.