Home Beavertail and InvisibleFerret malware
Post
Cancel

Beavertail and InvisibleFerret malware

Summary

FAMOUS CHOLLIMA, active since 2018, is a low-sophistication adversary almost certainly operates on behalf of the North Korean government (DPRK). The adversary primarily focuses on generating revenue through small-value cryptocurrency theft, credit card fraud, or illicit salaries via software development jobs. However, they can also exfiltrate data and may shift to intelligence collection or intellectual property theft if necessary.

The adversary uses custom malware families BeaverTail and InvisibleFerret, remote monitoring and management tools (RMMs), and malicious Node.js applications to deliver malware to victims. They also infiltrate corporate environments through malicious insiders, often hired as full-time equivalents directly or via contracting organizations. Despite their low sophistication, FAMOUS CHOLLIMA has displayed a high operational tempo and is expected to continue its activity in the foreseeable future with similar TTPs.

High-level overview of malware infection

Analysis

The malware manifests in the form of obfuscated JavaScript that is injected into NodeJS packages and application installers. The sample that has been analyzed was found within the run.js file of a NodeJS application.

Overview

The flow of activity on an infected device can be summarized as:

  • User is lured into installing a malicious NodeJS package
  • The NodeJS application executes obfuscated JavaScript
    • The embedded malicous JavaScript is the BeaverTail malware
  • BeaverTail attempts to:
    • Steal browser credentials and crypto related files
    • Performs GET/POST’s to:
      • C2_IP:1244/keys
      • C2_IP:1244/uploads
      • C2_IP:1244/pdown
      • C2_IP:1244/client/N3RFYU07 (InvisibleFerret)
    • Downloads and executes InvisibleFerret malware
    • Downloads a python wheel package called “pdown
      • https://pypi.org/project/pdown/
      • “A command line tool to download periscope charts and save the snapshot of the chart with a timestamp to a local file. Allows for historical data to be archived so that trends can be analyzed.”
  • InvisibleFerret
    • Collects system information, credentials, and installs a RAT
    • Exfiltrates information via Web, FTP, and Telegram
    • Performs GET/POST’s to:
      • C2_IP:1244/keys
      • C2_IP:1244/uploads
      • C2_IP:1244/brow/N3RFYU07
      • C2_IP:1244/adc

Analyzing the BeaverTail JavaScript

I downloaded the archive from VirusTotal that contains the malicious package. Upon decompressing it the extracted code was available for review. The README.md explained what this was and where it was originally hosted on BitBucket.

The original location to clone from is seen in the install instructions below but does not appear to be currently available on BitBucket (at least not publicly).

The malicious obfuscated code was discovered in the run.js file and is executed upon the start of the NodeJS server. This appears to be the BeaverTail malware that starts the infection chain.

I performed the first phase of deobfuscation by running it through https://deobfuscate.io. This doesn’t really deobfuscate it as much as it helps to format it better so you can start analyzing the JavaScript.

Upon making it more readable I copied it back into the run.js script to be able to debug it. I tried to analyze it in several ways statically, but it performs quite a bit of dynamic calls and creation of anonymous functions. In these cases, and due to the fact, it executes within the context of a NodeJS application I opted to understand it better dynamically.

Debugging
To setup this up it was the process of:

  • Installing NodeJS on FlareVM
  • Executing “npm install express
  • Opening the code repo in vscode (after all the installs are complete)
  • Replacing the embedded malicious JavaScript in run.js with the expanded version
  • Setting up a breakpoint at the beginning of the malicious code
  • Click Run->Start Debugging

The obfuscated code takes up 3.1Kb of space within the file.

The process involved a lot of stepping into anonymous functions, resolving and decoding strings from a shifted string array, and modifying the JavaScript to run properly within a debugger. There are several cases where I had to modify the malware code to not perform certain searches on the endpoint, disable anti-debug loops, and otherwise short circuit some logic to get to the calls to the C2.

The following screenshots illustrate some key parts of the malware code and areas that are worth talking about.

The string array
An array of strings is rotated until an integer condition is met. This array will be referenced later in the script as it pieces together these strings by index to make the script function. This is quite a common practice and seen routinely in obfuscated JavaScript.

The array of strings that are referenced can be found further down in the script. Throughout the remainder of the script these strings will be referenced by index.

A lot of these strings appear to be base64 encoded as well as some that are readily readable. We can do a quick decode on them just to see what we get.

We can already get an idea from the start that it’s going to be searching for browser data, keychain data, using curl and tar commands, etc. Later, we’ll see that some of the base64 strings are appended together to decode properly but this gives us some initial context.

Regex searches
The malware will perform regex searches that take quite a bit of time. During debugging this will cause a hang for a period of time, and this was not needed to continue. This can simply be changed to return immediately.

Debugger checks
There are some debugger checks that we can also return out of immediately to allow the script to continue to run. In this case, if a debugger is found it will put you in a forever while-true loop.

C2 IP address revealed
We finally get to a point where the C2 address and port are decoded and returned. In many cases in malware analysis, this is the key thing we are after especially if this is not revealing itself within a sandbox.

HTTP Requests are performed
The request to the C2 server is finally made. We can observe the URI and formData that it will submit. Throughout the running of this malware there are many requests to the same C2 performed as information is collected and exfiltrated.

Uploading browser data
As browser credentials and keys are collected there are calls made to submit them /uploads.

Downloading pdown The pdown package is hosted on the C2 IP addresses and downloaded through a GET request to C2_IP:1244/pdown. During my analysis this failed, and I had to pull this from VirusTotal. At the time of analysis, I did not catch how this was being used on the victim machine yet.

InvisibleFerret script
A final request is made to download the InvisibleFerret malware script. This was also pulled down from VirusTotal once the C2 was discovered.

Analyzing the InvisibleFerret loader script

The initial script is mostly base64 encoded and performs some XOR decoding.

Expanding out and annotating what the python code is doing to the base64 from above.

Using CyberChef to decode the script using the provided XOR key.

The key parts of the client script are shown below.

Decoding the C2
This script will decode the C2 to be used later to download another payload script in the next step.

Download the client payload script
If a ".n2" directory is already in the users home directory it deletes it and recreates it. It will then download another script from the C2 from the "/payload/" endpoint and save it to "~/.n2/pay".

At this point it will execute the newly downloaded script. The creation flags are setup to hide the window if on Windows. The script will also exit at this point if the victim machine appears to be macOS.

Download the brow script
Finally, if it is a Windows machine, it will additionally download a second script and save it to "~/.n2/bow" and execute it.

Analyzing the InvisibleFerret payload script

We start out with an encoded script again that needs to go through a decoding process.

This is a little interesting as it generates a lambda that performs:

  • reversing the bytes
  • base64 decoding them
  • zlib decompressing

The result ends up being another exec((_)(b'XXX as seen below.

This means that it is going to continue this process over and over until it reveals valid python code that will then be executed via the exec() function. But how many iterations is this going to take? To brute force this I wrote a little script to unpack it up to 255 iterations and tested for the text “import” as a success criteria.

source: decode_invisibleferret_payload.py

It took 50 iterations to reveal valid python code of which we can now dump out and analyze. The resulting script is no longer obfuscated, and I studied the code and summarized what it does below.

What does the payload script do?

  • Initial check-in with C2
    • Uses ip-api.com to get geo information on the victim IP
    • Collects uuid, system, release, version, hostname, and username from system
    • Connects to the C2 server and POST’s this data to "/keys"
  • Creates a thread to start a C2 communication loop with the adversary
    • Waits for commands from the C2
    • The incoming payloads are in JSON format
    • The payload will be a number that maps to a command class and arguments
    • Executes a keylogger and clipboard monitor

C2 Commands

CodeCommandDescription
1objEither executes a change directory (cd) command or executes an arbitrary command
(depending on provided arguments).
2cmdIf argument is "delete" the shell's session is forced to close.
3clipSends back the current clipboard and keylogging buffer.
4runTells client to download Browse script from "C2_IP:PORT/brow/".
5uploadParses one of several commands.
sdiraFTP's all files from current directory and down to the adversary-controlled server
with address and credentials supplied in arguments.
sdirFTP's the files of the current directory only to the adversary-controlled server
with address and credentials supplied in arguments.
sfileFTP's a specific file. The contents of the file are XOR encoded with
the key: G01d*8@(
sfindaRecursively searches for files matching a pattern starting from a directory provided
in arguments.
* Uploads discovered files via sfile command (above)
* Excludes searching an exclusion list of directories
sfindrSearches for files matching a pattern in the specified directory (non-recursive).
* Uploads discovered files via sfile command (above)
* Excludes searching an exclusion list of directories
sfindRecursively searches for files matching a pattern starting at current directory.
* Uploads discovered files via sfile command (above)
* Excludes searching an exclusion list of directories
6killKills all Chrome and Brave browser processes.
7anyTells client to download AnyDesk binary from "C2_IP:PORT/adc/".
8envFTP's files related to user based on their home directory location.
Windows
* Copies files from ~\\Documents
* Copies files from ~\\Downloads
Non-Windows
* Copies files from /home
* Copies files from /Volumes
9zcpMulti-stage search and exfiltration
* Searches and copies all profile information and data related to many browser extensions
for password managers, crypto, and authenticator (LastPass, Google authenticator, etc)
extensions (searches by static list of browser extension IDs)
* Searches and copies all AppData files for 1pass, exodus, atomic, electrum, winauth,
proxifier4, and dashlane
* Archives all files using a supplied password in argument or "2024" if not supplied
* Exfiltrates archive through Telegram

Exfiltration through Telegram
The adversary will send a token value as an argument to POST to a specific account.

Targeted browser extensions

The following table is a list of browser extensions that are searched for.

"aeachknmefphepccionboohckonoeemg": "Coin98"
"aholpfdialjgjfhomihkjbmgjidlcdno": "Exodus"
"bfnaelmomeimhlpmgjnjophhpkkoljpa": "Phantom"
"ejbalbakoplchlghecdalmeeeajnimhm": "MetaMask-Edge"
"ejjladinnckdgjemekebdpeokbikhfci": "PetraAptos"
"egjidjbpglichdcondbcbdnbeeppgdph": "Trust"
"fhbohimaelbohpjbbldcngcnapndodjp": "Binance"
"gjdfdfnbillbflbkmldbclkihgajchbg": "Termux"
"hifafgmccdpekplomjjkcfgodnhcellj": "Crypto.com"
"hnfanknocfeofbddgcijnmhnfnkdnaad": "CoinBase"
"ibnejdfjmmkpcnlpebklmnkoeoihofec": "TronLink"
"lgmpcpglpngdoalbgeoldeajfclnhafa": "Safepal"
"mcohilncbfahbmgdjkbpemcciiolgcge": "OKX"
"nkbihfbeogaeaoehlefnkodbefgpgknn": "MetaMask"
"nphplpgoakhhjchkkhmiggakijnkhfnd": "Ton"
"pdliaogehgdbhbnmkklieghmmjkpigpa": "ByBit"
"phkbamefinggmakgklpkljjmgibohnba": "Pontem"
"kkpllkodjeloidieedojogacfhpaihoh": "Enkrypt"
"agoakfejjabomempkjlepdflaleeobhb": "Core-Crypto"
"jiidiaalihmmhddjgbnbgdfflelocpak": "Bitget"
"kgdijkcfiglijhaglibaidbipiejjfdp": "Cirus"
"kkpehldckknjffeakihjajcjccmcjflh": "HBAR"
"idnnbdplmphpflfnlkomgpfbpcgelopg": "Xverse"
"fccgmnglbhajioalokbcidhcaikhlcpm": "Zapit"
"fijngjgcjhjmmpcmkeiomlglpeiijkld": "Talisman"
"enabgbdfcbaehmbigakijjabdpdnimlg": "Manta"
"onhogfjeacnfoofkfgppdlbmlmnplgbn": "Sub-Polkadot"
"amkmjjmmflddogmhpjloimipbofnfjih": "Wombat"
"glmhbknppefdmpemdmjnjlinpbclokhn": "Orange"
"hmeobnfnfcmdkdcmlblgagmfpfboieaf": "XDEFI"
"acmacodkjbdgmoleebolmdjonilkdbch": "Rabby"
"fcfcfllfndlomdhbehjjcoimbgofdncg": "LeapCosmos"
"anokgmphncpekkhclmingpimjmcooifb": "Compass-Sei"
"epapihdplajcdnnkdeiahlgigofloibg": "Sender"
"efbglgofoippbgcjepnhiblaibcnclgk": "Martian"
"ldinpeekobnhjjdofggfgjlcehhmanlj": "Leather"
"lccbohhgfkdikahanoclbdmaolidjdfl": "Wigwam"
"abkahkcbhngaebpcgfmhkoioedceoigp": "Casper"
"bhhhlbepdkbapadjdnnojkbgioiodbic": "Solflare"
"klghhnkeealcohjjanjjdaeeggmfmlpl": "Zerion"
"lnnnmfcpbkafcpgdilckhmhbkkbpkmid": "Koala"
"ibljocddagjghmlpgihahamcghfggcjc": "Virgo"
"ppbibelpcjmhbdihakflkdcoccbgbkpo": "UniSat"
"afbcbjpbpfadlkmhmclhkeeodmamcflc": "Math"
"ebfidpplhabeedpnhjnobghokpiioolj": "Fewcha-Move"
"fopmedgnkfpebgllppeddmmochcookhc": "Suku"
"gjagmgiddbbciopjhllkdnddhcglnemk": "Hashpack"
"jnlgamecbpmbajjfhmmmlhejkemejdma": "Braavos"
"pgiaagfkgcbnmiiolekcfmljdagdhlcm": "Stargazer"
"khpkpbbcccdmmclmpigdgddabeilkdpd": "Suiet"
"kilnpioakcdndlodeeceffgjdpojajlo": "Aurox"
"bopcbmipnjdcdfflfgjdgdjejmgpoaab": "Block"
"kmhcihpebfmpgmihbkipmjlmmioameka": "Eternl"
"aflkmfhebedbjioipglgcbcmnbpgliof": "Backpack"
"ajkifnllfhikkjbjopkhmjoieikeihjb": "Moso"
"pfccjkejcgoppjnllalolplgogenfojk": "Tomo"
"jaooiolkmfcmloonphpiiogkfckgciom": "Twetch"
"kmphdnilpmdejikjdnlbcnmnabepfgkh": "OsmWallet"
"hbbgbephgojikajhfbomhlmmollphcad": "Rise"
"nbdhibgjnjpnkajaghbffjbkcgljfgdi": "Ramper"
"fldfpgipfncgndfolcbkdeeknbbbnhcc": "MyTon"
"jnmbobjmhlngoefaiojfljckilhhlhcj": "OneKey"
"fcckkdbjnoikooededlapcalpionmalo": "MOBOX"
"gadbifgblmedliakbceidegloehmffic": "Paragon"
"ebaeifdbcjklcmoigppnpkcghndhpbbm": "SenSui"
"opfgelmcmbiajamepnmloijbpoleiama": "Rainbow"
"jfflgdhkeohhkelibbefdcgjijppkdeb": "OrdPay"
"kfecffoibanimcnjeajlcnbablfeafho": "Libonomy"
"opcgpfmipidbgpenhmajoajpbobppdil": "Sui"
"penjlddjkjgpnkllboccdgccekpkcbin": "OpenMask"
"kbdcddcmgoplfockflacnnefaehaiocb": "Shell"
"abogmiocnneedmmepnohnhlijcjpcifd": "Blade"
"omaabbefbmiijedngplfjmnooppbclkk": "Tonkeeper"
"cnncmdhjacpkmjmkcafchppbnpnhdmon": "HAVAH"
"eokbbaidfgdndnljmffldfgjklpjkdoi": "Fluent"
"fnjhmkhhmkbjkkabndcnnogagogbneec": "Ronin"
"dmkamcknogkgcdfhhbddcghachkejeap": "Keplr"
"dlcobpjiigpikoobohmabehhmhfoodbb": "ArgentX"
"aiifbnbfobpmeekipheeijimdpnlpgpp": "Station"
"eajafomhmkipbjmfmhebemolkcicgfmd": "Taho"
"mkpegjkblkkefacfnmkajcjmabijhclg": "MagicEden"
"ffbceckpkpbcmgiaehlloocglmijnpmp": "Initia"
"lpfcbjknijpeeillifnkikgncikgfhdo": "Nami"
"fpkhgmpbidmiogeglndfbkegfdlnajnf": "Cosmostation"
"kppfdiipphfccemcignhifpjkapfbihd": "Frontier"
"fdjamakpfbbddfjaooikfcpapjohcfmg": "Dashalane"
"bhghoamapcdpbohphigoooaddinpkbai": "GoogleAuth"

Analyzing the InvisibleFerret browser script

The browser script is in the same format as the initial client script appeared but uses a different XOR key to decode itself.

What does the browser script do?

  • Retrieves the users "Local State" file to extract the key bytes
  • For each browser
    • Opens the "Login Data" file
    • Decrypts all accounts and passwords
    • Opens the "Web Data" file
    • Decrypts all saved credit cards
  • Exfiltrates this data back to the C2 IP by POST’ing it to "/keys"

Example use-case for the Edge browser on Windows

  • Gets the "Local State" file to extract the decryption key
    • %AppData%\Local\Microsoft\Edge\User Data\Local State
  • Gets the "Login Data" file to decrypt accounts and passwords
    • C:\Users\Jack Simpson\AppData\Local\Microsoft\Edge\User Data\Default\Login Data
  • Gets the “Web Data” file to decrypt stored credit cards
    • %AppData%\Local\Microsoft\Edge\User Data\Default\Web Data

In a SqlLite3 browser we can execute the same query and see an example credential I added into the Edge browser. We can see that the password value is a blob and encrypted hex.

We can see the query to fetch the stored credit cards as well and that the numbers are protected.

However, after reading what the malware is doing, I re-wrote the stealer code in a simplified version to demonstrate how this works against these database files on a Windows system.

We will first extract the decryption key out of the “Local State” file for the browser.

We will then decrypt the credentials from the "Login Data" file that contains the accounts and passwords.

We will then decrypt the credit card information from the "Web Data" file.

When this is printed out, we can see that we have properly decrypted the data.

This data is now exfiltrated back out to the adversary controlled C2 IP by POST’ing it to "/keys".

source: ferret_browser_cred_theft.py

Finding additional C2 IP addresses

Given the pattern identified in requests to the C2’s I put together a query that should capture most of them based on In-The-Wild (ITW) URLs observed by VirusTotal. This was the primary way I collected additional C2 IP addresses for flow analysis.

1
2
3
4
itw:":1224/keys" or itw:":1244/keys" or itw:":1224/pdown" or itw:":1244/pdown" or 
itw:":3000/pdown" or itw:":1224/payload" or itw:":1244/payload" or 
itw:":1244/uploads" or itw:":1224/uploads" or itw:":1224/brow/" or itw:":1244/brow/" 
or itw:":1244/client/" or itw:":1224/client/"

Censys Query

The websites on port 1224/tcp, 1244/tcp, 1245/tcp, and 3000/tcp seem to have some unique text in the html response that I tried querying for. Most of these results are in my list of C2 servers. Two of them are not but look suspicious.

services.http.response.html_title="Node.js upload multiple files"

Snort signatures

There are two components that get downloaded upon infection by InvisibleFerret that we can detect on. These show up as a GET requests to /payload/X and /brow/X as well as POST’s to /keys.

1
2
3
4
5
alert tcp $HOME_NET any -> $EXTERNAL_NET 1224,1244,1245 ( msg:"Trojan.InvisibleFerret/MC/FerretInfo Script Download"; flow:to_server; content:"GET /payload/"; offset:0; depth:13; pcre:"/GET |2f|payload|2f|[A-Za-z0-9\/]+ HTTP|2f|1\.1/"; content:"User-Agent: python-requests"; within:65; content:"Accept-Encoding: gzip, deflate|0d 0a|"; within:41; classtype:trojan-activity; priority:3; sid:824081200; rev:1; )
alert tcp $HOME_NET any -> $EXTERNAL_NET 1224,1244,1245 ( msg:"Trojan.InvisibleFerret/MC/FerretBrowse Script Download"; flow:to_server; content:"GET /brow/"; offset:0; depth:10; pcre:"/GET |2f|brow|2f|[A-Za-z0-9\/]+ HTTP|2f|1\.1/"; content:"User-Agent: python-requests"; within:61; content: "Accept-Encoding: gzip, deflate|0d 0a|"; within:41; classtype:trojan-activity; priority:3; sid:824081201; rev:1; )
alert tcp $HOME_NET any -> $EXTERNAL_NET 1224,1244,1245 ( msg:"Trojan.InvisibleFerret/MC/POST to keys (python)"; flow:to_server; content:"POST /keys HTTP/1.1|0d 0a|"; offset:0; depth:21; content:"User-Agent: python-requests"; within:55; content:"Content-Type: application/x-www-form-urlencoded|0d 0a 0d 0a|"; within:150; classtype:trojan-activity; priority:3; sid:824081202; rev:1; )
alert tcp $HOME_NET any -> $EXTERNAL_NET 1224,1244,1245 ( msg:"Trojan.InvisibleFerret/MC/POST to keys (non-python)"; flow:to_server; content:"POST /keys HTTP/1.1|0d 0a|"; offset:0; depth:21; content:"content-type: multipart|2f|form-data"; within: 61; content:"name=|22|ts|22|"; within:500; classtype:trojan-activity; priority:3; sid:824081203; rev:1; )
alert tcp $HOME_NET any -> $EXTERNAL_NET 1224,1244,1245 ( msg:"Trojan.InvisibleFerret/MC/POST to uploads"; flow:to_server; content:"POST /uploads HTTP/1.1|0d 0a|"; offset:0; depth:24; content:"content-type: multipart|2f|form-data"; within: 61; content:"name=|22|type|22|"; within:500; classtype:trojan-activity; priority:3; sid:824081204; rev:1; )

Indicators of Compromise

NameTypeValue
malicious NodeJS (Beavertail)MD57b75e49344843199f357a0e5d17eb5c1
C2IPv423.106.253[.]194
23.106.253[.]209
45.61.131[.]218
45.61.169[.]99
67.203.7[.]163
67.203.7[.]171
95.164.17[.]24
147.124.212[.]89
147.124.212[.]146
147.124.214[.]129
147.124.214[.]131
147.124.214[.]237
167.88.168[.]152
172.86.98[.]240
185.235.241[.]208
main_N3RFYU07.py (InvisibleFerret)MD5682d404dc90e389130f71bcb6b2ab7fb
pay_N3RFYU07.py (InvisibleFerret - payload)MD5cb7a41eae4acd144505fc2d9b81ac2c5
brow_N3RFYU07.py (InvisibleFerret - browse)MD53686a3481b1c79f251dc3d4f428ac15e
This post is licensed under CC BY 4.0 by the author.