GP-4559 Creating new Memory Search Feature that include dynamic change

detection
This commit is contained in:
ghidragon
2024-06-12 15:50:20 -04:00
parent 86c126b7f6
commit 7f7559df56
134 changed files with 15580 additions and 3105 deletions
@@ -4,9 +4,9 @@
* 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.
@@ -778,10 +778,18 @@ public class DebuggerCoordinates {
return coords;
}
public boolean isAlive() {
public static boolean isAlive(Target target) {
return target != null && target.isValid();
}
public boolean isAlive() {
return isAlive(target);
}
public static boolean isAliveAndPresent(TraceProgramView view, Target target) {
return isAlive(target) && target.getSnap() == view.getSnap();
}
protected boolean isPresent() {
TraceSchedule defaultedTime = getTime();
return target.getSnap() == defaultedTime.getSnap() && defaultedTime.isSnapOnly();
@@ -0,0 +1,112 @@
/* ###
* 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.debug.gui;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Stream;
import db.Transaction;
import ghidra.app.nav.Navigatable;
import ghidra.app.plugin.core.debug.gui.action.DebuggerReadsMemoryTrait;
import ghidra.debug.api.target.Target;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.features.base.memsearch.bytesource.SearchRegion;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.trace.model.memory.*;
import ghidra.trace.model.program.TraceProgramView;
/**
* A byte source for searching the memory of a possibly-live target in the debugger.
*
* <p>
* Because we'd like the search to preserve its state over the lifetime of the target, and the
* target "changes" by navigating snapshots, we need to allow the view to move without requiring a
* new byte source to be constructed. We <em>cannot</em>, however, just blindly follow the
* {@link Navigatable} wherever it goes. This is roughly the equivalent of a {@link Program}, but
* with knowledge of the target to cause a refresh of actual target memory when necessary.
*/
public class DebuggerByteSource implements AddressableByteSource {
private final PluginTool tool;
private final TraceProgramView view;
private final Target target;
private final DebuggerReadsMemoryTrait readsMem;
public DebuggerByteSource(PluginTool tool, TraceProgramView view, Target target,
DebuggerReadsMemoryTrait readsMem) {
this.tool = tool;
this.view = view;
this.target = target;
this.readsMem = readsMem;
}
@Override
public int getBytes(Address address, byte[] bytes, int length) {
AddressSet set = new AddressSet(address, address.add(length - 1));
try {
readsMem.getAutoSpec()
.readMemory(tool, DebuggerCoordinates.NOWHERE.view(view).target(target), set)
.get(Target.TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
return view.getMemory().getBytes(address, bytes, 0, length);
}
catch (AddressOutOfBoundsException | MemoryAccessException | InterruptedException
| ExecutionException | TimeoutException e) {
return 0;
}
}
@Override
public List<SearchRegion> getSearchableRegions() {
AddressFactory factory = view.getTrace().getBaseAddressFactory();
List<AddressSpace> spaces = Stream.of(factory.getPhysicalSpaces())
.filter(s -> s.getType() != AddressSpace.TYPE_OTHER)
.toList();
if (spaces.size() == 1) {
return DebuggerSearchRegionFactory.ALL.stream()
.map(f -> f.createRegion(null))
.toList();
}
Stream<AddressSpace> concat =
Stream.concat(Stream.of((AddressSpace) null), spaces.stream());
return concat
.flatMap(s -> DebuggerSearchRegionFactory.ALL.stream().map(f -> f.createRegion(s)))
.toList();
}
@Override
public void invalidate() {
try (Transaction tx = view.getTrace().openTransaction("Invalidate memory")) {
TraceMemoryManager mm = view.getTrace().getMemoryManager();
for (AddressSpace space : view.getTrace().getBaseAddressFactory().getAddressSpaces()) {
if (!space.isMemorySpace()) {
continue;
}
TraceMemorySpace ms = mm.getMemorySpace(space, false);
if (ms == null) {
continue;
}
ms.setState(view.getSnap(), space.getMinAddress(), space.getMaxAddress(),
TraceMemoryState.UNKNOWN);
}
}
}
}
@@ -0,0 +1,130 @@
/* ###
* 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.debug.gui;
import java.util.List;
import ghidra.features.base.memsearch.bytesource.SearchRegion;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
public enum DebuggerSearchRegionFactory {
FULL_SPACE("All Addresses", """
Searches all memory in the space, regardless of known validity.""") {
@Override
AddressSetView getAddresses(AddressSpace space, Program program) {
AddressSet set = new AddressSet();
if (space != null) {
set.add(space.getMinAddress(), space.getMaxAddress());
return set;
}
for (AddressSpace s : program.getAddressFactory().getAddressSpaces()) {
set.add(s.getMinAddress(), s.getMaxAddress());
}
return set;
}
},
VALID("Valid Addresses", """
Searches listed memory regions in the space.""") {
@Override
AddressSetView getAddresses(AddressSpace space, Program program) {
AddressSet set = new AddressSet();
for (MemoryBlock block : program.getMemory().getBlocks()) {
if (space == null || space == block.getStart().getAddressSpace()) {
set.add(block.getAddressRange());
}
}
return set;
}
@Override
boolean isDefault(AddressSpace space) {
return space == null;
}
},
WRITABLE("Writable Addresses", """
Searches listed regions marked as writable in the space.""") {
@Override
AddressSetView getAddresses(AddressSpace space, Program program) {
AddressSet set = new AddressSet();
for (MemoryBlock block : program.getMemory().getBlocks()) {
if (block.isWrite() &&
(space == null || space == block.getStart().getAddressSpace())) {
set.add(block.getAddressRange());
}
}
return set;
}
};
public static final List<DebuggerSearchRegionFactory> ALL = List.of(values());
record DebuggerSearchRegion(DebuggerSearchRegionFactory factory, AddressSpace spaces)
implements SearchRegion {
@Override
public String getName() {
return factory.getName(spaces);
}
@Override
public String getDescription() {
return factory.getDescription(spaces);
}
@Override
public AddressSetView getAddresses(Program program) {
return factory.getAddresses(spaces, program);
}
@Override
public boolean isDefault() {
return factory.isDefault(spaces);
}
}
private final String namePrefix;
private final String description;
private DebuggerSearchRegionFactory(String namePrefix, String description) {
this.namePrefix = namePrefix;
this.description = description;
}
public SearchRegion createRegion(AddressSpace space) {
return new DebuggerSearchRegion(this, space);
}
String getName(AddressSpace space) {
if (space == null) {
return namePrefix;
}
return "%s (%s)".formatted(namePrefix, space.getName());
}
String getDescription(AddressSpace spaces) {
return description;
}
abstract AddressSetView getAddresses(AddressSpace space, Program program);
boolean isDefault(AddressSpace space) {
return false;
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -51,8 +51,7 @@ import ghidra.app.plugin.core.codebrowser.MarkerServiceBackgroundColorModel;
import ghidra.app.plugin.core.debug.disassemble.CurrentPlatformTraceDisassembleCommand;
import ghidra.app.plugin.core.debug.disassemble.CurrentPlatformTraceDisassembleCommand.Reqs;
import ghidra.app.plugin.core.debug.disassemble.DebuggerDisassemblerPlugin;
import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction;
import ghidra.app.plugin.core.debug.gui.action.*;
@@ -76,6 +75,8 @@ import ghidra.debug.api.listing.MultiBlendedListingBackgroundColorModel;
import ghidra.debug.api.modules.DebuggerMissingModuleActionContext;
import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.features.base.memsearch.bytesource.EmptyByteSource;
import ghidra.framework.model.DomainFile;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
@@ -1358,4 +1359,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
.setViewerPosition(vp.getIndex(), vp.getXOffset(), vp.getYOffset());
});
}
@Override
public AddressableByteSource getByteSource() {
if (current == DebuggerCoordinates.NOWHERE) {
return EmptyByteSource.INSTANCE;
}
return new DebuggerByteSource(tool, current.getView(), current.getTarget(), readsMemTrait);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -34,8 +34,7 @@ import docking.menu.MultiStateDockingAction;
import docking.widgets.fieldpanel.support.ViewerPosition;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.byteviewer.*;
import ghidra.app.plugin.core.debug.gui.DebuggerLocationLabel;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAction;
import ghidra.app.plugin.core.debug.gui.action.*;
import ghidra.app.plugin.core.debug.gui.action.AutoReadMemorySpec.AutoReadMemorySpecConfigFieldCodec;
@@ -46,6 +45,8 @@ import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.action.GoToInput;
import ghidra.debug.api.action.LocationTrackingSpec;
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.features.base.memsearch.bytesource.EmptyByteSource;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
@@ -666,4 +667,12 @@ public class DebuggerMemoryBytesProvider extends ProgramByteViewerComponentProvi
newProvider.panel.setViewerPosition(vp);
});
}
@Override
public AddressableByteSource getByteSource() {
if (current == DebuggerCoordinates.NOWHERE) {
return EmptyByteSource.INSTANCE;
}
return new DebuggerByteSource(tool, current.getView(), current.getTarget(), readsMemTrait);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -960,4 +960,35 @@ public abstract class AbstractDBTraceMemoryManagerMemoryTest
assertArrayEquals(b.arr(1, 2, 3, 4), read.array());
}
}
@Test
public void testReplicateNpeScenario() throws Exception {
ByteBuffer buf4k = ByteBuffer.allocate(0x1000);
AddressSetView set = b.set(
b.range(0x00400000, 0x00404fff),
b.range(0x00605000, 0x00606fff),
b.range(0x7ffff7a2c000L, 0x7ffff7a33fffL));
Random random = new Random();
for (int i = 0; i < 30; i++) {
try (Transaction tx = b.startTransaction()) {
for (int j = 0; j < 3; j++) {
for (AddressRange r : set) {
for (AddressRange rc : new AddressRangeChunker(r, 0x1000)) {
if (random.nextInt(100) < 20) {
memory.setState(0, rc, TraceMemoryState.ERROR);
continue;
}
buf4k.position(0);
buf4k.limit(0x1000);
memory.putBytes(0, rc.getMinAddress(), buf4k);
}
}
}
}
try (Transaction tx = b.startTransaction()) {
memory.setState(0, b.range(0, -1), TraceMemoryState.UNKNOWN);
}
}
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -1097,6 +1097,23 @@ public class RStarTreeMapTest {
assertTrue(obj.map.isEmpty());
}
@Test
public void testAddThenRemove1() {
assertTrue(obj.map.isEmpty());
try (Transaction tx = obj.openTransaction("AddPoint")) {
obj.map.put(new ImmutableIntRect(0, 0, 0, 10), "Test");
}
assertFalse(obj.map.isEmpty());
try (Transaction tx = obj.openTransaction("RemovePoint")) {
assertTrue(obj.map.remove(new ImmutableIntRect(0, 0, 0, 10), "Test"));
}
assertTrue(obj.map.isEmpty());
}
@Test
public void testClear() {
List<Pair<IntRect, String>> points = generatePoints(rect(1, 12, 1, 12));
+10 -4
View File
@@ -508,8 +508,10 @@ src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm||GHIDRA||||END|
src/main/help/help/topics/ScalarSearchPlugin/The_Scalar_Table.htm||GHIDRA||||END|
src/main/help/help/topics/ScalarSearchPlugin/images/ScalarWindow.png||GHIDRA||||END|
src/main/help/help/topics/ScalarSearchPlugin/images/SearchAllScalarsDialog.png||GHIDRA||||END|
src/main/help/help/topics/Search/Instruction_Mnemonic_Search.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Query_Results_Dialog.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Regular_Expressions.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Search_Formats.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Search_Instruction_Patterns.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Search_Memory.htm||GHIDRA||||END|
src/main/help/help/topics/Search/Search_Program_Text.htm||GHIDRA||||END|
@@ -520,6 +522,9 @@ src/main/help/help/topics/Search/Searching.htm||GHIDRA||||END|
src/main/help/help/topics/Search/images/DirectReferences.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/EncodedStringsDialog_advancedoptions.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/EncodedStringsDialog_initial.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/MemorySearchProvider.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/MemorySearchProviderWithOptionsOn.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/MemorySearchProviderWithScanPanelOn.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/MultipleSelectionError.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/QueryResultsSearch.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchForAddressTables.png||GHIDRA||||END|
@@ -536,11 +541,7 @@ src/main/help/help/topics/Search/images/SearchInstructionsIncludeOperands.png||G
src/main/help/help/topics/Search/images/SearchInstructionsIncludeOperandsNoConsts.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchInstructionsManualSearchDialog.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchLimitExceeded.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchMemoryBinary.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchMemoryDecimal.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchMemoryHex.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchMemoryRegex.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchMemoryString.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/SearchText.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/StringSearchDialog.png||GHIDRA||||END|
src/main/help/help/topics/Search/images/StringSearchResults.png||GHIDRA||||END|
@@ -919,6 +920,11 @@ src/main/resources/images/unlock.gif||GHIDRA||||END|
src/main/resources/images/verticalSplit.png||GHIDRA||||END|
src/main/resources/images/view-sort-ascending.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/view-sort-descending.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/view_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/resources/images/view_left_right.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/resources/images/view_top_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
src/main/resources/images/viewmag+.png||Crystal Clear Icons - LGPL 2.1||||END|
src/main/resources/images/viewmag.png||GHIDRA||||END|
src/main/resources/images/window.png||GHIDRA||||END|
src/main/resources/images/wizard.png||Nuvola Icons - LGPL 2.1|||nuvola|END|
src/main/resources/images/x-office-document-template.png||Tango Icons - Public Domain|||tango icon set|END|
@@ -395,6 +395,11 @@ icon.base.util.xml.functions.bookmark = imported_bookmark.gif
icon.base.util.datatree.version.control.archive.dt.checkout.undo = vcUndoCheckOut.png
icon.base.mem.search.panel.options = view_left_right.png
icon.base.mem.search.panel.scan = view_bottom.png
icon.base.mem.search.panel.search = view_top_bottom.png
@@ -0,0 +1,112 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>Search Memory</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
<META name="generator" content="Microsoft FrontPage 4.0">
</HEAD>
<BODY lang="EN-US">
<BLOCKQUOTE>
<H1><A name="Mnemonic_Search"></A>Search for Matching Instructions</H1>
<BLOCKQUOTE>
<P>This action works only on a selection of code. It uses the selected instructions to build
a combined mask/value bit pattern that is then used to populate the search field in a
<A href="Search_Memory.htm">Memory Search Window</A>. This enables searching through memory
for a particular ordering of
instructions. There are three options available:&nbsp;</P>
<UL>
<LI><B>Include Operands</B> - All bits that make up the instruction and all bits that make
up the operands will be included in the search pattern.</LI>
<LI><B>Exclude Operands</B> - All bits that make up the instruction are included in the
search pattern but the bits that make up the operands will be masked off to enable wild
carding for those bits.</LI>
<LI><B>Include Operands (except constants)</B> - All bits that make up the instruction are
included in the search pattern and all bits that make up the operands, except constant
operands, which will be masked off to enable wild carding for those bits.</LI>
</UL>
<BLOCKQUOTE>
<P>Example:</P>
<P>A user first selects the following lines of code. Then, from the Search menu they choose
<B>Search for Matching Instructions</B> and one of the following options:</P>
<P align="center"><IMG border="1" src="images/SearchInstructions.png" alt=""></P>
<B>Option 1:</B>
<BLOCKQUOTE>
<P>If the <B>Include Operands</B> action is chosen then the search will find all
instances of the following instructions and operands.</P>
<P align="center"><IMG border="1" src="images/SearchInstructionsIncludeOperands.png" alt=
""></P>
<P>All of the bytes that make up the selected code will be searched for exactly, with no
wild carding. The bit pattern <B>10000101 11000000 01010110 01101010 00010100
01011110</B> which equates to the byte pattern <B>85 c0 56 6a 14 5e</B> is searched
for.<BR>
<BR>
</P>
</BLOCKQUOTE><B>Option 2:</B>
<BLOCKQUOTE>
<P>If the <B>Exclude Operands</B> option is chosen then the search will find all
instances of the following instructions only.</P>
<P align="center"><IMG border="1" src="images/SearchInstructionsExcludeOperands.png" alt=
""></P>
<P>Only the parts of the byte pattern that make up the instructions will be searched for
with the remaining bits used as wildcards. The bit pattern <B>10000101 11...... 01010...
01101010 ........ 01011...</B> is searched for where the .'s indicate the wild carded
values.<BR>
<BR>
</P>
</BLOCKQUOTE><B>Option 3:</B>
<BLOCKQUOTE>
<P>If the <B>Include Operands (except constants)</B> option is chosen then the search
will find all instances of the instruction and all operands except the 0x14 which is a
constant.</P>
<P align="center"><IMG border="1" src=
"images/SearchInstructionsIncludeOperandsNoConsts.png" alt=""></P>
<P>The bit pattern <B>10000101 11000000 01010110 01101010 ........ 01011110</B> which
equates to the byte pattern <B>85 c0 56 6a xx 5e</B> is searched for where xx can be any
number N between 0x0 and 0xff.<BR>
<BR>
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P><IMG alt="Note" src="help/shared/note.png">The previous operations can only work on a
<B>single</B> selected region. If multiple regions are selected, the following error dialog
will be shown and the operation will be cancelled.</P>
<P align="center"><IMG border="1" src="images/MultipleSelectionError.png" alt=""></P>
<BR>
<BR>
<P class="providedbyplugin">Provided by: <I>Mnemonic Search Plugin</I></P>
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><A href="help/topics/Search/Search_Memory.htm">Search Memory</A></LI>
</UL><BR>
<BR>
</BLOCKQUOTE>
</BODY>
</HTML>
@@ -19,17 +19,21 @@
same as a regular expression for any standard Java application. Because of restrictions on how
regular expressions are processed, regular expression searches can only be performed in the
forward direction. Unlike standard string searches, case sensitivity and unicode options do not
apply. The <I>Search Memory</I> dialog below shows a sample regular expression entered in the
apply. The <I>Search Memory</I> window below shows a sample regular expression entered in the
<I>Value</I> field.</P>
<TABLE border="0" width="100%">
<BLOCKQUOTE>
<BLOCKQUOTE>
<TABLE border="0" width="80%">
<TBODY>
<TR>
<TD width="100%" align="center"><IMG border="0" src="images/SearchMemoryRegex.png" alt=""></TD>
<TD align="left"><IMG border="0" src="images/SearchMemoryRegex.png" alt=""></TD>
</TR>
</TBODY>
</TABLE>
</BLOCKQUOTE>
<H3>Examples</H3>
<BLOCKQUOTE>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

@@ -4,9 +4,9 @@
* 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.
@@ -18,6 +18,8 @@ package ghidra.app.nav;
import javax.swing.Icon;
import ghidra.app.util.ListingHighlightProvider;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.features.base.memsearch.bytesource.ProgramByteSource;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
@@ -193,5 +195,16 @@ public interface Navigatable {
* @param highlightProvider the provider
* @param program the program
*/
public void removeHighlightProvider(ListingHighlightProvider highlightProvider, Program program);
public void removeHighlightProvider(ListingHighlightProvider highlightProvider,
Program program);
/**
* Returns a source for providing byte values of the program associated with this
* navigatable. For a static program, this is just a wrapper for a program's memory. But
* dynamic programs require special handling for reading bytes.
* @return a source of bytes for the navigatable's program
*/
public default AddressableByteSource getByteSource() {
return new ProgramByteSource(getProgram());
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -305,6 +305,7 @@ class MemSearchDialog extends ReusableDialogComponentProvider {
JPanel inputPanel = new JPanel();
inputPanel.setLayout(new GridLayout(0, 1));
valueComboBox = new GhidraComboBox<>();
valueComboBox.setAutoCompleteEnabled(false); // we do our own completion with validation
valueComboBox.setEditable(true);
valueComboBox.setToolTipText(currentFormat.getToolTip());
valueComboBox.setDocument(new RestrictedInputDocument());
@@ -4,9 +4,9 @@
* 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.
@@ -16,14 +16,14 @@
package ghidra.app.plugin.core.searchmem;
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.util.*;
import javax.swing.Icon;
import javax.swing.JComponent;
import docking.*;
import docking.action.*;
import docking.action.DockingAction;
import docking.action.MenuData;
import docking.tool.ToolConstants;
import docking.widgets.fieldpanel.support.Highlight;
import docking.widgets.table.threaded.*;
@@ -64,7 +64,7 @@ import ghidra.util.task.*;
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
status = PluginStatus.DEPRECATED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.SEARCH,
shortDescription = "Search bytes in memory",
@@ -73,12 +73,12 @@ import ghidra.util.task.*;
" The value may contain \"wildcards\" or regular expressions" +
" that will match any byte or nibble.",
servicesRequired = { ProgramManager.class, GoToService.class, TableService.class, CodeViewerService.class },
servicesProvided = { MemorySearchService.class },
// servicesProvided = { MemorySearchService.class },
eventsConsumed = { ProgramSelectionPluginEvent.class }
)
//@formatter:on
public class MemSearchPlugin extends Plugin implements OptionsChangeListener,
DockingContextListener, NavigatableRemovalListener, MemorySearchService {
DockingContextListener, NavigatableRemovalListener {
/** Constant for read/writeConfig() for dialog options */
private static final String SHOW_ADVANCED_OPTIONS = "Show Advanced Options";
@@ -243,12 +243,12 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener,
}
@Override
public void setIsMnemonic(boolean isMnemonic) {
// provides the dialog with the knowledge of whether or not
// the action being performed is a MnemonicSearchPlugin
this.isMnemonic = isMnemonic;
}
// @Override
// public void setIsMnemonic(boolean isMnemonic) {
// // provides the dialog with the knowledge of whether or not
// // the action being performed is a MnemonicSearchPlugin
// this.isMnemonic = isMnemonic;
// }
private void setNavigatable(Navigatable newNavigatable) {
if (newNavigatable == navigatable) {
@@ -329,16 +329,16 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener,
return new BytesFieldLocation(program, address);
}
@Override
public void search(byte[] bytes, NavigatableActionContext context) {
setNavigatable(context.getNavigatable());
invokeSearchDialog(context);
}
@Override
public void setSearchText(String maskedString) {
searchDialog.setSearchText(maskedString);
}
// @Override
// public void search(byte[] bytes, NavigatableActionContext context) {
// setNavigatable(context.getNavigatable());
// invokeSearchDialog(context);
// }
//
// @Override
// public void setSearchText(String maskedString) {
// searchDialog.setSearchText(maskedString);
// }
private void createActions() {
searchAction = new NavigatableContextAction("Search Memory", getName(), false) {
@@ -349,9 +349,8 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener,
}
};
searchAction.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, searchAction.getName()));
String[] menuPath = new String[] { "&Search", "&Memory..." };
String[] menuPath = new String[] { "&Search", "Memory (Deprecated)..." };
searchAction.setMenuBarData(new MenuData(menuPath, "search"));
searchAction.setKeyBindingData(new KeyBindingData('S', 0));
searchAction.setDescription("Search Memory for byte sequence");
searchAction.addToWindowWhen(NavigatableActionContext.class);
tool.addAction(searchAction);
@@ -372,10 +371,10 @@ public class MemSearchPlugin extends Plugin implements OptionsChangeListener,
.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, searchAgainAction.getName()));
menuPath = new String[] { "&Search", "Repeat Memory Search" };
searchAgainAction.setMenuBarData(new MenuData(menuPath, "search"));
searchAgainAction.setKeyBindingData(new KeyBindingData(KeyEvent.VK_F3, 0));
searchAgainAction.setDescription("Search Memory for byte sequence");
searchAgainAction.addToWindowWhen(NavigatableActionContext.class);
tool.addAction(searchAgainAction);
}
private void initializeOptionListeners() {
@@ -4,9 +4,9 @@
* 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.
@@ -15,23 +15,36 @@
*/
package ghidra.app.services;
import ghidra.app.context.NavigatableActionContext;
import ghidra.app.nav.Navigatable;
import ghidra.features.base.memsearch.gui.MemorySearchProvider;
import ghidra.features.base.memsearch.gui.SearchSettings;
/**
* Service for invoking the {@link MemorySearchProvider}
* @deprecated This is not a generally useful service, may go away at some point
*/
@Deprecated(since = "11.2")
public interface MemorySearchService {
/*
* sets up MemSearchDialog based on given bytes
/**
* Creates a new memory search provider window
* @param navigatable the navigatable used to get bytes to search
* @param input the input string to search for
* @param settings the settings that determine how to interpret the input string
* @param useSelection true if the provider should automatically restrict to a selection if
* a selection exists in the navigatable
*/
public void search(byte[] bytes, NavigatableActionContext context);
public void createMemorySearchProvider(Navigatable navigatable, String input,
SearchSettings settings, boolean useSelection);
/*
* sets the search value field to the masked bit string
*/
public void setSearchText(String maskedString);
// These method were removed because they didn't work correctly and were specific to the needs of
// one outlier plugin. The functionality has been replaced by the above method, which is also
// unlikely to be useful.
/*
* determines whether the dialog was called by a mnemonic or not
*/
public void setIsMnemonic(boolean isMnemonic);
// public void search(byte[] bytes, NavigatableActionContext context);
//
// public void setSearchText(String maskedString);
//
// public void setIsMnemonic(boolean isMnemonic);
}
@@ -4,9 +4,9 @@
* 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.
@@ -25,8 +25,10 @@ import ghidra.app.util.viewer.format.FieldFormatModel;
import ghidra.app.util.viewer.format.FormatManager;
import ghidra.app.util.viewer.proxy.ProxyObj;
import ghidra.framework.options.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.*;
import ghidra.program.util.AddressFieldLocation;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.exception.AssertException;
@@ -128,16 +130,34 @@ public class ArrayValuesFieldFactory extends FieldFactory {
@Override
public FieldLocation getFieldLocation(ListingField lf, BigInteger index, int fieldNum,
ProgramLocation location) {
if (!(location instanceof ArrayElementFieldLocation)) {
// Unless the location is specifically targeting the address field, then we should
// process the location because the arrays display is different from all other format
// models in that one line actually represents more than one array data element. So
// this field has the best chance of representing the location for array data elements.
if (location instanceof AddressFieldLocation) {
return null;
}
ArrayElementFieldLocation loc = (ArrayElementFieldLocation) location;
ListingTextField btf = (ListingTextField) lf;
Data firstDataOnLine = (Data) btf.getProxy().getObject();
int elementIndex = loc.getElementIndexOnLine(firstDataOnLine);
RowColLocation rcl = btf.dataToScreenLocation(elementIndex, loc.getCharOffset());
return new FieldLocation(index, fieldNum, rcl.row(), rcl.col());
RowColLocation rcl = getDataRowColumnLocation(location, lf);
return new FieldLocation(index, fieldNum, rcl.row(), rcl.col());
}
private RowColLocation getDataRowColumnLocation(ProgramLocation location, ListingField field) {
ListingTextField ltf = (ListingTextField) field;
Data firstDataOnLine = (Data) field.getProxy().getObject();
if (location instanceof ArrayElementFieldLocation loc) {
int elementIndex = loc.getElementIndexOnLine(firstDataOnLine);
return ltf.dataToScreenLocation(elementIndex, loc.getCharOffset());
}
Address byteAddress = location.getByteAddress();
int byteOffset = (int) byteAddress.subtract(firstDataOnLine.getAddress());
int componentSize = firstDataOnLine.getLength();
int elementOffset = byteOffset / componentSize;
return ltf.dataToScreenLocation(elementOffset, 0);
}
@Override
@@ -0,0 +1,125 @@
/* ###
* 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.features.base.memsearch.bytesequence;
import ghidra.features.base.memsearch.bytesource.AddressableByteSource;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
/**
* This class provides a {@link ByteSequence} view into an {@link AddressableByteSource}. By
* specifying an address and length, this class provides a view into the byte source
* as a indexable sequence of bytes. It is mutable and can be reused by setting a new
* address range for this sequence. This was to avoid constantly allocating large byte arrays.
*/
public class AddressableByteSequence implements ByteSequence {
private final AddressableByteSource byteSource;
private final byte[] bytes;
private final int capacity;
private Address startAddress;
private int length;
/**
* Constructor
* @param byteSource the source of the underlying bytes that is a buffer into
* @param capacity the maximum size range that this object will buffer
*/
public AddressableByteSequence(AddressableByteSource byteSource, int capacity) {
this.byteSource = byteSource;
this.capacity = capacity;
this.length = 0;
this.bytes = new byte[capacity];
}
/**
* Sets this view to an empty byte sequence
*/
public void clear() {
startAddress = null;
length = 0;
}
/**
* Sets the range of bytes that this object will buffer. This immediately will read the bytes
* from the byte source into it's internal byte array buffer.
* @param range the range of bytes to buffer
*/
public void setRange(AddressRange range) {
// Note that this will throw an exception if the range length is larger then Integer.MAX
// which is unsupported by the ByteSequence interface
try {
setRange(range.getMinAddress(), range.getBigLength().intValueExact());
}
catch (ArithmeticException e) {
throw new IllegalArgumentException("Length exceeds capacity");
}
}
/**
* Returns the address of the byte represented by the given index into this buffer.
* @param index the index into the buffer to get its associated address
* @return the Address for the given index
*/
public Address getAddress(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException();
}
if (index == 0) {
return startAddress;
}
return startAddress.add(index);
}
@Override
public int getLength() {
return length;
}
@Override
public byte getByte(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException();
}
return bytes[index];
}
@Override
public byte[] getBytes(int index, int size) {
if (index < 0 || index + size > length) {
throw new IndexOutOfBoundsException();
}
byte[] results = new byte[size];
System.arraycopy(bytes, index, results, 0, size);
return results;
}
@Override
public boolean hasAvailableBytes(int index, int length) {
return index >= 0 && index + length <= getLength();
}
private void setRange(Address start, int length) {
if (length > capacity) {
throw new IllegalArgumentException("Length exceeds capacity");
}
this.startAddress = start;
this.length = length;
byteSource.getBytes(start, bytes, length);
}
}
@@ -0,0 +1,53 @@
/* ###
* 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.features.base.memsearch.bytesequence;
/**
* An interface for accessing bytes from a byte source.
*/
public interface ByteSequence {
/**
* Returns the length of available bytes.
* @return the length of the sequence of bytes
*/
public int getLength();
/**
* Returns the byte at the given index. The index must between 0 and the extended length.
* @param index the index in the byte sequence to retrieve a byte value
* @return the byte at the given index
*/
public byte getByte(int index);
/**
* A convenience method for checking if this sequence can provide a range of bytes from some
* offset.
* @param index the index of the start of the range to check for available bytes
* @param length the length of the range to check for available bytes
* @return true if bytes are available for the given range
*/
public boolean hasAvailableBytes(int index, int length);
/**
* Returns a byte array containing the bytes from the given range.
* @param start the start index of the range to get bytes
* @param length the number of bytes to get
* @return a byte array containing the bytes from the given range
*/
public byte[] getBytes(int start, int length);
}
@@ -0,0 +1,98 @@
/* ###
* 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.features.base.memsearch.bytesequence;
/**
* A class for accessing a contiguous sequence of bytes from some underlying byte source to
* be used for searching for a byte pattern within the byte source. This sequence of bytes
* consists of two parts; the primary sequence and an extended sequence. Search matches
* must begin in the primary sequence, but may extend into the extended sequence.
* <P>
* Searching large ranges of memory can be partitioned into searching smaller chunks. But
* to handle search sequences that span chunks, two chunks are presented at a time, with the second
* chunk being the extended bytes. On the next iteration of the search loop, the extended chunk
* will become the primary chunk, with the next chunk after that becoming the extended sequence
* and so on.
*/
public class ExtendedByteSequence implements ByteSequence {
private ByteSequence main;
private ByteSequence extended;
private int extendedLength;
/**
* Constructs an extended byte sequence from two {@link ByteSequence}s.
* @param main the byte sequence where search matches may start
* @param extended the byte sequence where search matches may extend into
* @param extendedLimit specifies how much of the extended byte sequence to allow search
* matches to extend into. (The extended buffer will be the primary buffer next time, so
* it is a full size buffer, but we only need to use a portion of it to support overlap.
*/
public ExtendedByteSequence(ByteSequence main, ByteSequence extended, int extendedLimit) {
this.main = main;
this.extended = extended;
this.extendedLength = main.getLength() + Math.min(extendedLimit, extended.getLength());
}
@Override
public int getLength() {
return main.getLength();
}
/**
* Returns the overall length of sequence of available bytes. This will be the length of
* the primary sequence as returned by {@link #getLength()} plus the length of the available
* extended bytes, if any.
* @return the
*/
public int getExtendedLength() {
return extendedLength;
}
@Override
public byte getByte(int i) {
int mainLength = main.getLength();
if (i >= mainLength) {
return extended.getByte(i - mainLength);
}
return main.getByte(i);
}
@Override
public byte[] getBytes(int index, int size) {
if (index < 0 || index + size > extendedLength) {
throw new IndexOutOfBoundsException();
}
int length = main.getLength();
if (index + size < length) {
return main.getBytes(index, size);
}
if (index >= length) {
return extended.getBytes(index - length, size);
}
// otherwise it spans
byte[] results = new byte[size];
for (int i = 0; i < size; i++) {
results[i] = getByte(index + i);
}
return results;
}
@Override
public boolean hasAvailableBytes(int index, int length) {
return index >= 0 && index + length <= getExtendedLength();
}
}
@@ -0,0 +1,59 @@
/* ###
* 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.features.base.memsearch.bytesource;
import java.util.List;
import ghidra.program.model.address.Address;
/**
* Interface for reading bytes from a program. This provides a level of indirection for reading the
* bytes of a program so that the provider of the bytes can possibly do more than just reading the
* bytes from the static program. For example, a debugger would have the opportunity to refresh the
* bytes first.
* <P>
* This interface also provides methods for determining what regions of memory can be queried and
* what addresses sets are associated with those regions. This would allow client to present choices
* about what areas of memory they are interested in AND are valid to be examined.
*/
public interface AddressableByteSource {
/**
* Retrieves the byte values for an address range.
*
* @param address The address of the first byte in the range
* @param bytes the byte array to store the retrieved byte values
* @param length the number of bytes to retrieve
* @return the number of bytes actually retrieved
*/
public int getBytes(Address address, byte[] bytes, int length);
/**
* Returns a list of memory regions where each region has an associated address set of valid
* addresses that can be read.
*
* @return a list of readable regions
*/
public List<SearchRegion> getSearchableRegions();
/**
* Invalidates any caching of byte values. This intended to provide a hint in debugging scenario
* that we are about to issue a sequence of byte value requests where we are re-acquiring
* previous requested byte values to look for changes.
*/
public void invalidate();
}
@@ -0,0 +1,42 @@
/* ###
* 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.features.base.memsearch.bytesource;
import java.util.List;
import ghidra.program.model.address.Address;
/**
* Implementation for an empty {@link AddressableByteSource}
*/
public enum EmptyByteSource implements AddressableByteSource {
INSTANCE;
@Override
public int getBytes(Address address, byte[] bytes, int length) {
return 0;
}
@Override
public List<SearchRegion> getSearchableRegions() {
return List.of();
}
@Override
public void invalidate() {
// nothing to do
}
}
@@ -0,0 +1,56 @@
/* ###
* 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.features.base.memsearch.bytesource;
import java.util.List;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryAccessException;
/**
* {@link AddressableByteSource} implementation for a Ghidra {@link Program}
*/
public class ProgramByteSource implements AddressableByteSource {
private Memory memory;
public ProgramByteSource(Program program) {
memory = program.getMemory();
}
@Override
public int getBytes(Address address, byte[] bytes, int length) {
try {
return memory.getBytes(address, bytes, 0, length);
}
catch (MemoryAccessException e) {
return 0;
}
}
@Override
public List<SearchRegion> getSearchableRegions() {
return ProgramSearchRegion.ALL;
}
@Override
public void invalidate() {
// nothing to do in the static case
}
}
@@ -0,0 +1,78 @@
/* ###
* 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.features.base.memsearch.bytesource;
import java.util.List;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
/**
* An enum specifying the selectable regions within a {@link Program} that users can select for
* searching memory.
*/
public enum ProgramSearchRegion implements SearchRegion {
LOADED("Loaded Blocks",
"Searches all memory blocks that represent loaded program instructions and data") {
@Override
public boolean isDefault() {
return true;
}
@Override
public AddressSetView getAddresses(Program program) {
Memory memory = program.getMemory();
return memory.getLoadedAndInitializedAddressSet();
}
},
OTHER("All Other Blocks", "Searches non-loaded initialized blocks") {
@Override
public boolean isDefault() {
return false;
}
@Override
public AddressSetView getAddresses(Program program) {
Memory memory = program.getMemory();
AddressSetView all = memory.getAllInitializedAddressSet();
AddressSetView loaded = memory.getLoadedAndInitializedAddressSet();
return all.subtract(loaded);
}
};
public static final List<SearchRegion> ALL = List.of(values());
private String name;
private String description;
ProgramSearchRegion(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
}
@@ -0,0 +1,52 @@
/* ###
* 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.features.base.memsearch.bytesource;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program;
/**
* Interface to specify a named region within a byte source (Program) that users can select to
* specify {@link AddressSetView}s that can be searched.
*/
public interface SearchRegion {
/**
* The name of the region.
* @return the name of the region
*/
public String getName();
/**
* Returns a description of the region.
* @return a description of the region
*/
public String getDescription();
/**
* Returns the set of addresses from a specific program that is associated with this region.
* @param program the program that determines the specific addresses for a named region
* @return the set of addresses for this region as applied to the given program
*/
public AddressSetView getAddresses(Program program);
/**
* Returns true if this region should be included in the default selection of which regions to
* search.
* @return true if this region should be selected by default
*/
public boolean isDefault();
}
@@ -0,0 +1,123 @@
/* ###
* 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.features.base.memsearch.combiner;
import java.util.*;
import java.util.function.BiFunction;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.program.model.address.Address;
/**
* An enum of search results "combiners". Each combiner determines how to combine two sets of
* memory search results. The current or existing results is represented as the "A" set and the
* new search is represented as the "B" set.
*/
public enum Combiner {
REPLACE("New", Combiner::replace),
UNION("Add To", Combiner::union),
INTERSECT("Intersect", Combiner::intersect),
XOR("Xor", Combiner::xor),
A_MINUS_B("A-B", Combiner::subtract),
B_MINUS_A("B-A", Combiner::reverseSubtract);
private String name;
private BiFunction<List<MemoryMatch>, List<MemoryMatch>, Collection<MemoryMatch>> function;
private Combiner(String name,
BiFunction<List<MemoryMatch>, List<MemoryMatch>, Collection<MemoryMatch>> function) {
this.name = name;
this.function = function;
}
public String getName() {
return name;
}
public Collection<MemoryMatch> combine(List<MemoryMatch> matches1, List<MemoryMatch> matches2) {
return function.apply(matches1, matches2);
}
private static Collection<MemoryMatch> replace(List<MemoryMatch> matches1,
List<MemoryMatch> matches2) {
return matches2;
}
private static Collection<MemoryMatch> union(List<MemoryMatch> matches1,
List<MemoryMatch> matches2) {
Map<Address, MemoryMatch> matches1Map = createMap(matches1);
for (MemoryMatch match2 : matches2) {
Address address = match2.getAddress();
MemoryMatch match1 = matches1Map.get(address);
if (match1 == null || match2.getLength() > match1.getLength()) {
matches1Map.put(address, match2);
}
}
return matches1Map.values();
}
private static Collection<MemoryMatch> intersect(List<MemoryMatch> matches1,
List<MemoryMatch> matches2) {
List<MemoryMatch> intersection = new ArrayList<>();
Map<Address, MemoryMatch> matches1Map = createMap(matches1);
for (MemoryMatch match2 : matches2) {
Address address = match2.getAddress();
MemoryMatch match1 = matches1Map.get(address);
if (match1 != null) {
MemoryMatch best = match2.getLength() > match1.getLength() ? match2 : match1;
intersection.add(best);
}
}
return intersection;
}
private static List<MemoryMatch> xor(List<MemoryMatch> matches1, List<MemoryMatch> matches2) {
List<MemoryMatch> results = new ArrayList<>();
results.addAll(subtract(matches1, matches2));
results.addAll(subtract(matches2, matches1));
return results;
}
private static Collection<MemoryMatch> subtract(List<MemoryMatch> matches1,
List<MemoryMatch> matches2) {
Map<Address, MemoryMatch> matches1Map = createMap(matches1);
for (MemoryMatch match2 : matches2) {
Address address = match2.getAddress();
matches1Map.remove(address);
}
return matches1Map.values();
}
private static Collection<MemoryMatch> reverseSubtract(List<MemoryMatch> matches1,
List<MemoryMatch> matches2) {
return subtract(matches2, matches1);
}
private static Map<Address, MemoryMatch> createMap(List<MemoryMatch> matches) {
Map<Address, MemoryMatch> map = new HashMap<>();
for (MemoryMatch result : matches) {
map.put(result.getAddress(), result);
}
return map;
}
}
@@ -0,0 +1,188 @@
/* ###
* 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.features.base.memsearch.format;
import java.util.*;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
import ghidra.util.HTMLUtilities;
/**
* {@link SearchFormat} for parsing and display bytes in a binary format. This format only
* accepts 0s or 1s or wild card characters.
*/
class BinarySearchFormat extends SearchFormat {
private static final String VALID_CHARS = "01x?.";
private static final int MAX_GROUP_SIZE = 8;
BinarySearchFormat() {
super("Binary");
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
List<String> byteGroups = getByteGroups(input);
if (hasInvalidChars(byteGroups)) {
return new InvalidByteMatcher("Invalid character");
}
if (checkGroupSize(byteGroups)) {
return new InvalidByteMatcher("Max group size exceeded. Enter <space> to add more.");
}
byte[] bytes = getBytes(byteGroups);
byte[] masks = getMask(byteGroups);
return new MaskedByteSequenceByteMatcher(input, bytes, masks, settings);
}
@Override
public String getToolTip() {
return HTMLUtilities.toHTML(
"Interpret value as a sequence of binary digits.\n" +
"Spaces will start the next byte. Bit sequences less\n" +
"than 8 bits are padded with 0's to the left. \n" +
"Enter 'x', '.' or '?' for a wildcard bit");
}
private boolean checkGroupSize(List<String> byteGroups) {
for (String byteGroup : byteGroups) {
if (byteGroup.length() > MAX_GROUP_SIZE) {
return true;
}
}
return false;
}
private List<String> getByteGroups(String input) {
List<String> list = new ArrayList<String>();
StringTokenizer st = new StringTokenizer(input);
while (st.hasMoreTokens()) {
list.add(st.nextToken());
}
return list;
}
private boolean hasInvalidChars(List<String> byteGroups) {
for (String byteGroup : byteGroups) {
if (hasInvalidChars(byteGroup)) {
return true;
}
}
return false;
}
private boolean hasInvalidChars(String string) {
for (int i = 0; i < string.length(); i++) {
if (VALID_CHARS.indexOf(string.charAt(i)) < 0) {
return true;
}
}
return false;
}
private byte getByte(String token) {
byte b = 0;
for (int i = 0; i < token.length(); i++) {
b <<= 1;
char c = token.charAt(i);
if (c == '1') {
b |= 1;
}
}
return b;
}
/**
* Return a mask byte that has a bit set to 1 for each bit that is not a wildcard. Any bits
* that aren't specified (i.e. token.lenght &lt; 8) are treated as valid test bits.
* @param token the string of bits to determine a mask for.
*/
private byte getMask(String token) {
byte b = 0;
for (int i = 0; i < 8; i++) {
b <<= 1;
if (i < token.length()) {
char c = token.charAt(i);
if (c == '1' || c == '0') {
b |= 1;
}
}
else {
b |= 1;
}
}
return b;
}
private byte[] getBytes(List<String> byteGroups) {
byte[] bytes = new byte[byteGroups.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = getByte(byteGroups.get(i));
}
return bytes;
}
private byte[] getMask(List<String> byteGroups) {
byte[] masks = new byte[byteGroups.size()];
for (int i = 0; i < masks.length; i++) {
masks[i] = getMask(byteGroups.get(i));
}
return masks;
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
SearchFormat oldFormat = oldSettings.getSearchFormat();
if (oldFormat.getFormatType() != SearchFormatType.STRING_TYPE) {
ByteMatcher byteMatcher = oldFormat.parse(text, oldSettings);
if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) {
byte[] bytes = matcher.getBytes();
byte[] mask = matcher.getMask();
return getMaskedInputString(bytes, mask);
}
}
return isValidText(text, newSettings) ? text : "";
}
private String getMaskedInputString(byte[] bytes, byte[] masks) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
for (int shift = 7; shift >= 0; shift--) {
int bit = bytes[i] >> shift & 0x1;
int maskBit = masks[i] >> shift & 0x1;
builder.append(maskBit == 0 ? '.' : Integer.toString(bit));
}
builder.append(" ");
}
return builder.toString().trim();
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.BYTE;
}
}
@@ -0,0 +1,280 @@
/* ###
* 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.features.base.memsearch.format;
import java.math.BigInteger;
import java.util.StringTokenizer;
import org.bouncycastle.util.Arrays;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
import ghidra.util.HTMLUtilities;
/**
* {@link SearchFormat} for parsing and display bytes in a decimal format. It supports sizes of
* 2,4,8,16 and can be either signed or unsigned.
*/
class DecimalSearchFormat extends SearchFormat {
DecimalSearchFormat() {
super("Decimal");
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
int byteSize = settings.getDecimalByteSize();
StringTokenizer tokenizer = new StringTokenizer(input);
int tokenCount = tokenizer.countTokens();
byte[] bytes = new byte[tokenCount * byteSize];
int bytesPosition = 0;
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
NumberParseResult result = parseNumber(token, settings);
if (result.errorMessage() != null) {
return new InvalidByteMatcher(result.errorMessage(), result.validInput());
}
System.arraycopy(result.bytes(), 0, bytes, bytesPosition, byteSize);
bytesPosition += byteSize;
}
return new MaskedByteSequenceByteMatcher(input, bytes, settings);
}
private NumberParseResult parseNumber(String tok, SearchSettings settings) {
BigInteger min = getMin(settings);
BigInteger max = getMax(settings);
try {
if (tok.equals("-")) {
if (settings.isDecimalUnsigned()) {
return new NumberParseResult(null,
"Negative numbers not allowed for unsigned values", false);
}
return new NumberParseResult(null, "Incomplete negative number", true);
}
BigInteger value = new BigInteger(tok);
if (value.compareTo(min) < 0 || value.compareTo(max) > 0) {
return new NumberParseResult(null,
"Number must be in the range [" + min + ", " + max + "]", false);
}
long longValue = value.longValue();
return createBytesResult(longValue, settings);
}
catch (NumberFormatException e) {
return new NumberParseResult(null, "Number parse error: " + e.getMessage(), false);
}
}
private BigInteger getMax(SearchSettings settings) {
boolean unsigned = settings.isDecimalUnsigned();
int size = settings.getDecimalByteSize();
int shift = unsigned ? 8 * size : 8 * size - 1;
return BigInteger.ONE.shiftLeft(shift).subtract(BigInteger.ONE);
}
private BigInteger getMin(SearchSettings settings) {
boolean unsigned = settings.isDecimalUnsigned();
int size = settings.getDecimalByteSize();
if (unsigned) {
return BigInteger.ZERO;
}
return BigInteger.ONE.shiftLeft(8 * size - 1).negate();
}
private NumberParseResult createBytesResult(long value, SearchSettings settings) {
int byteSize = settings.getDecimalByteSize();
byte[] bytes = new byte[byteSize];
for (int i = 0; i < byteSize; i++) {
byte b = (byte) value;
bytes[i] = b;
value >>= 8;
}
if (settings.isBigEndian()) {
reverse(bytes);
}
return new NumberParseResult(bytes, null, true);
}
@Override
public String getToolTip() {
return HTMLUtilities.toHTML(
"Interpret values as a sequence of decimal numbers, separated by spaces");
}
@Override
public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) {
int byteSize = settings.getDecimalByteSize();
// check each value one at a time, and return the first one different
for (int i = 0; i < bytes1.length / byteSize; i++) {
long value1 = getValue(bytes1, i * byteSize, settings);
long value2 = getValue(bytes2, i * byteSize, settings);
if (value1 != value2) {
if (byteSize == 8 && settings.isDecimalUnsigned()) {
return Long.compareUnsigned(value1, value2);
}
return Long.compare(value1, value2);
}
}
return 0;
}
public long getValue(byte[] bytes, int index, SearchSettings settings) {
boolean isBigEndian = settings.isBigEndian();
int byteSize = settings.getDecimalByteSize();
boolean isUnsigned = settings.isDecimalUnsigned();
byte[] bigEndianBytes = getBigEndianBytes(bytes, index, isBigEndian, byteSize);
long value = isUnsigned ? bigEndianBytes[0] & 0xff : bigEndianBytes[0];
for (int i = 1; i < byteSize; i++) {
value = (value << 8) | (bigEndianBytes[i] & 0xff);
}
return value;
}
private byte[] getBigEndianBytes(byte[] bytes, int index, boolean isBigEndian, int byteSize) {
byte[] bigEndianBytes = new byte[byteSize];
System.arraycopy(bytes, index * byteSize, bigEndianBytes, 0, byteSize);
if (!isBigEndian) {
reverse(bigEndianBytes);
}
return bigEndianBytes;
}
@Override
public String getValueString(byte[] bytes, SearchSettings settings) {
return getValueString(bytes, settings, false);
}
protected String getValueString(byte[] bytes, SearchSettings settings, boolean padNegative) {
int byteSize = settings.getDecimalByteSize();
boolean isBigEndian = settings.isBigEndian();
boolean isUnsigned = settings.isDecimalUnsigned();
StringBuilder buffer = new StringBuilder();
int numValues = bytes.length / byteSize;
for (int i = 0; i < numValues; i++) {
long value = getValue(bytes, i, settings);
String text = isUnsigned ? Long.toUnsignedString(value) : Long.toString(value);
buffer.append(text);
if (i != numValues - 1) {
buffer.append(", ");
}
}
int remainder = bytes.length - numValues * byteSize;
if (remainder > 0) {
byte[] remainderBytes = new byte[remainder];
System.arraycopy(bytes, numValues * byteSize, remainderBytes, 0, remainder);
byte[] padded = padToByteSize(remainderBytes, byteSize, isBigEndian, padNegative);
long value = getValue(padded, 0, settings);
String text = isUnsigned ? Long.toUnsignedString(value) : Long.toString(value);
if (!buffer.isEmpty()) {
buffer.append(", ");
}
buffer.append(text);
}
return buffer.toString();
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
SearchFormat oldFormat = oldSettings.getSearchFormat();
switch (oldFormat.getFormatType()) {
case BYTE:
return getTextFromBytes(text, oldSettings, newSettings);
case INTEGER:
return convertFromDifferentNumberFormat(text, oldSettings, newSettings);
case STRING_TYPE:
case FLOATING_POINT:
default:
return isValidText(text, newSettings) ? text : "";
}
}
private String convertFromDifferentNumberFormat(String text, SearchSettings oldSettings,
SearchSettings newSettings) {
int oldSize = oldSettings.getDecimalByteSize();
int newSize = newSettings.getDecimalByteSize();
boolean oldUnsigned = oldSettings.isDecimalUnsigned();
boolean newUnsigned = newSettings.isDecimalUnsigned();
if (oldSize == newSize && oldUnsigned == newUnsigned) {
return text;
}
// if the new format is smaller, first try re-parsing to avoid unnecessary 0's
if (oldSize > newSize) {
if (isValidText(text, newSettings)) {
return text;
}
}
return getTextFromBytes(text, oldSettings, newSettings);
}
private String getTextFromBytes(String text, SearchSettings oldSettings,
SearchSettings newSettings) {
byte[] bytes = getBytes(oldSettings.getSearchFormat(), text, oldSettings);
if (bytes == null) {
return "";
}
boolean padNegative = shouldPadNegative(text);
String valueString = getValueString(bytes, newSettings, padNegative);
return valueString.replaceAll(",", "");
}
private boolean shouldPadNegative(String text) {
if (text.isBlank()) {
return false;
}
int lastIndexOf = text.trim().lastIndexOf(" ");
if (lastIndexOf < 0) {
// only pad negative if there is only one word in the text and it begins with '-'
return text.charAt(0) == '-';
}
return false;
}
private byte[] getBytes(SearchFormat oldFormat, String text, SearchSettings settings) {
ByteMatcher byteMatcher = oldFormat.parse(text, settings);
if (byteMatcher instanceof MaskedByteSequenceByteMatcher matcher) {
return matcher.getBytes();
}
return null;
}
private byte[] padToByteSize(byte[] bytes, int byteSize, boolean isBigEndian,
boolean padNegative) {
if (bytes.length >= byteSize) {
return bytes;
}
byte[] newBytes = new byte[byteSize];
if (padNegative) {
Arrays.fill(newBytes, (byte) -1);
}
int startIndex = isBigEndian ? byteSize - bytes.length : 0;
System.arraycopy(bytes, 0, newBytes, startIndex, bytes.length);
return newBytes;
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.INTEGER;
}
}
@@ -0,0 +1,200 @@
/* ###
* 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.features.base.memsearch.format;
import java.util.StringTokenizer;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
import ghidra.util.HTMLUtilities;
/**
* {@link SearchFormat} for parsing and display bytes in a float or double format.
*/
class FloatSearchFormat extends SearchFormat {
private String longName;
private int byteSize;
FloatSearchFormat(String name, String longName, int size) {
super(name);
if (size != 8 && size != 4) {
throw new IllegalArgumentException("Only supports 4 or 8 byte floating point numbers");
}
this.longName = longName;
this.byteSize = size;
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
StringTokenizer tokenizer = new StringTokenizer(input);
int tokenCount = tokenizer.countTokens();
byte[] bytes = new byte[tokenCount * byteSize];
int bytesPosition = 0;
while (tokenizer.hasMoreTokens()) {
String tok = tokenizer.nextToken();
NumberParseResult result = parseNumber(tok, settings);
if (result.errorMessage() != null) {
return new InvalidByteMatcher(result.errorMessage(), result.validInput());
}
System.arraycopy(result.bytes(), 0, bytes, bytesPosition, byteSize);
bytesPosition += byteSize;
}
return new MaskedByteSequenceByteMatcher(input, bytes, settings);
}
private NumberParseResult parseNumber(String tok, SearchSettings settings) {
if (tok.equals("-") || tok.equals("-.")) {
return new NumberParseResult(null, "Incomplete negative floating point number", true);
}
if (tok.equals(".")) {
return new NumberParseResult(null, "Incomplete floating point number", true);
}
if (tok.endsWith("E") || tok.endsWith("e") || tok.endsWith("E-") || tok.endsWith("e-")) {
return new NumberParseResult(null, "Incomplete floating point number", true);
}
try {
long value = getValue(tok);
return new NumberParseResult(getBytes(value, settings), null, true);
}
catch (NumberFormatException e) {
return new NumberParseResult(null, "Floating point parse error: " + e.getMessage(),
false);
}
}
private long getValue(String tok) {
switch (byteSize) {
case 4:
float floatValue = Float.parseFloat(tok);
return Float.floatToIntBits(floatValue);
case 8:
default:
double dvalue = Double.parseDouble(tok);
return Double.doubleToLongBits(dvalue);
}
}
private byte[] getBytes(long value, SearchSettings settings) {
byte[] bytes = new byte[byteSize];
for (int i = 0; i < byteSize; i++) {
byte b = (byte) value;
bytes[i] = b;
value >>= 8;
}
if (settings.isBigEndian()) {
reverse(bytes);
}
return bytes;
}
@Override
public String getToolTip() {
return HTMLUtilities.toHTML(
"Interpret values as a sequence of\n" + longName + " numbers, separated by spaces");
}
@Override
public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) {
boolean isBigEndian = settings.isBigEndian();
// check each value one at a time, and return the first one different
for (int i = 0; i < bytes1.length / byteSize; i++) {
double value1 = getValue(bytes1, i, isBigEndian);
double value2 = getValue(bytes2, i, isBigEndian);
if (value1 != value2) {
return Double.compare(value1, value2);
}
}
return 0;
}
public Double getValue(byte[] bytes, int index, boolean isBigEndian) {
long bits = fromBytes(bytes, index, isBigEndian);
switch (byteSize) {
case 4:
float f = Float.intBitsToFloat((int) bits);
return (double) f;
case 8:
default:
return Double.longBitsToDouble(bits);
}
}
private long fromBytes(byte[] bytes, int index, boolean isBigEndian) {
byte[] bigEndianBytes = new byte[byteSize];
System.arraycopy(bytes, index * byteSize, bigEndianBytes, 0, byteSize);
if (!isBigEndian) {
reverse(bigEndianBytes);
}
long value = 0;
for (int i = 0; i < bigEndianBytes.length; i++) {
value = (value << 8) | (bigEndianBytes[i] & 0xff);
}
return value;
}
@Override
public String getValueString(byte[] bytes, SearchSettings settings) {
StringBuilder buffer = new StringBuilder();
int numValues = bytes.length / byteSize;
for (int i = 0; i < numValues; i++) {
double value = getValue(bytes, i, settings.isBigEndian());
buffer.append(Double.toString(value));
if (i != numValues - 1) {
buffer.append(", ");
}
}
return buffer.toString();
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
SearchFormat oldFormat = oldSettings.getSearchFormat();
switch (oldFormat.getFormatType()) {
case BYTE:
return getTextFromBytes(text, oldFormat, oldSettings);
case FLOATING_POINT:
case STRING_TYPE:
case INTEGER:
default:
return isValidText(text, newSettings) ? text : "";
}
}
private String getTextFromBytes(String text, SearchFormat oldFormat, SearchSettings settings) {
ByteMatcher byteMatcher = oldFormat.parse(text, settings);
if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) {
byte[] bytes = matcher.getBytes();
if (bytes.length >= byteSize) {
String valueString = getValueString(bytes, settings);
return valueString.replaceAll(",", "");
}
}
return isValidText(text, settings) ? text : "";
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.FLOATING_POINT;
}
}
@@ -0,0 +1,244 @@
/* ###
* 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.features.base.memsearch.format;
import java.util.*;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
import ghidra.util.HTMLUtilities;
/**
* {@link SearchFormat} for parsing and display bytes in a hex format. This format only
* accepts hex digits or wild card characters.
*/
class HexSearchFormat extends SearchFormat {
private static final String WILD_CARDS = ".?";
private static final String VALID_CHARS = "0123456789abcdefABCDEF" + WILD_CARDS;
private static final int MAX_GROUP_SIZE = 16;
HexSearchFormat() {
super("Hex");
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
List<String> byteGroups = getByteGroups(input);
if (hasInvalidChars(byteGroups)) {
return new InvalidByteMatcher("Invalid character");
}
if (checkGroupSize(byteGroups)) {
return new InvalidByteMatcher("Max group size exceeded. Enter <space> to add more.");
}
List<String> byteList = getByteList(byteGroups, settings);
byte[] bytes = getBytes(byteList);
byte[] masks = getMask(byteList);
return new MaskedByteSequenceByteMatcher(input, bytes, masks, settings);
}
@Override
public String getToolTip() {
return HTMLUtilities.toHTML("Interpret value as a sequence of\n" +
"hex numbers, separated by spaces.\n" + "Enter '.' or '?' for a wildcard match");
}
private byte[] getBytes(List<String> byteList) {
byte[] bytes = new byte[byteList.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = getByte(byteList.get(i));
}
return bytes;
}
private byte[] getMask(List<String> byteList) {
byte[] masks = new byte[byteList.size()];
for (int i = 0; i < masks.length; i++) {
masks[i] = getMask(byteList.get(i));
}
return masks;
}
/**
* Returns the search mask for the given hex byte string. Normal hex digits result
* in a "1111" mask and wildcard digits result in a "0000" mask.
*/
private byte getMask(String tok) {
char c1 = tok.charAt(0);
char c2 = tok.charAt(1);
int index1 = WILD_CARDS.indexOf(c1);
int index2 = WILD_CARDS.indexOf(c2);
if (index1 >= 0 && index2 >= 0) {
return (byte) 0x00;
}
if (index1 >= 0 && index2 < 0) {
return (byte) 0x0F;
}
if (index1 < 0 && index2 >= 0) {
return (byte) 0xF0;
}
return (byte) 0xFF;
}
/**
* Returns the byte value to be used for the given hex bytes. Handles wildcard characters by
* return treating them as 0s.
*/
private byte getByte(String tok) {
char c1 = tok.charAt(0);
char c2 = tok.charAt(1);
// note: the hexValueOf() method will turn wildcard chars into 0s
return (byte) (hexValueOf(c1) * 16 + hexValueOf(c2));
}
private List<String> getByteList(List<String> byteGroups, SearchSettings settings) {
List<String> byteList = new ArrayList<>();
for (String byteGroup : byteGroups) {
List<String> byteStrings = getByteStrings(byteGroup);
if (!settings.isBigEndian()) {
Collections.reverse(byteStrings);
}
byteList.addAll(byteStrings);
}
return byteList;
}
private List<String> getByteStrings(String token) {
if (isSingleWildCardChar(token)) {
// normally, a wildcard character represents a nibble. For convenience, if the there
// is a single wild card character surrounded by whitespace, treat it
// as if the entire byte is wild
token += token;
}
else if (token.length() % 2 != 0) {
// pad an odd number of nibbles with 0; assuming users leave off leading 0
token = "0" + token;
}
int n = token.length() / 2;
List<String> list = new ArrayList<String>(n);
for (int i = 0; i < n; i++) {
list.add(token.substring(i * 2, i * 2 + 2));
}
return list;
}
private boolean isSingleWildCardChar(String token) {
if (token.length() == 1) {
char c = token.charAt(0);
return WILD_CARDS.indexOf(c) >= 0;
}
return false;
}
private boolean hasInvalidChars(List<String> byteGroups) {
for (String byteGroup : byteGroups) {
if (hasInvalidChars(byteGroup)) {
return true;
}
}
return false;
}
private boolean checkGroupSize(List<String> byteGroups) {
for (String byteGroup : byteGroups) {
if (byteGroup.length() > MAX_GROUP_SIZE) {
return true;
}
}
return false;
}
private List<String> getByteGroups(String input) {
List<String> list = new ArrayList<String>();
StringTokenizer st = new StringTokenizer(input);
while (st.hasMoreTokens()) {
list.add(st.nextToken());
}
return list;
}
private boolean hasInvalidChars(String string) {
for (int i = 0; i < string.length(); i++) {
if (VALID_CHARS.indexOf(string.charAt(i)) < 0) {
return true;
}
}
return false;
}
/**
* Returns the value of the given hex digit character.
*/
private int hexValueOf(char c) {
if ((c >= '0') && (c <= '9')) {
return c - '0';
}
else if ((c >= 'a') && (c <= 'f')) {
return c - 'a' + 10;
}
else if ((c >= 'A') && (c <= 'F')) {
return c - 'A' + 10;
}
else {
return 0;
}
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
SearchFormat oldFormat = oldSettings.getSearchFormat();
if (oldFormat.getClass() == getClass()) {
return text;
}
if (oldFormat.getFormatType() != SearchFormatType.STRING_TYPE) {
ByteMatcher byteMatcher = oldFormat.parse(text, oldSettings);
if ((byteMatcher instanceof MaskedByteSequenceByteMatcher matcher)) {
byte[] bytes = matcher.getBytes();
byte[] mask = matcher.getMask();
return getMaskedInputString(bytes, mask);
}
}
return isValidText(text, newSettings) ? text : "";
}
private String getMaskedInputString(byte[] bytes, byte[] mask) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String s = String.format("%02x", bytes[i]);
builder.append((mask[i] & 0xf0) == 0 ? "." : s.charAt(0));
builder.append((mask[i] & 0x0f) == 0 ? "." : s.charAt(1));
builder.append(" ");
}
return builder.toString().trim();
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.BYTE;
}
}
@@ -0,0 +1,24 @@
/* ###
* 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.features.base.memsearch.format;
/**
* Used by the NumberSearchFormat and the FloatSearchFormat for intermediate parsing results.
* @param bytes The bytes that match the parsed number sequence
* @param errorMessage an optional parsing error message
* @param validInput boolean if the input was valid
*/
record NumberParseResult(byte[] bytes, String errorMessage, boolean validInput) {}
@@ -0,0 +1,66 @@
/* ###
* 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.features.base.memsearch.format;
import java.util.regex.PatternSyntaxException;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
/**
* {@link SearchFormat} for parsing input as a regular expression. This format can't generate
* bytes or parse results.
*/
class RegExSearchFormat extends SearchFormat {
RegExSearchFormat() {
super("Reg Ex");
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
try {
return new RegExByteMatcher(input, settings);
}
catch (PatternSyntaxException e) {
return new InvalidByteMatcher("RegEx Pattern Error: " + e.getDescription(), true);
}
}
@Override
public String getToolTip() {
return "Interpret value as a regular expression.";
}
@Override
public String getValueString(byte[] bytes, SearchSettings settings) {
return new String(bytes);
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
return isValidText(text, newSettings) ? text : "";
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.STRING_TYPE;
}
}
@@ -0,0 +1,159 @@
/* ###
* 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.features.base.memsearch.format;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.ByteMatcher;
/**
* SearchFormats are responsible for parsing user input data into a {@link ByteMatcher} that
* can be used for searching memory. It also can convert search matches back into string data and
* can convert string data from other formats into string data for this format.
*/
public abstract class SearchFormat {
//@formatter:off
public static SearchFormat HEX = new HexSearchFormat();
public static SearchFormat BINARY = new BinarySearchFormat();
public static SearchFormat DECIMAL = new DecimalSearchFormat();
public static SearchFormat STRING = new StringSearchFormat();
public static SearchFormat REG_EX = new RegExSearchFormat();
public static SearchFormat FLOAT = new FloatSearchFormat("Float", "Floating Point", 4);
public static SearchFormat DOUBLE = new FloatSearchFormat("Double", "Floating Point (8)", 8);
//@formatter:on
public static SearchFormat[] ALL =
{ HEX, BINARY, DECIMAL, STRING, REG_EX, FLOAT, DOUBLE };
// SearchFormats fall into one of 4 types
public enum SearchFormatType {
BYTE, INTEGER, FLOATING_POINT, STRING_TYPE
}
private final String name;
protected SearchFormat(String name) {
this.name = name;
}
/**
* Parse the given input and settings into a {@link ByteMatcher}
* @param input the user input string
* @param settings the current search/parse settings
* @return a ByteMatcher that can be used for searching bytes (or an error version of a matcher)
*/
public abstract ByteMatcher parse(String input, SearchSettings settings);
/**
* Returns a tool tip describing this search format
* @return a tool tip describing this search format
*/
public abstract String getToolTip();
/**
* Returns the name of the search format.
* @return the name of the search format
*/
public String getName() {
return name;
}
@Override
public String toString() {
return getName();
}
/**
* Reverse parses the bytes back into input value strings. Note that this is only used by
* numerical and string type formats. Byte oriented formats just return an empty string.
* @param bytes the to convert back into input value strings
* @param settings The search settings used to parse the input into bytes
* @return the string of the reversed parsed byte values
*/
public String getValueString(byte[] bytes, SearchSettings settings) {
return "";
}
/**
* Returns a new search input string, doing its best to convert an input string that
* was parsed by a previous {@link SearchFormat}. When it makes sense to do so, it will
* re-interpret the parsed bytes from the old format and reconstruct the input from those
* bytes. This allows the user to do conversions, for example, from numbers to hex or binary and
* vise-versa. If the byte conversion doesn't make sense based on the old and new formats, it
* will use the original input if that input can be parsed by the new input. Finally, if all
* else fails, the new input will be the empty string.
*
* @param text the old input that is parsable by the old format
* @param oldSettings the search settings used to parse the old text
* @param newSettings the search settings to used for the new text
* @return the "best" text to change the user search input to
*/
public abstract String convertText(String text, SearchSettings oldSettings,
SearchSettings newSettings);
/**
* Returns the {@link SearchFormatType} for this format. This is used to help with the
* {@link #convertText(String, SearchSettings, SearchSettings)} method.
* @return the type for this format
*/
public abstract SearchFormatType getFormatType();
/**
* Compares bytes from search results based on how this format interprets the bytes.
* By default, formats just compare the bytes one by one as if they were unsigned values.
* SearchFormats whose bytes represent numerical values will override this method and
* compare the bytes after interpreting them as numerical values.
*
* @param bytes1 the first array of bytes to compare
* @param bytes2 the second array of bytes to compare
* @param settings the search settings used to generate the bytes.
*
* @return a negative integer, zero, or a positive integer as the first byte array
* is less than, equal to, or greater than the second byte array
*
*/
public int compareValues(byte[] bytes1, byte[] bytes2, SearchSettings settings) {
return compareBytesUnsigned(bytes1, bytes2);
}
protected void reverse(byte[] bytes) {
for (int i = 0; i < bytes.length / 2; i++) {
int swapIndex = bytes.length - 1 - i;
byte tmp = bytes[i];
bytes[i] = bytes[swapIndex];
bytes[swapIndex] = tmp;
}
}
private int compareBytesUnsigned(byte[] oldBytes, byte[] newBytes) {
for (int i = 0; i < oldBytes.length; i++) {
int value1 = oldBytes[i] & 0xff;
int value2 = newBytes[i] & 0xff;
if (value1 != value2) {
return value1 - value2;
}
}
return 0;
}
protected boolean isValidText(String text, SearchSettings settings) {
ByteMatcher byteMatcher = parse(text, settings);
return byteMatcher.isValidSearch();
}
}
@@ -0,0 +1,145 @@
/* ###
* 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.features.base.memsearch.format;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import ghidra.features.base.memsearch.gui.SearchSettings;
import ghidra.features.base.memsearch.matcher.*;
import ghidra.util.StringUtilities;
/**
* {@link SearchFormat} for parsing and display bytes in a string format. This format uses
* several values from SearchSettings included character encoding, case sensitive, and escape
* sequences.
*/
class StringSearchFormat extends SearchFormat {
private final byte CASE_INSENSITIVE_MASK = (byte) 0xdf;
StringSearchFormat() {
super("String");
}
@Override
public ByteMatcher parse(String input, SearchSettings settings) {
input = input.trim();
if (input.isBlank()) {
return new InvalidByteMatcher("");
}
boolean isBigEndian = settings.isBigEndian();
int inputLength = input.length();
Charset charset = settings.getStringCharset();
if (charset == StandardCharsets.UTF_16) {
charset = isBigEndian ? StandardCharsets.UTF_16BE : StandardCharsets.UTF_16LE;
}
// Escape sequences in the "input" are 2 Characters long.
if (settings.useEscapeSequences() && inputLength >= 2) {
input = StringUtilities.convertEscapeSequences(input);
}
byte[] bytes = input.getBytes(charset);
byte[] maskArray = new byte[bytes.length];
Arrays.fill(maskArray, (byte) 0xff);
if (!settings.isCaseSensitive()) {
createCaseInsensitiveBytesAndMasks(charset, bytes, maskArray);
}
return new MaskedByteSequenceByteMatcher(input, bytes, maskArray, settings);
}
private void createCaseInsensitiveBytesAndMasks(Charset encodingCharSet, byte[] bytes,
byte[] masks) {
int i = 0;
while (i < bytes.length) {
if (encodingCharSet == StandardCharsets.US_ASCII &&
Character.isLetter(bytes[i])) {
masks[i] = CASE_INSENSITIVE_MASK;
bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK);
i++;
}
else if (encodingCharSet == StandardCharsets.UTF_8) {
int numBytes = bytesPerCharUTF8(bytes[i]);
if (numBytes == 1 && Character.isLetter(bytes[i])) {
masks[i] = CASE_INSENSITIVE_MASK;
bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK);
}
i += numBytes;
}
// Assumes UTF-16 will return 2 Bytes for each character.
// 4-byte UTF-16 will never satisfy the below checks because
// none of their bytes can ever be 0.
else if (encodingCharSet == StandardCharsets.UTF_16BE) {
if (bytes[i] == (byte) 0x0 && Character.isLetter(bytes[i + 1])) { // Checks if ascii character.
masks[i + 1] = CASE_INSENSITIVE_MASK;
bytes[i + 1] = (byte) (bytes[i + 1] & CASE_INSENSITIVE_MASK);
}
i += 2;
}
else if (encodingCharSet == StandardCharsets.UTF_16LE) {
if (bytes[i + 1] == (byte) 0x0 && Character.isLetter(bytes[i])) { // Checks if ascii character.
masks[i] = CASE_INSENSITIVE_MASK;
bytes[i] = (byte) (bytes[i] & CASE_INSENSITIVE_MASK);
}
i += 2;
}
else {
i++;
}
}
}
private int bytesPerCharUTF8(byte zByte) {
// This method is intended for UTF-8 encoding.
// The first byte in a sequence of UTF-8 bytes can tell
// us how many bytes make up a char.
int offset = 1;
// If the char is ascii, this loop will be skipped.
while ((zByte & 0x80) != 0x00) {
zByte <<= 1;
offset++;
}
return offset;
}
@Override
public String getToolTip() {
return "Interpret value as a sequence of characters.";
}
@Override
public String getValueString(byte[] bytes, SearchSettings settings) {
boolean isBigEndian = settings.isBigEndian();
Charset charset = settings.getStringCharset();
if (charset == StandardCharsets.UTF_16) {
charset = isBigEndian ? StandardCharsets.UTF_16BE : StandardCharsets.UTF_16LE;
}
return new String(bytes, charset);
}
@Override
public String convertText(String text, SearchSettings oldSettings, SearchSettings newSettings) {
return isValidText(text, newSettings) ? text : "";
}
@Override
public SearchFormatType getFormatType() {
return SearchFormatType.STRING_TYPE;
}
}
@@ -0,0 +1,75 @@
/* ###
* 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.features.base.memsearch.gui;
import java.util.Collection;
import java.util.List;
import ghidra.features.base.memsearch.combiner.Combiner;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.features.base.memsearch.searcher.MemorySearcher;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.datastruct.ListAccumulator;
import ghidra.util.task.TaskMonitor;
/**
* Table loader that performs a search and then combines the new results with existing results.
*/
public class CombinedMatchTableLoader implements MemoryMatchTableLoader {
private MemorySearcher memSearcher;
private List<MemoryMatch> previousResults;
private Combiner combiner;
private boolean completedSearch;
private MemoryMatch firstMatch;
public CombinedMatchTableLoader(MemorySearcher memSearcher,
List<MemoryMatch> previousResults, Combiner combiner) {
this.memSearcher = memSearcher;
this.previousResults = previousResults;
this.combiner = combiner;
}
@Override
public void loadResults(Accumulator<MemoryMatch> accumulator, TaskMonitor monitor) {
ListAccumulator<MemoryMatch> listAccumulator = new ListAccumulator<>();
completedSearch = memSearcher.findAll(listAccumulator, monitor);
List<MemoryMatch> followOnResults = listAccumulator.asList();
firstMatch = followOnResults.isEmpty() ? null : followOnResults.get(0);
Collection<MemoryMatch> results = combiner.combine(previousResults, followOnResults);
accumulator.addAll(results);
}
@Override
public boolean didTerminateEarly() {
return !completedSearch;
}
@Override
public void dispose() {
previousResults = null;
}
@Override
public MemoryMatch getFirstMatch() {
return firstMatch;
}
@Override
public boolean hasResults() {
return firstMatch != null;
}
}
@@ -0,0 +1,51 @@
/* ###
* 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.features.base.memsearch.gui;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.task.TaskMonitor;
/**
* Table loader for clearing the existing results
*/
public class EmptyMemoryMatchTableLoader implements MemoryMatchTableLoader {
@Override
public void loadResults(Accumulator<MemoryMatch> accumulator, TaskMonitor monitor) {
return;
}
@Override
public void dispose() {
// nothing to do
}
@Override
public boolean didTerminateEarly() {
return false;
}
@Override
public MemoryMatch getFirstMatch() {
return null;
}
@Override
public boolean hasResults() {
return false;
}
}
@@ -0,0 +1,94 @@
/* ###
* 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.features.base.memsearch.gui;
import java.util.List;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.features.base.memsearch.searcher.MemorySearcher;
import ghidra.program.model.address.Address;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.task.TaskMonitor;
/**
* Table loader for executing an incremental search forwards or backwards and adding that result
* to the table.
*/
public class FindOnceTableLoader implements MemoryMatchTableLoader {
private MemorySearcher searcher;
private Address address;
private List<MemoryMatch> previousResults;
private MemorySearchResultsPanel panel;
private MemoryMatch match;
private boolean forward;
public FindOnceTableLoader(MemorySearcher searcher, Address address,
List<MemoryMatch> previousResults, MemorySearchResultsPanel panel, boolean forward) {
this.searcher = searcher;
this.address = address;
this.previousResults = previousResults;
this.panel = panel;
this.forward = forward;
}
@Override
public void loadResults(Accumulator<MemoryMatch> accumulator, TaskMonitor monitor) {
accumulator.addAll(previousResults);
match = searcher.findOnce(address, forward, monitor);
if (match != null) {
MemoryMatch existing = findExisingMatch(match.getAddress());
if (existing != null) {
existing.updateBytes(match.getBytes());
}
else {
accumulator.add(match);
}
}
}
private MemoryMatch findExisingMatch(Address newMatchAddress) {
for (MemoryMatch memoryMatch : previousResults) {
if (newMatchAddress.equals(memoryMatch.getAddress())) {
return memoryMatch;
}
}
return null;
}
@Override
public boolean didTerminateEarly() {
return false;
}
@Override
public MemoryMatch getFirstMatch() {
return match;
}
@Override
public void dispose() {
previousResults = null;
}
@Override
public boolean hasResults() {
return match != null;
}
}
@@ -0,0 +1,242 @@
/* ###
* 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.features.base.memsearch.gui;
import java.awt.Color;
import java.util.*;
import org.apache.commons.lang3.ArrayUtils;
import docking.widgets.fieldpanel.support.Highlight;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import ghidra.app.nav.Navigatable;
import ghidra.app.util.ListingHighlightProvider;
import ghidra.app.util.SearchConstants;
import ghidra.app.util.viewer.field.*;
import ghidra.app.util.viewer.proxy.ProxyObj;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.listing.Program;
/**
* Listing highlight provider to highlight memory search results.
*/
public class MemoryMatchHighlighter implements ListingHighlightProvider {
private Navigatable navigatable;
private Program program;
private List<MemoryMatch> sortedResults;
private MemoryMatchTableModel model;
private MemorySearchOptions options;
private MemoryMatch selectedMatch;
public MemoryMatchHighlighter(Navigatable navigatable, MemoryMatchTableModel model,
MemorySearchOptions options) {
this.model = model;
this.options = options;
this.navigatable = navigatable;
this.program = navigatable.getProgram();
model.addThreadedTableModelListener(new ThreadedTableModelListener() {
@Override
public void loadingStarted() {
clearCache();
}
@Override
public void loadingFinished(boolean wasCancelled) {
// stub
}
@Override
public void loadPending() {
clearCache();
}
});
}
@Override
public Highlight[] createHighlights(String text, ListingField field, int cursorTextOffset) {
if (!options.isShowHighlights()) {
return NO_HIGHLIGHTS;
}
if (program != navigatable.getProgram()) {
return NO_HIGHLIGHTS;
}
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory().getClass();
if (fieldFactoryClass != BytesFieldFactory.class) {
return NO_HIGHLIGHTS;
}
ProxyObj<?> proxy = field.getProxy();
Object obj = proxy.getObject();
if (!(obj instanceof CodeUnit cu)) {
return NO_HIGHLIGHTS;
}
Address minAddr = cu.getMinAddress();
Address maxAddr = cu.getMaxAddress();
List<MemoryMatch> results = getMatchesInRange(minAddr, maxAddr);
if (results.isEmpty()) {
return NO_HIGHLIGHTS;
}
return getHighlights(text, minAddr, results);
}
private Highlight[] getHighlights(String text, Address minAddr, List<MemoryMatch> results) {
Highlight[] highlights = new Highlight[results.size()];
int selectedMatchIndex = -1;
for (int i = 0; i < highlights.length; i++) {
MemoryMatch match = results.get(i);
Color highlightColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR;
if (match == selectedMatch) {
selectedMatchIndex = i;
highlightColor = SearchConstants.SEARCH_HIGHLIGHT_CURRENT_ADDR_COLOR;
}
highlights[i] = createHighlight(match, minAddr, text, highlightColor);
}
// move the selected match to the end so that it gets painted last and doesn't get
// painted over by the non-active highlights
if (selectedMatchIndex >= 0) {
ArrayUtils.swap(highlights, selectedMatchIndex, highlights.length - 1);
}
return highlights;
}
private Highlight createHighlight(MemoryMatch match, Address start, String text, Color color) {
int highlightLength = match.getLength();
Address address = match.getAddress();
int startByteOffset = (int) address.subtract(start);
int endByteOffset = startByteOffset + highlightLength - 1;
startByteOffset = Math.max(startByteOffset, 0);
return getHighlight(text, startByteOffset, endByteOffset, color);
}
private Highlight getHighlight(String text, int start, int end, Color color) {
int charStart = getCharPosition(text, start);
int charEnd = getCharPosition(text, end) + 1;
return new Highlight(charStart, charEnd, color);
}
private int getCharPosition(String text, int byteOffset) {
int byteGroupSize = options.getByteGroupSize();
int byteDelimiterLength = options.getByteDelimiter().length();
int groupSize = byteGroupSize * 2 + byteDelimiterLength;
int groupIndex = byteOffset / byteGroupSize;
int groupOffset = byteOffset % byteGroupSize;
int pos = groupIndex * groupSize + 2 * groupOffset;
return Math.min(text.length() - 1, pos);
}
List<MemoryMatch> getMatches() {
if (sortedResults != null) {
return sortedResults;
}
if (model.isBusy()) {
return Collections.emptyList();
}
List<MemoryMatch> modelData = model.getModelData();
if (model.isSortedOnAddress()) {
return modelData;
}
sortedResults = new ArrayList<>(modelData);
Collections.sort(sortedResults);
return sortedResults;
}
private List<MemoryMatch> getMatchesInRange(Address start, Address end) {
List<MemoryMatch> matches = getMatches();
int startIndex = findFirstIndex(matches, start, end);
if (startIndex < 0) {
return Collections.emptyList();
}
int endIndex = findIndexAtOrGreater(matches, end);
if (endIndex < matches.size() && (matches.get(endIndex).getAddress().equals(end))) {
endIndex++; // end index is non-inclusive and we want to include direct hit
}
List<MemoryMatch> resultList = matches.subList(startIndex, endIndex);
return resultList;
}
private int findFirstIndex(List<MemoryMatch> matches, Address start, Address end) {
int startIndex = findIndexAtOrGreater(matches, start);
if (startIndex > 0) { // see if address before extends into this range.
MemoryMatch resultBefore = matches.get(startIndex - 1);
Address beforeAddr = resultBefore.getAddress();
int length = resultBefore.getLength();
if (start.hasSameAddressSpace(beforeAddr) && start.subtract(beforeAddr) < length) {
return startIndex - 1;
}
}
if (startIndex == matches.size()) {
return -1;
}
MemoryMatch result = matches.get(startIndex);
Address addr = result.getAddress();
if (end.compareTo(addr) >= 0) {
return startIndex;
}
return -1;
}
private int findIndexAtOrGreater(List<MemoryMatch> matches, Address address) {
MemoryMatch key = new MemoryMatch(address);
int index = Collections.binarySearch(matches, key);
if (index < 0) {
index = -index - 1;
}
return index;
}
private void clearCache() {
if (sortedResults != null) {
sortedResults.clear();
sortedResults = null;
}
}
void dispose() {
navigatable.removeHighlightProvider(this, program);
clearCache();
}
void setSelectedMatch(MemoryMatch selectedMatch) {
this.selectedMatch = selectedMatch;
}
}
@@ -0,0 +1,60 @@
/* ###
* 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.features.base.memsearch.gui;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.task.TaskMonitor;
/**
* Interface for loading the memory search results table. Various implementations handle the
* different cases such as a search all, or a search next, or combining results with a previous
* search, etc.
*/
public interface MemoryMatchTableLoader {
/**
* Called by the table model to initiate searching and loading using the threaded table models
* threading infrastructure.
* @param accumulator the accumulator to store results that will appear in the results table
* @param monitor the task monitor
*/
public void loadResults(Accumulator<MemoryMatch> accumulator, TaskMonitor monitor);
/**
* Returns true if the search/loading did not fully complete. (Search limit reached, cancelled
* by user, etc.)
* @return true if the search/loading did not fully complete
*/
public boolean didTerminateEarly();
/**
* Cleans up resources
*/
public void dispose();
/**
* Returns the first match found. Typically used to navigate the associated navigatable.
* @return the first match found
*/
public MemoryMatch getFirstMatch();
/**
* Returns true if at least one match was found.
* @return true if at least one match was found
*/
public boolean hasResults();
}
@@ -0,0 +1,287 @@
/* ###
* 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.features.base.memsearch.gui;
import java.awt.*;
import docking.widgets.table.*;
import generic.theme.GThemeDefaults.Colors.Tables;
import ghidra.docking.settings.Settings;
import ghidra.features.base.memsearch.format.SearchFormat;
import ghidra.features.base.memsearch.matcher.ByteMatcher;
import ghidra.features.base.memsearch.searcher.MemoryMatch;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program;
import ghidra.program.util.*;
import ghidra.util.HTMLUtilities;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.AddressBasedTableModel;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.table.field.*;
import ghidra.util.task.TaskMonitor;
/**
* Table model for memory search results.
*/
public class MemoryMatchTableModel extends AddressBasedTableModel<MemoryMatch> {
private Color CHANGED_COLOR = Tables.ERROR_UNSELECTED;
private Color CHANGED_SELECTED_COLOR = Tables.ERROR_SELECTED;
private MemoryMatchTableLoader loader;
MemoryMatchTableModel(ServiceProvider serviceProvider, Program program) {
super("Memory Search", serviceProvider, program, null, true);
}
@Override
protected TableColumnDescriptor<MemoryMatch> createTableColumnDescriptor() {
TableColumnDescriptor<MemoryMatch> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new AddressTableColumn()), 1, true);
descriptor.addVisibleColumn(new MatchBytesColumn());
descriptor.addVisibleColumn(new MatchValueColumn());
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new LabelTableColumn()));
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new CodeUnitTableColumn()));
return descriptor;
}
@Override
protected void doLoad(Accumulator<MemoryMatch> accumulator, TaskMonitor monitor)
throws CancelledException {
if (loader == null) {
return;
}
loader.loadResults(accumulator, monitor);
loader = null;
}
void setLoader(MemoryMatchTableLoader loader) {
this.loader = loader;
reload();
}
public boolean isSortedOnAddress() {
TableSortState sortState = getTableSortState();
if (sortState.isUnsorted()) {
return false;
}
ColumnSortState primaryState = sortState.getAllSortStates().get(0);
DynamicTableColumn<MemoryMatch, ?, ?> column =
getColumn(primaryState.getColumnModelIndex());
String name = column.getColumnName();
if (AddressTableColumn.NAME.equals(name)) {
return true;
}
return false;
}
@Override
public ProgramLocation getProgramLocation(int modelRow, int modelColumn) {
Program p = getProgram();
if (p == null) {
return null; // we've been disposed
}
DynamicTableColumn<MemoryMatch, ?, ?> column = getColumn(modelColumn);
Class<?> columnClass = column.getClass();
if (column instanceof MappedTableColumn mappedColumn) {
columnClass = mappedColumn.getMappedColumnClass();
}
if (columnClass == AddressTableColumn.class || columnClass == MatchBytesColumn.class ||
columnClass == MatchValueColumn.class) {
return new BytesFieldLocation(p, getAddress(modelRow));
}
return super.getProgramLocation(modelRow, modelColumn);
}
@Override
public Address getAddress(int row) {
MemoryMatch result = getRowObject(row);
return result.getAddress();
}
@Override
public ProgramSelection getProgramSelection(int[] rows) {
AddressSet addressSet = new AddressSet();
for (int row : rows) {
MemoryMatch result = getRowObject(row);
int addOn = result.getLength() - 1;
Address minAddr = getAddress(row);
Address maxAddr = minAddr;
try {
maxAddr = minAddr.addNoWrap(addOn);
addressSet.addRange(minAddr, maxAddr);
}
catch (AddressOverflowException e) {
// I guess we don't care--not sure why this is undocumented :(
}
}
return new ProgramSelection(addressSet);
}
public class MatchBytesColumn
extends DynamicTableColumnExtensionPoint<MemoryMatch, String, Program> {
private ByteArrayRenderer renderer = new ByteArrayRenderer();
@Override
public String getColumnName() {
return "Match Bytes";
}
@Override
public String getValue(MemoryMatch match, Settings settings, Program pgm,
ServiceProvider service) throws IllegalArgumentException {
return getByteString(match.getBytes());
}
private String getByteString(byte[] bytes) {
StringBuilder b = new StringBuilder();
int max = bytes.length - 1;
for (int i = 0;; i++) {
b.append(String.format("%02x", bytes[i]));
if (i == max) {
break;
}
b.append(" ");
}
return b.toString();
}
@Override
public int getColumnPreferredWidth() {
return 200;
}
@Override
public GColumnRenderer<String> getColumnRenderer() {
return renderer;
}
}
public class MatchValueColumn
extends DynamicTableColumnExtensionPoint<MemoryMatch, String, Program> {
private ValueRenderer renderer = new ValueRenderer();
@Override
public String getColumnName() {
return "Match Value";
}
@Override
public String getValue(MemoryMatch match, Settings settings, Program pgm,
ServiceProvider service) throws IllegalArgumentException {
ByteMatcher byteMatcher = match.getByteMatcher();
SearchSettings searchSettings = byteMatcher.getSettings();
SearchFormat format = searchSettings.getSearchFormat();
return format.getValueString(match.getBytes(), searchSettings);
}
@Override
public int getColumnPreferredWidth() {
return 200;
}
@Override
public GColumnRenderer<String> getColumnRenderer() {
return renderer;
}
}
private class ByteArrayRenderer extends AbstractGColumnRenderer<String> {
public ByteArrayRenderer() {
setHTMLRenderingEnabled(true);
}
@Override
protected Font getDefaultFont() {
return fixedWidthFont;
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data);
MemoryMatch match = (MemoryMatch) data.getRowObject();
String text = data.getValue().toString();
if (match.isChanged()) {
text = getHtmlColoredString(match, data.isSelected());
}
setText(text);
return this;
}
private String getHtmlColoredString(MemoryMatch match, boolean isSelected) {
Color color = isSelected ? Tables.ERROR_SELECTED : Tables.ERROR_UNSELECTED;
StringBuilder b = new StringBuilder();
b.append("<HTML>");
byte[] bytes = match.getBytes();
byte[] previousBytes = match.getPreviousBytes();
int max = bytes.length - 1;
for (int i = 0;; i++) {
String byteString = String.format("%02x", bytes[i]);
if (bytes[i] != previousBytes[i]) {
byteString = HTMLUtilities.colorString(color, byteString);
}
b.append(byteString);
if (i == max)
break;
b.append(" ");
}
return b.toString();
}
@Override
public String getFilterString(String t, Settings settings) {
// This returns the formatted string without the formatted markup
return t;
}
}
private class ValueRenderer extends AbstractGColumnRenderer<String> {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
super.getTableCellRendererComponent(data);
setText((String) data.getValue());
MemoryMatch match = (MemoryMatch) data.getRowObject();
if (match.isChanged()) {
setForeground(data.isSelected() ? CHANGED_SELECTED_COLOR : CHANGED_COLOR);
}
return this;
}
@Override
public String getFilterString(String t, Settings settings) {
return t;
}
}
}

Some files were not shown because too many files have changed in this diff Show More