Skip to main content

control.rip

From strings to riches: Finding a user-assisted LPE in the wild

Recently, @pwnsdx noticed that Blizzard’s Battle.net application modifies macOS' list of trusted X.509 certificates. Many applications use this information to decide whether a website or networked service should be trusted. As a result, modifying it is generally a bad idea. While researching this behavior with @pwnsdx, I discovered a user-assisted local privilege escalation (UALPE) vulnerability in the Battle.net installer. In this post, I would like to share how I discovered this issue, and outline some of the strategies that led me to it.

Table of contents

Special thanks

Before we begin, I would like to say thank you to @pwnsdx. They got me involved in looking at this piece of software. I would not have found this bug without them including me in their research efforts, and for sharing their observations with me.

Stumbling over learning opportunities

Security research seems to have a learning curve that just gets steeper the longer you explore it. In this post, I hope to offset that by taking you through the discovery of a small, but real-world security issue. That being an escalation from a standard macOS user to root with the help of Blizzard’s software and the end user. The discovery of this issue was somewhat convoluted. While that comes with the territory, I will do my best to explain my thought process and demystify the strategies I employed.

In this post, I will discuss:

  • Using some of the basic features of radare2 to better understand a program
  • Strategies for reverse engineering a program without running it
  • Developing a basic proof of concept exploit

My hope is that others who are looking to learn about security research will have a document to reference that will, for once, leave them with more answers than questions.

Setting a research objective

Blizzard is a large video game developer that sells and distributes its computer-game variants on a digital storefront called “Battle.net”. In order to play purchased games, users must download and install an application also called Battle.net. This post will focus on the macOS application that installs the Battle.net application on a user’s computer. I will refer to this piece of software as “the Battle.net installer” or, more informally, “the installer” throughout this post.

My goal going into this was to better understand why the Battle.net application modifies macOS' trusted certificates. This is evidenced by a macOS system dialog box that asks the user to enter their credentials so that the Battle.net application can make changes to system trust settings. The Battle.net installer felt like a logical place to start static analysis. Or, in other words, disassembling the installer and searching for clues without running the installer or any of Blizzard’s applications. This portion of my research was conducted on an Ubuntu machine using radare2, an excellent open source, text-based disassembler - no Mac required!

Where to begin?

One of the most difficult challenges of reverse engineering software is finding a starting point. I try to avoid running software I do not trust - both for security research, and for personal use. In this case, I would rather start with disassembly using a static analysis tool and learn as much as possible.

Initially, I was specifically interested in the installer’s interactions with macOS' certificate trust store. To the best of my knowledge, there are at least two ways that an application can do this:

  • By using macOS' Security Framework library, most likely via the SecTrustSettingsSetTrustSettings API function 1
  • By exec’ing the security CLI tool with the add-trusted-cert subcommand 2

If I can find the code that modifies macOS' trusted certificates, I can work my way back to the logic that will (hopefully) demonstrate why this is happening. With that in mind, I started by opening the Battle.net installer executable (Battle.net-Setup) in radare:

radare2 -AAA Battle.net-Setup.app/Contents/MacOS/Battle.net-Setup

The -AAA tells radare to perform automatic analysis steps. These options map to radare shell’s a command family. The AA meaning “analyze all”, which includes symbols and entry points. Additional “A"s refer to more intricate analysis steps that can help when a binary has been stripped of symbols. If you do not specify at least -AA, you will be unable to lookup string and symbol usages.

Once radare finishes loading, it will drop you to a text prompt known as the “radare shell”. This is where radare shell commands can be entered. These commands are usually not English words, but combinations of letters. The very first letter indicates the family of commands, for instance the letter a is the “analysis” family. Additional letters following the first change the behavior of the command. For example, the second “a” in aa analyzes entry points and symbols. A full list of command families can be displayed by typing ? (question mark). Further information about a command or its family can be seen by typing the command followed by a question mark, for example: a?.

The prompt indicates that radare is ready to accept commands:

[0x100001230]>

Since I sort of know what I am looking for, I can use the i command family, which returns information about the current file opened in radare. In particular, the izz command returns a list of all the strings found in the installer. Since radare’s shell behaves similarly to a standard sh or bash shell, these can be filtered by piping them through grep with search criteria. It is worth noting that this is not the only (or best) method to find library symbols. In fact, the f (flag) command has a higher likelihood of identifying imported library functions. More on that command later in the post.

My thought process was: if the installer is using any of the previously mentioned strategies, then a cursory search of its strings should reveal their usage. This is not always true, but it is what I did nonetheless:

[0x100001230]> izz | grep add-trusted-cert
# Returned nothing.
[0x100001230]> izz | grep SecTrustSettingsSetTrustSettings
# Returned nothing.

Not off to a good start! Neither of my searches returned any results. Since there are dozens of Security Framework functions, I can try searching for other function names. Looking at the Apple Developer documentation, it seems like most (if not all) of the function names are prefixed with Sec. Searching for Sec gives us the following:

[0x100001230]> izz | grep Sec
# Note: Several lines ommitted for brevity.
# Note: The '(...)' is data I omitted for readability.
10251 0x0055373b  0x10055373b (...) ascii   InitializeSectorTable
10257 0x0055380e  0x10055380e (...) ascii   ProcessSectors
10521 0x0055529a  0x10055529a (...) ascii   Section_corrupted
10561 0x00555864  0x100555864 (...) ascii   ValidateSectorTable
10598 0x00555b44  0x100555b44 (...) ascii   /System/Library/Frameworks/Security.framework/Versions/Current/Security
10599 0x00555b8c  0x100555b8c (...) ascii   SecCertificateCopyValues

Quite a few results this time. Each line represents a single string found by izz that contains Sec. Let’s break down the contents of the result for SecCertificateCopyValues:

# [ID]  [physical address] [virtual addr.] [type] [string value]
  10599 0x00555b8c         0x100555b8c     ascii  SecCertificateCopyValues

Since we are likely looking for a macOS Security Framework function, the SecCertificateCopyValues is intriguing. The Apple Developer documentation states: “Creates a dictionary that represents a certificate’s contents.” 3

While not exactly what we are looking for, it could be applicable. Perhaps the installer is generating a certificate, or copying an existing one and using the function to parse it?

We can find where this string is referenced in the installer logic by using the a (analysis) family of commands. The axt command can lookup references to the string’s virtual address:

[0x100001230]> axt 0x100555b8c
sym.func.10035cf18 0x10035d16b [DATA] lea rsi, qword str.SecCertificateCopyValues

Similar to izz, each line represents a single reference found by axt. Here, it found only one reference to this string. The important parts of the axt output are the first two space-separated strings. The first value (sym.func.10035cf18) is the ID of the referencing function as assigned by radare. This string is a common pattern used by radare, where sym means “symbol” and func means “function”.

The second value (0x10035d16b) is the exact location within the function where the string is referenced. This address gives us a potentially relevant location to start analyzing the installer’s logic.

Utilizing radare’s visual graph mode

Now that we have somewhere to start exploring the installer’s logic, we can use radare’s visual graph mode. Either of the two values we found with axt can be used with the s (seek) command to set the default memory address in radare. In this case, we will seek to the exact location within the function that references SecCertificateCopyValues:

[0x100001230]> s 0x10035d16b
[0x10035d16b]>

Many of radare’s commands automatically use the currently selected address as the default input. This should decrease the amount of typing we need to do. Also, it will make finding our way back to this logic easier. Now, we can enter visual graph mode - not to be confused with visual mode - with VV (two capital v’s):

[0x10035d16b]> VV

… at which point you will see the following:

Visual graph mode. Snippet from sym.func.10035cf18 [1/3].

This mode visually represents the logic flow of a program or library using ASCII art “blocks” filled with CPU instructions. The individual blocks in this graph are known as “basic blocks”. Each line of text in a block is either a comment added by radare, or a single CPU instruction. CPU instructions which are generally referred to as “assembly”, or “asm”. The instructions are executed in the order they appear: from the top of the block, to the bottom.

Basic blocks (BBs) are logical groups of instructions that act like nodes in a graph. They can branch to other blocks, or even themselves. This is visually represented with lines that connect BBs. When a BB is finished, it can optionally pass execution by evaluating a condition. In such a case, the different branches are displayed with t (true) and f (false). Think of this as a different representation of source code. The big difference is this code is interpreted by the CPU - with some helpful decoration by radare of course.

The advantage of visual graph mode and BBs are that they help build a high level understanding of logic without requiring us to read through every last CPU instruction. It is a great tool for quickly building an understanding of a piece of software’s internal workings.

A detour into assembly and radare’s syntax

While I do not intend to make this post about assembly, I would like to spend some time going over the basics, including how radare displays this information in BBs. If you eat CPU instructions for breakfast, you may want to skip this section. Otherwise, let’s talk about some assembly basics.

A basic block is read from top to bottom - as that is the order the CPU instructions are executed. Before we go too far, realize that there are different syntax representations for assembly. 4 This a subtle detail that can completely change the meaning of what you are reading. For example, in Intel syntax, an instruction’s parameters are listed by destination followed by the source. Conversely, the AT&T syntax reverses the parameters. As of radare 4.2.x, the default syntax is Intel.

With that out of the way, let’s go through the contents of the BB at our selected address line-by-line. This block in its entirety is pictured in the middle of the previous screenshot:

1
2
3
4
5
[0x10035d16b]
; 0x100555b8c
; "SecCertificateCopyValues"
lea rsi, qword str.SecCertificateCopyValues
mov rdi, rax

The first line is a comment added by radare that indicates the address of the BB surrounded in brackets. This is a bit confusing because, despite being a comment, it is not prefixed with a comment character.

Lines 2-3 are also comments added by radare, this time indicated with semicolons (;). You may recognize the first, which is the virtual address of the string that we found with izz. The second comment is the value found at that address. These comments add context to the str.SecCertificateCopyValues symbol usage on the following line.

On line 4, the installer lea’s the function’s symbol string into the rsi CPU register. On 64-bit x86 CPUs, this register holds the second argument to be provided to a callee function. 5

It then mov’s (copies) the value in the rax register into rdi on line 5. It is important to understand that mov is a copy operation. Despite its name, mov is not a move; the value in the source register remains the same after the mov instruction is executed.

Presumably, this is to save the value in rax before it is overwritten by the next line:

6
call sym.imp.dlsym ; [oBc]

Here, radare is indicating a call to an imported function named dlsym. Let’s walk through the string to the right of call. The sym means “symbol”, imp means “import”, and the final string is the name of the function. The dlsym function looks up the address of a symbol in a library. 6 In this case, it is looking up the SecCertificateCopyValues function. Basically, this code is preparing to call an external library function. The semicolon is another comment added by radare. The string oBc in brackets is a key sequence that, when typed, will take you to the function’s BB(s).

After executing dlsym, the installer checks the function call result:

7
8
9
mov r15, rax
test r15, r15
je 0x10035d282

This is done by mov‘ing the value from rax into the r15 register. The rax CPU register always holds the return value of a function call, and is owned by the callee function. 5 After the mov, the r15 register contains the same value.

In the final two lines of the BB, the value in r15 is tested against itself. This is effectively a bitwise AND, and is really a test for the value being equal to NULL (zero). The documentation for dlsym states that NULL is returned if the specified symbol could not be found. 6

The trick here is that test changes the FLAGS CPU register. If the bitwise AND is zero, then the zero flag (ZF) bit in FLAGS is set. 7 This would occur if dlsym failed and returned NULL. The final instruction in the BB is a jump if equal (je) instruction. Call flow is “jumped” to the specified address when the zero flag is set. So, if the call failed, execution is jumped to 0x10035d282. In this case, the code would proceed on the false branch if the function call succeeded. If the call failed, then the true branch would be followed.

The most confusing part about this in my opinion is the interaction between test and je. This hinges on the zero flag in the FLAGS register. However, the FLAGS register is not shown here despite being critical to the outcome of this logic. This is easy to miss. As a result, I thought it was worth pointing out here :)

Custom macOS code signing validation

Our objective here is to get a high level understanding of this code. That means we will not be reading every line of assembly. We will start by using visual graph mode to get a feeling for the current function’s logic. I am hoping other string references and imported function calls, like dlsym, will quickly tell a story about this code’s purpose. What does that mean mechanically? Well, doom scrolling upwards!

The “h/j/k/l” keys (like vi) or the arrow keys (if you are a heretic like me) can be used to move around the graph. All am doing here is working my way backwards from the current BB to the start of the function. This can be chaotic because of the many potential branching paths. Personally, I like to follow one branch and then scroll left or right to compare the two. This is only viable if the BBs are a handful of lines long of course.

During this process, I noticed quite a few references to the macOS Security Framework via call sym.imp.Sec(...). These two BBs in particular are a bit confounding:

Second snippet from sym.func.10035cf18 [2/3].

  • SecTrustEvaluate - “Evaluates trust for the specified certificate and policies.” 8
  • SecTrustGetResult - “Retrieves details on the outcome of a call to the function SecTrustEvaluate.” 9

I expected to see calls related to generating a certificate… But that is still not the case. Only three jumps above this, we can see evidence of what looks like… loading code signing information?

Third snippet from sym.func.10035cf18 [3/3].

  • SecStaticCodeCreateWithPath - “Creates a static code object representing the code at a specified file system path.” 10
  • SecCodeCopySigningInformation - “Retrieves various pieces of information from a code signature.” 11

We can be reasonably confident at this point that the installer is evaluating the code signing information of a file, including whether that file’s signer is trusted.

Let’s take a look at code that utilizes the current function we are examining. After all, it is entirely possible nothing calls this function. There are several ways to do this. Since we are in visual graph mode, we can type colon (:) like vi, and then enter a radare shell command. In this case, the command afi. will print out the current function’s address or symbol. We can then use axt to lookup references to the function:

:> afi.
sym.func.10035cf18
:> axt sym.func.10035cf18
sym.func.10035cd9b 0x10035cdd0 [CALL] call sym.func.10035cf18

Good news! It is referenced, and in only one place. That should make our job a little easier. Let’s seek to the exact address where the current function is referenced:

:> s 0x10035cdd0
# Note: The command menu can be closed with the enter key.

This time around, I do not need to doom scroll very far for clues. About eleven lines down in the subsequent BB (pictured bottom left), it appears to be case insensitive (ouch!) comparing the value stored in var_210h to the string Blizzard Entertainment, Inc.:

Snippet from sym.func.10035cd9b.

What the heck is var_210h you may ask? It is a local variable that was automatically named by radare. Look, it means well, OK? The variable’s value, most likely a pointer, was loaded into the rsi register prior to calling the function we just examined. In essence, one of the function’s arguments is a pointer, which is probably set to the organization name field found in the code signing certificate. This can be seen on the third line of the highlighted BB (the one in the middle of the screenshot) with lea rsi, qword [var210h]. This allows the callee function to update a variable value held by the caller - in this case var_210h.

As we delve deeper into this logic, we can rename functions to make things a bit more sane. This is done with afn, which takes a new name and an optional symbol. If you do not specify a symbol, then the current function will be renamed. Let’s start with renaming the first function we examined:

:> afn checkCodeSigningAndGetOrgName sym.func.10035cf18
# Note: You may need to reload the current view for this change to take effect.

On that note, this function’s first BB executes another function (call sym.func.10035ce26, seen in the top right of the previous screenshot). Looking at that function by typing oa, we can see more calls to code signing APIs:

Snippet from sym.func.10035ce26.

This function does not branch out to other installer functions, or do anything else of note. While we are here, let’s rename this function as well:

:> afn checkCodeSigningInfo sym.func.10035ce26

So far, we can be pretty sure that this code path (sym.func.10035cd9b) is trying to ascertain the code signing status of a file and whether or not macOS trusts that file’s signer.

Before we move on, let’s also rename this function. Unfortunately, all three of these functions can be described in a similar manner. This function could be described as a “wrapper”, since it calls out to the other two custom code signing functions:

:> afn checkCodeSigningWrapper sym.func.10035cd9b

Why implement code signing checks?

That begs the question: why would Blizzard care about this? In theory, macOS should be validating code signing information automatically. The answer to that question likely lies in the logic that calls checkCodeSigningWrapper. Let’s look up references to it:

:> axt checkCodeSigningWrapper
sym.func.100396d14 0x100396d48 [CALL] call checkCodeSigningWrapper
sym.func.1003a616a 0x1003a61a2 [CALL] call checkCodeSigningWrapper
sym.func.100419bd0 0x100419c2b [CALL] call checkCodeSigningWrapper

Three different code paths will be a quite a bit more work, but we can do it! Starting with the first reference, let’s s 0x100396d48 and see what’s what. Scrolling down two jumps, I noticed this:

Snippet from sym.func.100396d14 [1/2].

There are a few important details here:

  • Confirmation that the code we reviewed so far is specifically for checking code signing, manually implemented by Blizzard (Blizzard code check failed on ')
  • Debugging / log information might be generated, which could be useful research tools in both static and dynamic analysis. Those strings are most likely written somewhere, like a file or stderr
  • Evidence of execution of external programs. For example: . not launching. Error= string, coupled with all the code signing logic we have seen thus far

We could dig into the function calls here - there are several more not shown in the screenshot. But that feels unnecessary to me. If the code is this verbose, then perhaps the next BBs will be too. Sure enough, a few jumps before the end of this function, I found this:

Another snippet from sym.func.100396d14 [2/2].

I am not sure what a “bootstrapper” is, or why it is being “respawned”, but that seems to be the purpose of this function. Again, there are plenty of branches to investigate in this function alone. But, let’s try to stay focused on researching the remaining two functions that call checkCodeSigningWrapper. Before proceeding, let’s rename this function:

:> afn respawnBootstrapper sym.func.100396d14
:> s 0x1003a61a2

This function’s early error handling is quite similar to the respawnBootstrapper function, even referencing the same strings. Unfortunately, it is much more complicated, not only in branching function calls, but overall number of BBs. Here is the snippet of interest:

Snippet from sym.func.1003a616a.

So, what is being “launched” here? It is unclear, and I would rather spend my time analyzing the third function. Let’s rename this function and move on to the third function:

:> afn launchSomething sym.func.1003a616a
:> s 0x100419c2b

Just like before, this code appears to be “launching” something, this time an “Agent”:

Snippet from sym.func.100419bd0.

Let’s rename this function and review what we have learned so far:

:> afn launchAgent sym.func.100419bd0

The curious case of macOS system dialog boxes

There is now strong evidence of three separate code paths in the installer that validate the code signing information of files on disk, and then execute them. While I cannot point out where the code execution logic is (i.e., the exec system call), that feels less important at this point. Before starting this research, I asked @pwnsdx how many times the installation process asked for the user’s credentials. @pwnsdx observed this happening several times, including once for making changes to system trust store settings. This was clearly stated in the macOS system dialog box.

In parallel with my installer static analysis research, @pwnsdx discovered that the Battle.net application itself appeared to be generating and installing a certificate. My own research at the time corroborated this. I could not find any evidence of macOS trust store modification logic in the installer.

If that is the case… what else is the installer utilizing the user’s credentials for? Could it be related to the logic we have reviewed thus far? Running the installer might offer more clues about the code signing and “launching” code. Plus, it will give me more stuff to disassemble :)

During my installer static analysis research, I decided it was a good idea to multitask, and try to get a macOS Catalina VM running. That was going pretty terribly. Some (most) of it was Apple’s fault, but a good chunk of it belonged to me splitting my attention and trying to shove too much garbage into a 256 gb SSD. Why did no one tell me this was a terrible idea!? While I slowly worked through my macOS VM issues, I decided to continue my installer static analysis.

Why, and how, is the installer asking the user for their credentials?

The missing piece of the puzzle

If it was not already clear, I am not a macOS developer. I have some familiarity with macOS… But I am heavily relying on Google and Apple’s developer documentation. This time my Googling led me to the Authorization Services APIs in the Security Framework. Its documentation states: “Access restricted areas of the operating system, and control access to particular features of your macOS app.” 12

To be honest, I am not sure how I originally found references to this API in the installer. It may have been while doom scrolling the respawnBootstrapper function. In the interest of brevity, and showing you a better method, I will demonstrate how the f command can be applied here. This command manages “flags”, or interesting data, found by radare. There are several categories (also referred to as “namespaces”) of flags that can be broken down into the following:

  • classes
  • functions
  • imports
  • relocs
  • sections
  • segments
  • strings
  • symbols

By default, the f command returns results from all categories. This can be seen with the fss command, which returns the currently selected category:

[0x100396d14]> fss
0  * (selected)

Like the Sec functions, this API’s functions are prefixed with the string Authorization. Since we are likely looking for an imported function we need to switch our selected category using fs. From there, we can search for imported functions prefixed with Authorization:

[0x100396d14]> fs imports
[0x100396d14]> f | grep Authorization
0x10041e7b4 6 sym.imp.AuthorizationCopyRights
0x10041e7ba 6 sym.imp.AuthorizationCreate
0x10041e7c0 6 sym.imp.AuthorizationExecuteWithPrivileges
0x10041e7c6 6 sym.imp.AuthorizationFree

While this is a short list, we can deprioritize sym.imp.AuthorizationFree and sym.imp.AuthorizationCopyRights, as their function names imply freeing data, and copying existing data. The remaining two are intriguing not only because of their API documentation, but because I recognize one of the function names from an article by Patrick Wardle, which I found while researching the Authorization Services API. Wardle described AuthorizationExecuteWithPrivileges as follows:

“One of the insecure APIs that I discussed was the widely used AuthorizationExecuteWithPrivileges function. In a nutshell, this API takes a path to a binary (in the pathToTool argument) that will be executed with elevated privileges, once the user has authenticated[.]” 13

Now that is a red flag if I have ever seen one! Remember, this application is just that - a custom application. It does not use, or follow Apple’s Installer framework. So, one would assume it does not require administrative privileges. In addition to @pwnsdx observations, this is evidence to the contrary. Let’s take a look at what uses AuthorizationExecuteWithPrivileges, and rename the function(s):

[0x100419c2b]> axt 0x10041e7c0
sym.func.10035eb2e 0x10035f2fb [CALL] call sym.imp.AuthorizationExecuteWithPrivileges
[0x100419c2b]> afn authExecWithPrivs sym.func.10035eb2e

I wonder what uses that function…

[0x100419c2b]> axt authExecWithPrivs
sym.func.10034e81e 0x10034ebb8 [CALL] call authExecWithPrivs
sym.func.10035f425 0x10035f47c [CALL] call authExecWithPrivs
sym.func.10035f4d4 0x10035f67e [CALL] call authExecWithPrivs
sym.func.10035f707 0x10035f8a2 [CALL] call authExecWithPrivs
respawnBootstrapper 0x1003970d0 [CALL] call authExecWithPrivs
launchSomething 0x1003a6593 [CALL] call authExecWithPrivs
sym.func.1003e84a0 0x1003e88c5 [CALL] call authExecWithPrivs
sym.func.100419bd0 0x10041a158 [CALL] call authExecWithPrivs

Look at that! Two of the functions we reviewed earlier reference this code (respawnBootstrapper and launchSomething). While this is not a 100% confirmation that the installer actually runs this particular Apple function, it is strong evidence that it might. If the installer is using this function, then it may be vulnerable to running an attacker controlled executable. Oddly, it appears Blizzard may have attempted to mitigate this by manually checking code signing information before executing the target executable. I cannot say that for certain, but that appears to be the logic:

|-- respawnBootstrapper() / launchSomething()
    |-- checkCodeSigningWrapper()
    |-- authExecWithPrivs()

Another consequence of using this function is, as Wardle pointed out: “[the function will] be executed with elevated privileges, once the user has authenticated[.]” That means between the time the function is called, and when the user enters their credentials and clicks OK, a malicious program could replace the target executable that the Battle.net installer “verified”. That is huge time window to win a race condition in.

Around this time, I finally figured out my macOS VM issues. So, let’s jump into some dynamic analysis!

Searching for a log file

With a macOS Catalina VM up and running, we can start poking at a running instance of the Battle.net installer. My first objective is to figure out what file descriptors it is creating. If you recall from earlier, there were hints of log messages in the code. A log file might help make more sense of the installer’s inner workings. At the very least, it would give me more information to look for in static analysis.

Like anything, there are several ways to accomplish this. My personal favorite is to use lsof, which simply lists the open file descriptors of one or more programs. It also comes pre-installed on macOS, which saves us from wasting time trying to copy another tool onto the test system. The first thing we need to do is run the installer, followed by getting the process ID via ps or the Activity Monitor. With the process ID in hand, we can tell lsof to look at the installer, and search the results for possible log files:

admin@MacBook-Pro Desktop % lsof -p 687 | grep -i log
Battle.ne 687 admin  txt      REG    1,4    41696 1152921500312182936 /System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport
Battle.ne 687 admin  txt      REG    1,4    28512         12884940279 /Library/Preferences/Logging/.plist-cache.OQ3JHzDk
Battle.ne 687 admin  txt      REG    1,4   200368 1152921500312182960 /System/Library/PrivateFrameworks/login.framework/Versions/A/login
Battle.ne 687 admin    8w     REG    1,4     1191         12884942054 /Users/Shared/Battle.net/Setup/bna_2/Logs/battle.net-setup-20200918T230003.log

Sure enough, it looks like the installer is using /Users/Shared/Battle.net/ for file storage - including a log file! The files and directories stored there have file mode 0777 set for some reason. That means anyone on the system can read or modify data stored within:

admin@qs-MacBook-Pro ~ % cd /Users/Shared
admin@qs-MacBook-Pro Shared % ls -ltr
total 0
drwxrwxrwx@ 2 _fpsd  wheel  64 Aug 30 04:50 adi
drwxrwxrwx  3 admin  wheel  96 Sep 21 21:51 Battle.net
admin@qs-MacBook-Pro Shared % ls -ltr Battle.net/Setup/bna_2
total 0
drwxrwxrwx  4 admin  wheel  128 Sep 23 11:40 Logs

I decided to follow the log file while clicking through the installer with tail -f. Sure enough, the following string was written when the installer asked for my credentials:

I 2020-09-18 23:11:16.024508 [Main] {0x700007486000} Respawning bootstrapper: path=/Users/admin/Desktop/Battle.net-Setup.app elevate=1 arguments={[0]=--cmdver=2, [1]=--elevated, [2]=--locale=enUS, [3]=--mode=setup, [4]=--session=162091081290555640}

Huh… Does that mean the installer is the “bootstrapper” we saw earlier? After entering my credentials, the installer appeared to close and open again. Looking up the installer’s process with ps, we can see it is now running as root with a different PID:

admin@MacBook-Pro Desktop % ps auxw | grep '[B]attle.net'
root              860   1.4  3.0  4661088  63188   ??  S     7:20PM   0:04.70 /Users/admin/Desktop/Battle.net-Setup.app/Contents/MacOS/Battle.net-Setup # (...)

While I expected this, I was still surprised to see it happen before my eyes.

The vulnerability and its impact

A traditional local privilege escalation is a very niche vulnerability class that is used to elevate the privileges of an existing piece of attacker-controlled software. Such an attack is viable only if an attacker already has a presence on the victim’s computer - hence the term “local”. From there an attacker can use a LPE to increase their capability to do evil things. In our case, that will be going from a standard macOS user to the root user: the most powerful user on a Unix system.

In Wardle’s “Death by 1000 Installers; it’s all Broken!” presentation, he discussed the unfortunate usage of AuthorizationExecuteWithPrivileges by several installers. Therein, he referred to this vulnerability class as “user-assisted privilege escalation”. 14 While I would personally include “local” in the term, the key difference between this bug and traditional LPEs is that this issue requires user interaction. That interaction being the user entering their credentials and clicking OK on a macOS dialog box.

Confirming our suspicions

My initial attempt at testing this was to manually replace the Battle.net-Setup binary when the macOS system dialog box appeared with a bash script that wrote the name of the current user to a log file. This was unsuccessful, as the script wrote admin (the normal macOS user) to the file. I attempted to research this behavior, and the best answer I found was most Unix systems do not permit the ‘setuid’ bit for shell scripts. 15

As you may know, I am a big fan of Go. So I decided to write a tiny Go program that wrote out the current user’s UID, EUID, GID, and EGID. This indicated that the EUID was 0. In other words, while the process’ owning user is the normal user, it is effectively root (user ID 0). We can set our process' UID to 0 by invoking the setuid system call. 16 In Go, this is accessible using the Setuid function in Go’s syscall library.

This is a privileged operation that ordinary users cannot carry out. The AuthorizationExecuteWithPrivileges documentation is a bit light on how this is implemented. Regardless, the call to setuid in Go works! That confirms my theory - now we can write a proof of concept exploit.

Proof of concept exploit

This particular vulnerability is interesting because the installer attempts to verify the target executable before running it. Such a mitigation only tightens the race condition we need to win. Because the Apple API function only accepts a file path as an input, it could never possibly verify that the target executable is the same binary as the caller had hoped to invoke. 17

This still leaves us with the challenge of identifying when the Apple function is called. If we replace the target binary before the function is invoked, Blizzard’s mitigation will kick in. If we replace it after the user has clicked OK on the macOS system dialog box, then our exploit will not run. What we need is an oracle that will signal when the code signing verification is complete. If you recall, we already have one! A log message is written when the code signing verification is complete, and the AuthorizationExecuteWithPrivileges function is called:

I 2020-09-18 23:11:16.024508 [Main] {0x700007486000} Respawning bootstrapper: path=/Users/admin/Desktop/Battle.net-Setup.app (...)

Not only can this string act as an oracle, but we can also extract the file path to the Battle.net-Setup.app and the installer binary. No need to query macOS for running processes!

Here is what I am thinking for a logic flow:

                    ┌──────────────────────────────────┐
                    │ Was the application started with │
                    │ a Blizzard installer argument?   │
                    └──────────────────────────────────┘
                           t                  f
                           │                  │
  ┌────────────────────────────┐          ┌───────────────────────────────┐
  │ If so, attempt to setuid 0 │          │ If not, then read the current │
  └────────────────────────────┘          │ binary into memory            │
                │                         └───────────────────────────────┘
  ┌───────────────────────────┐                          |
  │ Connect to hardcoded Unix │          ┌─────────────────────────────────┐
  │ socket path               │          │ Find all existing log files and │
  └───────────────────────────┘          │ wait for a new one to appear    │
                │                        └─────────────────────────────────┘
┌────────────────────────────────┐                       |
│ Start interactive bash shell,  │        ┌───────────────────────────────┐
│ and hook it up the Unix socket │        │ Wait for the oracle string to │
└────────────────────────────────┘        │ be written to the log file    │
                                          └───────────────────────────────┘
                                                         |
                                         ┌──────────────────────────────────┐
                                         │ Extract the Battle.net-setup     │
                                         │ binary file path from the string │
                                         └──────────────────────────────────┘
                                                         |
                                         ┌─────────────────────────────────┐
                                         │ Truncate the Battle.net-setup   │
                                         │ file contents and write the     │
                                         │ proof of concept binary into it │
                                         └─────────────────────────────────┘
                                                         |
                                          ┌───────────────────────────────┐
                                          │ Start a Unix socket listener, │
                                          │ and wait for a connection     │
                                          └───────────────────────────────┘
                                                         |
                                           ┌────────────────────────────┐
                                           │ Upon connection, hook the  │
                                           │ std file descriptors up to │
                                           │ the Unix socket            │
                                           └────────────────────────────┘

After writing some terrible Go code, I produced this:

The code for this proof of concept exploit can be found here.

Remediation suggestions

Before I offer any suggestions, I should be clear that I do not have a clear understanding of the Battle.net installer’s inner workings, or Blizzard’s requirements. However, most of the features that I imagine Blizzard thinks it needs are available without elevating privileges beyond a standard macOS user.

Starting with the original issue that got us here, modifying what a TLS library trusts can be changed at the library level, such as in OpenSSL and Go. 18 19 The private key for the CA that generates the end-entity certificates does not need to be written to disk, or saved at all. The CA’s private key can be created in memory and then discarded once its job is complete. The CA certificate’s X.509 data can be saved to disk - it is not a secret. If Blizzard is concerned about it being silently modified by an attacker, it can be saved to the user’s macOS keychain.

Automating the startup of an application can be implemented without root. I believe there are a few methods to accomplish this. However, they may all tie into launchd, specifically Launch Agents. These differ from Launch Daemons, which require administrative privileges to create and modify. 20

Do not use the AuthorizationExecuteWithPrivileges function. It is both deprecated and insecure. Principle of least privilege should be practiced where possible. If running as root is required, then investigate using SMJobBless in the Service Management framework, as recommended by Wardle. 14 21

Avoid using /Users/Shared for storing data. Furthermore, do not use 0777 file mode - either in that directory, or anywhere else on the file system.

Disclosure

Blizzard does not appear to provide a mechanism for reporting security issues, or document a vulnerability disclosure process. The company’s support website recommends reporting “software glitches” (whatever those are) and “bugs” to their “Battle.net Report Forum”. 22 It does not make much sense to report this on a public forum that Blizzard has full control over. Furthermore, Blizzard’s handling of previous security issues does not inspire confidence. 23 I would rather report this publicly on my own website.

Conclusion

This particular security issue provided a unique opportunity to examine the nuances of bug hunting. The issue itself is not particularly exciting or severe. However, it does cast some suspicion on Blizzard’s Battle.net software, at least on macOS. I still do not understand why it mucks with macOS' trusted certificates. On top of that, seeing the installer go off the rails so quickly makes me question what else might be lurking in the installer alone.

The lack of a clear security issue reporting mechanism is also frustrating, and frankly inexcusable for a massive company like Blizzard.

Regardless - I hope that you enjoyed reading this, and learned something in the process. Good night, and good luck.

Updates and corrections

  • May 12, 2022 - Move sections around to be consistent with new post structure. Also updated references to use markdown footnotes
  • September 26, 2020 - Fixed various typos

Appendix

  • Battle.net-Setup.app, version: 1.16.3.2988
Battle.net-Setup.zip
    - SHA256: a727d7e977cb54eb3dc3709a5ad002de64e2d90f6074d4e6c34a502424640269
Battle.net-Setup.app/Contents/MacOS/Battle.net-Setup
    - SHA256: 5e2a00f75c7b9dc428f9858a6f789a7749fdbced3b1a7c95ee5b0aa226af6212

References


  1. developer.apple.com. (n.d.). “SecTrustSettingsSetTrustSettings”. ↩︎

  2. ss64.com. (n.d.). “security cert”. ↩︎

  3. developer.apple.com. (n.d.). “SecCertificateCopyValues”. ↩︎

  4. wikipedia.org. (2020, September 5). “x86 assembly language”. ↩︎

  5. web.stanford.edu. (n.d.). “Guide to x86-64”. ↩︎

  6. pubs.opengroup.org. (n.d.). “dlsym - obtain the address of a symbol from a dlopen object”. ↩︎

  7. wikipedia.org. (2020, August 13). “FLAGS register”. ↩︎

  8. developer.apple.com. (n.d.). “SecTrustEvaluate”. ↩︎

  9. developer.apple.com. (n.d.). “SecTrustGetResult”. ↩︎

  10. developer.apple.com. (n.d.). “SecStaticCodeCreateWithPath”. ↩︎

  11. developer.apple.com. (n.d.). “SecCodeCopySigningInformation”. ↩︎

  12. developer.apple.com. (n.d.). “Authorization Services”. ↩︎

  13. Wardle, P.. (2020, March 16). “Sniffing Authentication References on macOS”. ↩︎

  14. Wardle, P.. (2017, July 28). “[DefCon 2017] Death by 1000 Installers; it’s All Broken!”. ↩︎

  15. Gilles ‘SO- stop being evil’. (2018, November 30). “Allow setuid on shell scripts”. ↩︎

  16. pubs.opengroup.org. (n.d.). “setuid - set user ID”. ↩︎

  17. developer.apple.com. (n.d.). “AuthorizationExecuteWithPrivileges”. ↩︎

  18. openssl.org. (n.d.). “SSL_CTX_load_verify_locations”. ↩︎

  19. golang.org. (n.d.). “type CertPool”. ↩︎

  20. developer.apple.com. (2016, September 13). “Creating Launch Daemons and Agents”. ↩︎

  21. developer.apple.com. (n.d.). “SMJobBless”. ↩︎

  22. us.battle.net. (2020). “Reporting a Bug”. ↩︎

  23. Ormandy, T.. (2018, January 24). “Issue 1471: blizzard: agent rpc auth mechanism vulnerable to dns rebinding”. ↩︎