Merge branch 'GP-4034_thisita_fixOffcutScript--SQUASHED' (Closes #5928)

This commit is contained in:
Ryan Kurtz 2024-02-07 13:30:10 -05:00
commit bc24351495
3 changed files with 357 additions and 15 deletions

View File

@ -0,0 +1,184 @@
/* ###
* 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.
*/
// This script looks for offcut instruction(s) in the current selection or location and
// automatically fixes "safe" offcuts. This script is suitable for correcting polyglot instruction
// executable size optimizations, LOCK prefix issues, and offcut code used for code obfuscation.
//
// Offcuts are determined to be safe if they don't have additional conflicting offcuts in the same
// base instruction.
// The new instruction length override will be set by assuming there actually is an instruction at
// the safe offcut reference. If a failure to flow this instruction occurs the script will emit
// a warning about the exception and continue processing.
// A check is done for pseudo-disassembly viability before setting the instruction or flowing
// the code so these exceptions shouldn't be reached.
//
// When fixups are applied any existing Error level bookmarks for the Bad Instruction will be
// removed and replaced with info that an offcut was fixed. These can be interpreted that
// assumptions were made about the context flowed locally to the fixed instruction that should be
// taken as fact cautiously since the binary is already confirmed to be well behaved, that is
// strictly flowed.
//
//@category Analysis
import ghidra.app.script.GhidraScript;
import ghidra.app.script.ScriptMessage;
import ghidra.app.util.PseudoDisassembler;
import ghidra.app.util.PseudoInstruction;
import ghidra.program.disassemble.Disassembler;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.util.Msg;
public class FixOffcutInstructionScript extends GhidraScript {
public final String INFO_BOOKMARK_CATEGORY = "Offcut Instruction";
public final String INFO_BOOKMARK_COMMENT = "Fixed offcut instruction";
public final int MAX_OFFCUT_DISTANCE = 64;
private Listing currentListing;
private BookmarkManager currentBookmarkManager;
private ReferenceManager currentReferenceManager;
private int alignment;
@Override
protected void run() throws Exception {
currentListing = currentProgram.getListing();
currentBookmarkManager = currentProgram.getBookmarkManager();
currentReferenceManager = currentProgram.getReferenceManager();
alignment = currentProgram.getLanguage().getInstructionAlignment();
// run in strict mode if a selection
final boolean doExtraValidation = currentSelection != null;
// restrict processing to the current selection
final AddressSet restrictedSet =
(currentSelection != null) ? (new AddressSet(currentSelection))
: (new AddressSet(currentLocation.getAddress()));
final InstructionIterator instrIter = currentListing.getInstructions(restrictedSet, true);
while (instrIter.hasNext() && !monitor.isCancelled()) {
final Instruction curInstr = instrIter.next();
if (curInstr.isLengthOverridden()) {
continue;
}
final Address offcutAddress = getQualifiedOffcutAddress(curInstr, doExtraValidation);
if (offcutAddress == null) {
continue;
}
// This script is only useful for static offcut instruction fixing. Dynamic offcuts
// will raise an exception that will be logged here.
try {
fixOffcutInstruction(curInstr, offcutAddress);
}
catch (Exception e) {
Msg.error(this, new ScriptMessage("Failed to fix offuct instruction at " +
curInstr.getAddressString(false, true)), e);
}
}
}
private Address getQualifiedOffcutAddress(final Instruction instr,
final boolean doExtraValidation) {
// short-circuit too small instructions
if (instr.getLength() < 2) {
return null;
}
final Address instrAddr = instr.getAddress();
final AddressSet instrBody =
new AddressSet(instr.getMinAddress().add(1), instr.getMaxAddress());
Address offcutAddress = null;
for (final Address address : currentReferenceManager
.getReferenceDestinationIterator(instrBody, true)) {
if ((address.getOffset() % alignment) != 0) {
continue;
}
for (final Reference reference : currentReferenceManager.getReferencesTo(address)) {
final RefType refType = reference.getReferenceType();
if (doExtraValidation && Math.abs(
instrAddr.subtract(reference.getFromAddress())) > MAX_OFFCUT_DISTANCE) {
continue;
}
if (refType.isJump() && refType.hasFallthrough()) {
if (offcutAddress == null) {
offcutAddress = address;
}
}
else {
continue;
}
}
}
return offcutAddress;
}
private void fixOffcutInstruction(Instruction instr, Address offcutAddress)
throws CodeUnitInsertionException {
if (!canDisassembleAt(instr, offcutAddress)) {
Msg.warn(this,
new ScriptMessage("\t> Offcut construction would not be valid. Skipping..."));
return;
}
instr.setLengthOverride((int) offcutAddress.subtract(instr.getMinAddress()));
// Once the override is complete there will be code to disassemble.
disassemble(offcutAddress);
// Usually there will be a bookmark complaining about how there is a well formed instruction
// already at this location which this change has obsoleted
fixBookmark(offcutAddress);
}
private void fixBookmark(Address at) {
final Bookmark bookmark = currentBookmarkManager.getBookmark(at, BookmarkType.ERROR,
Disassembler.ERROR_BOOKMARK_CATEGORY);
if (bookmark != null) {
currentBookmarkManager.removeBookmark(bookmark);
// inform the user this instruction was fixed. even though the disassembly appears
// fixed the fact remains that there are two potentially conflicting context flows
// happening at this instruction and it was assumed that the exposed instruction holds
// flow attention for execution here due to the direct references.
// team opted for a simple remark rather repeat this fact about context since
// this script being applied implies the user understands the potential for conflicts
currentBookmarkManager.setBookmark(at, BookmarkType.INFO, INFO_BOOKMARK_CATEGORY,
INFO_BOOKMARK_COMMENT);
}
}
protected boolean canDisassembleAt(Instruction instr, Address at) {
try {
// only the instruction prototype is needed to determine if an instruction can exist
// in the offcut location
final PseudoDisassembler pdis = new PseudoDisassembler(currentProgram);
final PseudoInstruction testInstr = pdis.disassemble(at);
return (testInstr != null && testInstr.getMaxAddress().equals(instr.getMaxAddress()));
}
catch (InsufficientBytesException | UnknownInstructionException
| UnknownContextException e) {
Msg.error(this,
"Could not disassemble instruction at " + at + " (" + e.getMessage() + ")", e);
return false;
}
}
}

View File

@ -18,11 +18,15 @@ package ghidra.app.plugin.core.disassembler;
import db.Transaction;
import docking.action.MenuData;
import docking.widgets.dialogs.NumberInputDialog;
import ghidra.app.cmd.disassemble.DisassembleCommand;
import ghidra.app.context.ListingActionContext;
import ghidra.app.context.ListingContextAction;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.disassemble.Disassembler;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.util.CodeUnitInsertionException;
import ghidra.util.Msg;
@ -70,28 +74,15 @@ class SetLengthOverrideAction extends ListingContextAction {
int minLength = 0;
long maxLength = Math.min(Instruction.MAX_LENGTH_OVERRIDE, protoLen - 1);
Instruction nextInstr = listing.getInstructionAfter(address);
if (nextInstr != null &&
nextInstr.getAddress().getAddressSpace().equals(address.getAddressSpace())) {
long limit = nextInstr.getAddress().subtract(address);
maxLength = Math.min(limit, maxLength);
if (limit < instr.getParsedLength()) {
minLength = 1; // unable to restore to default length using 0 value
restoreTip = "";
}
}
if (maxLength == 0) {
// Assume we have an instruction whose length can't be changed
Msg.showError(this, null, "Length Override Error",
"Insufficient space to alter current length override of 1-byte");
"The length of a " + protoLen + "-byte instruction may not be overridden!");
return;
}
int currentLengthOverride = 0;
if (instr.isLengthOverridden()) {
currentLengthOverride = instr.getLength();
}
final int currentLengthOverride = getDefaultOffcutLength(program, instr);
NumberInputDialog dialog = new NumberInputDialog("Override/Restore Instruction Length",
"Enter byte-length [" + minLength + ".." + maxLength + restoreTip + alignTip + "]",
@ -112,7 +103,20 @@ class SetLengthOverrideAction extends ListingContextAction {
}
try (Transaction tx = instr.getProgram().openTransaction(kind + " Length Override")) {
if (lengthOverride == 0) {
// Clear any code units that may have been created in the offcut
final int trueLength = instr.getParsedLength();
listing.clearCodeUnits(address.add(currentLengthOverride),
address.add(trueLength - 1), false);
}
instr.setLengthOverride(lengthOverride);
final Address offcutStart = address.add(lengthOverride);
if (lengthOverride != 0 && isOffcutFlowReference(program, offcutStart)) {
tool.executeBackgroundCommand(new DisassembleCommand(offcutStart, null, true),
program);
removeErrorBookmark(program, offcutStart);
}
}
catch (CodeUnitInsertionException e) {
Msg.showError(this, null, "Length Override Error", e.getMessage());
@ -134,4 +138,39 @@ class SetLengthOverrideAction extends ListingContextAction {
return instr.getParsedLength() > alignment;
}
private int getDefaultOffcutLength(final Program program, final Instruction instr) {
if (instr.isLengthOverridden()) {
return instr.getLength();
}
final AddressSet instrBody = new AddressSet(instr.getMinAddress().next(),
instr.getMinAddress().add(instr.getParsedLength() - 1));
final Address addr =
program.getReferenceManager().getReferenceDestinationIterator(instrBody, true).next();
if (addr != null) {
final int offset = (int) addr.subtract(instr.getMinAddress());
if (offset % program.getLanguage().getInstructionAlignment() == 0) {
return offset;
}
}
return 0;
}
private boolean isOffcutFlowReference(final Program program, final Address address) {
for (Reference reference : program.getReferenceManager().getReferencesTo(address)) {
if (reference.getReferenceType().isFlow()) {
return true;
}
}
return false;
}
private void removeErrorBookmark(final Program program, final Address at) {
final BookmarkManager bookmarkManager = program.getBookmarkManager();
final Bookmark bookmark = bookmarkManager.getBookmark(at, BookmarkType.ERROR,
Disassembler.ERROR_BOOKMARK_CATEGORY);
if (bookmark != null) {
bookmarkManager.removeBookmark(bookmark);
}
}
}

View File

@ -0,0 +1,119 @@
/* ###
* 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.
*/
package ghidra.app.plugin.core.script;
import static org.junit.Assert.*;
import java.io.File;
import org.junit.*;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.test.*;
import ghidra.util.NumericUtilities;
/**
* Test the FixOffcutInstructionScript.
*/
public class FixOffcutInstructionScriptTest extends AbstractGhidraHeadedIntegrationTest {
private TestEnv env;
private PluginTool tool;
private Program program;
private ProgramBuilder programBuilder;
private CodeBrowserPlugin cb;
private File script;
private Address offcutInstructionAddress;
@Before
public void setUp() throws Exception {
env = new TestEnv();
program = buildProgram();
tool = env.launchDefaultTool(program);
cb = env.getPlugin(CodeBrowserPlugin.class);
script = Application.getModuleFile("Base", "ghidra_scripts/FixOffcutInstructionScript.java")
.getFile(true);
offcutInstructionAddress = addr("1001cea");
}
private Program buildProgram() throws Exception {
programBuilder = new ProgramBuilder("Test", ProgramBuilder._X64);
programBuilder.createMemory(".text", "0x1001000", 0x4000);
programBuilder.setBytes("1001cd8", "48 8d 4a 01");
programBuilder.setBytes("1001cdc", "48 89 d0");
programBuilder.setBytes("1001cdf", "64 83 3c 25 18 00 00 00 00");
// JZ with well formed reference into offcut instruction
programBuilder.setBytes("1001ce8", "74 01");
// LOCK CMPXCHG example offcut instruction
programBuilder.setBytes("1001cea", "f0 48 0f b1 0d 75 65 15 00");
programBuilder.setBytes("1001cf3", "48 39 d0");
programBuilder.setBytes("1001cf6", "0f 84 bd 00 00 00");
programBuilder.setBytes("1001cfc", "48 8b 15 65 65 15 00");
programBuilder.disassemble("1001cd8", 0x100, false);
return programBuilder.getProgram();
}
@After
public void tearDown() throws Exception {
env.dispose();
}
@Test
public void testFixOffcutInsruction() throws Exception {
makeSelection(tool, program, program.getMinAddress(), program.getMaxAddress());
ScriptTaskListener scriptID = env.runScript(script);
waitForScript(scriptID);
assertInstruction(offcutInstructionAddress, 1, "f0", "CMPXCHG.LOCK");
assertInstruction(offcutInstructionAddress.add(1), 8, "48 0f b1 0d 75 65 15 00", "CMPXCHG");
}
private Address addr(String address) {
return program.getAddressFactory().getAddress(address);
}
private void waitForScript(ScriptTaskListener scriptID) {
waitForScriptCompletion(scriptID, 100000);
program.flushEvents();
waitForBusyTool(tool);
}
private void assertInstruction(Address addr, int byteCount, String bytes, String mnemonic)
throws MemoryAccessException {
assertBytes(addr, byteCount, bytes);
Instruction instructionAt = program.getListing().getInstructionAt(addr);
assertEquals(byteCount, instructionAt.getLength());
assertEquals(mnemonic, instructionAt.getMnemonicString());
}
private void assertBytes(Address addr, int count, String bytes) throws MemoryAccessException {
byte[] x = new byte[count];
program.getMemory().getBytes(addr, x);
assertEquals(bytes, NumericUtilities.convertBytesToString(x, " "));
}
}