Skip to content
another pdf viewer - is it malicious?
Blog Threat Intelligence Another PD...

Another PDF Viewer - Is It Malicious?

Christopher Lopez Christopher Lopez
Senior macOS Security Researcher
23 min read

For security researchers, sometimes spending time reversing a potential suspicious file does not result in it being malicious. There is always something to learn from these efforts, and sometimes they can result in an interesting story even if it does not result in malware. I considered not writing this up but decided (with some help from friends) to release this as an article that details the process of trying to determine if something is malicious. 

This is one such story that details a PDF that requires a specific PDF viewer application in order to open and extract an encrypted embedded PDF to display to the user, definitely a little strange.

On September 17, 2024, MalwareHunterTeam (@malwrhunterteam) on Twitter/X shared a hash for a file named OSX-PDF-Viewer that was being detected as another DPRK (North Korea) attributed malware by several vendors on VirusTotal. They posed the question if this file was actually malware. 

Several responses to this post provided more details about this application including the use of the same XOR key seen in last year’s Rustbucket malware chain and the related PDF document. In this blog we will dive into this PDF Viewer application and the associated PDF to help try to answer that question in the title of this blog and try to help determine if this is in fact malicious and attributed to DPRK. 

Dedicated PDF Viewer.app Analysis

Before diving into the code, let’s look at the application bundle for potentially interesting information that may help with our overall understanding. The code signature indicates that this application is adhoc signed and lists the identifier as "com.activeminds.Viewer".

Executable=/Users/l0psec/malware/Dedicated PDF Viewer.app/Contents/MacOS/OSX-PDF-Viewer
Identifier=com.activeminds.Viewer
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20400 size=1679 flags=0x2(adhoc) hashes=42+7 location=embedded
<...>

Interestingly, there is a README file inside the application bundle’s Resources directory which provides some clues as to where this application may have originated. 

# OSX-PDF-Viewer

A PDF viewer created for a 3rd year Computer Science project. A Cocoa application created using Swift and the PDFkit provided by the Quartz framework. Created by Patrick Skinner and Cassidy Mowat.

<....>

Searching for this project on Github using the names listed above, we will see this open source project from at least 8 years ago.

Now that we have some information on the type of application this may be, we can start analyzing the mach-O and be able to compare what we see with this open source project. 

OSX-PDF-Viewer Analysis

The open source project above mentioned that the Cocoa application was written in Swift. There is also one file in the project named AppDelegate.swift that contains most of the important code for this application. Looking for evidence of this within the symbols of the target application in Binary Ninja leads to several method calls for this class that are potentially of interest. 

Running the application to see how the Application accepts a PDF document shows that there are two buttons used, Open Document and Recent Documents. Pressing the Open Document button leads to an opened panel targeting the user’s Downloads directory to select a file. We can also see that only the items that have the .pdf extension are highlighted as an option to open. 

Once the related PDF called “Investment Opportunity - Fenbushi Capital.pdf” is opened, the user (without the use of the PDF viewer) is instructed by the PDF to open with a “dedicated viewer.” This is why this application would even be used in the first place.

With this information, we can start looking for symbols related to opening a PDF. Searching for the word “Open” in the Symbols search box there are several methods with the name Open and even a Swift closure. We will focus our attention on the Open method highlighted below. 

AppDelegate.Open()

10000517c  void -[_TtC14OSX_PDF_Viewer11AppDelegate Open:](id self, SEL sel, id Open)

100005184      return @objc AppDelegate.Open(_:)(self, sel, Open, AppDelegate.Open(_:)) __tailcall

This function returns the function AppDelegate.Open() so we will continue the analysis there. 

At the start of this function, we can see that an NSOpenPanel object is initialized and will be used to handle how the user is able to select a file to open. 

1000039e8      struct objc_object* NSOpenPanel = -[_TtC14OSX_PDF_Viewer11AppDelegate init](self: _objc_allocWithZone(cls: _OBJC_CLASS_$_NSOpenPanel, zone), sel: "init")
100003a1c      // NSDownloadsDirectory
100003a0c      id defaultManaager = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: _objc_opt_self(obj: _OBJC_CLASS_$_NSFileManager), cmd: "defaultManager"))
100003a2c      // NSUserDomainMask
100003a2c      id obj = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: defaultManaager, cmd: "URLsForDirectory:inDomains:", 0xf, 1))
100003a38      _objc_release(obj: defaultManaager)
100003a44      void* DirectionaryForDomainsArray = static Array._unconditionallyBridgeFromObjectiveC(_:)(obj, metaDataURL)

There is also a call to [defaultManager URLsForDirectory:inDomains:] passing in the values of 0xf and 1 which returns an NSArray with one element for the user’s Downloads directory. This NSArray is then converted to a Swift array by using the function Array._unconditionallyBridgeFromObjectiveC() and passing in the NSArray and metadata for the URL type. 

This is important since it is why the panel is set to the Downloads directory when clicking the Open Document button in the application which we will see after an If statement checks the return value of the call to URLsForDirectory:inDomains:.

100003a8c          DownloadsDirectory = URL._bridgeToObjectiveC()()
100003aa0          _objc_msgSend(self: NSOpenPanel, cmd: "setDirectoryURL:", DownloadsDirectory)

The single element for the NSURL for ~/Downloads within the Swift Array is then converted to an NSURL object and passed to [NSOpenPanel setDirectoryURL:].  

 

Using the NSOpenPanel objects, other method calls are passed to set up additional configuration options for the panel presented to the user. 

100003c54      _objc_msgSend(self: NSOpenPanel, cmd: "setAllowsMultipleSelection:", 1)
100003c68      _objc_msgSend(self: NSOpenPanel, cmd: "setCanChooseDirectories:", 0)
100003c7c      _objc_msgSend(self: NSOpenPanel, cmd: "setCanCreateDirectories:", 0)
100003c90      _objc_msgSend(self: NSOpenPanel, cmd: "setCanChooseFiles:", 1)
100003cb4      int64_t pdf = Array._bridgeToObjectiveC()(_swift_initStaticObject(___swift_instantiateConcreteTypeFromMangledName(&demangling cache variabl...a for _ContiguousArrayStorage<String>), &data_1000159e8), type metadata for String)
100003ccc      _objc_msgSend(self: NSOpenPanel, cmd: "setAllowedFileTypes:", pdf)

The most interesting call here is the use of a Swift function called swift_initStaticObject() which we will dig into more since it results in the object pdf being passed to the setAllowedFileTypes: method. 

swift_initStaticObject()

We can find the function definition for this here

HeapObject *swift::swift_initStaticObject(HeapMetadata const *metadata, HeapObject *object)

 

This returns a HeapObject and takes two arguments: a pointer to metadata for the object and a pointer to the object. Switching to the disassembly helps see the arguments that are passed to the function.

100003c94  20f20810   adr     x0, demangling cache variabl...a for _ContiguousArrayStorage<String>
100003c98  1f2003d5   nop    
100003c9c  931f0094   bl      ___swift_instantiateConcreteTypeFromMangledName
100003ca0  41ea0810   adr     x1, data_1000159e8
100003ca4  1f2003d5   nop    
100003ca8  2c260094   bl      _swift_initStaticObject

The symbol demangling cache variable for type metadata for _ContiguousArrayStorage<String>: is passed to the ___swift_instantiateConcreteTypeFromMangledName which returns a pointer to metadata that is used to initialize this object. The object is in the binary at the address 0x1000159e8 and a pointer to this is passed as the second argument via the X1 register to  _swift_initStaticObject(). Let's take a look at what is at that address. 

We can see the ascii hex values for the value “pdf” at the offset of 0x1000159e8 + 0x20. The first 32 bytes of this heap object will be reserved for the metadata, count and capacity. Let’s look at this array in memory to see these properties. 

Once this Swift heap object has been initialized, it is passed to be converted to a NSArray. 

100003cb0  011c0658   ldr     x1, type metadata for String
100003cb4  78250094   bl      Array._bridgeToObjectiveC()
100003cb8  f40300aa   mov     x20, x0

The bridging functions we’ve covered accept a pointer to Swift metadata to pass information about types so the functions can handle the types appropriately. For this case, we can see that the NSArray will contain one element that is an NSString. Once the conversion is completed, the returned value is saved to X20 (which is self for Swift) and later passed as an object to the setAllowedFileTypes:, which we can see set in the screenshot below from an LLDB session when I printed out the NSOpenPanel object to see the configuration settings: 

Up until this point, this code matches the original open source project as we can see the calls to these methods. 

The first real change between the open source project and this application is in the closure passed to the beginWithCompletionHandler: that is used to handle the opening of the selected PDF file. 

beginWithCompletionHandler:

In the decompilation, we can see the use of a Block. Blocks in Objective-C are pieces of code that can be passed around like objects. Inside of this block is a call to a partial apply for a closure. Let’s dive into how this works. 

Partial application is a process of fixing a number of arguments of a function, producing another function of smaller arity. We won’t focus so much on the specifics of this term but instead focus on what it does. Let’s look at the function definition below. 

10000be18  int64_t partial apply for closure #1 in AppDelegate.Open(_:)(int64_t arg1, void* arg2 @ x20)

10000be1c      return closure #1 in AppDelegate.Open(_:)(arg1, NSOpenPanel: *(arg2 + 0x10), appDelegate: *(arg2 + 0x18)) __tailcall

We can see that it returns closure #1 in AppDelegate.Open(_:) and passes required arguments. To continue understanding how this works, we need to continue within this closure. 

closure #1 in AppDelegate.Open(_:)

This closure handles the selected PDF and the way that a developer can access the selected file from the NSOpenPanel is through the URLs property which returns an NSArray of NSURLs. This is what the writers of this application do to use the PDF the user selects. 

100003f40      id SelectedPDFArray = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: NSOpenPanel, cmd: "URLs"))
100003f50      void* selectedPDF = static Array._unconditionallyBridgeFromObjectiveC(_:)()
100003f5c int64_t arrayCount = *(selectedPDF + 0x10)

The NSArray containing the file URL of the selected PDF is then passed to the Array._unconditionallyBridgeFromObjectiveC(_:) function to convert it to a Swift Array. After this conversion, some error handling is completed to make sure there is at least one value in this array. This check is completed against the element count of the Swift array. Accessing this value in a Swift array is at the offset +0x10 from the start of the Array. The element in this array is then captured and used to check if the PDF is the correct one using a function called getProtectedPdfData(pdfUrl:).

We will next cover how the application checks if the selected PDF is the correct one. 

getProtectedPdfData(pdfUrl:)

Using a pointer to the PDF that was selected by the user, this function first creates a file handle using the File URL for the PDF passed to this function as the first and only argument. This argument will be in X0 at the start of this function and is moved to X20 which serves as self. 

10000be7c  f40300aa   mov     x20, x0  // PDF path passed to self

URL.relativePath.getter is then passed to the self object which is the File URL of the selected PDF in register X20 to get the relative path as a Swift String. 

10000bec4      int64_t PDF_Path = URL.relativePath.getter()
10000becc      int64_t PDF_path = String._bridgeToObjectiveC()()
10000bef4      id PDF_fileHandle = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: _objc_opt_self(obj: _OBJC_CLASS_$_NSFileHandle), cmd: "fileHandleForReadingAtPath:", PDF_path))

This relative path is then passed to the function String._bridgeToObjectiveC() to convert the Swift String to an NSString. The result is then passed to the function [NSFileHandle fileHandleForReadingAtPath: PDF_Path] to create a file handle. 

This file handle is then used for the call to readToEnd() and the bytes of the pdf are saved in a Data Storage object with a pointer to this saved in X1. 

We can see the size of the PDF document (0xaf7c5 = 718789 bytes) and a pointer to the bytes inside of the Data Storage object.

Using LLDB, we can see the PDF magic bytes at the start of the bytes within the Data Storage object. 

A Swift String is then initialized “This is a pdf protector” and is very important for the PDF check. 

10000c058          sizeofPDFSearchString = 0xd000000000000017
10000c058          int64_t This is a pdf protector = 0x800000010000ea70
10000c060          static String.Encoding.utf8.getter()

Swift strings are structs and within them is a struct called StringGuts that contains an object struct called StringObject which has two members: _countAndFlagsBits and _object. This is defined in this Swift file. The two values above can be used to determine which string is being initialized by matching these to their appropriate member. 

  @usableFromInline
  internal var _countAndFlagsBits: UInt64

  @usableFromInline
  internal var _object: Builtin.BridgeObject
_countAndFlagsBits = 0xd000000000000017
_object = 0x800000010000ea70

_countAndFlagsBits tells us the length of this large swift string is 0x17 bytes long. 0x800000010000ea70 is then used as the object which is identified as an ascii string due to the top bit 0x8 used as a discriminator and will require a calculation 0x10000ea70 + NativeBias which results in 32 or 0x20 for 64 bit systems. We can now evaluate what this string is by using Binary Ninja to resolve this address using the BinaryView class and the read method and passing the address + NativeBias and the length:

>>> bv.read(0x10000ea70+0x20, 0x17)
b'This is a pdf protector'

Using the bytes for the PDF and this string converted to bytes, a search is completed using the function Data.range(of:options:in:). We can see the values passed to the function below in the disassembly which uses a pointer to the PDF search bytes against the PDF bytes. 

10000c0c4  619a41a9   ldp     x1, x6, [x19, #0x18] {pdfProtector_1} {sizeOFPDFProtector}
10000c0c8  020080d2   mov     x2, #0
10000c0cc  030080d2   mov     x3, #0
10000c0d0  040080d2   mov     x4, #0
10000c0d4  25008052   mov     w5, #0x1
10000c0d8  e70318aa   mov     x7, x24  // ptr to PDF Bytes
10000c0dc  // Looks for these bytes in the PDF:
10000c0dc  // "This is a pdf protector"
10000c0dc  32040094   bl      Data.range(of:options:in:)

This function returns a range specifying the location of the found data, or nil if a match could not be found. This means that the corresponding PDF would need to have these bytes to match since the remaining behavior is based on the bytes following this found string. 

A search in VirusTotal for any other matches only shows this application, the mach-O slices and the PDF document named “Investment Opportunity - Fenbushi Capital.pdf” used in this analysis. 

If the user selects the correct corresponding PDF document, this range is then used to change the file pointer offset using the file handle with a call to seekToOffset:error:. This sets the file pointer to the end of the found “This is a pdf protector” bytes. The remaining bytes of the PDF starting from this file pointer are then read using a call to NSFileHandle.readToEnd() and then passed to the function called decryptData().

10000c124          bytesSize, EncryptedData = NSFileHandle.readToEnd()()
10000c340          void* result_1
10000c340          int64_t sizeOFDecryptedBytes
10000c340          result_1, sizeOFDecryptedBytes = decryptData(encryptedData:)(bytesSize, ptrToEncryptedBytes: EncryptedData)

We will now dive into what this function does and what it returns. 

decryptData()

The symbols for this application were not stripped so we can already tell what this function may do due to its name, but for the sake of completeness, we will walk through how it uses the encrypted bytes that were passed in via the PDF document. 

Before we cover how the bytes are converted, we need to cover the key that is used and how it is initialized. As was stated at the beginning of this document, the XOR key is what ties this back to potentially being related to DPRK activity since it matches the key used last year for similar behavior. This key is initialized with another call to _swift_initStaticObject

10000c5d0  c0a70410   adr     x0, demangling cache variabl...ta for _ContiguousArrayStorage<UInt8>
10000c5d4  1f2003d5   nop    
10000c5d8  44fdff97   bl      ___swift_instantiateConcreteTypeFromMangledName
10000c5dc  e19b0410   adr     x1, data_100015958
10000c5e0  1f2003d5   nop    
10000c5e4  dd030094   bl      _swift_initStaticObject

We already covered what this function does and what it accepts as arguments so we already know that the object itself will be at the address that was passed in to the X1 register. Let’s take a look at that object.

Here we can see the size of this object (0x64 = 100 bytes) and the key itself read backwards being:

296ce19dd63913c0b5945ed144100c99684cb4470ba0d0d675d8f3dcb65ca68ab32bd9ff8d281921cc1effbce2f3d5b3a939a6949e1924c733a77eda759ad5228daa17988dcc0cded44d4d5f4be96e201f3f7d15fc09ab338e1aa33f95aed9b3fb76bd4a

Here is what this looks like in memory after the call to initialize it. 

As stated, this same key was seen in previous DPRK malware chains and may be part of the reason that vendors started to detect this file on VirusTotal. This key is then used to decode/decrypt the encrypted bytes that were loaded from the PDF document with a call to specialized Sequence.forEach(_:). The assembly below highlights the arguments that are passed to this function. 

10000c63c  e00314aa   mov     x0, x20  // size of bytes for PDF data = 0x9b6b
10000c640  e10313aa   mov     x1, x19  // ptr to PDF bytes
10000c644  47020094   bl      outlined copy of Data?
10000c648  e2430091   add     x2, sp, #0x10 {bufferAndReturn}
10000c64c  e5230091   add     x5, sp, #0x8 {bufferAndReturn2}
10000c650  e00314aa   mov     x0, x20  // size of bytes for PDF data = 0x9b6b
10000c654  e10313aa   mov     x1, x19  // ptr to PDF bytes
10000c658  e30317aa   mov     x3, x23  // // size of bytes 0x64 = 100
10000c65c  e40316aa   mov     x4, x22  // pointer to DataStorage for bytes
10000c660  150080d2   mov     x21, #0
10000c664  29010094   bl      specialized Sequence.forEach(_:)

The size of the encrypted bytes (0x9b6b), a pointer to these encrypted bytes, the size of the key (0x64) and a pointer to the key are all passed to this function. We won’t cover the XORing specifically, however I will show how the result of this function appears in memory.

The returned Data Storage object shows the bytes have been decoded as a result of this function returning. The bytes also indicate that the bytes are for an embedded PDF within the original PDF that was opened by the PDF Viewer application. These bytes that are returned by the getProtectedPdfData() function are then used to initialize a PDF document and present it to the user. The PDF document itself does not appear to be malicious in nature and states that it is Strictly Confidential and has information related to cryptocurrency. The document contains information about projections and appears to be from a company named Fenbushi Capital, which appears to be a real company. Also, two people that are listed to be from this company are present towards the end of the PDF document. 

Conclusion

To answer the question from the title of this blog post: Is it malicious?

This is hard to answer without speculation and thus is difficult to be confident in this being malicious. However, it is suspicious and does not appear to be legitimate. Many vendors are currently attributing this PDF viewer application to DPRK and although there are similarities including the use of the same XOR key, this does not appear to download any additional malware as was observed in last year’s Rustbucket malware chain. Given the length of this 100 byte unique key it is highly unlikely for this to be coincidental. To summarize, a PDF that requires a specific adhoc signed PDF viewer to open and extract an encrypted embedded PDF using such a unique key to display to the user is interesting. 

IOC’s

Dedicated PDF Viewer.app.zip:

095184b6559bbe2e2fef999834d6905708ae254064193540d051f8c23910dfa6

 

OSX-PDF-Viewer mach-O:

6c925e2a39e8312c704575af4ad7fe75f161e73f92ffda6b9abd3663b6c789d4

 

Investment Opportunity - Fenbushi Capital.pdf:

743bd4c36afdcfaff4508fd613a4f4eee71d2e0bc5a31deb1c170d1039c953ae

 

Decrypted PDF:

37e6d18ba339b3efa5dd26e143af8bbeb8eefabbc4cfab72e6150e3bc3290b31

 

Open Source Related Project:

https://github.com/PatrickSkinner/OSX-PDF-Viewer