In this part we’re going to suppose that we’ve managed to obtain a copy of the Tupper C&C server binary. Our goal is to reverse engineer it and summarize the overall operating process of Tupper.
Reversing the Tupper C&C server
Let’s open up the binary in IDA and start reversing !
The main() function’s role is to setup a listening connection on port 1337. After a new connection has been accepted, a new thread is created to handle the client. Here is only the interesting part.
When creating a thread with pthread_create(), the argument start_routine defines the function that is going to run in this new thread. This function also takes a single argument, in this case it’s a pointer to the socket. reNpyZhYDe() is the function we need to analyse.
We see a call to recv() with a buffer of size 12. Knowing from having analysed the client, that a structure of size 12 (packet_t) is sent from the client, we have to redefine the same structure here.
When a packet is received, a function is called with the socket and a pointer to the packet as argument. It’s safe to assume that this function will perform different actions depending on the type of packet received, therefore we can rename it handleClientPacket().
In this function we can clearly identify a switch condition on the packet type and we see 3 possible packet types, exactly as we saw on the Tupper client. In each case, a different function is called with socket and pointer to the packet as argument. But before that, we have to look at the first function call to BEYcZJipvr().
This function looks very familiar, in fact it’s exactly the same as strToHex() on the client. With that aside, we can analyse one of the 3 previous cases. Maybe it’s best to start with the case that handles the very first packet sent by the client, the packet of type “i”.
This function is quite big so let’s start slowly. First, a test is made on the packet_t.client_id to test if it’s zero, otherwise we jump to another section of the function. Then a comparison is made on the value of a global variable (stored in the data segment, ds:n_victims). It verifies that the number of victims is not greater than 100, otherwise we jump to another section again.
If both conditions are satisfied, a 60 bytes buffer is dynamically allocated using malloc(). Then rand() is called two times and some operations are applied to the random numbers generated. The resulting number is then stored in the new buffer at offset 0x38 (=56). This a big indication on the nature of this buffer that tells it’s probably a struct with a field containing an int at this offset. 4 instructions later, we see that this number is then stored in packet_t.client_id (which was previously 0). This must be the client ID generation when a new victim is infected. Let’s create this new struct in IDA.
That’s all the information we have for now on this struct. We’ll complete it later on.
After saving the new ID, a new offset (0x34=52) in the struct is set to 0. We found a new field to add in the structure’s definition.
The server checks if additional data will follow the received packet and receives the corresponding amount in the unknown_t structure at offset 0. No length verification is made before receiving and we know for sure that there is data at offset 52 in our struct. If the additional data length is bigger than 52 bytes, data in the struct will be overwritten ! Let’s note that somewhere, it could be useful for later.
Buffer overflow when receiving additional data after a packet of type "i".
A pointer to the new struct is appended in a global list called victims at offset n_victims, n_victims is incremented by 1 afterwards. Our new struct must be storing information about each victim. We know from analysing the client that the logged in username is sent to the C&C server just after a packet of type “i”. That’s what is being stored at offset 0 in the structure and then printed server-side. A packet of type “i” containing the newly generated id is sent back to the client, followed by a packet of type “s”.
Let’s recap what we know about the Tupper C&C :
Receives packet "i" + client_id==0 (New victim registration) : id creation, victim info added to list, packet "i" with new id sent, packet "s" sent.
Now let’s take a look at what is done if client_id != 0.
The client ID is passed to the function BVbFUrAGRW() right away.
This function iterates over all victims contained in ds:victims and searches for a victim with the given client ID. If none is found, a NULL pointer is returned, otherwise a pointer to the victim_t (previously named unknown_t) structure containing victim information is returned. Let’s call this function getVictimWithId()
So, when receiving a packet of type “i” with a non-zero client ID, the server searches if this ID corresponds to a known victim. If it’s the case, a message is printed server-side indicating a reconnection and a packet of type “d” is sent back to the client. Remember, when the client receives a packet of type “d”, it exits. And if there was additional data after the packet of type “i”, this data is stored as the username in the victim_t struct. Same buffer overflow issue as before.
If the client ID doesn’t correspond to any in the list, a message is printed server-side and a packet of type “d” is sent back to the client. Additional data received after is stored in a 100 bytes buffer and not used. Again, their is a buffer overflow here.
In case the maximum number of victims has been reached, a packet of type “d” is sent to the client. Let’s recap :
Receives packet "i" + client_id==0 (New victim registration) : id creation, victim info added to list, packet "i" with new id sent, packet "s" sent. Receives packet "i" + client_id!=0 (Victim reconnection) : packet "d" sent. Receives packet "i" + n_victim>=100 (Too many victims) : packet "d" sent.
We can say that this function handles victim registration.
Now we’re going to look at the function called when a packet of type “k” is received by the C&C server. This type of packet was sent by the Tupper client before each PDF file encryption to retrieve the xor key.
Once again, the server attempts to retrieve the victim corresponding to the given ID. If the victim is known, a packet of type “k” is sent to the client. Then a verification is made on the second field of victim_t (the only one we didn’t know it’s use) if this value is not zero, the 8 bytes stored at this address are sent to the client. Now we know that this field stores a pointer to an 8 bytes key.
If the pointer is NULL, a new key buffer is allocated with malloc() and referenced in the victim_t structure. The key is generated by reading 8 bytes from “/dev/urandom”. Once generated, the key is sent to the client. This function handles the victim’s key requests.
Receives packet "i" + client_id==0 (New victim registration) : id creation, victim info added to list, packet "i" with new id sent, packet "s" sent. Receives packet "i" + client_id!=0 (Victim reconnection) : packet "d" sent. Receives packet "i" + n_victim>=100 (Too many victims) : packet "d" sent. Receives packet "k" (key request) : send key if existant or generate a new one
There is only one more function to analyse, the one that handles packets of type “e”.
This function begins by initialising buffers with NULL bytes. The victim corresponding to the client ID provided is retrieved and a single byte (corresponding to the encryption method used, “x” or “r”) is received. The following bytes are stored in a 1024 bytes buffer called filename. Once again there is a buffer overflow here.
Buffer overflow when receiving additional data after a packet of type "i". In the Heap. Buffer overflow when receiving filename after a packet of type "e". On the stack.
Depending on the type of encryption used, a different message will be printed server-side. This function just logs the encryption process for the C&C server’s admin to enjoy.
Receives packet "i" + client_id==0 (New victim registration) : id creation, victim info added to list, packet "i" with new id sent, packet "s" sent. Receives packet "i" + client_id!=0 (Victim reconnection) : packet "d" sent. Receives packet "i" + n_victim>=100 (Too many victims) : packet "d" sent. Receives packet "k" (key request) : send key if existant or generate a new one Receives packet "e" (key request) : print a message server-side
We’re done reverse engineering the Tupper “ransomware”!
Here is picture summarizing everything we discovered about Tupper.
Do you remember, at the very beginning I told you I forgot an important feature for a ransomware, did you find it ? The answer is in a HTML comment under this line.
In the next part we’ll analyse more deeply the security issues discovered on the Tupper server and try to abuse them.