Contents

Cracking JXcore

Recently, a co-worker was trying to figure out how to protect a node.js project from reverse engineering and modification. Of course, programmers have spent decades trying to figure out ways to allow an end user to run a program without letting the end user reverse engineer or modify the program, and I’ve never heard of anybody successfully doing it. At best, the program is still insecure and the developers have only managed to piss off their high-paying customers.

So naturally, my skepti-larm was blaring when my coworker sent me the link for JXcore.

Update on 2014-09-12
I’ve been informed that JXcore has fixed the security flaws discussed in this post.
Update on 2015-06-25
I have completed an analysis of the latest version of JXcore version 0.3.

Introduction

JXcore is a fork of node.js that is dubiously designed to improve upon some of node.js’s weaknesses, one of which is to protect source code from disclosure or modification. In fact, they make a startling claim:

JXcore
No obfuscation, no gibberish. Complete protection of your intellectual property!

I’m not kidding. Go take a look at their website. How do they protect the source code of a high level, highly dynamic, interpreted language without using obfuscation? Their page vaguely refers to encryption, which presumably means symmetric encryption where the key is distributed with your “protected” code. If that’s the case, then copy protection is worth precisely… um… carry the 1… hold on. My scientific calculations say that it’s precisely worth dick.

Come on, people! How many times do we need to try to reinvent stupid copy protection schemes to protect high level, dynamic languages. I remember years ago there was a company called ionCube that made the same types of outrageous claims for PHP source code. If you Google “ioncube crack", you will of course find dozens or hundreds of various sites that explain how to crack ionCube files or will even do it for you! (For a fee, of course.) This apparently hasn’t stopped ionCube from selling their useless product anyway.

Anyway, back to JXcore. None of my coworkers seemed to comprehend the degree of ridiculousness of their approach, so I sat down tonight to crack JXcore. I’m not a reverse engineer, and JXcore is a massive program (because it’s a fork of node.js/V81, not some wimpy wrapper), but I was able to extract the symmetric encryption key in a couple of hours. I’m sure somebody more talented than myself could have figured this out in a few minutes. Then I spent another 30 minutes writing a script to crack these files, just to make sure my key worked.

For the sake of my sanity and the sanity of others, I will quickly outline how I cracked it. Hopefully this highlights how futile and stupid these types of schemes are, and all of posterity will learn not to even bother with these harebrained, completely infeasible schemes. Sure they will.

Test Case

I made a little toy node.js program that I “encrypted” with JXcore to see if I could crack it.

$ cat test.js
console.log("SUPER S3CRET NODE.JS APP!");

$ jx package test.js test
Processing the folder..
JXP project file (test.jxp) is ready.

preparing the JX file..
compiling test 1.0
adding script test.js
compiled file is ready (test.jx)

$ xxd < test.jx | head
0000000: 1074 f116 ffa6 cfc3 a88c 5dd2 66b2 444a .t........].f.DJ
0000010: cf2e af8a 493f 7dd0 ff2f dd44 2011 df6c ....I?}../.D ..l
0000020: 7749 4738 6a5a 34bc a5dd 3398 dce1 7c6b wIG8jZ4...3...|k
0000030: 8280 39fe 9adc 2405 aacb 9cd2 4680 8195 ..9...$.....F...
0000040: a88f 0cf4 6055 337a 0d6b 1959 9fdc 1bc9 ....`U3z.k.Y....
0000050: 3877 14ef 31b6 75ef 2129 b811 eded 4d2b 8w..1.u.!)....M+
0000060: 8a97 5965 2595 1217 462c 6462 58f0 81e2 ..Ye%...F,dbX...
0000070: 3c8f 51c5 353e ddfc a6a9 0275 eb7b a9c7 .....u.{..
0000080: 2c80 7cae f3b1 6f70 7dba b2c5 7eb3 85c8 ,.|...op}...~...
0000090: 216d 8240 30c9 c5a5 bc40 c710 cea4 5d49 !m.@0....@....]I

$ jx test.jx
SUPER S3CRET NODE.JS APP!

Alrighty. We have test.jx encrypted, but JXcore can still run it. Our mission is to recover the highly valuable and expensive source code in test.jx.

Reversing

The first step was to load up JXcore inside a debugger and see if I could figure out where it loads .jx files and what it does to them. This part took me quite a while because I really don’t know how to use gdb. I initially tried running invalid JX archives, hoping that I could get the program to break inside some encryption pad-checking routine, but no such luck.

Eventually after single stepping for a while I finally found what I was looking for: node::MainSource(). This appears to be a part of node.js, so I’m sure a real node.js hacker would have found it much quicker than myself. This function appears to be obfuscated: it loads a couple dozen strings from functions like node::part22() and node::part39() and concatenates them all together.

Stepping down to the end of this function, there’s a moment where the C string is used to initialize a new V8 string.

0x00000000008a4ee7 <+631>: callq 0x5ebac0 <v8::String::New(char const*, int)>

At this point, the first argument to v8::String::New() is in the $rdi register, allowing me to view the entire string after it has been tediously assembled by all of the obfuscated functions that preceded it. What did I find?

/*ouvz&tJXPoaQnodxU899ad*/// Copyright Nubisa, Inc. 2014
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

// Hello, and welcome to hacking node.js!
//
// This file is invoked by node::Load in src/node.cc, and responsible for
// bootstrapping the node.js core. Special caution is given to the performance
// of the startup process, so many dependencies are invoked lazily.
(function(process) {
    this.global = this;

    var hasRestartListener = false;
    function startup() {
    var EventEmitter = NativeModule.require('events').EventEmitter;

    process.__proto__ = Object.create(EventEmitter.prototype, {
        constructor: {
        value: process.constructor
        }
    });
    EventEmitter.call(process);

/*** SNIP HUNDREDS OF LINES ***/

var $$uw = process.binding('memory_wrap');NativeModule.getSource = function(o) {if(!o){return null;}return $$uw.readSource(o);};NativeModule._source = {config : NativeModule.getSource('config')};NativeModule.hasOwnProperty = function(o){return $$uw.existsSource(o);};NativeModule.Q = ';var nodedec = function(c){'+'try{var cr=NativeMo'+'dule.require(\'cry'+'pto\');'+'var dc=cr.createDe'+'ciph'+'eriv(\'ae'+'s-25'+'6-cb'+'c\',\''+'NubisaSPZ2013'+'AllRightsRe'+'NubisaSP\','+'\'nubisaINC2013all\');'+'return Buffer.concat([dc.update(c) + dc.final()]);'+'}catch(e){return \'{}\'}};'+'Module._extensions[\'.jx\']=function(m, f, pa) {/*_jx_protected_*/'+'if(f.indexOf(\'.json.jx\')>0){Module._extensions[\'.jsonx\'](m,f);return;}'+'if($uw.existsSource(\'@\' +f)){'+'m._compile(\'@\' +f, f, true, true);return;'+'}'+'var con=NativeModule.require(\'fs\').readFileSync(f);'+'var buffer=new Buffer(con);'+'var ec=nodedec(buffer);ec=ec.toString(\'utf8\');'+'buffer = null;con=null;'+'var obj=JSON.parse(ec);ec=null;'+'if(!obj.project){console.log(\'Package is corrupted\',f);process.exit(-1);};'+'return readX(m,f,obj,pa,obj.project.extract);'+'};'+'Module._extensions[\'.j'+'x\'].toString=function(){};Object.freeze(Module._extensions[\'.jx\']);';var nodeen = function(c){/*_jx_protected_*/var cr=NativeModule.require('crypto');/*_jx_protected_*//*fh9UGNaQ#(dsl11dh*/var cp=cr.createCipheriv('aes-256-cbc','NubisaSPZ2013AllRightsReNubisaSP','nubisaINC2013all');/*_jx_protected_*/  return Buffer.concat([cp.update(c), cp.final()]);/*_jx_protected_*//*dhja,@3kdjdf*/};/*&uvz&tJXPoaQnodeJX&vztu&*/;startup();});
/*0hUxCz239D91ndfF28nvk#1udbnla@1dhja,@3kdjdfn$jdskfas18z239D91ndfF289dsnfjghUxCz239D91ndfF223428nvk#1udbnla@1dhjkdjdfn$jdskfas81z239D91df4n3288hUxCz239D91ndfF28nvk#1udfmvbnla@1dhja,@dfsjf3kdjdfn$jdskfas80z239D91ndfFdfsnsdfj287hUxCz239D91ndfF28nvk#1ufkb93dbnla@1dhja,@3kdjdfn$jdskfas1z239D91ndfmdkfF286hudbnla@1dhja,@3kdjdfn$jdskfas805mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskfasmnb2b3fh9UGNz239D91ndfkfl23fF285hUxCz239D91ndfF28nvk#1udbns80mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskfamnb2b3fh9UGNz2dfsnsh39D91ndfF28mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskmnb2b3fh9UGNz239D91ndfF2erj483hUxCz2gfdwer39D91ndfF28nvk#1udbnla@1dhja,@3kdjdfnsdfsdf$jds44z232349D91ndfF282hUxCz239D91ndfF28nvk#1udbnlasdasda@1dhjab2b3fh9gfhfghUGNaQ#(dsl1dhja,@3kdjdfn$jdskfas8mnb2b3fh9UGNz239D54391nd123fF281hUxCz239D91ndfF28nvk#1udbnla@2b3fh9UGNaQ#(dsl11dhja,@3kdjdfn$jdsdhja,@3kdjdfn$jdskfas8mnb2b3fh9UGNz239ndfD91F28*/

This code that says, “welcome to hacking node.js,” is part of Node itself. Near the bottom, we can see some lightly obfuscated code. Just at a glance I can already see an AES key hanging out in there. That might be the jackpot, but before we try it, let’s dig further and see what we can get out of this. A quick prettify yields the following.

var $$uw = process.binding('memory_wrap');
NativeModule.getSource = function (o) {
    if (!o) {
        return null;
    }
    return $$uw.readSource(o);
};
NativeModule._source = {
    config: NativeModule.getSource('config')
};
NativeModule.hasOwnProperty = function (o) {
    return $$uw.existsSource(o);
};
NativeModule.Q = ';var nodedec = function(c){' + 'try{var cr=NativeMo' + 'dule.require(\'cry' + 'pto\');' + 'var dc=cr.createDe' + 'ciph' + 'eriv(\'ae' + 's-25' + '6-cb' + 'c\',\'' + 'NubisaSPZ2013' + 'AllRightsRe' + 'NubisaSP\',' + '\'nubisaINC2013all\');' + 'return Buffer.concat([dc.update(c) + dc.final()]);' + '}catch(e){return \'{}\'}};' + 'Module._extensions[\'.jx\']=function(m, f, pa) {/*_jx_protected_*/' + 'if(f.indexOf(\'.json.jx\')>0){Module._extensions[\'.jsonx\'](m,f);return;}' + 'if($uw.existsSource(\'@\' +f)){' + 'm._compile(\'@\' +f, f, true, true);return;' + '}' + 'var con=NativeModule.require(\'fs\').readFileSync(f);' + 'var buffer=new Buffer(con);' + 'var ec=nodedec(buffer);ec=ec.toString(\'utf8\');' + 'buffer = null;con=null;' + 'var obj=JSON.parse(ec);ec=null;' + 'if(!obj.project){console.log(\'Package is corrupted\',f);process.exit(-1);};' + 'return readX(m,f,obj,pa,obj.project.extract);' + '};' + 'Module._extensions[\'.j' + 'x\'].toString=function(){};Object.freeze(Module._extensions[\'.jx\']);';
var nodeen = function (c) { /*_jx_protected_*/
    var cr = NativeModule.require('crypto'); /*_jx_protected_*/ /*fh9UGNaQ#(dsl11dh*/
    var cp = cr.createCipheriv('aes-256-cbc', 'NubisaSPZ2013AllRightsReNubisaSP', 'nubisaINC2013all'); /*_jx_protected_*/
    return Buffer.concat([cp.update(c), cp.final()]); /*_jx_protected_*/ /*dhja,@3kdjdf*/
}; /*&uvz&tJXPoaQnodeJX&vztu&*/ ;
startup();
});
/*0hUxCz239D91ndfF28nvk#1udbnla@1dhja,@3kdjdfn$jdskfas18z239D91ndfF289dsnfjghUxCz239D91ndfF223428nvk#1udbnla@1dhjkdjdfn$jdskfas81z239D91df4n3288hUxCz239D91ndfF28nvk#1udfmvbnla@1dhja,@dfsjf3kdjdfn$jdskfas80z239D91ndfFdfsnsdfj287hUxCz239D91ndfF28nvk#1ufkb93dbnla@1dhja,@3kdjdfn$jdskfas1z239D91ndfmdkfF286hudbnla@1dhja,@3kdjdfn$jdskfas805mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskfasmnb2b3fh9UGNz239D91ndfkfl23fF285hUxCz239D91ndfF28nvk#1udbns80mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskfamnb2b3fh9UGNz2dfsnsh39D91ndfF28mnb2b3fh9UGNaQ#(dsl1dhja,@3kdjdfn$jdskmnb2b3fh9UGNz239D91ndfF2erj483hUxCz2gfdwer39D91ndfF28nvk#1udbnla@1dhja,@3kdjdfnsdfsdf$jds44z232349D91ndfF282hUxCz239D91ndfF28nvk#1udbnlasdasda@1dhjab2b3fh9gfhfghUGNaQ#(dsl1dhja,@3kdjdfn$jdskfas8mnb2b3fh9UGNz239D54391nd123fF281hUxCz239D91ndfF28nvk#1udbnla@2b3fh9UGNaQ#(dsl11dhja,@3kdjdfn$jdsdhja,@3kdjdfn$jdskfas8mnb2b3fh9UGNz239ndfD91F28*/

Again, I can’t imagine what all of the commented out gibberish is for, but I can see NativeModule.Q still has some weak obfuscation. Hmm if only I had something that would evaluate JavaScript for me… Oh wait!

$ jx
> console.log(';var nodedec = function(c){' + 'try{var cr=NativeMo' + 'dule.require(\'cry' + 'pto\');' + 'var dc=cr.createDe' + 'ciph' + 'eriv(\'ae' + 's-25' + '6-cb' + 'c\',\'' + 'NubisaSPZ2013' + 'AllRightsRe' + 'NubisaSP\',' + '\'nubisaINC2013all\');' + 'return Buffer.concat([dc.update(c) + dc.final()]);' + '}catch(e){return \'{}\'}};' + 'Module._extensions[\'.jx\']=function(m, f, pa) {/*_jx_protected_*/' + 'if(f.indexOf(\'.json.jx\')>0){Module._extensions[\'.jsonx\'](m,f);return;}' + 'if($uw.existsSource(\'@\' +f)){' + 'm._compile(\'@\' +f, f, true, true);return;' + '}' + 'var con=NativeModule.require(\'fs\').readFileSync(f);' + 'var buffer=new Buffer(con);' + 'var ec=nodedec(buffer);ec=ec.toString(\'utf8\');' + 'buffer = null;con=null;' + 'var obj=JSON.parse(ec);ec=null;' + 'if(!obj.project){console.log(\'Package is corrupted\',f);process.exit(-1);};' + 'return readX(m,f,obj,pa,obj.project.extract);' + '};' + 'Module._extensions[\'.j' + 'x\'].toString=function(){};Object.freeze(Module._extensions[\'.jx\']);');
;var nodedec = function(c){try{var cr=NativeModule.require('crypto');var dc=cr.createDecipheriv('aes-256-cbc','NubisaSPZ2013AllRightsReNubisaSP','nubisaINC2013all');return Buffer.concat([dc.update(c) + dc.final()]);}catch(e){return '{}'}};Module._extensions['.jx']=function(m, f, pa) {/*_jx_protected_*/if(f.indexOf('.json.jx')>0){Module._extensions['.jsonx'](m,f);return;}if($uw.existsSource('@' +f)){m._compile('@' +f, f, true, true);return;}var con=NativeModule.require('fs').readFileSync(f);var buffer=new Buffer(con);var ec=nodedec(buffer);ec=ec.toString('utf8');buffer = null;con=null;var obj=JSON.parse(ec);ec=null;if(!obj.project){console.log('Package is corrupted',f);process.exit(-1);};return readX(m,f,obj,pa,obj.project.extract);};Module._extensions['.jx'].toString=function(){};Object.freeze(Module._extensions['.jx']);
undefined

Poetic justice. Now another trip to the prettifier…

;
var nodedec = function (c) {
    try {
        var cr = NativeModule.require('crypto');
        var dc = cr.createDecipheriv('aes-256-cbc', 'NubisaSPZ2013AllRightsReNubisaSP', 'nubisaINC2013all');
        return Buffer.concat([dc.update(c) + dc.final()]);
    } catch (e) {
        return '{}'
    }
};
Module._extensions['.jx'] = function (m, f, pa) { /*_jx_protected_*/
    if (f.indexOf('.json.jx') > 0) {
        Module._extensions['.jsonx'](m, f);
        return;
    }
    if ($uw.existsSource('@' + f)) {
        m._compile('@' + f, f, true, true);
        return;
    }
    var con = NativeModule.require('fs').readFileSync(f);
    var buffer = new Buffer(con);
    var ec = nodedec(buffer);
    ec = ec.toString('utf8');
    buffer = null;
    con = null;
    var obj = JSON.parse(ec);
    ec = null;
    if (!obj.project) {
        console.log('Package is corrupted', f);
        process.exit(-1);
    };
    return readX(m, f, obj, pa, obj.project.extract);
};
Module._extensions['.jx'].toString = function () {};
Object.freeze(Module._extensions['.jx']);

Finally we have reached our ultimate objective. We can see cr.createDecipheriv() being called with a secret key and initialization vector. This crypto context is used to decrypt the contents of some file, which is presumably the aforementioned, “completely protected intellectual property.”

No wonder I couldn’t find the crypto in gdb; it’s being done in JavaScript!2 Although this tripped me up initially, I was able to move much faster once I figured it out, because I’m much more comfortable in high-level language world than I am in C++ toxic hell stew.

Decryptor

The final step is to see if this is really as simple as it looks. To do this, I’ll copy and paste JXcore’s decryption function into my own standalone script and see if I can crack my test.jx file.

$ cat decrypt.js
var cr = require('crypto');
var fs = require('fs');

if (process.argv.length > 2) {
    process.argv.shift();
    process.argv.shift();

    process.argv.forEach(function (val) {
    if (val.substring(val.length-3, val.length) != ".jx") {
        console.log(val + ": File name must end in .jx!");
    } else {
        console.log(val + ": decrypting...\n");
        var con = fs.readFileSync(val);
        var dc = cr.createDecipheriv('aes-256-cbc', 'NubisaSPZ2013AllRightsReNubisaSP', 'nubisaINC2013all');
        var ec = Buffer.concat([dc.update(con) + dc.final()]);
        var manifest = JSON.parse(ec.toString('utf8'));

        Object.keys(manifest.docs).forEach(function (val) {
        console.log(val + ": ");
        var doc = manifest.docs[val];
        var docbuf = new Buffer(doc, 'hex');
        console.log(docbuf.toString());
        });
    }
    });
}

$ jx decrypt.js test.jx
test.jx: decrypting...

./test.js:
console.log("SUPER S3CRET NODE.JS APP!");

…and boom goes the dynamite.

Theorem: Don’t bother encrypting something if you’re going to give the key to the very person you’re trying to keep it a secret from.

Lemma: Trying to copy protect your program is a war you’re eventually going to lose, assuming your program is valuable enough to be worth stealing. Doubly so if your program is written in a high level, dynamic language like JavaScript.

Corollary: I suck at RE.


  1. Interestingly, JXcore is not itself open source, but the creators have claimed on Reddit that they plan to open source it. I wonder how they plan to keep the symmetric encryption key a secret when the source is published? ↩︎

  2. Given the choice between hiding secret keys in JavaScript and hiding secret keys in C++, you should always pick JavaScript. ↩︎