Denos Getting Digged Down 12
My first couple of articles all deal with low-level binary shenanigans. To this day, I think the allure of binary exploitation is very strong. Still, I’ve shifted more towards application and web security in the following years.
In this article, both things come together. Sort of.
We’re looking at the JavaScript runtime Deno
, specifically at its ability to produce stand-alone binaries.
The article acts as a companion piece to Deno Dig, a tool I’ve recently wrote which is able to extract application code and npm packages from said binaries.
It does so on every platform (including a web version) and for every version of Deno
.
But before we get ahead of ourselves…
What is Deno?
Deno is the open-source JavaScript runtime for the modern web.
Ryan Dahl, the creator of Node.js
, announced Deno
in 2018 after talking of 10 things he regrets about the former.
Deno
strives to be secure by default, which is why sensible APIs like file access and networking are all opt-in.
The dangers of running untrusted code should be as minimal as possible.
Other differentiators are built-in features like a code linter, a code formatter, a language server and an extensive standard library.
In essence, Deno
provides a more curated, more secure and generally more cohesive package than Node's
wild west ecosystem.
Oh, it also just released version 2.0.
Deno Compile
Deno
applications can be compiled into stand-alone executables. This is useful for environments where the runtime is not available.
Up until recently, it worked by appending the application code to a binary and extracting it at runtime. Akin to what we did with our Linux.Doomsday
virus.
A special executable called denort
(“Deno runtime”) is used as the host for the application code. It is a stripped-down version of the normal Deno
executable (no linter etc). Versions for a number of operating systems/architectures exist and are part of every new release (see assets).
Those different versions allow for cross compilation: Executables for foreign targets are simply fetched and the application code is added. No actual compilation step needed.
The feature was introduced in version 1.6.0
and has gone through a few iterations since then:
>=1.6.0 <1.7.0
: Bundle appended to the Deno binary>=1.7.0 <1.33.3
: Metadata + bundle appended to the Deno binary>=1.33.3 <1.46
: eszip appended to the Deno binary (introduction of npm package support)>
1.46=: eszip included in an object file section of the Deno binary (needed for code signing)
Node.js
has a similar feature, but it’s experimental and you need a PhD in order to use it.
Handling Appended Data (< Deno 1.46)
Extracting an appended bundle is pretty simple. We’re going to look at an example for version 1.6.0
, but the process is mostly the same for the other ones.
The last 16 bytes of a stand-alone binary have the following format:
| Magic (8) | Bundle Offset (8) |
The first eight magical bytes are always d3n0l4nd
. The following eight bytes are the starting position of the bundled application data. Those bytes can be taken as is, meaning they are big-endian
.
Let’s manually extract a bundle for clarification:
|
|
We extract the magical delimiter and bundle offset pointer (0x1f6a600
) in line four. Afterwards, we use dd
to extract the bundle. Because we extract everything to the end of the file, delimiter and offset pointer are also present in line 28.
Handling Injected Data (>= Deno 1.46)
Simply appending the data has a couple of drawbacks, but the main one is missing support for code signing on Winows
and macOS
.
Because of this, the implementation nowadays doesn’t append the data but injects it into proper sections of the object file. A discussion can be found in the pull request.
Under the hood it uses sui, an injection tool now part of Deno
.
Different object file formats need different handling:
ELF
: Appended to the end of the binary (like before)Mach-O
: A new section namedd3n0l4nd
is createdPE
: A newRC DATA
resource is created in the.pedata
section
Thankfully, the object crate provides us with mostly straightforward means of extracting the data.
PE
files, however, deserve some additional remarks: Resources are usually part of the .rsrc section. It took me a while to realize that in Deno's
case, a custom .pedata
section is used.
Probably because of this dependency.
Let’s have a look at a Deno
executable with PE-bear
:
As we can see, .pedata
contains the D3N0L4ND
Resource Directory Entry
. That entry contains a table which points to a Resource Data Description
.
It contains an offset (calculated from the start of the section) and a total size.
With that information, we can finally extract the blob we’re interested in.
Well, not quite yet! The offset is actually a relative virtual address (RVA
), which means we have to translate it into a file offset.
Apart from the RVA
and size of our blob, we also need two additional pieces of information:
RVA
of the containing section (.pedata
)- pointer to the raw section data
Afterwards we’re finally able to calculate the start of our blob (pinky swear!) by applying the following formular:
(RVA of the resource) - (virtual address of .pedata) + (raw data pointer of .pedata)
But what exactly are we extracting anyway?
¿Qué es un eszip?
A compact file format to losslessly serialize an ECMAScript module graph into a single file
In other words: Eszip is the homebrew format used for storing our loot and a lot of metadata surrounding it.
Here’s the file format taken from their repository:
| Magic (8) | Header size (4) | Header (n) | Header hash (32) | Sources size (4) | Sources (n) | SourceMaps size (4) | SourceMaps (n) |
Header:
( | Specifier size (4) | Specifier (n) | Entry type (1) | Entry (n) | )*
Entry (redirect):
| Specifier size (4) | Specifier (n) |
Entry (module):
| Source offset (4) | Source size (4) | SourceMap offset (4) | SourceMap size (4) | Module type (1) |
Sources:
( | Source (n) | Hash (32) | )*
SourceMaps:
( | SourceMap (n) | Hash (32) | )*
Because the online eszip-viewer doesn’t appear to work at the time of writing, we need to use the CLI version.
Let’s create an eszip file first:
|
|
Now we can look at the contents in a human friendly way:
$ cargo run --example eszip_viewer cow.eszip2 > cow.txt
The output is quite long, so I’ll only show the section for the assertion_error.ts
module (line 14 above).
|
|
Cool.
¿Qué son los metadatos?
Apart from the ezip, there’s also some structured metadata contained in the binary. It holds information about permissions, arguments and general configuration:
|
|
In this case, there’s not much to see. But we can spot the unstable kv
feature from my guinea pig project’s README.
¿Qué es un sistema de archivos virtual?
Lastly, let’s have a quick look at how npm
packages are serialized. A virtual file system (vfs) is used to represent the node_modules
folder.
That vfs uses JSON
and looks like this:
|
|
The original file has 28690 lines after prettyfication!
As you’d expect, the vfs supports three different types of nodes, Directories
, Files
and Symlinks
. Files have a name, a length and an offset.
¿Qué?
Enough theory! What problem does the Deno Dig
tool solve? What’s the elevator pitch?
- LEVEL THE PLAYING FIELD IN TODAY’S WORLD OF CYBER-KINETIC CAPABILITIES
- ZERO TRUST EXTRACTION OF APPLICATION CODE AND NPM PACKAGES
- HANDLES EVERY PERMUTATION IN THE IMPLEMENTATION SPACE OF
deno compile
- RUNNABLE ON THE EDGE THANKS TO WASM VERSION
- TRUE CROSS-PLATFORM (WANT TO USE THE MACH-O BUILD TO HANDLE PE FILES? YOU CAN!)
Here’s a demo video showing the command line version in action:
Pretty fast, right? That’s because it’s written in Rus… HOLD ON! There’s a reason.
Besides borrowing some structs from Deno
itself, we can also make use of the eszip
crate. This way, we don’t have to re-invent parsing and are future-proof should changes in their format arise.
If we already use eszip
, why don’t we also use the sui
crate? Great question!
Like always, PE
files are to blame. Sui
is designed to extract sections from the binary it’s part of. In case of PE
files, it’s assumed that those run under Windows.
Fair enough.
But based on that assumption, they make use of platform specific APIs.
That’s why we use the object
crate instead: It allows for extraction on all platforms.
Deno Dig
versions for many platforms exist, but why not simply check out the web version?
I hope people find a use for the tool. If something’s missing or not working, please let me know directly or via GitHub
issues.