Closed Bug 1562756 Opened 5 years ago Closed 2 years ago

Code Injection in Firefox macOS desktop client

Categories

(Core :: Security: Process Sandboxing, defect, P2)

defect

Tracking

()

RESOLVED FIXED
103 Branch
Tracking Status
firefox-esr102 --- fixed
firefox103 --- fixed

People

(Reporter: wojciechregula, Assigned: haik)

References

Details

(Keywords: csectype-priv-escalation, sec-want, Whiteboard: [adv-main103-])

Attachments

(2 files)

Attached image FirefoxPoC.png

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36

Steps to reproduce:

Vulnerability description

I've identified a code injection vulnerability in your macOS desktop client. Any malicious application, running with standard user permissions is able to exploit this vulnerability and execute code in your application's context.

Requirements

In order to exploit this vulnerability, a victim has to have a malicious application installed on the device.

Proof of Concept

To show you the impact I've prepared a proof of concept where malicious application without root permissions opens a calculator from Firefox's context.

1. Create a new dylib with following contents:

#include <Foundation/Foundation.h>

__attribute__((constructor)) static void pwn() {
       
   NSTask *task = [[NSTask alloc] init];
    task.launchPath = @"/Applications/Calculator.app/Contents/MacOS/Calculator";
    [task launch];
    
}

2. Compile it using gcc

gcc -dynamiclib malicious.m -o malicious.dylib -compatibility_version 10.10.10 -lobjc -framework Foundation

3. Inject to the Firefox binary

DYLD_INSERT_LIBRARIES=./malicious.dylib /Applications/Firefox.app/Contents/MacOS/firefox

4. Calculators should be run in Firefox's context (as I shown on the attached screenshot)

Actual results:

Impact

In the proof of concept I showed that any malicious application is able to execute code in Firefox's context. An attacker is able to steal browser cookies, sniff the traffic or perform actions on websites.

Expected results:

Recommendations

Assuming that the desktop client has been compiled using XCode, a developer needs to turn on "Hardened Runtime" capability making sure that Allow DYLD Environment Variables option is turned off. Another way to disallow the DYLD Environement variables is adding a _RESTRICTED segment to the application binary (this technique is used for example by Google Chrome).

References

Apple Docs - Hardened runtime entitlements
https://developer.apple.com/documentation/security/hardened_runtime_entitlements

Important notes

  • Physical access is not required to exploit this vulnerability.
  • Applications do not need root permission to open other applications with DYLD_INSERT_LIBRARIES environment variable - execve documentation.
Flags: sec-bounty?

Haik, from your newsgroup post about notarized builds, I'm assuming this is covered by the notarized builds work and is fixed in 69? Is that right, and if so, is there a more minimal fix we could apply to 68/esr ?

Group: firefox-core-security → dom-core-security
Component: Untriaged → Widget: Cocoa
Flags: needinfo?(haftandilian)
Product: Firefox → Core

(In reply to :Gijs (he/him) from comment #1)

Haik, from your newsgroup post about notarized builds, I'm assuming this is covered by the notarized builds work and is fixed in 69? Is that right, and if so, is there a more minimal fix we could apply to 68/esr ?

Hardened Runtime and Notarization are enabled together in 69, but we are allowing the dyld environment variable entitlement. I found that we depend on these variables (e.g., @executable_path). I don't know enough about these to say how difficult it would be to change that. I'll look into this some more.

Not to say this isn't something we should fix, but if an attacker can install a malicious dylib and control the launching of the browser, presumably they could instead install a malicious version of Firefox.

Our configuration for Hardened Runtime is here:
https://searchfox.org/mozilla-central/rev/8a990595ce6d5ed07ace2d4d5d86cc69aec90bde/security/mac/hardenedruntime/production.entitlements.xml

Flags: needinfo?(haftandilian)

We appreciate the bug report.

After looking into this some more, so far I can see that we depend on DYLD_INSERT_LIBRARIES and DYLD_LIBRARY_PATH. In order to set allow-dyld-environment-variables=false, we need to change that. When launching child processes, we use DYLD_INSERT_LIBRARIES[1] ourselves so that plugin child processes can load libplugin_child_interpose.dylib[2]. From the code comments, we always set it when launching a child process, but only need to use the interposer in plugin processes.

We might be able to link the parent process Firefox binary in a way that blocks DYLD_INSERT_LIBRARIES, but allow it for child processes where we can control how it is set. Some applications are using an undocumented __RESTRICT header in the binary to get dyld to ignore DYLD variables.

I was under the impression that allow-dyld-environment-variables=false also applies to the @variables listed in dyld(1), but that might be wrong. I will try to confirm that.

More investigation/testing needed.

  1. https://searchfox.org/mozilla-central/rev/8a990595ce6d5ed07ace2d4d5d86cc69aec90bde/ipc/glue/GeckoChildProcessHost.cpp#854
  2. https://searchfox.org/mozilla-central/rev/8a990595ce6d5ed07ace2d4d5d86cc69aec90bde/dom/plugins/ipc/interpose/plugin_child_interpose.mm
Assignee: nobody → haftandilian
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

Hey!

Known security researcher wrote yesterday an article about injecting code using DYLD_INSERT_LIBRARIES. Maybe that article will be helpful to you in resolving this issue :-) - https://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/

Cheers,

Component: Widget: Cocoa → Security: Process Sandboxing

(In reply to wojciechregula from comment #4)

Hey!

Known security researcher wrote yesterday an article about injecting code using DYLD_INSERT_LIBRARIES. Maybe that article will be helpful to you in resolving this issue :-) - https://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/

Thanks, I'll give it a look. Addressing this is going to require some Firefox changes because Firefox specifically depends on using DYLD_INSERT_LIBRARIES in some cases. We also use it in some tests, but that shouldn't prevent us from rolling this out in production. We do want to address this, but we need to figure out the scope of the Firefox changes required and take into account the attack vector.

Once we change Firefox to not depend on DYLD_INSERT_LIBRARIES or any other DYLD environment variables, the simplest fix for us would be to set com.apple.security.cs.allow-dyld-environment-variables to false in our Hardened Runtime entitlement list (production.entitlements.xml in the source tree). At present, we enable macOS Hardened Runtime on Firefox 69 and later, and we plan to enable this on Firefox 68 soon.

Also, starting with macOS 10.15 and Firefox 69, the dylib should need to be signed by a valid Apple Developer ID identity in order for it to be loaded. @wojciechregula, please don't feel obligated, but could you try and reproduce this on a macOS 10.15 Beta using Firefox 69 or newer?

Flags: needinfo?(wojciechregula)

Unfortunately, I do not have a VM with macOS 10.15 to test that. But I can verify that on the newest macOS 10.14 with Firefox 69. ;-)

Flags: needinfo?(wojciechregula)

(In reply to wojciechregula from comment #6)

Unfortunately, I do not have a VM with macOS 10.15 to test that. But I can verify that on the newest macOS 10.14 with Firefox 69. ;-)

I tested on macOS 10.15 and after changing the test code to use the correct Calculator path[1] on 10.15, the dylib still loads via DYLD_INSERT_LIBRARIES despite it not being signed. That need some more investigation.

  1. On 10.15, Calculator has been moved to /System/Applications.

(In reply to Haik Aftandilian [:haik] from comment #7)

I tested on macOS 10.15 and after changing the test code to use the correct Calculator path[1] on 10.15, the dylib still loads via DYLD_INSERT_LIBRARIES despite it not being signed. That need some more investigation.

Where did you find the information that, in the macOS 10.15, the injected dylibs have to be signed with a valid Developer ID? I only found info that the new checks will be performed on the first use when the app is quarantined. - https://developer.apple.com/videos/play/wwdc2019/701/

I took a screenshot of the new checks - https://i.imgur.com/PliPBFg.png

Did I miss something?

(In reply to wojciechregula from comment #8)

(In reply to Haik Aftandilian [:haik] from comment #7)

I tested on macOS 10.15 and after changing the test code to use the correct Calculator path[1] on 10.15, the dylib still loads via DYLD_INSERT_LIBRARIES despite it not being signed. That need some more investigation.

Where did you find the information that, in the macOS 10.15, the injected dylibs have to be signed with a valid Developer ID? I only found info that the new checks will be performed on the first use when the app is quarantined. - https://developer.apple.com/videos/play/wwdc2019/701/

I took a screenshot of the new checks - https://i.imgur.com/PliPBFg.png

Did I miss something?

I'm referring to Hardened Runtime restrictions on loading shared libraries. The documentation doesn't talk about injected dylibs specifically, but I was assuming the restrictions would apply to DYLD_INSERT_LIBRARIES. I don't know if Hardened Runtime is intended to prevent that or not. That's something we need to investigate.

As of 10.15, Hardened Runtime restrictions are enforced and an application signed with the "-o runtime" flag (or if its enabled with Xcode) can only load libraries signed by the same team ID as the application. The com.apple.security.cs.disable-library-validation entitlement can be used to allow the application to load dylibs signed by other developers, but they do have to be signed.

On 10.15, applications are required to be notarized in order to launch (ignoring workarounds) and notarization is only allowed on apps with hardened runtime enabled.

On the blog post you referenced on comment 4, the author wrote they couldn't use DYLD_INSERT_LIBRARIES once they enabled hardened runtime until they signed the dylib. The error message they included has "Code has to be at least ad-hoc signed."

I tested a basic app built with Xcode 10.2.1 on 10.15 Beta 4 (19A512f) with Hardened Runtime enabled (allowing dyld variables) and I could also load an unsigned shared library using DYLD_INSERT_LIBRARIES. That behavior is consistent with what we see with Firefox on 10.15.

However, with Xcode 11.0 beta 2 (11m337n) running on 10.15 Beta 4 (19A512f), with the same test program, using DYLD_INSERT_LIBRARIES with an unsigned shared library fails. That triggers the "Code has to be at least ad-hoc signed" error message. After signing the dylib, DYLD_INSERT_LIBRARIES worked.

We might just need to use a newer codesign command to get this benefit. And this might be documented somewhere in the WWDC 2019 presentations. More investigation needed and it should eventually be its own bug.

On 10.15, applications are required to be notarized in order to launch (ignoring workarounds) and notarization is only allowed on apps with hardened runtime enabled.

OK, you are right - I missed that ;-) Hardened runtime without com.apple.security.cs.disable-library-validation will force the valid Developer ID.

We might just need to use a newer codesign command to get this benefit. And this might be documented somewhere in the WWDC 2019 presentations. More investigation needed and it should eventually be its own bug.

That's interesting!

Priority: -- → P2

Apple recently changed the definition of com.apple.security.cs.disable-library-validation so that it matches the behavior we observed on Mojave. And I believe the more strict behavior of Catalina Beta was changed to match the new less strict definition in Beta 4. See bug 1570840 for more information.

Once bug 1570840 lands, it should not be possible to use DYLD_INSERT_LIBRARIES to inject an unsigned library. Update: We can't set com.apple.security.cs.disable-library-validation=false because we still need to load libraries signed by different Apple developer team ID's. More details on the bug.

We should still work to prevent using DYLD_INSERT_LIBRARIES to inject signed libraries. We need to make the necessary Firefox changes so that we can set the com.apple.security.cs.allow-dyld-environment-variables entitlement to false (at least on production builds).

See Also: → 1570840

FYI - I've just confirmed that the newest Firefox 68.0.2esr has the Hardened Runtime capability turned on (flags=0x10000(runtime)).

$ codesign -d --verbose /Volumes/Firefox/Firefox.app/Contents/MacOS/firefox
Executable=/Volumes/Firefox/Firefox.app/Contents/MacOS/firefox
Identifier=org.mozilla.firefox
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20500 size=415 flags=0x10000(runtime) hashes=4+5 location=embedded
Signature size=9019
Timestamp=13 Aug 2019 at 19:14:23
Info.plist entries=26
TeamIdentifier=43AQ936H96
Runtime Version=10.11.0
Sealed Resources version=2 rules=13 files=95
Internal requirements count=1 size=188
See Also: → 1575726
Depends on: 1575726
See Also: 1575726

Hey,

Any news? :-)

Not much to update at this time.

We should be able to set com.apple.security.cs.allow-dyld-environment-variables=false and possibly com.apple.security.cs.disable-library-validation=false for the parent process, but not child processes. This should be easy to test and I'll update the bug once I have some results. I originally thought hardened runtime entitlements were inherited by child processes (testing done with 10.14 suggested that), but that's incorrect. Sometimes parent process entitlements can affect child capabilities, but, from what I understand, only when the TCC subsystem is trying to decide if an operation is permitted and ends up falling back to the parent process's entitlements.

For child processes, we use one plugin-container executable for all child process types so we can't use disable-library-validation=false or allow-dyld-environment-variables=false yet. Once we remove Flash support from the tree, we can stop using the Mac interpose library we use and start using allow-dyld-environment-variables=false. If we used different binaries for different child process types, we could use different entitlements and therefore use library validation. I will file a bug to use different plugin-container binaries for different child process types (web content, plugin) so we can set disable-library-validation=false for web content.

I've done some testing with using separate entitlements for the parent process and plugin-container process. Setting com.apple.security.cs.allow-dyld-environment-variables=false for the parent process works as expected and DYLD_INSERT_LIBRARIES can not be used to inject a dylib. This only works if com.apple.security.get-task-allow=false which is what we use for production. get-task-allow is intended to be used for debugging.

Landing this depends on bug 1593072 to update the build automation to support separate entitlement files.

Depends on: 1593072
No longer depends on: 1575726
See Also: → 1606464
Depends on: 1606778

Like with bug 1606464, this bug does not need to remain private. Yes it's possible to load an unsigned shared library if you have remote-code-execution or if you can launch Firefox with DYLD_INSERT_LIBRARIES on the target system, but this was typical of all apps prior to Mojave when these hardening entitlements were introduced for app use.

For tracking purposes, we have meta bug 1606778 which covers the work required to block loading of libraries not signed by Mozilla or Apple and to block the use of DYLD_INSERT_LIBRARIES in the parent process. To extend that work to content processes, we depend on bug 1593389.

Group: dom-core-security
Flags: sec-bounty? → sec-bounty-
Depends on: 1697471
Keywords: stalled

Now that Flash is gone (via bug 1682030), we should be able to use allow-dyld-environment-variables=false without depending on bug 1593389. I'll work on testing the change.

Small update. I haven't had time to make much more progress on this. I previously tested without the dyld entitlement, but ran into launch failures caused by failures to load libnss3.dylib from the .app bundle. I don't know if this is due to the use of the dyld @executable_path variable or another issue. More debugging needed.

$ ./Nightly.app/Contents/MacOS/firefox -P CodeSignTests
dyld[24028]: Library not loaded: @executable_path/libnss3.dylib
  Referenced from: /Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container
  Reason: tried: '/Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/libnss3.dylib' (no such file), '/usr/lib/libnss3.dylib' (no such file)
[Parent 24027, IPC Launch #1] WARNING: parent MachReceivePortSendRight failed: 0x10004003 (ipc/rcv) timed out: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:1308
[Parent 24027, IPC I/O Parent] WARNING: Failed to launch socket subprocess: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:756
dyld[24029]: Library not loaded: @executable_path/libnss3.dylib
  Referenced from: /Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container
  Reason: tried: '/Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/libnss3.dylib' (no such file), '/usr/lib/libnss3.dylib' (no such file)
[Parent 24027, IPC Launch #1] WARNING: parent MachReceivePortSendRight failed: 0x10004003 (ipc/rcv) timed out: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:1308
[Parent 24027, IPC I/O Parent] WARNING: Failed to launch tab subprocess: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:756
UNSUPPORTED (log once): POSSIBLE ISSUE: unit 1 GLD_TEXTURE_INDEX_2D is unloadable and bound to sampler type (Float) - using zero texture because texture unloadable
JavaScript error: resource:///modules/sessionstore/SessionStore.jsm, line 6291: TypeError: can't access property "sendAsyncMessage", browser.messageManager is null
dyld[24030]: Library not loaded: @executable_path/libnss3.dylib
  Referenced from: /Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container
  Reason: tried: '/Users/haftandilian/r/codesign-tree/examples/02-firefox-old/Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/libnss3.dylib' (no such file), '/usr/lib/libnss3.dylib' (no such file)
[Parent 24027, IPC Launch #1] WARNING: parent MachReceivePortSendRight failed: 0x10004003 (ipc/rcv) timed out: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:1308
[Parent 24027, IPC I/O Parent] WARNING: Failed to launch tab subprocess: file /Users/haftandilian/r/mc/ipc/glue/GeckoChildProcessHost.cpp:756

I've got this to work on an ad-hoc package build. By starting with a package build and then changing the dylib loader paths for the plugin-container executable and the shared dylibs using install_name_tool(1) I get a package that launches cleanly with allow-dyld-environment-variables=false and injection appears to be blocked. The child process needs to be changed to be @executable_path/../../../<dylib> to reference the location of the dylibs. And the dylibs get changed to use @loader_path instead of @executable_path so they correctly load their dylib dependencies when loaded from the parent or child process. See examples below.

The current dyld paths are wrong for plugin-container, but the dylibs can be loaded because the parent process sets DYLD_LIBRARY_PATH to the directory in the bundle containing the dylibs such as /Applications/Nightly.app/Contents/MacOS. This doesn't work with allow-dyld-environment-variables=false because DYLD_LIBRARY_PATH is ignored.

For plugin-container, changing from

$ otool -L Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container 
Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container:
	@executable_path/libnss3.dylib (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/XUL (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/libmozglue.dylib (compatibility version 1.0.0, current version 1.0.0)
	...

to

$ otool -L Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container 
Nightly.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container:
	@executable_path/../../../libnss3.dylib (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/../../../XUL (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/../../../libmozglue.dylib (compatibility version 1.0.0, current version 1.0.0)
	...

And for the dylibs, using XUL as an example, changing from

$ otool -L Nightly.app/Contents/MacOS/XUL
Nightly.app/Contents/MacOS/XUL:
	@executable_path/XUL (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/libnss3.dylib (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/liblgpllibs.dylib (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/libmozglue.dylib (compatibility version 1.0.0, current version 1.0.0)
	...

to

$ otool -L Nightly.app/Contents/MacOS/XUL
Nightly.app/Contents/MacOS/XUL:
	@executable_path/XUL (compatibility version 1.0.0, current version 1.0.0)
	@loader_path/libnss3.dylib (compatibility version 1.0.0, current version 1.0.0)
	@loader_path/liblgpllibs.dylib (compatibility version 1.0.0, current version 1.0.0)
	@loader_path/libmozglue.dylib (compatibility version 1.0.0, current version 1.0.0)
	...

@glandium, do you see any problem with changing the build to have the dyld loader paths setup like this for plugin-container and our dylibs?

Flags: needinfo?(mh+mozilla)
Depends on: 1770484

Presumably not, but it depends what we do with non-packaged builds locally. I think we only use dist/*.app, not dist/bin, so that should be fine.

Flags: needinfo?(mh+mozilla)

Removing the stalled keyword, which is a security evaluation (we need more information to even characterize the bug) and not an engineering state (those would be "wontfix" or "future" or similar). The issue is described clearly.

Keywords: stalled
No longer depends on: 1593072, 1606778

Drop the com.apple.security.cs.allow-dyld-environment-variables entitlement to disallow use of dyld environment variables in signed production builds.

Leave the entitlement in for signed developer builds.

Firefox gtests depend on the use of DYLD_LIBRARY_PATH. However, testing infrastructure does not run gtests on signed builds and therefore gtests are not impacted by this change. gtests could be run on signed developer builds in the future which will still allow dyld environment variables after this change.

browser.production.entitlements.xml and plugin-container.production.entitlements.xml are not used, but being kept up to date.

Pushed by haftandilian@mozilla.com:
https://hg.mozilla.org/integration/autoland/rev/4f741519a2d6
Code Injection in Firefox macOS desktop r=spohl
See Also: → 1772952
Status: ASSIGNED → RESOLVED
Closed: 2 years ago
Resolution: --- → FIXED
Target Milestone: --- → 103 Branch

Comment on attachment 9279688 [details]
Bug 1562756 - Code Injection in Firefox macOS desktop r?spohl!

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration: The fix provides a security improvement preventing Firefox from loading third-party and possibly malicious dylibs via DYLD environment variables.

This can be a vector for malicious dylibs to inject into Firefox and bypass macOS security controls and gain access to resources Firefox has been allowed by the user to access. For example, if Firefox has been allowed access to the camera/mic for zoom.us, a malicious dylib could be injected into Firefox and use the camera without triggering an OS permission prompt.

The fix depends on bug 1770484.

  • User impact if declined: No user-visible impact. Firefox will continue to allow loading of third-party and possibly malicious dylibs.
  • Fix Landed on Version: 103
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): The change is only to Firefox entitlement files and blocks use of DYLD environment variables which we don't use on production builds.
Attachment #9279688 - Flags: approval-mozilla-esr102?

Comment on attachment 9279688 [details]
Bug 1562756 - Code Injection in Firefox macOS desktop r?spohl!

Approved for ESR102.1, thanks.

Attachment #9279688 - Flags: approval-mozilla-esr102? → approval-mozilla-esr102+

This bug in itself does not represent a bypass of any security boundaries, but presented a very useful opportunity for hardening. Flagging this accordingly.

Keywords: sec-moderatesec-want
Whiteboard: [adv-main103-]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: