GP-4559 Creating new Memory Search Feature that include dynamic change
detection
@@ -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));
|
||||
|
||||
@@ -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: </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>
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 12 KiB |
|
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 < 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||