Recently I started making a small library for loading and converting PE files (libpeconv, available on my GitHub). The library is still on early stages of development, so please don’t judge and don’t use it in any serious projects. The API may change anytime! However, I have so much fun developing and testing it, that I wanted to share some of my experiments and ideas.
Some time ago I solved some of the FlareOn4 challenges, i.e. the challenge 3. That time I didn’t have the libpeconv
yet, so I solved it by some other method. Now it came to my mind, that with the help of my new library solving it could be way much faster and easier. In this post I will describe my alternative solution and some of the related experiments.
Tool used
For the static analysis:
- IDA (demo version is enough)
For building the projects:
Overview
The challenge named greek_to_me.exe
is a 32bit PE file. It has stripped relocations.
When we deploy it, it shows the empty console and waits. It is not reading any data from the standard input, so we can conclude that it is using some another way to read the password from the user.
We will start from some static analysis in IDA. The crackme has a very simple and clean structure, it is not obfuscated. We can see that at the beginning of the execution it creates a socket and waits for the input.
The socket listens at localhost on port 2222:
After getting the connection, it reads 4 bytes from the input into the buffer:
After it read 4 bytes, it starts processing the input and uses it for decoding an encrypted buffer:
If the checksum is valid, it means the encrypted code was decrypted properly, and it is further executed.
As we can see, only 1 byte of the input is used for decoding the buffer, so we can easily brutforce it. The code responsible for decoding the buffer is also pretty simple:
const size_t encrypted_len = 0x79; for (int i = 0; i < encrypted_len; i++) { BYTE val = encrypted_code[i]; encrypted_code[i] = (unknown_byte ^ val) + 0x22; }
The only part of the crackme that may be somehow challenging is the checksum – this function is not that simple to reimplement. However, if we want to make a brutforcer, we need to be able to calculate the checksum after every attempt.
In my previous solution, I just reimplemented the checksum – it worked but it was not so much fun 😉 . I saw also some other approaches such as emulating the checksum function by the Unicorn engine, using angr framework, or making a brutforcer that talks to the original program via socket. Can it be done even faster? Let’s see…
LibPeConv comes into play
With PeConv we can convert any PE file from raw format to virtual and back. It also provides a custom PE loader – it’s goal is to provide a possibility of loading any PE file into the current process (even if it is not a DLL and even if it does not have no relocations table – it will be explained in the further part). This loaded PE can be later used as a fully functional PE file that can run from inside the curent process. We can also use any selected function from its code – all we need to know is the function’s RVA and the API.
In this case, I will use libpeconv
to load the crackme and import from it the function calculating checksum. Also, rather than copying the encrypted buffer to my code, I will read it directly from the loaded PE.
Preparing the required information
Let’s take a look at the crackme again in IDA. We need to find the appropriate offsets and understand the API of the function that we are going to import.
The function calculating the checksum starts at RVA 0x11E6:
It takes 2 arguments: pointer to the buffer and it’s size.
It returns a WORD type:
Summing up, we can define the function prototype as:
WORD calc_checksum(BYTE *decoded_buffer, size_t buf_size)
It is also worth to note, that this function is self-contained and does not call any imported libraries – that makes importing it even easier (we are not forced to load any imports for the module or to apply relocations).
Another thing that we need is the encrypted buffer. It starts at RVA 0x107C and is 0x79 (121) bytes long:
That’s all! Let’s start coding.
Solving the crackme with libPeConv
The current version of libpeconv
allows to load PE file in two ways. By the function load_pe_module
and by the function load_pe_executable
. The second one: load_pe_executable
is a complete loader, that loads given PE to the current process in the RWX memory, automatically applies relocations and load dependencies. The first one (load_pe_module
) does not load the dependencies and also gives more control: we may load the PE file in non-executable memory and applying the relocations is optional. More information (and *very* possible updates on the API) you can find here:
https://github.com/hasherezade/libpeconv/blob/master/libpeconv/include/peconv/pe_loader.h
As we saw, the function that we want to import is self-contained, so it will not harm if we load the crackme PE without imports and without relocations (to see it loaded as a fully functional PE see the next part of the article). I will use a function load_pe_module
BYTE* loaded_pe = (BYTE*)load_pe_module( path, v_size, // OUT: size of the loaded module true, // executable false // without relocations );
Now, let’s import the function. First let’s make a pointer to it:
WORD (*calc_checksum) (BYTE *buffer, size_t buf_size) = NULL;
Calculate the absolute offset to the function within the loaded module:
ULONGLONG offset = DWORD(0x11e6) + (ULONGLONG) loaded_pe;
And filling the pointer:
calc_checksum = ( WORD (*) (BYTE *, size_t ) ) offset;
That’s it, now we can use the function in our application like any other function.
But before we can start brutforcing, we also need to fill the pointer to the buffer:
g_Buffer = (uint8_t*) (0x107C + (ULONGLONG) loaded_pe);
This is the full brutforcer that I perpared:
https://gist.github.com/hasherezade/44b440675ccc065f111dd6a90ed34399#file-brutforcer_1-cpp
And it works 🙂 The value that we got is exactly what it was supposed to be:
But still, the found value is just a part of the solution, not the flag that we are searching for. As we know from the static analysis, if this value is given correct, the chunk of code will be decrypted and executed. Would be cool to see how exactly that chunk of code looks when it is written into it’s place, don’t you think?
And also it is very easy to achieve. The PE file is loaded in the RWX memory inside the current process – so we can easily substitute the encrypted chunk of code with the decoded. Simple memcpy
will do the job:
memcpy(g_Buffer, g_Buffer2, g_BufferLen);
Then, libPeConv
will help us to convert the PE file back to the raw format, so that we can open it in IDA. We can do it with the help of pe_virtual_to_raw
from libpeconv
:
size_t out_size = 0; BYTE* unmapped_module = pe_virtual_to_raw( loaded_pe, //pointer to the module v_size, //virtual size module_base, //in this case we need here //the original module base, because //the loaded PE was not relocated out_size //OUT: raw size of the unmapped PE );
And this is the complete solution:
https://gist.github.com/hasherezade/36a4a531840cfe1fd5997bc7c5f6be4d#file-brutforcer_2-cpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include "peconv.h" | |
BYTE *g_Buffer = NULL; | |
const size_t g_BufferLen = 0x79; | |
BYTE g_Buffer2[g_BufferLen] = { 0 }; | |
WORD (*calc_checksum) (BYTE *decoded_buffer, size_t buf_size) = NULL; | |
bool test_val(BYTE xor_val) | |
{ | |
for (size_t i = 0; i < g_BufferLen; i++) { | |
BYTE val = g_Buffer[i]; | |
g_Buffer2[i] = (xor_val ^ val) + 0x22; | |
} | |
WORD checksum = calc_checksum(g_Buffer2, g_BufferLen); | |
if (checksum == 0xfb5e) { | |
return true; | |
} | |
return false; | |
} | |
BYTE brutforce() | |
{ | |
BYTE xor_val = 0; | |
do { | |
xor_val++; | |
} while (!test_val(xor_val)); | |
return xor_val; | |
} | |
//— | |
bool dump_to_file(char *out_path, BYTE* buffer, size_t buf_size) | |
{ | |
FILE *f1 = fopen(out_path, "wb"); | |
if (!f1) { | |
return false; | |
} | |
fwrite(buffer, 1, buf_size, f1); | |
fclose(f1); | |
return true; | |
} | |
int main(int argc, char *argv[]) | |
{ | |
#ifdef _WIN64 | |
printf("Compile the loader as 32bit!\n"); | |
system("pause"); | |
return 0; | |
#endif | |
char default_path[] = "greek_to_me.exe"; | |
char *path = default_path; | |
if (argc > 2) { | |
path = argv[1]; | |
} | |
size_t v_size = 0; | |
BYTE* loaded_pe = peconv::load_pe_module(path, | |
v_size, | |
true, // load as executable? | |
false // apply relocations ? | |
); | |
if (!loaded_pe) { | |
printf("Loading module failed!\n"); | |
system("pause"); | |
return 0; | |
} | |
g_Buffer = (BYTE*) (0x107C + (ULONGLONG) loaded_pe); | |
ULONGLONG func_offset = 0x11e6 + (ULONGLONG) loaded_pe; | |
calc_checksum = ( WORD (*) (BYTE *, size_t ) ) func_offset; | |
BYTE found = brutforce(); | |
printf("Found: %x\n", found); | |
memcpy(g_Buffer, g_Buffer2, g_BufferLen); | |
size_t out_size = 0; | |
/*in this case we need to use the original module base, because | |
* the loaded PE was not relocated */ | |
ULONGLONG module_base = peconv::get_image_base(loaded_pe); | |
BYTE* unmapped_module = peconv::pe_virtual_to_raw(loaded_pe, | |
v_size, | |
module_base, //the original module base | |
out_size // OUT: size of the unmapped (raw) PE | |
); | |
if (unmapped_module) { | |
char out_path[] = "modified_pe.exe"; | |
if (dump_to_file(out_path, unmapped_module, out_size)) { | |
printf("Module dumped to: %s\n", out_path); | |
} | |
peconv::free_pe_buffer(unmapped_module, v_size); | |
} | |
peconv::free_pe_buffer(loaded_pe, v_size); | |
system("pause"); | |
return 0; | |
} |
Comparing the dumped executable with the original one we can see that the buffer was overwritten:
So let’s see the modified exe in IDA:
And yes! At the known offset there is the flag revealed:
et_tu_brute_force@flare-on.com
Bonus – loading and running a PE with stripped relocations
Ok, you may say – it was easy – the loaded function was self contained, so we could as well rip it off from the original file, not using any loaders. But what if the function calls several other functions within the given module and also imported functions? Will the same trick work? And could it work even for PE file without relocations?
To answer those questions I prepared another test case. Now, instead of loading one function, I will load and execute the full crackme from inside the brutforcer.
First we will modify few things. This time, instead of using load_pe_module I will use load_pe_executable – to load full executable with dependencies.
BYTE* loaded_pe = (BYTE*)load_pe_executable(path, v_size);
The function will automatically detect that the PE file has no relocations, and enforce loading it at it’s original module base. Mind the fact, that allocating memory at the specific base may not always work – so, sometimes it takes several runs to execute it properly. You must also make sure that the module base of the loader does not collide with the module base required by the payload (if the loader’s base is random it is good enough).
Once the PE file is loaded, we just need to get it’s Entry Point – and then we can call it like any other function*:
// Deploy the payload: // read the Entry Point from the headers: ULONGLONG ep_va = get_entry_point_rva(loaded_pe) + (ULONGLONG) loaded_pe; //make pointer to the entry function: int (*loaded_pe_entry)(void) = (int (*)(void)) ep_va; //call the loaded PE's ep: int ret = loaded_pe_entry();
* – but mind the fact that depending on the payload’s implementation details, once you redirected your execution to it’s entry point, it may just exit after finishing it’s job and never return back to your code.
I am going to modify the brutforcer code in such a way, that this time after finding the value the original crackme will be run. This is the code of the full application:
https://gist.github.com/hasherezade/9d5186b27c730d01849ac1787b3d699b#file-brutforcer_3-cpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include "peconv.h" | |
BYTE *g_Buffer = NULL; | |
const size_t g_BufferLen = 0x79; | |
BYTE g_Buffer2[g_BufferLen] = { 0 }; | |
WORD (*calc_checksum) (BYTE *decoded_buffer, size_t buf_size) = NULL; | |
bool test_val(BYTE xor_val) | |
{ | |
for (size_t i = 0; i < g_BufferLen; i++) { | |
BYTE val = g_Buffer[i]; | |
g_Buffer2[i] = (xor_val ^ val) + 0x22; | |
} | |
WORD checksum = calc_checksum(g_Buffer2, g_BufferLen); | |
if (checksum == 0xfb5e) { | |
return true; | |
} | |
return false; | |
} | |
BYTE brutforce() | |
{ | |
BYTE xor_val = 0; | |
do { | |
xor_val++; | |
} while (!test_val(xor_val)); | |
return xor_val; | |
} | |
//— | |
int main(int argc, char *argv[]) | |
{ | |
#ifdef _WIN64 | |
printf("Compile the loader as 32bit!\n"); | |
system("pause"); | |
return 0; | |
#endif | |
char default_path[] = "greek_to_me.exe"; | |
char *path = default_path; | |
if (argc > 2) { | |
path = argv[1]; | |
} | |
size_t v_size = 0; | |
BYTE* loaded_pe = peconv::load_pe_executable(path, v_size); | |
if (!loaded_pe) { | |
printf("Loading module failed!\n"); | |
system("pause"); | |
return 0; | |
} | |
g_Buffer = (BYTE*) (0x107C + (ULONGLONG) loaded_pe); | |
ULONGLONG func_offset = 0x11e6 + (ULONGLONG) loaded_pe; | |
calc_checksum = ( WORD (*) (BYTE *, size_t ) ) func_offset; | |
BYTE found = brutforce(); | |
printf("Found: %x\n", found); | |
// Deploy the payload! | |
// read the Entry Point from the headers: | |
ULONGLONG ep_va = peconv::get_entry_point_rva(loaded_pe) + (ULONGLONG) loaded_pe; | |
//make pointer to the entry function: | |
int (*loaded_pe_entry)(void) = (int (*)(void)) ep_va; | |
//call the loaded PE's ep: | |
printf("Calling the Entry Point of the loaded module:\n"); | |
int res = loaded_pe_entry(); | |
printf("Finished: %d\n", res); | |
system("pause"); | |
return 0; | |
} |
To make sure that everything works fine (the deployed payload really creates socket and gives response in exactly the same way like the one deployed independently), I wrote a small Python script that will communicate with it and display the response:
https://gist.github.com/hasherezade/328210a57464360e23e125929b62b301#file-test-py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import socket | |
import sys | |
import argparse | |
def main(): | |
parser = argparse.ArgumentParser(description="Send to the Crackme") | |
parser.add_argument('–key', dest="key", default="0xa2", help="The value to be sent") | |
args = parser.parse_args() | |
my_key = int(args.key, 16) % 255 | |
print '[+] Checking the key: ' + hex(my_key) | |
key = chr(my_key) + '012' | |
try: | |
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
s.connect(('127.0.0.1', 2222)) | |
s.send(key) | |
result = s.recv(512) | |
if result is not None: | |
print "[+] Response: " + result | |
s.close() | |
except socket.error: | |
print "Could not connect to the socket. Is the crackme running?" | |
if __name__ == "__main__": | |
sys.exit(main()) | |
And now, let’s see it all in action:
This is all what I prepared for today, I hope you enjoyed it! The lib is now under rapid development so many things will get refactored and improved, stay tuned!
The binaries of all the presented loaders, along with the crackme, are available here: https://drive.google.com/open?id=1ZFnRsuZxdlw6j2OVEfIJCLfmd8jwmu7y – the password to the zip is: crackme
Appendix
See other approaches to solve the same crackme:
Hi hasherezade, did you first learn to write programs in C before learning to reverse?
hi! yes, I learned C/C++ as a teen and then I worked many years as a programmer, doing reversing just as my hobby.
amazing tools for CTFer, greate work