The starting point for this analysis was the OrchidFiles article on GitHub repositories distributing malware.
That write-up describes a campaign where fake or cloned-looking GitHub repositories point users to archive downloads. The archive layout matched the sample I had in hand: a small Windows launcher, a Lua runtime, and an obfuscated Lua script.
The launcher was almost boring:
start lua51.exe rest.txt
The interesting part was `rest.txt`.
At first glance, it looked like an obfuscated Lua script.
After instrumentation, it became clear that Lua was mostly the outer shell.
The payload was using LuaJIT FFI to behave like native Windows malware: manually resolving APIs, reading process internals, capturing the screen, querying a Polygon smart contract, and trying to upload data to attacker infrastructure.
This post walks through the analysis in a practical way: what the payload does, what the Process Environment Block is, why the LuaJIT harness was useful, and how the smart contract added another layer of infrastructure indirection.
The Lua Script Was Not Just Lua
LuaJIT includes an FFI, or Foreign Function Interface. FFI lets Lua code declare C types and call native functions directly. That means a Lua script can call Windows APIs without being a normal PE executable with a nice import table.
In normal Lua, code mostly lives inside the Lua runtime and calls whatever libraries the interpreter exposes. LuaJIT FFI changes that boundary. A script can describe C structs, function prototypes, pointer types, arrays, and callbacks, then pass raw pointers around almost like C code. From the analyst's point of view, the payload stops looking like a simple script and starts behaving like an in-memory native loader.
For a threat actor, that has several practical advantages:
- The payload can ship as text-like script content instead of a conventional compiled executable.
- Native API use can be hidden behind runtime-generated FFI declarations and dynamic symbol resolution.
- The script can allocate memory, cast pointers, copy bytes, and call function pointers.
- Windows internals can be re-created from C structure definitions inside the script.
- Static detection based on PE imports becomes less useful because the Lua host process may not import the suspicious APIs directly.
This is why LuaJIT FFI is relevant tradecraft here.
It gives the operator the convenience of a script with many of the capabilities of native Windows code. The same payload can keep most of its logic in obfuscated Lua, then cross into native execution only when it needs lower-level behavior such as PEB walking, export parsing, GDI screen capture, or WinINet communication.
It also complicates analysis. A static strings pass might show fragments like `ffi`, `cdef`, or Windows type names, but the real behavior only appears when the FFI declarations are decoded and the script starts resolving and calling symbols. That is why the harness focused so heavily on wrapping `ffi.cdef`, `ffi.cast`, `ffi.copy`, `ffi.load`, and `ffi.C`.
The payload declared Windows structures such as:
IMAGE_DOS_HEADER
IMAGE_NT_HEADERS32
IMAGE_NT_HEADERS64
IMAGE_EXPORT_DIRECTORY
PEB
PEB_LDR_DATA
LDR_DATA_TABLE_ENTRY
UNICODE_STRING
BITMAPFILEHEADER
BITMAPINFOHEADER
BITMAP
BITMAPINFO
That set of types tells a story. The script wanted to understand PE files, walk the Windows loader structures, parse exports, and build bitmap data. In other words, it was preparing to resolve APIs manually and capture an image.
A Short Detour: What Is the PEB?
One of the clearest artifacts from the trace was a 7-byte native stub:
64 a1 30 00 00 00 c3
On 32-bit Windows, this disassembles to:
mov eax, fs:[0x30]
ret
That returns a pointer to the PEB, the Process Environment Block.
The PEB is an internal Windows process structure. It contains details about the current process, including a pointer to loader data. That loader data includes lists of modules already loaded into the process, such as `kernel32.dll`, `ntdll.dll`, and other DLLs.
Why does that matter for malware analysis?
Because malware can use the PEB to find DLLs without asking Windows through normal, easy-to-spot APIs. Once it finds a DLL base address, it can parse the module's PE export table and locate functions by name. This technique avoids ordinary imports and makes static analysis noisier.
In this sample, the Lua code used LuaJIT FFI to:
- Allocate executable memory.
- Copy the 7-byte PEB getter into that memory.
- Cast the memory to a function pointer.
- Call it.
- Walk the PEB loader lists.
- Parse fake or real PE export tables.
- Resolve Windows APIs dynamically.
That is a native loader technique implemented from Lua.
Why a Linux Lua Harness?
The sample clearly expected Windows, but the analysis environment was Linux. Running it normally would either fail early or, worse, execute real behavior in an uncontrolled environment.
So the goal was not to "run the malware." The goal was to let the Lua logic progress while stubbing every dangerous Windows action.
The harness wrapped LuaJIT FFI and logged operations such as:
ffi.cdef
ffi.load
ffi.new
ffi.cast
ffi.copy
ffi.string
ffi.C.<symbol>
It also built fake Windows structures:
- a fake PEB
- fake loader lists
- fake PE modules
- fake export tables
The fake modules included:
lua51.dll
kernel32.dll
ntdll.dll
advapi32.dll
wininet.dll
shell32.dll
shlwapi.dll
user32.dll
gdi32.dll
winbrand.dll
When the malware tried to resolve exports, the harness returned fake function pointers. When it called those functions, the harness logged the arguments and returned controlled values.
This approach is useful for this campaign because the same LuaJIT loader pattern may appear across many archives. Rather than detonating each sample on a real Windows host, analysts can extract:
- decoded FFI definitions
- resolved API names
- file paths
- registry keys
- network destinations
- smart contract addresses
- HTTP request bodies
- generated exfiltration payloads
All of that can be done while blocking actual file, registry, process, service, and network side effects.
What the Payload Did
With the fake Windows environment in place, the script completed execution under the harness.
It hid the console:
GetConsoleWindow
ShowWindow(SW_HIDE)
It resolved a broad Windows API surface:
LdrLoadDll
RtlInitUnicodeString
InternetOpenW
InternetConnectW
HttpOpenRequestW
HttpSendRequestW
InternetReadFile
InternetOpenUrlW
RegOpenKeyExW
RegQueryValueExW
OpenProcessToken
GetTokenInformation
SHGetFolderPathW
PathFileExistsW
GetDC
CreateCompatibleDC
CreateDIBSection
BitBlt
WinExec
CreateThread
WaitForSingleObject
API resolution does not automatically mean execution. For example, `WinExec` and `CreateThread` were resolved, but this trace did not show process creation.
The trace did show a registry open attempt:
HKLM\Software\Microsoft\Cryptography
That key is commonly interesting because `MachineGuid` lives under it. The harness blocked the registry operation before a value query completed, so we should avoid overstating this. The evidence supports "host identity collection attempt," not a confirmed value read.
The script also tried to read:
C:\Users\analysis\AppData\Roaming\-1.json
In the harness, that read failed because the file did not exist.
No service creation was observed. No file creation or file write was observed.
Screen Capture and Multipart Upload
The payload performed classic GDI screen-capture behavior:
GetDC
CreateCompatibleDC
CreateDIBSection
SelectObject
BitBlt
Then it built a multipart request body with a file part that started with `BM`, the BMP magic. Because this was a Linux/ARM64 FFI harness emulating Windows structures, the generated BMP header had widened fields and host tooling reported it as generic data. Structurally, though, the intent is clear: the payload constructed a BMP-like screenshot object and prepared it for upload.
The upload target observed in the trace was:
http://217.119.129.99/api/NTE3YjdjNWU1NjYzNjU2YTA1N2Y=
The base64 path component decodes to:
517b7c5e5663656a057f
The multipart body included:
- a `file` part with a random-looking filename
- a `data` JSON field
- a base64 value that decoded to a long hex-looking blob, likely encrypted or obfuscated metadata
That was the first clear C2/exfiltration path.
The Smart Contract Layer
The sample also performed repeated Polygon JSON-RPC calls. The request body was:
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0x1823A9a0Ec8e0C25dD957D0841e3D41a4474bAdc",
"data": "0x3bc5de30"
},
"latest"
],
"id": 1
}
The selector `0x3bc5de30` maps to `getData()` according to 4byte.directory
Replaying the call safely with `eth_call` returned an ABI-encoded string:
http://85.137.52.21
That means the malware was not just using the smart contract as noise. The contract was acting as a configuration pointer.
Storage inspection showed:
slot 0 = 0xde275ad38c3352a7cb6b0d3efcbf45900c9716f2
slot 1 = "http://85.137.52.21"
So the contract appears to store an admin address in slot 0 and the active data/config string in slot 1.
Other selectors identified in the bytecode:
0x092a5cce -> destroyContract()
0x3bc5de30 -> getData()
0x68446ead -> updateData(string)
0xf851a440 -> admin()
0x58eea4ad -> unknown selector, also returns the stored string
This is a useful campaign design pattern. The malware can ship with a fixed contract address and function selector. The operator can update the live infrastructure by changing contract storage, without modifying the Lua payload.
The Contract Pointed to 85.137.52.21
The returned URL introduced a second infrastructure lead:
http://85.137.52.21
Enrichment on `85.137.52.21` classified it as external infrastructure in:
Network: 85.137.52.0/24
Provider context: Virtual Systems LLC / VSYS Host
ASN: AS43641
Location: Amsterdam, Netherlands
Abuse contact: abuse-ams@v-sys.org
RIPE RDAP confirms the network as `85.137.52.0 - 85.137.52.255`, named `VSYS-AMS`, country `NL`, with a Virtual Systems Amsterdam abuse contact at `abuse-ams@v-sys.org`
The provider context is notable. A 2024 IBCAP press release announced a lawsuit against Virtual Systems, alleging that it advertised a "DMCA Ignored" policy and ignored more than 500 infringement notices.
TorrentFreak later reported that DISH won a default judgment against Virtual Systems in November 2025, with damages of $41,850,000 and a permanent injunction:
That context does not prove that VSYS knowingly hosted this malware infrastructure. It does make the infrastructure choice interesting: the smart contract pointed to hosting associated with an offshore provider publicly discussed in relation to DMCA-ignored hosting and abuse-handling concerns.
From a defender perspective, this is exactly why the smart contract pivot matters. The initial trace exposed `217.119.129.99`; the on-chain configuration exposed `85.137.52.21`. Without querying the contract, that second IP would have been easy to miss.
Practical Reproduction: Safe Read-Only Contract Calls
The contract data can be investigated without sending transactions. Use read-only JSON-RPC methods such as `eth_call`, `eth_getCode`, and `eth_getStorageAt`.
Replay the malware's call:
curl -sS -X POST https://polygon-public.nodies.app \
-H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x1823A9a0Ec8e0C25dD957D0841e3D41a4474bAdc","data":"0x3bc5de30"},"latest"],"id":1}'
Read the admin function:
curl -sS -X POST https://polygon-public.nodies.app \
-H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x1823A9a0Ec8e0C25dD957D0841e3D41a4474bAdc","data":"0xf851a440"},"latest"],"id":1}'
Read raw storage slot 1:
curl -sS -X POST https://polygon-public.nodies.app \
-H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["0x1823A9a0Ec8e0C25dD957D0841e3D41a4474bAdc","0x1","latest"],"id":1}'
Avoid state-changing functions such as `updateData(string)` and `destroyContract()`. For this kind of investigation, read-only calls are enough.
Detection and Hunting Ideas
Useful behavioral leads:
- Lua or LuaJIT launched by a batch file from an archive.
- LuaJIT processes using FFI to declare PE, PEB, and loader structures.
- Small executable allocation containing `64 a1 30 00 00 00 c3`.
- LuaJIT resolving APIs through `LdrLoadDll` and manual export parsing.
- GDI screen capture followed by WinINet multipart upload.
- Polygon JSON-RPC `eth_call` from non-browser, non-wallet processes.
- Calls to `0x1823A9a0Ec8e0C25dD957D0841e3D41a4474bAdc` with selector `0x3bc5de30`.
- Outbound HTTP to `217.119.129.99` or `85.137.52.21`.
Takeaways
There are three main lessons from this sample.
First, script malware can still be native malware. LuaJIT FFI gave this script direct access to Windows internals and native APIs.
Second, PEB walking is not limited to compiled loaders. Here, the malware implemented a native loader pattern from Lua by allocating a tiny x86 thunk and parsing PE exports through FFI-defined structures.
Third, blockchain infrastructure can be used as a small, resilient configuration layer. The payload did not need to hardcode every active endpoint. It could query a Polygon smart contract and recover an operator-controlled URL.
For this campaign, the GitHub repository is the lure, the Lua runtime is the execution substrate, LuaJIT FFI is the native bridge, the smart contract is the configuration pointer, and the observed payload behavior is screenshot collection and exfiltration.
Sources
- OrchidFiles, GitHub repositories distributing malware: https://orchidfiles.com/github-repositories-distributing-malware
- RIPE RDAP for `85.137.52.21`: https://rdap.db.ripe.net/ip/85.137.52.21
- 4byte signature lookup for `0x3bc5de30`: https://www.4byte.directory/api/v1/signatures/?hex_signature=0x3bc5de30
- IBCAP press release on the Virtual Systems lawsuit: https://www.globenewswire.com/news-release/2024/10/16/2963975/0/en/IBCAP-announces-41-million-lawsuit-against-Virtual-Systems.html
- TorrentFreak report on the 2025 default judgment: https://torrentfreak.com/dish-wins-42m-default-judgment-against-dmca-ignored-host-virtual-systems-251114/