Skip to content

Instantly share code, notes, and snippets.

@skyrising
Created October 16, 2017 17:28
Show Gist options
  • Save skyrising/00a3500e24ddeab167c5692445e6dd11 to your computer and use it in GitHub Desktop.
Save skyrising/00a3500e24ddeab167c5692445e6dd11 to your computer and use it in GitHub Desktop.
Deobfuscate some scripts obfuscated with https://github.com/javascript-obfuscator/javascript-obfuscator
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