Chapter8 End
Chapter8 End
127
128 Chapter 8 ■ Compilers and Optimizers
This list can get even longer in more complex binaries, with numerous depen-
dencies and libraries. Looking at this output, you know that the .text section
of the executable is located at address 0x080483a0. Disassembling the code at
this location can provide a hint to the entry point of the target code. Figure 8.2
shows the result of disassembling the code at this location in gdb.
When searching for the entry point to the target code, this can depend on the
exact compiler and language used to build. You’ll see an example for finding
starting code in a C/C++ application, as that’s still one of the most common
languages used today. To begin with, look for a call to __libc_start_main.
The address of the target code will be passed as a parameter to this function,
and given what you know of calling conventions, you know that means we’re
looking for what’s put on the stack before the call.
In Figure 8.2, the address 0x804848c is pushed onto the stack right before the
call to __libc_start_main, making it a parameter to the function. Therefore,
the target code begins at that address. Figure 8.3 shows a disassembly of the
main function, including calls to libc.
130 Chapter 8 ■ Compilers and Optimizers
Figure 8.3: Main function disassembly in gdb
Compilers
Compilers take code and translate it to machine code that the processor can
read. There are various things that compilers can do to affect reverse engi-
neering, both intentionally and unintentionally. This section focuses on unin-
tentional changes; intentional techniques such as obfuscation will be covered
in Chapter 12, “Defense.”
Optimization
Compilers can be configured to optimize code based on various metrics, including
speed and disk size, or not optimized at all. The code can look very different
based on whether optimizations are applied.
Consider the following code sample. This code implements a simple if state-
ment with two conditions.
int main(int argc, char* argv[])
{
if (argc >= 3 && argc <= 8)
{
printf("valid number of args\n");
}
}
Figure 8.4 shows what the code looks like in a disassembler (more on this in
Chapter 11, “Patching and Advanced Tooling,” don’t worry) when compiled
with no optimizations. Note that the checks for the two conditions comparing
the values to 2 and 8 are clearly visible in the code.
Chapter 8 ■ Compilers and Optimizers 131
Figure 8.5 shows the same code when optimized for speed and space.
The comparisons with the values 2 and 8 are no longer visible in the code,
and the code no longer looks like an if statement with two conditions.
Figure 8.6 shows the code optimized solely based on disk space. Again, the
two comparisons are missing.
If you examine the code, you’ll see that the code checks if (argc- 3) > 5. If
argc < 3, then subtracting 3 will cause an underflow and cause the value in
eax to be a large positive number. If argc > 8, then argc- 3 > 5. In both of these
cases, the result will be greater than 5, so the optimized statement is equivalent
to the original test. Compiler optimizations result in equivalent logic, but they
can make code much more difficult to read and reason about.
Most compilers have options for setting the level of optimization. While you’re
learning, if you’re having difficulty reversing an application you’ve written, try
disabling optimizations when compiling. On the flip side, if you want to make
your code more difficult to reverse engineer, compiler optimizations are an easy
and beneficial way to do so.
132 Chapter 8 ■ Compilers and Optimizers
Stripping
Stripping a binary means removing all information that is not necessary for the
code to execute, including the symbol table. An unstripped binary retains its
symbol table, while a stripped one does not.
Symbols can be extremely useful for debugging an application. For example,
consider the following code:
// Declare an external function
extern double bar(double x);
If a file is stripped, it will show that no debugging symbols are found when
opened in gdb, as shown in Figure 8.1. These files are much more difficult to
reverse engineer.
Symbols can be stripped from an application in a few different ways. One
option is to use compiler flags, such as gcc –fno-rtti –s. Another option is to
use post-build stripping tools, such as strip in Linux.
Symbols make it easier for an attacker to reverse engineer an application
because they can help with locating areas of interest and understanding the
intent behind certain variables. However, there are legitimate reasons to leave an
application unstripped. For example, symbols help with creating crash reports
and error logs and support legitimate debugging to fix client errors. While
learning, if you are writing your own code and compiling it to practice with, start
by making sure you’re building with symbols left in. As you progress in your
skills, then remove symbols. When reverse engineering someone else’s code, it’s
highly unlikely you will find symbols have been left in it, but it does happen!
Linking
Applications are rarely written in isolation anymore. What’s more common is
to include libraries that provide core pieces of capabilities (such as communica-
tions, logging, drawing, etc.). When compiling an application that uses libraries,
there are two options for how those get built. These libraries can be statically or
dynamically linked into the application. Each has its benefits and drawbacks
from a software cracking perspective.
Static Linking
With static linking, libraries are built into the application itself. This improves
the speed of execution because the target addresses of any calls to the library
are built into it at compile time. Also, statically linked applications are more
portable because they have fewer dependencies on the environment.
However, static linking also has its downsides. Statically linked applications
are larger because the entire library is built into the executable, even if you use
only one function from a large library. Additionally, any updates to the library
require recompilation of the applications using them.
The file bloat caused by static linking can be significant for programs. For
example, as shown in Figure 8.8, even a simple one-line “hello world” program
will link dozens of libraries.
Dynamic Linking
Dynamic linking is the other option and the default choice for many compilers.
With dynamic linking, the required libraries are located on the system at runtime.
Chapter 8 ■ Compilers and Optimizers 135
If a library is not already loaded into system memory, the library must be found
on the system and loaded into the shared library memory; however, common
libraries are likely already loaded and available for use.
Dynamic linking reduces application size and eliminates the need to recom-
pile an application after a library update if the update is backward compatible.
Additionally, dynamically linked applications can be faster at load time if the
libraries that they use are already loaded into memory.
However, dynamically linked applications depend on the libraries that they
need being installed on the system and can be slower than statically linked ones
(if dependencies are not already loaded and need to be located and loaded). In
addition to the need to load any libraries not already in memory, dynamically
linked applications need to find the address of called functions at runtime. This
involves searching the shared memory space for the library and may require a
great deal of memory paging.
need to search for the desired library in the shared library memory and locate
its address every single time, as it will move and be unpredictable.
Crackers, on the other hand, tend to prefer dynamically linked libraries.
Dynamic linking results in much less code to sift through, and crackers are
interested solely in an application’s custom code, not the shared library code.
Summary
The process of compiling and optimizing an application can make it much more
difficult to reverse engineer even if the compiler isn’t intentionally obfuscating
it. However, like any anti-reversing protection, this can only slow down the
process since no software is uncrackable.
CHAPTER
9
Reverse Engineering: Tools and
Strategies
Up until this point, the focus of this book has been on understanding how the
guts of computers work. This is essential to being an effective software cracker.
Now that you have the foundation, the focus shifts to the art of software
cracking. To experiment and practice cracking, you’ll work with a variety of
targets:
■■ Real software: Software taken from the real world. When analyzing real
software, you must take into account copyright law to ensure no copyright
violations.
■■ Manufactured examples: Applications written for this book to illustrate
specific concepts.
■■ crackmes: Small crackable programs written by other software crackers
to demonstrate an idea and challenge others.
crackmes like those used in this course are manufactured examples that pro-
vide a few benefits to an aspiring cracker. In general, they are designed to be
solvable, legal to crack, and safe to run in a debugger.
crackmes are also often labeled based on their focus, level of expertise, etc.
As a result, you can specifically seek out challenge problems suited to your
interests and skill level (i.e., advanced C cracker versus beginner Java cracker).
137
138 Chapter 9 ■ Reverse Engineering: Tools and Strategies
Lab: RE Bingo
This lab provides hands-on experience in reversing code that has been built
(and obfuscated) by a compiler.
Labs and all associated instructions can be found in their corresponding
folder here:
https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES
For this lab, please locate Lab RE Bingo and follow the provided instructions.
Skills
This lab uses objdump to practice identifying control flow constructs and com-
piler settings when reversing. Some of the key skills being tested include the
following:
■■ Reverse engineering x86
■■ Control flow constructs
■■ Impact of compiler settings
Takeaways
Quickly identifying control flow constructs can massively speed up reverse
engineering. They provide insights into the logic of an application and make it
more readable and comprehensible.
However, compiler configuration has a significant impact on the speed of
reversing. For example, stripping and optimizing, in general, slow things down.
In larger and more complex programs, automating some reverse engineering
is often necessary. It is common to write custom tools for a specific target.
Unpacking, deobfuscating, and circumventing anti-debug checks are common
tasks for automation.
Basic REconnaissance
As a software cracker, these are the most common situation that you’ll face:
■■ You want to crack a program.
■■ You have no source code.
■■ You have an executable.
In this situation, you need a means of quickly assessing the target executable
and finding a starting point for your analysis. Some of the most commonly used
Chapter 9 ■ Reverse Engineering: Tools and Strategies 139
initial tools for reverse engineers are objdump, strace, ltrace, and strings.
You’ll see more advanced tools as you progress through the book, but as these
are some of the most foundational, they’re a good starting point.
objdump
Object Dump (objdump) is a Linux-based tool for dumping the disassembly of
any program. As shown in Figure 9.1, it has numerous options. The most impor-
tant ones for quick reverse engineering include the following:
■■ -d: Instructs objdump to disassemble the content of all sections
■■ -Mintel: Tells objdump to display assembly in Intel syntax (as opposed
to AT&T)
For example, to disassemble an application named appname, use the command
objdump –d –Mintel appname.
Figure 9.2 shows the output from running objdump on a sample application.
Note that objdump will display memory locations, function names, x86 machine
code, and x86 assembly.
ltrace
ltrace (library trace) is a Linux command-line utility that traces library calls.
Library calls are calls by your application into dynamically linked libraries. The
syntax of the command is ltrace <command>.
For example, if you #include <stdio.h>, that library gets dynamically linked
when your program loads. When you call printf or fopen, that is calling into
the standard C library. This construct holds true for all programming languages,
which all include a notion of including external libraries.
strace
strace (system trace) is a Linux command-line utility that traces system calls.
The syntax of the command is strace <command>.
System calls are calls by your application into the operating system, which
manages things like files and your console window. Functions like fopen and
printf eventually, in their inner workings, must make calls into the operating
system. Just like with ltrace, this holds true for all programming languages;
it’s rare for an application to exist that doesn’t utilize OS-level functionality.
This image is complex and can be a lot to decipher. Looking through the
result, you can see some standard system calls at the beginning that are used
to get the echo program up and running.
The following lines are the interesting output, which are found at the very end:
write(1, "hello!\n", 7hello!
) =7
close(1) =0
This says that echo wrote a string to stream 1, which, remember, is stdout.
The write command had a return value of 7 because seven characters were
written. Finally, echo closed stream 1, which returned 0 for success. While this
seems simple, imagine using this to track where an application wrote a piece
of configuration data. Say you change a setting and want to see how it stores
that on the system.
However, analyzing the code in strace tells a different story, as shown here:
deltaop@deltaleph-
ubuntu:~$ strace ./kittens
...
poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
send(3, "!$\1\0\0\1\0\0\0\0\0\0\7kremlin\2ru\0\0\34\0\1",
28, MSG_NOSIGNAL) = 28
poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
ioctl(3, FIONREAD, [28]) =0
recvfrom(3, "!$\201\200\0\1\0\0\0\0\0\0\7kremlin\2ru\0\0\34\0\1", 1024,
0, {sa_family=AF_INET, sin_port=htons(53),sin_addr=inet_
addr("192.168.1.1")}, [16]) = 28
close(3) =0
socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(53),
sin_addr=inet_addr("192.168.1.1")}, 16) = 0
...
This sample output from strace shows multiple events. To focus on events
of interest, use grep (which limits results to lines that match your search string,
in this case connect).
Chapter 9 ■ Reverse Engineering: Tools and Strategies 143
deltaop@deltaleph-
ubuntu:~$ strace -
f ./kittens 2>&1 | grep connect
The previous sample output looks for events with the word connect in them.
This includes multiple Internet connections, including one to 195.208.24.91,
which is suspicious as it’s an external IP address, and why would your cursor
need to do that?
strings
strings is a Linux utility designed to extract the printable strings used by an
application. It looks for a series of ASCII printable characters with a (configu-
rable) minimum length and prints any that it finds.
strings can be very useful in reverse engineering because it provides a
high-level understanding of the sorts of things that a program might do. Also,
once you find strings of interest, you’ll see later how you can use those strings
to easily locate the associated piece of code. For example, a string that says
"incorrect password" can be used to quickly trace where the password handling
code is. For example, the following strings provide valuable hints about an
application:
■■ "Enter password:"
■■ "open_socket"
■■ "YOUR FILES HAVE BEEN ENCRYPTED!"
The syntax of the command is strings program. While it is commonly used with
no options, the following flags are sometimes useful when reverse engineering:
■■ -a: Show all strings in the file, as opposed to only those in the loaded sec-
tions of object files. This is often useful when dealing with obfuscated,
nested, or otherwise unusual binaries.
■■ -n: Specify the minimum length of successive printable characters for a
sequence of bytes to be considered a string. The default is 4. It is often
useful to expand or limit the number of strings found by the tool.
144 Chapter 9 ■ Reverse Engineering: Tools and Strategies
Dependency Walker
Dependency walking is a technique used to quickly understand the imports
and exports of an application. Dependency Walker is one example of such a
tool. (See the “Tools” section of our repository for links.)
Dependency walking provides a valuable, high-level view into what actions
a program will perform and is often a useful first step in cracking. Most appli-
cations don’t implement all their own functions; they will use functions from
the operating system, or external libraries. Each time an application reaches
outside of its code, that will show up as an imported function. Also, often
applications will share functionality with other applications, and anytime a
function is something “available to be shared,” it will show up as an export of
the application.
Loading a program into a program like Dependency Walker shows the DLLs
that it uses and the API calls it is expected to make. Figure 9.5 shows that the
program will create several registry keys.
Summary
This chapter introduced some of the core tools and techniques that you will use
as a software reverse engineer and cracker. Before moving on, take some time to
practice and get some hands-on experience using the tools. This practice time
will be invaluable later when you move on to more complex software and more
advanced RE and cracking techniques.
CHAPTER
10
Key Checkers
One of the most common practices for licensing software is through license keys.
In a goal to defeat piracy, every installation of the software requires a unique key
to complete the installation. In the case of software with multiple tiers of fea-
tures, they may have some features always freely available, while others reside
behind a license wall, or the software may not work at all without a license key.
License keys are a common anti-piracy solution, and they have their advan-
tages. These are two of the most significant:
■■ License keys are easy to generate and verify.
■■ The ratio of valid to invalid keys is so small that random guessing is
unlikely to generate a valid key (assuming a reasonable key length).
147
148 Chapter 10 ■ Cracking: Tools and Strategies
However, like all security, if they are implemented poorly, they can be highly
susceptible to cracking, and like all security, they are not entirely infallible.
A sufficiently knowledgeable and motivated cracker could eventually defeat
or bypass them. However, they’re still one of the stronger forms of protection;
this is just a reminder that there is no such thing as 100 percent secure software.
Back in the day when offline systems were more common, license checking
and validation were often done entirely offline, meaning all of the logic to verify
the key was resident on the system. Now, with prolific connectivity, we often see
license key checks that consist of both an offline and online component, where
they reach out to a license server for additional verification. There are a few
different ways to implement key checks with varying levels of effectiveness.
A Reasonable Way
A brute-force attack against a license key is guaranteed to work. . .eventually.
The best that a license key can do is waste enough of a cracker’s time that it
becomes infeasible or impossible to carry out a brute-force attack.
So, how to protect against brute-force attacks? One common option in other
contexts is a cryptographic hash. For example, a license key could be imple-
mented using one of the following options:
■■ Username: SHA(username)
■■ Random value: WXYZ-SHA(WXYZ)
The use of a hash function makes a brute-force attack against this much harder.
However, it’s trivially easy for a cracker to determine how the algorithm works
after a look at the code. Depending on your mindset, if you’re an attacker, this
means leveraging the reverse engineering skills you’ve learned to this point to
find the algorithm and unravel it, and if you’re a defender, it means this is a
key piece of code that you need to protect.
An alternative is to use a custom, complex hash rather than a standard one.
While this is normally a horrible idea in security, it’s not an unheard-of choice for
this application. The goal isn’t to provide absolute protection, just to slow down
reverse engineering. For anyone in the security space whose toes are curling at
the suggestion of making your own hash, just note that this suggestion comes
with the caveat that you are able to make a decently good one. As a defender,
keep in mind there are lots of tools out there to do common hashing techniques,
so those will be all the first things an attacker will try to unroll your key.
Also, find ways to add unique complexity so a key can be used only in a
unique setting, and not proliferated. Schemes such as concatenating the product
name and version and computer name within the hashed value adds a solid
level of complexity. This way, a cracked valid key for one installation doesn’t
unlock other releases.
A Better Way
Hashes are better, and, if implemented correctly, they can be decent. But there
are even better options. A great example of this is the approach Microsoft uses
when generating license keys for its software.
Instead of hash algorithms, Windows uses public key cryptography. With
public key cryptography, a digital signature can be generated using a private
key and verified using a public one. This means that a digitally signed license
key can be verified by an application without exposing sensitive keys.
150 Chapter 10 ■ Cracking: Tools and Strategies
When generating its license keys, Windows uses a lot of information about
the software, including but not limited to:
■■ Bitness (32, 64)
■■ Type (home, professional, enterprise)
■■ Product ID
■■ Hardware features
Including all of this information helps to lock a product key to a specific
installation of the software. If you’re interested in more information on the pro-
tocol, there are lots of resources online tearing into Microsoft’s key generation.
Other Suggestions
The methods introduced align with more of the industry best practices and the
most commonly used methods. But there is not a one size fits all to security,
Chapter 10 ■ Cracking: Tools and Strategies 151
and some of the following are techniques you could encounter in a cracking
scenario, or you might find them useful in a defensive scenario if you have
unique constraints.
Key Generators
If a piece of software uses a key for activation, crackers will want to build a
key generator for it. This is true regardless of which type of key activation you
did. Key generators are then distributed for people to generate a “free” key for
software.
You’ll see later how to patch software to simply remove a key check, so for
now focus on making a key generator, and assume you can’t just bypass the key
check. Key generators typically require a more in-depth analysis of the program
and a deeper understanding of the key algorithm.
152 Chapter 10 ■ Cracking: Tools and Strategies
computer name, etc. But the idea is something is going into a transformation to
come up with a result. And that result is compared to the input key, which has
also gone through some type of transformation (note this transformation could
be nothing, meaning the result is simply the key, or it could be more hashing
or mutation). With this model in mind, there are a few potential variants of
key checks.
Going back to the initial StarCraft/Half Life example, u would actually be the
first 12 digits of the key, and k is the last digit. In this setup, there is no username
entered; rather, part of the key is used to check the other part.
Another option is that u, and therefore f(u), is a constant (i.e., hard-coded
keys). In this setup, there is no username entered; rather, the key is transformed
and checked against a fixed value. For example, “the sum of all of the digits in
the key is equal to 1337.”
To crack this type of key check, reverse engineer g and derive g-1. Often, this
.
For this lab, please locate Introductory Keygen and follow the provided
instructions.
Skills
This lab practices the use of objdump and the strings utility to generate a
keygen. Some of the key skills it tests include the following:
■■ Initial reconnaissance
■■ Reverse engineering x86
■■ Key generation
Takeaways
In addition to modifying a program, it’s often possible to crack a program just by
observing how it works. The right approach is often determined by the program
constraints, and choosing which to use is an important skill.
Procmon
In reverse engineering, you want to learn as much about how the program
works as possible. Before jumping to super-fancy debugging, start easy by just
observing software’s behavior.
Procmon is a tool distributed as part of the Sysinternals suite of tools (avail-
able at http://technet.microsoft.com/en-us/sysinternals/bb842062). This
repository contains about 60 windows utilities made and freely distributed by
Microsoft. Note these tools work only on Windows OSs.
Example: Notepad.exe
Try taking a look at what notepad.exe does when you create a new file, change
the font, and then save some content. To do so, take the following steps:
1. Open Procmon.exe.
2. Launch Notepad.
156 Chapter 10 ■ Cracking: Tools and Strategies
To see only events related to the process Notepad.exe, define a filter stat-
ing that the Process Name is Notepad.exe, as shown in Figure 10.3. You can
accomplish this via the following steps:
1. Select Process Name from the Column list box.
2. Select is from the Relation list box.
3. Type Notepad.exe in the Value text box.
4. Select Include from the Action list box.
5. Click the Add button.
6. Click Apply and OK.
Chapter 10 ■ Cracking: Tools and Strategies 157
If Notepad has saved values to the Registry, it will create an event entry of type
'Operation' 'RegSetValue'. By right-clicking entries in Procmon’s log, you can
choose to include or exclude certain types of events, as shown in Figure 10.5.
This enables you to further refine your results and focus on events of interest.
Figure 10.6 shows a Procmon entry that seems to be related to the changes to
the font in Notepad. To see more information, right-click the entry and select
Properties.
158 Chapter 10 ■ Cracking: Tools and Strategies
Figure 10.7 shows the properties of the event. In the Data field, you can see
the text “Webdings,” indicating that this is an event triggered by changing the
Notepad font to Webdings.
Call Stacks
The Properties window for an event has a few different tabs. Clicking over to
the Stack tab shows the sequence of calls used to reach this point, as shown in
Figure 10.8.
Looking further down this stack trace, it’s possible to see the point where the
program left notepad.exe, as shown in Figure 10.9. This transition point from
application to libraries might be a good starting point for reversing.
File Operations
Procmon also records events for file operations, such as opening, closing, and
editing files. Figure 10.10 shows an example of this.
Chapter 10 ■ Cracking: Tools and Strategies 159
These file events can provide useful information for reversing. For example,
they can help with identifying and analyzing configuration files, export functions,
and proprietary file formats.
160 Chapter 10 ■ Cracking: Tools and Strategies
Figure 10.9: Stack trace for notepad.exe
Registry Queries
The Notepad.exe example showed how to find the Registry operation for chang-
ing the font in Notepad. However, this isn’t the only possible use for registry
queries.
For example, Figure 10.11 shows that Notepad looked for two keys with the
word “Security” in them but couldn’t find them. You could add these keys to
your Registry and place custom values in them to change how Notepad operates.
Resource Hacker
Resource Hacker (also known as ResHacker or ResHack) is a free extraction
utility or resource compiler for Windows. Resource Hacker can be used to add,
modify, or replace most resources within Windows binaries including strings,
images, dialogs, menus, and VersionInfo and Manifest resources. (For tool links,
visit the tools section of our GitHub site at https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES.)
Chapter 10 ■ Cracking: Tools and Strategies 161
Resource Hacker can be a useful tool for exploring the structure of a binary
prior to the cracking process. It can be used to find and understand the structure
of nag screens, key entry screens, help menus, and more.
Resource Hacker can also be used to add functionality to a program before
or after cracking. For example, it’s possible to add new icons, menus, and skins
to an existing application.
To get started, open an .exe file in ResHack to explore its strings, images,
dialogs, menus, etc., as shown in Figure 10.12. Then, click an item in ResHack
(left) to show how that item would look in the application (right).
Figure 10.12: Sample application in Resource Hacker
Example
Suppose you see the window shown in Figure 10.13 in a program. As a cracker,
you want to understand how that window would be used by the program.
To find out, open the program in ResHack. Then, use Ctrl+F to search for one
of the strings used in the dialog box, as shown in Figure 10.14.
162 Chapter 10 ■ Cracking: Tools and Strategies
The main Calculator window may not be the first result. Keep on searching until
you find the code defining the Calculator dialog box, as shown in Figure 10.18.
In Figure 10.18, the CAPTION string determines the title on the application
window. Change this string to rebrand the application as your own.
After changing the CAPTION, click the green arrow button shown in
Figure 10.19. This will compile the modified Calculator application.
After the application has been compiled, the updated version of the window
should be shown in the window preview. This should include the modified
caption, as shown in Figure 10.20.
Compiling the application doesn’t automatically save the modified version.
To do so, select File ➪ Save, as shown in Figure 10.21.
164 Chapter 10 ■ Cracking: Tools and Strategies
Patching
Patching involves modifying a compiled binary to modify code affecting its
execution. Depending on the situation, sometimes the easiest thing to do is
patch an application to circumvent its security.
In these situations, you may choose to fall back on key generators instead.
Otherwise, patching a program to remove its key checks (or any other logic you
want to avoid) is often the easier approach, when possible.
Where to Patch
Patching can be done in two different places: in memory or on disk.
Patching in memory modifies the machine code in memory. This is use-
ful for reverse engineering attempts because you may need to try dozens (or
hundreds. . .or more. . .gasp) of things before one works. In-memory patching
affects only the current execution of the application. Each time you restart the
application, any in-memory patching will be lost.
Patching on disk modifies the machine code in the compiled binary. This
is useful once you know what works and affects all future executions of the
application. It makes the modifications persistent and will be there every time
the application is launched.
NOPs
Recall the instruction nop. It is a one-byte instruction (0x90) that does nothing.
When patching applications, it is critical to not move the code. In fact, mod-
ifying the size or simply deleting code will crash the application. To remove
sections of code yet maintain the same size, fill the space with nops.
For those of you who are curious why simply deleting code doesn’t work,
there are many factors to this, but the most important is that some x86 code is
relative and some of it is absolute references. Looking at the relative case first:
this means some code translates to relative things like “jump forward 40 bytes
from where I am now.” In cases like this, if you remove code between the jump
and its destination 40 bytes away, you’ve messed up the jump. It will continue
to jump 40 bytes ahead, except that now it may land in the middle of an opcode
or skip critical instructions, which then results in a crash. If the code you remove
is outside of that 40-byte bubble and the jump forward 40 bytes still lands in
the same spot, then it would have no effect.
Now, consider absolute references. These types of references would look like
“use the data value at address 0x1234567.” If you remove code anywhere in
the binary before that address, you’ve caused everything to shift. So, when any
absolute reference goes to grab its values or perform an absolute jump, all of the
locations will be wrong, even if all you did was remove 1 byte from the binary.
This means relative references are affected only by adding/removing bytes if
they occur in between where the reference is made and the destination. How-
ever, all absolute references are destroyed if you shift the application even by
Chapter 10 ■ Cracking: Tools and Strategies 167
1 byte. This is why it’s critical in patching to maintain the size (unless of course
causing everything to crash is your goal, in which case smash away!).
Circling back to nop, if you want to remove a piece of code, such as causing
software to skip a key checker, instead of deleting the code, you simply replace
it all with nops. This maintains the application’s byte alignment but causes
nothing to happen when it reaches the undesirable code.
Other Debuggers
For reverse engineering with dynamic analysis on Windows, there are numerous
popular choices. Here are a few:
■■ OllyDbg
■■ Immunity
■■ x64dbg
■■ WinDbg
Which of these to use depends on the situation and user preference. All of
them have similar features, and skills in one typically translate to the others
as well. You’ll dip your toes into a few different pieces of software throughout
the book; the goal is to give you a taste of many so you can get a feel for when
each is useful.
OllyDbg
OllyDbg is an immensely popular and powerful debugger. While most debug-
gers focus on debugging, Olly has extended features, including the following:
■■ Extensibility, plugins, scripting
■■ Execution tracing system
■■ Code patching features
■■ Automatic parameter descriptions for most Windows functions
■■ Emphasis on binary code analysis (i.e., not based around source debugging)
■■ Small and portable
These features make OllyDbg excellent for the following:
■■ Writing exploits
■■ Analyzing malware
■■ Reverse engineering
168 Chapter 10 ■ Cracking: Tools and Strategies
However, while OllyDbg is a powerful and popular tool, it does have its
limitations. One of these is that it works only for 32-bit executables, which
admittedly are a dying breed but not dead yet.
The other is that the OllyDbg interface often takes some getting used to and
does not feel robust or intuitive at first. However, you should definitely stick
with it, as it is a powerful dynamic analysis tool.
Immunity
Immunity is a fork of OllyDby, meaning that it has many of the same capabil-
ities. It also introduces many additional features that make it popular for exploit
developers, such as support for Python scripting.
However, like OllyDbg, Immunity can be used only to debug 32-bit execut-
ables. Also, it inherits OllyDbg’s unintuitive user interface.
x86dbg
x86dbg is a replacement for OllyDbg that supports both 32-bit (x86dbg) and
64-bit (x64dbg) applications. This wider support means that it is commonly the
tool of choice when reversing or debugging 64-bit applications.
WinDbg
WinDbg is a debugger that is universally applicable, has strong support, and
offers excellent debugging symbol support (but which is less useful with RE).
However, it has a debugging focus and lacks some features of RE-focused tools.
Immunity: Assembly
Figure 10.23 shows a program’s disassembly in Immunity. Note that it shows
the memory address, machine code, and x86 assembly.
Chapter 10 ■ Cracking: Tools and Strategies 169
To select a line of code, click it. Once a line is selected, Immunity offers var-
ious keyboard shortcuts, including the following:
■■ ;: Add a comment to the selected line. This is the most important part of
reverse engineering; it helps you keep track of your work.
■■ ctrl-a: Auto-analyze the program. Immunity can do a fairly good job of
adding comments and guessing function parameters.
■■ <enter>: Navigate to the selected function. For example, if you see the
assembly call 0x1234 and want to find out what the function at 0x1234 does.
■■ -: Go back to the previous location. For example, after you’ve analyzed
function 0x1234 and want to return to where you were.
■■ +: Go to the next location (after pressing -). For example, if you returned
to the calling function with -, but then want to go back to function 0x1234.
■■ ctrl-r: Find cross-references to the selected line. For example, if you have
a string selected in the memory dump window and want to know who
uses that string; or if you have the top of a function selected in the disas-
sembly and want to find out who calls that function.
■■ Double-click address: Set a debugging breakpoint at this address.
Immunity: Modules
In Immunity, you can load the list of executable modules by pressing the e button.
This shows all the code—including dynamically loaded libraries—that you can
debug, as shown in Figure 10.24. After opening the list, you can double-click a
module to go to that code.
When you start Immunity, see what module you are currently looking at
by checking the eip register. In nearly every case, you will want to start by
debugging the main executable, not a shared library like ntdll. You can use
the modules window to switch to the main executable.
Immunity: Strings
It is often useful to find what code is using a certain string in the executable. To
find all the strings that a program is using, right-click and select Search For ➪
All Referenced Text Strings, as shown in Figure 10.25.
In the strings window, right-click and select Search For Text to find a specific
string, as shown in Figure 10.26. Then, right-click again, and select Search For
Next to find the next reference to that string. You can double-click a string’s
address to go to the location where it is used in the disassembly.
Chapter 10 ■ Cracking: Tools and Strategies 171
After execution has been halted by a breakpoint or the pause button, you can
click Step Into to progress the program one instruction, as shown in Figure 10.28.
Alternatively, if you are stopped on a function call but already know or do not
care about what the function does, click overstep Over, as shown in Figure 10.29,
to continue debugging after the function returns.
Immunity: Exceptions
Many applications generate exceptions as part of normal execution. For example,
a try {} except {} block will generate an exception if anything goes wrong in
the try block. As a debugger, dynamic analysis tools like Immunity typically
intercept the exception first to see if you want to do anything with it.
But for reverse engineering, you generally don’t want to interfere with normal
execution. Instead, you want to let the application handle the exception the way
it normally would. This means you almost always want to pass the exception
from the debugger to the application.
As shown in Figure 10.30, exceptions are reported at the bottom of the Immu-
nity window, but each debugger is slightly different. In Immunity, press Shift+F9
to pass the exception and continue execution.
174 Chapter 10 ■ Cracking: Tools and Strategies
In your first cracks, you will use the process of “noping” out code to remove
it from the program. This involves replacing program instructions with nop
instructions.
To do so in Immunity, first select the instruction(s) that you want to remove.
Then, right-click and select Binary ➪ Fill With NOPs, as shown in Figure 10.31.
This will replace the selected instruction(s) with a series of nops, as shown
in Figure 10.32.
After modifying the program, test the patch by rerunning the program. If you
patched the correct portion of code, you should find that the nag screen (key
check, etc.) has disappeared.
However, if the patch crashes or failed to remove your target, you can easily
revert your changes and try again. To do so, select the patch button to bring up
the patches window. Then, right-click your patch and select Restore Original
Code, as shown in Figure 10.33, to revert your patch and try again.
Once you have identified a working patch, save your changes to the executable
to make it permanent. As shown in Figure 10.34, right-click and select Copy To
Executable ➪ All Modifications. When a confirmation window appears, select
Copy All.
A modified executable window should appear, showing your changes. Close
the window, and select Yes to save your file. Give your file a new name, such
as cracked.exe.
If you are confident in your modification, you can run cracked.exe directly.
If you want to keep debugging with these new changes, you’ll need to reload
cracked.exe into Immunity.
176 Chapter 10 ■ Cracking: Tools and Strategies
Figure 10.33: Reverting modified code in Immunity debugger
For this lab, please locate Lab Cracking with Immunity and follow the provided
instructions.
Skills
This lab practices reverse engineering, patching, and circumventing software
protections using Immunity and Resource Hacker. Some of the key skills tested
include the following:
■■ Reverse engineering x86
Chapter 10 ■ Cracking: Tools and Strategies 177
■■ Patching
■■ Static versus dynamic analysis
Takeaways
Software can be easily modified to add, change, or remove functionality. These
same techniques can be used to circumvent anything from trivial to advanced
protections, as long as you understand how the software works.
Summary
Key checkers are intended to protect against the distribution and use of unli-
censed and cracked copies of software, but no defense is perfect. Tools like
Procmon, Resource Hacker, and debuggers can be used to understand these
defenses and defeat them through the use of key generators or patching.
CHAPTER
11
The previous chapter introduced software cracking and patching. This chapter
provides a more in-depth look at patching and some of the more advanced tools
that can be used for reversing and cracking.
179
180 Chapter 11 ■ Patching and Advanced Tooling
■■ Signed/unsigned int
■■ Signed/unsigned int64
■■ Float
■■ Double
■■ Variable name
■■ Variable value
You can jump to a specific address if you know where you need to go, as
shown in Figure 11.4. This location of “where to go” can be specified as a byte,
line number, sector, or short.
In 010 Editor, you can directly modify the hex. Simply place your cursor and
start typing to overwrite.
However, 010 Editor understands how important it is to maintain file size.
When you type values, in 010 Editor it overwrites existing values at that location.
It does not insert them, which would make the file larger.
CodeFusion Patching
After a researcher figures out how to crack a program, the next step is often
to create a patcher/cracker utility. This will allow others to crack the same
program.
CodeFusion is a popular patch generator. It creates a stand-alone execut-
able file that can be used to crack a specific application. (Find links in the
tools section of our GitHub site here: https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES).
To start creating a patcher, launch CodeFusion, and configure the information
that will appear when the patcher is launched. This information is shown in
Figure 11.5 and includes the program caption, program name, comments, icon,
etc. These can be whatever you want.
On the next screen, add the files to be patched, as shown in Figure 11.6. This
is the executable that you want to crack.
Chapter 11 ■ Patching and Advanced Tooling 183
Next, add the patch information by clicking the + icon shown in Figure 11.7.
This is typically the information you learned from Immunity, Cheat Engine,
IDA, etc. It usually includes an offset to patch, and the bytes to replace. Often,
the bytes to patch with are 0x90 (nops). On the next page, click Make Win32
Executable to create an EXE file to patch the target application.
Cheat Engine
Cheat Engine is a popular and powerful open-source memory scanner, hex editor,
and debugger. While the tool is primarily used for cheating in computer games, it
can also often be valuable for quick dynamic analysis in software cracking. (Find
links in our tools section on our GitHub here: https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES).
Cheat Engine enables searches for values input by the user with a wide
variety of options. These allow the user to find and sort through the com-
puter’s memory.
Takeaways
A variety of tools are available for reverse engineering and cracking; choosing
the “right” one depends on the challenge at hand and personal preference.
Crackmes are a (usually) safe, always legal, incredibly addictive way to prac-
tice your cracking skills.
IDA Introduction
If you’ve ever googled reverse engineering tools, IDA is guaranteed to come
up. It’s the Cadillac of reverse engineering tools.
IDA, aka the Interactive Disassembler, allows for binary visualization of dis-
assembly. It is available under a freemium model where limited features are
available for free, while some of the more powerful features (or more obscure
architectures) require a paid license.
Figure 11.16 shows the process of loading a new file in IDA. IDA automatically
recognizes many common file formats, but if it gets it wrong, you can select
the generic Binary File. IDA also offers a Processor Type drop-down menu to
change architectures.
One of IDA’s greatest strengths is its graph view, which shows a visual rep-
resentation of an executable’s x86 assembly and control flows. Figure 11.17
shows this view and some of the most useful components of it, including a
memory map of the executable, a list of functions, the logic block view, and
a graph window.
Chapter 11 ■ Patching and Advanced Tooling 191
Figure 11.19 shows the full list of strings in IDA. IDA shows the text of the
string itself, its address, and its predicted length.
Click a string to highlight it. Then, press X or right-click and select Jump To
Xref To Operand. This will open up a window showing all of the locations where
the string is used in the program, as shown in Figure 11.20.
Following one of these cross-references will show the disassembly where the
string is used. As shown in Figure 11.21, IDA understands how string references
work. When it sees one, it shows the string as a comment.
IDA: Comments
When reversing an application, it’s essential to be able to track what you’ve
figured out and done so far. In IDA, pressing ; opens up a box to enter com-
ments, as shown in Figure 11.26.
One tip is to put an identifier like “_x” in all of your comments. This gives
you something to search for to find all comments.
To start a search for comments, select Search ➪ Text, as shown in Figure
11.27.
Then, search for “_x” while selecting Find All Occurrences to find all of the
comments that you’ve placed in the program.
By using a consistent commenting style and searching for comments, it’s easy
to find places in the code that you’ve already explored. For example, as shown
in Figure 11.28, you can quickly identify locations that were marked “TODO”
for later analysis.
Chapter 11 ■ Patching and Advanced Tooling 197
Figure 11.28: Search results in IDA
IDA: Paths
IDA shows three types of paths between basic blocks:
■■ Red: Path taken if a conditional jump is not taken
■■ Green: Path taken if a conditional jump is taken
■■ Blue: Guaranteed path (no conditionals)
For example, consider the following code sample containing a simple if
statement:
int main(int argc, char* argv[])
{
if (argc > 1)
return 0;
return argc;
}
Figure 11.29 shows how this code would look in IDA. After the conditional
block, the paths diverge. The colors aren’t shown in this book, but the left path,
which is red in IDA, shows what happens if the jump is not taken. The right
path, which is green in IDA, is followed if the conditional resolves to false.
Below this point, several more arrows indicate transitions between basic
blocks. Since none of these involves conditionals, they will all be blue in IDA.
IDA Patching
IDA is another tool that can be used to patch executables. As an example, con-
sider the following code:
printf("please enter the password\n");
scanf("%s", user_entered_password);
if (strcmp(user_entered_password, correct_password) == 0)
{
printf("SUCCESS\n");
}
else
{
printf("Failure\n");
}
this jnz to a jz (0x74) will reverse the logic, causing the application to accept
only incorrect passwords. With the logic flipped, an incorrect password would
result in success and a correct one would result in failure.
Skills
This lab provides practice using IDA to reverse engineer control flow graphs.
The goal is to learn to quickly identify high-level coding constructs based
on their control flow patterns.
Takeaways
Analyzing a program’s control flow can make it easier to quickly understand
what is happening inside of code. Getting good at recognizing these flows
quickly can vastly improve your reverse engineering ability.
Ghidra
Ghidra is a static analysis tool released in 2019 by the NSA. It has many simi-
larities to IDA, but unlike IDA, it is free and open source. In many situations,
Ghidra is an adequate replacement to IDA.
IDA has a much longer reputation in the space, but Ghidra is also immensely
powerful and in many cases has a lot of the same features. This example demos
IDA given its long history in the reverse engineering space, but everything
shown can also be done in Ghidra. The tools are similar enough that skills in
one will often transfer over. Try Ghidra out for some of the later, open-ended
labs in this book and your own practice.
For this lab, please locate Lab Cracking with IDA and follow the provided
instructions.
Skills
This lab practices using IDA to crack large, real-world applications. The goal is
to learn to quickly identify points of interest and to prioritize multiple cracking
approaches.
202 Chapter 11 ■ Patching and Advanced Tooling
Takeaways
Real-world programs are too large for uniform, fine-grained analysis. Triage is
critical to finding the points of interest.
Multiple opportunities are usually available to a cracker. Selecting which to
pursue can save (or cost) significant time.
Summary
This chapter explored some of the most widely used tools for reversing and
cracking. Take the time to become familiar with them. It’ll pay off in the long run!
CHAPTER
12
Defense
How do you defend against cracking? To start, it’s essential to have a good key
check design (don’t pull a Starcraft/Half-Life). From there, you can implement
additional defensive options.
However, it’s important to remember that there is no such thing as uncrackable
software. As a defender, your job is to slow attackers down in the critical parts
of your software and make them frustrated enough they go to a different target.
Like many things in cybersecurity, you just don’t want to be the low-hanging
fruit. “When swimming in shark-infested water, you don’t have to be the fast-
est. . .just faster than the guy next to you.”
Obfuscation
Obfuscation is the practice of hiding the intended meaning of code by purpose-
fully making logic ambiguous and unclear. It can be valuable for slowing reverse
engineering to do the following:
■■ Slow cracking
■■ Slow tampering
■■ Protect intellectual property
203
204 Chapter 12 ■ Defense
Done well, obfuscation can make code essentially unreadable. For example,
the following C code (available from www.ioccc.org/1988/phillipps.c), when
compiled and run, prints out the lyrics to the entire 12 days of Christmas song.
It was one of the IOCCC winners, which is a competition to hand-obfuscate
code. Looking at it makes my brain hurt, and I can’t guess at how long I’d have
to reverse engineer the code before I figured out what it did.
#include <stdio.h>
main(t,_,a)
char
*
a;
{
return!
0<t?
t<3?
main(-79,-13,a+
main(-87,1-_,
main(- 86, 0, a+1 )
+a)):
1,
t<_?
main(t+1, _, a )
:3,
main ( -94, -27+t, a )
&&t == 2 ?_
<13 ?
main ( 2, _+1, "%s %d %d\n" )
:9:16:
t<0?
t<-72?
main( _, t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l,+,
/n{n+,/+#n+,/#;\
#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!
/+k#;\
q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw'
i;# ){nl]!/n{n#'; \
r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#\
\
n'wk nw' iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;\
{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;\
#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/")
:
t<-50?
_==*a ?
putchar(31[a]):
main(-65,_,a+1)
:
Chapter 12 ■ Defense 205
main((*a == '/') + t, _, a + 1 )
:
0<t?
main ( 2, 2 , "%s")
:*a=='/'||
main(0,
main(-
61,*a, "!ek;dc i@bK'(q)-
[w]*%n+r3#l,{}:\nuwloca-
O;m .vpbks,
fxntdCeghiry")
,a+1);}
The concept of obfuscation has also made its way into popular culture. The
following quotes are from a scene in one of the James Bond movies, Skyfall when
Q is attempting to get into Silva’s laptop.
■■ “There are algorithms and encryptions and asymmetrics!”
■■ “Looks like obfuscated code to conceal its true purpose. Security through
obscurity!”
Obfuscations can be applied by hand or automatically to a program at var-
ious stages of its life cycle, including the following:
■■ Source code
■■ Bytecode
■■ Object code
■■ Binary executable code
Evaluating Obfuscation
When evaluating options for obfuscation, there are a few different factors to
consider:
■■ Potency: How much obfuscation is applied to the program
■■ Resilience: How well-obfuscated code holds up to attack from reverse
engineering tools
■■ Stealth: How well-obfuscated code blends in with the rest of the program
■■ Cost: Performance penalty of an obfuscated application
In general, these factors tend to work against each other. For example, the
more potent the obfuscation is, the less stealthy it typically is.
In practice, performance cost is often the limiting factor. However, almost
all obfuscations allow some degree of scaling/tuning based on requirements.
206 Chapter 12 ■ Defense
Automated Obfuscation
Obfuscation can be performed manually. However, it’s almost always better to
use tools to obfuscate the code. Some of the common obfuscation techniques
include the following:
■■ Name mangling
■■ String encryption
■■ Control flow obfuscation
■■ Control flow flattening
■■ Opaque predicates
■■ Instruction substitution
Name Mangling
Name mangling involves obfuscating function and variable names. This can be
done a few different ways, including the following:
■■ Replace with gibberish (get_key -> aVJ230AM)
■■ Replace with misleading name (get_key -> draw_screen)
■■ Replace with nondescriptive name (get_key -> a)
After mangling, the purpose of functions and variables is no longer immedi-
ately apparent. For example, consider the following code sample:
public static void SelectionSort <T> (T[] data, int size)
where T: IComparable
{
for (int num1 = size – 1; num1 >= 1; num1-
-
)
{
T local1 = data[0];
int num2 = 0;
for (int num3 = 1; num3 <= num1; num3++)
{
if (data[num3].CompareTo(local1) > 0)
{
local1 = data[num3];
num2 = num3;
}
}
T local2 = data[num2];
data[num2] = data[num1];
data[num1] = local2;
}
}
Chapter 12 ■ Defense 207
In the original, it is relatively easy to determine that the code is a sort algorithm
even without the function name. However, doing so after mangling is much harder.
String Encryption
Another obfuscation technique is for the obfuscator to encrypt strings when the
executable is built. A decrypt function in the code will then decrypt individual
strings as needed at runtime. This renders tools like IDA’s string view unusable.
String encryption can have a dramatic effect on code readability. Consider
the following code:
public a() {
this.a = "Hi, my name is Paul."
}
208 Chapter 12 ■ Defense
public static void a() {
a a1 = new a();
Console.WriteLine("Enter password: ");
string text1 = Console.ReadLine();
if (!text1.Equals(a1.a))
{
Console.WriteLine("Incorrect password.");
}
else
{
Console.WriteLine("Correct password.");
}
Console.ReadLine();
}
binary with hundreds of thousands of lines of code, only five might be related to
the key checker, and using tools like strings is a powerful way to quickly hone in
on those five lines. Taking away strings is quite painful to the reverse engineer.
Opaque Predicates
Opaque predicates add junk code interleaved with real code. The junk code
never executes, while the real code always executes. However, to a reverse engi-
neer, this is a good way to distract them with useless code, making them spend
hours reverse engineering junk code that is essentially irrelevant. Figure 12.2
shows an example of this in IDA.
The path is determined by an if statement that always resolves to the same
value. However, it can take time to identify (an “opaque predicate”), slowing
analysis.
210 Chapter 12 ■ Defense
Instruction Substitution
Instruction substitution involves replacing easily identified instructions with
complex ones that perform the same action. For example, consider the follow-
ing code:
sub edx, 0x192A6C72
neg ecx
sub edx, ecx
add edx, 0x192A6C72
Obfuscators
Obfuscators typically provide “knobs” that allow the developer to tweak the
level of obfuscation. The reason for this is that more obfuscation is not always
better. In general, increasing obfuscation decreases execution speed and increases
file size. Also, drastically increasing obfuscation does not substantially increase
the difficulty of reverse engineering. Balancing usability and security requires
finding a middle ground.
If you manage to do that, obfuscation can be a valuable tool, especially for
code that is otherwise trivial to decompile (such as the JIT languages discussed
earlier, e.g., .NET, etc.). However, it’s also important to ensure that the tool you
are using does not also provide an easily accessible de-obfuscator.
Chapter 12 ■ Defense 211
For general-purpose obfuscation, OLLVM can be a good starting point. This tool
has a few benefits, including the fact that it works with the LLVM intermediate
representation (IR) and supports all LLVM front ends (gcc, clang) and many
source languages (C, C++, C#, Lisp, Fortran, Haskell, Python, Ruby, etc.).
The use of OLLVM is not recommended for production code. However, it
can be a good basis for custom obfuscators or simply learning/playing with
obfuscation.
In addition to OLLVM, there are numerous language-specific obfuscator tools
and tricks. Some examples include Dotfuscator for C# and Proguard for Java.
For JavaScript programs, tools such as YUICompressor and UglifyJS can be
used for obfuscation. In general, minimizers, simply as a byproduct, introduce
some reasonable level of obfuscation.
Python code can be compiled to bytecode to remove some variable names and
comments. Then, the bytecode can be obfuscated and released with a custom
interpreter. Some Python obfuscators include Tigress, BitBoost, and Opy, but
these are less popular than the ones mentioned earlier.
Defeating Obfuscators
Obfuscators are designed to protect against reverse engineering by making
it more difficult and time-consuming to perform. However, obfuscation isn’t
perfect, and as stated many times previously, motivated crackers can eventu-
ally defeat it.
Some of the ways that a reverse engineer can speed up the process of ana-
lyzing an obfuscated binary include the following:
■■ Run traces to identify real versus fake code
■■ Use symbolic analysis to simplify complexity
■■ Write custom scripts to remove obfuscations
Lab: Obfuscation
This lab explores obfuscation techniques. T Labs and all associated instructions
can be found in their corresponding folder here:
https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES
For this lab, please locate Lab Obfuscation and follow the provided instructions.
Skills
This lab provides experience in circumventing obfuscation techniques using
objdump. The goal is to understand the impact of common code defense techniques.
212 Chapter 12 ■ Defense
Takeaways
Obfuscation techniques will slow down—but not defeat—cracking. However,
remember that sometimes slowing down is enough. Advanced reverse engineers
often have tools to automatically circumvent common obfuscations.
Anti-Debugging
Debugging is often the fastest way to reverse engineer an executable. Anti-
debugging is a series of techniques to try to stop someone from having the
ability to dynamically analyze your application with a debugger. There are
many techniques in this space, but most of them are geared at trying to check
for the presence of a debugger. A few common anti-debugging checks include
the following:
■■ Memory checks
■■ CPU checks
■■ Timing checks
■■ Exception checks
■■ Environment checks
As with most security controls, there are usability trade-offs to anti-debugging,
code size and performance being the two most painful side effects. Because of
this, anti-debugging functionality is often added only selectively, reserving its
use for the code most likely to be attacked (key checkers, sensitive IP addresses,
etc.). But as with all security there are pros and cons; if you build a bunch of anti-
debugging checks around your sensitive code, you’re also painting a bull’s-eye
telling an attacker exactly where the interesting stuff is. So, while they might not
be able to debug it, they now know exactly where to focus with static analysis
techniques. But that doesn’t mean it’s not worth doing; static analysis might
take 100x longer than dynamic, so even if you paint arrows to your sensitive
code, forcing them to do it statically can still be a powerful tool.
The main goal with anti-debugging is to identify when a debugger is attached
and take an action. The most commonly used actions include the following:
■■ Forcibly disconnecting the debugger
■■ Exiting the program
■■ Executing red herring code to waste an attacker’s time
IsDebuggerPresent()
IsDebuggerPresent is a memory check for a debugger. The function
IsDebuggerPresent, which is located in Windows.h, returns true if a program
Chapter 12 ■ Defense 213
is being run under a debugger. The following code shows an example of how
it is used to exit an application if a debugger is attached:
if (IsDebuggerPresent())
exit(1);
Debug Registers
An application can also make use of the CPU’s debug registers to perform a
check for a debugger. Recall that the debugging section discussed software and
hardware breakpoints. A hardware breakpoint uses CPU hardware registers to
set itself.
These hardware breakpoints use debug registers (in x86: DR0, 1, 2, 3, 6, 7)
instead of memory modifications. It’s possible to detect debugging by exam-
ining these registers.
For example, consider the following code sample. It checks to see if any of
the debug registers are set, indicating a hardware breakpoint.
if (GetThreadContext(hThread, &ctx))
if ((ctx.Dr0 != 0x00) || ... || (ctx.Dr7 != 0x00))
exit(1);
RDTSC
RDTSC stands for the x86 instruction Read Timestamp Counter. This counter
can be used to read a timestamp from the CPU. This has lots of interesting uses,
but one of them is to perform a timing check for a debugger.
When running an application (with no debugger), the CPU is very fast, but
when a debugger is attached, it isn’t. Even if you’re not stepping and you’re just
letting the code run, it’s orders of magnitude slower than just letting the CPU
go. And it’s even slower if you’re doing something like single-stepping through
the code. With RDTSC, an application can take timestamps before and after a
block of code and measure how long the code took to execute. If the delta is
large, it’s likely that the code hit a breakpoint or was being manually stepped
through with a debugger.
The following pseudocode shows how RDTSC could be used to detect a
debugger:
a = __rdtsc();
keycheck();
b = __rdtsc();
if (b -a > 0x10000)
exit(1);
To defeat this type of anti-debugging check, you could break on the second
call to RDTSC. You could then modify the value of either a to be closer to b or
b to be closer to a. Essentially, make the difference between the two very small
so it assumes execution went as planned. Bypassable? Yes. Annoying to have
to patch every time you debug? Yes!
Invalid CloseHandle()
The use of an invalid call to CloseHandle is an example of an exception check for
a debugger. The Windows CloseHandle function throws an exception if called
with an invalid handle while running under a debugger (and not otherwise).
An application can use this knowledge to call CloseHandle on an invalid handle
to detect the presence of a debugger.
The following code demonstrates how CloseHandle can be used to detect a
debugger:
HANDLE hInvalid = (HANDLE)0xDEADBEEF;
__try { CloseHandle(hInvalid); }
__except (EXCEPTION_EXECUTE_HANDLER) { exit(1); }
Directory Scanning
Directory scanning is an environment check for a debugger. It involves scanning
the file system for installations of common debuggers and cracking tools. If
these tools are found, then the application can choose to exit.
However, this is an indiscriminate search, and these tools may not be actively
debugging the application. As a result, it hurts legitimate users of these tools.
To defeat this check, set a breakpoint on the directory traversal. Then, mask
out the tool directories so that the application doesn’t see or search them.
Offensive Anti-Debugging
Anti-debugging techniques need not be passive detection of debuggers. Many
“active defense” approaches exist, including the following:
■■ NtUserBlockInput: Block keyboard input to the attached debugger.
■■ NtUserFindWindowEx: Get a handle to the debugger window.
■■ Debugger-specific attacks: For example, IDA versions older than 7.0 crash
at about 10,000 instructions without a branch.
Many more options exist. For offensive anti-debugging, first you need to
recognize the debugger is there, and then you take some type of offensive
action. Open-source plugins are available to help, including some used in the
following lab.
For defensive anti-debugging, it’s important to remember that you don’t
need to reinvent the wheel. Ready-made solutions are available, including free,
open-source Windows anti-debugger checks.
Defeating Anti-Debugging
Like other software defenses, anti-debugging code can be defeated (though if
done right, it’s painful). The first step is to find and reverse engineer the anti-
debug check. Often, this is accomplished by working backward from where
you got caught using the debugger.
Once you’ve identified the anti-debug code, you have a few different options
for defeating it, including the following:
■■ Removing the check via nops
■■ Placing a breakpoint on the check and modifying memory/registers to
mask the debugger
■■ Using built-in debugger plugins or scripts
216 Chapter 12 ■ Defense
Lab: Anti-Debugging
This lab provides practice in defeating anti-debugging techniques. Labs and all
associated instructions can be found in their corresponding folder here:
https://github.com/DazzleCatDuo/
X86-SOFTWARE-REVERSE-ENGINEERING-CRACKING-AND-COUNTER-MEASURES
For this lab, please locate Lab Anti-Debugging and follow the provided
instructions.
Skills
This lab uses x64dbg to circumvent anti-debugging techniques. The goal is to
understand the impact of common defensive coding techniques.
Takeaways
Again, slowing down a reverse engineer is often enough; defenses don’t need
to be perfect. However, skilled reversers will have tools to overcome common
defensive techniques.
Summary
Developers want to defend themselves and their code against reversers and
crackers. This chapter explored some of the common methods for accomplishing
this, including obfuscation and anti-debugging protections.
CHAPTER
13
The previous chapter presented some basic techniques for protecting an appli-
cation against reverse engineering and cracking. This chapter demonstrates
some more advanced techniques that are more difficult to defeat, including
tamper-proofing, packing, virtualization, and the use of cryptors.
Tamper-Proofing
One of the powerful cracking techniques we’ve covered is patching, both for
long-term cracking but also in the aid of reverse engineering. Tamper-proofing
is a series of techniques geared toward making software more difficult for an
attacker to modify. Some common approaches include the following:
■■ Hashing
■■ Signature
■■ Watermark
■■ Software guards
All of the following techniques have ways of being defeated, but (and I can’t
stress this enough) just because they have ways of being defeated doesn’t mean
they are not worth doing. Each of them provides a layer of defense in depth,
and even if the method for defeating them fits into a few sentences, this doesn’t
mean it’s easy in practice.
217
218 Chapter 13 ■ Advanced Defensive Techniques
Hashing
An application can use hash functions to implement tamper-proofing via the
following steps:
1. Compute a hash of the software.
2. Embed the hash in the software.
3. Have the software check its own hash before executing.
4. Any modifications to the software modify the hash.
The defense relies on the fact that changes to the application will cause the
hash check to fail. To defeat this, an attacker will need to make their changes
and then recompute the hash after modifications and changing the checked
value or removing the hash check entirely.
Signatures
Digital signatures can provide strong data integrity and authenticity protec-
tions. They use public key cryptography where a public and private key pair
is generated. To use them for tamper-proofing, follow these steps:
1. Sign the software with a private key, creating a signature.
2. Embed the signature in the software.
3. Have the software check its signature with your public key before executing.
4. Any modifications to the software make the signature invalid.
One of the key benefits of digital signatures is that it is effectively impos-
sible to generate a valid signature without knowledge of the private key. To
defeat this type of protection, an attacker would have to remove the signa-
ture check entirely or get ahold of the private key so they can regenerate a
valid signature.
Watermark
To implement watermarking, each purchaser of your software receives a unique
version of the executable, where modifications are made to the following:
■■ Instruction order
■■ Function names
■■ Parameter order
■■ Instruction substitution
■■ Etc.
Chapter 13 ■ Advanced Defensive Techniques 219
The specific changes “watermark” that instance, allowing you to trace it back
to its owner, as well as detect modifications. Also, any modifications to the soft-
ware taint the watermark, making them obvious.
For an attacker to defeat this protection, they will need to identify water-
marked sections. Then, replace them with an alternate mark to hide the source
of the modified software.
Guards
With guards, code inside the program checks sensitive areas for modification.
For example, the code may specifically look at a critical jump to make sure it
still jumps to the intended location. Common areas to monitor with guards
include key checks, jump instructions, other guards, etc.
Any modifications to these sections are caught by the guards. The guards
will then change the software’s behavior (exit, change paths, undo modifi-
cations, etc.).
This defense relies on the fact that the guard is present and able to modify
the software as needed. If an attacker wants to defeat this technique, they will
need to remove the software guard code.
Packing
Packing is a broad term referring to techniques commonly used on executables
to compress and obfuscate their contents. Some common packing techniques
include the following:
■■ Compression/encryption of data sections
■■ Scrambling code sections
■■ Compression/encryption of code sections
■■ Anti-reverse engineering
One of the main advantages of packing is that it makes reverse engineering
harder. For example, a packer may include features that address many of the
common reverse engineering threats, including the following:
■■ Anti-debugging: Packers can conceal the use of IsDebuggerPresent,
making it more difficult to detect.
■■ Anti-virtualization: Packers can detect when an application is being
virtualized in a platform such as VMware and conceal detection code.
■■ Anti-dumping: Packers can erase headers in memory, making it difficult
to dump memory.
220 Chapter 13 ■ Advanced Defensive Techniques
quick look at some of the defenses. In each section, to evaluate the effectiveness
of an anti-cracking defense, we will use something called the CIA triad (CIA
stands for confidentiality, integrity, and availability). For those not familiar with
this, it’s a common way to think about security controls, as not all security con-
trols cover all three parts of the triad, so it’s important to know which is useful
in each pillar. Integrity is the authenticity of something. Is it as it was originally
intended, or has it been modified? Confidentiality is the ability of something to be
accessible to only authorized parties. Availability is the level to which something
is available to perform its intended function. These three together are commonly
known as the CIA triad. Evaluating packers against the CIA triad:
■■ Confidentiality: Yes, aside from the unpacking portion of the code, the
rest of it is in nonreadable format.
■■ Integrity: Yes, modifications to the binary would cause corruption of the
packed sections, causing likely application failure.
■■ Availability: Packers can have a negative effect on performance, which
can affect availability. However, if configured carefully, this effect can be
minimized.
Defeating Packing
So, how can packers be defeated? Debug the program and watch for the program
to decrypt in memory. Once it is unpacked in memory, you can analyze it, but
any patching done will be viable only on the unpacked binary. Patching can’t
be saved back to the packed binary.
One natural thought that occurs to people is once it’s unpacked in memory,
can’t I just memory dump that out to a new unpacked binary? This is techni-
cally possible but difficult to do. Applications include a lot of startup code, and
getting it loading in the right spot in memory, setting up the stack, etc., doesn’t
naturally come from dumping memory and just calling it an EXE.
Another option is to see if you can unpack the program. Some of the common
packers out there have unpacking tools that can be used to reverse the protec-
tions put in place; some examples include UPX, MEW, and ASPack.
However, there may be no stand-alone unpacker, and the unpacking code exists
only in the packed executable. However, that doesn’t mean we’re stuck! There
are a number of great plugins and tools built specifically for this purpose, such
as OllyDumpEx and ImpRec, which aim to reconstruct the import table. This is a
complex but doable process, but not the focus of our book. However, if this is of
interest, there are some great blogs to be found online on import reconstruction.
PEiD
Often when approaching a file, it can be difficult to figure out what types of
manipulations were done to it. If you somehow know out of the gate it was
222 Chapter 13 ■ Advanced Defensive Techniques
packed with a certain tool, then it’s easy to start down that path. But cracking
doesn’t typically come with a handy playbook telling you what defenses are in
place. PEiD is a tool to detect most common packers, cryptors, and compilers
for portable execution files (e.g., applications). It can detect the signatures of
more than 470 different obfuscation tools. Another more recent tool in this space
is Detect it Easy.
As we’ve mentioned, many defensive tools such as packers and cryptors
have unpackers and decryptors as well. Identifying the one used can reduce
analysis time by an order of magnitude by allowing you to strip away many of
an application’s protections.
Figure 13.2 shows an example of using PEiD. To start, select the file to check.
PEiD will then show the details of its packing, crypting, and compiling.
For this lab, please locate Lab Detecting and Unpacking and follow the provided
instructions.
Skills
Packers are a common protection against reversing. This lab explores the use
of IDA, Cheat Engine, and PEiD to test the following skills:
■■ Detecting the presence of packers
■■ Unpacking programs with existing tools
■■ Unpacking programs with advanced debugging
Chapter 13 ■ Advanced Defensive Techniques 223
Takeaways
Off-the-shelf unpackers are available for many packers (don’t reinvent the
wheel). When unpackers are not available, the unpacked, original program can
still be manually recovered from memory.
Virtualization
Virtualization provides a form of obfuscation and packing. It translates a program
into a custom machine language and generates a virtual environment/machine
(VM) to interpret it. The VM is embedded into the application and runs when
the application is executed. Note that in this case we’re not talking about typ-
ical large virtual machines such as Windows or Linux running in a hypervi-
sor. Virtualization in this case can quite simply mean a layer of abstraction/
interpretation being added between the host (x86) and the code.
For example, consider the following simple “hello world” program:
#include <stdio.h>
int main(void)
{
printf(“hello, world!\n”);
return 0;
}
char data[30000];
char program[30000];
int ip=0; /* instruction pointer */
int dp=0; /* data pointer */
int main(void) {
224 Chapter 13 ■ Advanced Defensive Techniques
do {
b=read_byte();
program[i]=b;
i++;
} while (b!='#’);
while (1) {
b=program[ip];
if (b==0) {
break;
} else if (b=='>') {
dp++;
} else if (b=='<') {
dp-
-;
} else if (b=='+') {
data[dp]++;
} else if (b=='- ') {
data[dp]- -
;
} else if (b=='.') {
write_byte(data[dp]);
} else if (b==',') {
data[dp]=read_byte();
} else if (b=='[') {
if (!data[dp]) {
int c=1;
do {
ip++;
if (program[ip]=='[‘) { c++; }
else if (program[ip]==']’) { c-
-
;}
} while (c);
}
} else if (b==']') {
if (data[dp]) {
int c=1;
do {
ip-
-;
if (program[ip]=='[‘) { c-
-
; }
else if (program[ip]==']’) { c++; }
} while (c);
}
} else {
/* do nothing */
}
ip++;
}
return 0;
}
This adds a layer of abstraction that a cracker or reverse engineer must get
through. First, reverse engineer the intermediate VM language. For those familiar
with the programming language Java, Java runs inside of a VM, called the Java
Chapter 13 ■ Advanced Defensive Techniques 225
virtual machine (JVM). While that was done for portability, not security, it does
add a layer of complexity. There are other languages that run inside of a VM,
but you can also create your own (as with the example).
Layered Virtualization
Virtualization protections can be layered as in the following process:
■■ Virtual machine VM0 implements the custom instruction set IS0.
■■ IS0 runs the virtual machine VM1, which implements custom instruction
set IS1.
■■ IS1 runs the original application.
An example of layered virtualization may include the following:
1. Compile the C source code to a custom language, such as DazzleZ.
2. Write the DazzleZ interpreter in a custom language such as CatCat.
3. Write the CatCat interpreter in x86.
4. Run the program on the regular x86 platform.
5. Reversing requires backing out all layers of virtualization.
Defeating Virtualization
Virtualization can be an effective defense because defeating it is time-
consuming and difficult. In general, the following process can be used to
defeat virtualization:
■■ Reverse the code-dispatch scheme: VMs typically follow the familiar
fetch-decode-execute cycle of a CPU, which makes it possible to under-
stand how code is dispatched.
■■ Reduce complexity: Use pattern matching, symbolic analysis, and similar
techniques to remove unnecessary complexity.
■■ “Devirtualize” the program: Attempt to recover a representation of the
original code. However, this is not always a simple “inverse” for complex
VMs and may not be possible to recover original code, forcing you to
reverse engineer the virtualized code.
■■ Reverse the recovered code: Use traditional tools to reverse the recovered
code. You may need to rely on static analysis if a functioning program
cannot be recovered.
Virtualization can be defeated by reverse engineering the virtual machine
and then transforming the application back into x86 machine code for anal-
ysis. Some tools that aid in accomplishing this include Themida, VMProtect,
and Tigress.
Chapter 13 ■ Advanced Defensive Techniques 227
Cryptors/Decryptors
Cryptors encrypt the application code sections (a subset of the techniques
discussed in earlier section on packers), often to avoid malware detection.
Many anti-malware tools will analyze a piece of software prior to running and
block software based on API calls to suspicious operating system functions. By
encrypting the code section, the malware makes it impossible for anti-malware
programs to inspect the content of the application before execution.
In general, encrypted software must decrypt itself prior to execution. Typically,
this means the decryption key is somewhere within the software. Therefore,
reverse engineering should be able to find the key and decrypt the software.
However, there are some exceptions to this. For example, node-locked soft-
ware may derive a key from the specific system on which it resides. Alterna-
tively, malware may beacon to a server to retrieve a decryption key on the fly.
Defeating Cryptors
Most encryptors have supporting decryptors, which are tools that can automat-
ically restore the original software. Often these decryptors are just the encryptor
itself with a different input flag
If you are reversing a crypted application, decrypting will get you back to
the original binary. Since this will be much easier to analyze, see if there is an
available decryptor before you begin your Reverse Engineering. Some common
cryptors to check include Yoda’s Cryptor, Morphine, and PGMP.
Summary
In looking at defense options, there is no silver bullet. Most anti-reversing tech-
niques also have downsides.
228 Chapter 13 ■ Advanced Defensive Techniques
14
CRC
A cyclic redundancy check (CRC) is a mathematical calculation performed on
the bytes of the data to be protected. The result is stored as the CRC, which is
often appended to the data (i.e., data data data data data data CRC). To
verify the data, recalculate and compare.
CRC algorithms have their advantages, including the following:
■■ Fast and compact
■■ Easy to accelerate with hardware
■■ Quick to calculate and compare
■■ Numerous options available (IEEE802.3, CRC-32, etc.)
In general, CRCs are great for detecting accidental errors or modification,
such as transmission errors.
229
230 Chapter 14 ■ Detection and Prevention
Digital signatures are validated using your public key; however, you need a
way to prove that a particular public key belongs to you. This is where public
key infrastructure (PKI) comes into the picture. Using the generated public key,
you apply for a certificate from a code signing certificate authority (CA). The
CA will verify your identity and issue a digital certificate, which contains your
public key and validates your ownership of this.
With this certificate, you can now generate digital signatures. To do so, you
would generate a hash of the executable and encrypt that hash with the private
key. Then, when you distribute the executable, you would bundle the resulting
signature and your digital certificate with the executable.
While you can go through this process manually, many build tools will do
this for you. You still would need to buy a certificate and load it into your build
tool, but then you can ask the build tool to sign the application. If this is your
first exposure to PKI, know that this is intentionally just scratching the surface
of it; there are many books dedicated to just this concept.
RASP
Runtime application self-protection (RASP) embeds security into the running
application. It does so by intercepting system calls and verifying that they are
Chapter 14 ■ Detection and Prevention 233
from an expected source. It also intercepts data manipulations and verifies that
they are coming from authorized sources.
RASP is a reactionary defense. It can be configured to “stop” attacks live. For
example, RASP can do the following:
■■ Drop/delete a call it deems malicious, such as a suspicious SQL call into
an application.
■■ End a user session.
■■ Halt execution.
Function Hooking
One technique that RASP uses is function hooking. This involves overwriting
the first few bytes of a function’s code with a jump to the RASP code.
The RASP code will include checks to verify that the call is legitimate. This
can include the following:
■■ Checking the parameters and context of the call
■■ Checking the code has not been modified (might compare a hash of the
function with a known good hash)
At the end of the RASP code, it will then execute the overwritten code before
jumping back to the original function.
Risks of RASP
If RASP detects an attack, it can stop execution. However, this may not be accept-
able depending on the use case of your software. For example, in hospitals,
manufacturing, critical infrastructure, automobiles, and similar environments,
an application suddenly halting can pose a significant risk to health and safety.
RASP can also have its downsides even in the absence of an attack. Some
effects include the following:
■■ Speed: Because of the function hooking, RASP has a nontrivial effect
on speed.
■■ Size: Function hooking and lookup tables help to assure security; how-
ever, they also bloat binaries.
Allowlisting
Allowlisting, sometimes called whitelisting, is providing the execution envi-
ronment with a list of “good” things. For example, a computer may allow only
allowlisted applications to run.
There are numerous commercial products that provide allowlists. For example,
the Windows operating system has built-in software restriction policies.
From a cracking perspective, allowlisting can prevent the use of cracking
and reverse engineering tools. For example, tools such as Procmon, debuggers,
Cheat Engine, ResourceHacker, Dependency Walker, and other reversing and
cracking applications are unlikely to be included in the allowlist.
Allowlisting is difficult to get right. It can be difficult to know all of the var-
ious libraries that your application needs. When generating a whitelist, a great
deal of testing must be performed to ensure that all required applications and
libraries are included on the allowlist.
Example: Metasploit
Metasploit is a popular hacking tool. Its main goal is to exploit an application
and inject a meterpreter, which provides the attacker with remote access to the
infected computer. (See the “Tools” section of our repository for links.)
With Metasploit, no new applications are started; a meterpreter injects into the
hacked process. From there it can “pivot” into any other running application.
Blocklisting
Blocklisting, sometimes referred to as blacklisting, is the exact opposite of allow-
listing. Instead of specifying everything that is permitted, it is a list of all the
things that are not allowed. The blocklist can be based on names, keys, or hashes.
Blocklists are easy to make but difficult to maintain. For example, consider
a blocklist including the malicious executable virus1.exe. What happens next
week when virus2.exe comes out?
236 Chapter 14 ■ Detection and Prevention
From a more cracking perspective, you might blocklist keys that you know
to be bad (i.e., cracked). Depending how your key generation works, it may be
possible to blocklist whole subsets of keys.
Alternatively, a program can also refuse to run if certain other applications
are seen. For example, the application may not run if a debugger is installed.
Many antiviruses use this approach to identify and block known malware.
They include a list of “signatures” of known bad applications. If something
matches the signature, it’s flagged as bad.
Remote Authentication
For most anti-reversing and anti-cracking strategies, the attacker has all of the
pieces that they need to overcome the defense. With enough time, they can
reverse engineer and/or patch the application.
Remote authentication requires the application to retrieve something remotely
in order to work. For example, it might get a key from a remote server that it
uses to decrypt some crucial code.
Most attackers will reverse engineer a system “offline.” They don’t want
it reaching out to your servers because they don’t want you to have their IP
address or to know that they are running your software. Keep in mind when
attempting to crack a piece of software, you’re likely launching and running the
startup and checking code frequently. Whereas a legitimate user would likely
launch the application at max a few times a day. That type of behavior is really
easy to spot on a remote authentication server. A user who is authenticating
100 times a day is likely doing something nefarious.
Architecting the application in such a way that it can’t run without information
from a server helps prevent reverse engineering. The attacker will either need
to reverse it “online” or give up.
Chapter 14 ■ Detection and Prevention 237
Remote Authentication Example
One possible approach to implement remote authentication is to encrypt every
part of the application except for the loaders. The loader sends system information,
a hash of the software, and the activation key to the server.
The server will verify the expected hash and the activation key. If they vali-
date, it uses an algorithm to produce a decryption key, which it will send back to
the application. The loader can then decrypt the application, enabling it to run.
An attacker will not be able to “mimic” your remote server and algorithm
without access to the server-side code. The only way to research the software
will be to activate it online. The application can have decryption code be resident
in memory only. This way, each startup requires server interaction.
The main challenge of this approach is that implementing cryptography
and enterprise key management solutions is not trivial. A mistake may allow
a cracker to bypass the validation code and generate their own decryption
keys. As discussed with packed applications, once it’s unpacked in memory,
you could take a memory dump of it for future static analysis. However, that
memory dump can’t easily (or sometimes not at all) be turned into a decrypted
application capable of running. The memory dump won’t be useful for patching
or testing modifications but never discount the value of static analysis.
Takeaways
Watching what a process does from the outside can be quicker/easier than
watching it from the inside (that is, debugging is not always the best approach).
There are usually many ways to crack a program; finding the best takes practice.
Summary
This chapter presented various methods of protecting against software cracking
and reversing. Some techniques are generally ineffective, while others can work
but also have some downsides.
It’s important to remember that almost any defense can be defeated given
enough time and effort. The goal is to slow an attacker down and, ideally, make
them frustrated enough to give up.
CHAPTER
15
Legal
overreaching in their use of it. The amendment, referred to as Aaron’s law, was
introduced months after Swartz’s death by Rep. Zoe Lofgren (D-Calif.) and
Sen. Ron Wyden (D-Oregon). The amendment would exclude breaches of terms
of service and user agreements from the law and also narrow the definition
of unauthorized access to make a clear distinction between criminal hacking
activity and simple acts that exceed authorized access on a minor level. Instead,
the amendment proposes to define unauthorized access as “circumventing
one or more technological measures that exclude or prevent unauthorized
individuals from obtaining or altering” information on a protected computer.
The amendment also would make it clear that the act of circumvention would
not include a user simply changing his MAC or IP address to gain access to
a system.
Copyright Act
Under the Copyright Act of 1976, a copyright for a computer program comes
into being as the source code for the computer program is being written by the
programmer. The program does not need to be complete or even functional for
copyright protection to come into being. Copyright case law treats the copyright
of the source code and object code as equivalent.
If you are not the copyright owner, it is typically not legal to perform any of
the following actions without permission:
However, there are many exceptions and nuances to this. The first copyright for
software was in 1964. The justification for why to begin granting protection of
software was they now viewed a computer program like a “how-to book.” The
Copyright Act of 1976 officially calls out software as copyrightable.
So, when a piece of software gets copyright protection, what exactly is copy-
righted? The copyright protects the expression of an idea, not the idea itself.
For example, if you develop the concept of a lemonade stand game, you can
copyright your implementation of it but not the idea of a lemonade stand game.
Second, the protection protects the object (executable) program, not the source
code. Lastly, it protects the screen displays produced by the program while it
executes.
The source code of software is generally kept as a trade secret and not released
under a copyright to the public.
242 Chapter 15 ■ Legal
Fair Use
Sometimes it is legal to reproduce a copyrighted work without permission. In
general, courts consider four factors when evaluating whether something falls
under the fair use exceptions:
■■ Purpose and how it’s used: If the purpose is criticism, commentary, news
reporting, teaching, or research, then it is likely permissible. However,
commercial use likely isn’t.
How about for character of use? The most important consideration is
how much the work has been transformed from the original. If the new
author has added new expressions or meaning, then it’s potentially a
candidate for fair use.
■■ Nature of work: Fair use is granted more favorably to works of nonfiction
than of works of fiction.
■■ Amount of work being copied: A brief excerpt is more likely to be OK
than copying an entire book or an entire chapter.
■■ Effect on market for copyrighted work: For example, copying out-of-print
material doesn’t have the same material effect as copying a newly written
and printed work.
Legality
Copyright law in relation to reverse engineering and code modification heavily
emphasizes intent and effects. When proceeding on your own, consult with a
lawyer. . .or keep it to yourself. This isn’t meant in a sneaky way, but recall that
a part of fair use is the effect your work has on the market. If you’re tinkering
and cracking for education or for research and your outcomes stay with you,
they don’t really affect the potential market for the work. That’s a key factor in
fair use. The second you use your knowledge to make a keygen that you put
online that causes a vendor to lose money, then it’s no longer considered fair
use. But if you’re keeping it all to yourself in a way that doesn’t affect the market
or others, then you’ve gone a long way to fall under fair use.
WA R N I N G To repeat, we are not lawyers, this is not legal advice, and this is our
interpretation and understanding of the regulatory landscape in the United States that
affects reverse engineering.
Summary
This chapter covered some of the legal considerations of reverse engineering
and cracking, but we are not lawyers. For legal advice, we recommend contact-
ing the EFF.
CHAPTER
16
Advanced Techniques
Up to this point, this book has covered the core tools and skills used for reverse
engineering and cracking. However, this is an evolving field, and new methods
are being developed to make it faster and easier. This section describes at a high
level some advanced techniques and tools on the cutting edge of reverse engi-
neering. Our goal with this chapter is that if at this point you’re still loving software
cracking and looking to take it even further to the next level, we want to present
you with a plethora of rabbit holes to go down. Depending on what interests
you, we hope the following will point you in the right directions to go deeper.
Timeless Debugging
Timeless debugging is also known as reverse debugging. The core idea is: “what
if we could go backwards when debugging?”
Consider the case where something went wrong while debugging. Maybe
a patch failed, you missed an anti-debug check, you don’t know how you got
here, etc.
There are a few different tools designed for timeless debugging, including
the following:
■■ Visual Studio Ultimate (.NET)
■■ rr
■■ gdb
245
246 Chapter 16 ■ Advanced Techniques
To get started, check out George Hotz @ Enigma in his 2016 USENIX Enigma
talk at www.youtube.com/watch?v=eGl6kpSajag.
Binary Instrumentation
Binary instrumentation is when you inject code to watch or modify a process
as it executes. This can be useful for finding memory leaks, tracing key checks,
performing anti-anti-debugging, etc.
Some tools for binary instrumentation include the following:
■■ PIN
■■ DynamoRIO
■■ Frida
■■ Valgrind
■■ QBDI
Intermediate Representations
Normally, for reversing and cracking, it’s necessary to learn and write tools for
each new architecture. The idea of intermediate representations is to translate
all assembly code for all architectures to the same language. That way, you can
learn and write tools for just that language.
There are a few different tools that can be used to work with intermediate
representations, including the following:
■■ Binary Ninja
■■ REIL
■■ VEX
■■ BNIL
■■ Ghidra PCode
■■ IDA microcode
■■ LLVM IR
Decompiling
The idea of decompiling is to recover original source code from advanced
automated analysis of assembly code. Some tools that offer decompilation
include the following:
■■ IDA’s Hex-Rays
■■ Ghidra
■■ Binary Ninja
■■ Snowman Decompiler
To learn more about decompiling, check out “Decompiling a Virus using IDA
Pro” at www.youtube.com/watch?v=gYkDcUO9otQ.
To learn more about automatic structure recovery, check out the dynStruct
idea and paper at https://github.com/ampotos/dynStruct.
Visualization
Code listings and text can be difficult to think and reason about. Visualization
can be used to deepen your understanding of file structure and execution.
Some reversing tools that offer useful visualizations include the following:
■■ BinWalk
■■ Hopper
■■ IDA plugins
■■ Veles
■■ ..cantor.dust..
■■ Cheat Engine
A good starting point for understanding how visualization can be used for
reversing includes the Derbycon talk “Dynamic Binary Visualization” from
Christopher Domas at www.youtube.com/watch?v=4bM3Gut1hIk.
248 Chapter 16 ■ Advanced Techniques
Deobfuscation
Obfuscation is designed to slow down reversing in an attempt to get a cracker
to give up. The idea is to use tools to automatically remove obfuscations from
programs using tools like Tigress Protection.
Check out “Lets break modern binary code obfuscation” at www.youtube
.com/watch?v=TDnAkm6ZTYw.
Theorem Provers
Theorem provers use mathematics to analyze code, including reduction, deob-
fuscation, boundaries, inputs, etc. Some theorem proving tools for reversing
include the following:
■■ Z3
■■ STP
■■ Boolector
■■ Yices
To see how theorem provers can be used, watch “Using z3 to find a password
and reverse obfuscated JavaScript” at www.youtube.com/watch?v=TpdDq56KH1I.
Also check out the yearly SMT-COMP!, which has some really interesting
benchmarks on many unique solvers at https://smt-comp.github.io/2023.
Symbolic Analysis
The idea behind symbolic analysis is trying to find inputs that cause interesting
results. For example, what inputs could cause a crash, pass a key check, unlock
a secret, etc.
Symbolic analysis tools will trace user input through a program. At each
branch, they ask a theorem prover which user input would go down the taken
path. What user input would go down the not-taken path?
For example, consider the following code:
if (strlen(username) > 10)
if (key_1^sum(username)==key_2)
printf("key passed");
Summary
At this point, the best way to improve your reversing and cracking skills is
via more hands-on practice. On the Windows VM, the allthethings folder on
the Desktop contains a variety of different crackmes to practice with sorted by
difficulty level.
CHAPTER
17
Bonus Topics
This last chapter of this book introduces software reversing and cracking. It is
primarily focused on understanding how a program works and bypassing or
modifying undesirable functionality (like key checkers).
This chapter takes this knowledge and applies it to real-world hacking. Stack
smashing and shellcoding both use an understanding of how a program and
the stack works to run malicious code within a program.
Stack Smashing
Stack smashing, also known as stack-based buffer overflows, is one of the most classic
attacks against software. It takes advantage of the fact that non-memory-safe
languages such as C/C++ have no built-in protection that prevents an applica-
tion from accessing or overwriting data in other parts of memory. For example,
C/C++ doesn’t automatically check that the data written to an array fits within
the bounds of that array. If you don’t know C, don’t worry. As long as you know
any programming language, you should be able to follow along.
Because stack smashing has been around for such a long time, there are
numerous compilers that have built-in automatic guards that are put into com-
piled code to prevent this. While it’s not as easy of an attack as it used to be,
everyone should fully understand how the attack works, because:
251
252 Chapter 17 ■ Bonus Topics
After this application has been compiled and the object has been dumped
from memory, it results in the following assembly code:
function:
push ebp
mov esp, ebp
sub ebp, 20 (*stack shown here)
leave
ret
main:
push ebp
mov ebp, esp
push 3
push 2
push 1
call function
add esp 0xc
leave
ret
NAME SIZE
buffer2 10
buffer1 5
ebp 4
Chapter 17 ■ Bonus Topics 253
NAME SIZE
ret 4
a 4
b 4
c 4
ebp 4
void main() {
char large_string[256];
int i;
function(large_string);
}
In this code, the main function builds a string that consists of 255 As. It then
passes a pointer to that buffer to function, and function allocates 16 bytes for
a local buffer but then copies (using strcpy) the input buffer blindly with no
length checks. This means the input buffer that was 255 As will overflow the
local buffer that was allocated only 16 bytes.
If you run the code, the result will be Segmentation fault (core dumped).
A segmentation fault occurs when an application attempts to read, write, or exe-
cute an invalid memory address. Let’s dig deeper to figure out what happened.
After assembly, the code is transformed into the following assembly code:
0804840c <function>:
804840c: 55 push ebp
804840d: 89 e5 mov ebp,esp
804840f: 83 ec 28 sub esp,0x28
8048412: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
8048415: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048419: 8d 45 e8 lea eax,[ebp-
0x18] ;[1]
804841c: 89 04 24 mov DWORD PTR [esp],eax
804841f: e8 cc fe ff ff call 80482f0 <strcpy@plt>
8048424: c9 leave
8048425: c3 ret
254 Chapter 17 ■ Bonus Topics
Looking at this, you can see that ebp-0x18 is the address at the start of the
buffer (marked as [1] in the previous code). Looking at the function preamble,
with the stack setup, you can see that 0x28 bytes were allocated for the stack.
Recall that ebp points to the bottom of the stack and esp the top. Therefore,
ebp = esp+0x28.
So, at the time of function setup, the start of the array, in terms relative to esp,
starts at esp+0x10. While this seems complicated, all it means is that the buffer
is 0x10 bytes away from the end of the function’s allocated stack, which makes
sense. Recall that 0x10 is 16 in base 10, and the function is allocated 16 bytes.
To see the effects of the stack smashing in action, run the application in gdb and
set a breakpoint right before the strcpy operation. At the breakpoint, printing
memory at the stack pointer should show something similar to Figure 17.1.
In this image, the allocated buffer takes up the row indicated by address
0xffffd130, and 0x10 bytes after that is the end of the function’s stack frame.
That is then followed by the saved value of the previous stacks ebp, and lastly
the return address. The value of the saved ebp (previous functions stack frame)
register is 0xffffd278, and the return address is 0x08048470.
After stepping over the strcpy operation, the same region of memory will
look like Figure 17.2. The strcpy operation overwrites the buffer, the saved ebp
register, and the return address with 0x41 (A).
When the application reaches the ret operation, it will pop the return address
off of the stack and attempt to continue execution at that location. However,
since 0x41414141 is an invalid address, the CPU segfaults.
This example causes the application to crash, but this is not the only possible
effect. At a high level, what you have the ability to do is control the return address
and the stack frame of the previous function. While stack frame manipulation
has its uses, it’s a lot more common to go after the return address manipula-
tion, so we’ll focus on that. In the first case, the return address was overwritten
with junk, but what if we were more tactical about what we overwrite with
Chapter 17 ■ Bonus Topics 255
the return address? The following code sample is designed to alter the return
address to control code execution. The goal is to skip over the x=1 instruction
in the following code:
#include <stdio.h>
void function(int a, int b, int c) {
//do something so we skip x=1 after a return
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
In this code, the main function sets up a local variable x and gives it an initial
value of 0. It then calls function with some fixed values. Inside of function,
there is no code yet. The next step is to figure out what code is needed there to
achieve the goal of rewriting the return address.
After returning from function, the main function updates the value of x to be
1 and then proceeds to print the value of x. Can we use our knowledge of cdecl
and the stack setup to make it so the code never runs x=1 and instead prints
x=0? Yes! The challenge is to write the contents of function in such a way that
the x=1 instruction inside of the main function is skipped.
For this code, the stack inside of Function would look like the following:
NAME ADDR
ebp ebp
return address ebp+4
a ebp+8
b ebp+12
c ebp+16
This is your run-of-the-mill standard cdecl stack setup. You know you’re
going to want a buffer since this chapter is all about buffer overflows, so add
a buffer to function. You’re also going to want a way to manipulate certain
values in the buffer, so add a pointer. You could also use syntax like buffer[z],
but the pointer helps to more explicitly state memory offsets, which is helpful
for learning.
#include <stdio.h>
void function(int a, int b, int c) {
char buffer[16];
256 Chapter 17 ■ Bonus Topics
int *r;
r = 0x99; //this is here so r is not optimized out
buffer[0] = 0x88; //this is here so buffer is not optimized out
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
Now there are new things on the stack, the pointer and the buffer.
NAME ADDR
buffer ebp-0x14
r ebp-4
ebp ebp
return address ebp+4
a ebp+8
b ebp+12
c ebp+16
In this stack frame, the return address is at buffer+0x18. The next step is to
update function’s code to have the pointer point to this address in memory.
For those not familiar with C, & is “address of,” so the following code sets
ret to point to the address in memory where buffer+0x18 is. By drawing out
the stack, you can see that this is the saved return address. At this point, the
return address hasn’t been changed, but we have a pointer to it. The next step
is to figure out what to change it to, to skip x=1.
#include <stdio.h>
void function(int a, int b, int c) {
Chapter 17 ■ Bonus Topics 257
char buffer[16];
int *ret;
To figure out how to manipulate the return address, take a look at the assem-
bled code for main:
0804841f <main>:
804841f: 55 push ebp
8048420: 89 e5 mov ebp,esp
8048422: 83 e4 f0 and esp,0xfffffff0
8048425: 83 ec 20 sub esp,0x20
8048428: c7 44 24 1c 00 00 00 mov DWORD PTR [esp+0x1c],0x0
8048430: c7 44 24 08 03 00 00 mov DWORD PTR [esp+0x8],0x3
8048438: c7 44 24 04 02 00 00 mov DWORD PTR [esp+0x4],0x2
8048440: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1
8048447: e8 c0 ff ff ff call 804840c <function>
804844c: c7 44 24 1c 01 00 00 mov DWORD PTR [esp+0x1c],0x1;x=1
8048454: 8b 44 24 1c mov eax,DWORD PTR [esp+0x1c]
8048458: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
804845c: c7 04 24 08 85 04 08 mov DWORD PTR [esp],0x8048508
8048463: e8 88 fe ff ff call 80482f0 <printf@plt>
8048468: c9 leave
8048469: c3 ret
Normally, the return address of the function would be 0x804844C, and, looking
at that instruction, that is the x=1 that we want to avoid! After this line, the next
instruction starts at 0x8048454.
Now, there are two options for changing the return address. One is to use
the pointer to the return address to change it to be the hard-coded 0x8048454.
The problem with this approach is that the address is a virtual address chosen
at build time by the compiler, and every time you launch it, it will be the same,
until you recompile. When you recompile, there is a chance you will get new
virtual addresses. You’d need to recompile to test this theory, so this approach
is a bit rigid.
258 Chapter 17 ■ Bonus Topics
Instead, the better approach is to note that the x=1 instruction is 8 bytes long.
That will always be consistent, so the stronger approach is to add 8 bytes to the
current return address.
N OT E When printing out assembly, gdb will often cut off the hex display, so if
you’re looking at the printout, you’ll count only 7 bytes on the x=1 line. That is simply
because it was cut off. Always do the math with the addresses to make sure you have
the right byte count.
To skip the x=1 instruction, the return address should be updated by adding
8 bytes. Adding that into the code produces the following:
#include <stdio.h>
void function(int a, int b, int c) {
char buffer[16];
int *ret;
Running this code (with the compile flag -fno-stack-protector) should result
in the program printing out a value of 0. This indicates that the return address was
successfully modified and the program skips over the x=1 instruction. Victory!
Shellcode
The ability to modify return addresses provides control over code execution,
which is powerful. One common application of this is to “pop a shell,” providing
the ability to run more powerful commands.
To pop a shell, you need to be able to run your own, arbitrary code within
the application. To do so, you need to place shellcode within the buffer that is
being overflowed and modify the return address to point to the beginning of this
code. Shellcode quite literally means code that will launch a command prompt
(shell). The shellcode can come before or after the return address depending on
the amount of buffer space you have available. The goal is to get your shellcode
into a buffer somewhere and then modify the return address to point to it.
Chapter 17 ■ Bonus Topics 259
The following code shows a very simple shellcode. It uses the execve Linux
syscall to execute /bin/sh, which is a common shell application. execve is asking
the Linux kernel to do something. In this case, passing in the shell application
asks Linux to launch the shell.
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
exit(0);
}
This code relies on standard C methods for execve and exit, which will move
around in memory, making it difficult to predict their addresses and embed
them in the code. Meaning that if you took this assembly code as is, dropped
the opcodes into a buffer, and updated the return address to point to it, when
the code reaches the call execve instruction, it would likely segfault. This is
because the address compiled into the shellcode is where execve was loaded
for that application (0x8048340), but that is not a universal address. You would
need to know where execve is loaded for the target application (if it even has
execve at all). This makes it necessary to find an alternative way of popping a
shell that doesn’t involve C libraries.
260 Chapter 17 ■ Bonus Topics
If you disassemble the execve and exit methods, you can see the underlying
implementation, as shown in the following code sample:
mov eax, 0xb
mov ebx, string_addr
lea ecx, string_addr
lea edx, null_string
int 0x80 ;sys call for exec
mov eax, 0x1
mov ebx, 0x0
int 0x80 ;sys call for exit
“:/bin/sh”\0
So that solves some of the struggle, and the C library calls distill down into
the int 0x80 syscalls covered earlier in the book. But now there is another
challenge: the values of string_addr and null_string are unknown since
you can’t predict where they will be loaded in memory. Again, the assembled
shellcode placed them in that local memory space (in this example 0x8048518
is the compiled address for /bin/sh), but when the shellcode is dropped into
the target buffer, those addresses will be wrong.
Making the shellcode work requires figuring out another way to find the
address that is relative and not hard-coded. One way to learn this value is to
take advantage of return addresses in function calls; again, apply your immense
knowledge of calling conventions and the stack! If a function call is placed right
before the string, then the address of the string will be at the top of the stack
within that function (because the string is sitting at the function’s return address).
To start, add in a few place holders to the existing shellcode.
jmp ??
pop esi
mov [esi+0x8],esi
mov [esi+0x7],0x0
mov [esi+0xc],0x0
mov eax, 0xb
mov ebx, esi
lea ecx, [esi+0x8]
lea edx, [0xc+esi]
int 0x80
mov eax, 0x1
mov ebx, 0
int 0x80
call ??
.string \"/bin/sh\"
This code sample takes the initial shellcode and adds two instructions to
the front and two to the end. The next step is to determine the address of the
string, which is located at the end of the assembly block. Ideally, the initial jmp
instruction should jump down to the new call at the bottom.
Chapter 17 ■ Bonus Topics 261
Then, this call should call the new pop esi line. Why? When using a call
(instead of a jump) to get back up to the top of the code, the return address
(the next address after the call) will be placed on the stack. We have no inten-
tion of doing a normal cdecl stack setup; this is abusing x86 knowledge to do
naughty things.
After the call back up to pop esi, the top of the stack will have the return
address, which in this case is the shell string. This address can be popped off
the stack into esi and used in the previous shellcode.
Now, that sounds awesome, but there are currently placeholders for the jump
and call. To figure out where those are going to jump to, we have to count our
bytes. Here we count the compiled bytes to determine the correct offsets for
jmp and call:
This modified code solves the problem of finding the string in memory by
making it all relative (no hard-coded addresses) and uses the fundamental work-
ings of x86 to help. The final challenge is getting the code to run, which requires
placing a binary representation of the code on the stack via a buffer overflow.
The bolded lines illustrate the things added by the compiler for stack pro-
tection. The compiler added code that will save the return address on function
entry and will verify that it is unchanged after a strcpy operation. The com-
piler knows calls like strcpy can be dangerous; this prevents the strcpy from
overwriting the return address.
There are a few options for protecting against stack smashing, including
gcc’s built-in stack protections, the use of memory-safe languages with bounds
checking, and Data Execution Prevention (DEP). However, buffer overflows are
still a threat in some cases because not all compilers will support stack protec-
tion or DEP, and as you can see, there is nuance to how it protects, not adding
stack guards around every single call. Yet protections are focused against specific
things like strcpy. And many compilers are pretty smart about which are most
dangerous and need protection.
It’s also necessary to know how to pass information to that function, i.e.,
its calling convention. For this case we will assume our C functions are using
cdecl. Recall the following, with cdecl:
After writing the assembly code, the next step is to assemble it using nasm.
Here’s an example: nasm example.asm –o example.o.
At this point, everything will be in assembly except those placeholders. If you
had no external functions, your code would be ready to run, but since it does,
264 Chapter 17 ■ Bonus Topics
you need a linker’s help. The final step is to link your assembly code to the C
function. If you’re using gcc and calling functions from the C library, gcc can
handle this automatically. For example, gcc example.o -o example will use
the linker to fill out any addresses that it knows, transforming call 0x????????
to call 0x08048320.
For example, consider the following example, which runs
printf hello world 42:
extern printf
global main
section .text
main:
push 42
push world
push hello
call printf
add esp, 12
mov eax, 1
mov ebx, 0
int 0x80
section .data
hello: db "hello %s %d", 0xa, 0
world: db "world"
This assembly code can be assembled using nasm –f elf example.asm and
linked with gcc –m32 example.o –o example.
It can be very helpful and powerful to be able to call simple things like printf
from your assembly code while you’re testing your crack/patch ideas.
Then, assemble your assembly code with nasm and compile and link the
complete program with gcc.
For example, consider the following C program:
// x.c
#include <stdio.h>
int add(int,int);
int main(void)
{
int x=add(1,2);
printf("%d\n",x);
return 0;
}
This program uses the add function, which is defined in the following assem-
bly code:
; y.asm
add:
push ebp
mov ebp, esp
leave
ret
The start function is responsible for a few different tasks, including the
following:
■■ Initializing the frame pointer
■■ Configuring the stack
■■ Setting up the standard arguments (parameters to main())
■■ Calling libc_start_main, which performs security checks, threading
subsystem, init, calls your main function, and finally calls exit()
When writing pure assembly code, you write everything yourself. You don’t
need all of the setup C does and can write your own _start function.
Chapter 17 ■ Bonus Topics 267
When combining assembly and C, you need gcc to step in. Often, gcc wants to
provide its own _start function and expects you to provide a main() function.
When writing an assembly program that will be linked against the standard
C library, do the following:
1. Use main instead of _start (libc_start_main will call main() for you).
2. Set up a stack frame only, not the entire stack (_start has already config-
ured your stack).
3. Finish with ret, not int 0x80 (ret will return to libc_start_main, which
will call the C exit function, which will call int 0x80 for you).
4. Set the return value in eax before ret’ing (usually 0).
For example, consider the following stand-alone assembly program, which
defines its own _start:
global _start
section .text
_start:
mov esp, stack
mov ebp, esp
...
mov eax, 1
mov ebx, 0
int 0x80
section .data
times 128 db 0
stack equ $-
4
When linking to libc, the program should use main instead.
global main
section .text
main:
push ebp
mov ebp, esp
...
mov eax, 0
leave
ret
268 Chapter 17 ■ Bonus Topics
Standard Arguments
In C, arguments can be read from the command link with stdargs. For example,
main() is commonly defined as int main(int argc, char **argv), which pro-
vides access to these command-line arguments. Recall that argc is the number
of arguments passed in, and argv is an array that holds those arguments.
It’s also possible to access command-line arguments when writing a main
function in assembly. Your assembly version of main will be automatically called
with cdecl. Recall that the following:
■■ Arguments are passed on the stack, pushed on from right to left.
■■ Arguments are at [ebp+8], [ebp+12], etc.
■■ argc will be the last argument and is at the top of the list of arguments
on the stack, at [ebp+8].
■■ argv is the first argument pushed to the stack and will be at [ebp+12].
For example, the following assembly program will print the first command-
line argument:
extern printf
global main
main:
push ebp
mov ebp, esp
return 0;
}
The extended form of inline assembly lets you set advanced “constraints.”
These constraints can include the following:
■■ Input variables: C variables that you want to manipulate using assembly.
■■ Output variables: Values produced in the inline assembly code that you
want to use in the C code.
■■ Clobbered registers: gcc translates the C to assembly and figures out
which registers to use. This list ensures that the registers used by the C
and assembly code won’t conflict.
Extended assembly can be specified as follows:
__asm__(
“assembly”
: input constraints
: output constraints
: clobber list
);
int main(void)
{
// getting the return address for the current function
int x;
__asm__("\
movl 0x4(%%ebp), %%eax \n\
movl %%eax, %0 \n\
"
:"=r"(x)
:
:"%eax"
270 Chapter 17 ■ Bonus Topics
);
printf("%08x\n", x);
return 0;
}
Summary
This chapter demonstrated how to use an understanding of x86 and the stack
for hacking. By smashing the stack and inserting shellcode, a reverser can trick
a program into running the attacker’s code.
Conclusion
Wow, this has been quite a journey! We’ve covered offense to defense; high-level
languages down to assembly; registers, control flow, reverse engineering; patch-
ing, tools, techniques, and mindset. If you’ve made it this far, you have an
amazing baseline of knowledge to build from as you continue to move forward.
And as you do move forward, you will always encounter something new. At
first, it will be assembly instructions you don’t know, then defenses you’ve never
seen, then architectures you’ve never heard of, and of course the latest, greatest
tool-of-the-day or defense-of-the-year. But now that you have the basics, you’ll
find that new things become easier and easier to pick up quickly.
Now that you know mov, you can easily understand the string version movs.
You’ve worked with bit manipulations like not, so negation with neg makes
sense pretty quickly. You’ve mastered comparisons like cmp , so cmps isn’t
much of a stretch, and from there how about cmpxchg or cmpxchg16b or
lock cmpxchg8b? The gist is: now that you have the basics, it becomes increasingly
easy to understand new instructions; whether it’s ud (undefined instruction) or
gf2p8affineinvqb (Galois field affine transformation inverse), the fundamen-
tals tend to be mostly the same for everything.
But of course, learning more doesn’t end there. New instructions are great,
but if you keep on this path, you’ll soon encounter entirely new architectures.
The good news is, they also tend to follow the same basic patterns, and now
that you’ve mastered one, you’ll be able to understand new ones in no time.
x64 (64-bit x86) is easy now that you’ve done x86—just extend the registers to
64 bits (rax instead of eax, rsp instead of esp) and follow some different calling
conventions (AMD64 ABI in addition to cdecl), and you’ll be able to apply all
the same tools and techniques to 64-bit code. From there, Arm comes pretty
easily—again, it’s just new registers (r0 instead of rax), instructions (b instead
271
272 Conclusion
of jmp), and calling conventions (Arm instead of cdecl). The underlying patterns
tend to be mostly the same, so whatever your target—PowerPC, MIPS, RISC-V,
MIL-STD-1750A, etc.—you can usually learn the basics in a few hours. Expanding
to new architectures will also let you apply your skills to new devices. Whether
it’s phones, routers, cars, or satellites, the fundamentals are fairly uniform.
Naturally, as you keep advancing, you won’t just encounter new architectures;
you’ll encounter new tools as well. The good news here, too, is that they tend
to build off of the same base set of concepts. We’ve worked through a bevy of
disassemblers, hex editors, debuggers, and decompilers. Now it’s time to start
exploring new options to see what clicks with you. Ghidra, Binary Ninja, and
Cutter/radare2 are popular next steps that will build off of your experience
with IDA and offer even more ways to dissect and understand a program. As
you grow your arsenal of tools, you’ll gradually build up your own scripts,
workflows, and strategies to become increasingly proficient with more and
more difficult targets.
And, of course, if you keep at it, you’ll begin to encounter new defenses.
Whether it’s the latest anti-cheat in online gaming, a new opaque predicate
obfuscation from academia, or creative new hashing in an esoteric keychecker,
keeping up-to-date with the latest trends will help you stay sharp, whether your
passion is offense or defense. Both academic journals and cracking forums can
be fantastic resources here.
But whatever your end goals with this skill set, the singular key to moving
forward is practice. Try writing your own keychecker, and then see if you
can crack it—playing both sides at once can offer interesting insights into the
challenges and limitations of an adversary. Crackmes offer a fantastic, fun,
and (mostly) safe way to get experience in reverse engineering and software
modification on a wide variety of languages and architectures. Whenever you
have a few minutes, grab a crackme that seems in line with your experience
and skill level and see if you can defeat it; if you have a few hours, find one
that uses a language you don’t know or defenses you’ve never seen. Beyond
cracking, modifying simple programs can quickly offer new insights and expand
your skill set. Drop your favorite 90s video game into IDA and see if you can
add infinite lives; try out Ghidra on your favorite text editor and see if you
can add a secret menu. Alternatively, capture-the-flag competitions can be an
exciting way to push your reverse engineering skills to their limit, while simulta-
neously branching into new areas like binary exploitation and computer forensics.
However you proceed, stay persistent, keep practicing, and continue to push
your limits into new domains. As you do, we hope that this book has helped
you establish a broad baseline of skills and that you’ll use them to dive ever
deeper into this awesome facet of security.