Software Protection: Preventing memory modifications
Top

Technical Level : Intermediate to Advanced, intended for C++ programmers

This article discusses how to safe guard sensitive memory in your program in order to prevent runtime memory modifications, often called a "loader based attack".

A common technique used by crackers is modify the memory in another running process after it has been loaded. For example, many programs may store in memory the number of days left remaining in a trial, the number of executions remaining, or simply a value indicating if the software is running in trial / non-trial mode. By changing this value in memory after the process has been started, the cracker may be able to "upgrade" to a non-trial version. This technique is also very popular as used to cheat in video games, because it can allow a user to change their health points, ammo, etc.


How is attack preformed?

The typical technical used is to create small EXE file which:

1. Runs your protected application
2. Waits until the application reaches a specific point (or a certain amount of time has passed).
3. Locates a specific memory location where your license information is stored. Typically this is a fixed address like 0x401050
4. Writes to the license information location to change it

Because this process can be automated, the user can bypass your licensing with a click of a button.

Can I stop another process from changing my memory?

It is impossible to prevent another process from reading or writing your memory. Instead you should make your sensitive data hard to find, and also difficult to modify. Thinstall does not change the location of where your program data is stored in memory, so you will need to take your own steps to protect against loader based attacks.

Making it hard to locate and modify your license data

· Always keep your license data encrypted in memory using a key that is random for each execution of your program
· Always store your license data at a different memory address each time your program runs
· Use checksums or CRC checking to make sure your license data has not been tampered with
· Decrypt and verify your license data each time you use it


An Example:

The following C++ example demonstrates a simple program that has sensitive license information we wish to protect:

BAD - Easy to crack using loader technique

struct license_infomation
{
enum { REGISTERED, TRIAL_MODE } current_mode;
int trial_days_left;
};

license_infomation license_info;

void main()
{
get_license_info(&license_info); // this function is written by you....

while (!quit())
{
if (current_mode==license_information::REGISTERED)
do_registered_features();
else
do_trial_features();
}

}

Why this is bad: Because this program stores the license information at the same location every time it runs, this information is easy to locate by comparing the memory contents of your program before and after the registration key has been entered. Also, the values stored in memory will always be consistent (0 or 1 for current_mode), and the values are not checked for changes by the program.


GOOD - Difficult to crack using loader technique

#include " secure_memory.hpp " // The bulk of the work is preformed in secure_memory.cpp

struct license_infomation
{
enum { REGISTERED, TRIAL_MODE } current_mode;
int trial_days_left;
};


void main()
{
license_infomation tmp;
get_license_info(&tmp); // this function is written by you....
store_secure_data(&tmp, sizeof(tmp);

while (!quit())
{
license_infomation secure_info;
get_secure_data(&secure_info);
if (secure_info.current_mode==license_information::REGISTERED)
do_registered_features();
else
do_trial_features();
}

}

Why this is good: This version of the same program stores license information in memory at a random location and uses encryption, checksums, and multiple data copies to prevent an outside program from tampering with the data.

secure_memory.hpp and secure_memory.cpp

The bulk of the work for the above example is contained in secure_memory.cpp. Let's dig into the details of this file.

First, this code makes sure memory is always allocated in a different location by using the Process ID (different each time your program is run), the System Time in milliseconds, and a separate counter. The code allocates a buffer of the size request plus an additional padding size. The padding size is random for each allocation, and the data is stored immediately after the padding:

void *allocate_at_random_location(int size)
{
SYSTEMTIME t;
GetSystemTime(&t);
unsigned int pad_size=t.wMilliseconds + GetCurrentProcessId() * key_counter;
key_counter++;
pad_size=(pad_size & 0xfff)+4;
void *v=malloc(size + pad_size);
void *ret=(char *)v+pad_size;
*((void **)ret-1)=v;
return ret;
}


The supplied code also uses the checksum algorithm to verify data has not been tampered with before it is used. The checksum algorithm is very simple to implement and very fast to execute.

unsigned int calc_checksum(void *data, int size)
{
unsigned char a=0,b=0,c=0,d=0;
for (int i=0; i<size; i++)
{
a+=*((unsigned char *)data+i);
b+=a;
c+=b;
d+=c;
}
return a | (b<<8) | (c<<16) | (d<<24);
}


We also encrypt the data in memory using a simple byte-by-byte xor. The values being xor'ed against the data will also be different each time the program runs, this mean each time your program is run the data in memory will look completely different (and be located at different memory addresses).

void xor_block(void *src, void *dst, int size, encryption_key &key)
{
for (int i=0; i<size; i++)
((unsigned char *)dst)[i] = ((unsigned char *)src)[i] ^ (key.key_data[i % (sizeof(key.key_data))] + i);
}


Secure_memory.cpp also stores to copies of your data and makes sure both versions decrypt and have the same checksum value. If the checksum values do not match, it will cause your program to crash in a subtle way:

// Eventually the program should crash or have a stack overflow
// We avoid showing a dialog box or quiting immediately to make it hard to
// locate this code
inline void subtle_crash_program()
{
IMAGE_DOS_HEADER *dh=(IMAGE_DOS_HEADER *)GetModuleHandle(0);
IMAGE_OPTIONAL_HEADER *oh=(IMAGE_OPTIONAL_HEADER *)((char *)dh+dh->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
unsigned long entry=oh->AddressOfEntryPoint + (unsigned int)dh;
_asm jmp [entry]
}