JIT Happens: Exposing LuaJIT Malware in the Wild

SBT Content Engineers 28/02/2025
JIT Happens: Exposing LuaJIT Malware in the Wild

This blog series will explore Security Blue Team's adventure into the reverse engineering of a novel SmartLoader malware variant. This malware was discovered during our research into Belsen Group's high-profile FortiGate leak. This leak was advertised as containing a list of affected companies with associated configuration data from their FortiGate devices.

Whilst running an initial Google search, one of the top results lead us to the GitHub repository pictured below, as we will discover this repository owned by a user AKBoss1221 is hosting SmartLoader malware. This tells us that the threat actor that has hosted this malware has probably done some level of SEO poisoning, targeting security researchers and organizations that are interested in the leaked data!

During our analysis, we identified various info-stealers—such as Lumma, Redline, and Radamanthys—being deployed as the loader's final payload. This suggests that the malware is operating under a Malware-as-a-Service (MaaS) model, where a Threat Actor distributes these tools to facilitate access for other cybercriminal campaigns.

So What's in The Repo?

Looking at the GitHub repository we can see in the README that the phishing pretext is promoting the fact that the tool hosted here will provide the victims with a list of IP address affected by the Fortigate leaks.

The malware authors are attempting to get cybersecurity researchers to download Application.zip which is in the Releases under Tag v1.0.0.

Examining the unzipped Application.zip we can see that there are multiple files

The README alludes to the user launching the program, this will guide them to the Launcher.bat file, examining the batch file it appears to execute the conf.txt file through the lua.exe executable.

Launcher.bat

It is interesting to note the dates within the zip file, where the Lua files were created in 2018 together and the malicious files were created in 2025.

What is LUA?

Lua is a lightweight and super flexible scripting language, mostly used in game development, automation, and embedded systems. It’s fast, easy to learn, and because it’s interpreted rather than compiled, it runs code dynamically instead of being converted into machine code beforehand. This makes it great for flexibility—but also a perfect tool for malware authors looking to stay under the radar.

More and more, hackers are using interpreted languages like Lua, Python, and JavaScript to hide their malware and dodge detection. Since these languages run at runtime, traditional antivirus tools have a harder time spotting malicious code before it actually executes. Plus, attackers can obfuscate their scripts, encrypt key functions, and even load additional payloads on demand, making static analysis a nightmare. With Lua-based malware, we’ve seen threat actors take advantage of JIT compilation and modular scripting, allowing them to deliver payloads stealthily while avoiding behavioral detection. It's yet another way attackers are staying one step ahead of security defenses.

Malware Alert! angry face with horns 

Alone the lua.exe and lua51.dll files seem benign and are more that likely the standard LuaJIT executable used to interpret Lua source code, however alarm bells began to ring when we took a look at conf.txt

Highly obfuscated Lua

It's highly unusual for a supposedly legitimate tool—one that claims to simply help users view leaked IPs—to be so heavily obfuscated. This level of obfuscation is typically seen in malware! This red flag immediately caught the attention of the our analysis team, prompting a deeper investigation. As suspected, further analysis revealed clear signs of malicious intent, reinforcing the need to treat this code as potential malware rather than a harmless utility.

Initial Triage

⚠️ Warning: Malware analysis is highly risky—always use isolated environments, disable network access, and follow strict security protocols to prevent accidental infection or legal consequences.

Knowing that we are dealing with a possible malware that is aimed at a Windows system we span up an isolated EC2 instance to do some quick live analysis with procmon and Wireshark so we can get a glimpse into what the aim of the malware is whilst also collecting any quick IOCs along the way.

Wireshark capturing initial network activity

Procmon showing the creation of persistence files

We have identified multiple areas for extra investigation, such as the C2 communication protocol ( what does it send, what commands exist?) and the techniques that it uses to achieve its objectives.

Looking into the network capture we can see the traffic to the 87 ip address:

It is using a PUT request with multipart/form-data which breaks RFC7231 where this request is reserved for uploading resources and not for sending form data.

We will document the steps we have seen so far then continue a deep dive. Check out blog post part 2 for a dynamic analysis deep dive!

OSINT (Open Source Intelligence)

We now have a vague understanding of the malware to the point to which we can now do some OSINT to identify if it has been seen before and if we can link it to a known threat actor.

Files

Looking up Application.zip we can see that it is picked up as a Generic Trojan using LuaJIT.

Looking up conf.txt we have similar results almost screaming at us that it is malware.

Looking up Lua.exe we are able to see that this file has been used in multiple campaigns:

All of the parent files appear to be archival (zip, rar, etc) and could indicate a TTP(Tool, technique or procedure) for the threat actor.

Domains

Doing some cursory google searches for LuaJIT ip-api allowed us to find the following blogs:

These all appear to be the same TTPs used across multiple campaigns, I think we have found our malware name SmartLoader, the obfuscation tool (Prometheus) and some extra details.

IP Addresses

We only have one IP address to work with at the moment, so we will see what we can find.

This has identified the source IP is owned by a company called Cloud Hosting Solutions, Limited. with the ASN 199785 the location varies depending on what tool is used to query, but the company and ASN remain static.

Lets look at peering connections for the ASN using peeringdb

It appears that this is a network based almost entirely in Moscow and is mainly outbound data (implies hosting).

An example of using this tool for a UK based company Zen Internet is as follows:

As you can see it has multiple interconnection facilities all based within the UK which falls in line with what is expected of a UK based ISP.

SmartLoader's Lifecycle 

SmartLoader's execution has multiple payloads, payload 0 (conf.txt) begins when the victim executes the batch file launcher.bat. The following is a high level summary of SmartLoader's execution flow split down into its multiple payloads.

SmartLoader Execution Flow

payload 0

1. Connect to http://ip-api.com/json/ to get current IP information and Geo location data. 

PS C:\Users\Analyst1> Invoke-RestMethod -Uri "http://ip-api.com/json/"


status      : success
country     : United Kingdom
countryCode : GB
region      : ENG
regionName  : England
city        : Northallerton
zip         : DL6
lat         : 54.4445
lon         : -1.4623
timezone    : Europe/London
isp         : BT Public Internet Service
org         :
as          : AS2856 British Telecommunications PLC
query       : 109.146.222.221

2. Connect to Microsoft 

3. Connect to C2 server 

a. Upload Screenshot of current desktop session 

b. Upload a JSON blob containing information about the victim 

c. Receive Initial tasks 

4. Cache the Tasks in the current users Pictures folder with the filename being the GUID of the machine. 

5. Copy all the files to %APPDATA%\<LoaderID>\ where LoaderID is Base64 encoded. 

6. Create a scheduled task to execute the newly persisted Malware. 

7. Collect payload defined in the task from C2 server 

8. Save task as session.lua(also been seen to save as debug.lua) in the %TEMP% directory 

9. Execute task pointing original lua.ex at session.lua 

10. Write an empty file in %temp% on completion

Payload1

We have seen SmartLoader abusing the GitHub issues bug (please see our very own Malik Girondin's previous research into this) to host its encrypted payloads, the first payload the malware retrieves is another blob of obfuscated Lua source code that looks very similar to conf.txt.

We have seen during analysis that payload 1 follows the same logic as the initial payload as it has similar side effects

  1. Connect to http://ip-api.com/json/
  2. Connect to Microsoft
  3. Connect to secondary C2 server
    1. Upload Screenshot of current desktop session
    2. Upload a JSON blob containing information about the victim
    3. Receive second tasking
  4. Cache the Tasks in the current users Pictures folder with the filename being the GUID of the machine.
  5. Copy all the files to %APPDATA%\<LoaderID>\ where LoaderID is Base64 encoded. (the loader ID is different so directory names, executable name and scheduled task names are different)
  6. Create a second scheduled task.
  7. Collect a second payload defined in the task from C2 server ( we have seen the C2 server task to collect #Lumma, #Redline and #rhadamasntrys stealers)

The Final Payload

The final payloads we've observed SmartLoader delivering include both shellcode and PE files. During our analysis, we found that when executing shellcode, SmartLoader relies on the Windows API CreateThread, passing the starting memory address of the shellcode as an argument. This technique allows the malware to directly execute code in memory, bypassing traditional disk-based detection methods and making analysis more challenging.

As part of SBT’s wireshark dissector code there is a function that will automatically decrypt, decode and dump any files that SmartLoader attempts to pull from Github user-attachments.

GitHub - SecurityBlueTeam/Smartloader_Wireshark: Wireshark dissector for Smartloader malware

During the course of our analysis we have also seen final payloads being delivered from actual Github Repos such as the one below. References to discord CDN have also been seen after editing the loaderID against the C2 server.

GitHub aidagluglu files (repo now taken down)

Crypto fail

During the analysis of SmartLoader the team found that both its C2 communication and final payloads are encrypted, we were able to easily break this encryption to allow a deeper level of analysis.

SmartLoader uses an extremely simple cryptography scheme that uses simple addition against an ASCII string, this allows for easier crypt analysis.

The algorithm takes each ASCII character of the plaintext and key and converts them to an integer, which results in an 8bit value.

The integers are then added together and if they are above 255 then they have 256 removed to ensure that the final value remains an 8 bit value.

For example with a Key of "A" and a plaintext of "A", after converting to integer we get

  • key = 0x41
  • plaintext = 0x41

We can now add them together to get the ciphertext, which results in 0x82 or the character

Using dynamic analysis we found the plaintext and the ciphertext, using brute force we were able to recover the key using the following algorithm.

By knowing the encryption scheme, plaintext and ciphertext we are able to reverse the operation and get an array of the key.

For each character in the plaintext and ciphertext if we subtract the plaintext from the ciphertext we shall be left with the key.

Taking our example from earlier we know the following:

  • Ciphertext = 0x82
  • Plaintext = 0x41

Working backwards we can now see that the key at this position is 0x82 - 0x41 = 0x41 and that matches for our key.

In the event that the subtraction makes the key a negative integer, we must add 256 to the value to make sure it remains in the correct range for an unsigned 8-bit integer.

Brute Forcing the Key

We now have the correct tools to find the key for the C2 communications, so lets grab two C2 strings and attempt to decrypt them:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 05 Feb 2025 16:10:21 GMT
Content-Type: application/json
Content-Length: 1860
Connection: keep-alive
cf-cache-status: DYNAMIC
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=JkbLld0seJ95Y7mhz26VipJi8rzyCum9zUAt%2F3Kl1Ys13Pi%2FiX7CknIBOB4KkPiKdv%2FOacn1dqnKe83%2FVMOL7sy8zyhETRfGvLNZcUF1aztVJfCpHTGSA%2BCamij7tCtoYf3F"}],"group":"cf-nel","max_age":604800}
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
CF-RAY: 90d42b86582d03ec-FRA
alt-svc: h3=":443"; ma=86400
server-timing: cfL4;desc="?proto=TCP&rtt=6712&min_rtt=6712&rtt_var=3356&sent=558&recv=3057&lost=0&retrans=0&sent_bytes=0&recv_bytes=3933667&delivery_rate=0&cwnd=144&unsent_bytes=0&cid=0000000000000000&ts=0&x=0"

{"loader":"YjMsNWIsZDIsYmMsYmYsOTIsYzEsZGYsYWIsYjYsZDAsYmEsYmYsZDUsYzYsOWQsYjYsOTYsOTQsOGQsN2IsYTMsNjMsNTYsOTMsYjYsYzUsZDIsYzcsZGEsYzYsNjgsNzIsNTksYTAsNmYsNmYsNTMsYzAsZDEsYjgsYjMsZTAsYzIsYmQsY2YsODQsNzIsNjQsZWYsN2MsZTEsYjQsZTQsYTgsNTYsNmMsNjEsN2UsOTQsODEsODUsN2EsYjksYWMsOWEsZTQsYjgsYzIsNTMsODgsOGMsYjIsYjMsZDcsYzcsYmYsZTQsOGUsNTgsNjYsZTgsYmIsY2YsYjcsZGMsYjcsNTYsNmMsNjEsY2MsODUsYzksY2EsZDAsYmEsNWEsNzMsOTAsNjUsOTAsOWYsNmUsZDEsYmUsYzQsZGEsYzYsN2EsZDYsYzUsOWIsYjksZTYsY2MsZDIsYWYsOTksNmYsNTQsNTQsYjQsYzUsYzQsYzksZGEsY2IsNjgsNzIsNTksZDYsYTQsYmIsYTQsYjMsZTksNzgsNzIsOGQsYmMsYzMsY2IsYzcsNWEsN2UsOTQsOGEsOTksNmIsOTksYjMsOTksYTQsYjQsYmEsZDYsYzksY2EsYzYsYTksOWQsNWIsYWEsNjMsODAsYWUs","tasks":"OTMsYjQsOTIsYWMsYjMsNTMsODgsOGMsODQsODMsOWYsODAsN2EsODksY2UsYTEsYjIsZGYsN2MsYTcsNmIsOTksYWIsYTgsYTYsYjEsYzQsOWQsODQsOTQsYmYsYWYsYWMsYTEsZTUsYTUsN2QsOTQsYmQsZDksN2IsYzcsZGUsYjksY2MsOTQsYzMsYWMsYjgsZDUsYmQsZDUsYjgsZGMsYjEsYTgsYTUsNzAsYjcsY2MsYzEsY2EsY2IsNzUsNjksNzEsYTYsNzYsODAsNjYsODYsYTEsN2IsYmUsOTksYzgsZDIsZGIsODQsNjQsNjQsOTYsYzAsZDYsYjcsZGMsYTIsYTQsOTMsYjUsYjksODUsOGYsODUsN2EsOWEsOWQsYTYsZTAsNjUsN2IsNTEsNzAsZDIsYjUsYmUsZDAsYjMsYzgsYzgsY2YsOWQsNjYsYWUsN2EsOGYsYmUsZGMsYjYsYTcsOWIsYjAsYmYsOTEsYzEsZGEsYjksNjgsNjQsNTksOTIsYjYsYzMsOTIsYzAsZTAsNmUsOGMsOGIsODUsODYsODcsODQsOTksYjksZTgsYzksZGYsYzAsZTUsNjUsNmUsNTIsNzEsN2QsODMsNzcsZDcsYmQsYjIsOTksYWUsZGUsYTYsYjcsNTMsODgsOGMsN2MsN2UsOGIsNzYsYzIsZDAsYzYsOWQsNjYsYWUsN2EsOWQsNzcsOTcsNjUsYTQsYTcsYWUsYzEsODUsOGYsODUsZDMsNjgsYWIsYTIsZWEsYTgsNzEsNmIsNmUsOTksN2QsN2UsOGIsNzYsY2QsZGIsYzMsYWMsYjksZTcsN2MsYTcsNmIsZGQsYTQsYTAsYTUsYTYsY2UsOGYsNzUsODcsYmMsYjIsYTQsOTgsZGMsYjIsYjAsOTUsYjMsZGUsNmUsOGMsOGIsY2YsN2MsY2QsZDcsYTYsYTcsOTYsOTQsOGQsYjksZWMsYWYsYTAsNWUsNjEsNzMsZDcsY2UsZDUsYmQsNjgsNzIsNTksOTIsOGYsYmUsOTIsYjIsYjgsYjUsYjQsZGQsYjUsY2MsZTAsODQsYjUsNzAsOTQsN2MsZDEsYjAsZTMsYWMsYWEsOTcsYjMsY2EsODUsOGYsODUsN2EsYTcsYTYsYjIsOTIsYzAsYWMs"}

The commands are sent back as a JSON Object with the two keys loader and tasks the contents of each are a Base64 blob

  • Loader = YjMsNWIsZDIsYmMsYmYsOTIsYzEsZGYsYWIsYjYsZDAsYmEsYmYsZDUsYzYsOWQsYjYsOTYsOTQsOGQsN2IsYTMsNjMsNTYsOTMsYjYsYzUsZDIsYzcsZGEsYzYsNjgsNzIsNTksYTAsNmYsNmYsNTMsYzAsZDEsYjgsYjMsZTAsYzIsYmQsY2YsODQsNzIsNjQsZWYsN2MsZTEsYjQsZTQsYTgsNTYsNmMsNjEsN2UsOTQsODEsODUsN2EsYjksYWMsOWEsZTQsYjgsYzIsNTMsODgsOGMsYjIsYjMsZDcsYzcsYmYsZTQsOGUsNTgsNjYsZTgsYmIsY2YsYjcsZGMsYjcsNTYsNmMsNjEsY2MsODUsYzksY2EsZDAsYmEsNWEsNzMsOTAsNjUsOTAsOWYsNmUsZDEsYmUsYzQsZGEsYzYsN2EsZDYsYzUsOWIsYjksZTYsY2MsZDIsYWYsOTksNmYsNTQsNTQsYjQsYzUsYzQsYzksZGEsY2IsNjgsNzIsNTksZDYsYTQsYmIsYTQsYjMsZTksNzgsNzIsOGQsYmMsYzMsY2IsYzcsNWEsN2UsOTQsOGEsOTksNmIsOTksYjMsOTksYTQsYjQsYmEsZDYsYzksY2EsYzYsYTksOWQsNWIsYWEsNjMsODAsYWUs
  • Tasks = OTMsYjQsOTIsYWMsYjMsNTMsODgsOGMsODQsODMsOWYsODAsN2EsODksY2UsYTEsYjIsZGYsN2MsYTcsNmIsOTksYWIsYTgsYTYsYjEsYzQsOWQsODQsOTQsYmYsYWYsYWMsYTEsZTUsYTUsN2QsOTQsYmQsZDksN2IsYzcsZGUsYjksY2MsOTQsYzMsYWMsYjgsZDUsYmQsZDUsYjgsZGMsYjEsYTgsYTUsNzAsYjcsY2MsYzEsY2EsY2IsNzUsNjksNzEsYTYsNzYsODAsNjYsODYsYTEsN2IsYmUsOTksYzgsZDIsZGIsODQsNjQsNjQsOTYsYzAsZDYsYjcsZGMsYTIsYTQsOTMsYjUsYjksODUsOGYsODUsN2EsOWEsOWQsYTYsZTAsNjUsN2IsNTEsNzAsZDIsYjUsYmUsZDAsYjMsYzgsYzgsY2YsOWQsNjYsYWUsN2EsOGYsYmUsZGMsYjYsYTcsOWIsYjAsYmYsOTEsYzEsZGEsYjksNjgsNjQsNTksOTIsYjYsYzMsOTIsYzAsZTAsNmUsOGMsOGIsODUsODYsODcsODQsOTksYjksZTgsYzksZGYsYzAsZTUsNjUsNmUsNTIsNzEsN2QsODMsNzcsZDcsYmQsYjIsOTksYWUsZGUsYTYsYjcsNTMsODgsOGMsN2MsN2UsOGIsNzYsYzIsZDAsYzYsOWQsNjYsYWUsN2EsOWQsNzcsOTcsNjUsYTQsYTcsYWUsYzEsODUsOGYsODUsZDMsNjgsYWIsYTIsZWEsYTgsNzEsNmIsNmUsOTksN2QsN2UsOGIsNzYsY2QsZGIsYzMsYWMsYjksZTcsN2MsYTcsNmIsZGQsYTQsYTAsYTUsYTYsY2UsOGYsNzUsODcsYmMsYjIsYTQsOTgsZGMsYjIsYjAsOTUsYjMsZGUsNmUsOGMsOGIsY2YsN2MsY2QsZDcsYTYsYTcsOTYsOTQsOGQsYjksZWMsYWYsYTAsNWUsNjEsNzMsZDcsY2UsZDUsYmQsNjgsNzIsNTksOTIsOGYsYmUsOTIsYjIsYjgsYjUsYjQsZGQsYjUsY2MsZTAsODQsYjUsNzAsOTQsN2MsZDEsYjAsZTMsYWMsYWEsOTcsYjMsY2EsODUsOGYsODUsN2EsYTcsYTYsYjIsOTIsYzAsYWMs

Decoding the blob in CyberChef allows us to see that inside it is a hex string.

Once decoded we have the hex string of our ciphertext.

  • loader_ciphertext = b35bd2bcbf92c1dfabb6d0babfd5c69db696948d7ba3635693b6c5d2c7dac6687259a06f6f53c0d1b8b3e0c2bdcf847264ef7ce1b4e4a8566c617e9481857ab9ac9ae4b8c253888cb2b3d7c7bfe48e5866e8bbcfb7dcb7566c61cc85c9cad0ba5a739065909f6ed1bec4dac67ad6c59bb9e6ccd2af996f5454b4c5c4c9dacb687259d6a4bba4b3e978728dbcc3cbc75a7e948a996b99b399a4b4bad6c9cac6a99d5baa6380ae

From our decoded strings in dynamic analysis we know there is a string that has the same length:

{"bypass_defender": 0, "autorun": 0, "relaunch": {"time": -1, "status": false}, "tablet": {"text": "An error occurred", "status": false}, "hide": 0, "persistence": 1}

Running the algorithm on both sides we get:

89pCO1NlLRkTZgb8DtZmKwC42AQcUeXF89pCO1NlLRkTZgb8DtZmKwC42AQcUeXF...

We can see that the string 89pCO1NlLRkTZgb8DtZmKwC42AQcUeXF is repeated, so this must be our key.

To test our theory we try to decrypt the task blob using CyberChef:

Now we have the cryptography and know the key, we are able to decrypt the C2 communication in Wireshark live, we created a protocol dissector for SmartLoader and this allowed us to see all of the communications.

GitHub - SecurityBlueTeam/Smartloader_Wireshark: Wireshark dissector for Smartloader malware

Hooking Lua

We know that Lua has to convert the embedded code into a form that it can actually execute, so we will recompile luajit for the same version and hook the library calls so we can log them to a file and identify arguments and strings used by the malware.

We start by creating a header file that gives us an inline function that allows us to append data to a file, as we don’t have enough time to see the text within the cmd window before the execution completes.

Debug print code

We were able to hook many functions within the code such as lj_ccall_func which is a function that takes a function call in Lua and converts it to a native C function call, this could be Windows API, native Lua functions or more.

We also hooked the tostring function in Lua to allow us to identify strings used within the code:

Another set of functions we hooked are in the FFI (Foreign Function Interface) library for Lua, this library is used as a bridge between Lua code and other libraries such as the WindowsAPI or DirectX.

Knowing that Lua code is pretty limited when it comes to access to native Windows functionality, we know that it must make use of this to perform advanced actions.

We hook the cdef call to get the function names and we hook the ffi_meta___call handler to get the name of the functions as they are called.

Using the data extracted from the file we were able to get the some interesting strings:

cdef:     typedef const char* pcstr;
    typedef const wchar_t* pcwstr;
    typedef unsigned long ulong;
    typedef unsigned long long ulonglong;
    typedef unsigned char uchar;
    typedef unsigned short ushort;
    typedef unsigned int uint; 
    typedef int (__stdcall* FARPROC)(); 
    typedef unsigned long (__stdcall* LPTHREAD_START_ROUTINE)(void*);

    static const int SW_HIDE = 0;
    
    typedef struct {
        ushort e_magic;
        ushort e_cblp;
        ushort e_cp;
        ushort e_crlc;
        ushort e_cparhdr;
        ushort e_minalloc;
        ushort e_maxalloc;
        ushort e_ss;
        ushort e_sp;
        ushort e_csum;
        ushort e_ip;
        ushort e_cs;
        ushort e_lfarlc;
        ushort e_ovno;
        ushort e_res[4];
        ushort e_oemid;
        ushort e_oeminfo;
        ushort e_res2[10];
        long e_lfanew;
    } IMAGE_DOS_HEADER;

    typedef struct {
        ulong VirtualAddress;
        ulong Size;
    } IMAGE_DATA_DIRECTORY;

    typedef struct {
        ushort Machine;
        ushort NumberOfSections;
        ulong TimeDateStamp;
        ulong PointerToSymbolTable;
        ulong NumberOfSymbols;
        ushort SizeOfOptionalHeader;
        ushort Characteristics;
    } IMAGE_FILE_HEADER;

    typedef struct _IMAGE_OPTIONAL_HEADER {
        ushort Magic;
        uchar MajorLinkerVersion;
        uchar MinorLinkerVersion;
        ulong SizeOfCode;
        ulong SizeOfInitializedData;
        ulong SizeOfUninitializedData;
        ulong AddressOfEntryPoint;
        ulong BaseOfCode;
        ulong BaseOfData;
        ulong ImageBase;
        ulong SectionAlignment;
        ulong FileAlignment;
        ushort MajorOperatingSystemVersion;
        ushort MinorOperatingSystemVersion;
        ushort MajorImageVersion;
        ushort MinorImageVersion;
        ushort MajorSubsystemVersion;
        ushort MinorSubsystemVersion;
        ulong Win32VersionValue;
        ulong SizeOfImage;
        ulong SizeOfHeaders;
        ulong CheckSum;
        ushort Subsystem;
        ushort DllCharacteristics;
        ulong SizeOfStackReserve;
        ulong SizeOfStackCommit;
        ulong SizeOfHeapReserve;
        ulong SizeOfHeapCommit;
        ulong LoaderFlags;
        ulong NumberOfRvaAndSizes;
        IMAGE_DATA_DIRECTORY DataDirectory[16];
    } IMAGE_OPTIONAL_HEADER32;

    typedef struct {
        ulong Signature;
        IMAGE_FILE_HEADER FileHeader;
        IMAGE_OPTIONAL_HEADER32 OptionalHeader;
    } IMAGE_NT_HEADERS32;

    typedef struct {
        ushort Magic;
        uchar MajorLinkerVersion;
        uchar MinorLinkerVersion;
        ulong SizeOfCode;
        ulong SizeOfInitializedData;
        ulong SizeOfUninitializedData;
        ulong AddressOfEntryPoint;
        ulong BaseOfCode;
        ulonglong ImageBase;
        ulong SectionAlignment;
        ulong FileAlignment;
        ushort MajorOperatingSystemVersion;
        ushort MinorOperatingSystemVersion;
        ushort MajorImageVersion;
        ushort MinorImageVersion;
        ushort MajorSubsystemVersion;
        ushort MinorSubsystemVersion;
        ulong Win32VersionValue;
        ulong SizeOfImage;
        ulong SizeOfHeaders;
        ulong CheckSum;
        ushort Subsystem;
        ushort DllCharacteristics;
        ulonglong SizeOfStackReserve;
        ulonglong SizeOfStackCommit;
        ulonglong SizeOfHeapReserve;
        ulonglong SizeOfHeapCommit;
        ulong LoaderFlags;
        ulong NumberOfRvaAndSizes;
        IMAGE_DATA_DIRECTORY DataDirectory[16];
    } IMAGE_OPTIONAL_HEADER64;

    typedef struct {
        ulong Signature;
        IMAGE_FILE_HEADER FileHeader;
        IMAGE_OPTIONAL_HEADER64 OptionalHeader;
    } IMAGE_NT_HEADERS64;

    typedef struct {
        ulong Characteristics;
        ulong TimeDateStamp;
        ushort MajorVersion;
        ushort MinorVersion;
        ulong Name;
        ulong Base;
        ulong NumberOfFunctions;
        ulong NumberOfNames;
        ulong AddressOfFunctions;    
        ulong AddressOfNames;         
        ulong AddressOfNameOrdinals;  
    } IMAGE_EXPORT_DIRECTORY;

    typedef struct _LIST_ENTRY {
        struct _LIST_ENTRY *Flink;
        struct _LIST_ENTRY *Blink;
    } LIST_ENTRY;

    typedef struct {
        uchar Reserved1[8];
        void* Reserved2[3];
        LIST_ENTRY InMemoryOrderModuleList;
    } PEB_LDR_DATA;

    typedef struct {
        uchar Reserved1[2];
        uchar BeingDebugged;
        uchar Reserved2[1];
        void* Reserved3[2];
        PEB_LDR_DATA* Ldr;
        void* ProcessParameters;
        void* Reserved4[3];
        void* AtlThunkSListPtr;
        void* Reserved5;
        ulong Reserved6;
        void* Reserved7;
        ulong Reserved8;
        ulong AtlThunkSListPtr32;
        void* Reserved9[45];
        uchar Reserved10[96];
        void* PostProcessInitRoutine;
        uchar Reserved11[128];
        void* Reserved12[1];
        ulong SessionId;
    } PEB;

    typedef struct {
        ushort Length;
        ushort MaximumLength;
        wchar_t* Buffer;
    } UNICODE_STRING;

    typedef struct {
        LIST_ENTRY InLoadOrderLinks;
        LIST_ENTRY InMemoryOrderModuleList;
        LIST_ENTRY InInitializationOrderModuleList;
        void* DllBase;
        void* EntryPoint;
        ulong SizeOfImage;
        UNICODE_STRING FullDllName;
        UNICODE_STRING BaseDllName;
        ulong Flags;
        ushort LoadCount;
        ushort TlsIndex;
        union
        {
            LIST_ENTRY HashLinks;
            struct
            {
                void* SectionPointer;
                ulong CheckSum;
            };
        };
        union
        {
            ulong TimeDateStamp;
            void* LoadedImports;
        };
        void* EntryPointActivationContext;
        void* PatchInformation;
    } LDR_DATA_TABLE_ENTRY;

    typedef struct
    {
        int unused;
    } *HKEY, *HDC, *HBITMAP;

    typedef struct {
        ushort bfType;
        ulong bfSize;
        ushort bfReserved1;
        ushort bfReserved2;
        ulong bfOffBits;
    } __attribute__((packed)) BITMAPFILEHEADER;

    typedef struct {
        ulong biSize;
        long biWidth;
        long biHeight;
        ushort biPlanes;
        ushort biBitCount;
        ulong biCompression;
        ulong biSizeImage;
        long biXPelsPerMeter;
        long biYPelsPerMeter;
        ulong biClrUsed;
        ulong biClrImportant;
    } BITMAPINFOHEADER;

    typedef struct {
        long bmType;
        long bmWidth;
        long bmHeight;
        long bmWidthBytes;
        ushort bmPlanes;
        ushort bmBitsPixel;
        void* bmBits;
    } BITMAP;

    typedef struct {
        uchar rgbBlue;
        uchar rgbGreen;
        uchar rgbRed;
        uchar rgbReserved;
    } RGBQUAD;

    typedef struct {
        BITMAPINFOHEADER bmiHeader;
        RGBQUAD bmiColors[1];
    } BITMAPINFO;

    typedef struct {
        ulong dwOSVersionInfoSize;
        ulong dwMajorVersion;
        ulong dwMinorVersion;
        ulong dwBuildNumber;
        ulong dwPlatformId;
        wchar_t szCSDVersion[128];
        ushort wServicePackMajor;
        ushort wServicePackMinor;
        ushort wSuiteMask;
        uchar wProductType;
        uchar wReserved;
    } OSVERSIONINFOEXW;

    typedef struct {
        ulong TokenIsElevated;
    } TOKEN_ELEVATION;

    void* VirtualAlloc(void*, size_t, ulong, ulong);
    bool VirtualFree(void*, size_t, ulong);

    bool GetComputerNameW(wchar_t*, ulong*);

    ulong GetLastError();
    bool CloseHandle(void*);

    void Sleep(int);

    int MultiByteToWideChar(ulong, ulong, pcstr, int, wchar_t*, int); 
    int WideCharToMultiByte(ulong, ulong, pcwstr, int, char*, int, pcstr, int*); 

    bool VerifyVersionInfoW(OSVERSIONINFOEXW*, ulong, ulonglong);
    ulonglong VerSetConditionMask(ulonglong, ulong, uchar);

    void* GetCurrentProcess();

    int GetSystemMetrics(int);
    HDC GetDC(void*);
    void* GetCurrentObject(HDC, uint);
    int GetObjectW(void*, int, void*);
    bool DeleteObject(void*);
    HDC CreateCompatibleDC(HDC);
    HBITMAP CreateDIBSection(HDC, const BITMAPINFO*, uint, void**, void*, ulong);
    void* SelectObject(HDC, void*);
    bool BitBlt(HDC, int, int, int, int, HDC, int, int, ulong);
    bool DeleteDC(HDC);
    int ReleaseDC(void*, HDC);

    ulong GetTempPathW(ulong, wchar_t*);
    bool IsWow64Process(void*, bool*);
    int MessageBoxW(void*, pcwstr, pcwstr, uint);

    void* GetConsoleWindow();
    int ShowWindow(void*, int);

Calling C Function: GetConsoleWindow
Calling GetConsoleWindow
Calling C Function: ShowWindow
Calling ShowWindow
Calling C Function: VirtualAlloc
Calling VirtualAlloc
Parsing Ctype: uchar[7]
Parsing Ctype: uint (*)(void)

This allowed us to see further into what the malware was doing, it appears to have the functionality to take screenshots, generate message boxes and allocated new memory regions (this could be used for shellcode).

Further OSINT

Through the strings we have identified 5 C2 IP addresses:

87.120.36.50

We have identified this to be highly likely to be hosted in Moscow, Russia.

178.236.243.5

Looking up the IP address we can find the ASN (215232) and company name:

We can look up the peering information and see that it is peered in Frankfurt and Kyiv, Ukraine

Navigating to the website https://it-garage.pro we can see that it is a Russian language cloud hosting provider.

The company is registered in the UK so we are able to use Companies House to look at the business.

It appears to be in the process of being struck off the Companies House register being only active since 13th November. This is very strange.

80.66.81.11

We can use the ОГРНИП to look up the company using Предоставление сведений из ЕГРЮЛ/ЕГРИП в электронном виде . This website appears to be registered to an individual in Russia who has identified as an individual entrepreneur.

Registration document

4023028807

150.241.105.82

Looking up this IP address we can see that it is hosted by u1host.com and has the ASN of 213877

This has no peering information available:

Looking into the website we can see that the prices are by rubels by default:

Scrolling to the bottom of the page we can find more russian company information:

Looking up the information again we can find that this company is also registered to an individual entrepreneur.

213.176.73.80

The final IP we have located appears to be using serv.host group as its hosting provider, which is the same as 80.66.81.11.

OSINT Conclusion

Even though there are links to Russia for all the hosting providers, we cannot state that this is a Russian-run operation. We can however note that there is an interesting choice of service providers that use the Russian language in their websites. All of the companies appear young (most founded/registered in 2024) and are small scale operations.

Clean-up

We have created a PowerShell script that will be able to clear out the scheduled tasks, Lua interpreters and configs.

$header = @"
███████╗███╗   ███╗ █████╗ ██████╗ ████████╗██╗      ██████╗  █████╗ ██████╗ ███████╗██████╗      ██████╗██╗     ███████╗ █████╗ ███╗   ██╗███████╗██████╗ 
██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝██║     ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗    ██╔════╝██║     ██╔════╝██╔══██╗████╗  ██║██╔════╝██╔══██╗
███████╗██╔████╔██║███████║██████╔╝   ██║   ██║     ██║   ██║███████║██║  ██║█████╗  ██████╔╝    ██║     ██║     █████╗  ███████║██╔██╗ ██║█████╗  ██████╔╝
╚════██║██║╚██╔╝██║██╔══██║██╔══██╗   ██║   ██║     ██║   ██║██╔══██║██║  ██║██╔══╝  ██╔══██╗    ██║     ██║     ██╔══╝  ██╔══██║██║╚██╗██║██╔══╝  ██╔══██╗
███████║██║ ╚═╝ ██║██║  ██║██║  ██║   ██║   ███████╗╚██████╔╝██║  ██║██████╔╝███████╗██║  ██║    ╚██████╗███████╗███████╗██║  ██║██║ ╚████║███████╗██║  ██║
╚══════╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   ╚══════╝ ╚═════╝ ╚═╝  ╚═╝╚═════╝ ╚══════╝╚═╝  ╚═╝     ╚═════╝╚══════╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝╚══════╝╚═╝  ╚═╝

Please Note this will only remove artefacts for the current user                                                                                                                                                           
"@

write-host $header

Write-Host "Getting Machine GUID"
$guid = ((Get-ItemProperty registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\ -Name MachineGuid).MachineGUID).ToUpper().Replace("-","")
write-host "GUID:" $guid

Write-Host "Checking Registry Entries"
$registry_path = "HKCU:\SOFTWARE\SibCode\"
if ( Test-Path $registry_path ) {
	write-host "Registry Key found"
	$regValues = Get-ItemProperty -Path $registry_path | ForEach-Object {
		$_.PSObject.Properties | Where-Object { $_.TypeNameOfValue -eq "System.Byte[]" }
	}
	if ($regValues) {
		$regValues | ForEach-Object {
			Write-Host "Found Key $($_.Name) Value: $($_.Value)"
			Remove-ItemProperty -Path $registry_path -Name $_.Name -ErrorAction SilentlyContinue
		}
	}
	Remove-Item $registry_path -Force -Recurse
	Write-Host "Removed registry key"
} else {
	Write-Host "No registry entries found"
}

Write-Host "Checking for Cached Data in Pictures"
# The Malware hides tasks in the User's Pictures folder with a file named after the Machine's GUID

$pictures_path = $env:USERPROFILE + "/Pictures"
$pictures = Get-ChildItem -Path $pictures_path -File 
$pictures | foreach {
	if ($_ -eq $guid) {
		write-host $_.Name
		$first_char = Get-Content $_.FullName -TotalCount 1 -Encoding Byte
		if ($first_char -eq 0x7B) {
			write-host "Found Malware Task stored in pictures"
			Remove-Item $_.FullName -Force
		}
	}
}

# Identify task allowing us to get the LoaderIDs
$loaderids = @() # Empty array for loader IDs

$tasks = Get-ScheduledTask -TaskPath "\"
$tasks | foreach {
    $task_name = $_.TaskName
    # Check if it contains an underscore
    if ( $task_name -notmatch "_" )
    {
        return
    }

    # Split on underscore
    $task_bits = $task_name.split("_")
    
    # Should have two parts
    if ( $task_bits.Length -ne 2 )
    {
        return
    }

    # Extract LoaderID
    try {
        $LoaderID = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($task_bits[1]))
    } catch {
        # Not valid B64
        return
    }

    # Ensure that the encoded LoaderID is in actions
    if ( $_.Actions[0].Arguments -match $task_bits[1] )
    {
        write-host "Found Loader: " $task_name $LoaderID $task_bits[1]
        $loaderids += $task_bits[1]
    }
}

# Now to look for loader in User profiles

$local_folder = $env:USERPROFILE + "/AppData/local/"
$loaderids | foreach {
    $new_path = $local_folder + $_
    if (Test-Path $new_path) {
	    write-Host "Smartloader folder found" + $new_path
	}
}

Write-Host "Clearing Temp files"

del ($local_folder + "\Temp\session.*")
del ($local_folder + "\Temp\shell.*")

IOCs

Domains

URLDescriptionSource
hxxps[://]github[.]com/AKboss1221/fortigate-belsen-leakOriginal Repository that we identifiedManual searching
hxxp[://]github[.]com/aidagluglu/files/raw/refs/heads/master/rhsc_e[.]txtFirst Connection by Lua downloaderlua.exe
hxxp[://]ip-api[.]com/json/Lua downloaderlua.exe
hxxp[://]87[.]120[.]36[.]50/api/YTAsODYsODIsOWQsYTEsODgsOTAsOTUsNjUsN2QsC2 request 

Files

FileSizeSHA256Source
Application.zip486KB81580758604dff8b2b8f9126645e4a897e9b86b63bede420a07b1a6b3a973638hxxps[://]github[.]com/AKboss1221/fortigate-belsen-leak/releases/tag/v1[.]0[.]0
conf.txt233KB4b1ba932f06251267820b1af5f0a533f869e809bb9c6a1729a8097553d2f874eApplication.zip
Launcher.bat3KB206be58d97697eb1ad84a4311cc03586955c8d4f0439bc4936abc9a219e8e4b4Application.zip
lua.exe89KB1cf20b8449ea84c684822a5e8ab3672213072db8267061537d1ce4ec2c30c42aApplication.zip
lua51.dll592KBff976f6e965e3793e278fa9bf5e80b9b226a0b3932b9da764bffc8e41e6cdb60Application.zip
ODEw.exe785,497KB990FE8FA259F0F0B182C8FB684F8D7ADBAE5DBD3696407715A53E1FB128AC22DC:\Users\<user>\AppData\Local\ODEw\
conf.txt233KB4B1BA932F06251267820B1AF5F0A533F869E809BB9C6A1729A8097553D2F874EC:\Users\<user>\AppData\Local\ODEw\
lua51.dll592KBFF976F6E965E3793E278FA9BF5E80B9B226A0B3932B9DA764BFFC8E41E6CDB60C:\Users\<user>\AppData\Local\ODEw\
shellbin810  C:\Users\<user>\AppData\Local\ODEw\
rhsc_e.txt1036KBE404BE386CE7B8865BD2E869BBBD39A4153E829173CC4EE8F2B7A81DAEE714BDhxxp[://]github[.]com/aidagluglu/files/raw/refs/heads/master/rhsc_e[.]txt
ldr_e.txt697K87db717742161c78ab90c09ba4c1d6ac178510117cdebea7178c2cc4bd7c24e9hxxp[://]github[.]com/aidagluglu/files/raw/refs/heads/master/ldr_e[.]txt
rh_e.txt4.2M0764b15af47407ae668ce1bd64ca95bacd30807442971f569515613286396490hxxp[://]github[.]com/aidagluglu/files/raw/refs/heads/master/rh_e[.]txt
l.txt   
rs.txt   

IP Addresses

IPDescriptionSource
87[.]120[.]36[.]50C2 Serverconf.txt
178[.]236[.]243[.]5C2 Serverconf.txt
80[.]66[.]81[.]11C2 Serverconf.txt
150[.]241[.]105[.]82C2 Serverconf.txt
213[.]176[.]73[.]80C2 Server(payload 1)session.lua/debug.lua

User Agents

User AgentDescriptionSource
s3klqc1wa6ntg1szvckzi0nt9oumpwy6hhqlua user agent (static across requests)lua.exe

Persistence

TypeLocationSourceDescription
Scheduled TaskWindowsErrorReporting_ODEwconf.txtLoads ODEw persistent luajit interpreter

References

GitHub Bug Used to Infect Game Hackers With Lua Malware

GitHub - prometheus-lua/Prometheus: Lua Obfuscator written in pure Lua

SmartLoader Malware using Lua JIT to push InfoStealers

The LuaJIT Project

Lua Decompiler

Lua 5.1 Reference Manual - contents

Hooking LuaJIT

About SBT

Security Blue Team is a leading online defensive cybersecurity training provider with over 100,000 students worldwide, training security teams across governments, military units, law enforcement agencies, managed security providers, and many more industries. If you're looking to take your skills to the next level, our Blue Team Level 2 (BTL2) certification offers comprehensive hands-on training designed for cybersecurity professionals ready to take the next step in their defensive security careers.

Contributors

David Elliott & Gareth Baddams, SBT Content Engineers

Continue reading

Read the next post in the series here.

About SBT Content Engineers

SBT Content Engineers

The Content Engineers at SBT stay on top of the latest industry news and cybersecurity trends, to bring fresh labs, blog content, and free resources for the benefit of our learning community.