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 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 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 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’ types 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 from
/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 from
/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 begining, 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 from
/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 from
/tmp/ransom/.id
Tupper only encrypts files under
/tmp/ransom
that have the extension .pdf or .txtTupper 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 ofinputAsNum**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 from
/tmp/ransom/.id
Tupper only encrypts files under
/tmp/ransom
that have the extension .pdf or .txtTupper 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 from
/tmp/ransom/.id
Tupper only encrypts files under
/tmp/ransom
that have the extension .pdf or .txtTupper handles files with extensions .pdf and .txt differently
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.