The lift command converts compiled binary executable files into the Oinkie Intermediate Representation (OIR) JSON format. The current implementation utilizes Ghidra in headless mode (without GUI) to translate machine-specific assembly instructions into platform-agnostic P-code.
🏃 Usage#
You can lift multiple binary files at once. The default behavior is to use Ghidra to analyze the executables and output the resulting JSON files to the ./pcodes directory.
oinkie lift [OPTIONS] [FILES]...Arguments#
<FILES>...
Path to the binary or intermediate files to lift.
Options#
-d, --dest <DIRECTORY>
Specify the directory to place the resulting JSON files of the lifted P-code. Defaults to the./pcodesdirectory.[default: pcodes]-l, --lifter-type <LIFTER_TYPE>
Specify the lifter type to use.[default: ghidra][possible values: ghidra, llvm, binary-ninja]-H, --home <HOME>
Specify the path to the home directory of the lifter (e.g.,GHIDRA_HOMEfor Ghidra). If not specified, the application will search the respective environment variable or look for common default paths.-i, --intermediate <DIRECTORY>
Specify a directory to keep intermediate lifter files (like Ghidra project directories). If not provided, a temporary directory is used and deleted automatically.--script <SCRIPT>
Path to a custom lifting script. The script’s interpretation depends on the lifter type. For Ghidra, it’s the path to a Java script.-S, --skip
Skip the lifting process if the output JSON file already exists in the destination directory.
🔍 Under the Hood: Ghidra Lifter#
In the backend, oinkie runs a headless Ghidra process to analyze the binary structures, extract assembly symbols, and decompile or translate instructions into P-code (Ghidra’s register-transfer language).
By default, the process automatically manages the creation, analysis, and clean-up of temporary Ghidra projects so that you only receive the final platform-independent OIR JSON representations.
☕ Default Ghidra Script: HighPCodeLifter.java#
When using the default Ghidra lifter, oinkie deploys a specialized Ghidra Script to interface with the Ghidra Decompiler API.
If you want to customize your lifting script (using the --script parameter), you can use the following Java source code of HighPCodeLifter.java as a foundational template. It decompiles each function, tracks CALL targets to populate resolved symbols, and formats instructions as P-code operations:
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.PcodeOpAST;
import ghidra.program.model.pcode.Varnode;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.listing.Function;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
public class HighPCodeLifter extends GhidraScript {
@Override
public void run() throws Exception {
DecompInterface decompInterface = new DecompInterface();
decompInterface.openProgram(currentProgram);
var path = java.nio.file.Path.of(currentProgram.getExecutablePath());
List<String> jsonOutput = new ArrayList<>();
jsonOutput.add("{");
jsonOutput.add(String.format(" \"program\": \"%s\",", currentProgram.getName()));
jsonOutput.add(String.format(" \"path\": \"%s\",", path));
List<String> functionBlocks = new ArrayList<>();
Function func = getFirstFunction();
HashMap<String, String> symbols = new HashMap<>();
while (func != null && !monitor.isCancelled()) {
if (!func.isThunk() && !func.isExternal()) {
DecompileResults results = decompInterface.decompileFunction(func, 30, monitor);
if (results != null && results.decompileCompleted()) {
functionBlocks.add(getFunctionJson(func, results.getHighFunction(), symbols));
}
}
func = getFunctionAfter(func);
}
jsonOutput.add(" \"symbols\": {");
String items = symbols.entrySet().stream()
.map(e -> String.format(" \"0x%s\": \"%s\"", e.getKey(), e.getValue()))
.collect(Collectors.joining(",\n"));
jsonOutput.add(items);
jsonOutput.add(" },");
jsonOutput.add(" \"functions\": [");
jsonOutput.add(String.join(",\n", functionBlocks));
jsonOutput.add(" ]");
jsonOutput.add("}");
outputToFile(currentProgram.getName(), jsonOutput);
decompInterface.dispose();
}
private void outputToFile(String fileName, List<String> outputs) {
Path cwd = Path.of(".");
try (var out = Files.newBufferedWriter(cwd.resolve(fileName + ".json"))) {
var w = new java.io.PrintWriter(out);
outputs.stream()
.forEach(line -> w.println(line));
} catch(java.io.IOException e) {
e.printStackTrace();
}
}
private String getFunctionJson(Function func, HighFunction highFunc, HashMap<String, String> symbols) {
List<String> opsJson = new ArrayList<>();
Iterator<PcodeOpAST> opIter = highFunc.getPcodeOps();
while (opIter.hasNext()) {
PcodeOpAST op = opIter.next();
opsJson.add(getOpJson(op));
pushSymbolsIfNeeded(op, symbols);
}
return String.format(
" {\n \"name\": \"%s\",\n \"ops\": [\n%s\n ]\n }",
func.getName(),
opsJson.stream().map(s -> " " + s).collect(Collectors.joining(",\n"))
);
}
private void pushSymbolsIfNeeded(PcodeOpAST op, HashMap<String, String> symbols) {
if (op.getOpcode() == PcodeOp.CALL) {
Varnode target = op.getInput(0);
if (target != null && target.isAddress()) {
Address addr = target.getAddress();
Function targetFunc = getFunctionAt(addr);
if (targetFunc != null) {
symbols.put(addr.toString(), targetFunc.getName());
}
}
}
}
private String getOpJson(PcodeOp op) {
String mnemonic = op.getMnemonic();
Varnode out = op.getOutput();
Varnode[] inputs = op.getInputs();
List<String> inputStrings = new ArrayList<>();
for (Varnode in : inputs) {
inputStrings.add(String.format("\"%s\"", in.toString()));
}
String inputsJson = String.join(", ", inputStrings);
if (out != null) {
return String.format(
"{\"op\": \"%s\", \"out\": \"%s\", \"inputs\": [%s]}",
mnemonic, out.toString(), inputsJson
);
} else {
return String.format(
"{\"op\": \"%s\", \"inputs\": [%s]}",
mnemonic, inputsJson
);
}
}
}📄 The OIR (Oinkie IR) JSON Schema#
The output is structured as a JSON file representing the program’s functions and their corresponding P-code operations.
Here is an example of the generated JSON file:
{
"program": "factorizer",
"path": "/home/tamada/products/oinkie/testdata/factorizer/factorizer.json",
"symbols": {
"0x1000006b0": "_atoll",
"0x1000006bc": "_printf",
"0x100000460": "_factorize"
},
"functions": [
{
"name": "_factorize",
"ops": [
{"op": "CALL", "inputs": ["(ram, 0x1000006bc, 8)", "(unique, 0x1000006d, 8)"]},
{"op": "COPY", "out": "(unique, 0x1000006d, 8)", "inputs": ["(const, 0x1000006c8, 8)"]},
{"op": "INT_SLESS", "out": "(unique, 0x2200, 1)", "inputs": ["(register, 0x4000, 8)", "(const, 0x2, 8)"]}
]
},
{
"name": "entry",
"ops": [
{"op": "INT_SLESS", "out": "(unique, 0x2200, 1)", "inputs": ["(register, 0x4000, 4)", "(const, 0x2, 4)"]},
{"op": "CBRANCH", "inputs": ["(ram, 0x100000614, 1)", "(unique, 0x2200, 1)"]}
]
}
]
}JSON Property Descriptions#
program: The name of the lifted program.path: The absolute path to the generated JSON file.symbols: A mapping of instruction memory addresses to resolved function/external API names.functions: An array of objects representing each identified function in the binary.name: The name of the function.ops: A list of sequential P-code operations inside this function. Each operation contains:op: The P-code operation type (e.g.,CALL,COPY,INT_SLESS,CBRANCH).out: (Optional) The output variable storage details.inputs: (Optional) An array of inputs used by the instruction.
