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
- The embedded malicous JavaScript is the
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"
- Uses
- 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
Code | Command | Description | ||||||||||||
1 | obj | Either executes a change directory (cd) command or executes an arbitrary command (depending on provided arguments). | ||||||||||||
2 | cmd | If argument is "delete" the shell's session is forced to close. | ||||||||||||
3 | clip | Sends back the current clipboard and keylogging buffer. | ||||||||||||
4 | run | Tells client to download Browse script from "C2_IP:PORT/brow/". | ||||||||||||
5 | upload | Parses one of several commands.
| ||||||||||||
6 | kill | Kills all Chrome and Brave browser processes. | ||||||||||||
7 | any | Tells client to download AnyDesk binary from "C2_IP:PORT/adc/". | ||||||||||||
8 | env | FTP'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 | ||||||||||||
9 | zcp | Multi-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
- Opens the
- 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 passwordsC:\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
Name | Type | Value |
---|---|---|
malicious NodeJS (Beavertail) | MD5 | 7b75e49344843199f357a0e5d17eb5c1 |
C2 | IPv4 | 23.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) | MD5 | 682d404dc90e389130f71bcb6b2ab7fb |
pay_N3RFYU07.py (InvisibleFerret - payload) | MD5 | cb7a41eae4acd144505fc2d9b81ac2c5 |
brow_N3RFYU07.py (InvisibleFerret - browse) | MD5 | 3686a3481b1c79f251dc3d4f428ac15e |