Sitemap

Linux/Trigona: analysis with r2ai

13 min readAug 25, 2025

Update Aug 26: added reverse engineering of VMWare ESXi features

I am presenting parts of this at the end of the week at Barb’hack 2025, but I realize there is far more to say and it will never fit in 30 minutes. So, this is the full story of the analysis of a rather complexe sample with assistance from AI.

Although the Trigona family goes back to 2022 and was partly taken down in October 2023, there are still some samples in the wild, including the one we analyze in this blog post. This particular samples dates from April 2025, and targets the Linux platform. Its sha256 hash is c08a752138a6f0b332dfec981f20ec414ad367b7384389e0c59466b8e10655ec. You may also find it under the name of Filecoder.

Summary/Agenda:

  • How to detail with output token limit in r2ai
  • Improving the quality of code generated by r2ai direct mode
  • Renaming functions and variables in r2: AI does the work
  • Asking for explanations on specific options of the malware like /chattr-i
  • Malware stops VMs
  • How to put pieces of the puzzle together (with AI, but without r2ai)

The main is too long

I ask r2ai to decompile the main of malware. Normally, this works fine, but here the main is too big, and the generated C code I get is truncated.

[0x004130a0]> r2ai -d
sym_EXCLUDEDMASKSPROCESSOR_DELETEEMPTYMASK_TSTRINGLIST(0x6e94e0);
sym_EXCLUDEDMASKSPROCESSOR_SIMPLIFYMASKSLISTANDSLASHCONVERTATION_TSTRINGLIST(0x6e94e0);
sym_EXCLUDEDMASKSPROCESS

This is a typical output token limit. By default, r2ai sets this limit to 4096, which is reasonable if your function isn’t too long and/or if your model doesn’t support more output tokens. In the case of Trigona, if we want to get the work done, we need to use a model which supports more output tokens, and raise the limit. Read your model’s documentation to find out what’s your limit.

We get the full main with those settings:

r2ai -e api=anthropic
r2ai -e model=claude-3-7-sonnet-20250219
r2ai -e max_tokens=16000

Another option, if you can’t raise max_tokens enough, is to ask less 😏.

[0x004118d0]> r2ai -a please decompile the 100 first instructions of main
[r2cmd]>
[0x004118d0]> afl~main;iz~/
I'll decompile the first 100 instructions of the main function for you. Let me first take a look at those instructions using radare2.
[r2cmd]>
[0x004118d0]> pd 100 @ main

A quick word on models

I am using a version of Claude Sonnet 3.5 or greater through all this blog post. It simply always produces the best results when it comes to reverse engineering binaries (as of August 2025). Sometimes, I use a model from Mistral, or GPT-OSS or qwen/qwen3–32b from a local server. This only occurs when I know the question is simple and wish to save a few cents, as Claude requires a paid API key. So far, I haven’t ever encountered a case where any of those models, including paid API keys for other GPT models, gave better results that Claude Sonnet. Also, Mistral does not behave correctly with r2ai’s auto mode (it doesn’t handle the tools correctly).
This does not wish to sound like advertisement for Claude, it’s more like feedback of what I experienced. I’d be glad to switch to another model, especially a free one!

Improving the generate code with auto mode and better prompts

The code I got initially with r2ai -d (direct mode) was good, but several points can be improved:

  1. I do not wish to see Delphi’s internal stack management functions or exception handling.
  2. Delphi’s function names are too long IMHO
  3. Variables like var_b0h need to be renamed to something appropriate.
int *var_b0h = 0;
int *var_a8h = 0;
int *var_a0h = 0;
int *var_68h = 0;

sym_FPC_PUSHEXCEPTADDR(1, 0x177fa0, 0x177fe0, 0, 0);
int retValue = sym_fpc_setjmp(0, 0x177fa0);

if (retValue == 0) {
if (byte_at_0x6e95a9 == 0) {
sym_ONPROGRESSLOGS_WRITELNTOLOG_ANSISTRING("Load config…");

To fix that, I could probably modify the prompt used in r2ai -d. It is entirely configurable with r2ai -e prompt=your new prompt. There are 2 issues with this technique: (1) it will take time to find the good prompt, and most of the time, this prompt will be specific to your sample, you can’t re-use it for other samples, and (2) there isn’t (yet) any option to reset the prompt to the default one, this is a bit of a burden to save it in a file and reset it afterwards.

The automatic mode of r2ai is better solution, I think, except it consumes more tokens (but actually, we’re talking about cents here). In the automatic mode, we enter our question/remarks in a simple way, and the fact that the AI is allowed to issue r2 commands (under supervision) helps it achieve results that would have taken a very long prompt.

[0x004130a0]> r2ai -a decompile the main. Rename Delphi functions with shorter meaningful names. Remove functions related to stack.
[r2cmd]>
[0x004130a0]> afl~main

The results are excellent: variables have been renamed, function names are much shorter — I personally prefer, and we got rid of specifics due to use of Delphi.

int main(int argc, char **argv) {
// Initialize program units
initializeUnits();

// Set default parameters
int paramCounter = 1;
bool chattrFlag = false;
bool disableKillFlag = false;
bool disableCmdFlag = false;
bool stealthMode = false;
bool isDetached = false;
bool useNohup = false;
bool doPowerOff = false;
bool deleteFlag = false;
bool useFullScan = false;
bool useFastScan = false;
bool eraseMode = false;
bool isTestingMode = false;
bool logEnabled = false;
bool shortLogEnabled = false;
bool allowSystemFlag = false;
bool shutdownAfterFlag = false;

char* targetPath = "/vmfs/"; // Default target path
int sleepSeconds = 0;
int depthValue = 0;

// Load configuration
writeToLog("Load config...");
if (!loadConfigsFromRes()) {
writeToLog("Can't load config. Terminated.");
cleanup();
return 1;
}

// Process command line arguments
if (paramCount() == 2) {
if (inOptionsList(paramStr(1))) {
// Valid option found
} else {
// Use first parameter as target path
targetPath = paramStr(1);
}
}

// Process all command line options
while (paramCounter <= paramCount()) {
String param = paramStr(paramCounter);

if (compareEqual(param, "/chattr-i")) {
chattrFlag = true;
paramCounter++;
}
else if (compareEqual(param, "/notkill")) {
disableKillFlag = true;
paramCounter++;
}
...
else if (compareEqual(param, "/test_vid")) {
if (isTestingMode) {
stringToArray(paramStr(paramCounter + 1), testVendorIds);
}
paramCounter += 2;
}
else if (compareEqual(param, "/p") || compareEqual(param, "/path")) {
targetPath = paramStr(paramCounter + 1);
paramCounter += 2;
}
else {
paramCounter++;
}
}

// Validate target path
if (!targetPath) {
printf("Can't encrypt or erase all data.\n");
printf("Please use /p option for launch.\n");
printf("\n");
cleanup();
return 1;
}
...
// Test shell access if chattr flag is set
if (chattrFlag) {
if (!isDetached) {
printf("\nTest shell");
}

if (!testShell()) {
chattrFlag = false;

if (!isDetached) {
printf("Shell is not available. chattr-i flag set to false");
}
}
}

// Process excluded paths
deleteEmptyMask(excludedPathsList);
simplifyMasksListAndSlashConversion(excludedPathsList);
convertMasksListToMasksOperations(excludedPathsList);

// Power off ESXi VMs if needed
if (targetPath == "/vmfs/" && doPowerOff) {
if (!logEnabled && !shortLogEnabled && !isDetached) {
printf("Power off ESXI VMs");
}

if (!isDetached) {
printf("ESXI VMs power off logs disabled");
}

stopVM();
}

// Kill processes if not disabled
if (getTaskKillerEnabled() && !disableKillFlag) {
killTasksAndServices();
}

// Sleep if needed
if (sleepSeconds > 0) {
writeToLog("Sleep " + intToStr(sleepSeconds) + " seconds before run...");
sleep(sleepSeconds * 1000);
}

...
// Execute command list if not disabled
if (getCmdListEnabled() && !disableCmdFlag) {
executeCmdList();
}

// Perform encryption or erasing
priorityEncryptionOrErasing();

writeToLog("Start process...");

// Process target path
String normalizedPath = osConvertSlashToBackslash(targetPath);
if (fileExists(normalizedPath)) {
encryptOrEraseFileByPath(targetPath, false);
} else {
encryptOrEraseFolderByPath(targetPath, false);
}

// Shutdown system if requested
if (shutdownAfterFlag) {
reboot(0x4321FEDC); // Special reboot code
}

// Delete self if requested
if (deleteFlag) {
String selfPath = paramStr(0);
if (!selfPath) {
selfPath = "";
}
deleteFile(selfPath);
}

cleanup();
return 0;
}

Renaming functions and strings

In the previous result, functions were adequately renamed in the generated C code. How about renaming the functions in r2, so that we can refer to them in the rest of our reverse engineering with better names?

I had to try a few different prompts with r2ai -a. The first ones didn’t pick up enough functions to rename.

[0x004130a0]> r2ai -a please rename the project, debug, sysutils function names by something shorter, mostly lower case and meaningful. sym.ONPROGRESSLOGS_WRITELNTOLOG_ANSISTRING for example would be fine with write_line_to_log.
I'll help rename those functions to be more concise and readable. Let me identify and rename them:

1. From the log/progress functions:
[r2cmd]>
[0x004130a0]> afn sym.write_line_to_log sym.ONPROGRESSLOGS_WRITELNTOLOG_ANSISTRING
afn sym.write_line_to_log sym.ONPROGRESSLOGS_WRITELNTOLOG_ANSISTRING

As for strings, r2 sees many Delphi strings and names them obj.__PROJECT1__Lxx. Same: I’d like that to be renamed to something meaningful. The AI sees the strings correctly, but fails to issue the correct command in r2.

[0x004130a0]> r2ai -a In the main, rename string objects like obj.__PROJECT1__Ld47, obj.__PROJECT1__Ld49 etc (they start with obj.__PROJECT) to a name close to the string value.
[r2cmd]>
[Assistant]

Let me analyze the string objects and their values. I found several string objects that need to be renamed. Here they are with their values:

1. obj.__PROJECT1__Ld52 = "Preparing to launch..."
2. obj.__PROJECT1__Ld53 = "Version: "
3. obj.__PROJECT1__Ld54 = "."
4. obj.__PROJECT1__Ld55 = "2 sec delay"
5. obj.__PROJECT1__Ld56 = "Load config..."
...
Let me rename these objects to be more descriptive based on their values:
[r2cmd]>
[0x004130a0]> afn str_preparing_launch 0x6aea00

The correct way to do it (AFAIK) is via flag renaming: remove the previous “flag” and create a new one with the desired name. I could have edited each command manually, but that would have been long, so I ask the AI again, with a prompt explaining what r2 commands it would need to use.

[0x004130a0]> r2ai -a You missed several strings to rename in the main like obj.__PROJECT1__Ld54 - or you didnt rename successfully. Those objects are flags. To rename a flag, you must do f- obj.blah, then f new_name 0xADDR
[r2cmd]>
[0x004130a0]> pdf~obj.__PROJECT
pdf~obj.__PROJECT

│ │ 0x00413184 48b828ea6a.. movabs rax, obj.__PROJECT1__Ld53 ; str_version
│ │ 0x004131ce 48b848ea6a.. movabs rax, obj.__PROJECT1__Ld54 ; str_dot
...
[Assistant]

Ah, thank you for the clarification! You're right. I need to first delete the old flags and then create new ones. Let me do this properly:
[r2cmd]>
[0x004130a0]> < CT1__Ld57; f str_config_load_error @ 0x6aeaa0; f-obj.__PROJECT1__Ld58; f str_vmfs_path @ 0x6aead0; f-obj.__PROJECT1__Ld59; f str_read_launch_options @ 0x6aeae8

This worked well. Sometimes, it forgot some variables and I had to ask it to process the missing ones.

Note that Ghidra-MCP does this sort of renaming pretty well, because one of the functions exported by the MCP is precisely the renaming of functions. I haven’t tried radare2-mcp yet, but it should be helpful on that side.

Reversing specific commands of the malware

chattr-i

It will be difficult to get any better from the main. If we want to dig in further for this sample, we have to investigate other functions. For example, this sample features a new /chattr-i option. What is it doing?

We can get explanations in direct mode with r2ai -x. This sends a pre-defined prompt to the AI that asks for a verbal description of a function. But here, we want an explanation on an option whose implementation spans over several functions. The automatic mode is better for this sort of question, because the AI will have the ability to navigate through the assembly on its own and fetch the interesting parts.

Be warned though, there is a risk that the AI loops on silly r2 commands and is unable to converge to a decent solution. This happens from time to time. I personally stop any conversation of more than 5 interactions if I see no progress. Most of the time, if the AI has no clue how to proceed, it’s going to be a waste of time (and tokens).

We are lucky, this doesn’t happen in our case, and Claude is quite helpful.

[0x004130a0]> r2ai -a can you explain what the option "/chattr-i" does if enabled?
[r2cmd]>

Let me analyze the binary to understand how this option is used.

First, I'll try to find any references to "chattr-i" in the binary:
[r2cmd]>
[0x004130a0]> iz~chattr
iz~chattr

42 0x000ae990 0x006ae990 9 10 .data ascii /chattr-i
57 0x000aeb90 0x006aeb90 51 52 .data ascii 2Shell is not available. chattr-i flag set to false
1080 0x000bbc08 0x006bbc08 91 92 .data ascii Can't access to file. Try to unlock file by chattr -i terminal command and open file again.
1081 0x000bbc78 0x006bbc78 11 12 .data ascii chattr -i "
...

[Assistant]

Based on the strings and code references found in the binary, the "/chattr-i" option appears to be related to file immutability in Linux systems. Here's what it does:

1. When enabled, it appears to prevent shell access and file modifications
2. It's related to the Linux `chattr` command, specifically the immutable flag (`i`)
3. There's an error message suggesting that when files can't be accessed, users need to use `chattr -i` to unlock them

...
// The actual implementation effects are seen in error messages and behavior:
// 1. When chattr-i is false (default):
// - "Shell is not available. chattr-i flag set to false" (at 0x6aeb90)
// - Files are protected with immutable flag
// - Shell access is restricted

// 2. When a file can't be accessed:
// - "Can't access to file. Try to unlock file by chattr -i terminal command and open file again." (at 0x6bbc08)
// - Program suggests using "chattr -i" command to unlock the file

// The functionality works as follows:
// 1. When the /chattr-i option is NOT provided:
// - Files remain in immutable state (chattr +i)
// - Shell access is restricted
// - Prevents modification of protected files

// 2. When the /chattr-i option IS provided:
// - Allows the program to modify protected files
// - May temporarily remove the immutable flag
// - Enables certain shell operations

Basically, the chattr option of the malware ensures the malware doesn’t delete important system files. For that, it issues the Unix command chattr -i filename on each file it wishes to protect.

Delete

We can do the same for the /delete option. Note the AI followed pretty well my instructions, i.e a 5-line comment + decompiled code.

[0x004130a0]> r2ai -a please explain what the /delete option does. I want 5 line comment + 10-20 line decompiled code that shows how the /delete option works.
[r2cmd]>
[0x004130a0]> iz~delete
...
Based on the cross-references and analysis of the code around 0x4139e7, here's the decompiled version with explanation:

```c
/*
* The /delete option enables deletion mode in the program.
* When this flag is set, instead of just scanning or analyzing files,
* the program will actually remove files that match certain criteria.
* This is used in conjunction with other options to clean up or remove
* files from the system.
* The option sets a global deletion flag that other functions check before performing deletions.
*/
bool process_command_options(const char* option) {
// Global deletion flag at address 0x6e9599
bool* g_deletion_enabled = (bool*)0x6e9599;

// Check if the current option is /delete
if (strcmp(option, "/delete") == 0) {
// Enable deletion mode
*g_deletion_enabled = true;

// Move to next parameter
g_param_counter++;

return true;
}

// If we find "Record deleted from looping storage" string,
// it means the deletion was successful
return check_other_options(option);
}

/fast and /full options

I recorded a video to reverse the /fast option. Through reverse engineering, I saw the option set a flag at address 0x6e959f and asked the AI to investigate.

We learn through the reverse engineering that the /fast and /full options are mutually exclusive. The /fast option consists in having the ransomware only encrypt files smaller than 512KB, while /full will encrypt everything.

/allow_system option

Following the same methodology, I try to get information on yet another flag, /allow_system.


[0x0041380c]> r2ai -a When /allow_system is enabled, a flag 0x6e95a6 is set (see 0x00413d49). Show me where this flag is used and how. Please keep your comments to max 5 lines. You can include decompiled code up to 20 lines.
...
void encrypt_or_erase_folder(const char* folder_path) {
// Check if system folders are allowed
if (*((uint8_t*)0x6e95a6) == 0) { // If system folders NOT allowed
goto process_folder;
}

// If system folders are allowed, do additional checks
char* normalized_path = normalize_path(folder_path);
if (check_if_system_folder(normalized_path)) {
log_message("Potentially system directory. Skipped.");
skipped_folders_count++;
return;
}
...

So, if the option is set, there are extra checks on system folders.

VM handling

In the function list, we spot a function at 0x004124f0 whose name suggests it deals with virtual machines (initial namesym.P_PROJECT1_STOPVM__BOOLEAN ).

For once, let’s try and use gpt-oss-20b to get the work done. I use it from my own LLM server with LM Studio.

[0x004124f0]> r2ai -d
ERROR: OpenAI API error 400
ERROR: OpenAI API error response: {"error":"Trying to keep the first 9585 tokens when context the overflows. However, the model is loaded with context length of only 4096 tokens, which is not enough. Try to load the model with a larger context length, or provide a shorter input"}

It doesn’t work because the context is too big, precisely the input tokens are above the limit. In this blog, I show how to configure the model to accept a bigger context. However, this is something you can do only on your own servers. If you don’t have one, try and find another provider with a higher limit.

Below, I tried gpt-oss-20b but from Groq. The limit is higher (8000 tokens instead of 4096 previously), but it’s still not enough.

[0x004124f0]> r2ai -e api=groq
[0x004124f0]> r2ai -d
ERROR: OpenAI API error 413
ERROR: OpenAI API error response: {"error":{"message":"Request too large for model `openai/gpt-oss-20b` in organization `org_01jrg29psvfn3v8gn8cx57388d` service tier `on_demand` on tokens per minute (TPM): Limit 8000, Requested 9586, please reduce your message size and try again. Need more tokens? Upgrade to Dev Tier today at https://console.groq.com/settings/billing","type":"tokens","code":"rate_limit_exceeded"}}

One solution would be to purchase a higher tier. Another one is to use another model altogether. Claude Sonnet comes to the rescue once again. I use version 3.5 as it’s cheaper and enough to get the result.

[0x004124f0]> r2ai -e api=anthropic
[0x004124f0]> r2ai -e model=claude-3-5-sonnet-20241022
[0x004124f0]> r2ai -d

The output code shows that the malware is able to issue Virtual Infrastructure Management commands to a VMWare ESXi server. It lists all VMs, and then kills them all.

int stopVM() {
bool result = false;
char *output = NULL;
char *vmid = NULL;
char *command = NULL;
char *tmp = NULL;

// Test shell availability
printf("\nTest shell\n");

if(!testShell()) {
printf("-Shell is not available. Process terminated...\n");
return false;
}

prepareToExecute();

// Execute command to get VM list
command = "vim-cmd vmsvc/getallvms";
int ret = shellExecute(command, &output, true);
if(ret == -1) {
printf("Can't execute shell. Process terminated...\n");
return false;
}

if(!output) {
printf("Log file empty. Process terminated...\n");
return false;
}

// Parse VMID from output
char *vmid_pos = strstr(output, "Vmid");
if(!vmid_pos) {
printf("Invalid VM log format\n");
printf("%s\n", output);
return false;
}

// Extract VMID number
char *newline = strchr(output, '\n');
if(newline) {
char *space = strchr(output, ' ');
if(space) {
int len = space - output;
vmid = strndup(output, len);
if(!isNumber(vmid)) {
printf("Invalid VMID format\n");
return false;
}
}
}

// Execute power off command
char poweroff_cmd[256];
snprintf(poweroff_cmd, sizeof(poweroff_cmd), "vim-cmd vmsvc/power.off %s", vmid);
ret = shellExecute(poweroff_cmd, &command, true);

if(ret == -1) {
printf("Can't execute shell. Can't power off VM\n");
return false;
}

if(!command) {
printf("Log is empty. Can't power off VM\n");
return false;
}

printf("Result: %s\n", command);

result = true;
return result;
}

Wrapping it up

To get a full picture of what the malware does, I don’t feel that r2ai is appropriate. I prefer to copy/paste various outputs I got with r2ai in a large prompt and ask ChatGPT to summarize it for me. As a matter of fact, I even asked Dall-E to generate infographics.

Press enter or click to view image in full size
This infographic was generated by Dall-E from various reverse engineering output I gave it. The graphic part is good, but the text was wrong (missing letters, typos etc) in several places, and I had to manually fix them.

I also asked the AI to generate a “character” that would illustrate the malware. Indeed, malware names are often elusive, and a drawing can help.

Press enter or click to view image in full size
I’m not over enthusiastic with the result I got, I’d have to improve my prompt probably.

— Cryptax

--

--

@cryptax
@cryptax

Written by @cryptax

Mobile and IoT malware researcher. The postings on this account are solely my own opinion and do not represent my employer.

No responses yet