> Packed with goodies: analyzing Cylance
(01/03/2026), 5 minute read ⏰
Note: these are just my markdown notes, but a bit cleaned up. The original sample link is there too! Feel free to check out the original notes here!
Today is a great day to analyze malware. I have no intel on this sample from anybody, apart from the fact that it was mentioned in a Discord server I am in, as a neat sample to check out. My last writeup was quite a washout and I'm not at all happy with it, so this time, I'm making an addition to our protocol, that being a well-defined goal.
From the words of struppigel on the OALabs discord: "What's your analysis goal, though?"
My past writeups had no clear goal apart from let's analyze this sample until I wanna die. That's not gonna help me in any manner.
Let's first define the steps we'll follow for analyzing this sample!
1. Determine if the sample is packed or not using DiE. Unpack as necessary. Perform some basic fingerprinting; get SHA256 hash, find resources, file type, compiler info, imports/exports, and strings.
2. Run the binary through a sandbox (VT).
3. Decompile the sample (check for dynamic API resolution, obfuscation, and shellcode.) If we encounter any sort of obfuscation, determine how it's done and reverse its functionality to continue analysis.
My goals for this writeup are as follows:
1) Determine the type of malware we are dealing with.
2) Determine at least 3 capabilities/actions of the malware.
A third and final bonus task for myself is to write a YARA rule for the sample, with 1 condition per capability/action performed.
I'll be using my past toolkit (Malcat, Binja, DiE, RH), as well as (for no other reason apart from learning) pestudio for this analysis. I'd like to get familiar with all the tools I have available to me so I know when to use them.
With all that being said, let's begin our analysis!
At first sight with DiE, we can see we're dealing with another ancient Windows binary.
No packers are detected, but as I went through the various Advanced menus, the entropy section reports otherwise.
Spoiler, it is not UPX from the looks of it. Throwing the sample into UnpacMe gives us some answers.
Awesome sauce. We can see a lot of good findings here, which have MITRE TTPs tagged as well, which I will gladly add to the TTPs section of this writeup.
Now with the unpacked sample in our hands, let's run it back through DiE again.
Let's check the strings now!
Very nice of them to call some file/program/Mutex name (whatever it may be) FILE SYSTEM WIPER. Gee, I wonder what kind of malware this is. Real talk, it is either a Wiper, Ransomware, or a File Infector.
One particular string I left out was GetSystemInfo, but take a FAT guess as to what that does. We also have CreateProcessW, which I do not like at all. CreateFileW is also present, which is interesting (wink wink nudge nudge).
Lastly, there are some weird "symbols" near the bottom of the strings feed, which I can only assume are encrypted at the moment.
Last thing to check out are the imports.
Marvelous. Let's summarize what we've found so far:
- The sample was originally packed with a generic packer according to UnpacMe.
- The sample performs a number of system info enumeration tasks.
- The sample performs multiple "sleep" operations using ping and forcefully shuts down/reboots.
- The sample appears to create and edit files, including some temp files of its own.
- The sample potentially wipes the OS or a select amount of files.
- The sample is a 32-bit binary written in C, compiled with MSVC.
I'm not including the findings from UnpacMe, since we still need to verify those.
The last thing I will do at this stage is run the unpacked sample through pestudio, just to see how it looks there and to check out the UI.
I am a happy man after seeing these features. I'll definitely start using pestudio after DiE from time to time. I do also have PEBear to check out as well.
We're done here, so let's carry on.
Next up is to run the binary through VT.
Some very interesting behaviour is noted under the Behaviour tab for this sample, stating that it will self delete, which is something I've never seen before. The temp file that we spotted previously is in fact opened/edited/dropped to the system. It also appears that this sample interacts with a lot of DLLs?
We can confirm as well that the Global\FSWipe name is a Mutex created by the sample.
We're basically done with this step, so let's move on to our static analysis phase. Because we've uncovered quite a bit, I'll go straight to analyzing the sample in Binja, then Malcat.
Why? Because Malcat may just give us all the answers we need with YARA hits, and I'd rather leave that for the end, but we're on a roll at the moment.
At first sight in Binja, this is what we can see:
(There are actually supposed to be TWO backslashes in that string, you'll see why in a bit!) Let's check out that subroutine and see what we can find.
There is not at all enough space to write notes on this, so I'll write them here! First things first, the thread is basically told "run as fast as you can". This is whatever when we compare it to the nightmare that is coming up.
The second blob will create a new file with Generic Write access, and is basically locked (like a Mutex lock) by passing FILE_SHARE_NONE, which prevents any opening of the file until the handle is closed. No fancy security descriptor is set for the file. The file is created if it isn't yet made, and truncated otherwise.
Something interesting occurs with the dwFlagsAndAttributes argument. There are 2 flags that get passed, which are FILE_FLAG_OVERLAPPED and FILE_FLAG_BACKUP_SEMANTICS. Simply put, these allows for async IO operations but also providing a handle to directories and/or to create backup files.
One thing I did not address was the filename, which I am partially uncertain about, but my best guess is that it is the temp file path, with the number of bytes appended to it. Furthermore, the reason why there's that weird \\?\ string is that this increases the path size to 32,767 characters (according to the CreateFileW docs.) I only say this because I don't know why else the temp filepath was passed as a variable to the calling thread if not for this.
The third and final blob has a lot going on. We basically check if we have not made some OOB (out of bound) handle. If we are good with this check, then we allocate 4096 bytes (4MB) of memory, which is reserved for later use, granting only read/write permissions.
Here comes the fun. We write out to the last created file the data within the last allocated 4MB block. Whatever is in that memory is either garbage or has been previously loaded by the sample, but I believe it to be the former.
So we basically have garbage data being written to garbage files, so what gives? I'm wondering the same thing honestly. This subroutine ends here, so we can go back to the entry point and dig deeper.
At the very end of the entry point, another subroutine is called (after all threads and the heap have been cleared up).
The unicode character I'm referring to is Ą. I believe the sample does self delete since I can't seem to find any other filenames being pushed, or at least any that would MAKE SENSE in this scenario.
The subroutine continues, and if command line arguments WERE passed, then they would run given the provided argument. In all cases, the sample is QUEUED to self-deletes (param_1 will be used later...).
The highlighted blob at the bottom is all that's left for us really. The remainder of the function just closes all handles and exits just fine.
This CreateProcessW call will:
- specify the process name for a unicode character given i_3. Binja tells us it has a value of 0, so to this end, the name will just be whatever lpCommandLine is, which is the passed self deletion + reboot/shutdown command!
- set the default security descriptor for the process' thread (also is non-inheritable by child processes), given lpProcessAttributes is NULL. No handles from the sample process can be inherited by this process either, given bInheritHandles as NULL.
- create the process with no window.
- set the process environment as the calling process' environment, given lpEnvironment as NULL (basically inheriting it).
- pass the process/startup info previously calculated to this new process.
Simply put, we're just silently deleting the malware at this stage.
At this point, we've actually completed our analysis. My last 2 tasks will be checking this out in Malcat and completing our goal assignments.
Malcat shows basically nothing of interesting in terms of the YARA hits, and it basically confirmed our findings of running batch commands/hardware enumeration.
Recall the 2 goals (+ bonus) goal I had in mind at the beginning of this writeup:
1) Determine the type of malware we are dealing with.
2) Determine at least 3 capabilities/actions of the malware.
A third and final bonus task for myself is to write a YARA rule for the sample, with 1 condition per capability/action performed.
Because I am having too much fun. I decided to create 3 YARA rules encapsulating the 3 big artifacts of this sample, those being the temp filepath/string, the 3 available "completion" commands of the sample, and finally the Mutex name. I made all of these with the help of Malcat's YARA scanner page, which was very nice to interact with. NOTE: these rules are slightly broken with their conditions. See the first edit below.
That is all I have to share with this sample. I am very pleased with the findings we got, though the sample remains unknown to a degree. If anything, we may have lost some information during the unpacking phase. Regardless, I learnt a lot this time around, and wrote YARA rules for the 2nd time in my life (firstly during HTB's YARA module).
Thanks for reading this writeup!
Edit (01/04/26)
The amazing struppigel on the OALabs discord server noted that my YARA rules were in fact broken and matched for literally anything w/ the wrong conditions, so I fixed those up! I will leave the rules here for anyone that wants them (let me know if something is still broken, or if you want to point something out) (sorry mobile viewers, you'll need to scroll to read this):
rule Cylance_Mutex_Fingerprint : suspicious {
meta:
name = "Cylance_Mutex_Fingerprint"
category = "fingerprint"
description = "Name of mutex created by Cylance"
author = "0xnubb"
created = "2026-01-03"
reliability = 90
tlp = "TLP:white"
sample = "a6c41f368f42a7c57c307a48ce2440a60a744226b6414fadb6517a80a5d160a2"
strings:
$s1 = "Global\\FSWiper" wide
condition:
all of them
}
rule Cylance_Commands : suspicious {
meta:
name = "Cylance_Commands"
category = "destruction"
description = "Commmands used to self-delete Cylance"
author = "0xnubb"
created = "2026-01-03"
reliability = 90
tlp = "TLP:white"
sample = "a6c41f368f42a7c57c307a48ce2440a60a744226b6414fadb6517a80a5d160a2"
strings:
$s1 = "cmd.exe /c ping 127.0.0.1 -n 5 > nul & del \"%s\" & shutdown /r /t 0" wide
$s2 = "cmd.exe /c ping 127.0.0.1 -n 5 > nul & del \"%s\" & shutdown /s /t 0" wide
$s3 = "cmd.exe /c ping 127.0.0.1 -n 5 > nul & del \"%s\"" wide
condition:
any of them
}
rule Cylance_TmpFile : suspicious {
meta:
name = "Cylance_TmpFile"
category = "destruction"
description = "Temp file created by Cylance during multithreaded CreateFile/WriteFile calls"
author = "0xnubb"
created = "2026-01-03"
reliability = 90
tlp = "TLP:white"
sample = "a6c41f368f42a7c57c307a48ce2440a60a744226b6414fadb6517a80a5d160a2"
strings:
$s1 = "\\\\?\\%hc:\\0F3LWP.tmp" wide
condition:
all of them
}
Sources
GetSystemInfo docs
CreateMutexW docs
wsprintf format specifiers
CreateFileW docs (mainly for flags)
PROCESS_INFORMATION struct docs
CreateProcessW docs
TTPs
Execution
T1059: Accept command line arguments to reboot/shutdown on completionDefense Evasion
T1027: Obfuscation using XOR, Curve255519, base64, Salsa20/ChaChaT1070.004: Malware Self-Deletion
Discovery
T1082: Enumerate system information (CPU architecture, logical drives, memory capacity)Impact
T1529: Force shutdown/rebootIOCS
Sample SHA256: 7a5e813ec451cde49346d7e18aca31065846cafe52d88d08918a297196a6a49f
Unpacked SHA256: a6c41f368f42a7c57c307a48ce2440a60a744226b6414fadb6517a80a5d160a2