├── .gitmodules ├── .eslintignore ├── emulator ├── ArrayList.js ├── WBEMScriptingSWbemNamedValueSet.js ├── ScriptControl.js ├── WBEMScriptingSWbemDateTime.js ├── WordApplication.js ├── BinaryFormatter.js ├── Enumerator.js ├── MSScriptControlScriptControl.js ├── MemoryStream.js ├── InternetExplorerApplication.js ├── Dictionary.js ├── WindowsInstaller.js ├── FakeFiles.js ├── DOM.js ├── Base64Transform.js ├── AsciiEncoding.js ├── ADODBConnection.js ├── WScriptNetwork.js ├── TextStream.js ├── ShellApplication.js ├── ScheduleService.js ├── XMLHTTP.js ├── ADODBStream.js ├── ADODBRecordSet.js ├── WBEMScriptingSWBEMLocator.js ├── WScriptShell.js └── FileSystemObject.js ├── .travis.yml ├── integrations ├── docker │ └── Dockerfile ├── api │ ├── Dockerfile │ ├── swagger.yaml │ └── api.js ├── README.md └── export │ └── export.js ├── run.js ├── patches ├── eval.js ├── prototype.js ├── catch.js ├── equality_op.js ├── nothis.js ├── except_while_loop.js ├── prototype-plugin.js ├── typeof.js ├── v8-patch.diff ├── this.js ├── counter_while_loop.js └── index_break_statement.js ├── appended-code.js ├── update-docs.js ├── tools └── makeProcList.js ├── require_override.js ├── .eslintrc.js ├── LICENSE ├── test └── test.js ├── equality_rewriter.js ├── argv.js ├── package.json ├── RELEASE_NOTES.txt ├── _run.js ├── patch.js ├── loop_rewriter.js ├── flags.json ├── lib.js └── decoder.c /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | samples/ 2 | *.results 3 | patch.js 4 | -------------------------------------------------------------------------------- /emulator/ArrayList.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function ArrayList() { 4 | 5 | } 6 | 7 | module.exports = lib.proxify(ArrayList, "System.Collections.ArrayList"); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' # First supported version (implements the ES6 features we need) 4 | - 'node' # Latest stable release 5 | script: 6 | - npm run test -------------------------------------------------------------------------------- /emulator/WBEMScriptingSWbemNamedValueSet.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function SWbemNamedValueSet() { 4 | 5 | this.clazz = "SWbemNamedValueSet"; 6 | 7 | this.add = function(name, val) { 8 | }; 9 | } 10 | 11 | module.exports = lib.proxify(SWbemNamedValueSet, "WbemScripting.SWbemNamedValueSet"); 12 | -------------------------------------------------------------------------------- /integrations/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | RUN apk update && apk upgrade 3 | RUN apk add file gcc m4 4 | RUN apk add -U --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing aufs-util 5 | RUN npm install box-js --global --production 6 | WORKDIR /samples 7 | CMD box-js /samples --output-dir=/samples --loglevel=debug 8 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Makes node-box not throw an error 4 | function __eval_hook(arg) { 5 | return arg; 6 | } 7 | 8 | try { 9 | eval("(x, y) => [x, y]"); 10 | } catch (e) { 11 | console.log("You must use a recent version of V8 (at least Node 6.0)."); 12 | process.exit(1); 13 | } 14 | 15 | require("./_run.js"); 16 | -------------------------------------------------------------------------------- /emulator/ScriptControl.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function ScriptControl() { 4 | 5 | this.clazz = "ScriptControl"; 6 | 7 | this.addobject = () => {}, 8 | this.addcode = (code) => lib.logSnippet(lib.getUUID(), { 9 | as: "Code snippet in ScriptControl", 10 | }, code); 11 | } 12 | 13 | module.exports = lib.proxify(ScriptControl, "ScriptControl"); 14 | -------------------------------------------------------------------------------- /emulator/WBEMScriptingSWbemDateTime.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function SWbemDateTime() { 4 | this.value = 0; 5 | this.getvardate = function() { 6 | 7 | // Fake up an answer based on the current date. 8 | // 9 | // DmtfDateTime is of the form "yyyyMMddHHmmss.ffffff+UUU" 10 | return "20250319131005.000000+000" 11 | }; 12 | } 13 | 14 | module.exports = lib.proxify(SWbemDateTime, "WbemScripting.SWbemDateTime"); 15 | -------------------------------------------------------------------------------- /patches/eval.js: -------------------------------------------------------------------------------- 1 | module.exports = (args) => ({ 2 | type: "CallExpression", 3 | callee: { 4 | type: "Identifier", 5 | name: "eval", 6 | }, 7 | arguments: [ 8 | { 9 | type: "CallExpression", 10 | callee: { 11 | type: "Identifier", 12 | name: "rewrite", 13 | }, 14 | arguments: [ 15 | ...args, 16 | { 17 | "type": "Literal", 18 | "value": true, 19 | "raw": "true" 20 | } 21 | ], 22 | }, 23 | ], 24 | }); -------------------------------------------------------------------------------- /emulator/WordApplication.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function WordApplication() { 4 | 5 | this.documents = { 6 | Open : function(path) { 7 | lib.logIOC("Word.Application", path, "The script opened a Word document."); 8 | lib.logUrl('Word Document', path); 9 | }, 10 | }; 11 | 12 | this.quit = function() {}; 13 | } 14 | 15 | module.exports = lib.proxify(WordApplication, "WordApplication"); 16 | -------------------------------------------------------------------------------- /integrations/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | #ENV http_proxy http://PROXY_IP:PORT 3 | #ENV https_proxy http://PROXY_IP:PORT 4 | RUN apk update && apk upgrade 5 | RUN apk add file gcc m4 6 | RUN apk add -U --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing aufs-util 7 | # Install the latest v1 of box-js 8 | RUN npm install box-js@"^1.0.0" --global --production 9 | WORKDIR /samples 10 | CMD box-js /samples --output-dir=/samples --loglevel=debug 11 | -------------------------------------------------------------------------------- /emulator/BinaryFormatter.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function BinaryFormatter() { 4 | 5 | this.deserialize = function (stream) { 6 | var currData = stream; 7 | if (typeof(stream.data) !== "undefined") currData = stream.data; 8 | return data; 9 | } 10 | 11 | this.deserialize_2 = this.deserialize; 12 | } 13 | 14 | module.exports = lib.proxify(BinaryFormatter, "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter"); 15 | -------------------------------------------------------------------------------- /emulator/Enumerator.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | module.exports = class Enumerator { 4 | constructor(array) { 5 | this.array = array; 6 | this.index = 0; 7 | } 8 | get length() { 9 | return this.array.length; 10 | } 11 | atEnd() { 12 | return this.index === this.array.length; 13 | } 14 | item() { 15 | return this.array[this.index]; 16 | } 17 | moveFirst() { 18 | this.index = 0; 19 | } 20 | moveNext() { 21 | this.index++; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /emulator/MSScriptControlScriptControl.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function ScriptControl() { 4 | 5 | this.Language = undefined; 6 | this.Timeout = undefined; 7 | 8 | this.addcode = code => { 9 | lib.info(`Script added dynamic code '''${code}'''`); 10 | lib.logIOC("DynamicCode", {code}, "The script wrote dynamic code with MSScriptControl.ScriptControl ActiveX object."); 11 | return 0; 12 | }; 13 | } 14 | 15 | module.exports = lib.proxify(ScriptControl, "MSScriptControl.ScriptControl"); 16 | -------------------------------------------------------------------------------- /emulator/MemoryStream.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function MemoryStream() { 4 | 5 | this.data = ""; 6 | 7 | this.write = function (data) { 8 | let buff = new Buffer.alloc(data.length); 9 | buff.write(data); 10 | let base64data = buff.toString('base64'); 11 | lib.logIOC("System.IO.MemoryStream", {data, base64data}, "The script wrote data to a memory stream."); 12 | this.data = data; 13 | } 14 | 15 | } 16 | 17 | module.exports = lib.proxify(MemoryStream, "System.IO.MemoryStream"); 18 | -------------------------------------------------------------------------------- /emulator/InternetExplorerApplication.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function InternetExplorerApplication() { 4 | this.navigate = function(url) { 5 | lib.logUrl('IE URL Navigation', url); 6 | lib.logIOC("InternetExplorer.Application", {url: url}, "The script navigated IE to " + url); 7 | } 8 | this.busy = false; 9 | this.readystate = 4; 10 | this.document = { 11 | "documentElement" : { 12 | "outerText" : "_Loaded_IE_Doc_outerText_" 13 | } 14 | }; 15 | } 16 | 17 | module.exports = lib.proxify(InternetExplorerApplication, "InternetExplorerApplication"); 18 | -------------------------------------------------------------------------------- /emulator/Dictionary.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function Dictionary() { 4 | this.dictionary = {}; 5 | /* eslint no-return-assign: 0 */ 6 | // See https://github.com/eslint/eslint/issues/7285 7 | this.add = function(key, value) { 8 | this.dictionary[key] = value; 9 | }; 10 | this.item = (key) => this.dictionary[key]; 11 | this.items = function() { 12 | r = []; 13 | for (var key in this.dictionary){ 14 | r.push(this.dictionary[key]); 15 | }; 16 | return r; 17 | }; 18 | } 19 | 20 | module.exports = lib.proxify(Dictionary, "Scripting.Dictionary"); 21 | -------------------------------------------------------------------------------- /appended-code.js: -------------------------------------------------------------------------------- 1 | 2 | // go through all declared variables in the script looking for valid JavaScript in the contents 3 | // eval the javascript so it gets sandboxed 4 | const vm = require('vm'); 5 | for (varName in this) { 6 | varValue = this[varName] 7 | if (typeof(varValue) == "string") { 8 | // check that the string is valid JS syntax 9 | try { 10 | const script = new vm.Script(varValue); 11 | logJS(varValue) 12 | // Automatically evaling all JS can result in the program state getting polluted. 13 | //eval(varValue) 14 | } 15 | catch (err) {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /emulator/WindowsInstaller.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function WindowsInstaller() { 4 | 5 | this.clazz = "WindowsInstaller"; 6 | 7 | this.installproduct = function(url) { 8 | lib.logIOC("WindowsInstaller", {"url": url}, "The script installed a remote MSI."); 9 | lib.logUrl('Remote MSI Install', url); 10 | }; 11 | 12 | this.openpackage = function(url) { 13 | lib.logIOC("WindowsInstaller", {"url": url}, "The script opened a remote MSI package."); 14 | lib.logUrl('Remote MSI Install', url); 15 | }; 16 | } 17 | 18 | module.exports = lib.proxify(WindowsInstaller, "WindowsInstaller"); 19 | -------------------------------------------------------------------------------- /emulator/FakeFiles.js: -------------------------------------------------------------------------------- 1 | // Load JSON file with faked up file contents. This maps file names to 2 | // file contents. 3 | const fakeFileInfo = require('./FakeFileInfo.json'); 4 | const fs = require('node:fs'); 5 | 6 | // See if we can read some fake file contents. 7 | function ReadFakeFileContents(fname) { 8 | 9 | // See if we can return some real file contents. 10 | try { 11 | const data = fs.readFileSync(fname, 'utf8'); 12 | return data; 13 | } 14 | catch (err) { } 15 | 16 | // No real contents, try fake contents. 17 | return fakeFileInfo[fname]; 18 | } 19 | 20 | exports.ReadFakeFileContents = ReadFakeFileContents; 21 | -------------------------------------------------------------------------------- /patches/prototype.js: -------------------------------------------------------------------------------- 1 | /* 2 | function foo.bar(baz) { ... } becomes foo.bar = function(baz) { ... } 3 | 4 | Additionally marked as a candidate to hoist. 5 | Needs prototype-plugin enabled. 6 | */ 7 | 8 | module.exports = (fexpr) => ({ 9 | type: "ExpressionStatement", 10 | hoist: true, 11 | hoistExpression: fexpr.type == "FunctionExpression", 12 | expression: { 13 | type: "AssignmentExpression", 14 | operator: "=", 15 | left: fexpr.id, 16 | right: { 17 | type: "FunctionExpression", 18 | id: null, 19 | params: fexpr.params, 20 | body: fexpr.body 21 | } 22 | } 23 | }); -------------------------------------------------------------------------------- /emulator/DOM.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function VirtualDOMTag(name) { 4 | this.name = name; 5 | } 6 | 7 | // Catches requests to .nodeTypedValue in order to emulate them correctly 8 | module.exports = function(name) { 9 | return new Proxy(new VirtualDOMTag(name), { 10 | get: function(target, name) { 11 | name = name.toLowerCase(); 12 | switch (name) { 13 | case "nodetypedvalue": 14 | if (target.dataType !== "bin.base64") return target.text; 15 | return new Buffer(target.text, "base64"); 16 | default: 17 | if (name in target) return target[name]; 18 | lib.kill(`VirtualDOMTag.${name} not implemented!`); 19 | } 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /update-docs.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const columnify = require("columnify"); 3 | const flags = JSON.parse(fs.readFileSync("flags.json", "utf8")); 4 | const text = columnify( 5 | require("./argv.js").flags.run.map((flag) => ({ 6 | name: (flag.alias ? `-${flag.alias}, ` : "") + `--${flag.name}`, 7 | description: flag.description, 8 | })), 9 | { 10 | config: { 11 | description: { 12 | maxWidth: 80, 13 | }, 14 | }, 15 | } 16 | ).split("\n").map(line => " " + line).join("\n"); 17 | const README = fs.readFileSync("README.md", "utf8"); 18 | fs.writeFileSync( 19 | "README.md", 20 | README.replace(/(?:.|\r|\n)+/m, `\n${text}\n`) 21 | ); -------------------------------------------------------------------------------- /emulator/Base64Transform.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function Base64Transform() { 4 | 5 | this.clazz = "Base64Transform"; 6 | 7 | this.transformfinalblock = function (bytes, start, count) { 8 | 9 | // Chop out the block of "bytes" to b64 decode. 10 | const chunk = bytes.slice(start, start + count); 11 | 12 | // Convert the "bytes" back to a string. 13 | var str = ""; 14 | for (let i = 0; i < chunk.length; i++) { 15 | str += String.fromCharCode(chunk[i]); 16 | } 17 | const r = Buffer.from(str, 'base64').toString('binary'); 18 | //const r = atob(str); 19 | return r; 20 | } 21 | 22 | } 23 | 24 | module.exports = lib.proxify(Base64Transform, "System.Security.Cryptography.FromBase64Transform"); 25 | -------------------------------------------------------------------------------- /tools/makeProcList.js: -------------------------------------------------------------------------------- 1 | /* run 2 | * 3 | * gwmi -Query "SELECT * FROM Win32_Process" > a.txt 4 | * 5 | * then run this script. 6 | */ 7 | 8 | const fs = require("fs"); 9 | const a = fs.readFileSync("a.txt", "ucs2").replace(/\r\n/g, "\n"); 10 | 11 | const processes = a.split("\n\n"); 12 | const properties = processes.map(proc => proc.split("\n") 13 | .filter(a => a.includes(":")) 14 | .map(a => a.split(":")) 15 | .map(a => a.map(b => b.trim()))); 16 | 17 | const propString = properties 18 | .map(proc => proc.map(([a, b]) => JSON.stringify(a.toLowerCase()) + ": " + JSON.stringify(b)).join(",\n")) 19 | .join("},\n{"); 20 | 21 | const jsonString = "[{\n" + propString + "\n}]"; 22 | 23 | const jsonStringBeau = JSON.stringify(JSON.parse(jsonString), null, 2); 24 | fs.writeFileSync("processes.json", jsonStringBeau); 25 | -------------------------------------------------------------------------------- /require_override.js: -------------------------------------------------------------------------------- 1 | // Use stubbed box-js packages in some cases. Stubbed packages are 2 | // defined in boilerplate.js. 3 | function require(arg) { 4 | 5 | // Override some Node packages with stubbed box-js versions. Add 6 | // any new stubbed packages here so they are loaded via require() 7 | // when sandboxing with box-js. 8 | const overrides = { 9 | "child_process" : { 10 | execSync: _execSync, 11 | spawn: _spawn, 12 | exec: _execSync, 13 | }, 14 | "http" : _http, 15 | "net" : { 16 | createConnection: _createConnection, 17 | Socket: _Socket, 18 | createServer: _createServer, 19 | }, 20 | "request" : { 21 | }, 22 | } 23 | if (typeof overrides[arg] !== "undefined") return overrides[arg]; 24 | return _origRequire(arg); 25 | } 26 | -------------------------------------------------------------------------------- /emulator/AsciiEncoding.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function AsciiEncoding() { 4 | 5 | this.clazz = "AsciiEncoding"; 6 | 7 | this.issinglebyte = true; 8 | 9 | this.getbytecount = function (str, pos=0) { 10 | return str.length - pos; 11 | } 12 | 13 | this.getbytecount_2 = this.getbytecount; 14 | 15 | this.getbytes = function (str, buffer) { 16 | if (typeof(buffer) === "undefined") buffer = []; 17 | for(let i = 0; i < str.length; i++){ 18 | let code = str.charCodeAt(i); 19 | buffer.push(code); 20 | } 21 | return buffer; 22 | } 23 | 24 | this.getbytes_2 = this.getbytes; 25 | this.getbytes_3 = this.getbytes; 26 | this.getbytes_4 = this.getbytes; 27 | } 28 | 29 | module.exports = lib.proxify(AsciiEncoding, "System.Text.ASCIIEncoding"); 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": "google", 4 | "env": { 5 | "node": true 6 | }, 7 | "rules": { 8 | "arrow-parens": 0, 9 | "camelcase": [1, { 10 | properties: "never" 11 | }], 12 | "curly": 0, 13 | "eol-last": 0, 14 | "eqeqeq": 1, 15 | "indent": [2, "tab", {SwitchCase: 1}], 16 | "max-len": 0, 17 | "new-cap": 1, 18 | "no-else-return": 1, 19 | "no-extend-native": 0, 20 | "no-invalid-this": 1, 21 | "no-global-assign": 1, 22 | "no-loop-func": 0, 23 | "no-return-assign": [1, "except-parens"], 24 | "no-tabs": 0, 25 | "no-unused-vars": 1, 26 | "no-var": 1, 27 | "prefer-const": 1, 28 | "quotes": ["error", "double"], 29 | "require-jsdoc": 0 30 | } 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 CapacitorSet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /emulator/ADODBConnection.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const iconv = require("iconv-lite"); 3 | 4 | function ADODBConnection() { 5 | 6 | this.clazz = "ADODBConnection"; 7 | 8 | this.open = function(query, conn) { 9 | log.logIOC("ADODBConnection", {"query": query}, "The script opened an ADODB connection."); 10 | }; 11 | 12 | this.cursorlocation1 = function(arg) { 13 | log.logIOC("ADODBConnection", {"arg": arg}, "The script called ADODB cursor location."); 14 | }; 15 | 16 | this.close = () => {}; 17 | } 18 | 19 | module.exports = function() { 20 | return new Proxy(new ADODBConnection(), { 21 | get: function(target, name) { 22 | name = name.toLowerCase(); 23 | switch (name) { 24 | case "size": 25 | case "length": 26 | return target.buffer.length; 27 | case "readtext": 28 | return target.buffer; 29 | default: 30 | if (name in target) return target[name]; 31 | lib.kill(`ADODBConnection.${name} not implemented!`); 32 | } 33 | }, 34 | set: function(a, b, c) { 35 | b = b.toLowerCase(); 36 | a[b] = c; 37 | return true; 38 | }, 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /emulator/WScriptNetwork.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const argv = require("../argv.js").run; 3 | 4 | var fakeUserDomain = ""; 5 | if (argv["fake-domain"]) { 6 | fakeUserDomain = argv["fake-domain"]; 7 | } 8 | 9 | class DriveInfo { 10 | constructor(driveLetters) { 11 | this._drives = driveLetters; 12 | }; 13 | 14 | Item(i) { return this._drives[i]; }; 15 | 16 | get length() { 17 | return this._drives.length; 18 | }; 19 | }; 20 | 21 | function WScriptNetwork() { 22 | this.computername = "COMPUTER_NAME"; 23 | this.enumprinterconnections = () => [{ 24 | foo: "bar", 25 | }]; 26 | this.userdomain = fakeUserDomain; 27 | this.username = "harvey_danger"; 28 | this.mapnetworkdrive = function(letter, path) { 29 | lib.info(`Script maps network drive ${letter} to path ${path}`); 30 | lib.logUrl("map", ("https:" + path).replace("@", ":").replace(/\\/g, "/")); 31 | }; 32 | this.removenetworkdrive = function(letter) { 33 | lib.info(`Script removes network drive ${letter}`); 34 | }; 35 | this.enumnetworkdrives = function() { 36 | return new DriveInfo(["D:", "E:", "F:"]); 37 | }; 38 | } 39 | 40 | module.exports = lib.proxify(WScriptNetwork, "WScriptNetwork"); 41 | -------------------------------------------------------------------------------- /emulator/TextStream.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | module.exports = class TextStream { 4 | constructor(str) { 5 | this.str = str; 6 | this.pos = 0; 7 | } 8 | 9 | get AtEndOfLine() { 10 | lib.kill("TextStream.AtEndOfLine not implemented!"); 11 | } 12 | get AtEndOfStream() { 13 | return this.pos === this.str.length; 14 | } 15 | get Column() { 16 | lib.kill("TextStream.Column not implemented!"); 17 | } 18 | get Line() { 19 | lib.kill("TextStream.Line not implemented!"); 20 | } 21 | 22 | Close() { 23 | lib.kill("TextStream.Close not implemented!"); 24 | } 25 | Read() { 26 | lib.kill("TextStream.Read not implemented!"); 27 | } 28 | ReadAll() { 29 | return this.str; 30 | } 31 | ReadLine() { 32 | lib.kill("TextStream.ReadLine not implemented!"); 33 | } 34 | Skip() { 35 | lib.kill("TextStream.Skip not implemented!"); 36 | } 37 | SkipLine() { 38 | lib.kill("TextStream.SkipLine not implemented!"); 39 | } 40 | Write() { 41 | lib.kill("TextStream.Write not implemented!"); 42 | } 43 | WriteBlankLines() { 44 | lib.kill("TextStream.WriteBlankLines not implemented!"); 45 | } 46 | WriteLine() { 47 | lib.kill("TextStream.WriteLine not implemented!"); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const assert = require("assert"); 4 | const exec = require("child_process").exec; 5 | const fs = require("fs"); 6 | const tmpDir = require("os").tmpdir(); 7 | const boxDir = `${__dirname}/..`; 8 | const boxCommand = `node ${boxDir}/run.js`; 9 | 10 | describe("package.json", function() { 11 | const source = fs.readFileSync(`${boxDir}/package.json`, "UTF8"); 12 | const config = JSON.parse(source); 13 | it("should include a box-js executable", function() { 14 | assert("bin" in config); 15 | const bin = config.bin; 16 | assert("box-js" in bin); 17 | }); 18 | }); 19 | 20 | describe("run.js", function() { 21 | it("should exist", function() { 22 | assert.doesNotThrow(function() { 23 | fs.accessSync(`${boxDir}/run.js`, fs.F_OK); 24 | }); 25 | }); 26 | it("should display a help text when no files are passed", function(done) { 27 | exec(boxCommand, function(err, stdout) { 28 | assert.strictEqual(err, null); 29 | assert(stdout.includes("Usage:")); 30 | done(); 31 | }); 32 | }); 33 | it("should run on a blank script", function(done) { 34 | const path = `${tmpDir}/blank.js`; 35 | fs.writeFileSync(`${tmpDir}/blank.js`, ""); 36 | exec(`${boxCommand} ${path}`, done); 37 | }); 38 | it("should run on all files in a folder"); 39 | it("should accept several paths"); 40 | }); 41 | 42 | describe("analyze.js", function() { 43 | }); -------------------------------------------------------------------------------- /equality_rewriter.js: -------------------------------------------------------------------------------- 1 | const escodegen = require("escodegen"); 2 | 3 | // Rewrite "a == b" to '((a) == "CURRENT_SCRIPT_IN_FAKED_DIR.js") || 4 | // ((b) == "CURRENT_SCRIPT_IN_FAKED_DIR.js") ? true : a == b'. This 5 | // makes checks against the faked up script file name always evaluate 6 | // to true. 7 | function rewriteScriptCheck(key, val) { 8 | if (!val) return; 9 | 10 | // Binary expression? 11 | if (val.type != "BinaryExpression") return; 12 | 13 | // == check? 14 | if (val.operator != "==") return; 15 | 16 | // Got a == expression. Pull out the left and right hand 17 | // expressions. 18 | const lhs = val.left; 19 | const rhs = val.right; 20 | 21 | // Don't rewrite this if it is already checking for the fake 22 | // box-js script name. 23 | const lhsCode = escodegen.generate(lhs); 24 | const rhsCode = escodegen.generate(rhs); 25 | if ((lhsCode == "'CURRENT_SCRIPT_IN_FAKED_DIR.js'") || 26 | (rhsCode == "'CURRENT_SCRIPT_IN_FAKED_DIR.js'")) return 27 | 28 | //console.log("----"); 29 | //console.log(JSON.stringify(val, null, 2)); 30 | r = require("./patches/equality_op.js")(lhs, rhs); 31 | //console.log("REWRITE EQUALITY!!"); 32 | //console.log(JSON.stringify(r, null, 2)); 33 | //console.log(escodegen.generate(r)); 34 | return r; 35 | } 36 | 37 | module.exports = { 38 | rewriteScriptCheck, 39 | }; 40 | -------------------------------------------------------------------------------- /patches/catch.js: -------------------------------------------------------------------------------- 1 | /* Transforms 2 | * 3 | * try { 4 | * // ... 5 | * } catch (ex) { 6 | * // ... 7 | * } 8 | * 9 | * into 10 | * 11 | * { 12 | * var ex; 13 | * try { 14 | * // ... 15 | * } catch (_ex) { 16 | * ex = _ex; 17 | * } 18 | * } 19 | */ 20 | 21 | module.exports = (args) => ({ 22 | type: "BlockStatement", 23 | body: [ 24 | { 25 | type: "VariableDeclaration", 26 | declarations: [ 27 | { 28 | type: "VariableDeclarator", 29 | id: args.handler.param, 30 | init: null, 31 | }, 32 | ], 33 | kind: "var", 34 | }, 35 | { 36 | autogenerated: true, 37 | type: args.type, 38 | block: args.block, 39 | handler: { 40 | type: "CatchClause", 41 | param: { 42 | type: "Identifier", 43 | name: "_" + args.handler.param.name, 44 | }, 45 | body: { 46 | type: "BlockStatement", 47 | body: [ 48 | { 49 | type: "ExpressionStatement", 50 | expression: { 51 | type: "AssignmentExpression", 52 | operator: "=", 53 | left: args.handler.param, 54 | right: { 55 | type: "Identifier", 56 | name: "_" + args.handler.param.name, 57 | }, 58 | }, 59 | }, 60 | args.handler.body, 61 | ], 62 | }, 63 | }, 64 | finalizer: args.finalizer, 65 | }, 66 | ], 67 | }); -------------------------------------------------------------------------------- /argv.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const commandLineArgs = require("command-line-args"); 4 | 5 | // e.g., --test-foo and -t should match 6 | const isFlag = argument => /^(--|-)[\w-]*$/.test(argument); 7 | 8 | const transformFlag = flag => { 9 | if (flag.type === "String") flag.type = String; 10 | if (flag.type === "Number") flag.type = Number; 11 | if (flag.type === "Boolean") flag.type = Boolean; 12 | return flag; 13 | }; 14 | 15 | const flags = JSON.parse(fs.readFileSync(path.join(__dirname, "flags.json"), "utf8")); 16 | flags.run = flags.run.map(transformFlag); 17 | flags.export = flags.export.map(transformFlag); 18 | 19 | function getArgs(flags) { 20 | const argv = commandLineArgs(flags, { 21 | partial: true, 22 | }); 23 | 24 | if (!argv.loglevel) argv.loglevel = "info"; // The default value handling in the library is buggy 25 | if (argv.loglevel === "verbose") argv.loglevel = "verb"; 26 | if (argv.loglevel === "warning") argv.loglevel = "warn"; 27 | 28 | return argv; 29 | } 30 | 31 | const allFlags = [...flags.run, ...flags.export].reduce((set, item) => { 32 | // Make unique 33 | if (!set.some(it => it.name === item.name)) 34 | return set.concat(item); 35 | return set; 36 | }, []); 37 | const argv = commandLineArgs(allFlags, {partial: true}); 38 | if (argv._unknown != null && argv._unknown.some(isFlag)) { 39 | throw new Error(`Unknown arguments: ${unknownArguments.filter(isFlag)}`); 40 | } 41 | 42 | module.exports = { 43 | flags, 44 | run: getArgs(flags.run), 45 | export: getArgs(flags.export), 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "box-js", 3 | "version": "1.9.27", 4 | "description": "A tool for studying JavaScript malware.", 5 | "dependencies": { 6 | "acorn": ">=8.8.0", 7 | "xmldom": "*", 8 | "columnify": "*", 9 | "command-line-args": "^4.0.7", 10 | "escodegen": "^1.9.1", 11 | "global": "^4.4.0", 12 | "iconv-lite": "^0.4.23", 13 | "jschardet": "^1.6.0", 14 | "jsdom": "^16.2.2", 15 | "limiter": "^1.1.3", 16 | "node-html-parser": "*", 17 | "queue": "^4.4.2", 18 | "sync-request": "^6.0.0", 19 | "system-registry": "^1.0.0", 20 | "uglify-es": "^3.3.9", 21 | "uuid": "^3.2.1", 22 | "vm2": ">=3.9.19", 23 | "walk-sync": "^0.3.2" 24 | }, 25 | "devDependencies": { 26 | "babel-eslint": "^8.2.3", 27 | "eslint": "^8.21.0", 28 | "eslint-config-google": "*", 29 | "mocha": "^10.0.0" 30 | }, 31 | "engines": { 32 | "node": ">=6.0.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/CapacitorSet/box-js.git" 37 | }, 38 | "author": "CapacitorSet", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/CapacitorSet/box-js/issues" 42 | }, 43 | "homepage": "https://github.com/CapacitorSet/box-js#readme", 44 | "bin": { 45 | "box-js": "run.js", 46 | "box-export": "integrations/export/export.js" 47 | }, 48 | "scripts": { 49 | "lint": "eslint run.js analyze.js test/ emulator/", 50 | "test": "mocha test/" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /emulator/ShellApplication.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | function ShellApplication(name) { 4 | this.shellexecute = (file, args = "", dir = "") => { 5 | if (dir == null) dir = ""; 6 | if (args == null) args = ""; 7 | lib.runShellCommand(dir + file + " " + args); 8 | }; 9 | const shellExecFuncRef = this.shellexecute; 10 | 11 | this.namespace = (folder) => { 12 | const folders = { 13 | 7: "C:\\Users\\MyUsername\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp", 14 | }; 15 | 16 | if (!(folder in folders)) 17 | throw new Error(`Unknown ShellApplication.Namespace ${folder}`); 18 | 19 | return { 20 | Self: { 21 | Path: folders[folder], 22 | }, 23 | }; 24 | }; 25 | 26 | this.windows = function () { 27 | return { 28 | __name: "ShellApplication.Windows()", 29 | Count: 5, 30 | Item: function() { 31 | //Forkontore.Item(0).Document.Application.ShellExecute(Lacto,String.fromCharCode(34)+Legestue+String.fromCharCode(34),"","open",0); 32 | return { 33 | __name: "ShellApplication.Windows().Item()", 34 | Document: { 35 | Application: { 36 | ShellExecute: shellExecFuncRef, 37 | //function (file, args, dir) { 38 | // console.log("BLAH"); 39 | //}, 40 | } 41 | } 42 | }; 43 | } 44 | }; 45 | }; 46 | } 47 | 48 | module.exports = lib.proxify(ShellApplication, "ShellApplication"); 49 | -------------------------------------------------------------------------------- /patches/equality_op.js: -------------------------------------------------------------------------------- 1 | function MakeBinaryExpression(lhs, rhs, op) { 2 | // {"type":"BinaryExpression","start":30,"end":35,"left":{"type":"Identifier","start":30,"end":31,"name":"a"},"operator":"+","right":{"type":"Literal","start":34,"end":35,"value":1,"raw":"1"}} 3 | return { 4 | type: "BinaryExpression", 5 | left: lhs, 6 | right: rhs, 7 | operator: op 8 | }; 9 | }; 10 | 11 | function MakeLiteral(value) { 12 | return { 13 | type: "Literal", 14 | value: value 15 | }; 16 | }; 17 | 18 | function MakeIfThen(test, body) { 19 | return { 20 | type: "IfStatement", 21 | test: test, 22 | consequent: body 23 | }; 24 | }; 25 | 26 | // a == b 27 | // to 28 | // ((a) == "CURRENT_SCRIPT_IN_FAKED_DIR.js") || ((b) == "CURRENT_SCRIPT_IN_FAKED_DIR.js") ? true : a == b 29 | function GenScriptCheck(lhs, rhs) { 30 | 31 | // Check for box-js fake script name. 32 | const scriptName = MakeLiteral("CURRENT_SCRIPT_IN_FAKED_DIR.js"); 33 | const lhsScriptCheck = MakeBinaryExpression(lhs, scriptName, "=="); 34 | const rhsScriptCheck = MakeBinaryExpression(rhs, scriptName, "=="); 35 | const scriptCheck = MakeBinaryExpression(lhsScriptCheck, rhsScriptCheck, "||"); 36 | 37 | // Recreate original equality check. 38 | const origCheck = MakeBinaryExpression(lhs, rhs, "=="); 39 | 40 | // Make the ternary operator. 41 | const r = { 42 | "autogenerated": true, 43 | type: "ConditionalExpression", 44 | test: scriptCheck, 45 | consequent: MakeLiteral(true), 46 | alternate: origCheck 47 | } 48 | return r; 49 | }; 50 | 51 | module.exports = (lhs, rhs) => (GenScriptCheck(lhs, rhs)); 52 | -------------------------------------------------------------------------------- /patches/nothis.js: -------------------------------------------------------------------------------- 1 | // foo(bar, baz) becomes (x => eval == x ? arg => eval(logJS(arg)) : x)(foo)(bar, baz) 2 | module.exports = (foo) => ({ 3 | "autogenerated": true, 4 | "type": "CallExpression", 5 | "callee": { 6 | "type": "ArrowFunctionExpression", 7 | "id": null, 8 | "params": [ 9 | { 10 | "type": "Identifier", 11 | "name": "x", 12 | }, 13 | ], 14 | "defaults": [], 15 | "body": { 16 | "type": "ConditionalExpression", 17 | "test": { 18 | "type": "BinaryExpression", 19 | "operator": "==", 20 | "left": { 21 | "type": "Identifier", 22 | "name": "eval", 23 | }, 24 | "right": { 25 | "type": "Identifier", 26 | "name": "x", 27 | }, 28 | }, 29 | "consequent": { 30 | "type": "ArrowFunctionExpression", 31 | "id": null, 32 | "params": [ 33 | { 34 | "type": "Identifier", 35 | "name": "arg", 36 | }, 37 | ], 38 | "defaults": [], 39 | "body": { 40 | "type": "CallExpression", 41 | "callee": { 42 | "type": "MemberExpression", 43 | "computed": false, 44 | "object": { 45 | "type": "Identifier", 46 | "name": "eval", 47 | }, 48 | "property": { 49 | "type": "Identifier", 50 | "name": "call", 51 | }, 52 | }, 53 | "arguments": [ 54 | { 55 | "type": "ThisExpression", 56 | }, 57 | { 58 | "type": "CallExpression", 59 | "callee": { 60 | "type": "Identifier", 61 | "name": "logJS", 62 | }, 63 | "arguments": [ 64 | { 65 | "type": "Identifier", 66 | "name": "arg", 67 | }, 68 | ], 69 | }, 70 | ], 71 | }, 72 | "generator": false, 73 | "expression": true, 74 | }, 75 | "alternate": { 76 | "type": "Identifier", 77 | "name": "x", 78 | }, 79 | }, 80 | "generator": false, 81 | "expression": true, 82 | }, 83 | "arguments": [ 84 | foo, 85 | ], 86 | }); -------------------------------------------------------------------------------- /patches/except_while_loop.js: -------------------------------------------------------------------------------- 1 | function MakeBinaryExpression(lhs, rhs, op) { 2 | // {"type":"BinaryExpression","start":30,"end":35,"left":{"type":"Identifier","start":30,"end":31,"name":"a"},"operator":"+","right":{"type":"Literal","start":34,"end":35,"value":1,"raw":"1"}} 3 | return { 4 | type: "BinaryExpression", 5 | left: lhs, 6 | right: rhs, 7 | operator: op 8 | }; 9 | }; 10 | 11 | function MakeLiteral(value) { 12 | return { 13 | type: "Literal", 14 | value: value 15 | }; 16 | }; 17 | 18 | function MakeIfThen(test, body) { 19 | return { 20 | type: "IfStatement", 21 | test: test, 22 | consequent: body 23 | }; 24 | }; 25 | 26 | function GenSimpleLoop(fexpr, tryStmt, updateStmt) { 27 | 28 | // First just run the loop once to trigger the exception. 29 | var oldBody = fexpr.body; 30 | 31 | // Do function calls only for defined entries in an array. 32 | var tryBody = tryStmt.block.body; 33 | if (Array.isArray(tryBody)) { 34 | tryBody = tryBody[0]; 35 | } 36 | if ((tryBody.type == "ExpressionStatement") && (tryBody.expression.type == "AssignmentExpression")) { 37 | tryBody = tryBody.expression.right; 38 | } 39 | var arrayAcc = ""; 40 | if (typeof(tryBody.expression) != "undefined") { 41 | arrayAcc = tryBody.expression.callee; 42 | } 43 | else { 44 | arrayAcc = tryBody.callee; 45 | } 46 | var undef = { 47 | type: "Identifier", 48 | name: "undefined" 49 | }; 50 | var ifTest = MakeBinaryExpression(arrayAcc, undef, "!="); 51 | var funcCall = tryBody; 52 | var newIf = MakeIfThen(ifTest, funcCall); 53 | 54 | // In new loop body do guarded call followed by existing var update. 55 | var loopBody = { 56 | type: "BlockStatement", 57 | body: [newIf, updateStmt] 58 | }; 59 | var newLoop = { 60 | type: "WhileStatement", 61 | test: fexpr.test, 62 | body: loopBody 63 | }; 64 | 65 | // Put it all together. 66 | var r = { 67 | type: "BlockStatement", 68 | body: [tryStmt, newLoop] 69 | }; 70 | return r; 71 | }; 72 | 73 | module.exports = (fexpr, tryStmt, updateStmt) => (GenSimpleLoop(fexpr, tryStmt, updateStmt)); 74 | -------------------------------------------------------------------------------- /patches/prototype-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Acorn plugin which parses "member function statements" in JScript code e.g. 3 | * function Object.prototype.func(args) { } 4 | * 5 | * Required by prototype.js patch 6 | */ 7 | 8 | // Needs to be revisited to work with Acorn 8.* plugin system. 9 | // Check to see if this is handled natively by Acorn 8.*. 10 | module.exports = function(acorn) { 11 | acorn.plugins.JScriptMemberFunctionStatement = function(parser) { 12 | parser.extend("parseFunction", function(base) { 13 | return function(node, isStatement, allowExpressionBody, isAsync) { 14 | /** 15 | * If it's function statement and identifier is expected: 16 | * set flag for next parseIdent call 17 | **/ 18 | if(this.type == acorn.tokTypes.name) 19 | { 20 | this.isFuncStatementId = true; 21 | 22 | // A bit dirty, but parsing statement is associated with additional checkLVal 23 | let r = base.call(this, node, false, allowExpressionBody, isAsync); 24 | 25 | // Recovering original node type 26 | if(isStatement) 27 | r.type = "FunctionDeclaration" 28 | 29 | return r 30 | } 31 | return base.apply(this, arguments); 32 | } 33 | }); 34 | 35 | parser.extend("parseIdent", function(base) { 36 | return function() { 37 | let r = base.apply(this, arguments); 38 | if(this.isFuncStatementId) 39 | { 40 | // Unset flag (allow recursion) 41 | this.isFuncStatementId = false; 42 | 43 | while(this.eat(acorn.tokTypes.dot)) 44 | { 45 | /** 46 | * For each dot successor - build MemberExpression 47 | * Fortunately, JScript allows only dots as subscript separator in this case. 48 | **/ 49 | r = { 50 | type: "MemberExpression", 51 | object: r, 52 | property: this.parseIdent(), 53 | computed: false 54 | } 55 | } 56 | } 57 | return r; 58 | } 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /patches/typeof.js: -------------------------------------------------------------------------------- 1 | /* 2 | typeof foo becomes 3 | 4 | (function(){ 5 | try { 6 | return foo.typeof ? foo.typeof : typeof foo 7 | } catch(e) { 8 | return typeof foo 9 | } 10 | })() 11 | */ 12 | module.exports = (foo) => ({ 13 | "type": "CallExpression", 14 | "autogenerated": true, 15 | "callee": { 16 | "type": "FunctionExpression", 17 | "id": null, 18 | "params": [], 19 | "defaults": [], 20 | "body": { 21 | "type": "BlockStatement", 22 | "body": [ 23 | { 24 | "type": "TryStatement", 25 | "block": { 26 | "type": "BlockStatement", 27 | "body": [ 28 | { 29 | "type": "ReturnStatement", 30 | "argument": { 31 | "type": "ConditionalExpression", 32 | "test": { 33 | "type": "MemberExpression", 34 | "computed": false, 35 | "object": foo, 36 | "property": { 37 | "type": "Identifier", 38 | "name": "typeof", 39 | }, 40 | }, 41 | "consequent": { 42 | "type": "MemberExpression", 43 | "computed": false, 44 | "object": foo, 45 | "property": { 46 | "type": "Identifier", 47 | "name": "typeof", 48 | }, 49 | }, 50 | "alternate": { 51 | "type": "UnaryExpression", 52 | "operator": "typeof", 53 | "argument": foo, 54 | "prefix": true, 55 | }, 56 | }, 57 | }, 58 | ], 59 | }, 60 | "guardedHandlers": [], 61 | "handlers": [ 62 | { 63 | "type": "CatchClause", 64 | "param": { 65 | "type": "Identifier", 66 | "name": "e", 67 | }, 68 | "body": { 69 | "type": "BlockStatement", 70 | "body": [ 71 | { 72 | "type": "ReturnStatement", 73 | "argument": { 74 | "type": "UnaryExpression", 75 | "operator": "typeof", 76 | "argument": foo, 77 | "prefix": true, 78 | }, 79 | }, 80 | ], 81 | }, 82 | }, 83 | ], 84 | "handler": { 85 | "type": "CatchClause", 86 | "param": { 87 | "type": "Identifier", 88 | "name": "e", 89 | }, 90 | "body": { 91 | "type": "BlockStatement", 92 | "body": [ 93 | { 94 | "type": "ReturnStatement", 95 | "argument": { 96 | "type": "UnaryExpression", 97 | "operator": "typeof", 98 | "argument": foo, 99 | "prefix": true, 100 | }, 101 | }, 102 | ], 103 | }, 104 | }, 105 | "finalizer": null, 106 | }, 107 | ], 108 | }, 109 | "generator": false, 110 | "expression": false, 111 | }, 112 | "arguments": [], 113 | }); 114 | -------------------------------------------------------------------------------- /RELEASE_NOTES.txt: -------------------------------------------------------------------------------- 1 | Version 1.19.27, 11/22/2024 2 | -------------------------- 3 | 4 | * Added --fake-language=LANG option for setting the language to return 5 | for Win32_OperatingSystem.OSLanguage. 6 | * Added --fake-domain=DOM option for setting the domain to return for 7 | WScript.Network.UserDomain. 8 | * Added tracking for XMLHTTP URLs. 9 | * Added emulation for FileSystemObject::GetFileName(). 10 | * Added emulation for WBEMScripting::execQuery(). 11 | * Added emulation for navigator.clipboard.writeText(). 12 | 13 | Version 1.19.26, 8/13/2024 14 | -------------------------- 15 | 16 | * Added --prepended-code=default option for using the default box-js boilerplate.js file. 17 | * Added --limit-file-checks command line flag to switch faked file/folder exists 18 | behavior once many file check have been executed. Used to 19 | (potentially) break infinite file check loops. 20 | * Added --ignore-rewrite-errors command line flag. 21 | * Added --fake-script-engine command line flag. 22 | * Added --real-script-name command line option. 23 | * More browser DOM element emulations. 24 | * More ActiveX class emulations. 25 | * Make Math.Random() return a predictable series of values. This helps 26 | make emulation deterministic. 27 | * Bug fixes in JS code rewriting. 28 | 29 | Version 1.19.25, 6/27/2023 30 | -------------------------- 31 | 32 | * Track execution of JS served out by a C2 as an IOC. 33 | * Track ActiveX object creations as IOCs. 34 | * Added a --fake-download command line flag that makes box-js fake valid HTTP responses. 35 | * Added a --fake-sample-name command line flag for specifying a fake file name for the analyzed sample. 36 | * Upgrade the Acorn JS parser package to most recent version. 37 | * Bug fixes in JS code rewriting. 38 | 39 | Version 1.19.24, 2/15/2023 40 | -------------------------- 41 | 42 | * Added --activex-as-ioc command line flag for tracking ActiveX object creations as IOCs. 43 | * Added --fake-cl-args command line argument for providing fake command line arguments for the executing script. 44 | * Added --ignore-wscript-quit command line flag for ignoring WScript.Quit() calls during emulation. 45 | * Code rewrite bug fixes and speed ups. 46 | * Track addEventListener() as IOC. 47 | 48 | Version 1.19.20, 2/15/2023 49 | -------------------------- 50 | 51 | * Added anti-emulation loop rewriting functionality. 52 | * Added functionality for faking being run with cscript.exe or wscript.exe. 53 | * Added functionality for throttling lots of small file writes. 54 | * Added support for WMI.GetObject.Run(). 55 | * Added support for ADODBStream.flush(). 56 | * Added support for InternetExplorer.Application. 57 | * Added support for XMLHttpRequest. 58 | * Added some stubbed JQuery functionality. 59 | * Added support for ScheduleService. 60 | * Track IOCs being passed through the '|' operator in analyzed JS code. 61 | * Added support for WindowsInstaller.installer. 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /patches/v8-patch.diff: -------------------------------------------------------------------------------- 1 | diff --git a/deps/v8/src/runtime/runtime-compiler.cc b/deps/v8/src/runtime/runtime-compiler.cc 2 | index b5910e4..6c7bf13 100644 3 | --- a/deps/v8/src/runtime/runtime-compiler.cc 4 | +++ b/deps/v8/src/runtime/runtime-compiler.cc 5 | @@ -395,10 +395,54 @@ bool CodeGenerationFromStringsAllowed(Isolate* isolate, 6 | } 7 | } 8 | 9 | -static Object* CompileGlobalEval(Isolate* isolate, Handle source, 10 | +static Object* CompileGlobalEval(Isolate* isolate, Handle old_source, 11 | Handle outer_info, 12 | LanguageMode language_mode, 13 | int eval_scope_position, int eval_position) { 14 | + /* The eval patch is MIT licensed. The text of the license follows. 15 | + 16 | +Copyright (c) 2016 @CapacitorSet 17 | + 18 | +Permission is hereby granted, free of charge, to any person obtaining a copy 19 | +of this software and associated documentation files (the "Software"), to deal 20 | +in the Software without restriction, including without limitation the rights 21 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | +copies of the Software, and to permit persons to whom the Software is 23 | +furnished to do so, subject to the following conditions: 24 | + 25 | +The above copyright notice and this permission notice shall be included in all 26 | +copies or substantial portions of the Software. 27 | + 28 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | +SOFTWARE. 35 | + 36 | + */ 37 | + 38 | + Handle source; 39 | + 40 | + std::stringstream temp_ostream; 41 | + (**old_source).PrintUC16(temp_ostream); 42 | + std::string source_string = temp_ostream.str(); 43 | + 44 | + std::string trigger_string("/* __eval_hook_skip */"); 45 | + if (source_string.find(trigger_string) == std::string::npos) { 46 | + std::stringstream patched_stream; 47 | + patched_stream << "/* Patched in deps/v8/src/runtime/runtime-compiler.cc */ /* __eval_hook_skip */ __eval_trap(`" << source_string << "`);"; 48 | + 49 | + std::string patched_string = patched_stream.str(); 50 | + const char* patched_cstr = patched_string.c_str(); 51 | + 52 | + Factory* factory = isolate->factory(); 53 | + source = factory->InternalizeUtf8String(patched_cstr); 54 | + } else { 55 | + source = old_source; 56 | + } 57 | + 58 | Handle context = Handle(isolate->context()); 59 | Handle native_context = Handle(context->native_context()); 60 | 61 | -------------------------------------------------------------------------------- /patches/this.js: -------------------------------------------------------------------------------- 1 | /* 2 | foo.bar(baz) becomes: 3 | 4 | (fun => { 5 | return function() { 6 | if (fun == eval) logJS(arguments[0]); 7 | return fun.apply(foo, arguments) 8 | } 9 | })(foo.bar)(baz) 10 | */ 11 | module.exports = (foo, foobar) => ({ 12 | "type": "CallExpression", 13 | "callee": { 14 | "autogenerated": true, 15 | "type": "ArrowFunctionExpression", 16 | "id": null, 17 | "params": [ 18 | { 19 | "type": "Identifier", 20 | "name": "fun", 21 | }, 22 | ], 23 | "defaults": [], 24 | "body": { 25 | "type": "BlockStatement", 26 | "body": [ 27 | { 28 | "type": "ReturnStatement", 29 | "argument": { 30 | "type": "FunctionExpression", 31 | "id": null, 32 | "params": [], 33 | "defaults": [], 34 | "body": { 35 | "type": "BlockStatement", 36 | "body": [ 37 | { 38 | "type": "IfStatement", 39 | "test": { 40 | "type": "BinaryExpression", 41 | "operator": "==", 42 | "left": { 43 | "type": "Identifier", 44 | "name": "fun", 45 | }, 46 | "right": { 47 | "type": "Identifier", 48 | "name": "eval", 49 | }, 50 | }, 51 | "consequent": { 52 | "type": "ExpressionStatement", 53 | "expression": { 54 | "type": "CallExpression", 55 | "callee": { 56 | "type": "Identifier", 57 | "name": "logJS", 58 | }, 59 | "arguments": [ 60 | { 61 | "type": "MemberExpression", 62 | "computed": true, 63 | "object": { 64 | "type": "Identifier", 65 | "name": "arguments", 66 | }, 67 | "property": { 68 | "type": "Literal", 69 | "value": 0, 70 | "raw": "0", 71 | }, 72 | }, 73 | ], 74 | }, 75 | }, 76 | "alternate": null, 77 | }, 78 | { 79 | "type": "ReturnStatement", 80 | "argument": { 81 | "type": "CallExpression", 82 | "callee": { 83 | "type": "MemberExpression", 84 | "computed": false, 85 | "object": { 86 | "type": "Identifier", 87 | "name": "fun", 88 | }, 89 | "property": { 90 | "type": "Identifier", 91 | "name": "apply", 92 | }, 93 | }, 94 | "arguments": [ 95 | foo, 96 | { 97 | "type": "Identifier", 98 | "name": "arguments", 99 | }, 100 | ], 101 | }, 102 | }, 103 | ], 104 | }, 105 | "generator": false, 106 | "expression": false, 107 | }, 108 | }, 109 | ], 110 | }, 111 | "generator": false, 112 | "expression": false, 113 | }, 114 | "arguments": [ 115 | foobar, 116 | ], 117 | }); -------------------------------------------------------------------------------- /emulator/ScheduleService.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | // WARNING: Only handles a single global task object. 4 | TaskFolderObject = { 5 | _tasks: {}, 6 | GetTask: function(name) { 7 | lib.info('The sample looked for scheduled task "' + name + '".'); 8 | if (typeof(this._tasks[name]) == "undefined") throw "task not found"; 9 | return this._tasks[name]; 10 | }, 11 | 12 | RegisterTaskDefinition: function(name, taskObj) { 13 | var taskInfo = { 14 | name: name, 15 | path: taskObj.path, 16 | args: taskObj.args, 17 | workingDir: taskObj.workingDir, 18 | }; 19 | if (typeof(taskObj.triggerObj) != "undefined") { 20 | taskInfo.triggerId = taskObj.triggerObj.id; 21 | taskInfo.triggerUserId = taskObj.triggerObj.userId; 22 | }; 23 | lib.logIOC("Task", taskInfo, 'The sample registered task "' + name + '".'); 24 | this._tasks[name] = taskObj; 25 | }, 26 | }; 27 | 28 | class TaskTriggerObject { 29 | 30 | constructor() { 31 | }; 32 | 33 | set ID(v) { 34 | lib.info('The sample set a task trigger ID to "' + v + '".'); 35 | this.id = v; 36 | }; 37 | 38 | set UserId(v) { 39 | lib.info('The sample set a task user ID to "' + v + '".'); 40 | this.userId = v; 41 | }; 42 | }; 43 | 44 | //debug 45 | var num = 0; 46 | class TaskObject { 47 | 48 | constructor() { 49 | this.settings = {}; 50 | this.triggers = { 51 | Create: function() { 52 | var r = new TaskTriggerObject(); 53 | this.taskObj.triggerObj = r; 54 | return r; 55 | }, 56 | }; 57 | this.Actions = { 58 | Create: function() { 59 | return this.taskObj; 60 | }, 61 | }; 62 | this.Actions.Create.taskObj = this; 63 | this.Actions.Create = this.Actions.Create.bind(this.Actions.Create); 64 | this.triggers.Create.taskObj = this; 65 | this.triggers.Create = this.triggers.Create.bind(this.triggers.Create); 66 | this.debug = "DEBUG_" + (num++); 67 | }; 68 | 69 | set Path(v) { 70 | lib.info('The sample set task path to "' + v + '".'); 71 | this.path = v; 72 | }; 73 | 74 | set Arguments(v) { 75 | lib.info('The sample set task arguments to "' + v + '".'); 76 | this.args = v; 77 | }; 78 | 79 | set WorkingDirectory(v) { 80 | lib.info('The sample set task working directory to "' + v + '".'); 81 | this.workingDir = v; 82 | }; 83 | 84 | RunEx() { 85 | lib.info('The sample ran a scheduled task.'); 86 | }; 87 | }; 88 | 89 | function ScheduleService() { 90 | 91 | this.clazz = "ScheduleService"; 92 | 93 | this.Language = undefined; 94 | this.Timeout = undefined; 95 | 96 | this.connect = () => { 97 | lib.info('The sample connected to the task scheduler.'); 98 | }; 99 | 100 | this.getfolder = root => { 101 | lib.info('The sample got a scheduled task folder object rooted at "' + root + '".'); 102 | return TaskFolderObject; 103 | }; 104 | 105 | this.newtask = () => { 106 | return new TaskObject(); 107 | }; 108 | } 109 | 110 | module.exports = lib.proxify(ScheduleService, "Schedule.Service"); 111 | -------------------------------------------------------------------------------- /emulator/XMLHTTP.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const argv = require("../argv.js").run; 3 | 4 | var _fileCheckCount = 0; 5 | var _lastUrl = ""; 6 | function XMLHTTP() { 7 | this.headers = {}; 8 | this.onreadystatechange = () => {}; 9 | this.readystate = 0; 10 | this.statustext = "UNSENT"; 11 | this.status = undefined; 12 | this.url = "URL NOT SET"; 13 | this.method = "METHOD NOT SET"; 14 | 15 | this.open = function(method, url) { 16 | // Maybe you can skip the http part of the URL and XMLHTTP 17 | // still handles it? 18 | if (url.startsWith("//")) { 19 | url = "http:" + url; 20 | } 21 | this.url = url; 22 | this.method = method; 23 | this.readystate = 1; 24 | this.statustext = "OPENED"; 25 | lib.logUrl('XMLHTTP', url); 26 | lib.logIOC("XMLHTTP", {url: url}, "The script opened URL " + url + " with XMLHTTP"); 27 | 28 | // Try to break infinite network loops by exiting if we do 29 | // many open()s. 30 | _fileCheckCount++; 31 | if (argv["limit-file-checks"]) { 32 | if ((_fileCheckCount > 100) && (url == _lastUrl)) { 33 | lib.info("Possible infinite network loop detected. Exiting."); 34 | process.exit(0); 35 | } 36 | } 37 | _lastUrl = url; 38 | }; 39 | this.responsetext = "console.log('The script executed JS returned from a C2 server.')"; 40 | this.setrequestheader = function(key, val) { 41 | key = key.replace(/:$/, ""); // Replace a trailing ":" if present 42 | this.headers[key] = val; 43 | lib.info(`Header set for ${this.url}:`, key, val); 44 | }; 45 | this.settimeouts = function() { 46 | // Stubbed out. 47 | }; 48 | this.setproxy = function() { 49 | // Stubbed out. 50 | }; 51 | this.send = function(data) { 52 | if (data) 53 | lib.info(`Data sent to ${this.url}:`, data); 54 | this.readystate = 4; 55 | let response; 56 | 57 | // Track the initial request. 58 | response = lib.fetchUrl(this.method, this.url, this.headers, data); 59 | 60 | // Fake that the request worked? 61 | if (argv["fake-download"]) { 62 | this.status = 200; 63 | this.statustext = "OK"; 64 | } 65 | 66 | // Not faking request. 67 | else { 68 | try { 69 | 70 | // Actually try the request? 71 | if (argv.download) { 72 | this.status = 200; 73 | this.statustext = "OK"; 74 | } else { 75 | this.status = 404; 76 | this.statustext = "Not found"; 77 | }; 78 | } catch (e) { 79 | // If there was an error fetching the URL, pretend that the distribution site is down 80 | this.status = 404; 81 | this.statustext = "Not found"; 82 | response = { 83 | body: new Buffer(""), 84 | headers: {}, 85 | }; 86 | }; 87 | }; 88 | this.responsebody = response.body; 89 | this.responsetext = this.responsebody.toString("utf8"); 90 | this.responseheaders = response.headers; 91 | this.onreadystatechange(); 92 | }; 93 | this.setoption = () => {}; 94 | // Fake up setting options. 95 | this.option = {}; 96 | this.getresponseheader = (key) => this.responseheaders[key]; 97 | this.waitforresponse = function() {}; 98 | } 99 | 100 | module.exports = lib.proxify(XMLHTTP, "XMLHTTP"); 101 | -------------------------------------------------------------------------------- /emulator/ADODBStream.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const iconv = require("iconv-lite"); 3 | 4 | /* Includes code (ADODBStream.writetext, .loadfromfile) from 5 | * https://github.com/HynekPetrak/malware-jail. The license follows. 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2016 Hynek Petrak 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | function ADODBStream() { 31 | this.virtual_filename = "(undefined)"; 32 | this.charset = ""; 33 | this.position = 0; 34 | this.open = () => {}; 35 | this.savetofile = function(filename) { 36 | this.virtual_filename = filename; 37 | lib.logIOC("ADODBStream", {"name": filename}, "The script wrote a file."); 38 | lib.writeFile(filename, this.buffer); 39 | lib.logResource(lib.getUUID(), this.virtual_filename, this.buffer, true); 40 | }; 41 | this.close = () => {}; 42 | this.read = this.readtext = function() { 43 | return this.buffer; 44 | }; 45 | this.write = this.writetext = function(text) { 46 | lib.logIOC("ADODBStream", text, "The script wrote text to a stream."); 47 | if (this.charset) { 48 | this.buffer = iconv.encode(text, this.charset); 49 | } else 50 | this.buffer = text; 51 | }; 52 | this.loadfromfile = function(filename) { 53 | if (this.charset) 54 | this.buffer = iconv.decode(lib.readFile(filename), this.charset); 55 | else 56 | this.buffer = lib.readFile(filename); 57 | }; 58 | this.tojson = function(data) { 59 | return "[1]"; 60 | }; 61 | this.flush = function() {}; 62 | this.copyto = (target) => target.write(this.buffer); 63 | } 64 | 65 | module.exports = function() { 66 | return new Proxy(new ADODBStream(), { 67 | get: function(target, name) { 68 | name = name.toLowerCase(); 69 | switch (name) { 70 | case "size": 71 | case "length": 72 | return target.buffer.length; 73 | default: 74 | lib.info(`Script called ADODBStream.${name}`); 75 | if (name in target) return target[name]; 76 | lib.kill(`ADODBStream.${name} not implemented!`); 77 | } 78 | }, 79 | set: function(a, b, c) { 80 | b = b.toLowerCase(); 81 | /* if (c.length < 1024) 82 | console.log(`ADODBStream[${b}] = ${c};`); 83 | */ 84 | a[b] = c; 85 | return true; 86 | }, 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /emulator/ADODBRecordSet.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | // http://stackoverflow.com/a/30410454 4 | function nthOfGenerator(generator, n) { 5 | let i = 0; 6 | 7 | if (n < 0) throw new Error("Invalid index"); 8 | 9 | for (const value of generator) 10 | if (i++ === n) return value; 11 | 12 | throw new Error(`Generator has fewer than ${n} elements`); 13 | } 14 | 15 | /* Because Proxies can't change their targets (i.e. you can't 16 | * do `target = new Buffer(...)`), this wrapper is needed. 17 | */ 18 | function RewritableTarget(target) { 19 | this.value = target; 20 | this.get = (prop) => this.value[prop]; 21 | this.set = (prop, val) => (this.value[prop] = val); 22 | this.rewrite = (val) => (this.target = val); 23 | } 24 | 25 | function ProxiedField(field, updateFn) { 26 | return new Proxy( 27 | new RewritableTarget(field), 28 | { 29 | get: function(target, name) { 30 | name = name.toLowerCase(); 31 | switch (name) { 32 | case "appendchunk": 33 | return (chunk) => { 34 | target.value = Buffer.concat([ 35 | target.value, 36 | Buffer.from(chunk), 37 | ]); 38 | updateFn(target.value); 39 | }; 40 | case "getchunk": 41 | return (length) => target.value.toString("utf8", 0, length); 42 | default: 43 | if (name in target.value) return target.value[name]; 44 | if (name in target) return target[name]; 45 | lib.kill(`ProxiedField.${name} not implemented!`); 46 | } 47 | }, 48 | } 49 | ); 50 | } 51 | 52 | function ADODBRecordSet() { 53 | this.addnew = this.close = this.open = this.update = () => {}; 54 | 55 | // Contains the data contained in the fields. That's for internal use. 56 | this._fields = new Map(); 57 | // Also for internal use. Used with movefirst, etc. 58 | this._currentRecordName = ""; 59 | this._index = 0; 60 | this._goToRecord = () => (this._currentRecordName = nthOfGenerator(this._fields.keys(), this._index)); 61 | 62 | this.movefirst = () => { 63 | this._index = 0; 64 | this._goToRecord(); 65 | }; 66 | this.movelast = () => { 67 | this._index = this._fields.size() - 1; 68 | this._goToRecord(); 69 | }; 70 | this.movenext = () => { 71 | this._index++; 72 | this._goToRecord(); 73 | }; 74 | this.moveprevious = () => { 75 | this._index--; 76 | this._goToRecord(); 77 | }; 78 | 79 | this.fields = new Proxy( 80 | (argument) => new ProxiedField( 81 | this._fields.get(argument), 82 | (newVal) => this._fields.set(argument, newVal) 83 | ), 84 | { 85 | get: (target, name) => { 86 | name = name.toLowerCase(); 87 | switch (name) { 88 | case "append": 89 | return (name, type, definedSize, attrib, fieldValue = "") => { 90 | if (Number(type) !== 204) { 91 | lib.warn(`Warning: unknown datatype ${type} in ADODBRecordSet`); 92 | } 93 | this._fields.set(name, Buffer.from(fieldValue)); 94 | }; 95 | case "fields": 96 | return (key) => new Proxy(target[key], { 97 | get: function(target, name) { 98 | if (name in target) return target[name]; 99 | lib.kill(`ADODBRecordSet.Fields.${name} not implemented!`); 100 | }, 101 | }); 102 | default: 103 | if (name in target) return target[name]; 104 | lib.kill(`ADODBRecordSet.Fields.${name} not implemented!`); 105 | } 106 | }, 107 | set: function(a, b, c) { 108 | b = b.toLowerCase(); 109 | a[b] = c; 110 | return true; 111 | }, 112 | } 113 | ); 114 | } 115 | 116 | module.exports = function() { 117 | this._instance = new ADODBRecordSet(); 118 | return new Proxy((data) => this._instance.fields(data), { 119 | get: (target, name) => { 120 | name = name.toLowerCase(); 121 | if (name in target) return target[name]; 122 | if (name in this._instance) return this._instance[name]; 123 | lib.kill(`ADODBRecordSet.${name} not implemented!`); 124 | }, 125 | set: function(a, b, c) { 126 | b = b.toLowerCase(); 127 | a[b] = c; 128 | return true; 129 | }, 130 | }); 131 | }; -------------------------------------------------------------------------------- /integrations/README.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | ## Submitting to Cuckoo, Malwr, VirusTotal 4 | 5 | You can automatically submit the results of an analysis to a Cuckoo sandbox, Malwr or VirusTotal. Run `box-export --help` (or `node integrations/export/export.js --help`) for more information. 6 | 7 | ## Running in Cuckoo/CAPE 8 | 9 | Start the REST API server (see below), and see [CAPE documentation](https://capev2.readthedocs.io/en/latest/integrations/box-js.html). 10 | 11 | ## Running in Docker 12 | 13 | You might want to run the analysis in a temporary Docker container in order to isolate the process. This has positive security implications: although box-js already uses a hardened sandbox, Docker provides another stronger level of isolation. 14 | 15 | You can run the analysis like this: 16 | 17 | ``` 18 | docker run --rm --volume /tmp/samplecollection:/samples capacitorset/box-js box-js /samples --output-dir=/samples --loglevel=debug 19 | ``` 20 | 21 | >If you wish to make changes, the Dockerfile is in `integrations/docker/Dockerfile`. 22 | 23 | This does the following: 24 | 25 | * `docker run ... box-js` run the container called `box-js` which you previously created; 26 | * `--rm` specifies that the container filesystem should be destroyed when the analysis finishes; 27 | * `--volume /tmp/samplecollection:/samples` means that your folder `/tmp/samplecollection` will be mounted as `/samples` inside the container. Take special care not to expose sensitive files! 28 | * `box-js /samples --output-dir=/samples --loglevel=debug` is the command to be executed. It will analyze all files in /samples (i.e. in /tmp/samplecollection), write results to /samples/.results, and print all log messages, including debug ones. 29 | 30 | >Of course, you can use whichever flags you want, eg. `--download --timeout=120`. 31 | 32 | When the analysis terminates, you will find the results in /tmp/samplescollection, where you can analyze them by yourself or with `box-export`. 33 | 34 | If you wish to use Docker in an external application, it can be useful to use `--debug` and check the return code: 35 | 36 | * 0 is, of course, success. 37 | * 1 is returned for generic errors. 38 | * 2 is returned when the script times out. 39 | >You should retry the analysis with a higher timeout, if the current/default one is too short. 40 | * 3 is returned when an error occurred during rewriting. 41 | >You should try to re-run the analysis with --no-rewrite. 42 | * 4 is returned when the file couldn't be parsed, most likely because it's not JavaScript (eg. it's actually VBScript, or it's JSE and needs to be decoded). 43 | >You should try to decode the sample with the given decoder, and re-run the analysis on the plaintext sample. 44 | * 5 is returned when a shell fake-error was not catched by the dropper. 45 | >You should try to re-run the analysis with --no-shell-error. 46 | * 255 is returned when no files or directories were passed (or all the directories were empty). 47 | 48 | ## REST API 49 | 50 | >This section is a work-in-progress. 51 | 52 | Box-js includes a small REST API for uploading samples. To use it, install Hapi and rimraf (`npm install hapi rimraf`) and run `node integrations/api/api.js`. 53 | 54 | >The user running the API server must also be able to spawn Docker containers. 55 | 56 | ### Methods 57 | 58 | * `GET /concurrency`: returns an integer representing the concurrency, i.e. how many analyses to run at the same time at most. 59 | * `POST /concurrency` with parameter `value`: change the concurrency. 60 | * `GET /sample/{id}`: returns the status of the given analysis (1 if it's complete, 0 otherwise). 61 | * `POST /sample` with parameter `sample`, a file: enqueue the sample you uploaded. Returns the analysis ID (to be used with `GET /sample/{id}`). 62 | * `DELETE /sample/{id}`: delete the folder corresponding to the given sample. Should only be called after the analysis terminates. 63 | * `GET /sample/{id}/urls`: get the list of URLs extracted by the given sample. Should only be called after the analysis terminates. 64 | * `GET /sample/{id}/resources`: get the list of resources created by the given sample. Should only be called after the analysis terminates. 65 | 66 | ### Response 67 | 68 | > This section is a work in progress. 69 | 70 | The response always contains a field called "server_err", which is an integer which can take the following values: 71 | 72 | * 0: No error. 73 | * 1: Invalid ID (an UUID was expected). 74 | * 2: Folder not found. 75 | * 3: File not found. 76 | * 4: Analysis not yet ready. 77 | * 5: No file given. 78 | * 99: Other error 79 | -------------------------------------------------------------------------------- /emulator/WBEMScriptingSWBEMLocator.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const enumerator = require("./Enumerator"); 3 | const argv = require("../argv.js").run; 4 | 5 | // Fake OS language can be set with the --fake-language option. 6 | // Default language is English. 7 | var langCode = 1033; 8 | if (argv["fake-language"]) { 9 | 10 | // Were we given a supported language? 11 | const langStr = argv["fake-language"]; 12 | const langs = { 13 | 'spanish' : 2058, 14 | 'english' : 1033, 15 | 'portuguese' : 1046, 16 | }; 17 | langCode = langs[langStr]; 18 | if (!langCode) { 19 | lib.error("Language '" + langStr + "' not supported."); 20 | process.exit(4); 21 | }; 22 | } 23 | 24 | // Fake SWBEMService.instancesof results. 25 | 26 | _fake_win32_operatingsystem = { 27 | "Status" : "OK", 28 | "Name" : "Microsoft Windows 10 Enterprise|C:\WINDOWS|\Device\Harddisk0\Partition3", 29 | "FreePhysicalMemory" : "3409216", 30 | "FreeSpaceInPagingFiles" : "57801156", 31 | "FreeVirtualMemory" : "9689128", 32 | "Caption" : "Microsoft Windows 10 Enterprise", 33 | "Description" : "", 34 | "InstallDate" : "4/6/2022 11:31:32 AM", 35 | "CreationClassName" : "Win32_OperatingSystem", 36 | "CSCreationClassName" : "Win32_ComputerSystem", 37 | "CSName" : "MAC935", 38 | "CurrentTimeZone" : "-200", 39 | "Distributed" : "False", 40 | "LastBootUpTime" : "3/22/2024 2:11:13 PM", 41 | "LocalDateTime" : "4/19/2024 12:02:47 PM", 42 | "MaxNumberOfProcesses" : "2294967295", 43 | "MaxProcessMemorySize" : "37438953344", 44 | "NumberOfLicensedUsers" : "", 45 | "NumberOfProcesses" : "104", 46 | "NumberOfUsers" : "10", 47 | "OSType" : "18", 48 | "OtherTypeDescription" : "", 49 | "SizeStoredInPagingFiles" : "59278400", 50 | "TotalSwapSpaceSize" : "", 51 | "TotalVirtualMemorySize" : "25828864", 52 | "TotalVisibleMemorySize" : "16550464", 53 | "Version" : "10.0.19045", 54 | "BootDevice" : "\Device\HarddiskVolume1", 55 | "BuildNumber" : 19045, 56 | "BuildType" : "Multiprocessor Free", 57 | "CodeSet" : "1252", 58 | "CountryCode" : "1", 59 | "CSDVersion" : "", 60 | "DataExecutionPrevention_32BitApplications" : "False", 61 | "DataExecutionPrevention_Available" : "False", 62 | "DataExecutionPrevention_Drivers" : "False", 63 | "DataExecutionPrevention_SupportPolicy" : "1", 64 | "Debug" : "False", 65 | "EncryptionLevel" : "256", 66 | "ForegroundApplicationBoost" : "2", 67 | "LargeSystemCache" : "", 68 | "Locale" : "0409", 69 | "Manufacturer" : "Microsoft Corporation", 70 | "MUILanguages" : "{en-US}", 71 | "OperatingSystemSKU" : "4", 72 | "Organization" : "USERS", 73 | "OSArchitecture" : "64-bit", 74 | // US English 75 | //"OSLanguage" : "1033", 76 | // Mexican Spanish 77 | get OSLanguage() { 78 | lib.logIOC("SWBEMService", langCode, "Read Win32_OperatingSystem.OSLanguage."); 79 | return langCode; 80 | }, 81 | // International Spanish 82 | //"OSLanguage" : "3082", 83 | "OSProductSuite" : "256", 84 | "PAEEnabled" : "", 85 | "PlusProductID" : "", 86 | "PlusVersionNumber" : "", 87 | "PortableOperatingSystem" : "False", 88 | "Primary" : "True", 89 | "ProductType" : "1", 90 | "RegisteredUser" : "user", 91 | "SerialNumber" : "12379-38562-28486-38294", 92 | "ServicePackMajorVersion" : "0", 93 | "ServicePackMinorVersion" : "0", 94 | "SuiteMask" : "272", 95 | "SystemDevice" : "\Device\HarddiskVolume3", 96 | "SystemDirectory" : "C:\WINDOWS\system32", 97 | "SystemDrive" : "C:", 98 | "WindowsDirectory" : "C:\WINDOWS", 99 | "PSComputerName" : "", 100 | "CimClass" : "root/cimv2:Win32_OperatingSystem", 101 | "CimInstanceProperties" : "{Caption, Description, InstallDate, Name...}", 102 | "CimSystemProperties" : "Microsoft.Management.Infrastructure.CimSystemProperties", 103 | } 104 | 105 | function VirtualSWBEMServices() { 106 | this.instancesof = function(item) { 107 | lib.info(`SWBEMServices: emulating getting instances of ${item}`); 108 | switch (item.toLowerCase()) { 109 | case 'win32_operatingsystem': { 110 | return [_fake_win32_operatingsystem]; 111 | }; 112 | 113 | default : { 114 | lib.warning("SWBEMService '" + item + "' not known. Returning empty list."); 115 | return []; 116 | } 117 | }; 118 | }; 119 | 120 | this.execquery = function(query) { 121 | lib.logIOC("SWBEMService", query, "Executed SWBEMService query '" + query + "'."); 122 | if (query.indexOf("Win32_OperatingSystem") > -1) return [_fake_win32_operatingsystem]; 123 | return []; 124 | }; 125 | 126 | this.get = function(item) { 127 | return { 128 | spawninstance_ : function() { 129 | return { 130 | add : function(item) {} 131 | }; 132 | } 133 | }; 134 | }; 135 | } 136 | 137 | function VirtualWBEMLocator() { 138 | this.connectserver = function(server, namespace) { 139 | lib.info(`WBEMLocator: emulating a connection to server ${server} with namespace ${namespace}`); 140 | 141 | return lib.proxify(VirtualSWBEMServices, "WBEMScripting.SWBEMServices"); 142 | }; 143 | } 144 | 145 | module.exports = lib.proxify(VirtualWBEMLocator, "WBEMScripting.SWBEMServices"); 146 | -------------------------------------------------------------------------------- /patches/counter_while_loop.js: -------------------------------------------------------------------------------- 1 | /* 2 | 'while (x < y) { x = x + z; }' becomes 'x = x + y * z;' 3 | 4 | Additionally marked as a candidate to hoist. 5 | Needs prototype-plugin enabled. 6 | */ 7 | 8 | /* 9 | For < loops, positive increment: 10 | final_loop_counter = upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 11 | 12 | For <= loops, positive increment: 13 | final_loop_counter = upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 14 | */ 15 | 16 | function MakeNot(expr) { 17 | // {"type":"UnaryExpression","start":30,"end":32,"operator":"!","prefix":true,"argument":{"type":"Literal","start":31,"end":32,"value":1,"raw":"1"}} 18 | return { 19 | type: "UnaryExpression", 20 | operator: "!", 21 | argument: expr 22 | }; 23 | }; 24 | 25 | function MakeLiteral(value) { 26 | return { 27 | type: "Literal", 28 | value: value 29 | }; 30 | }; 31 | 32 | function MakeBinaryExpression(lhs, rhs, op) { 33 | // {"type":"BinaryExpression","start":30,"end":35,"left":{"type":"Identifier","start":30,"end":31,"name":"a"},"operator":"+","right":{"type":"Literal","start":34,"end":35,"value":1,"raw":"1"}} 34 | return { 35 | type: "BinaryExpression", 36 | left: lhs, 37 | right: rhs, 38 | operator: op 39 | }; 40 | }; 41 | 42 | function MakeMemberExpression(object, property, args) { 43 | /* 44 | { 45 | "type": "CallExpression", 46 | "callee": { 47 | "type": "MemberExpression", 48 | "object": { 49 | "type": "Identifier", 50 | "name": "Math" 51 | }, 52 | "property": { 53 | "type": "Identifier", 54 | "name": "sign" 55 | }, 56 | "computed": false 57 | }, 58 | "arguments": [{ 59 | "type": "Literal", 60 | "value": 12, 61 | }] 62 | } 63 | */ 64 | return { 65 | type: "CallExpression", 66 | callee:{ 67 | type: "MemberExpression", 68 | object: {type: "Identifier", name: object}, 69 | property: {type: "Identifier", name: property}, 70 | }, 71 | arguments: args 72 | }; 73 | }; 74 | 75 | function MakeMathSign(upperBound, initialCounter, increment) { 76 | // Make Math.sign((upper_bound-initial_loop_counter)%increment) 77 | return { 78 | type: "CallExpression", 79 | callee: { 80 | type: "MemberExpression", 81 | object: { 82 | type: "Identifier", 83 | name: "Math" 84 | }, 85 | property: { 86 | type: "Identifier", 87 | name: "sign" 88 | }, 89 | computed: false 90 | }, 91 | arguments: [{ 92 | type: "BinaryExpression", 93 | left: { 94 | type: "BinaryExpression", 95 | left: upperBound, 96 | operator: "-", 97 | right: initialCounter 98 | }, 99 | operator: "%", 100 | right: increment 101 | }] 102 | }; 103 | }; 104 | 105 | function PullIncrement(fexpr) { 106 | 107 | // Figure out if we have "x = x + z" or "x++". 108 | line = fexpr.body.body[0].expression; 109 | 110 | // Pull loop counter increment value from expression in loop body. 111 | var r; 112 | if (line.type == "AssignmentExpression") { 113 | var line = fexpr.body.body[0].expression; 114 | var baseExpr; 115 | var r; 116 | if (line.operator == "="){ 117 | var rhs = line.right; 118 | if (rhs.left.name != fexpr.test.left.name) { 119 | baseExpr = rhs.left; 120 | } 121 | else { 122 | baseExpr = rhs.right; 123 | } 124 | r = baseExpr; 125 | } 126 | if (line.operator == "+="){ 127 | r = line.right; 128 | }; 129 | }; 130 | 131 | if (line.type == "UpdateExpression") { 132 | r = MakeLiteral(1); 133 | }; 134 | 135 | // Done. 136 | return r; 137 | } 138 | 139 | function PullLoopUpperBound(fexpr) { 140 | // Pull the upper bound from the while test. 141 | return fexpr.test.right; 142 | } 143 | 144 | function PullLoopCounter(fexpr) { 145 | 146 | // Pull the loop counter from the while test. 147 | return fexpr.test.left; 148 | } 149 | 150 | function GenFinalLoopVal(fexpr) { 151 | /* 152 | For < loops, positive increment: 153 | upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 154 | 155 | For <= loops, positive increment: 156 | upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 157 | */ 158 | var upperBound = PullLoopUpperBound(fexpr); 159 | var loopCounter = PullLoopCounter(fexpr); 160 | var increment = PullIncrement(fexpr); 161 | // upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 162 | var modExpr = MakeBinaryExpression( 163 | MakeMathSign(upperBound, loopCounter, increment), 164 | MakeBinaryExpression(increment, MakeLiteral(1), "-"), 165 | "*" 166 | ); 167 | var r = MakeBinaryExpression(upperBound, modExpr, "+"); 168 | if (fexpr.test.operator == "<=") { 169 | // !(Math.sign((y - x) % z) * (z - 1)); 170 | // upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 171 | var modExpr1 = MakeBinaryExpression(MakeBinaryExpression(upperBound, loopCounter, "-"), increment, "%"); 172 | var p0 = MakeMemberExpression("Math", "sign", [modExpr1]); 173 | var p1 = MakeNot(p0); 174 | var p2 = MakeMemberExpression("Math", "sign", [p1]); 175 | r = MakeBinaryExpression(r, p2, "+"); 176 | }; 177 | return r; 178 | } 179 | 180 | module.exports = (fexpr) => ({ 181 | type: "ExpressionStatement", 182 | "autogenerated": true, 183 | expression: { 184 | type: "AssignmentExpression", 185 | operator: "=", 186 | left: fexpr.test.left, 187 | right: GenFinalLoopVal(fexpr) 188 | } 189 | }); 190 | -------------------------------------------------------------------------------- /patches/index_break_statement.js: -------------------------------------------------------------------------------- 1 | /* 2 | 'while (x < y) { x = x + z; }' becomes 'x = x + y * z;' 3 | 4 | Additionally marked as a candidate to hoist. 5 | Needs prototype-plugin enabled. 6 | */ 7 | 8 | /* 9 | For < loops, positive increment: 10 | final_loop_counter = upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 11 | 12 | For <= loops, positive increment: 13 | final_loop_counter = upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 14 | */ 15 | 16 | function MakeNot(expr) { 17 | // {"type":"UnaryExpression","start":30,"end":32,"operator":"!","prefix":true,"argument":{"type":"Literal","start":31,"end":32,"value":1,"raw":"1"}} 18 | return { 19 | type: "UnaryExpression", 20 | operator: "!", 21 | argument: expr 22 | }; 23 | }; 24 | 25 | function MakeLiteral(value) { 26 | return { 27 | type: "Literal", 28 | value: value 29 | }; 30 | }; 31 | 32 | function MakeBinaryExpression(lhs, rhs, op) { 33 | // {"type":"BinaryExpression","start":30,"end":35,"left":{"type":"Identifier","start":30,"end":31,"name":"a"},"operator":"+","right":{"type":"Literal","start":34,"end":35,"value":1,"raw":"1"}} 34 | return { 35 | type: "BinaryExpression", 36 | left: lhs, 37 | right: rhs, 38 | operator: op 39 | }; 40 | }; 41 | 42 | function MakeMemberExpression(object, property, args) { 43 | /* 44 | { 45 | "type": "CallExpression", 46 | "callee": { 47 | "type": "MemberExpression", 48 | "object": { 49 | "type": "Identifier", 50 | "name": "Math" 51 | }, 52 | "property": { 53 | "type": "Identifier", 54 | "name": "sign" 55 | }, 56 | "computed": false 57 | }, 58 | "arguments": [{ 59 | "type": "Literal", 60 | "value": 12, 61 | }] 62 | } 63 | */ 64 | return { 65 | type: "CallExpression", 66 | callee:{ 67 | type: "MemberExpression", 68 | object: {type: "Identifier", name: object}, 69 | property: {type: "Identifier", name: property}, 70 | }, 71 | arguments: args 72 | }; 73 | }; 74 | 75 | function MakeMathSign(upperBound, initialCounter, increment) { 76 | // Make Math.sign((upper_bound-initial_loop_counter)%increment) 77 | return { 78 | type: "CallExpression", 79 | callee: { 80 | type: "MemberExpression", 81 | object: { 82 | type: "Identifier", 83 | name: "Math" 84 | }, 85 | property: { 86 | type: "Identifier", 87 | name: "sign" 88 | }, 89 | computed: false 90 | }, 91 | arguments: [{ 92 | type: "BinaryExpression", 93 | left: { 94 | type: "BinaryExpression", 95 | left: upperBound, 96 | operator: "-", 97 | right: initialCounter 98 | }, 99 | operator: "%", 100 | right: increment 101 | }] 102 | }; 103 | }; 104 | 105 | function PullIncrement(fexpr) { 106 | 107 | // Figure out if we have "x = x + z" or "x++". 108 | line = fexpr.body.body[0].expression; 109 | 110 | // Pull loop counter increment value from expression in loop body. 111 | var r; 112 | if (line.type == "AssignmentExpression") { 113 | var line = fexpr.body.body[0].expression; 114 | var baseExpr; 115 | var r; 116 | if (line.operator == "="){ 117 | var rhs = line.right; 118 | if (rhs.left.name != fexpr.test.left.name) { 119 | baseExpr = rhs.left; 120 | } 121 | else { 122 | baseExpr = rhs.right; 123 | } 124 | r = baseExpr; 125 | } 126 | if (line.operator == "+="){ 127 | r = line.right; 128 | }; 129 | }; 130 | 131 | if (line.type == "UpdateExpression") { 132 | r = MakeLiteral(1); 133 | }; 134 | 135 | // Done. 136 | return r; 137 | } 138 | 139 | function PullLoopUpperBound(fexpr) { 140 | // Pull the upper bound from the while test. 141 | return fexpr.test.right; 142 | } 143 | 144 | function PullLoopCounter(fexpr) { 145 | 146 | // Pull the loop counter from the while test. 147 | return fexpr.test.left; 148 | } 149 | 150 | function GenFinalLoopVal(fexpr) { 151 | /* 152 | For < loops, positive increment: 153 | upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 154 | 155 | For <= loops, positive increment: 156 | upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 157 | */ 158 | var upperBound = PullLoopUpperBound(fexpr); 159 | var loopCounter = PullLoopCounter(fexpr); 160 | var increment = PullIncrement(fexpr); 161 | // upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) 162 | var modExpr = MakeBinaryExpression( 163 | MakeMathSign(upperBound, loopCounter, increment), 164 | MakeBinaryExpression(increment, MakeLiteral(1), "-"), 165 | "*" 166 | ); 167 | var r = MakeBinaryExpression(upperBound, modExpr, "+"); 168 | if (fexpr.test.operator == "<=") { 169 | // !(Math.sign((y - x) % z) * (z - 1)); 170 | // upper_bound + Math.sign((upper_bound-initial_loop_counter)%increment)*(increment-1) + Math.sign(!Math.sign((upper_bound-initial_loop_counter)%increment)); 171 | var modExpr1 = MakeBinaryExpression(MakeBinaryExpression(upperBound, loopCounter, "-"), increment, "%"); 172 | var p0 = MakeMemberExpression("Math", "sign", [modExpr1]); 173 | var p1 = MakeNot(p0); 174 | var p2 = MakeMemberExpression("Math", "sign", [p1]); 175 | r = MakeBinaryExpression(r, p2, "+"); 176 | }; 177 | return r; 178 | } 179 | 180 | function MakeBinaryExpression(lhs, rhs, op) { 181 | // {"type":"BinaryExpression","start":30,"end":35,"left":{"type":"Identifier","start":30,"end":31,"name":"a"},"operator":"+","right":{"type":"Literal","start":34,"end":35,"value":1,"raw":"1"}} 182 | return { 183 | type: "BinaryExpression", 184 | left: lhs, 185 | right: rhs, 186 | operator: op 187 | }; 188 | }; 189 | 190 | function MakeLiteral(value) { 191 | return { 192 | type: "Literal", 193 | value: value 194 | }; 195 | }; 196 | 197 | function MakeIfThen(test, body) { 198 | return { 199 | type: "IfStatement", 200 | test: test, 201 | consequent: body 202 | }; 203 | }; 204 | 205 | function GenBreakIf(theVar, theVal) { 206 | const guard = MakeBinaryExpression(theVar, MakeLiteral(theVal), "=="); 207 | const breakStatement = { type: "BreakStatement" }; 208 | const r = MakeIfThen(guard, breakStatement); 209 | return r; 210 | } 211 | 212 | module.exports = (theVar, theVal) => (GenBreakIf(theVar, theVal)); 213 | -------------------------------------------------------------------------------- /_run.js: -------------------------------------------------------------------------------- 1 | const cp = require("child_process"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const walk = require("walk-sync"); 5 | const argv = require("./argv.js").run; 6 | 7 | // Just printing where to find default boilerplate code? 8 | if (argv["prepended-code"] == "show-default") { 9 | const defaultBP = __dirname + "/boilerplate.js" 10 | console.log(defaultBP); 11 | process.exit(0); 12 | } 13 | 14 | function list_delete(arr, item) { 15 | for( var i = 0; i < arr.length; i++){ 16 | 17 | if ( arr[i] === item) { 18 | arr.splice(i, 1); 19 | i--; 20 | } 21 | } 22 | return arr; 23 | } 24 | 25 | // Track whether we should return an error shell code or not. 26 | var single_sample = false; 27 | 28 | // Read and format JSON flag documentation 29 | if (argv.help || process.argv.length === 2) { 30 | const columnify = require("columnify"); 31 | console.log(`box-js is a utility to analyze malicious JavaScript files. 32 | 33 | Usage: 34 | box-js [flags] 35 | 36 | Pass a list of samples to be analyzed. Note that directories are searched 37 | recursively, so you can pass a directory that contains several samples and 38 | they will be analyzed in parallel. 39 | Creates one .results directory for each sample; see README.md for more 40 | information. 41 | 42 | Flags: 43 | `); 44 | console.log(columnify( 45 | require("./argv.js").flags.run.map((flag) => ({ 46 | name: (flag.alias ? `-${flag.alias}, ` : "") + `--${flag.name}`, 47 | description: flag.description, 48 | })), 49 | { 50 | config: { 51 | description: { 52 | maxWidth: 80, 53 | }, 54 | }, 55 | } 56 | )); 57 | process.exit(0); 58 | } 59 | 60 | if (argv.version) { 61 | console.log(require("./package.json").version); 62 | process.exit(0); 63 | } 64 | 65 | if (argv.license) { 66 | console.log(fs.readFileSync(__dirname + "/LICENSE", "utf8")); 67 | process.exit(0); 68 | } 69 | 70 | let timeout = argv.timeout; 71 | if (!timeout) { 72 | console.log("Using a 10 seconds timeout, pass --timeout to specify another timeout in seconds"); 73 | timeout = 10; 74 | } 75 | 76 | Array.prototype.functionalSplit = function(f) { 77 | // Call f on every item, put it in a if f returns true, put it in b otherwise. 78 | const a = []; 79 | const b = []; 80 | for (const elem of this) 81 | if (f(elem)) 82 | a.push(elem); 83 | else 84 | b.push(elem); 85 | return [a, b]; 86 | } 87 | 88 | const args = process.argv.slice(2); 89 | args.push(`--timeout=${timeout}`); 90 | 91 | const [targets, options] = args.functionalSplit(fs.existsSync); 92 | 93 | // Array of {filepath, filename} 94 | const tasks = []; 95 | 96 | var [folders, files] = targets.functionalSplit(path => fs.statSync(path).isDirectory()); 97 | 98 | // The output dir does not have samples to analyze. 99 | const outputDir = argv["output-dir"] || "./"; 100 | if (outputDir != "./") { 101 | folders = list_delete(folders, outputDir); 102 | } 103 | 104 | files 105 | .map(filepath => ({ 106 | filepath, 107 | filename: path.basename(filepath), 108 | })) 109 | .forEach(task => tasks.push(task)); 110 | 111 | folders 112 | .map(root => ({root, files: walk(root, {directories: false})})) 113 | .map(({root, files}) => files.map(file => root + "/" + file)) 114 | .reduce((a, b) => a.concat(b), []) // flatten 115 | .map(filepath => ({ 116 | filepath, 117 | filename: path.basename(filepath), 118 | })) 119 | .forEach(task => tasks.push(task)); 120 | 121 | if (tasks.length === 0) { 122 | console.log("Please pass one or more filenames or directories as an argument."); 123 | process.exit(255); 124 | } 125 | 126 | // Prevent "possible memory leak" warning 127 | process.setMaxListeners(Infinity); 128 | 129 | const q = require("queue")(); 130 | // Screw you, buggy option parser 131 | if (argv.threads === 0) q.concurrency = Infinity; 132 | else if (argv.threads) q.concurrency = argv.threads; 133 | else q.concurrency = require("os").cpus().length; 134 | 135 | if (tasks.length > 1) { // If batch mode 136 | if (argv.threads) { 137 | console.log(`Analyzing ${tasks.length} items with ${q.concurrency} threads`) 138 | } 139 | else { 140 | console.log(`Analyzing ${tasks.length} items with ${q.concurrency} threads (use --threads to change this value)`) 141 | } 142 | } 143 | 144 | // queue the input files for analysis 145 | tasks.forEach(({filepath, filename}) => q.push(cb => analyze(filepath, filename, cb))); 146 | 147 | let completed = 0; 148 | 149 | q.on("success", () => { 150 | completed++; 151 | if (tasks.length !== 1) 152 | console.log(`Progress: ${completed}/${tasks.length} (${(100 * completed/tasks.length).toFixed(2)}%)`); 153 | }); 154 | 155 | // Exit with a meaningful return code if we are only analyzing 1 sample. 156 | single_sample = (q.length == 1); 157 | 158 | // Start analyzing samples. 159 | q.start(); 160 | 161 | function analyze(filepath, filename, cb) { 162 | 163 | let directory = path.join(outputDir, filename + ".results"); 164 | 165 | // Find a suitable directory name 166 | for (let i = 1; fs.existsSync(directory); i++) 167 | directory = path.join(outputDir, filename + "." + i + ".results"); 168 | 169 | if (!argv["check"]) { 170 | fs.mkdirSync(directory); 171 | } 172 | directory += "/"; // For ease of use 173 | const worker = cp.fork(path.join(__dirname, "analyze"), [filepath, directory, ...options]); 174 | 175 | const killTimeout = setTimeout(() => { 176 | console.log(`Analysis for ${filename} timed out.`); 177 | if (!argv.preprocess) 178 | console.log("Hint: if the script is heavily obfuscated, --preprocess --unsafe-preprocess can speed up the emulation."); 179 | worker.kill(); 180 | // Useful analysis may have occurred. 181 | process.exit(0); 182 | cb(); 183 | }, timeout * 1000); 184 | 185 | let expectShellError = false; 186 | 187 | worker.on("message", function(message) { 188 | switch (message) { 189 | case "expect-shell-error": 190 | expectShellError = true; 191 | break; 192 | case "no-expect-shell-error": 193 | expectShellError = false; 194 | break; 195 | } 196 | }); 197 | 198 | worker.on("exit", function(code) { 199 | if (argv.debug && expectShellError) { 200 | // Use the appropriate exit code, as documented in the README 201 | process.exit(5); 202 | } 203 | if (code === 1) { 204 | console.log(` 205 | * If the error is about a weird \"Unknown ActiveXObject\", try --no-kill. 206 | * Otherwise, report a bug at https://github.com/CapacitorSet/box-js/issues/ .`); 207 | } 208 | if (code != 0) { 209 | final_code = code; 210 | } 211 | clearTimeout(killTimeout); 212 | worker.kill(); 213 | if (argv.debug || single_sample) process.exit(code); 214 | cb(); 215 | }); 216 | 217 | worker.on("error", function(err) { 218 | console.log("error!"); 219 | console.log(err); 220 | clearTimeout(killTimeout); 221 | worker.kill(); 222 | if (argv.debug) process.exit(1); 223 | cb(); 224 | }); 225 | 226 | process.on("exit", () => { 227 | worker.kill(); 228 | cb(); 229 | }); 230 | process.on("SIGINT", () => { 231 | worker.kill(); 232 | cb(); 233 | }); 234 | // process.on('uncaughtException', () => worker.kill()); 235 | } 236 | -------------------------------------------------------------------------------- /patch.js: -------------------------------------------------------------------------------- 1 | /* !!!! Patches from box-js !!!! */ 2 | const argv = require("./argv.js").run; 3 | let __PATCH_CODE_ADDED__ = true; 4 | window = this; 5 | 6 | _globalTimeOffset = 0; 7 | _sleepCount = 0; 8 | WScript.sleep = function(delay) { 9 | _globalTimeOffset += delay; 10 | _sleepCount++; 11 | } 12 | 13 | let fullYearGetter = Date.prototype.getFullYear; 14 | Date.prototype.getFullYear = function() { 15 | console.log("Warning: the script tried to read the current date."); 16 | console.log("If it doesn't work correctly (eg. fails to decrypt a string,"); 17 | console.log("try editing patch.js with a different year."); 18 | 19 | // return 2017; 20 | return fullYearGetter.call(this); 21 | }; 22 | Date.prototype.getYear = function() { 23 | return this.getFullYear(); 24 | }; 25 | Date.prototype.toString = function() { 26 | // Example format: Thu Aug 24 18:17:18 UTC+0200 2017 27 | const dayName = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.getDay()]; 28 | const monName = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][this.getMonth()]; 29 | return [ 30 | dayName, monName, this.getUTCDay(), 31 | this.getUTCHours() + ":" + this.getUTCMinutes() + ":" + this.getUTCSeconds(), 32 | "UTC-0500", // New York timezone 33 | this.getFullYear() 34 | ].join(" "); 35 | } 36 | let toLocaleStringGetter = Date.prototype.toLocaleString; 37 | Date.prototype.toLocaleString = function(lang, opts) { 38 | 39 | try { 40 | // Try doing the real toLocaleDateString() with the given args. 41 | return toLocaleStringGetter.call(this, lang, opts); 42 | } catch (e) { 43 | // Invalid args. cscript defaults to some sensible options in 44 | // this case, so return that result. 45 | const sensibleOpts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }; 46 | return toLocaleStringGetter.call(this, undefined, sensibleOpts).replace(" at ", " "); 47 | } 48 | }; 49 | Date.prototype.gethours = Date.prototype.getHours; 50 | Date.prototype.getminutes = Date.prototype.getMinutes; 51 | 52 | const legacyDate = Date; 53 | Date = function() { 54 | // Use a fixed old date for the current date if the --use-old-date 55 | // command line option is used. 56 | if (argv["use-old-date"] && (arguments.length == 0)) { 57 | arguments = [2017, 3, 6] 58 | } 59 | var proxiedDate = new legacyDate(...arguments); 60 | return new Proxy({ 61 | _actualTime: proxiedDate, 62 | }, { 63 | get: (target, prop) => { 64 | // Fast forward through time to foil anti-sandboxing busy 65 | // loops. The _myOffset field caches the faked time offset 66 | // used when the Date object was 1st looked at and 67 | // advances if so future date objects jump forward in 68 | // time. 69 | if (typeof(target._myOffset) == "undefined") { 70 | target._myOffset = _globalTimeOffset; 71 | _globalTimeOffset += 100; 72 | } 73 | const modifiedDate = new legacyDate(target._actualTime.getTime() + target._myOffset); 74 | if (prop === Symbol.toPrimitive) return hint => { 75 | switch (hint) { 76 | case "string": 77 | case "default": { 78 | return modifiedDate.toString(); 79 | } 80 | case "number": { 81 | return modifiedDate.getTime(); 82 | } 83 | default: 84 | throw new Error("Unknown hint!"); 85 | } 86 | } 87 | if (typeof prop !== "symbol") { 88 | if (!(prop in modifiedDate) && (prop in legacyDate)) return legacyDate[prop]; 89 | if (!(prop in legacyDate.prototype)) return undefined; 90 | } 91 | if (typeof(modifiedDate[prop]) === "undefined") return undefined; 92 | const boundFn = modifiedDate[prop].bind(modifiedDate); 93 | return function() { 94 | const ret = boundFn.apply(null, arguments); 95 | target._actualTime = new legacyDate(modifiedDate.getTime() - _globalTimeOffset); 96 | return ret; 97 | } 98 | } 99 | }); 100 | } 101 | Date.now = () => legacyDate.now() + _globalTimeOffset; 102 | Date.length = 7; 103 | Date.parse = legacyDate.parse; 104 | Date.UTC = legacyDate.UTC; 105 | Date.toString = () => legacyDate.toString() 106 | Date.valueOf = () => legacyDate.valueOf() 107 | 108 | Array.prototype.Count = function() { 109 | return this.length; 110 | }; 111 | 112 | let _OriginalFnToString = Function.prototype.toString; 113 | Function.prototype.toString = function() { 114 | /** 115 | * WSH's toString() looks a bit different for built-in functions 116 | * than result generated by Node.js (tabbed and wrapped with newlines) 117 | * which is sometimes checked by malicious scripts. 118 | */ 119 | let source = _OriginalFnToString.call(this); 120 | let r = source.replace( 121 | /^function (\S+) { \[native code\] }$/, 122 | ((m, fnName) => `\nfunction ${fnName} {\n [native code]\n}\n`) 123 | ) 124 | // Some obfuscators flag funcs with newlines. 125 | r = r.replace(/\n/g, "").replace(/{ +/g, "{").replace(/ +}/g, "}"); 126 | return r; 127 | } 128 | 129 | // Handle dynamic code executed via c("...") where c = SomeFunc.constructor. 130 | let _OriginalFnConstructor = Function.prototype.constructor; 131 | function _CreateFunc(...args) { 132 | let originalSource = args.pop(); 133 | let source; 134 | if (typeof originalSource === "function") { 135 | originalSource = originalSource.toString(); 136 | source = rewrite("(" + originalSource + ")"); 137 | } else if (typeof originalSource === "string") { 138 | 139 | // Sheesh. Looks like in a browser you can obfuscate a 140 | // function call like `(function 141 | // def...)..constructor("debugger").call("action")`, all this 142 | // does is call the defined function. 143 | // 144 | // Look for that case here. 145 | if (originalSource === "debugger") { 146 | var r = {}; 147 | r._func = this; 148 | r.call = function (cmd) { 149 | // Call the original function? 150 | if (cmd === "action") return this._func(); 151 | }; 152 | return r; 153 | } 154 | source = `/* Function arguments: ${JSON.stringify(args)} */\n` + rewrite(originalSource); 155 | } else { 156 | // What the fuck JS 157 | // For some reason, IIFEs result in a call to Function. 158 | //return new _OriginalFunction(...args, source); 159 | return new _OriginalFnConstructor(...args, source); 160 | } 161 | logJS(source); 162 | //return new _OriginalFunction(...args, source); 163 | return new _OriginalFnConstructor(...args, source); 164 | } 165 | 166 | Function.prototype.constructor = _CreateFunc; 167 | 168 | // Handle dynamic code executed via Function("..."). 169 | let _OriginalFunction = Function; 170 | Function = _CreateFunc; 171 | Function.toString = () => _OriginalFunction.toString() 172 | Function.valueOf = () => _OriginalFunction.valueOf() 173 | 174 | String.prototype.xstrx = function() { 175 | const hex = this.valueOf(); 176 | var str = ''; 177 | for (let i = 0; i < hex.length; i += 2) { 178 | const hexValue = hex.substr(i, 2); 179 | const decimalValue = parseInt(hexValue, 16); 180 | str += String.fromCharCode(decimalValue); 181 | } 182 | return str; 183 | } 184 | 185 | // Track the values of elements set by JQuery $("#q").val(...) uses. 186 | var jqueryVals = {}; 187 | 188 | // Fake up JQuery $("#q").val(...) uses. 189 | String.prototype.val = function(value) { 190 | if (!this.startsWith("#")) return; 191 | logIOC("JQuery", value, "The script used JQuery $(\"#q\").val(...) to set an element.") 192 | var name = this.slice(1); 193 | jqueryVals[name] = value; 194 | } 195 | 196 | // Fake up JQuery $("#q").fadeIn(...) uses. 197 | String.prototype.fadeIn = function() {}; 198 | 199 | Object.prototype.replace = function() { 200 | return ""; 201 | } 202 | 203 | constructor.prototype.bind = function(context, func) { 204 | const r = function() { 205 | if (typeof(func) !== "undefined") { 206 | return func.apply(context, arguments); 207 | } 208 | }; 209 | return r; 210 | }; 211 | Function.constructor.prototype.bind = constructor.prototype.bind; 212 | 213 | // Fake version of require() to fake importing some packages. 214 | /* 215 | let _origRequire = require; 216 | console.log(_origRequire); 217 | const require = function(pname) { 218 | if (pname == "requests") return fakeRequests; 219 | return _origRequire(pname); 220 | } 221 | */ 222 | 223 | /* End patches */ 224 | 225 | -------------------------------------------------------------------------------- /loop_rewriter.js: -------------------------------------------------------------------------------- 1 | const escodegen = require("escodegen"); 2 | 3 | function rewriteSimpleWaitLoop(key, val) { 4 | if (!val) return; 5 | 6 | // TODO: Currently only rewriting while() loops like 7 | // 'while(x < y) { x = x + 1 }'. 8 | 9 | // While loop? 10 | if (val.type != "WhileStatement") return; 11 | 12 | // Simple loop guard? 13 | if (val.test.type != "BinaryExpression") return; 14 | if (val.test.left.type != "Identifier") return; 15 | 16 | // Skip some benign loops we have trouble rewriting by checking the 17 | // loop index variable. Specifically skip loops where the loop index is 18 | // cardCountIndexFinal. 19 | if (val.test.left.name == "cardCountIndexFinal") return; 20 | 21 | // Only handling "<" and "<=" for now. 22 | if ((val.test.operator != "<") && (val.test.operator != "<=")) return; 23 | 24 | // Single statement in the loop body? 25 | if (val.body.type != "BlockStatement") return; 26 | if (val.body.body.length != 1) return; 27 | 28 | // Loop body statement is update to loop variable? 29 | line = val.body.body[0]; 30 | if (line.type != "ExpressionStatement") return; 31 | line = line.expression; 32 | if ((line.type != "AssignmentExpression") && (line.type != "UpdateExpression")) return; 33 | if (line.type == "AssignmentExpression") { 34 | if (line.left.type != "Identifier") return; 35 | if (line.left.name != val.test.left.name) return; 36 | if ((line.operator != "=") && (line.operator != "+=")) return; 37 | }; 38 | if (line.type == "UpdateExpression") { 39 | if (line.argument.type != "Identifier") return; 40 | if (line.argument.name != val.test.left.name) return; 41 | if (line.operator != "++") return; 42 | }; 43 | 44 | //console.log("----"); 45 | //console.log(JSON.stringify(val, null, 2)); 46 | r = require("./patches/counter_while_loop.js")(val); 47 | //console.log("REWRITE WAIT!!"); 48 | //console.log(JSON.stringify(r, null, 2)); 49 | //console.log(escodegen.generate(r)); 50 | return r; 51 | } 52 | 53 | function rewriteSimpleControlLoop(key, val) { 54 | if (!val) return; 55 | 56 | // While loop? 57 | if (val.type != "WhileStatement") return; 58 | 59 | // 2 statements in the loop body? Could also have a useless 60 | // assignment statement. 61 | if (val.body.type != "BlockStatement") return; 62 | if ((val.body.body.length != 2) && (val.body.body.length != 3)) return; 63 | 64 | // Should have 1 increment statement and 1 try catch in the loop 65 | // body. Figure out which is which. 66 | var line1 = val.body.body[0]; 67 | var line2 = val.body.body[1]; 68 | var line3 = "??"; 69 | if (val.body.body.length == 3) { 70 | line3 = val.body.body[2]; 71 | } 72 | 73 | // Any loop body statement is a try/catch? 74 | if ((line1.type != "TryStatement") && (line2.type != "TryStatement") && (line3.type != "TryStatement")) return; 75 | // Any loop body statement an expression? 76 | if ((line1.type != "ExpressionStatement") && (line2.type != "ExpressionStatement") && (line3.type != "ExpressionStatement")) return; 77 | 78 | // Carve out the try/catch and the expression. 79 | var exceptBlock; 80 | var updateStmt; 81 | if (line1.type == "TryStatement") exceptBlock = line1; 82 | if (line2.type == "TryStatement") exceptBlock = line2; 83 | if (line3.type == "TryStatement") exceptBlock = line3; 84 | if (line1.type == "ExpressionStatement") updateStmt = line1; 85 | if (line2.type == "ExpressionStatement") updateStmt = line2; 86 | if (line3.type == "ExpressionStatement") updateStmt = line3; 87 | line1 = exceptBlock; 88 | line2 = updateStmt; 89 | 90 | // 1 statement in try block? 91 | if (line1.block.type != "BlockStatement") return; 92 | if (line1.block.body.length != 1) return; 93 | line1 = line1.block.body[0]; 94 | 95 | // Possible calling funtion from array in try block? 96 | if (line1.type != "ExpressionStatement") return; 97 | line1 = line1.expression; 98 | if (line1.type == "AssignmentExpression") { 99 | line1 = line1.right; 100 | } 101 | if (line1.type != "CallExpression") return; 102 | if (line1.callee.type != "MemberExpression") return; 103 | 104 | // 1 or 2 statement in catch block. 105 | var catch_line = exceptBlock.handler; 106 | if ((catch_line == undefined) || (catch_line.type != "CatchClause")) return; 107 | var catch_body = catch_line.body; 108 | if (catch_body.type != "BlockStatement") return; 109 | if ((catch_body.body.length != 1) && (catch_body.body.length != 2)) return; 110 | catch_body = catch_body.body[0]; 111 | 112 | // Catch statement should be an assignment. 113 | if (catch_body.type != "ExpressionStatement") return; 114 | if (catch_body.expression.type != "AssignmentExpression") return; 115 | 116 | // 2nd loop body statement an assignment? 117 | if (line2.type != "ExpressionStatement") return; 118 | line2 = line2.expression; 119 | if ((line2.type != "AssignmentExpression") && (line2.type != "UpdateExpression")) return; 120 | 121 | // Is the expression statement in the loop body an update expression? 122 | if (typeof(line2.expression) !== "undefined") line2 = line2.expression; 123 | if ((line2.type != "AssignmentExpression") && (line2.type != "UpdateExpression")) return; 124 | 125 | // We have a certain type of control flow loop. Rewrite it so that exceptions are not 126 | // repeatedly thrown. 127 | //console.log("----"); 128 | //console.log(JSON.stringify(val, null, 2)); 129 | r = require("./patches/except_while_loop.js")(val, exceptBlock, updateStmt); 130 | //console.log("REWRITE CONTROL!!"); 131 | //console.log(JSON.stringify(r, null, 2)); 132 | //console.log(escodegen.generate(r)); 133 | return r; 134 | }; 135 | 136 | function rewriteLongWhileLoop(key, val) { 137 | if (!val) return; 138 | 139 | // TODO: Currently only rewriting while() loops like 140 | // 'while(true) { ...; if (val > BIG_NUMBER) break; }'. 141 | 142 | // While loop? 143 | if (val.type != "WhileStatement") return; 144 | 145 | // while(true) guard? 146 | if (escodegen.generate(val.test) !== "true") return; 147 | 148 | // Multiple statements in loop body? 149 | if (val.body.type != "BlockStatement") return; 150 | const body = val.body.body; 151 | 152 | // Look through each statement in the loop body for a if statement 153 | // like 'if (val > BIG_NUMBER) break;'. 154 | var newBody = []; 155 | var changed = false; 156 | for (i in body) { 157 | var currStatement = body[i]; 158 | 159 | // Break if statement? 160 | if ((currStatement.type == "IfStatement") && 161 | (currStatement.test.type == "BinaryExpression") && 162 | (currStatement.test.operator == "==") && 163 | (currStatement.consequent.type == "BreakStatement")) { 164 | 165 | // Checking to see if a variable is equal to a literal? 166 | left = currStatement.test.left 167 | right = currStatement.test.right 168 | var theVar; 169 | if (left.type == "Identifier") theVar = left; 170 | if (right.type == "Identifier") theVar = right; 171 | var theVal; 172 | if (left.type == "Literal") theVal = left; 173 | if (right.type == "Literal") theVal = right; 174 | 175 | // Is the value a big number? 176 | if ((typeof(theVar) !== "undefined") && 177 | (typeof(theVal) !== "undefined") && 178 | (theVal.value > 10000000)) { 179 | 180 | // Change the value being checked to a smaller number. 181 | currStatement = require("./patches/index_break_statement.js")(theVar, 1000000); 182 | changed = true; 183 | } 184 | } 185 | 186 | // How can nulls and functions show up?? 187 | if ((!currStatement) || (typeof(currStatement) !== "object")) continue; 188 | 189 | // Save the (maybe) modified statement for the loop body. 190 | newBody.push(currStatement); 191 | } 192 | 193 | // Did we modify the loop body? 194 | if (!changed) return; 195 | 196 | // Set up the new loop body. 197 | val.body.body = newBody; 198 | //console.log("----"); 199 | //console.log("REWRITE INDEX CHECK LOOP!!"); 200 | //console.log(JSON.stringify(val, null, 2)); 201 | //console.log(escodegen.generate(val)); 202 | return val 203 | } 204 | 205 | module.exports = { 206 | rewriteSimpleWaitLoop, 207 | rewriteSimpleControlLoop, 208 | rewriteLongWhileLoop, 209 | }; 210 | -------------------------------------------------------------------------------- /integrations/api/swagger.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Box-js API 4 | version: beta 5 | servers: 6 | - url: 'http://127.0.0.1:9000/' 7 | paths: 8 | '/sample/{id}': 9 | get: 10 | summary: Retrieves the analysis results 11 | parameters: 12 | - name: id 13 | required: true 14 | in: path 15 | description: >- 16 | The analysis ID, as a UUID (eg. 17 | `e483593a-4640-42cd-888d-0e97206e9131`) 18 | schema: 19 | type: string 20 | format: uuid 21 | responses: 22 | '200': 23 | description: OK 24 | content: 25 | application/json: 26 | schema: 27 | type: object 28 | properties: 29 | code: 30 | type: integer 31 | description: > 32 | Exit code of the analysis, as documented 33 | [here](https://github.com/CapacitorSet/box-js/tree/master/integrations#running-in-docker) 34 | stderr: 35 | type: string 36 | description: | 37 | Errors printed during the analysis, if any 38 | example: '' 39 | '400': 40 | description: Invalid ID. The ID must be an UUID. 41 | '404': 42 | description: 'Folder not found, or analysis not ready.' 43 | delete: 44 | summary: Deletes the analysis data 45 | parameters: 46 | - name: id 47 | required: true 48 | in: path 49 | description: >- 50 | The analysis ID, as a UUID (eg. 51 | `e483593a-4640-42cd-888d-0e97206e9131`) 52 | schema: 53 | type: string 54 | format: uuid 55 | responses: 56 | '200': 57 | description: OK (returns "1"). 58 | '400': 59 | description: Invalid ID. The ID must be an UUID. 60 | '404': 61 | description: >- 62 | Folder not found. The analysis may have already been deleted, or it 63 | never existed in the first place. 64 | '/sample/{id}/urls': 65 | get: 66 | summary: Returns the list of extracted URLs 67 | parameters: 68 | - name: id 69 | required: true 70 | in: path 71 | description: >- 72 | The analysis ID, as a UUID (eg. 73 | `e483593a-4640-42cd-888d-0e97206e9131`) 74 | schema: 75 | type: string 76 | format: uuid 77 | responses: 78 | '200': 79 | description: OK 80 | content: 81 | application/json: 82 | schema: 83 | type: array 84 | items: 85 | type: string 86 | example: 'http://malware.com/cryptolocker.exe' 87 | '400': 88 | description: Invalid ID. The ID must be an UUID. 89 | '404': 90 | description: >- 91 | File not found. Either the analysis was deleted, isn't yet ready, or 92 | no URL was extracted yet. 93 | '/sample/{id}/resources': 94 | get: 95 | summary: Returns the list of resources (i.e. files) 96 | parameters: 97 | - name: id 98 | required: true 99 | in: path 100 | description: >- 101 | The analysis ID, as a UUID (eg. 102 | `e483593a-4640-42cd-888d-0e97206e9131`) 103 | schema: 104 | type: string 105 | format: uuid 106 | responses: 107 | '200': 108 | description: OK 109 | content: 110 | application/json: 111 | schema: 112 | type: array 113 | items: 114 | type: object 115 | properties: 116 | : 117 | type: object 118 | properties: 119 | path: 120 | type: string 121 | description: The path in the emulated environment 122 | example: 'C:\Windows\malware.exe' 123 | type: 124 | type: string 125 | description: 'The file type, as reported by `file`' 126 | example: 'PE32 executable (GUI) Intel 80386, for MS Windows' 127 | md5: 128 | type: string 129 | description: The MD5 hash of the resource 130 | '400': 131 | description: Invalid ID. The ID must be an UUID. 132 | '404': 133 | description: >- 134 | File not found. Either the analysis was deleted, isn't yet ready, or 135 | no URL was extracted yet. 136 | '/sample/{id}/snippets': 137 | get: 138 | summary: Returns the list of snippets (i.e. all code that was executed) 139 | parameters: 140 | - name: id 141 | required: true 142 | in: path 143 | description: >- 144 | The analysis ID, as a UUID (eg. 145 | `e483593a-4640-42cd-888d-0e97206e9131`) 146 | schema: 147 | type: string 148 | format: uuid 149 | responses: 150 | '200': 151 | description: OK 152 | content: 153 | application/json: 154 | schema: 155 | type: array 156 | items: 157 | type: object 158 | properties: 159 | : 160 | type: object 161 | properties: 162 | as: 163 | type: string 164 | description: The type of code (JS or shell command) 165 | example: eval'd JS 166 | '400': 167 | description: Invalid ID. The ID must be an UUID. 168 | '404': 169 | description: File not found. Either the analysis was deleted or isn't ready yet. 170 | '/sample/{id}/raw/{filename}': 171 | get: 172 | summary: Returns a file from an analysis 173 | parameters: 174 | - name: id 175 | required: true 176 | in: path 177 | description: >- 178 | The analysis ID, as a UUID (eg. 179 | `e483593a-4640-42cd-888d-0e97206e9131`) 180 | schema: 181 | type: string 182 | format: uuid 183 | - name: filename 184 | required: true 185 | in: query 186 | description: The filename (eg. `analysis.log`) 187 | schema: 188 | type: string 189 | responses: 190 | '200': 191 | description: OK (returns the file). 192 | '400': 193 | description: Invalid ID. The ID must be an UUID. 194 | '404': 195 | description: >- 196 | File not found. Either the analysis was deleted, isn't yet ready or 197 | the file does not exist. 198 | /sample: 199 | post: 200 | summary: Uploads a sample 201 | responses: 202 | '200': 203 | description: OK (returns the sample ID) 204 | /concurrency: 205 | get: 206 | summary: >- 207 | Returns the concurrency level (i.e. how many analyses can run at the 208 | same time) 209 | responses: 210 | '200': 211 | description: OK (returns the concurrency level) 212 | post: 213 | summary: Updates the concurrency level 214 | requestBody: 215 | content: 216 | application/json: 217 | schema: 218 | type: object 219 | properties: 220 | value: 221 | type: integer 222 | description: The new concurrency level 223 | example: 4 224 | application/x-www-form-urlencoded: 225 | schema: 226 | type: object 227 | properties: 228 | value: 229 | type: integer 230 | description: The new concurrency level 231 | example: 4 232 | responses: 233 | '200': 234 | description: OK (returns "1") 235 | /debug/connectivity: 236 | get: 237 | summary: Returns "1" 238 | description: > 239 | This endpoint can be used to run connectivity checks, i.e. to ensure 240 | that you can reach the API server. Simply request /debug/connectivity; 241 | it should return "1". 242 | responses: 243 | '200': 244 | description: OK (returns "1") 245 | /debug/docker: 246 | get: 247 | summary: Runs a test container 248 | description: > 249 | This endpoint will try to create the container 250 | [`hello-world`](https://hub.docker.com/_/hello-world/) and verify that 251 | its output is correct. It is used to verify that the application can 252 | create containers successfully; an error may indicate that Docker wasn't 253 | installed correctly, or that the user running the API server can't 254 | create Docker containers because of incorrect permissions. 255 | responses: 256 | '200': 257 | description: OK (returns "1") 258 | '500': 259 | description: >- 260 | An error occurred creating the Docker container, read the output for 261 | more information. 262 | -------------------------------------------------------------------------------- /emulator/WScriptShell.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib.js"); 2 | const TextStream = require("./TextStream.js"); 3 | const argv = require("../argv.js").run; 4 | 5 | function WScriptShell() { 6 | 7 | this.clazz = "WScriptShell"; 8 | 9 | const vars = { 10 | /* %APPDATA% equals C:\Documents and Settings\{username}\Application Data on Windows XP, 11 | * but C:\Users\{username}\AppData\Roaming on Win Vista and above. 12 | */ 13 | appdata: argv["windows-xp"] 14 | ? "C:\\Documents and Settings\\User\\Application Data" 15 | : "C:\\Users\\User\\AppData\\Roaming", 16 | computername: "DOMAIN-CONTROLLER-1", 17 | comspec: "%SystemRoot%\\system32\\cmd.exe", 18 | os: "Windows_NT", 19 | processor_revision: "0209", 20 | processor_architecture: "x86", 21 | processor_architew6432: "AMD64", 22 | programdata: "C:\\ProgramData", 23 | systemroot: "C:\\WINDOWS", 24 | //tmp: "C:\\DOCUME~1\\User\\LOCALS~1\\Temp", 25 | tmp: "C:\\Users\\SYSOP1~1\\AppData\\Local\\Temp", 26 | //temp: "C:\\DOCUME~1\\User\\LOCALS~1\\Temp", 27 | temp: "C:\\Users\\SYSOP1~1\\AppData\\Local\\Temp", 28 | username: "Sysop12", 29 | userprofile: "C:\\Users\\Sysop12\\", 30 | windir: "C:\\WINDOWS" 31 | }; 32 | 33 | this._envVarLookup = function (argument) { 34 | argument = argument.toLowerCase(); 35 | if (argument in vars) return vars[argument]; 36 | // Return a fake value so all environment variable reads succeed? 37 | if (argv["fake-reg-read"]) return ("Unknown environment variable " + argument); 38 | lib.kill(`Unknown parameter ${argument} for WScriptShell.Environment.*`); 39 | }; 40 | 41 | this.environment = (x) => { 42 | if ((x.toLowerCase() === "system") || (x.toLowerCase() === "process")) { 43 | var r = this._envVarLookup; 44 | r.Item = function(x) { 45 | if (x.toLowerCase() === "programdata") 46 | return "C:\\ProgramData"; 47 | return "Unknown environment variable " + x; 48 | }; 49 | return r; 50 | } 51 | return `(Environment variable ${x})`; 52 | }; 53 | 54 | this.environment1 = undefined; 55 | this.specialfolders = (x) => `${x}`; 56 | this.createshortcut = function(shortcut) { 57 | 58 | // Thrown error for things that don't look like MS shortcuts. 59 | const shortcutS = shortcut.trim(); 60 | if (!shortcutS.endsWith(".lnk") && 61 | !shortcutS.endsWith(".URL") && 62 | !shortcutS.endsWith(".url")) throw "Shortcut '" + shortcutS + "' is invalid."; 63 | 64 | // Valid shortcut file name. Return a fake shortcut object. 65 | return { 66 | name: shortcut, 67 | Save: function() { 68 | var name = "???"; 69 | if (typeof(this.name) !== "undefined") { 70 | name = this.name; 71 | }; 72 | var cmd = "???"; 73 | if ((typeof(this.targetPath) !== "undefined") && (typeof(this.arguments) !== "undefined")) { 74 | cmd = "" + this.targetPath + " " + this.arguments; 75 | } 76 | lib.logIOC("CreateShortcut", {name: name, cmd: cmd}, "The script saved a shortcut."); 77 | } 78 | }; 79 | }; 80 | this.expandenvironmentstrings = (path) => { 81 | Object.keys(vars).forEach(key => { 82 | 83 | const regex = RegExp("%" + key + "%", "gi"); 84 | 85 | if (!regex.test(path)) return; 86 | 87 | lib.logIOC("Environ", key, "The script read an environment variable"); 88 | path = path.replace(regex, vars[key]); 89 | }); 90 | 91 | if (/%\w+%/i.test(path)) { 92 | lib.warning("Possibly failed to expand environment strings in " + path); 93 | } 94 | 95 | return path; 96 | }; 97 | 98 | this.run = cmd => { 99 | lib.runShellCommand(cmd); 100 | return 0; 101 | }; 102 | 103 | this.exec = function(cmd) { 104 | lib.runShellCommand(cmd); 105 | var r = { 106 | ExitCode: 1, 107 | ProcessID: Math.floor(Math.random() * 1000), 108 | Status: 1, // Finished 109 | StdErr: null, 110 | StdIn: { 111 | writeline: function(txt) { 112 | lib.logIOC("Run", txt, "The script piped text to a process: '" + txt + "'."); 113 | }, 114 | }, 115 | StdOut: new TextStream(``), 116 | }; 117 | return lib.noCasePropObj(r); 118 | }; 119 | 120 | if (!this._reg_entries) { 121 | this._reg_entries = require("system-registry"); 122 | 123 | // lacks the HKEY_CURRENT_USER reg key by default (y tho?) 124 | this._reg_entries["HKEY_CURRENT_USER"] = {} 125 | this._reg_entries["HKEY_CURRENT_USER"]["Control Panel"] = {"International" : {"Locale" : "0x407"}} 126 | } 127 | 128 | // expand registry acronyms and make lowercase 129 | function normalizeRegKey(key) { 130 | key = key 131 | .replace("HKLM", "HKEY_LOCAL_MACHINE") 132 | .replace("HKCR", "HKEY_CLASSES_ROOT") 133 | .replace("HKU", "HKEY_USERS") 134 | .replace("HKCU", "HKEY_CURRENT_USER") 135 | .replace("HKCC", "HKEY_CURRENT_CONFIG"); 136 | return key.toLowerCase(); 137 | }; 138 | 139 | // traverse registry object searching for the key 140 | this._resolveRegKey = (inKey) => { 141 | 142 | var inKeyParts = inKey.split("\\") 143 | var currRegEntry = this._reg_entries 144 | 145 | // compare the given key to the "this" value (see usage below) 146 | var keysEqual = function(key) { 147 | return normalizeRegKey(key) === normalizeRegKey(this) 148 | } 149 | 150 | for (inKeyPart of inKeyParts) { 151 | 152 | // give the part of the input key we're searching for as the "this" value of keysEqual 153 | var foundKey = Object.keys(currRegEntry).filter(keysEqual, inKeyPart) 154 | if (foundKey.length > 0) { 155 | currRegEntry = currRegEntry[foundKey[0]] 156 | } 157 | else { 158 | // Return a fake value so all registry reads succeed? 159 | if (argv["fake-reg-read"]) return "FAKE_REG_VALUE"; 160 | return undefined 161 | } 162 | } 163 | 164 | return currRegEntry 165 | } 166 | 167 | this.regread = (key) => { 168 | 169 | // log the IOC whether or not we handle the read correctly 170 | lib.logIOC("RegRead", {key}, "The script read a registry key"); 171 | value = this._resolveRegKey(key) 172 | 173 | if (value) { 174 | lib.verbose(`Read registry key ${key}`); 175 | return value 176 | } 177 | else { 178 | lib.warning(`Unknown registry key ${key}`); 179 | //return ""; 180 | throw("Registry key not found."); 181 | } 182 | }; 183 | 184 | this.regwrite = (key, value, type = "(unspecified)") => { 185 | 186 | // log the IOC whether or not we correctly handle it 187 | lib.logIOC("RegWrite", {key, value, type}, "The script wrote to a registry key"); 188 | 189 | var badKey = false 190 | var existingKey = key 191 | var existingRegEntry = undefined 192 | var keysToCreate = [] 193 | 194 | // find the deepest part of the given key that exists in our registry object 195 | do { 196 | existingRegEntry = this._resolveRegKey(existingKey) 197 | 198 | // if we've checked the very top level key and didn't find it 199 | if (existingKey.split("\\").length == 1 && !existingRegEntry) { 200 | lib.info("script tried to write to an invalid key root " + existingKey) 201 | badKey = true 202 | } 203 | 204 | // chop off the last element of the key path and try again 205 | // save the last part of the key that didn't exist as the key we need to create 206 | if (!existingRegEntry && !badKey) { 207 | keyParts = existingKey.split("\\") 208 | keysToCreate.unshift(keyParts.pop()) 209 | existingKey = keyParts.join("\\") 210 | } 211 | } while (!existingRegEntry && !badKey); 212 | 213 | if (!badKey) { 214 | // the key already existed, just need to overwrite the last element 215 | if (keysToCreate.length == 0) { 216 | keysToCreate.unshift(key.split("\\").pop()) 217 | } 218 | 219 | lib.info(`Setting registry key ${key} to ${value} of type ${type}`); 220 | 221 | // iterate through keys that need new nested objects 222 | while (keysToCreate.length > 1) { 223 | newKey = keysToCreate.shift() 224 | existingRegEntry[newKey] = {} 225 | existingRegEntry = existingRegEntry[newKey] 226 | } 227 | 228 | // set the value in our (possibly) newly created registry entry 229 | existingRegEntry[keysToCreate.shift()] = value 230 | } 231 | }; 232 | 233 | this.regdelete = (key) => { 234 | 235 | lib.logIOC("RegDelete", {key}, "The script deleted a registry key."); 236 | 237 | keyParts = key.split("\\") 238 | keyToDelete = keyParts.pop() 239 | pathtoKey = keyParts.join("\\") 240 | 241 | toDelete = this._resolveRegKey(pathtoKey) 242 | 243 | if (toDelete) { 244 | lib.info(`deleting registry key ${key}`); 245 | delete toDelete[keyToDelete] 246 | } 247 | else { 248 | lib.warning(`registry key not present ${key}`) 249 | } 250 | } 251 | 252 | this.appactivate = function(app) { 253 | lib.info(`Activate application '${app}'`); 254 | return true; 255 | }; 256 | 257 | this.sendkeys = function(keys) { 258 | lib.info(`Send keystrokes '${keys}'`); 259 | return true; 260 | }; 261 | 262 | this.popup = function(text, a, title = "[Untitled]", b) { 263 | if (!argv["no-echo"]) { 264 | lib.verbose(`Script opened a popup window: title "${title}", text "${text}"`); 265 | lib.verbose("Add flag --no-echo to disable this."); 266 | } 267 | //return true; // Emulates a click 268 | return 1; 269 | }; 270 | } 271 | 272 | module.exports = lib.proxify(WScriptShell, "WScriptShell"); 273 | -------------------------------------------------------------------------------- /emulator/FileSystemObject.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | const argv = require("../argv.js").run; 3 | const winpath = require("path").win32; 4 | const crypto = require('crypto'); 5 | 6 | function TextStream(filename) { 7 | this.buffer = lib.readFile(filename) || ""; 8 | this.uuid = lib.getUUID(); 9 | this.filename = filename; 10 | this.bufferarray = this.buffer.split("\n"); 11 | this.__atendofstream = function() { 12 | return (this.bufferarray.length === 0); 13 | }; 14 | // Call AtEndOfStream as property. 15 | Object.defineProperty(this, 'atendofstream', { 16 | get: () => this.__atendofstream(), 17 | }); 18 | this.close = () => {}; 19 | this.readall = () => { 20 | return this.buffer; 21 | }; 22 | this.readline = function() { 23 | if (this.bufferarray.length === 0) 24 | this.bufferarray = this.buffer.split("\n"); 25 | return this.bufferarray.shift(); 26 | }; 27 | this.shortpath = (path) => path; 28 | this.write = (line) => { 29 | this.buffer = this.buffer + line; 30 | lib.writeFile(filename, this.buffer); 31 | lib.logResource(this.uuid, this.filename, this.buffer); 32 | }; 33 | this.writeline = (line) => { 34 | this.buffer = this.buffer + line + "\r\n"; 35 | lib.writeFile(filename, this.buffer); 36 | lib.logResource(this.uuid, this.filename, this.buffer); 37 | }; 38 | } 39 | 40 | function ProxiedTextStream(filename) { 41 | return new Proxy(new TextStream(filename), { 42 | get: function(target, name) { 43 | name = name.toLowerCase(); 44 | if (name in target) return target[name]; 45 | lib.kill(`TextStream.${name} not implemented!`); 46 | }, 47 | set: function(a, b, c) { 48 | b = b.toLowerCase(); 49 | b = b.replace("bufferarray", ""); 50 | if (c.length < 1024 && !(c.length === 1 && c[0] === "")) 51 | lib.info(`FSObject[${b}] = ${c};`); 52 | a[b] = c; 53 | return true; 54 | }, 55 | }); 56 | } 57 | 58 | function makeFakeSubfolders(path) { 59 | 60 | // Make list of fake subfolders of the given path. 61 | var r = []; 62 | for (var x = 0; x < 6; x++) { 63 | r[x] = path + "\\_FAKE_BOXJS_FOLDER_" + x; 64 | } 65 | 66 | // Add a Count attrbute to the list to mimic ActiveX Subfolders object. 67 | Object.defineProperty(r, 'Count', { 68 | get: function() { return this.length } 69 | }); 70 | 71 | return r; 72 | } 73 | 74 | function Folder(path, autospawned) { 75 | this.attributes = 16; 76 | this.datelastmodified = new Date(new Date() - 15 * 60 * 1000); // Last changed: 15 minutes ago 77 | this.files = []; 78 | this.name = (path.replace(/\w:/i, "").match(/\\(\w*)(?:\\)?$/i) || [null, ""])[1], 79 | this.path = path; 80 | //this.subfolders = autospawned ? [] : [new ProxiedFolder(path + "\\RandomFolder", true)]; 81 | this.type = "folder"; 82 | this.subfolders = makeFakeSubfolders(this.path); 83 | } 84 | 85 | function ProxiedFolder(path, name, autospawned = false) { 86 | return new Proxy(new Folder(path, name, autospawned), { 87 | get: function(target, name) { 88 | name = name.toLowerCase(); 89 | if (name in target) return target[name]; 90 | lib.kill(`FileSystemObject.Folder.${name} not implemented!`); 91 | }, 92 | }); 93 | } 94 | 95 | function File(contents, name = "example-file.exe", typ = "Application") { 96 | lib.info("The sample created a file named '" + name + "'.") 97 | // Handle blobs/arrays. 98 | if (typeof(contents) === "undefined") contents = "???"; 99 | if ((contents.constructor.name == "Array") && (contents.length > 0)) { 100 | contents = contents[0]; 101 | } 102 | if (contents.constructor.name == "Blob") { 103 | contents = contents.data; 104 | } 105 | lib.writeFile(name, contents); 106 | this.uuid = crypto.randomUUID(); 107 | lib.logResource(this.uuid, name, contents); 108 | this.attributes = 32; 109 | this.openastextstream = () => new ProxiedTextStream(contents); 110 | this.shortpath = "C:\\PROGRA~1\\example-file.exe"; 111 | this._name = name; 112 | this.size = Infinity; 113 | this.type = typ; 114 | this.copy = (src, dest, overwrite) => { 115 | lib.logIOC("Copy", {src, dest}, "The script copied a file."); 116 | lib.info(`Copying ${src} to ${dest}`); 117 | lib.writeFile(dest, `(Contents of ${dest})`); 118 | }; 119 | } 120 | 121 | function ProxiedFile(filename) { 122 | var r = lib.proxify(File, "FileSystemObject.File"); 123 | Object.defineProperty(r, 'name', { 124 | set: function(v) { 125 | lib.info('The sample set a file name to "' + v + '".'); 126 | this._name = v; 127 | }, 128 | get: function(v) { 129 | return this._name; 130 | } 131 | }); 132 | Object.defineProperty(r, 'shortname', { 133 | get: function() { 134 | return this._name; 135 | } 136 | }); 137 | return r; 138 | } 139 | 140 | function Drive(name) { 141 | this.availablespace = 80*1024*1024*1024; 142 | this.drivetype = 2; 143 | this.filesystem = "NTFS"; 144 | this.serialnumber = 1234; 145 | this.volumename = name; 146 | this.path = name + "\\"; 147 | this.isready = true; 148 | } 149 | 150 | function ProxiedDrive(name) { 151 | return lib.proxify(Drive, "FileSystemObject.Drive"); 152 | } 153 | 154 | function FileSystemObject() { 155 | this.buildpath = (...args) => args.join("\\"); 156 | this.createfolder = (folder) => { 157 | lib.logIOC("FolderCreate", {folder}, "The script created a folder."); 158 | return "(Temporary new folder)"; 159 | } 160 | this.createtextfile = this.opentextfile = (filename) => new ProxiedTextStream(filename); 161 | this.copyfile = (src, dest, overwrite) => { 162 | lib.logIOC("FileCopy", {src, dest}, "The script copied a file."); 163 | lib.info(`Copying ${src} to ${dest}`); 164 | lib.writeFile(dest, `(Contents of ${dest})`); 165 | }; 166 | this.copy = this.copyfile; 167 | this.drives = [new ProxiedDrive("C:")]; 168 | this.deletefile = (path) => { 169 | lib.logIOC("FileDelete", {path}, "The script deleted a file."); 170 | return true; 171 | }; 172 | this.deletefolder = (path) => { 173 | lib.logIOC("FolderDelete", {path}, "The script deleted a folder."); 174 | return true; 175 | }; 176 | this.fileexists = (path) => { 177 | var value = !argv["no-file-exists"]; 178 | if (value) { 179 | lib.info(`Returning true for FileSystemObject.FileExists(${path}); use --no-file-exists if nothing happens`); 180 | } 181 | if (typeof(this._fileCheckCount) == "undefined") this._fileCheckCount = 0; 182 | this._fileCheckCount++; 183 | if (argv["limit-file-checks"] && (this._fileCheckCount > 10)) { 184 | // Flip whether the file exists or not to see if that 185 | // breaks a loop. 186 | value = !value; 187 | // Might break emulation based on WScript.quit(). Stop 188 | // ignoring WScript.quit(). 189 | lib.doWscriptQuit(true); 190 | } 191 | lib.logIOC("FileExists", path, "The script checked to see if a file exists."); 192 | return value; 193 | }; 194 | this.folderexists = (path) => { 195 | var value = !argv["no-folder-exists"]; 196 | if (value) { 197 | lib.info(`Returning true for FileSystemObject.FolderExists(${path}); use --no-folder-exists if nothing happens`); 198 | } 199 | if (typeof(this._fileCheckCount) == "undefined") this._fileCheckCount = 0; 200 | this._fileCheckCount++; 201 | if (argv["limit-file-checks"] && (this._fileCheckCount > 500)) { 202 | // Flip whether the file exists or not to see if that 203 | // breaks a loop. 204 | value = !value; 205 | // Might break emulation based on WScript.quit(). Stop 206 | // ignoring WScript.quit(). 207 | lib.doWscriptQuit(true); 208 | } 209 | lib.logIOC("FolderExists", path, "The script checked to see if a folder exists."); 210 | return value; 211 | }; 212 | this.getabsolutepathname = (path) => { 213 | if (!winpath.isAbsolute(path)) path = "C:\\Users\\User\\Desktop\\" + path; 214 | const ret = winpath.resolve(path); 215 | lib.logIOC("FileSystemObject", {"path": path, "absolute": ret}, "The script got an absolute path."); 216 | return ret; 217 | }; 218 | this.getdrive = (drive) => new ProxiedDrive(drive); 219 | this.getdrivename = (path) => { 220 | const matches = path.match(/^\w:/); 221 | if (matches === null) 222 | return ""; 223 | return matches[0]; 224 | }; 225 | this.getfile = function(filename) { 226 | var r = new ProxiedFile(filename); 227 | return r; 228 | }; 229 | this.getfilename = function(filename) { 230 | filename = "" + filename; 231 | const i = filename.lastIndexOf("\\"); 232 | if (i > 0) { 233 | filename = filename.slice(i + 1); 234 | } 235 | return filename; 236 | }; 237 | this.getfileversion = () => ""; 238 | this.getfolder = (str) => new ProxiedFolder(str); 239 | this.getspecialfolder = function(id) { 240 | const folders = { 241 | 0: "C:\\WINDOWS\\", 242 | 1: "C:\\WINDOWS\\(System folder)\\", 243 | 2: "C:\\(Temporary folder)\\", 244 | }; 245 | if (id in folders) return folders[id]; 246 | return `C:\\(Special folder ${id}\\`; 247 | }; 248 | this.gettempname = () => "(Temporary file)"; 249 | this.movefile = (src, dest, overwrite) => { 250 | lib.logIOC("FileMove", {src, dest}, "The script moved a file."); 251 | lib.info(`Moving ${src} to ${dest}`); 252 | lib.writeFile(dest, `(Contents of ${dest})`); 253 | }; 254 | this.getparentfoldername = (path) => { 255 | var r = path; 256 | if (r.includes("\\")) { 257 | var end = r.lastIndexOf("\\"); 258 | r = r.substring(0, end); 259 | } 260 | //return r; 261 | return "C:\\Temp\AppData" 262 | }; 263 | this.getextensionname = (path) => { 264 | var r = ""; 265 | if (path.includes(".")) { 266 | var start = path.lastIndexOf("."); 267 | r = path.substring(start, path.length); 268 | } 269 | return r; 270 | }; 271 | this.file = File; 272 | } 273 | 274 | module.exports = lib.proxify(FileSystemObject, "FileSystemObject"); 275 | -------------------------------------------------------------------------------- /integrations/export/export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const argv = require("../../argv.js").export; 3 | const child_process = require("child_process"); 4 | const fs = require("fs"); 5 | const RateLimiter = require("limiter").RateLimiter; 6 | const walk = require("walk-sync"); 7 | 8 | function lacksBinary(name) { 9 | const path = child_process.spawnSync("command", ["-v", name], {shell: true}).stdout; 10 | return path.length === 0; 11 | } 12 | if (lacksBinary("curl")) 13 | throw new Error("Curl must be installed."); 14 | 15 | function log(tag, text) { 16 | const levels = { 17 | "debug": 0, 18 | "verb": 1, 19 | "info": 2, 20 | "warn": 3, 21 | "error": 4, 22 | }; 23 | if (!(tag in levels)) { 24 | log("warn", `Application error: unknown logging tag ${tag}`, false); 25 | return; 26 | } 27 | if (!(argv.loglevel in levels)) { 28 | const oldLevel = argv.loglevel; // prevents infinite recursion 29 | argv.loglevel = "debug"; 30 | log("warn", `Log level ${oldLevel} is invalid (valid levels: ${Object.keys(levels).join(", ")}), defaulting to "info"`, false); 31 | } 32 | const level = levels[tag]; 33 | if (level < levels[argv.loglevel]) return; 34 | console.log(`[${tag}] ${text}`); 35 | } 36 | 37 | if (argv.help || process.argv.length === 2) { 38 | const columnify = require("columnify"); 39 | console.log(`box-export is a utility to submit the output of box-js to various services like 40 | Cuckoo Sandbox, VirusTotal and Malwr. 41 | 42 | Usage: 43 | box-export [flags] 44 | 45 | Pass a list of directories produced by box-js (eg. sample.js.results) and 46 | one or more analysis methods. 47 | Note that directories are searched recursively, so you can pass a directory 48 | that contains several .results directories. 49 | 50 | Flags: 51 | `); 52 | console.log(columnify( 53 | require("../../argv.js").flags.export.map((flag) => ({ 54 | name: (flag.alias ? `-${flag.alias}, ` : "") + `--${flag.name}`, 55 | description: flag.description, 56 | })), 57 | { 58 | config: { 59 | description: { 60 | maxWidth: 80, 61 | }, 62 | }, 63 | } 64 | )); 65 | process.exit(0); 66 | } 67 | 68 | if (argv.version) { 69 | console.log(require("../../package.json").version); 70 | process.exit(0); 71 | } 72 | 73 | if (argv.license) { 74 | console.log(fs.readFileSync(__dirname + "/../../LICENSE", "utf8")); 75 | process.exit(0); 76 | } 77 | 78 | Array.prototype.flatten = function() { 79 | return this.reduce((a, b) => a.concat(b), []); 80 | }; 81 | Array.prototype.uniqueStrings = function() { 82 | // O(n) check using an object as hash map 83 | // Works for arrays of strings, not guaranteed to work for anything else 84 | const unique = {}; 85 | for (const elem of this) 86 | unique[elem] = null; 87 | return Object.keys(unique); 88 | }; 89 | 90 | function request(method, URL, params, files) { 91 | const args = [ 92 | // Disables "Expect: 103 continue", useful for debugging 93 | // "-H", "Expect:", 94 | "--user-agent", "box-export (https://github.com/CapacitorSet/box-js/)", 95 | ]; 96 | if (method === "GET") { 97 | URL += "?" + Object.keys(params) 98 | .map(key => ({key, value: params[key]})) 99 | .map(({key, value}) => `${key}=${encodeURIComponent(value)}`) 100 | .join("&"); 101 | if (Object.keys(files).length !== 0) { // Allow an empty object 102 | log("error", "Tried to upload file(s) " + JSON.stringify(files) + " to endpoint " + URL); 103 | throw new Error("Cannot upload files in a GET request!"); 104 | } 105 | } else if (method === "POST") { 106 | for (const key of Object.keys(params)) 107 | args.push("--data", key + "=" + params[key]); 108 | for (const key of Object.keys(files)) 109 | args.push("--form", key + "=@" + files[key]); 110 | } 111 | 112 | args.push(URL); 113 | 114 | return child_process.spawnSync("curl", args); 115 | } 116 | 117 | function APISubmit(method, endpoint, params, files, limiter, cb) { 118 | const f = () => { 119 | request(method, endpoint, params, files); 120 | cb(); 121 | }; 122 | if (limiter) 123 | limiter.removeTokens(1, f); 124 | else 125 | f(); 126 | } 127 | 128 | function bulkAPISubmit( 129 | items, 130 | method, endpoint, 131 | paramFactory = () => ({}), fileFactory = () => ({}), 132 | setupMsg = total => `Submitting ${total} items`, progressMsg = "Items submitted:", 133 | limiter, rateLimit, unit) { 134 | const numItems = items.length; 135 | log("info", setupMsg(numItems)); 136 | let processedItems = 0; 137 | 138 | if (numItems <= rateLimit) 139 | log("info", `Due to API limits, this will take ${(numItems / rateLimit - 1).toFixed(1)} ${unit}s.`); 140 | for (const item of items) 141 | APISubmit(method, endpoint, paramFactory(item), fileFactory(item), limiter, () => { 142 | processedItems++; 143 | log("info", progressMsg + ` ${processedItems}/${numItems} (${(100 * processedItems/numItems).toFixed(2)}%)`); 144 | }); 145 | } 146 | 147 | const args = process.argv.slice(2); 148 | let folders = args.filter(fs.existsSync); 149 | 150 | log("info", "Reading file list..."); 151 | folders = folders 152 | .filter(item => { 153 | const ret = fs.statSync(item).isDirectory(); 154 | if (!ret) 155 | log("warn", `Ignoring argument "${item}" because it is not a directory`); 156 | return ret; 157 | }) 158 | .map(path => ({root: path, files: walk(path)})) 159 | .map(({root, files}) => files.map(file => root + "/" + file).concat(root)) 160 | .flatten() 161 | .filter(item => fs.statSync(item).isDirectory()); 162 | 163 | if (folders.length === 0) { 164 | log("error", "Please pass one or more filenames or directories as an argument."); 165 | process.exit(-1); 166 | } 167 | 168 | let cuckooAddress; 169 | if (argv["cuckoo-urls"] || argv["cuckoo-all-files"] || argv["cuckoo-executables"]) { 170 | if (!argv["cuckoo-address"]) 171 | throw new Error("Please enter a valid Cuckoo address (see --help for more information)."); 172 | cuckooAddress = argv["cuckoo-address"]; 173 | if (!/^http/i.test(cuckooAddress)) 174 | cuckooAddress = "http://" + cuckooAddress; 175 | } 176 | 177 | let malwrApiKey; 178 | // Malwr doesn't enforce rate limits, but let's be nice 179 | const malwrRateLimit = 1; 180 | let malwrLimiter; 181 | if (argv["malwr-all-files"] || argv["malwr-executables"]) { 182 | if (!argv["malwr-key"]) 183 | throw new Error("Please enter a valid API key for Malwr (see --help for more information)."); 184 | malwrApiKey = argv["malwr-key"]; 185 | malwrLimiter = new RateLimiter(malwrApiKey, "second"); 186 | } 187 | 188 | let vtApiKey; 189 | let vtRateLimit, vtLimiter; 190 | if (argv["vt-urls"] || argv["vt-all-files"] || argv["vt-executables"]) { 191 | if (!argv["vt-key"]) 192 | throw new Error("Please enter a valid API key for VirusTotal (see --help for more information)."); 193 | vtApiKey = argv["vt-key"]; 194 | // The public API is limited to 4 requests per minute. 195 | vtRateLimit = argv["vt-rate-limit"] || 4; 196 | vtLimiter = new RateLimiter(vtRateLimit, "minute"); 197 | } 198 | 199 | let urls, numUrls; 200 | if (argv["cuckoo-urls"] || argv["vt-urls"]) { 201 | log("info", "Parsing URLs..."); 202 | urls = folders 203 | .filter(item => { 204 | if (!fs.existsSync(item + "/urls.json")) { 205 | log("verb", `URL collection: discarding ${item} because it doesn't contain urls.json`); 206 | return false; 207 | } 208 | log("debug", `URL collection: folder ${item} is valid.`); 209 | return true; 210 | }) 211 | .map(item => JSON.parse(fs.readFileSync(item + "/urls.json"))) 212 | .flatten() 213 | .uniqueStrings(); 214 | numUrls = urls.length; 215 | } 216 | 217 | if (argv["cuckoo-urls"]) { 218 | bulkAPISubmit( 219 | urls, 220 | "POST", cuckooAddress + "/tasks/create/url", 221 | url => ({url}), undefined, 222 | num => `Submitting ${numUrls} URLs to Cuckoo`, "URLs submitted to Cuckoo:" 223 | ); 224 | } 225 | 226 | if (argv["vt-urls"]) { 227 | bulkAPISubmit( 228 | urls, 229 | "GET", "http://localhost:8000/test", 230 | url => ({apikey: vtApiKey, url}), undefined, 231 | num => `Submitting ${num} URLs to VirusTotal`, "URLs submitted to VirusTotal:", 232 | vtLimiter, vtRateLimit, "minute" 233 | ); 234 | } 235 | 236 | let allFiles = [], executables = []; 237 | if (argv["cuckoo-all-files"] || argv["cuckoo-executables"] 238 | || argv["malwr-all-files"] || argv["malwr-executables"] 239 | || argv["vt-all-files"] || argv["vt-executables"]) { 240 | for (const folder of folders) { 241 | if (!fs.existsSync(folder + "/resources.json")) { 242 | log("verb", `URL collection: discarding ${folder} because it doesn't contain resources.json`); 243 | continue; 244 | } 245 | log("debug", `URL collection: folder ${folder} is valid.`); 246 | const resourcesString = fs.readFileSync(folder + "/resources.json", "utf8"); 247 | if (resourcesString === "") continue; 248 | const resources = JSON.parse(resourcesString); 249 | for (const filename in resources) { 250 | if (!resources.hasOwnProperty(filename)) continue; 251 | const resource = resources[filename]; 252 | // If the resource was already inserted, skip. 253 | if (allFiles.some(file => file.sha256 === resource.sha256)) 254 | continue; 255 | allFiles.push({ 256 | path: folder + "/" + filename, 257 | emulatedPath: resource.path, 258 | type: resource.type, 259 | md5: resource.md5, 260 | sha1: resource.sha1, 261 | sha256: resource.sha256, 262 | }); 263 | } 264 | } 265 | executables = allFiles.filter(item => /executable/.test(item.type)); 266 | } 267 | 268 | if (argv["cuckoo-all-files"]) { 269 | bulkAPISubmit( 270 | allFiles, 271 | "POST", cuckooAddress + "/tasks/create/file", 272 | undefined, file => ({file: file.path}), 273 | num => `Submitting ${num} files to Cuckoo`, "Files submitted to Cuckoo:" 274 | ); 275 | } else if (argv["cuckoo-executables"]) { 276 | bulkAPISubmit( 277 | executables, 278 | "POST", cuckooAddress + "/tasks/create/file", 279 | undefined, file => ({file: file.path}), 280 | num => `Submitting ${num} files to Cuckoo`, "Files submitted to Cuckoo:" 281 | ); 282 | } 283 | 284 | if (argv["malwr-all-files"]) { 285 | bulkAPISubmit( 286 | allFiles, 287 | "POST", "http://localhost:8000/test", 288 | () => ({api_key: malwrApiKey, shared: !argv["malwr-private"]}), file => ({file: file.path}), 289 | num => `Submitting ${num} files to Malwr`, "Files submitted to Malwr:", 290 | malwrLimiter, malwrRateLimit, "second" 291 | ); 292 | } else if (argv["malwr-executables"]) { 293 | bulkAPISubmit( 294 | executables, 295 | "POST", "http://localhost:8000/test", 296 | () => ({api_key: malwrApiKey, shared: !argv["malwr-private"]}), file => ({file: file.path}), 297 | num => `Submitting ${num} files to Malwr`, "Files submitted to Malwr:", 298 | malwrLimiter, malwrRateLimit, "second" 299 | ); 300 | } 301 | 302 | if (argv["vt-all-files"]) { 303 | bulkAPISubmit( 304 | allFiles, 305 | "POST", "http://localhost:8000/test", 306 | () => ({apikey: vtApiKey}), file => ({file: file.path}), 307 | num => `Submitting ${num} files to VirusTotal`, "Files submitted to VirusTotal:", 308 | vtLimiter, vtRateLimit, "minute" 309 | ); 310 | } else { 311 | bulkAPISubmit( 312 | executables, 313 | "POST", "http://localhost:8000/test", 314 | () => ({apikey: vtApiKey}), file => ({file: file.path}), 315 | num => `Submitting ${num} files to VirusTotal`, "Files submitted to VirusTotal:", 316 | vtLimiter, vtRateLimit, "minute" 317 | ); 318 | } 319 | -------------------------------------------------------------------------------- /integrations/api/api.js: -------------------------------------------------------------------------------- 1 | const cp = require("child_process"); 2 | const fs = require("fs"); 3 | const express = require("express"); 4 | const bodyParser = require("body-parser"); 5 | const os = require("os"); 6 | const path = require("path"); 7 | const base_path = os.homedir(); 8 | const p = require("util").promisify; 9 | const uuid = require("uuid").v4; 10 | const lib = { 11 | getOutputFolder: id => path.join(base_path, "tmp-boxjs", "box-js-" + id), 12 | getDebugOutputDir: () => path.join(base_path, "test_dir") 13 | }; 14 | 15 | const app = express(); 16 | 17 | app.use(require("express-fileupload")()); 18 | // to support JSON-encoded bodies 19 | app.use(bodyParser.json()); 20 | // to support URL-encoded bodies 21 | app.use(bodyParser.urlencoded({ 22 | extended: true 23 | })); 24 | 25 | function spawn(proc, args, opts = {stdio: "pipe"}) { 26 | return new Promise((resolve, reject) => { 27 | const newProcess = cp.spawn(proc, args, opts); 28 | newProcess.on("error", reject); 29 | let stdout, stderr; 30 | if (opts.stdio === "pipe") { 31 | stdout = stderr = ""; 32 | newProcess.stdout.on("data", it => stdout += it); 33 | newProcess.stderr.on("data", it => stderr += it); 34 | } 35 | newProcess.on("close", code => resolve({ 36 | code, 37 | stdout, 38 | stderr 39 | })); 40 | }); 41 | } 42 | 43 | /* 44 | app.use((req, res, next) => { 45 | console.log("New request: " + req.originalUrl); 46 | next(); 47 | }); 48 | */ 49 | 50 | const q = require("queue")(); 51 | q.concurrency = os.cpus().length; 52 | q.autostart = true; 53 | 54 | /* GET /concurrency 55 | * 56 | * Returns the maximum number of analyses to be run at a time. 57 | */ 58 | app.get("/concurrency", (req, res) => res.send(String(q.concurrency))); 59 | 60 | /* POST /concurrency 61 | * 62 | * Arguments: 63 | * - value: the maximum number of analyses to be run at a time 64 | * 65 | * Returns "1". 66 | */ 67 | app.post("/concurrency", (req, res) => { 68 | q.concurrency = req.body.value; 69 | res.json({server_err: 0}); 70 | }); 71 | 72 | /* GET /debug/connectivity 73 | * 74 | * Returns "1". Used for checking connectivity to the API server. 75 | */ 76 | app.get("/debug/connectivity", (req, res) => { 77 | console.log("Connectivity check successful."); 78 | res.json({server_err: 0}); 79 | }); 80 | 81 | /* GET /debug/docker 82 | * 83 | * Returns "1" if a test container could be spinned successfully. 84 | * Used for debugging issues with Docker. 85 | */ 86 | app.get("/debug/docker", async (req, res) => { 87 | console.log("Docker check running..."); 88 | const proc = await spawn("docker", [ 89 | "run", 90 | "--rm", // Delete container fs after finishing 91 | "hello-world" 92 | ]); 93 | console.log(proc); 94 | const stderr = proc.stderr ? proc.stderr.toString() : ""; 95 | if (proc.code !== 0) { // Check for success status code 96 | console.log("Failed: status code is not zero."); 97 | console.log(proc.stderr); 98 | res.json({ 99 | server_err: 99, 100 | code: proc.code, 101 | stderr 102 | }); 103 | return; 104 | } 105 | if (!/Hello from Docker!/g.test(proc.stdout.toString())) { // Check for presence of hello world 106 | console.log("Failed: stdout does not contain hello world."); 107 | console.log(proc); 108 | const stdout = proc.stdout ? proc.stdout.toString() : ""; 109 | res.json({ 110 | server_err: 99, 111 | stdout, 112 | stderr 113 | }); 114 | return; 115 | } 116 | console.log("Successful."); 117 | res.json({server_err: 0}); 118 | }); 119 | 120 | /* GET /sample/:id 121 | * 122 | * Arguments: 123 | * - id: the analysis ID 124 | * 125 | * Returns 404 if the analysis is still taking place, or a JSON object if it is 126 | * finished. The keys are "status" for the exit code, and "stderr" for the 127 | * contents of stderr. 128 | */ 129 | app.get("/sample/:id", async (req, res) => { 130 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 131 | res.json({server_err: 1}); 132 | return; 133 | } 134 | 135 | const outputFolder = lib.getOutputFolder(req.params.id); 136 | if (!await p(fs.exists)(outputFolder)) { 137 | res.json({server_err: 2}); 138 | return; 139 | } 140 | 141 | const outputFile = path.join(outputFolder, ".analysis-completed"); 142 | if (!await p(fs.exists)(outputFile)) { 143 | res.json({server_err: 4}); 144 | return; 145 | } 146 | 147 | res.send(await p(fs.readFile)(outputFile)); 148 | }); 149 | 150 | /* POST /sample 151 | * 152 | * Arguments: 153 | * - sample: a file to be analyzed 154 | * - flags: a string containing any additional flags (eg. "--preprocess --unsafe-preprocess") 155 | * 156 | * Returns the analysis ID. 157 | */ 158 | app.post("/sample", async (req, res) => { 159 | if (!req.files) { 160 | res.json({server_err: 5}); 161 | return; 162 | } 163 | if (!req.body.flags) 164 | req.body.flags = ""; 165 | 166 | const analysisID = uuid(); 167 | const outputFolder = lib.getOutputFolder(analysisID); 168 | await p(fs.mkdir)(outputFolder); 169 | const outputFile = path.join(outputFolder, "sample.js"); 170 | console.log(`New sample received, saving to ${outputFile}`); 171 | res.json({ 172 | server_err: 0, 173 | analysisID 174 | }); 175 | req.files.sample.mv(outputFile, function(err) { 176 | if (err) { 177 | console.log("Couldn't receive uploaded file:"); 178 | console.log(err); 179 | return; 180 | } 181 | q.push(async cb => { 182 | console.log(`Analyzing ${analysisID}...`); 183 | const proc = await spawn("docker", [ 184 | "run", 185 | "--rm", // Delete container fs after finishing 186 | "--volume", outputFolder + ":/samples", // Volumes 187 | "box-js", // Image name 188 | ...("box-js /samples/* --output-dir=/samples --loglevel=debug --debug").split(" "), 189 | ...(req.body.flags.split(" ")) 190 | ]); 191 | 192 | const stderr = proc.stderr.toString(); 193 | if (proc.code !== 0) { // Check for success status code 194 | console.log(`Analysis for ${analysisID} failed, status code != 0.`); 195 | console.log("The error follows:"); 196 | console.log(stderr.replace(/^/gm, " | ")); 197 | } 198 | await p(fs.writeFile)(path.join(outputFolder, ".analysis-completed"), JSON.stringify({ 199 | server_err: 0, // successful 200 | code: proc.code, 201 | stderr 202 | })); 203 | console.log(`Analysis for ${analysisID} completed.`); 204 | cb(); 205 | }); 206 | }); 207 | }); 208 | 209 | /* DELETE /sample/:id 210 | * 211 | * Arguments: 212 | * - id: the analysis ID 213 | * 214 | * Deletes the given sample (or attempts to). Returns "1". 215 | */ 216 | app.delete("/sample/:id", async (req, res) => { 217 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 218 | res.json({server_err: 1}); 219 | return; 220 | } 221 | 222 | const outputFolder = lib.getOutputFolder(req.params.id); 223 | if (!await p(fs.exists)(outputFolder)) { 224 | res.json({server_err: 2}); 225 | return; 226 | } 227 | 228 | // We must delegate the directory removal to a setuid script. 229 | await spawn(path.join(__dirname, "rimraf.js"), [req.params.id]); 230 | res.json({server_err: 0}); 231 | }); 232 | 233 | /* GET /sample/:id/raw/:filename 234 | * 235 | * Arguments: 236 | * - id: the analysis ID 237 | * - filename: the filename (usually a UUID) 238 | * 239 | * Returns the given file from sample.results. 240 | */ 241 | app.get("/sample/:id/raw/:filename", async (req, res) => { 242 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 243 | res.json({server_err: 1}); 244 | return; 245 | } 246 | 247 | const outputFolder = lib.getOutputFolder(req.params.id); 248 | if (!await p(fs.exists)(outputFolder)) { 249 | res.json({server_err: 2}); 250 | return; 251 | } 252 | 253 | const outputFile = path.join(outputFolder, "sample.js.results", req.params.filename); 254 | if (!await p(fs.exists)(outputFile)) { 255 | res.json({server_err: 3}); 256 | return; 257 | } 258 | 259 | res.send(await p(fs.readFile)(outputFile)); 260 | }); 261 | 262 | /* GET /sample/:id/urls 263 | * 264 | * Arguments: 265 | * - id: the analysis ID 266 | * 267 | * Returns a JSON array of URLs extracted during the analysis. 268 | */ 269 | app.get("/sample/:id/urls", async (req, res) => { 270 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 271 | res.json({server_err: 1}); 272 | return; 273 | } 274 | 275 | const outputFolder = lib.getOutputFolder(req.params.id); 276 | if (!await p(fs.exists)(outputFolder)) { 277 | res.json({server_err: 2}); 278 | return; 279 | } 280 | 281 | const file = path.join(outputFolder, "sample.js.results", "urls.json"); 282 | if (!await p(fs.exists)(file)) { 283 | res.send("[]"); 284 | return; 285 | } 286 | 287 | res.send(await p(fs.readFile)(file, "utf8")); 288 | }); 289 | 290 | /* GET /sample/:id/resources 291 | * 292 | * Arguments: 293 | * - id: the analysis ID 294 | * 295 | * Returns a JSON object of resources extracted during the analysis. 296 | */ 297 | app.get("/sample/:id/resources", async (req, res) => { 298 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 299 | res.json({server_err: 1}); 300 | return; 301 | } 302 | 303 | const outputFolder = lib.getOutputFolder(req.params.id); 304 | if (!await p(fs.exists)(outputFolder)) { 305 | res.json({server_err: 2}); 306 | return; 307 | } 308 | 309 | const file = path.join(outputFolder, "sample.js.results", "resources.json"); 310 | if (!await p(fs.exists)(file)) { 311 | res.send("{}"); 312 | return; 313 | } 314 | 315 | res.send(await p(fs.readFile)(file, "utf8")); 316 | }); 317 | 318 | /* GET /sample/:id/snippets 319 | * 320 | * Arguments: 321 | * - id: the analysis ID 322 | * 323 | * Returns a JSON object of snippets executed during the analysis. 324 | */ 325 | app.get("/sample/:id/snippets", async (req, res) => { 326 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 327 | res.json({server_err: 1}); 328 | return; 329 | } 330 | 331 | const outputFolder = lib.getOutputFolder(req.params.id); 332 | if (!await p(fs.exists)(outputFolder)) { 333 | res.json({server_err: 2}); 334 | return; 335 | } 336 | 337 | const file = path.join(outputFolder, "sample.js.results", "snippets.json"); 338 | if (!await p(fs.exists)(file)) { 339 | res.send("[]"); 340 | return; 341 | } 342 | 343 | res.send(await p(fs.readFile)(file, "utf8")); 344 | }); 345 | 346 | /* GET /sample/:id/ioc 347 | * 348 | * Arguments: 349 | * - id: the analysis ID 350 | * 351 | * Returns a JSON object of IOCs (Indicators of Compromise) executed during the analysis. 352 | */ 353 | app.get("/sample/:id/ioc", async (req, res) => { 354 | if (!/^[0-9a-f-]+$/.test(req.params.id)) { 355 | res.json({server_err: 1}); 356 | return; 357 | } 358 | 359 | const outputFolder = lib.getOutputFolder(req.params.id); 360 | if (!await p(fs.exists)(outputFolder)) { 361 | res.json({server_err: 2}); 362 | return; 363 | } 364 | 365 | const file = path.join(outputFolder, "sample.js.results", "IOC.json"); 366 | if (!await p(fs.exists)(file)) { 367 | res.send("[]"); 368 | return; 369 | } 370 | 371 | res.send(await p(fs.readFile)(file, "utf8")); 372 | }); 373 | 374 | console.log("Building the latest version of box-js..."); 375 | spawn("docker", [ 376 | "build", 377 | "--no-cache", 378 | "-t", 379 | "box-js", 380 | "." 381 | ], { 382 | stdio: "inherit" // Show progress 383 | }).then(proc => { 384 | if (proc.code !== 0) 385 | throw new Error("Docker process returned exit code " + proc.code); 386 | }).catch(e => { 387 | console.error("Couldn't build the latest version of box-js."); 388 | if (e) { 389 | console.error(e); 390 | process.exit(1); 391 | } 392 | }).then(() => new Promise((resolve, reject) => app.listen( 393 | 9000, 394 | "127.0.0.1", // Use "0.0.0.0" to bind to all interfaces, or change as appropriate 395 | err => { 396 | if (err) reject(err); 397 | else resolve(); 398 | } 399 | ))).catch(e => { 400 | console.error("Couldn't initialize the Web server."); 401 | if (e) 402 | console.error(e); 403 | process.exit(2); 404 | }).then(() => { 405 | console.log("API server running!"); 406 | console.log(`Output folder: ${lib.getOutputFolder("")}`); 407 | q.start(); 408 | }); 409 | -------------------------------------------------------------------------------- /flags.json: -------------------------------------------------------------------------------- 1 | { 2 | "run": [ 3 | { 4 | "name": "help", 5 | "alias": "h", 6 | "type": "Boolean", 7 | "description": "Show the help text and quit" 8 | }, 9 | { 10 | "name": "version", 11 | "alias": "v", 12 | "type": "Boolean", 13 | "description": "Show the package version and quit" 14 | }, 15 | { 16 | "name": "license", 17 | "type": "Boolean", 18 | "description": "Show the license and quit" 19 | }, 20 | { 21 | "name": "debug", 22 | "type": "Boolean", 23 | "description": "Die when an emulation error occurs, even in \"batch mode\", and pass on the exit code." 24 | }, 25 | { 26 | "name": "loglevel", 27 | "type": "String", 28 | "description": "Logging level (debug, verbose, info, warning, error - default \"info\")" 29 | }, 30 | { 31 | "name": "threads", 32 | "type": "Number", 33 | "description": "When running in batch mode, how many analyses to run at the same time (0 = unlimited, default: as many as the number of CPU cores)" 34 | }, 35 | { 36 | "name": "download", 37 | "type": "Boolean", 38 | "description": "Actually download the payloads" 39 | }, 40 | { 41 | "name": "encoding", 42 | "type": "String", 43 | "description": "Encoding of the input sample (will be automatically detected by default)" 44 | }, 45 | { 46 | "name": "timeout", 47 | "type": "Number", 48 | "description": "The script will timeout after this many seconds (default 10)" 49 | }, 50 | { 51 | "name": "output-dir", 52 | "type": "String", 53 | "description": "The location on disk to write the results files and folders to (defaults to the current directory)" 54 | }, 55 | { 56 | "name": "preprocess", 57 | "type": "Boolean", 58 | "description": "Preprocess the original source code (makes reverse engineering easier, but takes a few seconds)" 59 | }, 60 | { 61 | "name": "unsafe-preprocess", 62 | "type": "Boolean", 63 | "description": "More aggressive preprocessing. Often results in better code, but can break on some edge cases (eg. redefining prototypes)" 64 | }, 65 | { 66 | "name": "prepended-code", 67 | "type": "String", 68 | "description": "Input file or directory containing code that should be prepended to the JS file(s) we're analyzing. If directory is given, prepends contents of all files in the directory. If 'default' is given use the default boilerplate.js that comes with box-js. if 'show-default' is given just print path of boilerplate.js and exit (useful if you want to copy and modify default boilerplate code)." 69 | }, 70 | { 71 | "name": "fake-script-engine", 72 | "type": "String", 73 | "description": "The script engine to report in WScript.FullName and WScript.Name (ex. 'cscript.exe', 'wscript.exe', or 'node'). Default is wscript.exe." 74 | }, 75 | { 76 | "name": "fake-cl-args", 77 | "type": "String", 78 | "description": "Fake script command line arguments. In the string these should be comma separated. Give '' for an empty argument in the list." 79 | }, 80 | { 81 | "name": "fake-sample-name", 82 | "type": "String", 83 | "description": "Fake file name to use for the sample being analyzed. Can be a full path or just the file name to use. If you have '\\' in the path escape them as '\\\\' in this command line argument value (ex. --fake-sample-name=C:\\\\foo\\\\bar.js)." 84 | }, 85 | { 86 | "name": "fake-language", 87 | "type": "String", 88 | "description": "Specify the language code to return for Win32_OperatingSystem.OSLanguage. Supported values are 'spanish', 'english', and 'portuguese'." 89 | }, 90 | { 91 | "name": "fake-domain", 92 | "type": "String", 93 | "description": "Specify the user domain to return for WScript.Network.UserDomain." 94 | }, 95 | { 96 | "name": "fake-download", 97 | "type": "Boolean", 98 | "description": "Fake that HTTP requests work and have them return a fake payload" 99 | }, 100 | { 101 | "name": "fake-reg-read", 102 | "type": "Boolean", 103 | "description": "Return fake values for registry reads of keys that are unknown to box-js. Also return fake values for environment variable reads." 104 | }, 105 | { 106 | "name": "no-kill", 107 | "type": "Boolean", 108 | "description": "Do not kill the application when runtime errors occur" 109 | }, 110 | { 111 | "name": "no-echo", 112 | "type": "Boolean", 113 | "description": "When the script prints data, do not print it to the console" 114 | }, 115 | { 116 | "name": "no-rewrite", 117 | "type": "Boolean", 118 | "description": "Do not rewrite the source code at all, other than for `@cc_on` support" 119 | }, 120 | { 121 | "name": "no-catch-rewrite", 122 | "type": "Boolean", 123 | "description": "Do not rewrite try..catch clauses to make the exception global-scoped" 124 | }, 125 | { 126 | "name": "no-cc_on-rewrite", 127 | "type": "Boolean", 128 | "description": "Do not rewrite `/*@cc_on <...>@*/` to `<...>`" 129 | }, 130 | { 131 | "name": "no-eval-rewrite", 132 | "type": "Boolean", 133 | "description": "Do not rewrite `eval` so that its argument is rewritten" 134 | }, 135 | { 136 | "name": "no-file-exists", 137 | "type": "Boolean", 138 | "description": "Return `false` for Scripting.FileSystemObject.FileExists(x)" 139 | }, 140 | { 141 | "name": "limit-file-checks", 142 | "type": "Boolean", 143 | "description": "Switch default value for folder/file exists checks if many checks are performed (try to break infinite file check loops)." 144 | }, 145 | { 146 | "name": "no-folder-exists", 147 | "type": "Boolean", 148 | "description": "Return `false` for Scripting.FileSystemObject.FileExists(x)" 149 | }, 150 | { 151 | "name": "function-rewrite", 152 | "type": "Boolean", 153 | "description": "Rewrite function calls in order to catch eval calls" 154 | }, 155 | { 156 | "name": "no-rewrite-prototype", 157 | "type": "Boolean", 158 | "description": "Do not rewrite expressions like `function A.prototype.B()` as `A.prototype.B = function()`" 159 | }, 160 | { 161 | "name": "no-hoist-prototype", 162 | "type": "Boolean", 163 | "description": "Do not hoist expressions like `function A.prototype.B()` (implied by no-rewrite-prototype)" 164 | }, 165 | { 166 | "name": "no-shell-error", 167 | "type": "Boolean", 168 | "description": "Do not throw a fake error when executing `WScriptShell.Run` (it throws a fake error by default to pretend that the distribution sites are down, so that the script will attempt to poll every site)" 169 | }, 170 | { 171 | "name": "no-typeof-rewrite", 172 | "type": "Boolean", 173 | "description": "Do not rewrite `typeof` (e.g. `typeof ActiveXObject`, which must return 'unknown' in the JScript standard and not 'object')" 174 | }, 175 | { 176 | "name": "proxy", 177 | "type": "String", 178 | "description": "[experimental] Use the specified proxy for downloads. This is not relevant if the --download flag is not present." 179 | }, 180 | { 181 | "name": "windows-xp", 182 | "type": "Boolean", 183 | "description": "Emulate Windows XP (influences the value of environment variables)" 184 | }, 185 | { 186 | "name": "dangerous-vm", 187 | "type": "Boolean", 188 | "description": "Use the `vm` module, rather than `vm2`. This sandbox can be broken, so **don't use this** unless you're 100% sure of what you're doing. Helps with debugging by giving correct stack traces." 189 | }, 190 | { 191 | "name": "rewrite-loops", 192 | "type": "Boolean", 193 | "description": "Rewrite some types of loops to make analysis faster" 194 | }, 195 | { 196 | "name": "throttle-writes", 197 | "type": "Boolean", 198 | "description": "Throttle reporting and data tracking of file writes that write a LOT of data" 199 | }, 200 | { 201 | "name": "throttle-commands", 202 | "type": "Boolean", 203 | "description": "Stop the analysis if a LOT of the same commands have been run" 204 | }, 205 | { 206 | "name": "extract-conditional-code", 207 | "type": "Boolean", 208 | "description": "Pull the actual code to analyze from JScript conditional comments (/*@if(...)." 209 | }, 210 | { 211 | "name": "loose-script-name", 212 | "type": "Boolean", 213 | "description": "Rewrite == checks so that comparisons of the current script name to a hard coded script name always return true." 214 | }, 215 | { 216 | "name": "real-script-name", 217 | "type": "Boolean", 218 | "description": "Return the real file name of the currently analyzed script rather than a fake name." 219 | }, 220 | { 221 | "name": "activex-as-ioc", 222 | "type": "Boolean", 223 | "description": "Logs All ActiveX calls as IOC's and tries to determine if the call is obfuscated in the JS source." 224 | }, 225 | { 226 | "name": "ignore-wscript-quit", 227 | "type": "Boolean", 228 | "description": "Ignore calls to WSCript.Quit() and continue execution." 229 | }, 230 | { 231 | "name": "ignore-rewrite-errors", 232 | "type": "Boolean", 233 | "description": "Analyze original sample if any sample rewrites fail." 234 | }, 235 | { 236 | "name": "use-old-date", 237 | "type": "Boolean", 238 | "description": "Lie about the current date/time and return an old date." 239 | }, 240 | { 241 | "name": "check", 242 | "alias": "c", 243 | "type": "Boolean", 244 | "description": "Just check the JS syntax of the sample and exit." 245 | } 246 | ], 247 | "export": [ 248 | { 249 | "name": "help", 250 | "alias": "h", 251 | "type": "Boolean", 252 | "description": "Show the help text and quit" 253 | }, 254 | { 255 | "name": "version", 256 | "alias": "v", 257 | "type": "Boolean", 258 | "description": "Show the package version and quit" 259 | }, 260 | { 261 | "name": "license", 262 | "type": "Boolean", 263 | "description": "Show the license and quit" 264 | }, 265 | { 266 | "name": "loglevel", 267 | "type": "String", 268 | "description": "Logging level (debug, verbose, info, warning, error - default \"info\")" 269 | }, 270 | { 271 | "name": "cuckoo-address", 272 | "type": "String", 273 | "description": "Address of the Cuckoo API (format: host:port, eg. localhost:8000)" 274 | }, 275 | { 276 | "name": "cuckoo-all-files", 277 | "type": "Boolean", 278 | "description": "Submit all files written to disk to a Cuckoo sandbox" 279 | }, 280 | { 281 | "name": "cuckoo-executables", 282 | "type": "Boolean", 283 | "description": "Submit all executables written to disk to a Cuckoo sandbox. Has no effect when used with --cuckoo-all-files." 284 | }, 285 | { 286 | "name": "cuckoo-urls", 287 | "type": "Boolean", 288 | "description": "Submit URLs to a Cuckoo sandbox" 289 | }, 290 | { 291 | "name": "malwr-key", 292 | "type": "String", 293 | "description": "Malwr API key" 294 | }, 295 | { 296 | "name": "malwr-all-files", 297 | "type": "Boolean", 298 | "description": "Submit all files written to disk to Malwr" 299 | }, 300 | { 301 | "name": "malwr-executables", 302 | "type": "Boolean", 303 | "description": "Submit all executables written to disk to Malwr. Has no effect when used with --malwr-all-files." 304 | }, 305 | { 306 | "name": "malwr-private", 307 | "type": "Boolean", 308 | "description": "Mark Malwr samples as private (not to be shared with the community)" 309 | }, 310 | { 311 | "name": "vt-key", 312 | "type": "String", 313 | "description": "VirusTotal API key" 314 | }, 315 | { 316 | "name": "vt-all-files", 317 | "type": "Boolean", 318 | "description": "Submit all files written to disk to VirusTotal" 319 | }, 320 | { 321 | "name": "vt-executables", 322 | "type": "Boolean", 323 | "description": "Submit all executables written to disk to VirusTotal. Has no effect when used with --vt-all-files." 324 | }, 325 | { 326 | "name": "vt-rate-limit", 327 | "type": "Number", 328 | "description": "Rate limit (in requests per minute) against the VirusTotal API (default 4)" 329 | }, 330 | { 331 | "name": "vt-urls", 332 | "type": "Boolean", 333 | "description": "Submit URLs to VirusTotal" 334 | } 335 | ] 336 | } 337 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | const crypto = require("crypto"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const request = require("sync-request"); 6 | const uuid = require("uuid"); 7 | const argv = require("./argv.js").run; 8 | const fakeFiles = require("./emulator/FakeFiles"); 9 | 10 | const directory = path.normalize(process.argv[3]); 11 | 12 | const urls = []; 13 | const activeUrls = []; 14 | const snippets = {}; 15 | const resources = {}; 16 | const files = {}; 17 | const IOC = []; 18 | 19 | let latestUrl = ""; 20 | 21 | function simpleStringify(object, a, b) { 22 | if (object && typeof object === 'object') { 23 | const tmpObject = copyWithoutCircularReferences([object], object); 24 | var newObject = []; 25 | for (var i = 0; i < Object.keys(tmpObject).length; i++) { 26 | newObject.push(tmpObject[""+i]); 27 | } 28 | object = newObject; 29 | } 30 | return JSON.stringify(object, a, b); 31 | 32 | function copyWithoutCircularReferences(references, object) { 33 | var cleanObject = {}; 34 | Object.keys(object).forEach(function(key) { 35 | var value = object[key]; 36 | if (value && typeof value === 'object') { 37 | if (references.indexOf(value) < 0) { 38 | references.push(value); 39 | cleanObject[key] = copyWithoutCircularReferences(references, value); 40 | references.pop(); 41 | } else { 42 | cleanObject[key] = '###_Circular_###'; 43 | } 44 | } else if (typeof value !== 'function') { 45 | cleanObject[key] = value; 46 | } 47 | }); 48 | return cleanObject; 49 | } 50 | } 51 | 52 | const logSnippet = function(filename, logContent, content) { 53 | snippets[filename] = logContent; 54 | fs.writeFileSync(path.join(directory, filename), "" + content); 55 | fs.writeFileSync(path.join(directory, "snippets.json"), simpleStringify(snippets, null, "\t")); 56 | }; 57 | 58 | function kill(message) { 59 | if (argv["no-kill"]) 60 | throw new Error(message); 61 | console.trace(message); 62 | console.log("Exiting (use --no-kill to just simulate a runtime error)."); 63 | process.exit(0); 64 | } 65 | 66 | function log(tag, text, toFile = true, toStdout = true) { 67 | if (argv["check"]) return; 68 | const levels = { 69 | "debug": 0, 70 | "verb": 1, 71 | "info": 2, 72 | "warn": 3, 73 | "error": 4, 74 | }; 75 | if (!(tag in levels)) { 76 | log("warn", `Application error: unknown logging tag ${tag}`, false); 77 | return; 78 | } 79 | if (!(argv.loglevel in levels)) { 80 | const oldLevel = argv.loglevel; // prevents infinite recursion 81 | argv.loglevel = "debug"; 82 | log("warn", `Log level ${oldLevel} is invalid (valid levels: ${Object.keys(levels).join(", ")}), defaulting to "info"`, false); 83 | } 84 | const level = levels[tag]; 85 | if (level < levels[argv.loglevel]) return; 86 | const message = `[${tag}] ${text}`; 87 | if (toStdout || argv.loglevel === "debug") // Debug level always writes to stdout and file 88 | console.log(message); 89 | if (toFile || argv.loglevel === "debug") 90 | fs.appendFileSync(path.join(directory, "analysis.log"), message + "\n"); 91 | } 92 | 93 | function hash(algo, string) { 94 | return crypto.createHash(algo).update(string).digest("hex"); 95 | } 96 | 97 | const getUUID = uuid.v4; 98 | 99 | function logIOC(type, value, description) { 100 | log("info", "IOC: " + description); 101 | IOC.push({type, value, description}); 102 | fs.writeFileSync(path.join(directory, "IOC.json"), simpleStringify(IOC, null, "\t")); 103 | } 104 | 105 | function logUrl(method, url) { 106 | log("info", `${method} ${url}`); 107 | latestUrl = url; 108 | if (urls.indexOf(url) === -1) urls.push(url); 109 | fs.writeFileSync(path.join(directory, "urls.json"), simpleStringify(urls, null, "\t")); 110 | } 111 | 112 | // Track the # of times we have seen a file written to so we don't spam 113 | // emulation output. 114 | const MAXWRITES = 10; 115 | fileWriteCount = {}; 116 | 117 | // If needed stop writing bytes to very large files. 118 | const MAXBYTES = 1e+6 * 10; // 10MB 119 | resourceWriteCount = {}; 120 | var throttleWrites = false; 121 | tooBigFiles = {}; 122 | 123 | // Function for enabling/disabling file write throttling. 124 | function throttleFileWrites(val) { 125 | throttleWrites = val; 126 | }; 127 | 128 | // If needed stop writing lots of the same sorts of commands. 129 | var _throttleCommands = false; 130 | var numCommandsRun = 0; 131 | const maxCommands = 5000; 132 | const maxPrefixCommands = 500; 133 | var commandPrefixCounts = {}; 134 | 135 | // Function for enabling/disabling command throttling. 136 | function throttleCommands(val) { 137 | _throttleCommands = val; 138 | }; 139 | 140 | function noCasePropObj(obj) 141 | { 142 | var handler = 143 | { 144 | get: function(target, key) 145 | { 146 | //console.log("key: " + key.toString()); 147 | if (typeof key == "string") 148 | { 149 | var uKey = key.toUpperCase(); 150 | 151 | if ((key != uKey) && (key in target)) 152 | return target[key]; 153 | return target[uKey]; 154 | } 155 | return target[key]; 156 | }, 157 | set: function(target, key, value) 158 | { 159 | if (typeof key == "string") 160 | { 161 | var uKey = key.toUpperCase(); 162 | 163 | if ((key != uKey) && (key in target)) 164 | target[key] = value; 165 | target[uKey] = value; 166 | } 167 | else 168 | target[key] = value; 169 | }, 170 | deleteProperty: function(target, key) 171 | { 172 | if (typeof key == "string") 173 | { 174 | var uKey = key.toUpperCase(); 175 | 176 | if ((key != uKey) && (key in target)) 177 | delete target[key]; 178 | if (uKey in target) 179 | delete target[uKey]; 180 | } 181 | else 182 | delete target[key]; 183 | }, 184 | }; 185 | function checkAtomic(value) 186 | { 187 | if (typeof value == "object") 188 | return new noCasePropObj(value); // recursive call only for Objects 189 | return value; 190 | } 191 | 192 | var newObj; 193 | 194 | if (typeof obj == "object") 195 | { 196 | newObj = new Proxy({}, handler); 197 | // traverse the Original object converting string keys to upper case 198 | for (var key in obj) 199 | { 200 | if (typeof key == "string") 201 | { 202 | var objKey = key.toUpperCase(); 203 | 204 | if (!(key in newObj)) 205 | newObj[objKey] = checkAtomic(obj[key]); 206 | } 207 | } 208 | } 209 | else if (Array.isArray(obj)) 210 | { 211 | // in an array of objects convert to upper case string keys within each row 212 | newObj = new Array(); 213 | for (var i = 0; i < obj.length; i++) 214 | newObj[i] = checkAtomic(obj[i]); 215 | } 216 | return newObj; // object with upper cased keys 217 | }; 218 | 219 | _doWscriptQuit = false; 220 | function doWscriptQuit(flag) { 221 | if (typeof(flag) != "undefined") _doWscriptQuit = flag; 222 | return _doWscriptQuit; 223 | } 224 | 225 | module.exports = { 226 | argv, 227 | kill, 228 | getUUID, 229 | throttleFileWrites, 230 | throttleCommands, 231 | noCasePropObj, 232 | doWscriptQuit, 233 | 234 | debug: log.bind(null, "debug"), 235 | verbose: log.bind(null, "verb"), 236 | info: log.bind(null, "info"), 237 | warning: log.bind(null, "warn"), 238 | error: log.bind(null, "error"), 239 | 240 | proxify: (actualObject, objectName = "") => { 241 | /* Creating a Proxy is a common operation, because they normalize property names 242 | * and help catch unimplemented features. This function implements this behaviour. 243 | */ 244 | return new Proxy(new actualObject, { 245 | get: function(target, prop) { 246 | const lProp = prop.toLowerCase(); 247 | if (lProp in target) return target[lProp]; 248 | kill(`${objectName}.${prop} not implemented!`); 249 | }, 250 | set: function(a, b, c) { 251 | b = b.toLowerCase(); 252 | a[b] = c; 253 | return true; 254 | }, 255 | }); 256 | }, 257 | fetchUrl: function(method, url, headers = {}, body) { 258 | // Ignore HTTPS errors 259 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 260 | logUrl(method, url); 261 | logIOC("UrlFetch", {method, url, headers, body}, "The script fetched an URL."); 262 | 263 | // Fake that the request worked? 264 | if (argv["fake-download"]) { 265 | log("info", "Returning HTTP 200 (Success) with fake response payload 'console.log(\"EXECUTED DOWNLOADED PAYLOAD\");'"); 266 | return { 267 | body: new Buffer("console.log(\"EXECUTED DOWNLOADED PAYLOAD\");"), 268 | headers: {}, 269 | }; 270 | } 271 | 272 | // Do no download, say that it failed. 273 | if (!argv.download) { 274 | log("info", "Returning HTTP 404 (Not found); use --download to actually try to download the payload or --fake-download to fake the download"); 275 | return { 276 | body: new Buffer(""), 277 | headers: {}, 278 | }; 279 | } 280 | try { 281 | log("info", "Downloading..."); 282 | 283 | headers["User-Agent"] = "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0)"; 284 | const options = { 285 | headers, 286 | maxRedirects: 20, 287 | timeout: 4000, 288 | }; 289 | if (body) 290 | options.body = body; 291 | if (argv.proxy) 292 | options.proxy = argv.proxy; 293 | 294 | const file = request(method, url, options); 295 | Buffer.prototype.charCodeAt = function(index) { 296 | return this[index]; 297 | }; 298 | log("info", `Downloaded ${file.body.length} bytes.`); 299 | return file; 300 | } catch (e) { 301 | // Log and rethrow 302 | log("error", `An error occurred while emulating a ${method} request to ${url}.`); 303 | log("error", e); 304 | throw e; 305 | } 306 | }, 307 | writeFile: function(filename, contents) { 308 | // Don't spam lots of file write info to same file. 309 | if (typeof(fileWriteCount[filename]) == "undefined") fileWriteCount[filename] = 0; 310 | fileWriteCount[filename]++; 311 | var doLog = (!throttleWrites) || (fileWriteCount[filename] <= MAXWRITES); 312 | if (doLog) logIOC("FileWrite", {file: filename, contents}, "The script wrote file '" + filename + "'."); 313 | files[filename] = contents; 314 | }, 315 | readFile: function(filename) { 316 | logIOC("FileRead", {file: filename}, "The script read a file."); 317 | // Do we have some fake contents for this file? 318 | const r = fakeFiles.ReadFakeFileContents(filename); 319 | if (typeof(r) !== "undefined") { 320 | return r; 321 | } 322 | return files[filename]; 323 | }, 324 | logUrl, 325 | logResource: function(resourceName, emulatedPath, content) { 326 | 327 | // Writing a Blob? 328 | if (content.constructor.name == "Blob") { 329 | 330 | // Grab the actual contents as a byte string. 331 | content = content.data; 332 | }; 333 | 334 | // Throttle lots of small writes to a resource when the resource is large. 335 | const filePath = path.join(directory, resourceName); 336 | if (typeof(resourceWriteCount[resourceName]) == "undefined") resourceWriteCount[resourceName] = 0; 337 | resourceWriteCount[resourceName]++; 338 | var throttle = (resourceWriteCount[resourceName] > MAXWRITES * 50) && 339 | (content.length > MAXBYTES/5) && throttleWrites 340 | if (throttle) { 341 | if (typeof(tooBigFiles[filePath]) == "undefined") { 342 | log("warn", "File '" + filePath + "' is too big with too many small writes. Not writing."); 343 | tooBigFiles[filePath] = true; 344 | } 345 | return; 346 | } 347 | 348 | // Has this file aready gotten too large? 349 | if (throttleWrites && (content.length > MAXBYTES)) { 350 | if (typeof(tooBigFiles[filePath]) == "undefined") { 351 | log("warn", "File '" + filePath + "' is too big. Not writing."); 352 | tooBigFiles[filePath] = true; 353 | } 354 | return; 355 | }; 356 | 357 | // Save the new file contents. Save as binary data to make 358 | // sure we dump the exact bytes to disk. 359 | fs.writeFileSync(filePath, content, "binary"); 360 | 361 | // Don't spam lots of file write info to same file. 362 | if (typeof(fileWriteCount[filePath]) == "undefined") fileWriteCount[filePath] = 0; 363 | fileWriteCount[filePath]++; 364 | var doLog = (!throttleWrites) || (fileWriteCount[filePath] <= MAXWRITES); 365 | let filetype = ""; 366 | if (doLog) { 367 | log("info", `Saved ${filePath} (${content.length} bytes)`); 368 | filetype = child_process.execSync("file " + simpleStringify(filePath)).toString("utf8"); 369 | filetype = filetype.replace(`${filePath}: `, "").replace("\n", ""); 370 | log("info", `${filePath} has been detected as ${filetype}.`); 371 | } 372 | if (fileWriteCount[filePath] == (MAXWRITES + 1)) { 373 | log("warn", "Throttling file write reporting for " + filePath); 374 | } 375 | 376 | if (doLog && (/executable/.test(filetype))) { 377 | // Log active url 378 | log("info", `Active URL detected: ${latestUrl}`); 379 | if (activeUrls.indexOf(latestUrl) === -1) 380 | activeUrls.push(latestUrl); 381 | fs.writeFileSync(path.join(directory, "active_urls.json"), simpleStringify(activeUrls, null, "\t")); 382 | } 383 | 384 | if (doLog) { 385 | const md5 = hash("md5", content); 386 | log("verb", "md5: " + md5); 387 | const sha1 = hash("sha1", content); 388 | log("verb", "sha1: " + sha1); 389 | const sha256 = hash("sha256", content); 390 | log("verb", "sha256: " + sha256); 391 | 392 | const resource = { 393 | path: emulatedPath, 394 | type: filetype, 395 | latestUrl, 396 | md5, 397 | sha1, 398 | sha256 399 | }; 400 | logIOC("NewResource", resource, "The script created a resource."); 401 | fs.writeFileSync(path.join(directory, "resources.json"), simpleStringify(resources, null, "\t")); 402 | resources[resourceName] = resource; 403 | } 404 | }, 405 | logSnippet, 406 | logJS: function(code) { 407 | const filename = uuid.v4() + ".js"; 408 | log("verb", `Code saved to ${filename}`); 409 | logSnippet(filename, {as: "eval'd JS"}, code); 410 | return code; // Helps with tail call optimization 411 | }, 412 | logIOC, 413 | runShellCommand: (command) => { 414 | // Have too many commands been run? 415 | if (_throttleCommands) { 416 | 417 | // Too many commands total? 418 | numCommandsRun++; 419 | if (numCommandsRun > maxCommands) { 420 | log("warn", "Too many commands were run. Terminating."); 421 | console.log(""); 422 | // We want this to take precedence over the --no-kill 423 | // flag, so directly exit. 424 | process.exit(0); 425 | } 426 | 427 | // Too many very similar commands? 428 | if (command.lastIndexOf(" ") > 10) { 429 | 430 | // Assuming that a command may just vary by an 431 | // argument, strip off the last argument. 432 | const end = command.lastIndexOf(" "); 433 | const prefix = command.slice(0, end); 434 | if (!(prefix in commandPrefixCounts)) { 435 | commandPrefixCounts[prefix] = 0; 436 | } 437 | commandPrefixCounts[prefix]++; 438 | if (commandPrefixCounts[prefix] > maxPrefixCommands) { 439 | log("warn", "Too many similar commands were run. Terminating."); 440 | console.log(""); 441 | // We want this to take precedence over the --no-kill 442 | // flag, so directly exit. 443 | process.exit(0); 444 | } 445 | } 446 | } 447 | 448 | const filename = getUUID(); 449 | command = "" + command; 450 | logIOC("Run", {command}, "The script ran the command '" + command + "'."); 451 | logSnippet(filename, {as: "WScript code"}, command); 452 | process.send("expect-shell-error"); 453 | if (!argv["no-shell-error"]) 454 | throw new Error("If you can read this, re-run box.js with the --no-shell-error flag."); 455 | process.send("no-expect-shell-error"); 456 | } 457 | }; 458 | -------------------------------------------------------------------------------- /decoder.c: -------------------------------------------------------------------------------- 1 | /**********************************************************************/ 2 | /* scrdec.c - Decoder for Microsoft Script Encoder */ 3 | /* Version 1.8 */ 4 | /* */ 5 | /* COPYRIGHT: */ 6 | /* (c)2000-2005 MrBrownstone, mrbrownstone@ virtualconspiracy.com */ 7 | /* v1.8 Now correctly decodes characters 0x00-0x1F, thanks to 'Zed' */ 8 | /* v1.7 Bypassed new HTMLGuardian protection and added -dumb switch */ 9 | /* to disable this */ 10 | /* v1.6 Added HTML Decode option (-htmldec) */ 11 | /* v1.5 Bypassed a cleaver trick defeating this tool */ 12 | /* v1.4 Some changes by Joe Steele to correct minor stuff */ 13 | /* */ 14 | /* DISCLAIMER: */ 15 | /* This program is for demonstrative and educational purposes only. */ 16 | /* Use of this program is at your own risk. The author cannot be held */ 17 | /* responsible if any laws are broken by use of this program. */ 18 | /* */ 19 | /* If you use or distribute this code, this message should be held */ 20 | /* intact. Also, any program based upon this code should display the */ 21 | /* copyright message and the disclaimer. */ 22 | /**********************************************************************/ 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #define LEN_OUTBUF 64 29 | #define LEN_INBUF 1024 30 | 31 | #define STATE_INIT_COPY 100 32 | #define STATE_COPY_INPUT 101 33 | #define STATE_SKIP_ML 102 34 | #define STATE_CHECKSUM 103 35 | #define STATE_READLEN 104 36 | #define STATE_DECODE 105 37 | #define STATE_UNESCAPE 106 38 | #define STATE_FLUSHING 107 39 | #define STATE_DBCS 108 40 | #define STATE_INIT_READLEN 109 41 | #define STATE_URLENCODE_1 110 42 | #define STATE_URLENCODE_2 111 43 | #define STATE_WAIT_FOR_CLOSE 112 44 | #define STATE_WAIT_FOR_OPEN 113 45 | #define STATE_HTMLENCODE 114 46 | 47 | unsigned char rawData[292] = { 48 | 0x64,0x37,0x69, 0x50,0x7E,0x2C, 0x22,0x5A,0x65, 0x4A,0x45,0x72, 49 | 0x61,0x3A,0x5B, 0x5E,0x79,0x66, 0x5D,0x59,0x75, 0x5B,0x27,0x4C, 50 | 0x42,0x76,0x45, 0x60,0x63,0x76, 0x23,0x62,0x2A, 0x65,0x4D,0x43, 51 | 0x5F,0x51,0x33, 0x7E,0x53,0x42, 0x4F,0x52,0x20, 0x52,0x20,0x63, 52 | 0x7A,0x26,0x4A, 0x21,0x54,0x5A, 0x46,0x71,0x38, 0x20,0x2B,0x79, 53 | 0x26,0x66,0x32, 0x63,0x2A,0x57, 0x2A,0x58,0x6C, 0x76,0x7F,0x2B, 54 | 0x47,0x7B,0x46, 0x25,0x30,0x52, 0x2C,0x31,0x4F, 0x29,0x6C,0x3D, 55 | 0x69,0x49,0x70, 0x3F,0x3F,0x3F, 0x27,0x78,0x7B, 0x3F,0x3F,0x3F, 56 | 0x67,0x5F,0x51, 0x3F,0x3F,0x3F, 0x62,0x29,0x7A, 0x41,0x24,0x7E, 57 | 0x5A,0x2F,0x3B, 0x66,0x39,0x47, 0x32,0x33,0x41, 0x73,0x6F,0x77, 58 | 0x4D,0x21,0x56, 0x43,0x75,0x5F, 0x71,0x28,0x26, 0x39,0x42,0x78, 59 | 0x7C,0x46,0x6E, 0x53,0x4A,0x64, 0x48,0x5C,0x74, 0x31,0x48,0x67, 60 | 0x72,0x36,0x7D, 0x6E,0x4B,0x68, 0x70,0x7D,0x35, 0x49,0x5D,0x22, 61 | 0x3F,0x6A,0x55, 0x4B,0x50,0x3A, 0x6A,0x69,0x60, 0x2E,0x23,0x6A, 62 | 0x7F,0x09,0x71, 0x28,0x70,0x6F, 0x35,0x65,0x49, 0x7D,0x74,0x5C, 63 | 0x24,0x2C,0x5D, 0x2D,0x77,0x27, 0x54,0x44,0x59, 0x37,0x3F,0x25, 64 | 0x7B,0x6D,0x7C, 0x3D,0x7C,0x23, 0x6C,0x43,0x6D, 0x34,0x38,0x28, 65 | 0x6D,0x5E,0x31, 0x4E,0x5B,0x39, 0x2B,0x6E,0x7F, 0x30,0x57,0x36, 66 | 0x6F,0x4C,0x54, 0x74,0x34,0x34, 0x6B,0x72,0x62, 0x4C,0x25,0x4E, 67 | 0x33,0x56,0x30, 0x56,0x73,0x5E, 0x3A,0x68,0x73, 0x78,0x55,0x09, 68 | 0x57,0x47,0x4B, 0x77,0x32,0x61, 0x3B,0x35,0x24, 0x44,0x2E,0x4D, 69 | 0x2F,0x64,0x6B, 0x59,0x4F,0x44, 0x45,0x3B,0x21, 0x5C,0x2D,0x37, 70 | 0x68,0x41,0x53, 0x36,0x61,0x58, 0x58,0x7A,0x48, 0x79,0x22,0x2E, 71 | 0x09,0x60,0x50, 0x75,0x6B,0x2D, 0x38,0x4E,0x29, 0x55,0x3D,0x3F, 72 | 0x51,0x67,0x2f 73 | } ; 74 | 75 | const unsigned char pick_encoding[64] = { 76 | 1, 2, 0, 1, 2, 0, 2, 0, 0, 2, 0, 2, 1, 0, 2, 0, 77 | 1, 0, 2, 0, 1, 1, 2, 0, 0, 2, 1, 0, 2, 0, 0, 2, 78 | 1, 1, 0, 2, 0, 2, 0, 1, 0, 1, 1, 2, 0, 1, 0, 2, 79 | 1, 0, 2, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 1, 0, 2 80 | }; 81 | 82 | unsigned char transformed[3][127]; 83 | int digits[0x7a]; 84 | 85 | int urlencoded = 0; 86 | int htmlencoded = 0; 87 | int verbose = 0; 88 | int smart = 1; 89 | 90 | unsigned char unescape (unsigned char c) 91 | { 92 | static unsigned char escapes[] = "#&!*$"; 93 | static unsigned char escaped[] = "\r\n<>@"; 94 | int i=0; 95 | 96 | if (c > 127) 97 | return c; 98 | while (escapes[i]) 99 | { 100 | if (escapes[i] == c) 101 | return escaped[i]; 102 | i++; 103 | } 104 | return '?'; 105 | } 106 | 107 | void maketrans (void) 108 | { 109 | int i, j; 110 | 111 | for (i=0; i<32; i++) 112 | for (j=0; j<3; j++) 113 | transformed[j][i] = i; 114 | 115 | for (i=31; i<=127; i++) 116 | for (j=0; j<3; j++) 117 | transformed[j][rawData[(i-31)*3 + j]] = (i==31) ? 9 : i; 118 | } 119 | 120 | void makedigits (void) 121 | { 122 | int i; 123 | 124 | for (i=0; i<26; i++) 125 | { 126 | digits['A'+i] = i; 127 | digits['a'+i] = i+26; 128 | } 129 | for (i=0; i<10; i++) 130 | digits['0'+i] = i+52; 131 | digits[0x2b] = 62; 132 | digits[0x2f] = 63; 133 | } 134 | 135 | unsigned long int decodeBase64 (unsigned char *p) 136 | { 137 | unsigned long int val = 0; 138 | 139 | val += (digits[p[0]] << 2); 140 | val += (digits[p[1]] >> 4); 141 | val += (digits[p[1]] & 0xf) << 12; 142 | val += ((digits[p[2]] >> 2) << 8); 143 | val += ((digits[p[2]] & 0x3) << 22); 144 | val += (digits[p[3]] << 16); 145 | val += ((digits[p[4]] << 2) << 24); 146 | val += ((digits[p[5]] >> 4) << 24); 147 | 148 | /* 543210 543210 543210 543210 543210 543210 149 | 150 | 765432 151 | 10 152 | ba98 153 | fedc 154 | 76 155 | 543210 156 | fedcba 98---- 157 | |- LSB -||- -||- -| |- MSB -| 158 | */ 159 | return val; 160 | } 161 | 162 | /* 163 | Char. number range | UTF-8 octet sequence 164 | (hexadecimal) | (binary) 165 | --------------------+--------------------------------------------- 166 | 0000 0000-0000 007F | 0xxxxxxx 167 | 0000 0080-0000 07FF | 110xxxxx 10xxxxxx 168 | 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx 169 | 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 170 | */ 171 | 172 | int isLeadByte (unsigned int cp, unsigned char ucByte) 173 | { 174 | /* Code page 932 - Japanese Shift-JIS - 0x81-0x9f 175 | 0xe0-0xfc 176 | 936 - Simplified Chinese GBK - 0xa1-0xfe 177 | 949 - Korean Wansung - 0x81-0xfe 178 | 950 - Traditional Chinese Big5 - 0x81-0xfe 179 | 1361 - Korean Johab - 0x84-0xd3 180 | 0xd9-0xde 181 | 0xe0-0xf9 */ 182 | switch (cp) 183 | { 184 | case 932: 185 | if ((ucByte > 0x80) && (ucByte < 0xa0)) return 1; 186 | if ((ucByte > 0xdf) && (ucByte < 0xfd)) return 1; 187 | else return 0; 188 | case 936: 189 | if ((ucByte > 0xa0) && (ucByte < 0xff)) return 1; 190 | else return 0; 191 | case 949: 192 | case 950: 193 | if ((ucByte > 0x80) && (ucByte < 0xff)) return 1; 194 | else return 0; 195 | case 1361: 196 | if ((ucByte > 0x83) && (ucByte < 0xd4)) return 1; 197 | if ((ucByte > 0xd8) && (ucByte < 0xdf)) return 1; 198 | if ((ucByte > 0xdf) && (ucByte < 0xfa)) return 1; 199 | else return 0; 200 | default: 201 | return 0; 202 | } 203 | 204 | } 205 | 206 | 207 | struct entitymap { 208 | char *entity; 209 | char mappedchar; 210 | }; 211 | 212 | struct entitymap entities[] = { 213 | {"excl",33},{"quot",34},{"num",35},{"dollar",36},{"percent",37}, 214 | {"amp",38},{"apos",39},{"lpar",40},{"rpar",41},{"ast",42}, 215 | {"plus",43},{"comma",44},{"period",46},{"colon",58},{"semi",59}, 216 | {"lt",60},{"equals",61},{"gt",62},{"quest",63},{"commat",64}, 217 | {"lsqb",91},{"rsqb",93},{"lowbar",95},{"lcub",123},{"verbar",124}, 218 | {"rcub",125},{"tilde",126}, {NULL, 0} 219 | }; 220 | 221 | 222 | char decodeMnemonic ( unsigned char *mnemonic) 223 | { 224 | int i=0; 225 | while (entities[i].entity != NULL) 226 | { 227 | if (strcmp(entities[i].entity, mnemonic)==0) 228 | return entities[i].mappedchar; 229 | i++; 230 | } 231 | printf ("Warning: did not recognize HTML entity '%s'\n", mnemonic); 232 | return '?'; 233 | } 234 | 235 | int ScriptDecoder (unsigned char *inname, unsigned char *outname, unsigned int cp) 236 | { 237 | unsigned char inbuf[LEN_INBUF+1]; 238 | unsigned char outbuf[LEN_OUTBUF+1]; 239 | unsigned char c, c1, c2, lenbuf[7], csbuf[7], htmldec[8]; 240 | unsigned char marker[] = "#@~^"; 241 | int ustate, nextstate, state = 0; 242 | int i, j, k, m, ml, hd = 0; 243 | int utf8 = 0; 244 | unsigned long int csum = 0, len = 0; 245 | FILE *infile, *outfile; 246 | 247 | infile = fopen ((const char*)inname, "rb"); 248 | outfile = fopen ((const char*)outname, "wb"); 249 | if (!infile || !outfile) 250 | { 251 | printf ("Error opening file!\n"); 252 | return 10; 253 | } 254 | 255 | maketrans(); 256 | makedigits(); 257 | memset (inbuf, 0, sizeof (inbuf)); 258 | memset (outbuf, 0, sizeof (outbuf)); 259 | memset (lenbuf, 0, sizeof (lenbuf)); 260 | 261 | state = STATE_INIT_COPY; 262 | i = 0; 263 | j = 0; 264 | 265 | while (state) 266 | { 267 | if (inbuf[i] == 0) 268 | { 269 | if (feof (infile)) 270 | { 271 | if (len) 272 | { 273 | printf ("Error: Premature end of file.\n"); 274 | if (utf8>0) 275 | printf ("Tip: The file seems to contain special characters, try the -cp option.\n"); 276 | } 277 | break; 278 | } 279 | 280 | memset (inbuf, 0, sizeof (inbuf)); 281 | fgets ((char*)inbuf, LEN_INBUF, infile); 282 | i = 0; 283 | continue; 284 | } 285 | 286 | if (j == LEN_OUTBUF) 287 | { 288 | fwrite (outbuf, sizeof(char), j, outfile); 289 | j = 0; 290 | } 291 | 292 | if ((urlencoded==1) && (inbuf[i]=='%')) 293 | { 294 | ustate = state; /* save state */ 295 | state = STATE_URLENCODE_1; /* enter decoding state */ 296 | i++; /* flush char */ 297 | continue; 298 | } 299 | 300 | /* 2 means we do urldecoding but wanted to avoid decoding an 301 | already decoded % for the second time */ 302 | 303 | if (urlencoded==2) 304 | urlencoded=1; 305 | 306 | if ((htmlencoded==1) && (inbuf[i]=='&')) 307 | { 308 | ustate = state; 309 | state = STATE_HTMLENCODE; 310 | hd = 0; 311 | i++; 312 | continue; 313 | } 314 | 315 | /* 2 means we do htmldecoding but wanted to avoid decoding an 316 | already decoded & for the second time */ 317 | 318 | if (htmlencoded==2) 319 | htmlencoded=1; 320 | 321 | switch (state) 322 | { 323 | case STATE_INIT_COPY: 324 | ml = strlen ((const char*)marker); 325 | m = 0; 326 | state = STATE_COPY_INPUT; 327 | break; 328 | 329 | /* after decoding a block, we have to wait for the current 330 | script block to be closed (>) */ 331 | 332 | case STATE_WAIT_FOR_CLOSE: 333 | if (inbuf[i] == '>') 334 | state = STATE_WAIT_FOR_OPEN; 335 | outbuf[j++] = inbuf[i++]; 336 | break; 337 | 338 | /* and a new block to be opened again (<) */ 339 | case STATE_WAIT_FOR_OPEN: 340 | if (inbuf[i] == '<') 341 | state = STATE_INIT_COPY; 342 | outbuf[j++] = inbuf[i++]; 343 | break; 344 | 345 | case STATE_COPY_INPUT: 346 | if (inbuf[i] == marker[m]) 347 | { 348 | i++; 349 | m++; 350 | } 351 | else 352 | { 353 | if (m) 354 | { 355 | k = 0; 356 | state = STATE_FLUSHING; 357 | } 358 | else 359 | outbuf[j++] = inbuf[i++]; 360 | 361 | } 362 | if (m == ml) 363 | state = STATE_INIT_READLEN; 364 | break; 365 | 366 | case STATE_FLUSHING: 367 | outbuf[j++] = marker[k++]; 368 | m--; 369 | if (m==0) 370 | state = STATE_COPY_INPUT; 371 | break; 372 | 373 | case STATE_SKIP_ML: 374 | i++; 375 | if (!(--ml)) 376 | state = nextstate; 377 | break; 378 | 379 | 380 | case STATE_INIT_READLEN: 381 | ml = 6; 382 | state = STATE_READLEN; 383 | break; 384 | 385 | case STATE_READLEN: 386 | lenbuf[6-ml] = inbuf[i++]; 387 | if (!(--ml)) 388 | { 389 | len = decodeBase64 (lenbuf); 390 | if (verbose) 391 | printf ("Msg: Found encoded block containing %lu characters.\n", len); 392 | m = 0; 393 | ml = 2; 394 | state = STATE_SKIP_ML; 395 | nextstate = STATE_DECODE; 396 | } 397 | break; 398 | 399 | case STATE_DECODE: 400 | if (!len) 401 | { 402 | ml = 6; 403 | state = STATE_CHECKSUM; 404 | break; 405 | } 406 | if (inbuf[i] == '@') 407 | state = STATE_UNESCAPE; 408 | else 409 | { 410 | if ((inbuf[i] & 0x80) == 0) 411 | { 412 | outbuf[j++] = c = transformed[pick_encoding[m%64]][inbuf[i]]; 413 | csum += c; 414 | m++; 415 | } 416 | else 417 | { 418 | if (!cp && (inbuf[i] & 0xc0)== 0x80) 419 | { 420 | // utf-8 but not a start byte 421 | len++; 422 | utf8=1; 423 | } 424 | outbuf[j++] = inbuf[i]; 425 | if ((cp) && (isLeadByte (cp,inbuf[i]))) 426 | state = STATE_DBCS; 427 | } 428 | } 429 | i++; 430 | len--; 431 | break; 432 | 433 | case STATE_DBCS: 434 | outbuf[j++] = inbuf[i++]; 435 | state = STATE_DECODE; 436 | break; 437 | 438 | case STATE_UNESCAPE: 439 | outbuf[j++] = c = unescape (inbuf[i++]); 440 | csum += c; 441 | len--; 442 | m++; 443 | state = STATE_DECODE; 444 | break; 445 | 446 | case STATE_CHECKSUM: 447 | csbuf[6-ml] = inbuf[i++]; 448 | if (!(--ml)) 449 | { 450 | csum -= decodeBase64 (csbuf); 451 | if (csum) 452 | { 453 | printf ("Error: Incorrect checksum! (%lu)\n", csum); 454 | if (cp) 455 | printf ("Tip: Maybe try another codepage.\n"); 456 | else 457 | { 458 | if (utf8>0) 459 | printf ("Tip: The file seems to contain special characters, try the -cp option.\n"); 460 | else 461 | printf ("Tip: the file may be corrupted.\n"); 462 | } 463 | csum=0; 464 | } 465 | else 466 | { 467 | if (verbose) 468 | printf ("Msg: Checksum OK\n"); 469 | } 470 | m = 0; 471 | ml = 6; 472 | state = STATE_SKIP_ML; 473 | if (smart) 474 | nextstate = STATE_WAIT_FOR_CLOSE; 475 | else 476 | nextstate = STATE_INIT_COPY; 477 | } 478 | break; 479 | 480 | /* urlencoded, the first character */ 481 | case STATE_URLENCODE_1: 482 | c1 = inbuf[i++] - 0x30; 483 | if (c1 > 0x9) c1-= 0x07; 484 | if (c1 > 0x10) c1-= 0x20; 485 | state = STATE_URLENCODE_2; 486 | break; 487 | 488 | /* urlencoded, second character */ 489 | case STATE_URLENCODE_2: 490 | c2 = inbuf[i] - 0x30; 491 | if (c2 > 0x9) c2-= 0x07; 492 | if (c2 > 0x10) c2-= 0x20; 493 | inbuf[i] = c2 + (c1<<4); /* copy decoded char back on input */ 494 | urlencoded=2; /* avoid looping in case this was an % */ 495 | state = ustate; /* restore old state */ 496 | break; 497 | 498 | /* htmlencoded */ 499 | case STATE_HTMLENCODE: 500 | c1 = inbuf[i]; 501 | if (c1 != ';') 502 | { 503 | i++; 504 | htmldec[hd++] = c1; 505 | if (hd>7) 506 | { 507 | htmldec[7]=0; 508 | printf ("Error: HTML decode encountered a too long mnemonic (%s...)\n", htmldec); 509 | exit(10); 510 | } 511 | } 512 | else /* ';' = end of mnemonic */ 513 | { 514 | htmldec[hd] = 0; 515 | inbuf[i] = decodeMnemonic (htmldec); /* skip the & */ 516 | htmlencoded = 2; /* avoid looping in case of & */ 517 | state = ustate; 518 | } 519 | break; 520 | default: 521 | printf ("Internal Error: Invalid state: %d\n", state); 522 | break; 523 | } 524 | } 525 | 526 | fwrite (outbuf, sizeof (char), j, outfile); 527 | fclose (infile); 528 | fclose (outfile); 529 | return 0; 530 | } 531 | 532 | 533 | int main (int argc, char **argv) 534 | { 535 | int i, cp = 0; 536 | 537 | if (argc < 3) 538 | { 539 | puts ("ScrDec v1.8 - Decoder for Microsoft Script Encoder\n" 540 | "(c)2000-2005 MrBrownstone, mrbrownstone@ virtualconspiracy.com\n" 541 | "Home page: http://www.virtualconspiracy.com/scrdec.html\n\n" 542 | "Usage: scrdec18 [-cp codepage] [-urldec|-htmldec]\n" 543 | " [-verbose] [-dumb]\n\n" 544 | "Code pages can be 932 - Japanese\n" 545 | " 936 - Chinese (Simplified)\n" 546 | " 950 - Chinese (Traditional)\n" 547 | " 949 - Korean (Wansung)\n" 548 | " 1361 - Korean (Johab)\n" 549 | "Any other code pages don't need to be specified.\n\n" 550 | "Use -urldec to unescape %xx style encoding on the fly, or\n" 551 | " -htmldec to unescape & style encoding.\n" 552 | "For extra information, add the -verbose switch\n" 553 | "You might not want to use the smart HTMLGuardian defeation mechanism.\n" 554 | " In that case, add the -dumb switch.\n"); 555 | return 10; 556 | } 557 | 558 | i=3; 559 | while (i