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 theadd-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:
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:
|
|
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:
|
|
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:
|
|
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 test
ed 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:
SecTrustEvaluate
- “Evaluates trust for the specified certificate and policies.” 8SecTrustGetResult
- “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?
SecStaticCodeCreateWithPath
- “Creates a static code object representing the code at a specified file system path.” 10SecCodeCopySigningInformation
- “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.
:
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:
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:
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:
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:
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”:
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
-
developer.apple.com. (n.d.). “SecTrustSettingsSetTrustSettings”. ↩︎
-
developer.apple.com. (n.d.). “SecCertificateCopyValues”. ↩︎
-
wikipedia.org. (2020, September 5). “x86 assembly language”. ↩︎
-
web.stanford.edu. (n.d.). “Guide to x86-64”. ↩︎
-
pubs.opengroup.org. (n.d.). “dlsym - obtain the address of a symbol from a dlopen object”. ↩︎
-
wikipedia.org. (2020, August 13). “FLAGS register”. ↩︎
-
developer.apple.com. (n.d.). “SecTrustEvaluate”. ↩︎
-
developer.apple.com. (n.d.). “SecTrustGetResult”. ↩︎
-
developer.apple.com. (n.d.). “SecStaticCodeCreateWithPath”. ↩︎
-
developer.apple.com. (n.d.). “SecCodeCopySigningInformation”. ↩︎
-
developer.apple.com. (n.d.). “Authorization Services”. ↩︎
-
Wardle, P.. (2020, March 16). “Sniffing Authentication References on macOS”. ↩︎
-
Wardle, P.. (2017, July 28). “[DefCon 2017] Death by 1000 Installers; it’s All Broken!”. ↩︎
-
Gilles ‘SO- stop being evil’. (2018, November 30). “Allow setuid on shell scripts”. ↩︎
-
pubs.opengroup.org. (n.d.). “setuid - set user ID”. ↩︎
-
developer.apple.com. (n.d.). “AuthorizationExecuteWithPrivileges”. ↩︎
-
openssl.org. (n.d.). “SSL_CTX_load_verify_locations”. ↩︎
-
golang.org. (n.d.). “type CertPool”. ↩︎
-
developer.apple.com. (2016, September 13). “Creating Launch Daemons and Agents”. ↩︎
-
developer.apple.com. (n.d.). “SMJobBless”. ↩︎
-
us.battle.net. (2020). “Reporting a Bug”. ↩︎
-
Ormandy, T.. (2018, January 24). “Issue 1471: blizzard: agent rpc auth mechanism vulnerable to dns rebinding”. ↩︎