Sunday, February 3, 2013

Exploiting 64-bit Linux like a boss

Back in November 2012, a Chrome Releases blog post mysteriously stated: "Congratulations to Pinkie Pie for completing challenge: 64-bit exploit".

Chrome patches and autoupdates bugs pretty fast but this is a WebKit bug and not every consumer of WebKit patches bugs particularly quickly. So I've waited a few months to release a full breakdown of the exploit. The exploit is notable because it is against 64-bit Linux. 64-bit exploits are generally harder than 32-bit exploits for various reasons, including the fact that some types of heap sprays are off the table. On top of that, Linux ASLR is generally better than Windows ASLR (although not perfect). For example, Pinkie Pie's Pwnium 2 exploit defeated Win 7 ASLR by relying on a statically-addressed system object! That sort of nonsense is generally absent from Linux ASLR.

Without any further ado, I'll paste my raw notes from the exploit deconstruction below. The number of different techniques used and steps involved is quite impressive.

The bug
A single WebKit use-after-free bug was used to gain code execution. The logic flaw in WebKit was reasonably simple: when a WebCore::HTMLVideoElement is garbage collected, the base class member WebCore::HTMLMediaElement::m_player -- a WebCore::MediaPlayer -- is freed. A different object, a WebCore::MediaSource, holds a stale pointer to the freed WebCore::MediaPlayer. The stale pointer can be prodded indirectly via Javascript methods on either the JS MediaSource object, or JS SourceBuffer objects owned by the JS MediaSource.

The exploit
The exploit is moderately complicated, with multiple steps and techniques used. Pinkie Pie states that the complexity is warranted and generally caused by limited lack of control, and therefore limited options for making progress at each stage.

The exploit steps are as follows:

1. Allocate a large number of RTCIceCandidate objects (100000) and then unreference a small subset of them.
   tempia = new Uint32Array(176/4);
   rtcs = [];
   rtcstring = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
   rtcdesc = {'candidate': rtcstring, 'sdpMid': rtcstring}
   for(var i = 0; i < 100000; i++) {
       rtcs.push(new RTCIceCandidate(rtcdesc));
   }
   for(var i = 0; i < 10000; i++) rtcs[i] = null;
   for(var i = 90000; i < 100000; i++) rtcs[i] = null;

This step indirectly creates a lot of WebCore::WebCoreStringResource (v8 specific) objects and a later garbage collection will free some subset of them.
These objects are 24 bytes in size (fitting into a tcmalloc slab of 32 byte sized allocations), so it means that any future 24 byte allocation has a large probability of being placed directly before a WebCore::WebCoreStringResource object. This is significant later.
A 176-byte sized buffer is also allocated.

2. Trigger free of MediaPlayer and the 176-byte sized buffer; allocate another MediaSource object.
   buffer = ms.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
   vid.parentNode.removeChild(vid);
   vid = null;
   gc();
   tempia = null;
   gc();
   ms2 = new WebKitMediaSource();

   sbl = ms2.sourceBuffers;
The WebCore::MediaPlayer is 264 bytes in size (tcmalloc bucket 257 - 288). When it is freed, many child objects are also freed. The only important one is a 168 byte sized WebKit::WebMediaPlayerClientImpl object (tcmalloc bucket 161 - 176).
Allocation of the WebCore::MediaSource (176 bytes) also subsequently allocates a WebCore::SourceBufferList child object (168 bytes). The free of the temporary 176 byte buffer (tempia) is to ensure that its freed slot is used for the WebCore::MediaSource object, leaving the freed slot that was occupied by a WebKit::WebMediaPlayerClientImpl to be occupied by a new WebCore::SourceBufferList object.

3. Call vtable of freed WebMediaPlayerClientImpl.
   buffer.timestampOffset = 42; // free
In C++, this triggers the call chain WebCore::SourceBuffer -> WebCore::MediaSource -> WebCore::MediaPlayer -> (virtual) WebKit::WebMediaPlayerClientImpl.
You’ll notice that the call chain bounces through the WebCore::MediaPlayer, which is freed. However, the only access is to the WebCore::MediaPlayer::m_private member at offset 72. delete’ing the object only interferes with the first 16 bytes (on account of tcmalloc writing two freelist pointers) and the WebCore::MediaPlayer::m_mediaPlayerClient member. The WebCore::MediaPlayer free slot isn’t otherwise meaningfully re-used by this point.

What happens next is fascinating. WebCore::MediaPlayer::sourceSetTimestampOffset dissassembles to:
   0x00007f61a0ced4c0 <+0>: mov    rdi,QWORD PTR [rdi+0x48]
   0x00007f61a0ced4c4 <+4>: mov    rax,QWORD PTR [rdi]
   0x00007f61a0ced4c7 <+7>: mov    rax,QWORD PTR [rax+0x208]
   0x00007f61a0ced4ce <+14>: jmp    rax

This loads the vtable for the WebCore::MediaPlayer::m_private member and calls the vtable function at 0x208. WebCore::MediaPlayer::m_private is supposed to be a WebKit::WebMediaPlayerClientImpl object but a WebCore::SourceBufferList was overlayed there. WebCore::SourceBufferList objects have a vtable, but a much smaller one! Offset 0x208 in this vtable hits a vtable function in a totally different vtable, specifically WebCore::RefCountedSupplement::~RefCountedSupplement, which disassembles to:
   0x00007ffd9ec51e00 <+0>: lea    rax,[rip+0x3276969]
   0x00007ffd9ec51e07 <+7>: mov    QWORD PTR [rdi],rax
   0x00007ffd9ec51e0a <+10>: jmp    0x7ffd9e5b2c80 <
WTF::fastFree(void*)>

As these opcodes execute, rdi is a this pointer for a WebCore::SourceBufferList object (which the calling code believed was a this pointer to a WebKit::WebMediaPlayerClientImpl object). As you can see, the side effects of these opcodes are:
- Trash the vtable pointer of the WebCore::SourceBufferList object.
- Do a free(this), i.e free the WebCore::SourceBufferList object.
- Return cleanly to the caller.

4. Use HTML5 WebDatabase functionality to allocate a SQLStatement as a side effect.
   transaction.executeSql('derp', [], function() {}, function() {});
   slength = sbl.length;
A WebCore::SQLStatement object is 176 bytes in size. So it is allocated into the slot just vacated by free’ing the WebCore::SourceBufferList object in step 3 above. This is the same slot that we free’d the WebKit::WebMediaPlayerClientImpl from.
There are now two Javascript objects pointing to freed objects: a direct handle to a freed WebCore::SourceBufferList (sbl) and an indirect handle to a freed WebKit::WebMediaPlayerClientImpl (buffer).
At this time, a call is made in Javascript to sbl.length. It is not required for the exploit and nothing is done with the integer result, but looking at this call under the covers is instructive.
To return the length, a 64-bit size_t is read from offset 136 into the WebCore::SourceBufferList object. Since a WebCore::SQLStatement was put on top of the freed WebCore::SourceBufferList, the actual value read is a WebCore::SQLStatement::m_statementErrorCallbackWrapper::m_callback member pointer. Leaking this value to Javascript might be useful as it is a heap address. However, Javascript lengths are 32-bit so only the lower 32-bits of the address are leaked. The entropy that’s important for ASLR on 64-bit Linux is largely in the next 8 bits above the bottom 32 bits, so the heap address cannot be usefully leaked!
Exploitation of similar overlap situations would not be a problem on systems with 32-bit pointers.

5. Abuse overlapping fields in SourceBufferList vs. SQLStatement.
   sb = sbl[0xa8/8];
Next, the Javascript array index operator is used. At this time, the Javascript handle to the WebCore::SourceBufferList is actually backed by a WebCore::SQLStatement object at the C++ level. The WebCore::SourceBufferList::m_list member is a WTF::Vector and that starts with two important 64-bit fields: a length and a pointer to the underlying buffer.
As covered above, the length now maps to a pointer value. A pointer value, when treated as an integer, will be very large, effectively sizing the vector massively. And the vector’s underlying buffer pointer now maps to the member SQLStatement::m_statementErrorCallbackWrapper::m_scriptExecutionContext.

Therefore, the Javascript array operator on JS SourceBufferList will return a JS SourceBuffer object which is backed in C++ by a pointer pulled from somewhere in a C++ WebCore::ScriptExecutionContent object, depending on the array index.

The exploit uses array index 21, which corresponds to offset 168, or WebCore::ScriptExecutionContext::m_pendingExceptions. This is a pointer to a WTF::Vector. So, there is now a Javascript handle to a JS SourceBuffer object which is really backed by a WTF::Vector.

6. Read vtable value as a Javascript number.
   converterF64[0] = sb.timestampOffset;
In C++, the timestampOffset property is read from a 64-bit double at offset 32 of the WebCore::SourceBuffer object. The WebCore::SourceBuffer object is currently backed by a WTF::Vector object, which is 24 bytes in size and lives in a 32 byte tcmalloc slot. Therefore, a read at offset 32 will in fact read from the beginning of the next tcmalloc slot. Looking back to step 1, it was arranged to be likely that the adjacent 32 byte slot will contain a WebCore::WebCoreStringResource object. Therefore, the WebCore::WebCoreStringResource vtable is read and returned to Javascript as a number. Javascript numbers are 64-bit doubles so there are no truncation issues like those discussed with reading an integer length above in step 4.

That’s a lot of effort, but finally the exploit has leaked a vtable value to Javascript. For a given build of Chrome, it is now easy to calculate the exact address of all opcodes, functions, etc. in the binary.

7. Re-trigger use-after-free and back freed object with array buffer.
   buffer2 = ms3.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
   vid2.parentNode.removeChild(vid2);
   vid2 = null;
   gc();
   var ia = new Uint32Array(168/4);
   rtc2 = new webkitRTCPeerConnection({'iceServers': []});
This time, the freed WebKit::WebMediaPlayerClientImpl is replaced with a 168 raw byte buffer that can be read and written through Javascript. This is now a useful primitive because ASLR was defeated and a useful vtable pointer value can be put in the first 8 bytes of the raw byte buffer.
A WebCore::RTCPeerConnection is also allocated (264 bytes) to occupy the slot for the freed WebCore::MediaPlayer. This protects the freed WebCore::MediaPlayer from corruption. Significantly, it makes sure nothing overwrites the WebCore::MediaPlayer::m_private pointer. This pointer is needed intact. It is at offset 72 and WebCore::RTCPeerConnection does not overwrite that field during construction.

8. Leak address of a heap buffer under Javascript control.
   add64(converterI32, 0, converterI32, 0, -prepdata['found_vt']);
   add64(ia, 0, converterI32, 0, prepdata['mov_rdx_112_rdi_pp']);
   add64(ia, 0, ia, 0, -0x1e8);
   var ib8 = new Uint8Array(0x10000);
   var ib = new Uint32Array(ib8.buffer);
   buffer2.append(ib8);
   var ibAddr = [ia[112/4], ia[112/4 + 1]];
Using knowledge of the binary layout, a vtable value is chosen that will result in the WebCore::MediaPlayer::sourceAppend vtable call site calling the function v8::internal::HStoreNamedField::SetSideEffectDominator. An appropriate function name. It disassembles to:
   0x00007f153efd7340 <+0>: mov    QWORD PTR [rdi+0x70],rdx
   0x00007f153efd7344 <+4>: ret    
As can be seen, the value of rdx (the 2nd non-this function parameter) is written to offset 112 of this. this is backed by a raw buffer pointer for the ia Javascript Uint32Array and rdx in the context of WebCore::MediaPlayer::sourceAppend is a raw buffer pointer for the ib Javscript Uint32Array.
Therefore, the address of a heap buffer under the control of Javascript has been leaked to Javascript.

9. Proceed as normal.
The exploit now has control over a vtable pointer. It can point the vtable pointer at a heap buffer where the contents can be controlled arbitrarily. The exploit is free to start ROP chains etc.
As it happens, the exploit payload is expressed in terms of valid full function calls. This is achieved by bouncing into a useful sequence of opcodes in a template base::internal::Invoker<3>:
   0x00007f153fc71d40 <+0>: mov    rax,rdi
   0x00007f153fc71d43 <+3>: lea    rcx,[rdi+0x30]
   0x00007f153fc71d47 <+7>: mov    rsi,QWORD PTR [rdi+0x20]
   0x00007f153fc71d4b <+11>: mov    rdx,QWORD PTR [rdi+0x28]
   0x00007f153fc71d4f <+15>: mov    rax,QWORD PTR [rax+0x10]
   0x00007f153fc71d53 <+19>: mov    rdi,QWORD PTR [rdi+0x18]
   0x00007f153fc71d57 <+23>: jmp    rax
As can be seen, these opcodes pull a jump target, a new this pointer and two function arguments from the current this pointer. A very useful construct.

Monday, September 24, 2012

The joys and hazards of multi-process browser security

Web browsers with some form of multi-process model are becoming increasingly common. Depending on the exact setup, there can be significant consequences for security posture and exploitation methods.

Spray techniques

Probably the most significant security effect of multi-process models is the effect on spraying. Spraying, of course, is a technique where parts of a processes' heap or address space are filled with data helpful for exploitation. It's sometimes useful to spray the heap with a certain pattern of data, or spray the address space in general with executable JIT mappings, or both.

In the good ol' days, when every part of the browser and all the plug-ins were run in the same process, there were many possible attack permutations:

  • Spray Java JIT pages to exploit a browser bug.
  • Spray Java JIT pages to exploit a Flash bug.
  • Spray Flash JIT pages to exploit a browser bug.
  • Spray Java JIT pages to exploit Java.
  • You could even spray browser JS JIT pages to exploit Java if you wanted to ;-)
  • ...etc.

Since the good ol' days, various things happened to lock all this down:

  • The Java plug-in was rearchitected so that it runs out-of-process in most browsers.
  • IE and Chromium placed page limits on JavaScript-derived JIT pages (covered a little in the famous Accuvant paper.)
  • Firefox introduced its out-of-process plug-ins feature (for some plug-ins, most notably Flash) and Chromium had all plug-ins out-of-process since the first release.

The end result is trickier exploitation, although it's worth noting that one worrysome combination remains: IE still runs Flash in-process, and this has been abused by attackers in many of the recent IE 0days.

One-shot vs. multi-shot

The terms "one-shot" and "multi-shot" have long been used in the world of server-side exploitation. "One-shot" refers to a service that is dead after just one crash -- so your exploit had better be reliable! "Multi-shot" refers to a service whereby it remains running after your lousy exploit causes a crash. This could be because the service has a parent process that launches new children if they die or it could simply be because the service is launched by a framework that automatically restarts dead services.

Although moving to a multi-process browser is generally very positive thing for security and stability, you do run the risk of introducing "multi-shot" attacks.

In other words, let's say your exploit isn't 100% reliable. Wouldn't it be nice if you could just use a bit of JavaScript to run the exploit over and over in a child process until it works? Perhaps you simply weren't able to default ASLR and you're in the situation where you have a 1/256 chance of your hard-coded address being correct. Again, this could be brute-forced in a "multi-shot" attack.

The most likely "multi-shot" attacks are against plug-ins that are run out-of-process, or against browser tabs, if browser tabs can have separate processes.

These attacks can be defended against by limiting the rate of child process crashes or spawns. Chromium deploys some tricks in this area.

Broker escalation

Once an attack has gained code execution inside a sandbox, there are various directions it might go next. It might attack the OS kernel. Or for the purposes of this discussion, it might attack the privileged broker. The privileged broker typically runs outside of the sandbox, so any memory corruption vulnerability in the broker is a possible avenue for sandbox escape.

To attack the memory corruption bug, you'll likely need to defeat DEP / ASLR in the broker process. An interesting question is, how far along are you already, by virtue of code execution in the sandboxed process? Obviously, you know the full memory map layout of the compromised sandboxed process.

The answer, is it depends on your OS and the way the various processes relate to each other. The situation is not ideal on Windows; due to the way the OS works, certain system-critical DLLs are typically located at the same address across all processes. So ASLR in the broker process is already compromised to an extent, no matter how the sandboxed processes are created. I found this interesting.

The situation is better on Linux, where each process can have a totally different address space layout, including system libraries, executable, heap, etc. This is taken advantage of by the Chromium "zygote" process model for the sandboxed processes. So a compromise of a sandboxed process does not give any direct details about the address space layout of the broker process. There may be ways to leak it, but not directly, and /proc certainly isn't mapped in the sandboxed context! All this is another reason I recommend 64-bit Linux running Chrome as a browsing platform.

Wednesday, July 4, 2012

Chrome 20 on Linux and Flash sandboxing

[Very behind on blog posts so time to crank some out]

A week or so ago, Chrome 20 was released to the stable channel. There was little fanfare and even the official Chrome blog didn't have much to declare apart from bugfixes.

There were some things going on under the hood for the Linux platform, though. Security things, and some of them I implemented and am quite excited by.

The biggest item is an improvement to Flash security. Traditionally, Linux -- across all browsers -- hasn't had great Flash security, due to lack of sandboxing options. That just changed: so-called Pepper Flash shipped to the stable channel on Linux with Chrome 20 (other platforms to follow real soon). I went into a little detail about the technical sandbox measures in Pepper Flash for Linux in an older blog post.

As mentioned in the previous blog post, native 64-bit Flash also gives a useful security boost on 64-bit Linux platforms.

There's more. Perhaps you're running 64-bit Ubuntu 12.04? Courtesy of Kees Cook, this release sneaked in Will Drewry's seccomp filter patches, which I blogged about earlier this year in the context of vsftpd-3.0.0's usage of seccomp filter sandboxing.

So why have just one Flash sandbox if you can have two? A bit of double-bagging if you like. Assuming you're running 64-bit Ubuntu 12.04 and Chrome 20 or newer, you'll also have a seccomp filter policy slapped on Flash -- in additional to the chroot() and PID namespace. This may impede attackers trying to perform a local privilege escalation, who can no longer call crazy brand-new syscalls or use socket() to load crazy protocol modules, etc.

No sandbox or combination of sandboxes will ever be perfect, but "some" is better than "none". For people who want to run Flash, Chrome 20 on 64-bit Ubuntu 12.04 is one of the more locked-down ways to do it.

Monday, April 9, 2012

vsftpd-3.0.0 and seccomp filter sandboxing is here!

vsftpd-3.0.0 is released.

Aside from the usual few fixes, I'm excited about built-in support for Will Drewry's seccomp filter, which landed in Ubuntu. To give it a whirl, you'll need a 64-bit Ubuntu 12.04 (beta at time of writing), and a 64-bit build of vsftpd.

Why all the excitement?

vsftpd has always piled on all of the Linux sandboxing / privilege facilities available, including chroot, capabilities, file descriptor passing, pid / network / etc. namespaces, rlimits, and even a ptrace-based demo (never quite production).

seccomp filter brings a new level of power and granularity in the form of the ability to permit, deny or emulate raw syscalls, with some control over the arguments. In many ways it's similar to what can be achieved with a clunky ptrace-based sandbox -- but it will go a lot faster, have a lot less bugs and not be prone to various fail-open conditions. In other words, it's designed to be used as a security technology whereas ptrace() is not.

Some of the more compelling points of seccomp filter include:

  • Ability to restrict access to the kernel API. In all likelihood, a compromise of a vsftpd process wouldn't be much use to an attacker due to the use of chroot() and namespaces. The attacker would be looking to escalate privileges and the most fruitful way to do this would be going after a kernel bug. By only allowing a small subset of syscalls, the number of kernel APIs exposed to attack is minimal.

  • Application-defined. An unprivileged application can install a filter. This has various benefits. For example, a future Chromium will likely ship without the need for a "setuid helper". A future vsftpd might offer robust sandboxing even when not started as root.

  • Compatible with syscall emulation. Doing access control on user-space pointer arguments is racy with ptrace and impossible with seccomp filter. However, a denied syscall can be emulated via a SIGSYS signal. In the signal handler, something like an open() call can be "faked", perhaps even to the extent of sending the filename over a local socketpair for validation and delegated open. Very tasty. I'll look at writing a general wrapper if no-one else does.

  • Defense against glibc vulnerabilities. I'll go into this in more detail in another post, but a recent glibc memory corruption vulnerability illustrated that glibc takes an "interesting" code path in response to detecting bad situations. This failure code path ended up making the glibc bug highly exploitable. Fortunately, the syscalls needed by the "interesting" code path don't need to be permitted in a seccomp filter policy, thus blocking much of the problem.

It's all very powerful, and vsftpd isn't the only exited consumer. There's already a patch in OpenSSH, to be released with version 6.

Personally, I'm not sure I have the skill to attack vsftpd + seccomp filter. Even if I were to achieve code execution, the set of permitted syscalls is pretty limited. If you look at some of the memorable Linux kernel vulns of recent years: AF_CAN by Ben Hawkes, sock_sendpage by Julien Tinnes and Tavis Ormandy, or sys_tee -- all of these would be blocked either at the syscall, or syscall argument validation level. If you go back to 2003, there's brk(), which would probably have done the trick. If you know of any other examples, I'd love to collect them.

Tuesday, April 3, 2012

vsftpd-3.0.0-pre2

Just a quick note that vsftpd-3.0.0 is imminent. The big-ticket item is the new seccomp filter sandboxing support.

Please test this, particularly on 64-bit Ubuntu Precise Beta 2 (or newer) or if you use SSL support.

I would love to get a quick note (e-mail or comment here) even if just to say it seems to work in your configuration.

https://security.appspot.com/downloads/vsftpd-3.0.0-pre2.tar.gz

https://security.appspot.com/downloads/vsftpd-3.0.0-pre2.tar.gz.asc

Wednesday, March 28, 2012

vsftpd-3.0.0-pre1 and seccomp filter

For the brave, there now exists a pre-release version of vsftpd-3.0.0:

https://security.appspot.com/downloads/vsftpd-3.0.0-pre1.tar.gz

https://security.appspot.com/downloads/vsftpd-3.0.0-pre1.tar.gz.asc

The most significant change is an initial implementation of a secondary sandbox based on seccomp filter, as recently merged to Ubuntu 12.04. This secondary sandbox is pretty powerful, but I'll go into more details in a subsequent post.

For now, suffice to say I'm interested in testing of this new build, e.g.
  • Does it compile for you? (I've added various new gcc flags etc).

  • Any runtime regressions?

  • Does it run ok on 64-bit Ubuntu 12.04-beta2 or newer?

This last question is key as that is the configuration that will automatically use a seccomp filter. The astute among you will note that beta2 is not due out until tomorrow, but an apt-get dist-upgrade from beta1 will pull in the kernel that you need.

Will Drewry's excellent work on seccomp filter is the most exciting Linux security feature in a long time and the eventual vsftpd combined sandbox that will result should be a very tough nut to crack indeed.

Thursday, March 22, 2012

On the failings of Pwn2Own 2012

This year's Pwn2Own and Pwnium contests were interesting for many reasons. If you look at the results closely, there are many interesting observations and conclusions to be made.

$60k is more than enough to encourage disclosure of full exploits

As evidenced by the Pwnium results, $60k is certainly enough to motivate researchers into disclosing full exploits, including sandbox escapes or bypasses.

There was some minor controversy on this point leading up to the competitions, culminating in this post from ZDI. The post unfortunately was a little strong in its statements including "In fact, we don't believe that even the entirety of the $105,000 we are offering would be considered an acceptable bounty", "for the $60,000 they are offering, it is incredibly unlikely that anyone will participate" and "such an exploit against Chrome will never see the light of day at CanSecWest". At least we all now have data; I don't expect ZDI to make this mistake again. Without data, it's an understandable mistake to have made.

Bad actors will find loopholes and punk you

One of the stated -- and laudable -- goals of both Pwn2Own and Pwnium is to make users safer by getting bugs fixed. As recently noted by the EFF, there are some who are not interested in getting bugs fixed. At face value, it would seem to be counterproductive for these greyhat or blackhat parties to participate.

Enter VUPEN, who somehow managed to turn up and get the best of all worlds: $60k, tons of free publicity for their dubious business model and... minimal cost. To explore the minimal cost, let's look at one of the bugs they used: a Flash bug (not Chrome as widely reported), present in Flash 11.1 but already fixed in Flash 11.2. In other words, the bug they used already had a fixed lifetime. Using such a bug enabled them to collect a large prize whilst only handing over a doomed asset in return.

Although operating within the rules, their entry did not do much to advance user security and safety -- the bug fix was already in the pipeline to users. They did however punk $60k out of Pwn2Own and turned the whole contest into a VUPEN marketing spree.

Game theory

At the last minute at Pwn2Own, contestants Vincenzo and Willem swooped in with a Firefox exploit to collect a $30k second place prize. The timing suggests that they were waiting to see if their single 0-day would net them a prize or not. It did. We'll never know what they would have done if the $30k reward was already sewn up by someone else, but one possibility is a non-disclosure -- which wouldn't help make anyone safer.

Fixing future contests

The data collected suggests some possible structure to future contests to ensure they bring maximal benefit to user safety:
  • Require full exploits, including sandbox escapes or bypasses.

  • Do not pay out for bugs already fixed in development releases or repositories.

  • Have a fixed reward value per exploit.