Skip to content

Commit bc24351

Browse files
committed
Merge branch 'GP-4034_thisita_fixOffcutScript--SQUASHED' (Closes NationalSecurityAgency#5928)
2 parents d7fb34b + ea99091 commit bc24351

File tree

3 files changed

+357
-15
lines changed

3 files changed

+357
-15
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/* ###
2+
* IP: GHIDRA
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// This script looks for offcut instruction(s) in the current selection or location and
17+
// automatically fixes "safe" offcuts. This script is suitable for correcting polyglot instruction
18+
// executable size optimizations, LOCK prefix issues, and offcut code used for code obfuscation.
19+
//
20+
// Offcuts are determined to be safe if they don't have additional conflicting offcuts in the same
21+
// base instruction.
22+
// The new instruction length override will be set by assuming there actually is an instruction at
23+
// the safe offcut reference. If a failure to flow this instruction occurs the script will emit
24+
// a warning about the exception and continue processing.
25+
// A check is done for pseudo-disassembly viability before setting the instruction or flowing
26+
// the code so these exceptions shouldn't be reached.
27+
//
28+
// When fixups are applied any existing Error level bookmarks for the Bad Instruction will be
29+
// removed and replaced with info that an offcut was fixed. These can be interpreted that
30+
// assumptions were made about the context flowed locally to the fixed instruction that should be
31+
// taken as fact cautiously since the binary is already confirmed to be well behaved, that is
32+
// strictly flowed.
33+
//
34+
//@category Analysis
35+
36+
import ghidra.app.script.GhidraScript;
37+
import ghidra.app.script.ScriptMessage;
38+
import ghidra.app.util.PseudoDisassembler;
39+
import ghidra.app.util.PseudoInstruction;
40+
import ghidra.program.disassemble.Disassembler;
41+
import ghidra.program.model.address.Address;
42+
import ghidra.program.model.address.AddressSet;
43+
import ghidra.program.model.lang.*;
44+
import ghidra.program.model.listing.*;
45+
import ghidra.program.model.symbol.*;
46+
import ghidra.program.model.util.CodeUnitInsertionException;
47+
import ghidra.util.Msg;
48+
49+
public class FixOffcutInstructionScript extends GhidraScript {
50+
51+
public final String INFO_BOOKMARK_CATEGORY = "Offcut Instruction";
52+
public final String INFO_BOOKMARK_COMMENT = "Fixed offcut instruction";
53+
public final int MAX_OFFCUT_DISTANCE = 64;
54+
private Listing currentListing;
55+
private BookmarkManager currentBookmarkManager;
56+
private ReferenceManager currentReferenceManager;
57+
private int alignment;
58+
59+
@Override
60+
protected void run() throws Exception {
61+
currentListing = currentProgram.getListing();
62+
currentBookmarkManager = currentProgram.getBookmarkManager();
63+
currentReferenceManager = currentProgram.getReferenceManager();
64+
alignment = currentProgram.getLanguage().getInstructionAlignment();
65+
// run in strict mode if a selection
66+
final boolean doExtraValidation = currentSelection != null;
67+
68+
// restrict processing to the current selection
69+
final AddressSet restrictedSet =
70+
(currentSelection != null) ? (new AddressSet(currentSelection))
71+
: (new AddressSet(currentLocation.getAddress()));
72+
73+
final InstructionIterator instrIter = currentListing.getInstructions(restrictedSet, true);
74+
75+
while (instrIter.hasNext() && !monitor.isCancelled()) {
76+
final Instruction curInstr = instrIter.next();
77+
78+
if (curInstr.isLengthOverridden()) {
79+
continue;
80+
}
81+
82+
final Address offcutAddress = getQualifiedOffcutAddress(curInstr, doExtraValidation);
83+
if (offcutAddress == null) {
84+
continue;
85+
}
86+
87+
// This script is only useful for static offcut instruction fixing. Dynamic offcuts
88+
// will raise an exception that will be logged here.
89+
try {
90+
fixOffcutInstruction(curInstr, offcutAddress);
91+
}
92+
catch (Exception e) {
93+
Msg.error(this, new ScriptMessage("Failed to fix offuct instruction at " +
94+
curInstr.getAddressString(false, true)), e);
95+
}
96+
}
97+
}
98+
99+
private Address getQualifiedOffcutAddress(final Instruction instr,
100+
final boolean doExtraValidation) {
101+
// short-circuit too small instructions
102+
if (instr.getLength() < 2) {
103+
return null;
104+
}
105+
final Address instrAddr = instr.getAddress();
106+
final AddressSet instrBody =
107+
new AddressSet(instr.getMinAddress().add(1), instr.getMaxAddress());
108+
Address offcutAddress = null;
109+
for (final Address address : currentReferenceManager
110+
.getReferenceDestinationIterator(instrBody, true)) {
111+
if ((address.getOffset() % alignment) != 0) {
112+
continue;
113+
}
114+
for (final Reference reference : currentReferenceManager.getReferencesTo(address)) {
115+
final RefType refType = reference.getReferenceType();
116+
if (doExtraValidation && Math.abs(
117+
instrAddr.subtract(reference.getFromAddress())) > MAX_OFFCUT_DISTANCE) {
118+
continue;
119+
}
120+
if (refType.isJump() && refType.hasFallthrough()) {
121+
if (offcutAddress == null) {
122+
offcutAddress = address;
123+
}
124+
}
125+
else {
126+
continue;
127+
}
128+
}
129+
}
130+
return offcutAddress;
131+
}
132+
133+
private void fixOffcutInstruction(Instruction instr, Address offcutAddress)
134+
throws CodeUnitInsertionException {
135+
if (!canDisassembleAt(instr, offcutAddress)) {
136+
Msg.warn(this,
137+
new ScriptMessage("\t> Offcut construction would not be valid. Skipping..."));
138+
return;
139+
}
140+
141+
instr.setLengthOverride((int) offcutAddress.subtract(instr.getMinAddress()));
142+
143+
// Once the override is complete there will be code to disassemble.
144+
disassemble(offcutAddress);
145+
146+
// Usually there will be a bookmark complaining about how there is a well formed instruction
147+
// already at this location which this change has obsoleted
148+
fixBookmark(offcutAddress);
149+
}
150+
151+
private void fixBookmark(Address at) {
152+
final Bookmark bookmark = currentBookmarkManager.getBookmark(at, BookmarkType.ERROR,
153+
Disassembler.ERROR_BOOKMARK_CATEGORY);
154+
if (bookmark != null) {
155+
currentBookmarkManager.removeBookmark(bookmark);
156+
157+
// inform the user this instruction was fixed. even though the disassembly appears
158+
// fixed the fact remains that there are two potentially conflicting context flows
159+
// happening at this instruction and it was assumed that the exposed instruction holds
160+
// flow attention for execution here due to the direct references.
161+
// team opted for a simple remark rather repeat this fact about context since
162+
// this script being applied implies the user understands the potential for conflicts
163+
currentBookmarkManager.setBookmark(at, BookmarkType.INFO, INFO_BOOKMARK_CATEGORY,
164+
INFO_BOOKMARK_COMMENT);
165+
}
166+
}
167+
168+
protected boolean canDisassembleAt(Instruction instr, Address at) {
169+
try {
170+
// only the instruction prototype is needed to determine if an instruction can exist
171+
// in the offcut location
172+
final PseudoDisassembler pdis = new PseudoDisassembler(currentProgram);
173+
final PseudoInstruction testInstr = pdis.disassemble(at);
174+
return (testInstr != null && testInstr.getMaxAddress().equals(instr.getMaxAddress()));
175+
}
176+
catch (InsufficientBytesException | UnknownInstructionException
177+
| UnknownContextException e) {
178+
Msg.error(this,
179+
"Could not disassemble instruction at " + at + " (" + e.getMessage() + ")", e);
180+
return false;
181+
}
182+
}
183+
184+
}

Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/disassembler/SetLengthOverrideAction.java

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
import db.Transaction;
1919
import docking.action.MenuData;
2020
import docking.widgets.dialogs.NumberInputDialog;
21+
import ghidra.app.cmd.disassemble.DisassembleCommand;
2122
import ghidra.app.context.ListingActionContext;
2223
import ghidra.app.context.ListingContextAction;
2324
import ghidra.framework.plugintool.PluginTool;
25+
import ghidra.program.disassemble.Disassembler;
2426
import ghidra.program.model.address.Address;
27+
import ghidra.program.model.address.AddressSet;
2528
import ghidra.program.model.listing.*;
29+
import ghidra.program.model.symbol.Reference;
2630
import ghidra.program.model.util.CodeUnitInsertionException;
2731
import ghidra.util.Msg;
2832

@@ -70,28 +74,15 @@ public void actionPerformed(ListingActionContext context) {
7074

7175
int minLength = 0;
7276
long maxLength = Math.min(Instruction.MAX_LENGTH_OVERRIDE, protoLen - 1);
73-
Instruction nextInstr = listing.getInstructionAfter(address);
74-
if (nextInstr != null &&
75-
nextInstr.getAddress().getAddressSpace().equals(address.getAddressSpace())) {
76-
long limit = nextInstr.getAddress().subtract(address);
77-
maxLength = Math.min(limit, maxLength);
78-
if (limit < instr.getParsedLength()) {
79-
minLength = 1; // unable to restore to default length using 0 value
80-
restoreTip = "";
81-
}
82-
}
8377

8478
if (maxLength == 0) {
8579
// Assume we have an instruction whose length can't be changed
8680
Msg.showError(this, null, "Length Override Error",
87-
"Insufficient space to alter current length override of 1-byte");
81+
"The length of a " + protoLen + "-byte instruction may not be overridden!");
8882
return;
8983
}
9084

91-
int currentLengthOverride = 0;
92-
if (instr.isLengthOverridden()) {
93-
currentLengthOverride = instr.getLength();
94-
}
85+
final int currentLengthOverride = getDefaultOffcutLength(program, instr);
9586

9687
NumberInputDialog dialog = new NumberInputDialog("Override/Restore Instruction Length",
9788
"Enter byte-length [" + minLength + ".." + maxLength + restoreTip + alignTip + "]",
@@ -112,7 +103,20 @@ public void actionPerformed(ListingActionContext context) {
112103
}
113104

114105
try (Transaction tx = instr.getProgram().openTransaction(kind + " Length Override")) {
106+
if (lengthOverride == 0) {
107+
// Clear any code units that may have been created in the offcut
108+
final int trueLength = instr.getParsedLength();
109+
listing.clearCodeUnits(address.add(currentLengthOverride),
110+
address.add(trueLength - 1), false);
111+
}
115112
instr.setLengthOverride(lengthOverride);
113+
114+
final Address offcutStart = address.add(lengthOverride);
115+
if (lengthOverride != 0 && isOffcutFlowReference(program, offcutStart)) {
116+
tool.executeBackgroundCommand(new DisassembleCommand(offcutStart, null, true),
117+
program);
118+
removeErrorBookmark(program, offcutStart);
119+
}
116120
}
117121
catch (CodeUnitInsertionException e) {
118122
Msg.showError(this, null, "Length Override Error", e.getMessage());
@@ -134,4 +138,39 @@ public boolean isEnabledForContext(ListingActionContext context) {
134138
return instr.getParsedLength() > alignment;
135139
}
136140

141+
private int getDefaultOffcutLength(final Program program, final Instruction instr) {
142+
if (instr.isLengthOverridden()) {
143+
return instr.getLength();
144+
}
145+
final AddressSet instrBody = new AddressSet(instr.getMinAddress().next(),
146+
instr.getMinAddress().add(instr.getParsedLength() - 1));
147+
final Address addr =
148+
program.getReferenceManager().getReferenceDestinationIterator(instrBody, true).next();
149+
if (addr != null) {
150+
final int offset = (int) addr.subtract(instr.getMinAddress());
151+
if (offset % program.getLanguage().getInstructionAlignment() == 0) {
152+
return offset;
153+
}
154+
}
155+
return 0;
156+
}
157+
158+
private boolean isOffcutFlowReference(final Program program, final Address address) {
159+
for (Reference reference : program.getReferenceManager().getReferencesTo(address)) {
160+
if (reference.getReferenceType().isFlow()) {
161+
return true;
162+
}
163+
}
164+
return false;
165+
}
166+
167+
private void removeErrorBookmark(final Program program, final Address at) {
168+
final BookmarkManager bookmarkManager = program.getBookmarkManager();
169+
final Bookmark bookmark = bookmarkManager.getBookmark(at, BookmarkType.ERROR,
170+
Disassembler.ERROR_BOOKMARK_CATEGORY);
171+
if (bookmark != null) {
172+
bookmarkManager.removeBookmark(bookmark);
173+
}
174+
}
175+
137176
}

0 commit comments

Comments
 (0)