10 February 2021, Kevin Watzal

I recently came across the book called Surreptitious Software which is about protecting software from being changed, copied or sometimes even understood in various ways.
Through the introduction of SaaS and the increasing number of web applications the need and knowledge of tamper-proof software decreases.
Nevertheless, there are still applications which do not run on a server or in the browser for various reasons. Especially when a license, or any other payment method is needed to use the software it is vital that the check behaves like it was intended, at least for the business of the creator.

Obfuscation

There is a difference between tamper-proofing and obfuscation. While the aim of obfuscation is to make it hard to reverse-engineer and understand the software it does not prevent modification.
However, when code is not obfuscated, it is easier to understand how it behaves and to find and remove the checks, that detect modifications (which is ironic, don’t you think?).

Hashes

One way to detect modifications is clearly building a hash or checksum over the code, because it enables simple and efficient integrity checks. But there are many pitfalls, which I will explain to illustrate what needs to be considered.
Building only one hash over the whole code and checking it on startup is easy to reverse engineer, because checks against hashes or large integer values are not common in today’s software especially on startups. Even if there are some hash checks the attacker can try to manipulate some ifs to find the correct one. Therefore, many hash checks need to be sprinkled across the code, which hash only smaller parts of the code. Then these checks can be checked by other checks. This makes finding all checks a bit annoying, but not necessarily harder.

So an algorithm was introduced which includes a nonce so that the hash value becomes 0 if the code was not changed. Not only are checks to 0 more common, in programming languages like C for example, an if condition with 0 results to FALSE, enabling further possibilities to hide the check.

Since I already mentioned C, there are further possibilities. Instead of 0, the hash could possibly return a value, which is used for a pointer to point to the correct value, or to jump to the correct execution location. When somebody modifies the code, the value of the pointer is completely random cause the application to behave like segmentation fault (core dumped).

Adding a nonce to the executable or handling with pointers is rather hard if not impossible in high-level languages like Java, C# or Python, because it may require post-compilation modification on the executable, or the language simply does not support such mad ideas.

Server side checks

A very interesting approach is to execute tamper proof checks remotely. Assuming that the application needs to communicate with a server the checks can be made there, so the attacker won’t be able to change the code (so easily).

One algorithm aims to change the code constantly. The server sends blocks of code to the client which is then being interpreted and executed. The result is then returned. If the response takes too long or does not fulfill a certain requirement the communication will be cut.
This implementation is hard to achieve and rolled out, because it would need to be stateful, lightweight for the server and enable infinite block generation.

But there are other ways like the pioneer protocol, which are applicable for devices with very low processing power like smart cards. The server sends a random nonce and the client has to return the correct hash of the code where the random value has a place where it needs to be.
The client only has a very short amount of time to return the correct answer, because it is assuming that the hardware is known and the network is fast and stable.

The last concept I want to mention is slicing functions. Some critical parts of a function is computed by a server while other resource intensive parts are kept on the client. For example license checks are most probably done on the server.

Responses

When a modification has been detected, it cannot be guaranteed that the code runs as intended and there are many ways to handle this. If the program exits immediately after detection, the reverse-engineered code can be understood better and a workaround or the check could be found more easily.
Therefore, responses should be strictly decoupled logically, timely and also in the execution path. The farther away the response is, the better.

Besides exit(1), there are other ways to make a program stop. When calculations are made, the divisor of a division could be extracted to a variable and changed to 0 to cause a DivideByZeroException. Some frequently used buttons in the UI can be set to be disabled. Some loops or recursive functions can be changed to run eternally. The network communication can be turned off. Remember the pointers to the upcoming execution path? Creativity helps a lot here.

The Attack-Detect cycle

For every way to check if the code has been modified there (soon) exists a way to prevent the application from detecting it. To close the circle, there are also many ways to prevent attacks, until a new attack has been found. For example the code could be debugged to follow the instructions and understand the code. Therefore, developers implemented checks to see if a method ran in a specific amount of time.
On Linux you can also detect if the current program is currently debugged by GDB, also there are similar ways for similar environments like the JavaScript sandbox.

When the application hashes parts of the own code this can be detected regardless of the output of the hash function, because it is very uncommon for applications to read their own code. To mitigate detecting changes Translation Lookaside Buffers (TLB) of the kernel can be adapted, because the TLB is different for data addresses and instruction addresses.
When data is being read for the hash function it can be redirected to the original code, but when instruction addresses are looked up, it can be redirected to use the modified code.

The TLB attack was detected by adding code which modifies itself. Assembly code can be embedded in C, which enables a program to include future instructions in the current one. When the wrong part of the code was used for either the read operation or the instruction the calculation would not succeed.

Self-modifying Code
   movb $1,  A+1
   movb A+1, %al
A: andb $0,  %al

Besides, reverse engineering the code and changing instructions there are other possibilities to attack an application without the need to edit code by changing the environment the software runs in. The system time could be changed, processor debuggers can be used, network traffic can be intercepted, …​ creativity opens gates again.

Downsides

Well, tamper-proofing does not come without problems. When using multiple hash checks in your code this will increase its size. If the hash is the address of a pointer it will be harder to understand the code for attackers, but also for the developers. Checking that your program is not debugged makes it thus hard to debug the executable. Reading the same code over and over again will reduce the performance for the thing the application should actually do.

When a new feature gets introduced, or a bug has been fixed the executable needs to be updated, while all the checks still need to be working correctly. It will be best to introduce a process for adding the checks after the development cycle. Gladly nowadays version control like Git will be a helpful tool for this.

Closing words

This blog post did not reveal a certain solution, but it reminds that these checks still exist and are still needed in some cases. It is also nice to be remembered how important creativity (and C code) is in developing software, but on the other hand also how well it is suited to attack software.
However, tamper-proofing software is and will probably always be a battle of detecting changes and breaking or circumventing the detection.

Tamper-proof