inspectors/BytecodeCleaner/service/main.js
const IFC = require("../../../src/InspectorFrontController.js");
var CONST = require("../../../src/CoreConst.js");
const Disassembler = require("../../../src/Disassembler.js");
var Controller = new IFC.FrontController();
var DEBUG = false;
/**
* Count NOP instructions
*/
function nopCount(context){
let subject=null, bb=null;
let counters = {
any: 0,
nop: 0
};
context.analyze.db.methods.map((k,v)=>{
if( v.instr != null && v.instr.length > 0){
for(let i=0; i<v.instr.length ; i++){
for(let j=0; j<v.instr[i].stack.length; j++){
opcode = v.instr[i].stack[j].opcode;
if(opcode != null && opcode.type===CONST.INSTR_TYPE.NOP){
counters.nop++;
}
counters.any++;
}
}
}
});
return { status:200, data:counters };
}
function nopClean(context){
let subject=null, bb=null, newbb=null;
let DB = context.analyze.db;
let counters = {
nop: 0
};
context.analyze.db.methods.map((k,v)=>{
if( v.instr != null && v.instr.length > 0){
for(let i=0; i<v.instr.length ; i++){
newbb = [];
for(let j=0; j<v.instr[i].stack.length; j++){
opcode = v.instr[i].stack[j].opcode;
if(opcode != null && opcode.type !==CONST.INSTR_TYPE.NOP){
newbb.push(v.instr[i].stack[j]);
}else{
counters.nop++;
}
}
v.instr[i].stack = newbb;
}
}
});
return { status:200, data:counters };
}
// ======= Goto =========
function gotoLocalXref(method,goto_name){
let xref = {
name: null,
ref: [],
ctr: 0
};
for(let i=0; i<method.instr.length; i++){
for(let j=0; j<method.instr[i].stack.length; j++){
if(method.instr[i].stack[j].opcode.type === CONST.INSTR_TYPE.GOTO){
console.log(method.instr[i].stack[j].right.name+" ... "+goto_name);
if(method.instr[i].stack[j].right.name === goto_name){
//xref.name = method.instr[i].stack[j].right.name;
console.log("XREF found");
xref.ref.push({ bb: method.instr[i], offset:j });
xref.ctr++;
if(j !== method.instr[i].stack.length-1){
console.log(method.signature(),"[:goto_"+goto_name,"] Not the last instruction")
}
}
}
}
}
return xref;
}
/*
0. Check if the function is eligible (no cond)
1. Find single GOTO instruction
2. Collect next contiguous basic blocks from the target label
3. Replace the GOTO instruction by the identified basic blocks
4. Remove the moved basic blocks if there was not duplicated
5. Remove empty basic block if not used (TODO during 2. ?)
*/
function checkIfEligible(method){
let notElgible = [
CONST.INSTR_TYPE.IF,
CONST.INSTR_TYPE.SWITCH,
CONST.INSTR_TYPE.MONITOR
];
// count GOTOs
if( method.instr != null && method.instr.length > 0){
for(let i=0; i<method.instr.length ; i++){
for(let j=0; j<method.instr[i].stack.length; j++){
instruction = method.instr[i].stack[j];
if(instruction.opcode != null && notElgible.indexOf(instruction.opcode.type)>-1)
return false;
}
}
}
return true;
}
function findSingleGoto(method){
let found = {}, singles = [];
// count GOTOs
if( method.instr != null && method.instr.length > 0){
for(let i=0; i<method.instr.length ; i++){
for(let j=0; j<method.instr[i].stack.length; j++){
instruction = method.instr[i].stack[j];
if(instruction.opcode != null && instruction.opcode.type==CONST.INSTR_TYPE.GOTO){
if(found[instruction.right.name]===undefined)
found[instruction.right.name]=1;
else
found[instruction.right.name]+=1;
//if(found[instruction.right.name]==1) console.log("block "+i+", instr "+j+", goto "+instruction.right.name);
}
}
}
}
// filter
for(let i in found){
if(found[i]===1)
singles.push(i);
}
if(singles.length > 0){
// Disassembler.method(method);
//console.log(singles);
}
return singles;
}
function nextSingleGoto(method){
let found = {}, singles = [];
// count GOTOs
if( method.instr != null && method.instr.length > 0){
for(let i=0; i<method.instr.length ; i++){
for(let j=0; j<method.instr[i].stack.length; j++){
instruction = method.instr[i].stack[j];
if(instruction.opcode != null && instruction.opcode.type==CONST.INSTR_TYPE.GOTO){
if(found[instruction.right.name]===undefined)
found[instruction.right.name]=1;
else
found[instruction.right.name]+=1;
}
}
}
}
// filter
for(let i in found){
if(found[i]===1)
return i;
}
return null;
}
function isJump(instr){
return instr !=null
&& instr.opcode != null
&& (instr.opcode.type==CONST.INSTR_TYPE.GOTO || instr.opcode.type==CONST.INSTR_TYPE.RET );
}
function hasJump(block, label=null){
let f = false;
if(label == null)
block.forEach(element => {
if(isJump(element)){
f = true;
}
});
else
block.forEach(element => {
if(isJump(element)){
f = (element.right.name==label);
}
});
return f;
}
function isReturn(instr){
return instr !=null
&& instr.opcode != null
&& (instr.opcode.type==CONST.INSTR_TYPE.RET );
}
function hasReturn(block, label=null){
let f = false;
bbloccks.forEach(blk => {
block.forEach(element => {
if(isReturn(element)){
f = true;
}
});
})
return f;
}
/**
* To identifiy where is the first basic block and how many contiguous basic
* block should be moved.
*
* If the target blocks contains "return" opcode, the targeted basic blocks must be duplicated.
*
* @param {*} method
* @param {*} gotoLabel
*/
function findTargetBasicBlocks(method, gotoLabel){
//console.log("Searching block at :goto_"+gotoLabel);
let targetBBs = null, found=false, offset=0, duplicate=false;
if( method.instr != null && method.instr.length > 0){
for(let i=0; i<method.instr.length ; i++){
if(method.instr[i].goto_name === gotoLabel){
targetBBs = [method.instr[i]];
offset=i;
found=true;
if(method.instr[i] == null){
console.log("Error");
}
if(method.instr[i].hasInstr(CONST.INSTR_TYPE.RET))
duplicate = true;
//console.log(method.instr[i].stack);
if(hasJump(method.instr[i].stack)){
if(DEBUG) console.log("block has jump :",method.instr[i].goto_name,gotoLabel,targetBBs);
break;
}
}
else if(found){
targetBBs.push(method.instr[i]);
//console.log(hasJump(method.instr[i].stack));
if(method.instr[i].hasInstr(CONST.INSTR_TYPE.RET))
duplicate = true;
if(hasJump(method.instr[i].stack)){
if(DEBUG) console.log("block has jump POST :",gotoLabel,targetBBs[targetBBs.length-1].stack);
break;
}
}
}
}
// if a BB contains "return", check if previous incond BB are tagged.
if(duplicate){
duplicate = false;
for(let i=offset-1; i>0 ; i--){
if(method.instr[i].goto_name !== null){
duplicate = true;
break;
}
}
}
return { blk:targetBBs, offset:offset, duplicate:duplicate };
}
function moveBasicBlock(method, bblocks, gotoLabel){
//console.log("Moving basic blocks "+bblocks.offset+"(len:"+bblocks.blk.length+") at instr goto_"+gotoLabel);
let bbs = [], flag=false, lastWasGoto=false, endbb=0, tmp=null;
// find cut point
if( method.instr != null && method.instr.length > 0){
for(let i=0; i<method.instr.length ; i++){
for(let j=0; j<method.instr[i].stack.length; j++){
instruction = method.instr[i].stack[j];
lastWasGoto=false
if(instruction == null){
continue;
}
if(instruction.opcode != null && instruction.opcode.type==CONST.INSTR_TYPE.GOTO){
// check if the instruction must be patched
if(instruction.right.name == gotoLabel){
//console.log(bblocks);
// check if the target is before
endbb = bblocks.offset+bblocks.blk.length;
if(endbb<=i){
if(bblocks.duplicate){
bbs = method.instr.slice(0,i);
}else{
bbs = method.instr.slice(0,bblocks.offset);
bbs = bbs.concat(method.instr.slice(
endbb,
i+1));
}
//console.log("Targeted block before");
}else if(bblocks.offset>i){
bbs = method.instr.slice(0,i+1);
//console.log("Targeted block after");
}
//Disassembler.method(method);
if(method.instr[i].stack != undefined && method.instr[i].stack.length>0){
// console.log("Remove goto instruction");
// si le dernier basic block est un return-* on duplique
// sinon on déplace
if(bbs[bbs.length-1] == undefined){
//console.log("The new list of blocks is null : ",bbs.length,bbs);
}
if(j==bbs[bbs.length-1].stack.length-1){
bbs[bbs.length-1].stack.pop();
}else{
/*
Case :
nop
nop
goto
nop
Else there is a basic block segmentation error
*/
tmp = [];
for(let ii=0; ii<bbs[bbs.length-1].stack.length; ii++){
if(bbs[bbs.length-1].stack[ii].opcode.type==CONST.INSTR_TYPE.GOTO) continue;
tmp.push(bbs[bbs.length-1].stack[ii]);
}
bbs[bbs.length-1].stack = tmp;
}
}
// append others basic blocks
if(bblocks.blk.length>0){
bblocks.blk[0].goto_name = null;
for(let l=0; l<bblocks.blk.length; l++)
bbs.push(bblocks.blk[l]);
}
flag = true;
lastWasGoto = true;
if(bblocks.duplicate){
frag = method.instr.slice(bblocks.offset,endbb);
for(let b=0; b<frag.length; b++){
bbs.push(frag[b].clone());
bbs = bbs.concat(method.instr.slice(i+1));
}
}else{
if(endbb<=i){
if(i+1<method.instr.length)
bbs = bbs.concat(method.instr.slice(i+1));
}
else if(bblocks.offset>i){
//console.log(i+1,bblocks.offset, endbb+1, method.instr.slice(i+1, bblocks.offset));
bbs = bbs.concat(method.instr.slice(i+1, bblocks.offset));
bbs = bbs.concat(method.instr.slice(endbb ));
}
}
}
}
else if(lastWasGoto){
if(instruction.opcode.type !== CONST.INSTR_TYPE.NOP){
console.log("[ERROR] CFG changed !!!");
}
}
}
}
}
//method.instr = bbs;
return bbs;
}
function flatternGotoOf(method){
let blocksToMove = null;
let singleGoto = findSingleGoto(method);
let ret = false;
let disass = false;
if(singleGoto.length > 0){
singleGoto.sort();
if(DEBUG){
Disassembler.method(method);
}
for(let i=0; i<singleGoto.length; i++){
//console.log("flat : ",singleGoto[i]);
if(DEBUG){
Disassembler.method(method);
}
blocksToMove = findTargetBasicBlocks(method, singleGoto[i]);
if(blocksToMove.blk !== null){
if(DEBUG){
blocksToMove.blk.forEach(x=>console.log(x.stack));
console.log(blocksToMove, singleGoto[i])
}
method.instr = moveBasicBlock(method, blocksToMove, singleGoto[i]);
if(DEBUG)
Disassembler.method(method);
ret = true;
}
}
}
return ret;
}
/*
A
| \
C |
| /
X
Si la derniere instructions du bloc precedent est un goto vers le bloc courant
*/
/**
*
* Not clean methods containing if-* and throw instruction
* @param {*} context
*/
function gotoConditionnalClean(context){
console.log("Conditional goto clean");
let gotos = 0;
context.analyze.db.methods.map((k,v)=>{
if(checkIfEligible(v)==false) return;
if( v != null && v.instr != null && v.instr.length > 0){
if(flatternGotoOf(v)) gotos++;
}
});
return { status:200, data:{ count:gotos }};
}
function gotoClean(context){
console.log("Inconditional goto clean");
let counters = {
goto: 0
};
context.analyze.db.methods.map((k,v)=>{
if( v != null && v.instr != null && v.instr.length > 0){
if(flatternGotoOf(v)) counters.goto++;
}
});
return { status:200, data:{ counter:counters.goto } };
}
function hasSingleCall(method){
if(method == null){
console.log("method is null");
return false;
}
if(Object.entries(method._useMethod).length !== 1 )
return false;
return true;
}
/**
* Double static heuristic is when a static method wraps another statis method
*/
function renameDoubleStatic(database, method, pContext){
if(!hasSingleCall(method)) return false;
// check if the current method is static
if(method.getModifier().static === false) return false;
// get the called method
let called = database.methods.getEntry( Object.keys(method._useMethod)[0] );
let args = Object.values(method._useMethod)[0];
if(called == null
|| called.getModifier() == null
|| called.getModifier().static === false) return false;
//let args = called.args;
// add arg type comparison
let paramOnly = true;
if(args.length > 0){
args.map( pLocation => {
let instr = method.getInstr(pLocation.bb,pLocation.instr);
if(instr != null){
instr.left.map( vParam => {
if(vParam.t !== CONST.LEX.TOKEN.PARAM) paramOnly = false;
})
}
})
/*
console.log(args, args[0], method.getInstr(args[0].bb,args[0].instr)) ;
args[0].forEach(x=>{
if(x.t !== CONST.LEX.TOKEN.PARAM) paramOnly = false;
});*/
}
if(paramOnly === false) return false;
if(called.enclosingClass.name !== method.enclosingClass.name){
method.setAlias(called.enclosingClass.name+"_"+called.name);
}else{
method.setAlias(called.name);
}
return true;
}
/**
*
* static
* test/toto;->doSomething(Ltest/blabla;Ljava/lang/String;)Ljava/io/File;
*
* invoke-virtual {p0, p1}, Ltest/blabla;->a0cc175b9(Ljava/lang/String;)Ljava/io/File;
* move-result-object v0
* return-object v0
*/
function renameStaticInterface(database, method, pContext){
if(!hasSingleCall(method)) return false;
if(method.args.length==0) return false;
// get the called method
let called = database.methods.getEntry( Object.keys(method._useMethod)[0] );
let args = Object.values(method._useMethod)[0];
//let param = method.args[0];
// check if the called method is not null
if(called == null){
// if is often caused by extended methods which cannot be resolved
// parsing error found
console.log("[ERROR] The called method is NULL. It seems to be a parsing error. Caller : ",method.signature());
return false;
}
// check if the method is not static
// TODO : add check based on the opcode type instead of the modifiers of the called method
if(called == null
|| called.getModifier() == null
|| called.getModifier().static === true) return false;
// check if the first param of the caller is an
// instance of the class who defines the called method
if(method.args[0].name !== called.enclosingClass.name)
return false;
// check if each parameter of the called method are parameter of the caller
// {p0, p1} or {p0 ... p5}
let paramOnly = true;
if(args.length > 0){
args.map( pLocation => {
let instr = method.getInstr(pLocation.bb,pLocation.instr);
if(instr != null){
instr.left.map( vParam => {
if(vParam.t !== CONST.LEX.TOKEN.PARAM) paramOnly = false;
})
}
})
}
if(paramOnly === false) return false;
// check if some parameters of the called method are defined statically and locally
if(called.enclosingClass.name !== method.enclosingClass.name){
method.setAlias(called.enclosingClass.name+"_"+called.name);
}else{
method.setAlias(called.name);
}
return true;
}
function wrapClean(context){
let db = context.analyze.db;
let ctr = {
doubleStatic: 0,
staticInterface: 0
};
let edited = [];
// scan with several heuristic
db.methods.map((k,v)=>{
if(renameDoubleStatic(db, v, context)){
edited.push(v);
ctr.doubleStatic++;
return;
}
if(renameStaticInterface(db, v, context)){
edited.push(v);
ctr.staticInterface++;
return;
}
});
if(edited.length > 0){
context.trigger({
type: "method.alias.update.mult",
meths: edited
});
}
return { status:200, data:{ counter:ctr } };
}
/**
* Delegate front controller
*/
Controller.registerHandler(IFC.HANDLER.GET, function(ctx,req,res){
var action = req.query.action;
var act ={
status: 404,
data: { error: "Action not found. "}
};
switch(action){
case 'nop_count':
act = nopCount(ctx);
break;
case 'nop_clean':
act = nopClean(ctx);
break;
case 'goto_clean':
if(req.query.cleanif==1)
act = gotoConditionnalClean(ctx);
else
act = gotoClean(ctx);
//console.log(act.data.counter),"methods cleaned";
break;
case 'wrap_clean':
// pause auto-save
act = wrapClean(ctx);
break;
}
res.status(act.status).send(act.data);
});
/*
Controller.registerHandler(IFC.HANDLER.POST, function(ctx,req,res){
console.log("POST", req.query);
res.send({ msg:"ok" });
});*/
module.exports = Controller;