Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
blue-devil committed Dec 27, 2022
1 parent dee6d47 commit b79d462
Show file tree
Hide file tree
Showing 3 changed files with 370 additions and 5 deletions.
6 changes: 3 additions & 3 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
nexeruncator - exports/inserts javascript from nexe-compiled binaries
Copyright (C) 2022 BlueDeviL // SCT <bluedevil.SCT@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand All @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

<program> Copyright (C) <year> <name of author>
nexeruncator Copyright (C) 2022 BlueDeviL // SCT <bluedevil.SCT@gmail.com>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
Expand Down
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,69 @@
# nexeruncator
extract/insert javascript source from/to nexe compiled binaries
<div align="center"><img src="https://user-images.githubusercontent.com/10853207/209234630-b29fbaaa-536b-4899-8eda-3a42a2d73023.png" width=300></div>

<p align="center"></p>

<div align="center">
<img src="https://img.shields.io/badge/license-GPLv3-green">
<img src="https://img.shields.io/badge/Go-v1.19-00ADD8">
</div>

# neXeruncator

nexeruncator; extracts or inserts javascript source file from or to nexe-compiled binaries.

* The word `nexeruncator` is derived from `nexe` and `aberuncate`. Nothing special!

## Notes

From a reverse engineer's point of view, the nexe-compiled application consists of 4 main parts:

* NodeJS part, which contains NodeJS runtime and big in size, ~55mb
* Initializer javascript code.
* User's javacript code.
* 32 byte footer part.

Initializer javascript part contains name of user's javascript source filename. Let's say we have a javascript file "awesome.js". That means "awesome.js" string occurs two times in initializer part.

This is how the nexe-compiled binary ends:

```txt
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
035E3820 3C 6E 65 78 65 7E 7E 73 65 6E 74 69 6E 65 <nexe~~sentine
035E3830 6C 3E 00 00 00 00 00 FE CE 40 00 00 00 00 00 00 l>.....þÎ@......
035E3840 43 40 C@
```

The above sniplet is 32 bytes. The first 16 byte is the string literal `<nexe~~sentinel>`. The second 16 byte is actually comprise of 2 8 byte parts. And they are in little endian. Let's check [Nexe][gh-nexe]'s sources: `nexe/src
/
compiler.ts`

```ts
<snip>
const code = this.code(),
codeSize = Buffer.byteLength(code),
lengths = Buffer.from(Array(16))

lengths.writeDoubleLE(codeSize, 0)
lengths.writeDoubleLE(this.bundle.size, 8)
return new (MultiStream as any)([
binary,
toStream(code),
this.bundle.toStream(),
toStream(Buffer.concat([Buffer.from('<nexe~~sentinel>'), lengths])),
])
}
}
```

The first 8 byte is `codeSize` which is what I called jsInit. It is a kind og initializer javascript code just before user's javascript code and finished with double semi-coloumns. And the second 8 byte is size of user's appended javascript source file.

## Resources

* [Github - Nexe][gh-nexe]

## License

This project is under GPLv3 license.

[gh-nexe]: https://github.com/nexe/nexe/
298 changes: 298 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
// nexeruncator - exports/inserts javascript from nexe-compiled binaries
// Copyright (C) 2022 BlueDeviL // SCT <bluedevil.SCT@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package main

import (
"bytes"
"flag"
"fmt"
"math"
"os"
"strconv"
)

var Version = "0.1.0"
var AppName = "nexeruncator"

var MagicBytes = []byte{
0x21, 0x28, 0x66, 0x75, 0x6E, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28,
0x29, 0x20, 0x7B, 0x70, 0x72, 0x6F, 0x63, 0x65, 0x73, 0x73, 0x2E, 0x5F,
0x5F, 0x6E, 0x65, 0x78, 0x65, 0x20, 0x3D, 0x20, 0x7B, 0x22, 0x72, 0x65,
0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x3A, 0x7B, 0x22, 0x2E,
0x2F,
}

func main() {
// define flags
helpFlag := flag.Bool("h", false, "display help message")
extractFlag := flag.Bool("e", false, "extract script from nexe-compiled binary")
insertFlag := flag.Bool("i", false, "insert script into nexe-compiled binary")
verFlag := flag.Bool("v", false, "print version")

// parse flags
flag.Parse()

// check for version flag
if *verFlag {
fmt.Println(AppName + " " + Version)
os.Exit(0)
}

// check for help flag
if *helpFlag {
fmt.Println("nexeruncator - js extractor/inserter from&to nexe compiled binaries")
fmt.Println("Usage: nexeruncator [options]")
fmt.Println("Options:")
flag.PrintDefaults()
os.Exit(0)
}

// check for extract flag
if *extractFlag {
// check if there are enough arguments
if flag.NArg() < 1 {
fmt.Println("[-] Error: missing binary file argument")
os.Exit(1)
}
// extract script from binary file
extract(flag.Arg(0))
os.Exit(0)
}

// check for insert flag
if *insertFlag {
// check if there are enough arguments
if flag.NArg() < 2 {
fmt.Println("[-] Error: missing script and binary file arguments")
os.Exit(1)
}
// insert script into binary file
insert(flag.Arg(0), flag.Arg(1))
os.Exit(0)
}

// no flags specified, display error message
fmt.Println("[-] Error: no options specified")
os.Exit(1)
}

func getFilename(slice []byte) string {
// search for MagicBytes in slice
index := bytes.Index(slice, MagicBytes)
if index == -1 {
// MagicBytes not found
return ""
}
// MagicBytes found, create new slice starting at index + len(MagicBytes)
filenameSlice := slice[index+len(MagicBytes):]
// search for double quote in filenameSlice
index = bytes.IndexByte(filenameSlice, '"')
if index == -1 {
// double quote not found
return ""
}
// double quote found, create new slice from start to index
filenameSlice = filenameSlice[:index]
// convert slice to string and return
return string(filenameSlice)
}

func extract(filename string) {
// read file into nexeFile slice
nexeFile, err := os.ReadFile(filename)
if err != nil {
fmt.Printf("[-] Error reading file: %s\n", err)
os.Exit(1)
}
// search for MagicBytes in nexeFile
index := bytes.Index(nexeFile, MagicBytes)
if index == -1 {
fmt.Println("[-] Are you sure if this is a nexe compiled binary?")
os.Exit(1)
}
// MagicBytes found, store index in nexeStart
nexeStart := index
// create jsWhole slice from nexeStart to end of nexeFile
jsWhole := nexeFile[nexeStart:]
// get filename from jsWhole slice
jsFilename := getFilename(jsWhole)
if jsFilename == "" {
fmt.Println("[-] Cannot get filename...")
os.Exit(1)
}
// search for ";;" in jsWhole slice
index = bytes.Index(jsWhole, []byte(";;"))
if index == -1 {
fmt.Println("[-] Error: cannot find ;;")
os.Exit(1)
}

// ";;" found, create script2 slice from index + 2 to end of jsWhole
script2 := jsWhole[index+2:]
// search for <nexe~~sentinel> in script2 slice
index = bytes.Index(script2, []byte("<nexe~~sentinel>"))
if index == -1 {
fmt.Println("[-] Cannot find <nexe~~sentinel>...")
os.Exit(1)
}
// <nexe~~sentinel> found, create finalScript slice from start of script2 to index
finalScript := script2[:index]
// save finalScript slice as binary file with jsFilename
err = os.WriteFile(jsFilename, finalScript, 0644)
if err != nil {
fmt.Printf("[-] Error saving file: %s\n", err)
os.Exit(1)
}
fmt.Printf("[+] Extracted script saved as %s\n", jsFilename)
}

func setFilenameAndSize(slice []byte, fileName string, size uint64) ([]byte, uint64) {
magicBytes := []byte{0x3A, 0x7B, 0x22, 0x2E, 0x2F}
// search for magicBytes in slice
index := bytes.Index(slice, magicBytes)
if index == -1 {
// magicBytes not found
return nil, 0
}
// magicBytes found, create retVal slice starting at index + len(magicBytes)
retVal := make([]byte, 16000)
copy(retVal, slice[:index+len(magicBytes)])
// convert fileName to slice and append to retVal
retVal = append(retVal, []byte(fileName)...)
// append [0x22, 0x3A, 0x5B, 0x30, 0x2C] to retVal
retVal = append(retVal, []byte{0x22, 0x3A, 0x5B, 0x30, 0x2C}...)
// convert size to string and then to slice, append to retVal
sizeStr := strconv.FormatUint(uint64(size), 10)
retVal = append(retVal, []byte(sizeStr)...)
// search for [0x5D, 0x7D, 0x7D, 0x3B, 0x0A, 0x7D, 0x29] in slice
index = bytes.Index(slice, []byte{0x5D, 0x7D, 0x7D, 0x3B, 0x0A, 0x7D, 0x29})
if index == -1 {
// [0x5D, 0x7D, 0x7D, 0x3B, 0x0A, 0x7D, 0x29] not found
return nil, 0
}

// search 0x65, 0x78, 0x65, 0x63, 0x50, 0x61, 0x74, 0x68, 0x29, 0x2C, 0x22, 0x2E, 0x2F
index2 := bytes.Index(slice, []byte{0x65, 0x78, 0x65, 0x63, 0x50, 0x61, 0x74, 0x68, 0x29, 0x2C, 0x22, 0x2E, 0x2F})
if index2 == -1 {
// [0x65, 0x78, 0x65, 0x63, 0x50, 0x61, 0x74, 0x68, 0x29, 0x2C, 0x22, 0x2E, 0x2F] not found
return nil, 0
}

// [0x5D, 0x7D, 0x7D, 0x3B, 0x0A, 0x7D, 0x29] found, append from index to end of slice to retVal
temp := slice[index:(index2 + 13)]
retVal = append(retVal, temp...)
// convert fileName to slice and append to retVal
retVal = append(retVal, []byte(fileName)...)

// search 0x22, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70
index2 = bytes.Index(slice, []byte{0x22, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70})
if index2 == -1 {
// [0x65, 0x78, 0x65, 0x63, 0x50, 0x61, 0x74, 0x68, 0x29, 0x2C, 0x22, 0x2E, 0x2F] not found
return nil, 0
}
retVal = append(retVal, slice[index2:]...)

// return retVal and its size
return retVal, uint64(len(retVal))
}

func insert(nexeFile string, scriptFile string) {
// read script and nexe files as slices of bytes
jsscript, err := os.ReadFile(scriptFile)
if err != nil {
fmt.Printf("[-] Error reading script file: %s\n", err)
os.Exit(1)
}
wholeProgram, err := os.ReadFile(nexeFile)
if err != nil {
fmt.Printf("[-] Error reading nexe file: %s\n", err)
os.Exit(1)
}
// store script file size and name
lenScript := uint64(len(jsscript))
nameScript := scriptFile

// create engine slice from start of wholeProgram to magicBytes
index := bytes.Index(wholeProgram, MagicBytes)
if index == -1 {
fmt.Println("[-] Are you sure if this is a nexe compiled binary?")
os.Exit(1)
}
engine := wholeProgram[:index]

// create jsWhole slice from index to end of wholeProgram
jsWhole := wholeProgram[index:]
// search for ";;" in jsWhole
index = bytes.Index(jsWhole, []byte(";;"))
if index == -1 {
fmt.Println("[-] Error: cannot find ;;")
os.Exit(1)
}
// ";;" found, create jsInit slice from start of jsWhole to index + 2
jsInit := jsWhole[:index+2]
// store size of jsInit
oldCodeSize := uint64(len(jsInit))
fmt.Println(oldCodeSize)
// call setFilenameAndSize with jsInit, nameScript, and lenScript
jsInitFin, codeSize := setFilenameAndSize(jsInit, nameScript, lenScript)

// convert lenScript and codeSize to float64 and make 8-byte slices
lenScriptF64 := math.Float64bits(float64(lenScript))
lenScriptBytes := []byte{
byte(lenScriptF64 >> 56),
byte(lenScriptF64 >> 48),
byte(lenScriptF64 >> 40),
byte(lenScriptF64 >> 32),
byte(lenScriptF64 >> 24),
byte(lenScriptF64 >> 16),
byte(lenScriptF64 >> 8),
byte(lenScriptF64),
}
// reverse and append lenScriptBytes and codeSizeBytes to create footer
codeSizeF64 := math.Float64bits(float64(codeSize))
codeSizeBytes := []byte{
byte(codeSizeF64 >> 56),
byte(codeSizeF64 >> 48),
byte(codeSizeF64 >> 40),
byte(codeSizeF64 >> 32),
byte(codeSizeF64 >> 24),
byte(codeSizeF64 >> 16),
byte(codeSizeF64 >> 8),
byte(codeSizeF64),
}
for i, j := 0, len(lenScriptBytes)-1; i < j; i, j = i+1, j-1 {
lenScriptBytes[i], lenScriptBytes[j] = lenScriptBytes[j], lenScriptBytes[i]
}
for i, j := 0, len(codeSizeBytes)-1; i < j; i, j = i+1, j-1 {
codeSizeBytes[i], codeSizeBytes[j] = codeSizeBytes[j], codeSizeBytes[i]
}
footer := []byte("<nexe~~sentinel>")
footer = append(footer, codeSizeBytes...)
footer = append(footer, lenScriptBytes...)

// create patched slice by concatenating engine, jsInitFin, jsscript, and footer
patched := append(engine, jsInitFin...)
patched = append(patched, jsscript...)
patched = append(patched, footer...)

// write patched slice to patched.exe file
err = os.WriteFile("patched.exe", patched, 0644)
if err != nil {
fmt.Printf("[-] Error writing patched file: %s\n", err)
os.Exit(1)
}
}

0 comments on commit b79d462

Please sign in to comment.