Created
October 16, 2017 17:28
-
-
Save skyrising/00a3500e24ddeab167c5692445e6dd11 to your computer and use it in GitHub Desktop.
Deobfuscate some scripts obfuscated with https://github.com/javascript-obfuscator/javascript-obfuscator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const {generate} = require('escodegen') | |
const {parse} = require('acorn') | |
const fs = require('fs') | |
const walk = require('acorn/dist/walk') | |
const vm = require('vm') | |
const standard = require('standard') | |
function inline (call, decl) { | |
if (decl.body.body.length > 1) return | |
if (decl.body.body[0].type !== 'ReturnStatement') return | |
const retExpr = JSON.parse(JSON.stringify(decl.body.body[0].argument)) | |
const args = [] | |
for (const i in decl.params) { | |
const param = decl.params[i] | |
if (param.type !== 'Identifier') return | |
args[param.name] = i | |
} | |
walk.simple(retExpr, { | |
Identifier (node) { | |
if (!(node.name in args)) return | |
Object.assign(node, call.arguments[args[node.name]]) | |
} | |
}) | |
Object.assign(call, retExpr) | |
} | |
function unrollShuffledLoop (orderSt, loop) { | |
const switchSt = loop.body.body[0] | |
if (switchSt.type !== 'SwitchStatement') return [orderSt, loop] | |
const order = orderSt.declarations[0].init.callee.object.value.split('|') | |
const cases = {} | |
for (const caseSt of switchSt.cases) { | |
cases[caseSt.test.value] = caseSt.consequent.slice(0, -1) | |
} | |
const ordered = [] | |
for (const label of order) { | |
for (const stmt of cases[label]) ordered.push(stmt) | |
} | |
return ordered | |
} | |
const source = fs.readFileSync('original.js', 'UTF-8') | |
const ast = parse(source, { | |
ecmaVersion: 7, | |
sourceType: 'script', | |
ranges: true | |
}) | |
// string lookup table | |
const ltSource = ast.body.shift() | |
const lookupTable = vm.runInNewContext(generate(ltSource.declarations[0].init)) | |
// rotate lookup table according to rotation function | |
const rotSource = ast.body.shift().expression | |
let rot = rotSource.arguments[1].value + 1 | |
while (--rot) lookupTable.push(lookupTable.shift()) | |
const lkfnSource = ast.body.shift() | |
// function used for looking up strings in the lookup table | |
const lookupFnName = lkfnSource.declarations[0].id.name | |
// function inside $(document).ready(...) | |
const readyFn = ast.body[0].expression.arguments[0] | |
// simple functions replacing ==, <, +, -, <<, etc. | |
const fnsSource = readyFn.body.body.shift().declarations[0] | |
const fnsName = fnsSource.id.name | |
const fns = [] | |
for (const prop of fnsSource.init.properties) { | |
fns[prop.key.value] = prop.value | |
} | |
// known global variables | |
const scope = ['window', 'document', 'jQuery', '$'].concat(Object.getOwnPropertyNames(global)) | |
// custom names for variables | |
const named = { | |
1: 'var1', | |
2: 'var2' | |
} | |
// renaming of global variables | |
const names = { | |
z: 'global1' | |
} | |
const comments = [ | |
'eslint-env jquery' | |
].concat(Object.values(names).map(name => 'global ' + name)) | |
for (const globalName of scope) names[globalName] = globalName | |
let i = 0 | |
walk.simple(ast, { | |
// use let instead of var | |
VariableDeclaration (node, state) { | |
node.kind = 'let' | |
}, | |
// scope management for variable renaming | |
VariableDeclarator (decl, state) { | |
state.scope.push(decl.id.name) | |
}, | |
// find and unroll shuffled loops | |
BlockStatement (block) { | |
for (let i = 0; i < block.body.length; i++) { | |
const current = block.body[i] | |
if (current.type === 'VariableDeclaration') { | |
const next = block.body[i + 1] | |
if (next && next.type === 'WhileStatement') { | |
const before = block.body.slice(0, i) | |
const after = block.body.slice(i + 2) | |
block.body = before.concat(unrollShuffledLoop(current, next)).concat(after) | |
} | |
} | |
} | |
}, | |
MemberExpression (node) { | |
let id | |
if (node.property.type === 'CallExpression') { | |
const call = node.property | |
if (call.callee.type !== 'Identifier' || call.callee.name !== lookupFnName) return | |
if (call.arguments.length < 1 || call.arguments[0].type !== 'Literal') return | |
// replace call to lookup function with value | |
id = lookupTable[+call.arguments[0].value] | |
} else if (node.property.type === 'Literal') { | |
id = node.property.value | |
} else return | |
if (!/^[a-z]+$/i.test(id)) return | |
// replace e.g. x['split']('|') -> x.split('|') | |
node.property = { | |
type: 'Identifier', | |
name: id | |
} | |
node.computed = false | |
}, | |
CallExpression (node) { | |
// inline calls to simple functions | |
if (node.callee.type === 'MemberExpression') { | |
if (node.callee.object.type !== 'Identifier' || node.callee.object.name !== fnsName) return | |
if (node.callee.property.type !== 'Identifier') return | |
inline(node, fns[node.callee.property.name]) | |
} | |
// replace call to lookup function with value | |
if (node.callee.type !== 'Identifier' || node.callee.name !== lookupFnName) return | |
if (node.arguments.length < 1 || node.arguments[0].type !== 'Literal') return | |
Object.assign(node, { | |
type: 'Literal', | |
value: lookupTable[+node.arguments[0].value] | |
}) | |
} | |
}, null, {scope}) | |
walk.ancestor(ast, { | |
// for (x=0; ...) -> for (let x = 0; ...) | |
ForStatement (stmt) { | |
if (stmt.init.type === 'VariableDeclaration') return | |
stmt.init = { | |
type: 'VariableDeclaration', | |
declarations: [{ | |
type: 'VariableDeclarator', | |
id: stmt.init.left, | |
init: stmt.init.right | |
}], | |
kind: 'let' | |
} | |
}, | |
BinaryExpression (expr) { | |
// make linter happy | |
if (expr.operator === '==') expr.operator = '===' | |
if (expr.operator === '!=') expr.operator = '!==' | |
if (expr.left.type !== 'Literal' || expr.right.type !== 'Literal') return | |
// simplify constant multiplications | |
if (expr.operator === '*') Object.assign(expr, {type: 'Literal', value: expr.left.value * expr.right.value}) | |
}, | |
// eliminate with(x) { ... } | |
WithStatement (stmt) { | |
function updateId (id, state, anc) { | |
if (scope.includes(id.name)) return | |
Object.assign(id, { | |
type: 'MemberExpression', | |
object: Object.assign({}, stmt.object), | |
property: Object.assign({}, id), | |
computed: false | |
}) | |
} | |
walk.ancestor(stmt, { | |
Identifier: updateId, | |
AssignmentExpression (expr, state, anc) { | |
if (expr.left.type === 'Identifier') updateId(expr.left) | |
} | |
}) | |
Object.assign(stmt, stmt.body) | |
}, | |
// concatenate nested blocks | |
BlockStatement (block) { | |
for (let i = 0; i < block.body.length; i++) { | |
const current = block.body[i] | |
if (current.type === 'BlockStatement') { | |
const before = block.body.slice(0, i) | |
const after = block.body.slice(i + 2) | |
block.body = before.concat(current.body).concat(after) | |
} | |
} | |
} | |
}) | |
// rename variables | |
walk.simple(ast, { | |
Identifier (id) { | |
if (id.name in names) id.name = names[id.name] | |
}, | |
VariableDeclarator (decl) { | |
if (decl.id.name[0] === '_' && decl.id.name in names) delete names[decl.id.name] | |
if (decl.id.name in names) decl.id.name = names[decl.id.name] | |
else if (decl.id.name.length > 3) { | |
const name = (i + 1) in named ? named[++i] : 'var' + ++i | |
names[decl.id.name] = name | |
decl.id.name = name | |
} | |
}, | |
AssignmentExpression (expr) { | |
if (expr.left.type === 'Identifier' && expr.left.name in names) expr.left.name = names[expr.left.name] | |
} | |
}) | |
// write final formatted output | |
fs.writeFileSync('./ast.json', JSON.stringify(ast, null, 2)) | |
const commentsText = comments.map(comment => '/* ' + comment + '*/').join('\n') + '\n' | |
const output = commentsText + generate(ast, { | |
format: { | |
indent: { | |
style: ' ' | |
}, | |
escapeless: true, | |
semicolons: false | |
}, | |
comment: true | |
}) | |
const results = standard.lintTextSync(output, { | |
fix: true | |
}) | |
const lintedOutput = results.results[0].output || results.results[0].source || output | |
fs.writeFileSync('./deobfuscated.js', lintedOutput) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment