ghidra/Ghidra/Features/Decompiler/ghidra_scripts/FindPotentialDecompilerProb...

469 lines
15 KiB
Java

/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Finds potential problems in code that will cause trouble for the decompiler.
//
// This script runs the decompiler on each currently defined function.
// Function that has potential issues in the decompiler output is flagged in a table with a suggestion.
// For example any references to "in_" variables, or "unaff_" are flagged with a potential solution.
// This script is still a work in progress, but it can help diagnose initial analysis of a binary for problems that
// affect decompiled output and thus the cleanness of code for follow on uses. Things like unidentified
// epilog functions that have side-effects on the stack will be uncovered.
//
// @category Analysis
import java.util.*;
import ghidra.app.decompiler.*;
import ghidra.app.decompiler.parallel.*;
import ghidra.app.script.GhidraScript;
import ghidra.app.tablechooser.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.pcode.*;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;
public class FindPotentialDecompilerProblems extends GhidraScript {
@Override
public void run() throws Exception {
if (isRunningHeadless()) {
printf("This script cannot be run in headless mode.\n");
return;
}
TableChooserDialog tableDialog = createTableChooserDialog(
"Possible Decompiler Problems: " + currentProgram.getName(), null);
configureTableColumns(tableDialog);
tableDialog.show();
IssueEntries entryList = new TableEntryList(tableDialog);
DecompilerCallback<Void> callback =
new DecompilerCallback<Void>(currentProgram, new BasicConfigurer(currentProgram)) {
@Override
public Void process(DecompileResults results, TaskMonitor tMonitor)
throws Exception {
for (ProblemLocation probLoc : processFunc(results)) {
entryList.add(probLoc);
}
return null;
}
};
Set<Function> funcsToDecompile = new HashSet<>();
FunctionIterator fIter = currentProgram.getFunctionManager().getFunctionsNoStubs(true);
fIter.forEach(e -> funcsToDecompile.add(e));
if (funcsToDecompile.isEmpty()) {
popup("No functions to decompile!");
return;
}
ParallelDecompiler.decompileFunctions(callback, funcsToDecompile, monitor);
monitor.checkCanceled();
tableDialog.setMessage("Finished");
}
private List<ProblemLocation> processFunc(DecompileResults decompResult) {
//TODO: skip if func name contains SEH_epilog or SEH_prolog?
//add general way to have architecture-specific messages?
List<ProblemLocation> problems = new ArrayList<>();
Function func = decompResult.getFunction();
HighFunction hf = decompResult.getHighFunction();
if (hf == null) {
problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(),
func.getEntryPoint(), "", "Decompilation Error"));
return problems;
}
Iterator<HighSymbol> symIter = hf.getLocalSymbolMap().getSymbols();
while (symIter.hasNext() && !monitor.isCancelled()) {
HighSymbol sym = symIter.next();
HighVariable highVar = sym.getHighVariable();
if (!(highVar instanceof HighLocal)) {
continue;
}
if (sym.getName().startsWith("in_") && !sym.getName().equals("in_FS_OFFSET")) {
// Has an input variable that is not a parameter
String possible =
"Function signature missing register param, called function passed too many register params, or only a subpiece" +
" of a register actually used.";
if (!(hf.getFunction().getSymbol().isGlobal()) &&
!(hf.getFunction().getCallingConventionName().contains("thiscall"))) {
possible += " Function might need calling convention changed to thiscall";
}
if (sym.getName().startsWith("in_stack_ff")) {
possible =
"Too many stack parameters passed to a called function. May need to redefine in the called function (could be varargs).";
}
if (sym.getName().startsWith("in_stack_00")) {
possible =
"Too few stack parameters defined for this function. May need to redefine parameters.";
}
Address funcAddr =
getFirstFuncWithVar(func, sym.getHighVariable().getRepresentative());
//if we didn't find a good location for the cause of the problem,
//just use the entry point of the function with the problem
if (funcAddr == null || funcAddr.equals(Address.NO_ADDRESS)) {
funcAddr = func.getEntryPoint();
}
problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(), funcAddr,
sym.getName(), possible));
}
if (sym.getName().startsWith("unaff_")) {
Address firstAddr = getFirstCalledFunction(func);
if (sym.getName().equals("unaff_EBP")) {
problems.add(
new ProblemLocation(currentProgram, firstAddr, func.getEntryPoint(),
sym.getName(), "Suspect function is EH_PROLOG/EH_EPILOG"));
continue;
}
// Has a side effect variable
String possible = (firstAddr != null ? "Side effect from a call"
: "Undefined parameter or global register save");
//TODO: sym.getPCAddress() points outside of function bodies in certain cases...
problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(),
sym.getPCAddress(), sym.getName(), possible));
}
//extraout_X: will sym.getHighVariable().getRepresentative return X?
if (sym.getName().startsWith("extraout")) {
// Has a side effect variable
Address funcAddr =
getFirstFuncWithVar(func, sym.getHighVariable().getRepresentative());
if (funcAddr.equals(Address.NO_ADDRESS)) {
funcAddr = func.getEntryPoint();
}
String possible =
"Bad parameter in called function or extra return value/global register/function register side effect";
if (sym.getName().startsWith("extraout_var")) {
possible = "Function containing problem may need return type adjusted.";
}
problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(), funcAddr,
sym.getName(), possible));
}
}
return problems;
}
/**
* Returns the target of the first (in address order) call in the body of {@code func}
* which takes {@code vn} as a parameter.
* @param func {@link Function} whose body to search for calls
* @param vn {@link Varnode} representing required parameter
* @return entry point of first function called by {@code func} which uses {@code vn}
* as a parameter, or {@code Address.NO_ADDRESS} if no such function found.
*/
private Address getFirstFuncWithVar(Function func, Varnode vn) {
Address variableAddr = vn.getAddress();
if (variableAddr == null) {
return Address.NO_ADDRESS;
}
// Note: this handles some cases where functions consist of non-contiguous blocks,
// but since we start at the entry point we might miss things if part of the body of
// the function is before the entry point (in address order)
ReferenceIterator refIter =
func.getProgram().getReferenceManager().getReferenceIterator(func.getEntryPoint());
Address maxAddr = func.getBody().getMaxAddress();
// return the first call to a function which takes vn as an argument
for (Reference ref : CollectionUtils.asIterable(refIter)) {
// check whether we are at an address not in the function
// only necessary in case func consists of non-contiguous blocks
// TODO: handle tail-call elimination
if (!func.getBody().contains(ref.getFromAddress())) {
continue;
}
if (isValidCallReference(ref)) {
Function calledFunc =
currentProgram.getFunctionManager().getFunctionAt(ref.getToAddress());
Parameter[] params = calledFunc.getParameters();
for (Parameter param : params) {
Address addr = param.getMinAddress();
if (addr != null && addr.equals(variableAddr)) {
return ref.getToAddress();
}
}
}
// The references are sorted by their "from" addresses, so if this condition is true,
// we've searched all the references from the body of func and haven't found anything.
// So, stop looking.
if (ref.getFromAddress().compareTo(maxAddr) > 0) {
return Address.NO_ADDRESS;
}
}
//in case there are no references with "from" addresses after the body of func
return Address.NO_ADDRESS;
}
/**
* Returns the address of first function called by {@code func}. That is, the returned {@link Address}
* is the target of the call instruction with the least address within the body of {@code func}.
* @param func the {@link Function} to search for calls
* @return the {@link Address} of the first called function, or {@code Address.NO_ADDRESS} if
* no calls are found.
*/
private Address getFirstCalledFunction(Function func) {
//could be issues if func's body has addresses that are before the entry point of
//func - see the comment in getFirstFuncWithVar
ReferenceIterator refIter =
func.getProgram().getReferenceManager().getReferenceIterator(func.getEntryPoint());
Address maxAddr = func.getBody().getMaxAddress();
for (Reference ref : CollectionUtils.asIterable(refIter)) {
// check whether we are at an address not in the function
// only necessary in case func consists of non-contiguous blocks
// TODO: handle tail-call elimination
if (!func.getBody().contains(ref.getFromAddress())) {
continue;
}
// return the first call for the function
if (isValidCallReference(ref)) {
return ref.getToAddress();
}
// The references are sorted by their "from" addresses, so if this condition is true,
// we've searched all the references from the body of func and haven't found anything.
// So, stop looking.
if (ref.getFromAddress().compareTo(maxAddr) > 0) {
return Address.NO_ADDRESS;
}
}
//in case there are no references with "from" addresses after the body of func
return Address.NO_ADDRESS;
}
//returns true precisely when ref is a call reference to a defined function
private boolean isValidCallReference(Reference ref) {
if (!ref.getReferenceType().isCall()) {
return false;
}
if (ref.getToAddress() == null) {
return false;
}
if (currentProgram.getFunctionManager().getFunctionAt(ref.getToAddress()) != null) {
return true;
}
return false;
}
static class BasicConfigurer implements DecompileConfigurer {
private Program p;
public BasicConfigurer(Program prog) {
p = prog;
}
@Override
public void configure(DecompInterface decompiler) {
decompiler.toggleCCode(true);
decompiler.toggleSyntaxTree(true);
decompiler.setSimplificationStyle("decompile");
DecompileOptions opts = new DecompileOptions();
opts.grabFromProgram(p);
decompiler.setOptions(opts);
}
}
////////////////////////////////////////////////////////////////////////////////////
// table stuff //
////////////////////////////////////////////////////////////////////////////////////
private void configureTableColumns(TableChooserDialog dialog) {
StringColumnDisplay explanationColumn = new StringColumnDisplay() {
@Override
public String getColumnName() {
return "Potential Problem";
}
@Override
public String getColumnValue(AddressableRowObject rowObject) {
ProblemLocation entry = (ProblemLocation) rowObject;
return entry.getExplanation();
}
};
StringColumnDisplay funcColumn = new StringColumnDisplay() {
@Override
public String getColumnName() {
return "Func Name";
}
@Override
public String getColumnValue(AddressableRowObject rowObject) {
ProblemLocation entry = (ProblemLocation) rowObject;
Function func = entry.getProgram().getFunctionManager().getFunctionContaining(
entry.getAddress());
if (func == null) {
return "";
}
return func.getName();
}
};
ColumnDisplay<Address> probLocColumn = new ColumnDisplay<Address>() {
@Override
public String getColumnName() {
return "Location of Possible Cause";
}
@Override
public Address getColumnValue(AddressableRowObject rowObject) {
ProblemLocation probLocation = (ProblemLocation) rowObject;
return probLocation.getWhyAddr();
}
@Override
public int compare(AddressableRowObject o1, AddressableRowObject o2) {
return getColumnValue(o1).compareTo(getColumnValue(o2));
}
@Override
public Class<Address> getColumnClass() {
return Address.class;
}
};
StringColumnDisplay varNameColumn = new StringColumnDisplay() {
@Override
public String getColumnName() {
return "Problematic Variable";
}
@Override
public String getColumnValue(AddressableRowObject rowObject) {
ProblemLocation probLocation = (ProblemLocation) rowObject;
return probLocation.getVarName();
}
@Override
public int compare(AddressableRowObject o1, AddressableRowObject o2) {
return getColumnValue(o1).compareTo(getColumnValue(o2));
}
};
dialog.addCustomColumn(funcColumn);
dialog.addCustomColumn(probLocColumn);
dialog.addCustomColumn(varNameColumn);
dialog.addCustomColumn(explanationColumn);
}
class ProblemLocation implements AddressableRowObject {
private Program program;
private Address problemAddress;
private Address causeAddress;
private String varName;
private String explanation;
ProblemLocation(Program prog, Address problemAddress, Address whyAddr, String varName,
String explanation) {
this.problemAddress = problemAddress;
this.causeAddress = whyAddr;
this.varName = varName;
this.explanation = explanation;
this.program = prog;
}
public Program getProgram() {
return program;
}
@Override
public Address getAddress() {
return getFuncAddr();
}
public Address getFuncAddr() {
if (problemAddress == null) {
return Address.NO_ADDRESS;
}
return problemAddress;
}
public Address getWhyAddr() {
if (causeAddress == null) {
return Address.NO_ADDRESS;
}
return causeAddress;
}
public String getVarName() {
return varName;
}
public String getExplanation() {
return explanation;
}
@Override
public String toString() {
return "Issue at:" + getAddress() + " found: " + getVarName() + " " +
getExplanation() + " at " + getWhyAddr();
}
}
interface IssueEntries {
void add(ProblemLocation location);
int getNumEntries();
void setMessage(String string);
}
class TableEntryList implements IssueEntries {
private TableChooserDialog tableDialog;
public TableEntryList(TableChooserDialog tableDialog) {
this.tableDialog = tableDialog;
}
@Override
public void add(ProblemLocation location) {
tableDialog.add(location);
}
@Override
public void setMessage(String string) {
tableDialog.setMessage(string);
}
@Override
public int getNumEntries() {
return tableDialog.getRowCount();
}
}
}