Today we will reverse-engineer a simple “ransomware” made specifically for this purpose. Let’s give it the name Tupper. Tupper is not really a ransomware because it doesn’t ask for a ransom, it only attacks a specific location on the filesystem and I unintentionally forgot another important feature of ransomwares, try to find it. 🙂
The aim is to practice reversing and for beginners, learn how to use IDA without pseudo-code (we will not use the very handy F5 of hexrays). We’ll also write a decryption tool that abuses the cryptographic weaknesses of this binary. Because a ransomware usually communicates with a C&C server, we’ll also reverse-engineer the communication protocol used between the client and the server. Later, we’ll make the assumption that we’ve managed to put our hand on a copy of the C&C server’s binary, so that we can reverse it and have a complete overview on how this particularly bad designed “ransomware” works. In the end, our goal will be to find vulnerabilities in the server-side code and exploit it to gain a shell on the C&C server.
Reversing the Tupper client
Let’s start by running the command file on the Tupper client :
$ file client client: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f27ab53a856cf2d0c8a97b2de3486f5628e145e5, not stripped
It’s a 32-bits Linux binary and it’s not stripped, which means that debugging symbols are still present. When opening the binary with IDA the functions will be named as they were in source code.
We can run the command strings on the client to gain some more informations, maybe we’ll see some interesting strings :
$ strings client /lib/ld-linux.so.2 HoV( libgmp.so.10 _ITM_deregisterTMCloneTable __gmon_start__ _Jv_RegisterClasses _ITM_registerTMCloneTable __gmpz_init __gmpz_init_set_ui __gmpz_init_set_str __gmpz_clears __gmpz_powm _fini __gmp_sprintf libc.so.6 _IO_stdin_used socket strcpy exit readdir htons fopen strrchr perror __isoc99_sscanf connect closedir __stack_chk_fail strlen send getlogin_r recv __isoc99_fscanf fclose opendir stderr gethostbyname fwrite fread fprintf strcmp __libc_start_main ferror snprintf _edata __bss_start _end GLIBC_2.4 GLIBC_2.1 GLIBC_2.7 GLIBC_2.0 PTRh .ENO .ENO UWVS t$,U [^_] %02X %2hhx .pdf .txt 98364165919251246243846667323542318022804234833677924161175733253689581393607346667895298253718184273532268982060905629399628154981918712070241451494491161470827737146176316011843738943427121602324208773653180782732999422869439588198318422451697920640563880777385577064913983202033744281727004289781821019463 %0256Zx Could not open file for encryption /tmp/ransom/.id %s/%s Could not create socket evil.eno.cc Unknown host %s. connect failed. Error /tmp/ransom ;*2$" GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 crtstuff.c __JCR_LIST__ deregister_tm_clones __do_global_dtors_aux completed.7209 __do_global_dtors_aux_fini_array_entry frame_dummy __frame_dummy_init_array_entry client.c __FRAME_END__ __JCR_END__ __init_array_end _DYNAMIC __init_array_start __GNU_EH_FRAME_HDR _GLOBAL_OFFSET_TABLE_ __libc_csu_fini strcmp@@GLIBC_2.0 _ITM_deregisterTMCloneTable __x86.get_pc_thunk.bx YrjPJBLxgh __isoc99_fscanf@@GLIBC_2.7 cifjwJUHVy stderr@@GLIBC_2.0 __gmpz_clears jZigriWhiq ferror@@GLIBC_2.0 _edata fclose@@GLIBC_2.1 gjsHYqnQSZ gqWpzyMmnT __gmpz_powm __stack_chk_fail@@GLIBC_2.4 __gmp_sprintf htons@@GLIBC_2.0 VefQgDMsVb getlogin_r@@GLIBC_2.0 perror@@GLIBC_2.0 fwrite@@GLIBC_2.0 fread@@GLIBC_2.0 strcpy@@GLIBC_2.0 __data_start __gmon_start__ exit@@GLIBC_2.0 hTtYrgYKSh __dso_handle _IO_stdin_used strlen@@GLIBC_2.0 __libc_start_main@@GLIBC_2.0 fprintf@@GLIBC_2.0 __libc_csu_init qaciBHvkjt sJdFNfLAtw __isoc99_sscanf@@GLIBC_2.7 fopen@@GLIBC_2.1 ibXwMYRwgj snprintf@@GLIBC_2.0 _fp_hw aCLhfJaEkB __bss_start main __gmpz_init_set_ui readdir@@GLIBC_2.0 cNWBjMPuEf strrchr@@GLIBC_2.0 _Jv_RegisterClasses __gmpz_init_set_str sprintf@@GLIBC_2.0 socket@@GLIBC_2.0 __TMC_END__ izuWTGiAKw _ITM_registerTMCloneTable __gmpz_init gethostbyname@@GLIBC_2.0 connect@@GLIBC_2.0 recv@@GLIBC_2.0 iaTQydjqLw close@@GLIBC_2.0 closedir@@GLIBC_2.0 opendir@@GLIBC_2.0 send@@GLIBC_2.0 .symtab .strtab .shstrtab .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .jcr .dynamic .got.plt .data .bss .comment
Just by looking at the output of this command, we can see the following :
- gmp functions to work with big numbers
- a big number is present (a key ?)
- 2 extensions can be identified
- path and file in /tmp/ransom
- network related functions
- something that looks like a hostname : evil.eno.cc (C&C server ?)
- function names seem to be obfuscated
Time to open it with IDA !
On the left sidebar we can see the obfuscated custom functions. In the graph of the decompiled main() function, we can clearly see a call to gethostbyname() with the string “evil.eno.cc” as parameter, in the block number 3. Block 1 is simply a socket creation and block 2 is the error handling. Usually, error handling blocks will be pointed by red arrows but they are easily identified because they are short and often end with a call to exit().
Block 4 and 6 are error handling blocks. Block 5 makes a call to htons() followed by a call to connect(). The documentation for connect() says that a sockaddr struct is passed as the addr argument. This struct contains the IP and port for the connection, in the sa_data field. We already found the hostname, we just need to find the port. The parameter passed to htons() seems promising but it’s in hexadecimal, let’s convert it to decimal by clicking on it and pressing the key “h”. It turns out that it corresponds to the number 1337, which seems like a good port number !
When reversing a binary, it’s important to keep written or mental notes about features we discover. We can fill in the newly acquired informations in our knowledge base :
The Tupper client connects to the C&C server at evil.eno.cc:1337
Once the connection is successful, the socket’s file descriptor is stored in the variable fd. Let’s rename it to socket by clicking on it’s name (in green) and pressing the key “n”.
The interesting part begins in block 7. The first call to a user-defined function (gqWpzyMmnT) is made right after the connection has been successfully established. This function seems to have a single parameter, the socket’s file descriptor. IDA seems to have correctly declared the function thanks to the debugging symbols that are still present.
Because the Tupper client is relatively small and for the sake of the exercise, we’re going to reverse every user-defined function. That’s why we’re diving into this new function before continuing. Let’s disassemble the new function and verify the type declaration by pressing the key “y”.
The declaration is incorrect, the function doesn’t return anything. It’s not necessary but for the sake of correctness, change the return type to void. Now, let’s look at the first block.
There is a call to getlogin_r() which writes the logged in username in the buffer called name. There is no error handling which is a sign that Tupper was developed by poorly skilled or lazy peoples. 🙂
Just after follows a call to a new user-defined function (jZigriWhiq) with no parameters.
This function is quite simple. At the beginning we see the stack canary being stored on the stack (var_C) and it’s checked at the end of the function (block 3). If changed, stack_chk_fail() is called.
Then, the file “/tmp/ransom/.id” is opened in read mode and the file descriptor is stored in the variable stream. A check is made to ensure the file was correctly opened, otherwise it jumps directly to block 3.
Block 2 performs a call to fscanf(“%u”, &var_14). This reads the first unsigned int value in the file. This value is then stored in the EAX register before returning so it’s the return value.
Basically this function returns some sort of ID that is stored in the file “/tmp/ransom/.id”. We’ll rename the function to getID() by clicking on its name and pressing “n”. We can update our knowledge base :
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads an ID from /tmp/ransom/.id
We can go back to the previous function (press “esc”) and rename the variable that stores the return value to id.
After having read the ID, the length of the username is stored in the local variable n. Just after that, a call to send() is prepared. The data sent over the socket begins at buf and is 0xc (=12) bytes long. Let’s check the size of buf on the stack.
buf is only 4 bytes long so it’s not the only thing being sent over the socket. 12 bytes starting at buf contains also the two 4-bytes variables id and n. This looks like a struct that is being sent, that’s why we’re going to construct it’s definition in IDA. This can be done in the “Structures” tab and by pressing “insert”. Let’s call it packet_t and define the 3 fields (press “d” multiple times on the newly created struct). We are only sure that the last two fields are 4 bytes long, therefore we’re going to define 4 db (define bytes), followed by 2 dd (define doubles = 4 bytes). We can now rename them like the name of the variables on the stack (press “n”).
Now let’s go back to the stack frame view and click on buf then press “alt+q” to change the type to the struct we just created. We can now rename our struct instance to something like packet.
Now we can go back to our function and see that IDA now shows us the struct and members. We can even do more. Because we know that 12 is the struct’s size, click on it and press “t”, IDA can rename it to “size packet_t” for better readability. Now we can see that packet.buf is set to a number that looks like printable ASCII (press “r” on it).
To recap, the first call to send() sends a struct containing 3 times 4 bytes :
‘i\x00\x00\x00’ | getID() | strlen(username)
Then, the next call to send() simply sends the username.
The next call is a call to recv(). The amount of data to be received is 12 (sizeof(packet_t) again) and is stored in var_58. It looks like another packet_t structure is present on the stack to contain the server’s response. We can tell IDA about this new information just like we did before.
In block 2, a check is made on a single char (buf), if it’s equal to ‘i’, it goes to block 4. Otherwise, it checks if it’s equal to ‘d’. If it is, the client exits. Otherwise the function exits. This looks like a switch statement on the first field of packet_t struct. The first char of the struct must play the role of packet type. We can now rename our struct members to better describe their purpose. It’s very important to do that as soon as a new information is discovered.
Time to enter the new function (ibXwMYRwgj), which takes the server_response.id (unsigned int) as parameter and modify it’s type declaration.
Very simple function that writes the ID received from the server in the file “/tmp/ransom/.id”. We can rename this function to writeID() and the previous one to clientHello() because it’s some sort of identification of the Tupper client to the C&C server.
Let’s go back to the main function and rename/change variables’ type if necessary before continuing. Also update the knowledge base :
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id
We’re back in block 7 after the call to clientHello(), there is another packet received. If this packet is of type ‘s’ it does something, otherwise it closes the connection and exits.
The function (aCLhfJaEkB) takes 2 parameters, the socket (int) and a path (char *) and doesn’t return anything (no saving of EAX after the call). Let’s reverse it.
The graph view isn’t particularily adapted for this function so better switch to the flat representation (press “space”) and start renaming.
This function calls opendir() with “/tmp/ransom” as parameter. After that the first entry is read with readdir() and stored in directory_entry. A call to snprintf() performs the concatenation of the initial path (“/tmp/ransom”), “/” and directory_entry->d_name and stores this new path in newPath. Next is checked whether directory_entry->d_type == DT_DIR (4).
In case the entry is a directory, a verification is made on it’s name. Basically it skips the directories named “.” and “..” and does a recursive call to itself with the new directory path.
If it’s a file, the function (gjsHYqnQSZ) is called with the socket and path to the file as argument.
To recap, this function scans every file under “/tmp/ransom” and applies the function gjsHYqnQSZ() to them. Because we know that Tupper encrypts our files, we can assume that this function will encrypt a given file so we are going to rename it to encryptFile(). We can change it later if it’s incorrect. As for the function we are currently in, we can rename it encryptFileSystem().
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id Tupper only encrypts files under /tmp/ransom
We’re approching the interesting part, let’s enter the encryptFile() function.
Right at the beggining, a call to the function cNWBjMPuEf() is made so we’re going to look at this one first.
This function is checking the file extension and returns 1 if it’s “.pdf”, 2 if it’s “.txt” and 0 otherwise. We can call this one checkExtension().
After the call to checkExtension(), a new packet is prepared of type “e”, that’s why getID() is called. The length of filePath is sent in the packet as well and then we clearly see a switch condition on the extension. It seems like Tupper handles files differently based on their extension.
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id Tupper only encrypts files under /tmp/ransom Tupper handles files with extensions .pdf and .txt differently
Here we can see that nothing is done to files that do not have the extension “.pdf” or “.txt”.
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id Tupper only encrypts files under /tmp/ransom that have the extension .pdf or .txt Tupper handles files with extensions .pdf and .txt differently
The only difference between case_txt and case_pdf is the function called (encryption method probably) and a letter that is sent to the C&C server afterwards. Once the encryption has been done, the packet of type “e” crafted at the beginning is sent, followed by a letter (“r” or “x”) and finally the file’s path is sent.
Let’s start by looking at how “.txt” files are handled by reversing cifjwJUHVy().
The file passed as parameter is opened in read mode. A string copy of the file’s path is stored in FileNameAfter and concatenated with the new extension “.ENO”. This new file is then opened in write byte mode which creates the file if not already present.
The file is read in chunks of 43 bytes at a time and placed in a buffer. This buffer is then passed to iaTQydjqLw() along with var_4F2 (output buffer ?). Then comes a call to VefQgDMsVb() with var_4F2 and ptr (output buffer ?) as parameters. The content of ptr is then written to the newly created encrypted file. This process is repeated until no more content is read from the original file. This is has to be the encryption process. We need to understand those 2 functions. Let’s start with iaTQydjqLw().
This little function uses sprintf() to convert a string in it’s hexadecimal representation. It needs 3 parameters, the first is the string to convert, the second is a pointer to a buffer where the result is stored (should be 2 times bigger than the original) and the last is the number of bytes to convert. We can rename this function to strToHex(). Now let’s move on to VefQgDMsVb().
This function take our hexadecimal representation as parameter and a pointer to an output buffer. It uses gmp to work with big numbers as we saw at the beginning. Globally, this function works with 4 gmp numbers :
- Three, which is just 3.
- bigNum, initialised from a string representing a big number in base 10.
- inputAsNum, initialised from our hexadecimal string interpreted as a number in base 16.
- resultingNumber, the result of inputAsNum**3 % bigNum
resultingNumber is then converted in it’s hexadecimal representation with gmp_sprintf() (equivalent of sprintf() in gmp) and the result is stored in a buffer which is then passed to an unknown function sJdFNfLAtw().
To cut short, this function uses sscanf() to do the invert of strToHex(). It’s converting an hexadecimal representation of a string into a string. We can rename it hexToStr().
To me, this looks like RSA encryption. We’ll go more into the details in another part, for now it’s time to update the knowledge base and move on to the PDF files.
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id Tupper only encrypts files under /tmp/ransom that have the extension .pdf or .txt Tupper encrypts files with extensions .txt in chunks, using RSA and hardcoded public key.
The first thing done after variable initialisation in the PDF encryption function is calling qaciBHvkjt(). Let’s analyse this one first.
The Tupper client sends a packet of type “k” to the C&C server and receives a response. If the response packet is also of type “k”, the client receives server_response.n bytes of additional data and writes it in the buffer passed as parameter. We can guess that the field n of packet_t is an indicator that a given amount of additional information follows, so we can rename it to additional_data_len. It’s important to note that no verification is made on the size of the buffer, if someone modifies the packet and puts a big additional length, a buffer overflow on the client might occur. Seeing such mistakes in the Tupper client can give us hints that the C&C server might be full of security holes as well.
We can name this function getKey() because we’ll suppose that the client asks for a key to the C&C server.
The PDF encryption function is very similar to the TXT one. I’ve put only the part that differs. After having read a chunk of 256 bytes from the original file, this chunk is passed to the function hTtYrgYKSh() along with the key the client receives from the server, an output buffer and the length of the chunk read.
This function performs a xor between two strings. The first parameter is the string to be xored, the second is the key, the third is the output buffer and the last is the size of the string to be encrypted. We can rename this function to strXor().
There’s only one last function to understand in order to have the complete picture of the Tupper client. This function is izuWTGiAKw() and it’s called after having written the xor encryption result to a file. A single argument is passed to it, the key.
Well, this doesn’t look very interesting, the important thing to notice is that it modifies the key that was passed as a parameter. It must be some kind of key scheduling, nothing we’re interested in right now. We can rename it keySchedule().
We are done with the client ! Time to update the knowledge base :
The Tupper client connects to the C&C server at evil.eno.cc:1337 The Tupper client reads and writes an ID in /tmp/ransom/.id Tupper only encrypts files under /tmp/ransom that have the extension .pdf or .txt Tupper encrypts files with extensions .txt in chunks, using RSA and hardcoded public key. Tupper encrypts files with extensions .pdf in chunks, using xor encryption and a key given by the C&C server for each file. The xor key is modified between each chunk that is encrypted.
In the next part we’ll try to find cryptographic weaknesses to break the two encryptions methods used by Tupper.