Contents

Cracking JXcore… Again

In a previous article, I investigated the security claims of a product called JXcore. That has turned out to be one of the most popular (of the relatively few) articles on my blog. Not long after I posted it, I was informed that JXcore had fixed the security flaws that I pointed out. Taking them at their word, I updated that article with a note about this claim, but I never actually investigated the claim to see if it is true.

Marketing

Today, I revisit JXcore to see how they have improved their security in the past ~12 months. The current version is 0.3 and they have released their code under the MIT license (which makes reverse engineering much easier). I’ll be looking at commit aa2ce0, if you want to follow along.

My primary motivation for the original article was to teach some coworkers. A developer on my team wanted to use JXcore as a copy protection mechanism for a project we were working on, and when I read the JXcore website (at that time), I was appalled by the aggressive (and completely inaccurate) security claims they were making. So before we analyze the new product, let’s quickly look at the current security claims they are making.

Their marketing site still makes the same, bold claim as it did when I last analyzed their software:

Marketing for JXcore

This page is at least partially outdated, because the link in the top right says “Download BETA-2”, but JXcore is currently on release 0.3. Still, this page is published on their site (currently at http://jxcore.com/jxp/, if they haven’t taken it down since this was published) and it is one of the top results when searching Google for “jxcore security”. It’s not clear to me if they are still asserting the same security claim or if they left this page up by accident.

Update on 2015-06-25
Ugur Kadakal contacted me on LinkedIn to let me know that the page above was in fact an accident and was not meant to be part of their current site. They have removed the “complete protection” claim from that page. That page now says that JXcore does not “protect or claim to protect source codes,” but the technical documentation linked to below still contains a section called “Packaging & Code Protection”.

Their technical documentation makes a much more qualified and reasonable security claim:

JXcore

JXcore introduces a unique feature for packaging of source files and other assets into JX packages.

Let’s assume you have a large project consisting of many files. This feature packs them all into a single file to simplify the distribution. It also protects your server side JavaScript code by keeping all source files inside a package, which makes them more difficult to reach.

Native Executable

This is actually a neat feature from the point of view of deploying software: you can deploy a node.js project as a native executable and it can contain third party modules as well as your own source code. For example, you can deploy a real ELF binary to a Linux server, which is the use case that I will be assessing in this post. What’s more, you can produce such binaries without even having a compiler installed!

$ cat test.js
console.log('My s3cret app!');

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

preparing the JX file..
Compiling TestApp 1.0
adding script test.js
..............
[OK] compiled file is ready (./TestApp)

$ file TestApp
TestApp: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=eadc7b0957496ba6a86c5630567327f82e1c251e, not stripped

$ ./TestApp
My s3cret app!

How does this black magic work? JXcore uses an ugly-but-beautiful hack: TestApp is just a copy of the jx binary with a few bytes changed and the contents of my source code compressed and appended to the end of the executable. Here is a hex dump of the end of the jx binary.

0b705e0: 3132 5f53 4146 4542 4147 005f 5a4e 3276  12_SAFEBAG._ZN2v
0b705f0: 3838 696e 7465 726e 616c 3136 4c46 6f72  88internal16LFor
0b70600: 496e 4361 6368 6541 7272 6179 4431 4576  InCacheArrayD1Ev
0b70610: 00                                       .

Now here is the the same part of TestApp, for comparison:

0b705e0: 3132 5f53 4146 4542 4147 005f 5a4e 3276  12_SAFEBAG._ZN2v
0b705f0: 3838 696e 7465 726e 616c 3136 4c46 6f72  88internal16LFor
0b70600: 496e 4361 6368 6541 7272 6179 4431 4576  InCacheArrayD1Ev
0b70610: 0009 89f3 9a2b 2693 42be 63c7 6998 b6be  .....+&.B.c.i...
0b70620: f984 64c9 959e 1605 e99d f687 316d b443  ..d.........1m.C
0b70630: 5301 7801 7592 5f6f da30 14c5 bf8b b547  S.x.u._o.0.....G

Notice how TestApp has exactly the same bytes from 0xb705e0 through 0xb70610, but then it has some extra bytes on the end. There are about 600 more bytes (not shown here) in TestApp than in jx.

$ ls -la jx TestApp
-rwxrwxr-x 1 mhaase mhaase 11994641 Jun 19 13:59 jx
-rwxrwxr-x 1 mhaase mhaase 11995221 Jun 25 12:17 TestApp

Furthermore, the files are completely identical in the first 7MB.

$ head -c 7000000 ../../jx | md5sum && head -c 7000000 TestApp | md5sum
2320600a3d6791b700a47c1c80fd1693  -
2320600a3d6791b700a47c1c80fd1693  -

So, basically, TestApp is jx with just a couple small changes and some data appended to the end.

The question of the day: is our source code “completely protected” and “difficult to reach”? (Spoiler: “definitely not” and “not really”.)

Reversing

How does jx distinguish when its running as just plain jx or when its running as a packaged application? The answer is found in jxcore.cc.

216
217
218
219
220
221
222
223
std::string original("jxcore.bin(?@@?!$<$?!*)");
if (original.c_str()[14] != '?') {
    main_node_->is_packaged_ = true;
}

if (!main_node_->is_packaged_) {
    for (; i < argc; i++) {
    const char *arg = argv[i];

The is_packaged_ variable indicates that the binary is running a packaged application. Note that if this is false, then jx starts processing command line arguments like package and -native.

This is where things get crazy. You may be looking at this code thinking how could is_packaged_ ever be set to true? The condition original.c_str()[14] != '?' is checking to see if the 15th character of a string is a question mark. But that string is a constant! The 15th character will always be ?, right?

Actually, when jx packages your application, it finds the location of this constant string jxcore.bin(?@@?!$<$?!\*) inside the new binary and changes it (!) to jxcore.bin(?@@!!$<$?!\*). Can you spot the difference? The 15th character in the new string is ! instead of ?.

Now we know how jx decides whether to run in jx mode or to run your packaged application, so the next question is how does it extract your source code from the binary? We already know that the source code is appended to the end of the binary file, but jx needs a way to know how many bytes to read off of the end of the file, and there’s a similarly devious hack for that.

As with the jxcore.bin string, the length of the appended code is stored in an obfuscated string. Here’s what the encoded length looks like in TestApp:

06d0420: 6269 6e40 7279 2e76 4072 7369 6f6e 402a  bin@ry.v@rsion@*
06d0430: 2124 2140 2121 2128 217b 2128 2124 2140  !$!@!!!(!{!(!$!@
06d0440: 217c 295d 3f23 5d28 2340 5d23 235d 3f21  !|)]?#](#@]##]?!
06d0450: 5d5d 2840 2324 5d3c 0000 0000 0000 0000  ]](@#$]<........

I won’t go through the details of reverse engineering the obfuscation, but if you want to see how it works, look at src/node.js:1948-2040 and 2079-2132. Once decoded, TestApp reveals a payload length of 532 bytes. As an added layer of obfuscation, jx adds 16 random-ish bytes to the end of the file, so we actually want to get the last 548 bytes of the binary and then throw away the last 16 bytes. The resulting array of 532 bytes is a compressed blob that can be decompressed with zlib.

Once decompressed, that blob turns out to be a JSON object that looks like the following. (I’ve omitted some irrelevant parts for brevity.)

{
    "project": {
    "name": "TestApp",
    "version": "1.0",
    "author": "",
    "files": [
        "test.js"
    ]
    },
    "docs": {
    ".\/test.js": "AHgBAR8A4P9jb25zb2xlLmxvZygnTXkgczNjcmV0IGFwcCEnKTsKsQQKBA=="
    }
}

The docs object contains the contents of all the embedded source files. Each is compressed and base64 encoded. (I don’t know why they are compressed twice.) I’ll point out here that JXcore has given up on the idea of symmetric encryption, which is a good thing! Symmetric encryption was the entire focal point of my previous analysis of the software, and I am happy to see that they have not opted for the same poor design this time around. However, there is still a major, unnecessary flaw in their design.

Extractor

I’ll illustrate the flaw in a moment, but first let’s cap off with a Python script that automates extraction of source code from “completely protected” and “difficult to reach” native packages:

#!/usr/bin/env python3

import binascii
import base64
import json
import os
import sys
import zlib

def main():
    if len(sys.argv) < 2:
        sys.stderr.write('Usage: {} \n'.format(sys.argv[0]))
        sys.exit(1)

    # Figure out where the code is.
    app = open(sys.argv[1], 'rb')
    magic_word = get_binary_version(app)
    code_size = get_code_size(magic_word)

    # Extract code and decompress.
    app.seek(-code_size -16, os.SEEK_END)
    compressed_code = app.read()
    decompressed_code = zlib.decompress(compressed_code[1:-16])
    del compressed_code

    # Display extracted code.
    project = json.loads(decompressed_code.decode('utf8'))

    for path, code in project['docs'].items():
        print_code(path, code)

def get_binary_version(app):
    app.seek(0)
    buff = b''
    block_size = 4096
    cursor = 0
    magic_word = b'bin@ry.v@rsion@*'

    while True:
        buff = buff[-50:] + app.read(block_size)
        mw_index = buff.find(magic_word)
        if mw_index != -1:
            return buff[mw_index:mw_index+56]

    raise ValueError('Magic word not found in file.')

def get_code_size(magic_word):
    encoded_size = magic_word[14:magic_word.find(b')')]
    hex_size = encoded_size.replace(b'*', b'd') \
                            .replace(b'#', b'0') \
                            .replace(b'$', b'1') \
                            .replace(b'@', b'2') \
                            .replace(b'!', b'3') \
                            .replace(b'(', b'4') \
                            .replace(b'{', b'5') \
                            .replace(b'?', b'6') \
                            .replace(b'<', b'7') \
                            .replace(b'\\', b'8') \
                            .replace(b'|', b'9')
    str_size = binascii.unhexlify(hex_size)

    return (int(str_size, 10) + 123456789) // 5

def print_code(path, code):
    compressed_code = base64.b64decode(code)
    decompressed_code = zlib.decompress(compressed_code[1:])
    print('Decoding file: {}'.format(path))
    print('==============================')
    print(decompressed_code.decode('utf8'))
    print('\n')

if __name__ == '__main__':
    main()

Running this script on TestApp yields the following:

$ python3 crack_jxcore.py TestApp
Decoding file: ./test.js
==============================
console.log('My s3cret app!');

Besides being quite easy to reverse engineer, the central flaw here is that they still don’t obfuscate your source code! It’s sitting there in its original form, ready for easy extraction by anybody that you distribute your application to. I mentioned obfuscation in my previous article on JXcore and I’ll repeat that assertion here: obfuscation is the only reasonable protection to defend high level source code from reverse engineering. Nothing can prevent reverse engineering, but good obfuscation can raise the cost substantially.

I don’t mean to pick on JXcore. They seem to have honed their business model over the last year and the product is sounding more and more interesting. The ability to distribute native packages (without build-essential) is pretty awesome for JavaScript developers who don’t want to fuss with C/C++/gcc/make/etc.

But I have to forcefully reiterate: do not rely on JXcore to protect your source code from copying or reverse engineering! JXcore does neither of those things.