Malware development – Welcome to the Dark Side: Part 2-2

 

In the previous parts of this blog series, I introduced the concept of writing a full-undetectable malware and about writing a client-side socket using Windows API. In this blog, we are going to parse the reply received from the netcat server and prompt a reply as to whether the command was parsed or not. And finally, we will try to understand the compiler flags and options that we are using to compile the binary.

Since we have our TCP Socket client ready, the next thing we need to do is create a buffer which will store the command that we will receive from the netcat/python server. Most people would consider using <strings.h> header to craft it, mainly because it is secure and automatically adds a newline to stop the buffer when using strcpy. This helps us to create a buffer overflow free binary. But another thing to question is that how much space would the <strings> header occupy. Using the strings header automatically shoots the size of the binary to 800+ KB which is a problem from a malware perspective. This is the reason why we will have to stick with char from C.

Char has its own set of disadvantages. It always needs to be initiated with a specific length which gets stored on stack. If we want a dynamic size, a char whose length which we don’t know of, then we would have to store the char on heap rather than stack. Also, we need to use the strcpy and memcpy very carefully so that we don’t cause a buffer overflow. Main reason being strcpy is vulnerable to the power infinity. Once we define the value of char which will be empty as of now, we need to set the socket in the blocking mode which will keep listening for an incoming command/buffer. Once a command is received, we will print the received command along with the size of the buffer received on screen. For calculating the size of the buffer, we also need to define an integer.

This is how the code looks like.

    else {
        std::cout << "[+] Connected to client. waiting for incoming command..." << std::endl;

        char CommandReceived[DEFAULT_BUFLEN] = "";
        while (true)
        {
            int Result = recv(tcpsock, CommandReceived, DEFAULT_BUFLEN, 0);
            std::cout << "Command received: " << CommandReceived;
            std::cout << "Length of Command received: " << Result << std::endl;
            memset(CommandReceived, 0, sizeof(CommandReceived));
        }
    }

Put the above code in the else bracket where we inserted our code to print the connection status. Here, Result is the integer variable which will store the value of the buffer received, and CommandReceived is the buffer which will store the command. The size of the CommandReceived is set to a maximum of 1024 which we defined previously using the #define since that is also the maximum buffer that our socket can accumulate. Also, recv is a windows Windows API which is inserted in a While (true) loop in order to put our socket in blocking and listening mode to retrieve the command unless until we receive an exit command.

The last line with the memset code is used to reset the value of the char CommandReceived with the actual size and empty buffer. The reason we do this is because if we receive a command with a small buffer size after we receive a command with a bigger buffer size, then the remaining buffer space in smaller buffer size will be filled with garbage as shown below:

Primary Buffer: [powershell]

p o w e r s h e l l
0 1 2 3 4 5 6 7 8 9

 

Secondary Buffer: [whoami]

w h o a m i h e l l
0 1 2 3 4 5 6 7 8 9

As you can see, if the memory reset is not performed, the whoami command becomes whoamihell, because only the first 6 characters were overwritten and the buffer was neither resized nor erased.

Using <strings.h> would be much easier here since it does the optimization automatically, but at the cost of increasing the size of the binary.

Now, we write another set of code to compare the command received with a string which will route to a function. As an example, I will take up three commands: whoami, pwd, exit. Every time we receive a command, we will check if it matches a string. If it does, then we print the command received on screen, and execute a function. We will write this function in the next part.

if ((strcmp(CommandReceived, "whoami") == 0)) {
    std::cout << "Command parsed: whoami" << std::endl;
    //Execute a whoami() function
}
else if ((strcmp(CommandReceived, "pwd") == 0)) {
    std::cout << "Command parsed: pwd" << std::endl;
    //Execute a pwd() function
}
else if ((strcmp(CommandReceived, "exit") == 0)) {
    std::cout << "Command parsed: exit";
    std::cout << "Closing connection" << std::endl;
    //Exit gracefully
}
else {
    std::cout << "Command not parsed!" << std::endl;
}

This code will go above the memset part of the code.

Finally, we go ahead and compile the binary with some mingw flags and options as listed below:

For Linux:

$ i686-w64-mingw32-g++ -std=c++11 maldev.cpp -o maldev.exe -s -lws2_32 -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc

For Windows:

> g++ -std=c++11 maldev.cpp -o maldev.exe -s -lws2_32 -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc

And this is how it looks like when we run the binary:

On the left, we have our netcat binary listening for incoming connections, and on the right, we execute our malware. However, something has gone wrong here. None of the commands we sent are getting parsed.

Another issue here is, if we see the command whoami, the length the letters of whoami should be 6, whereas it printed 7 on the console. Similarly, there is an increase in the length of one in every command that we sent. The main reason for this is because netcat sends the value with a newline, and newline also takes up a character. So, the next thing that we need to do is add a “\n” to the end of every string that we parse above i.e. whoami, pwd and exit. This is how they should be:

...
if ((strcmp(CommandReceived, "whoami\n") == 0)) {
...
}
else if ((strcmp(CommandReceived, "pwd\n") == 0)) {
...
}
else if ((strcmp(CommandReceived, "exit\n") == 0)) {
...
}

And now, if run the binary, it will parse all the commands properly.

Another thing to note here is that when we write our python code, we do not need to use the new line because it sends the data in the raw unicode format unlike netcat. So, we will either have to remove the newline in C++ code or add newline in both python3 Server and the C++ malware code. The main reason not to use netcat is because it cannot handle multiple simultaneous connections and also when it sends a command, it sends it along with the newline. If we want to execute any Windows API that uses this command as an argument, it will fail to execute the command due to the newline. We can also remove the newline with a simple for loop, but it won’t be worth it. We will eliminate the use of netcat in our next blog where we will build our python3 server to handle threaded TCP connections and run CMD based commands via that.

Now, let’s go ahead and understand the compiler flags and options:

-std=c++11 maldev.cpp -o maldev.exe -s -lws2_32 -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
  • -std=c++11 indicates that we are going to use C++ standard 11. My g++ version is 6.3.0 as of the time of writing this blog. My g++ version comes with default standard as C++17. Compiling with C++17 gives me a of new features such as quick conversion from string to int, using numbers in Case:break conditions, but at the same time it increases the size of the binary. Compiling with C++17 gave me a size of 32Kb as the final size of binary, whereas C++11 gave me the size of 24Kb. Thus, c++11 was choosen.
  • -s strips off any metadata left in the binary, debug symbols, information about where the binary was compiled, username of the compiler, hostname of the compiler, directory where it was compiled and so on.
    • Tip: When compiling the final binary, always make sure you do it in a new VM with clean install so that it does not leave back any trace of the author and the coding environment
  • -lsw2_32 informs the compiler to link lsw2_32.lib library with the socket required for windows. This will allow even older version of windows to work with the sockets
  • -Wno-write-strings is required to convert string to char. This is only useful when debugging the binary. During final compilation this has no effect since we will not be using <strings.h> or the <iostream> header.
  • -fno-exceptions as describes itself is used to specify the compiler that the binary that is getting compiled has no exceptions. If exception is enabled, it slows down the processing time of the execution of the binary. Although at this stage, the process execution time is miniscule, when we import large functions in the malware which can also include exploits, the processing time tends to slow down a lot. This is the main reason for not using exceptions in our malware.
  • -fmerge-all-constants will combine all the constant arrays/integers and chars and initialize them at the very start. This is very useful in code optimization since this will increase the speed of processing/comparing buffers and find addresses of the buffers in memory quickly. This option will merge all the addresses and store them at a location which is known and thus it will be quicker to access these locations.
  • -static-libstdc++ and -static-libgcc are used to compile C and C++ headers statically together in a C++ program. Since we are going to mix up C and C++ code, we would be needing these two a lot. Also, when dynamic links are performed, its easier for antiviruses to detect the binary as malicious. We will not be linking any DLL to our binary atleast for now. And so we will require static linking

In the next blog, we will be building up our python3 botnet server which can interact with our bot(s) and then we will start writing our main Bot code to handler various commands in the form of C++ functions.

Author


5 comments

Thats awesome mate, but has mention you are creating an FUD, how you concluded it ?
Did you upload it on VT to check whether all the test passes and no AV has detected your maldev.exe ? However I cant find any hashes/details specifically for maldev.exe on VT.

Hi Araya,

I am not uploading any custom written malwares to the virus total reason being that it starts debugging and creates signatures once you upload it. I’ve however created multiple virtual machines with updated antivirus database, and scan my binary with multiple AVs. I will doing that in the part 4 of the blog post 🙂

Snip from above:
I’ve however created multiple virtual machines with updated antivirus database, and scan my binary with multiple AVs.
Me: But, do you really think it’s gonna work or help in real env, do you have SEP or kaspersky on your virtual hub ? If you are able to bypass them so then its awesome.

I have done this same, during my OSCE but couldn’t help me much this post might be help for beginners but this is more of socket program, anyways will look forward for your post 4 cheers.

Hey Araya,

The code I’ve written are just snippets of the whole malware. You can check the Part 4 which is now up and I’ve specified how one can use WINAPI calls to silently execute commands on the remote host. I’ve also used these malwares in production enviroments during red teaming and bypassing multiple layers of security. Also, you can surely go ahead and scan the exe uploaded on my git: https://github.com/paranoidninja/Botnet-blogpost/ to check whether they are detected on not. For me, they are undetectable as for the moment

So, just for your reference I made a video of the final Botnet code that I have. I tested it out on Defender [in part 4]: http://niiconsulting.com/checkmate/2018/03/malware-development-welcome-to-the-dark-side-part-4/
and on Kaspersky:
https://vimeo.com/261693007
with the latest and updated kaspersky as shown in the video 😀
The code that I have is an updated version of bot code 😀

Leave a Reply

Your email address will not be published. Required fields are marked *