-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dee6d47
commit b79d462
Showing
3 changed files
with
370 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |