- ZScaler reports that njRat was first observed in the wild in 2013 and was prevalent for many years. Written in .NET, it contains a lot of standard RAT functionalities:
- Loading other malware
- Stealing private cryptocurrency keys
- Keylogging, password stealing
- Self-propagation, and more
References
ZScaler - njRAT pushes Lime ransomware and Bitcoin wallet stealer
BlackBerry - Threat Thursday: Don't Let njRAT Take Your Cheddar
Hash Type | File Hash |
---|---|
MD5 | ba17472a8155ff38b3b6fa9a17f5aa70 |
SHA1 | fd81eb5f702b8c0d8a790bf6cced05f3d2b4ae7e |
SHA-256 | ed04bf92f6fdea80a5580a3dc115ab38332ad30418d4611f8942e3f8f18b45fb |
Analysis of the Main Function
- The
main()
function starts by immediately performing a sleep for 3 seconds before it proceeds to initializes the configuration.- If the call to
Settings.InitializeSettings
fails, themain()
function exits with code0
.
- If the call to
- The
Settings.InitializeSettings
method uses simple Base64 decoding followed by AES decryption to de-obfuscate config settings.
- After the configuration is initialized, the sample proceeds to call methods under its
Mutex
class, invoking theCreateMutexHandle
method. This method generates a call to the Windows APICreateMutexHandle
, which will return a handle to the newly created mutex object.
- Next, from the
main()
function, there is a sequence of calls to methods under theSettings
class. At this point, theSettings
class clearly contains a lot of the initialization functionality.
- A call is made to a less commonly seen Windows API,
InternetGetConnectedState
, which returns aBOOL
value -- TRUE if there is an internet connection, otherwise FALSE. - If no internet connectivity is found, the
main()
function performs an indefinite sleep. Thewhile
loop with an empty catch block ensures that this logic will simply repeat every 5 minutes until the host is online.
- Once internet connectivity is established, execution proceeds to the
Installation.Install
method, which creates a file named$77helppanel.exe
under some type of "ServiceProfiles" folder under a "Templates" directory. - Next, the sample performs a basic success check and then proceeds to set the desired access rights for the file path. In particular, it denies the current user
READ
access, making the user much less likely to notice the presence of the executable. - Next, it uses
process.Kill()
to kill other instances of itself. After some more hiding of file system and process artifacts, it establishes persistence via a RegistryRun
key.
- The specimen checks the domain group of the current user to determine if they are an admin user (member of the
Administrators
built-in role). - If they are, the ASEP key is set under the
HKLM
hive (effective for all users). If they are not, the ASEP key is simply set under the current user profile (HKCU
).
-
Some pre-launch commands are run to start the main
.exe
and delete the temporary batch file from disk. -
The
AddException
method is called next, which creates a Microsoft Defender exclusion for allexe
,bat
,dll
, and.ps1
files in a hidden window.
- Returning back to the
main()
function, a scheduled task is created as an additional persistence mechanism.
- Once the local initialization on the endpoint is complete and persistence has been established, njRAT proceeds to the
MessageRead.DiscordNotif
method where it uploads basic host and user data to a Discord channel with a notification message to the operator.
- After returning to the
main()
function, it callsPreventSleep
which uses the windows APISetThreadExecutionState
with the appropriate flags to prevent the system from sleeping.
- At the end of the
main()
function, njRAT invokes an infinite loop withfor (;;)
. This loop ensures that the client is online and perpetually connected with a built-in reconnect delay for redundancy.
- As we saw before, the config data is base64 encoded and encrypted with AES-256. In order to obtain the plaintext, we'll simply have to re-create that process in our own controlled environment.
-
The decryption method used by the malware author works like this:
- Takes an array of encrypted bytes as a parameter (
cipherBytes
) and prepares an empty array into which it will output the decrypted bytes. - Creates an AES cryptographic object with
Aes.Create()
- Uses a pre-written password and salt to generate a key and IV using
PBKDF2
(Password-Based Key Derivation Function 2). - Extracts 32 bytes for the AES encryption key (AES-256 requires a 256-bit key, which is 32 bytes).
- Extracts 16 bytes for the initialization vector (IV) (AES in CBC mode uses a 128-bit IV, which is 16 bytes).
- Creates a
MemoryStream
object to temporarily store the decrypted data in memory. - Creates a
CryptoStream
object, which is used to perform the AES decryption transform. - The encrypted data is then written to the
CryptoStream
object, decrypted, and the output is finally passed to the byte array.
- Takes an array of encrypted bytes as a parameter (
-
We can accomplish the same exact thing with a quick Python script. We'll have to use the
AES
class from theCrypto.Cipher
module and thePBKDF2
class from theCrypto.Protocol.KDF
module. -
Once we import those, we'll have some useful functions. If you're lazy, I have included the script below so you can simply copy and paste.
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
import base64
import io
def decrypt_bytes(cipher_bytes):
# Password and salt from original function written by TA
password = b"0x49,0x76,0x61,10,0x20,0x4D,0x65,100,0x76,0x65,100,0x65,0x76"
salt = bytes([73, 118, 97, 110, 32, 77, 101, 100, 118, 101, 100, 101, 118])
# Derive the key (AES-256) and IV
key = PBKDF2(password, salt, dkLen=32, count=1000)
iv = PBKDF2(password, salt, dkLen=16, count=1000)
# Initialize AES decryptor
aes = AES.new(key, AES.MODE_CBC, iv)
# Decrypt the data
with io.BytesIO() as memory_stream:
try:
decrypted_data = aes.decrypt(cipher_bytes)
memory_stream.write(decrypted_data)
return memory_stream.getvalue()
except Exception as e:
print(f"Decryption error: {e}")
return memory_stream.getvalue()
base64_ciphertext = "ENCRYPTED_CONFIG_TEXT_GOES_HERE"
ciphertext = base64.b64decode(base64_ciphertext)
plaintext = decrypt_bytes(ciphertext)
print("Decrypted plaintext:", plaintext.decode('utf-8', errors='ignore'))
- The output from the script appears like this:
- Using the script as well as our findings from the analysis phase, we are left with some IOCs that we can incorporate into our detection logic.
IOCs
IOC | Description |
---|---|
SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\$77helppanel.exe |
ASEP key for persistence |
Creation of a scheduled task named $77helppanel.exe |
Scheduled task for persistence |
Creation of %TEMP%\*.bat |
Batch file used for pre-launch commands. |
Network callouts to *discord.com/api/webhooks/1306009594367180840/Zg6W2rH_yPNkl7Hn5Z-GWjtm8W94xN_PzceHo8g5RjjoNr4vkRdq1c70arvb91az-VPT |
Discord webhook URL |
HTTP GET requests for *h53p.io/p_2522c7w8u1.png |
The URI of the logo imported in the config |