From c91e9aac3f0abd0a6a3ed5d96dbf27988f5198eb Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Thu, 30 Nov 2023 18:14:29 -0500 Subject: [PATCH] GP-2628 Add 'Search|For Encoded Strings' --- .../SampleStringTranslationPlugin.java | 5 +- Ghidra/Features/Base/certification.manifest | 4 +- .../Base/data/stringngrams/StringModel.sng | 2 + .../help/topics/Search/Search_for_Strings.htm | 116 ++ .../EncodedStringsDialog_advancedoptions.png | Bin 0 -> 28990 bytes .../images/EncodedStringsDialog_initial.png | Bin 0 -> 18577 bytes .../ViewStringsPlugin/ViewStringsPlugin.htm | 6 +- .../FindPossibleReferencesPlugin.java | 2 +- .../AutoTableDisassemblerPlugin.java | 3 +- .../InstructionSearchPlugin.java | 2 +- .../core/scalartable/ScalarSearchPlugin.java | 3 +- .../plugin/core/string/StringTablePlugin.java | 3 +- .../ManualStringTranslationService.java | 5 +- .../string/translate/TranslateAction.java | 3 +- .../translate/TranslateStringsPlugin.java | 16 +- .../core/strings/CharacterScriptUtils.java | 85 ++ .../core/strings/EncodedStringsDialog.java | 1159 +++++++++++++++++ .../strings/EncodedStringsFilterStats.java | 67 + .../core/strings/EncodedStringsOptions.java | 67 + .../core/strings/EncodedStringsPlugin.java | 123 ++ .../core/strings/EncodedStringsRow.java | 82 ++ .../strings/EncodedStringsTableModel.java | 448 +++++++ .../EncodedStringsThreadedTablePanel.java | 64 + .../app/plugin/core/strings/StringInfo.java | 76 ++ .../core/strings/StringInfoFeature.java | 21 + .../core/strings/StringTrigramIterator.java | 53 + .../app/plugin/core/strings/Trigram.java | 174 +++ .../core/strings/TrigramStringValidator.java | 250 ++++ .../core/strings/UndefinedStringIterator.java | 260 ++++ .../core/strings/ViewStringsPlugin.java | 77 +- .../core/strings/ViewStringsTableModel.java | 45 +- .../services/StringTranslationService.java | 25 +- .../app/services/StringValidatorQuery.java | 32 + .../app/services/StringValidatorService.java | 73 ++ .../app/services/StringValidityScore.java | 40 + .../strings/TrigramStringValidatorTest.java | 65 + .../widgets/spinner/IntegerSpinner.java | 17 +- .../table/GDynamicColumnTableModel.java | 2 +- .../model/data/StringDataInstance.java | 16 +- .../EncodedStringsDialogScreenShots.java | 114 ++ .../strings/EncodedStringsDialogTest.java | 183 +++ 41 files changed, 3716 insertions(+), 72 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Search/images/EncodedStringsDialog_advancedoptions.png create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Search/images/EncodedStringsDialog_initial.png create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/CharacterScriptUtils.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsFilterStats.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsOptions.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsRow.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsTableModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsThreadedTablePanel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfo.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfoFeature.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringTrigramIterator.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/Trigram.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/TrigramStringValidator.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/UndefinedStringIterator.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorQuery.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorService.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidityScore.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/strings/TrigramStringValidatorTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/EncodedStringsDialogScreenShots.java create mode 100644 Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/strings/EncodedStringsDialogTest.java diff --git a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/SampleStringTranslationPlugin.java b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/SampleStringTranslationPlugin.java index 3bd9a6707c..a1a1b52468 100644 --- a/Ghidra/Extensions/sample/src/main/java/ghidra/examples/SampleStringTranslationPlugin.java +++ b/Ghidra/Extensions/sample/src/main/java/ghidra/examples/SampleStringTranslationPlugin.java @@ -63,12 +63,13 @@ public class SampleStringTranslationPlugin extends Plugin implements StringTrans } @Override - public void translate(Program program, List dataLocations) { + public void translate(Program program, List stringLocations, + TranslateOptions options) { TaskLauncher.launchModal("Yeehaw-ify strings", monitor -> { int id = program.startTransaction("Yeehaw-ify strings"); try { - for (ProgramLocation progLoc : dataLocations) { + for (ProgramLocation progLoc : stringLocations) { Data data = DataUtilities.getDataAtLocation(progLoc); StringDataInstance str = StringDataInstance.getStringDataInstance(data); String s = str.getStringValue(); diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 9b63f669c3..6201fd2744 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -41,7 +41,7 @@ data/parserprofiles/linux_64.prf||GHIDRA||||END| data/parserprofiles/objc_mac_carbon.prf||GHIDRA||||END| data/parserprofiles/vs12Local.prf||GHIDRA||||END| data/pcodetest/EmuTesting.gdt||GHIDRA||||END| -data/stringngrams/StringModel.sng||GHIDRA||reviewed||END| +data/stringngrams/StringModel.sng||GHIDRA||||END| data/symbols/README.txt||GHIDRA||||END| data/symbols/win16/commctrl.exports||GHIDRA||||END| data/symbols/win16/commdlg.exports||GHIDRA||||END| @@ -510,6 +510,8 @@ src/main/help/help/topics/Search/Search_for_DirectReferences.htm||GHIDRA||||END| src/main/help/help/topics/Search/Search_for_Strings.htm||GHIDRA||||END| 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/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| diff --git a/Ghidra/Features/Base/data/stringngrams/StringModel.sng b/Ghidra/Features/Base/data/stringngrams/StringModel.sng index a7a9f2386d..2e0f1ec946 100644 --- a/Ghidra/Features/Base/data/stringngrams/StringModel.sng +++ b/Ghidra/Features/Base/data/stringngrams/StringModel.sng @@ -10,6 +10,8 @@ # [$] denotes end of string # [SP] denotes space # [HT] denotes horizontal tab +# Thresholds: -2.71, -3.26, -3.52, -3.84, -4.23, -4.49, -4.55, -4.74, -4.88, -5.03, -5.06, -5.2, -5.24, -5.29, -5.29, -5.42, -5.51, -5.52, -5.53, -5.6, -5.6, -5.62, -5.7, -5.7, -5.78, -5.79, -5.81, -5.81, -5.84, -5.85, -5.86, -5.88, -5.92, -5.92, -5.93, -5.95, -5.99, -6.0, -6.0, -6.0, -6.02, -6.02, -6.02, -6.05, -6.06, -6.07, -6.08, -6.1, -6.12, -6.12, -6.13, -6.13, -6.13, -6.13, -6.13, -6.13, -6.13, -6.15, -6.15, -6.16, -6.16, -6.16, -6.17, -6.19, -6.19, -6.21, -6.21, -6.21, -6.21, -6.21, -6.21, -6.25, -6.25, -6.25, -6.25, -6.25, -6.25, -6.25, -6.26, -6.26, -6.26, -6.26, -6.26, -6.26, -6.26, -6.26, -6.26, -6.29, -6.29, -6.3 +# Symbol Size: 128 [HT] [HT] [HT] 17 [HT] [HT] [SP] 8 diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_for_Strings.htm b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_for_Strings.htm index 35c8fc4a8e..06e7c200a2 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_for_Strings.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Search/Search_for_Strings.htm @@ -228,6 +228,122 @@

Provided By: StringTablePlugin

+ +

Search For Encoded Strings

+ +
+

The Encoded Strings Dialog is an alternate way to find and + create string instances in undefined data locations. It allows setting the character + set (charset) of the string to be created, as well as the ability to filter out byte + sequences from the selected locations that are not valid strings.

+

The Encoded Strings Dialog will initially allow the user to select the character set of the + string to create, and displays a preview of the strings found in the current selection:

+
+
+
+ + + + + + +
+
+
+ +

Advanced options

+ +
+

Click the Advanced... and the A-Z,我的... (Filter by character scripts) + buttons to show additional options that will allow filtering the selected byte range for + strings containing specific scripts (alphabets) and also excluding strings that have + properties that are unwanted.

+
+
+
+ + + + + + +
+
+
+ +

Character Script filtering

+ +
+

The Script drop-down list and the various Allow Additional toggle buttons + control how strings are filtered based on the script (Latin, Cyrillic, Arabic, etc) of each + of the characters found in the string.

+

The script chosen in the drop-down list will limit the included strings to strings that + include at least one character of the desired script.

+

If no Allow Additional toggle buttons are pressed, included strings will be limited + to strings that are solely comprised of characters from the chosen script (alphabet). This + would exclude strings that contain characters such as the space character or numeric characters + (labeled as the Common script), which typically is not desired. Select the 0-9,!? + toggle to allow those characters.

+

The A-Z (Latin) toggle will allow Latin characters to be present in included + strings. This is redundant if the Script drop-down list is already set to Latin, + but becomes useful when another script is chosen, to allow including strings that are a + mixture of the selected script and Latin, which commonly occurs when strings have symbol + names, scientific units, etc.

+

The Any toggle will allow any additional script to be present in included strings.

+

More advanced filtering logic can be had by creating a column + filter using Create Column Filter button in the lower right corner of the preview area + and filtering on the Unicode Script column.

+
+ +

Exclude codec errors, non-standard control chars

+ +
+

The Exclude codec errors check box excludes strings that contain the Unicode + REPLACEMENT character, which is placed into decoded strings when the charset codec logic + encounters a byte or byte sequence that is invalid. For example, the US-ASCII + charset will translate bytes greater than 0x7f into REPLACEMENT characters.

+

The Exclude non-std ctrl chars check box excludes strings that contain + characters that correspond to control characters in the range 1..31, but ignoring + common control characters such as tab, CR, LF.

+
+ + +

Exclude invalid strings

+ +
+

The Exclude invalid strings option tests each candidate string against a pre-built + trigram frequency model and rejects strings that score lower than a cut-off value.

+

The built-in string model file was trained with mostly english strings, and will + probably mark valid words from other languages as invalid.

+
+ +

Misc options

+ +
+

Minimum Length - excludes strings shorter than this (measured in characters, not + bytes)

+

Align start of string - ensures strings start at a location that is evenly divisible + by the alignment requirements of the character size of the charset.

+

Truncate at ref - ends strings early when there is an inbound reference to a + character inside the string.

+
+ +

Tip

+ +
+

When an option is responsible for excluding / filtering-out a string, that option will + have a red superscripted number next to it that contains the total count of strings + excluded by that option.

+
+ +

Related

+ +
+

See the Defined Strings window to see + already created strings.

+
+ +

Provided By: EncodedStringsPlugin

Related Topics:

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Search/images/EncodedStringsDialog_advancedoptions.png b/Ghidra/Features/Base/src/main/help/help/topics/Search/images/EncodedStringsDialog_advancedoptions.png new file mode 100644 index 0000000000000000000000000000000000000000..4dd045889abdd7154e1d6f91fede70608fc3b5a5 GIT binary patch literal 28990 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 JtaD0e0sv_x3IqTE literal 0 HcmV?d00001 diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ViewStringsPlugin/ViewStringsPlugin.htm b/Ghidra/Features/Base/src/main/help/help/topics/ViewStringsPlugin/ViewStringsPlugin.htm index cf10906e2a..2dc4812a89 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/ViewStringsPlugin/ViewStringsPlugin.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/ViewStringsPlugin/ViewStringsPlugin.htm @@ -22,7 +22,8 @@

 

This plugin is not intended to be used to locate undefined strings. Please see - Search for Strings for this + Search for Strings or + Search for Encoded Strings for this feature.

Defined Strings Table Columns

@@ -43,8 +44,9 @@
  • Has Encoding Error - boolean flag that indicates the string had byte(s) that could not be converted by the character set. This is usually caused by having the wrong character set or if the string isn't really a string.
  • Charset - name of the character set that this string is encoded in.
  • +
  • Unicode Script - a list of the the scripts (alphabets) used in the string.
  • -

    The Is Ascii, Has Encoding Error, and Charset columns are not visible by default. To display +

    The Is Ascii, Has Encoding Error, Unicode Script, and Charset columns are not visible by default. To display them in the table, right click on the column header row and select Add/Remove Columns....

    diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/FindPossibleReferencesPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/FindPossibleReferencesPlugin.java index 8730fe8b81..a8abf409f5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/FindPossibleReferencesPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/FindPossibleReferencesPlugin.java @@ -100,7 +100,7 @@ public class FindPossibleReferencesPlugin extends Plugin { private void createActions() { action = new ActionBuilder(SEARCH_DIRECT_REFS_ACTION_NAME, getName()) .menuPath(ToolConstants.MENU_SEARCH, "For Direct References") - .menuGroup("search for") + .menuGroup("search for", "DirectReferences") .helpLocation(new HelpLocation(HelpTopics.SEARCH, SEARCH_DIRECT_REFS_ACTION_NAME)) .description(getPluginDescription().getDescription()) .withContext(ListingActionContext.class, true) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/disassembler/AutoTableDisassemblerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/disassembler/AutoTableDisassemblerPlugin.java index a33002a7a9..80a720cc64 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/disassembler/AutoTableDisassemblerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/disassembler/AutoTableDisassemblerPlugin.java @@ -151,7 +151,8 @@ public class AutoTableDisassemblerPlugin extends ProgramPlugin implements Domain findTableAction.setHelpLocation( new HelpLocation(HelpTopics.SEARCH, findTableAction.getName())); findTableAction.setMenuBarData(new MenuData( - new String[] { ToolConstants.MENU_SEARCH, "For Address Tables" }, null, "search for")); + new String[] { ToolConstants.MENU_SEARCH, "For Address Tables" }, null, "search for", + -1, "AddressTables")); findTableAction.setDescription(getPluginDescription().getDescription()); findTableAction.addToWindowWhen(NavigatableActionContext.class); tool.addAction(findTableAction); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/instructionsearch/InstructionSearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/instructionsearch/InstructionSearchPlugin.java index 67fea0d700..2342ae5381 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/instructionsearch/InstructionSearchPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/instructionsearch/InstructionSearchPlugin.java @@ -259,7 +259,7 @@ public class InstructionSearchPlugin extends ProgramPlugin { searchAction.setHelpLocation(new HelpLocation("Search", "Instruction_Pattern_Search")); searchAction.setMenuBarData( new MenuData(new String[] { ToolConstants.MENU_SEARCH, "For Instruction Patterns" }, - null, "search for")); + null, "search for", -1, "InstructionPatterns")); searchAction.setDescription("Construct searches using selected instructions"); tool.addAction(searchAction); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java index 1b43313c57..ee1f2371f2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java @@ -148,7 +148,8 @@ public class ScalarSearchPlugin extends ProgramPlugin implements DomainObjectLis searchAction.setHelpLocation(new HelpLocation(this.getName(), "Scalar_Search")); searchAction.setMenuBarData(new MenuData( - new String[] { ToolConstants.MENU_SEARCH, "For Scalars..." }, null, "search for")); + new String[] { ToolConstants.MENU_SEARCH, "For Scalars..." }, null, "search for", -1, + "Scalars")); searchAction.setDescription("Search program for scalars"); searchAction.addToWindowWhen(NavigatableActionContext.class); tool.addAction(searchAction); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTablePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTablePlugin.java index ba7c75a7ee..d348d272e7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTablePlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/StringTablePlugin.java @@ -70,7 +70,8 @@ public class StringTablePlugin extends ProgramPlugin { }; stringSearchAction.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, SEARCH_ACTION_NAME)); stringSearchAction.setMenuBarData(new MenuData( - new String[] { ToolConstants.MENU_SEARCH, "For &Strings..." }, null, "search for")); + new String[] { ToolConstants.MENU_SEARCH, "For &Strings..." }, null, "search for", -1, + "Strings1")); stringSearchAction.setDescription(getPluginDescription().getDescription()); stringSearchAction.addToWindowWhen(NavigatableActionContext.class); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/ManualStringTranslationService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/ManualStringTranslationService.java index 8eb76c286d..f08aa2978d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/ManualStringTranslationService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/ManualStringTranslationService.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.string.translate; -import static ghidra.program.model.data.TranslationSettingsDefinition.*; +import static ghidra.program.model.data.TranslationSettingsDefinition.TRANSLATION; import java.util.List; @@ -50,7 +50,8 @@ public class ManualStringTranslationService implements StringTranslationService } @Override - public void translate(Program program, List stringLocations) { + public void translate(Program program, List stringLocations, + TranslateOptions options) { TaskLauncher.launchModal("Manually translate strings", monitor -> { int id = program.startTransaction("Translate strings"); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateAction.java index 05ed7b4dbe..23b2af2670 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateAction.java @@ -19,6 +19,7 @@ import java.util.List; import docking.action.MenuData; import ghidra.app.services.StringTranslationService; +import ghidra.app.services.StringTranslationService.TranslateOptions; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; import ghidra.util.HelpLocation; @@ -52,6 +53,6 @@ public class TranslateAction extends AbstractTranslateAction { @Override public void actionPerformed(Program program, List dataLocations) { - service.translate(program, dataLocations); + service.translate(program, dataLocations, TranslateOptions.NONE); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateStringsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateStringsPlugin.java index a23a556a15..e8dfc3c71f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateStringsPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/TranslateStringsPlugin.java @@ -15,7 +15,8 @@ */ package ghidra.app.plugin.core.string.translate; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import docking.action.DockingAction; import ghidra.app.CorePluginPackage; @@ -41,6 +42,7 @@ import ghidra.program.model.listing.Data; //@formatter:on public class TranslateStringsPlugin extends Plugin { + private List translationActions = new ArrayList<>(); private List translationServices = new ArrayList<>(); @@ -51,7 +53,7 @@ public class TranslateStringsPlugin extends Plugin { @Override protected void init() { - createTranslateActions(); + createTranslateActions(StringTranslationService.getCurrentStringTranslationServices(tool)); createTranslateMetaActions(); } @@ -67,25 +69,23 @@ public class TranslateStringsPlugin extends Plugin { private void createTranslateActionsIfNeeded() { List newServices = - new ArrayList<>(Arrays.asList(tool.getServices(StringTranslationService.class))); + StringTranslationService.getCurrentStringTranslationServices(tool); boolean isSame = newServices.containsAll(translationServices) && translationServices.containsAll(newServices); if (!isSame) { - createTranslateActions(); + createTranslateActions(newServices); } } - private void createTranslateActions() { + private void createTranslateActions(List newServices) { for (DockingAction prevAction : translationActions) { tool.removeAction(prevAction); } translationActions.clear(); translationServices.clear(); - translationServices.addAll(Arrays.asList(tool.getServices(StringTranslationService.class))); - Collections.sort(translationServices, - (s1, s2) -> s1.getTranslationServiceName().compareTo(s2.getTranslationServiceName())); + translationServices.addAll(newServices); for (StringTranslationService service : translationServices) { DockingAction action = new TranslateAction(getName(), service); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/CharacterScriptUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/CharacterScriptUtils.java new file mode 100644 index 0000000000..e4ce602b85 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/CharacterScriptUtils.java @@ -0,0 +1,85 @@ +/* ### + * 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.strings; + +import static java.lang.Character.UnicodeScript.*; + +import java.awt.Font; +import java.lang.Character.UnicodeScript; +import java.util.*; + +public class CharacterScriptUtils { + /** + * Scripts that are not helpful to use when filtering strings + */ + public static final List IGNORED_SCRIPTS = List.of(INHERITED, UNKNOWN); + + /** + * The {@link UnicodeScript} value that represents the "ANY" choice. This is a bit of a hack + * and re-uses the INHERITED enum value for this purpose. + */ + public static final UnicodeScript ANY_SCRIPT_ALIAS = UnicodeScript.INHERITED; + + /** + * Premade examples of characters from each specified script, using info from + * https://omniglot.com/language/phrases/hovercraft.htm and + * google translate, similar to lorem ipsum placeholding text. + *

    + * Encoded using escape sequences to avoid any mangling by ASCII processing. + *

    + * Scripts not in this map will have an example created from the first couple of characters + * from their unicode block that are visible to the user with their current font. + */ + static Map PREMADE_EXAMPLES = Map.of( + COMMON, "0-9,!?", + ARABIC, + "\u062d\u064e\u0648\u0651\u0627\u0645\u062a\u064a \u0645\u064f\u0645\u0652\u062a\u0650\u0644\u0626\u0629 \u0628\u0650\u0623\u064e\u0646\u0652\u0642\u064e\u0644\u064e\u064a\u0652\u0633\u0648\u0646", + CYRILLIC, + "\u041c\u043e\u0451 \u0441\u0443\u0434\u043d\u043e \u043d\u0430 \u0432\u043e\u0437\u0434\u0443", + HAN, "\u6211\u7684\u6c23\u588a\u8239\u88dd\u6eff\u4e86\u9c3b\u9b5a", + HANGUL, + "\uc81c \ud638\ubc84\ud06c\ub798\ud504\ud2b8\uac00 \uc7a5\uc5b4\ub85c \uac00\ub4dd\ud574\uc694", + KATAKANA, + "\u79c1\u306e\u30db\u30d0\u30fc\u30af\u30e9\u30d5\u30c8\u306f\u9c3b\u3067\u3044\u3063\u3071\u3044\u3067\u3059" // mix of han, hiragana, katakana + ); + + /** + * Builds a map of example character sequences for every current UnicodeScript, where the + * specified font can display the characters of that script. + * + * @param f {@link Font} + * @param maxExampleLen length of the character sequence to generate + * @return map of unicodescript-to-string + */ + public static Map getDisplayableScriptExamples(Font f, + int maxExampleLen) { + Map result = new HashMap<>(); + for (int i = 0; i < Character.MAX_CODE_POINT; i++) { + if (!Character.isISOControl(i)) { + UnicodeScript us = UnicodeScript.of(i); + String s = result.getOrDefault(us, ""); + if (s.length() < maxExampleLen && f.canDisplay(i)) { + // Note: waiting until after f.canDisplay ensures we don't add PREMADEs if not displayable + String premade = PREMADE_EXAMPLES.get(us); + s = premade == null ? s + Character.toString(i) : premade; + result.put(us, s); + } + } + } + return result; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java new file mode 100644 index 0000000000..560be1f79c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java @@ -0,0 +1,1159 @@ +/* ### + * 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.strings; + +import java.awt.*; +import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.lang.Character.UnicodeScript; +import java.nio.charset.Charset; +import java.util.*; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.widgets.checkbox.GCheckBox; +import docking.widgets.combobox.GhidraComboBox; +import docking.widgets.label.*; +import docking.widgets.list.GListCellRenderer; +import docking.widgets.spinner.IntegerSpinner; +import docking.widgets.table.threaded.ThreadedTableModelListener; +import generic.jar.ResourceFile; +import generic.theme.GThemeDefaults; +import ghidra.app.services.GoToService; +import ghidra.app.services.StringTranslationService; +import ghidra.app.services.StringTranslationService.TranslateOptions; +import ghidra.docking.settings.Settings; +import ghidra.docking.settings.SettingsImpl; +import ghidra.framework.Application; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.data.*; +import ghidra.program.model.data.DataUtilities.ClearDataMode; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.Program; +import ghidra.program.model.util.CodeUnitInsertionException; +import ghidra.program.util.ProgramLocation; +import ghidra.util.Msg; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.layout.PairLayout; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; +import ghidra.util.task.*; + +public class EncodedStringsDialog extends DialogComponentProvider { + + private static final Map CHARSET_TO_DT_MAP = Map.ofEntries( + // charsets not in this map will use StringDataType and will + // set the charset setting at the memory location of the string to be created + Map.entry(CharsetInfo.USASCII, StringDataType.dataType), + Map.entry(CharsetInfo.UTF8, StringUTF8DataType.dataType), + Map.entry(CharsetInfo.UTF16, UnicodeDataType.dataType), + Map.entry(CharsetInfo.UTF32, Unicode32DataType.dataType)); + + private final PluginTool tool; + private final EncodedStringsPlugin plugin; + private final Program program; + private AddressSetView selectedAddresses; + private EncodedStringsTableModel tableModel; + private EncodedStringsThreadedTablePanel threadedTablePanel; + private GhidraTableFilterPanel filterPanel; + private GhidraTable table; + + private JPanel optionsPanel; + private GhidraComboBox charsetComboBox; + private JToggleButton showAdvancedOptionsButton; + private JToggleButton showScriptOptionsButton; + private JToggleButton showTranslateOptionsButton; + + private GhidraComboBox requiredUnicodeScript; + private Map scriptExampleStrings = new HashMap<>(); + private JToggleButton allowLatinScriptButton; + private JToggleButton allowCommonScriptButton; + private JToggleButton allowAnyScriptButton; + private GCheckBox excludeStringsWithCodecErrorCB; + private GCheckBox excludeStringsWithNonStdCtrlCharsCB; + private IntegerSpinner minStringLengthSpinner; + private GCheckBox alignStartOfStringCB; + private GCheckBox breakOnRefCB; + + private GDHtmlLabel codecErrorsCountLabel; + private GDHtmlLabel nonStdCtrlCharsErrorsCountLabel; + private GDHtmlLabel stringModelFailedCountLabel; + private GDHtmlLabel minLenFailedCountLabel; + private GDHtmlLabel scriptFailedCountLabel; + private GDHtmlLabel otherScriptsFailedCountLabel; + private GDHtmlLabel latinScriptFailedCountLabel; + private GDHtmlLabel commonScriptFailedCountLabel; + private GDHtmlLabel advancedFailedCountLabel; + + private JButton createButton; + + private GhidraComboBox translateComboBox; + + private EncodedStringsOptions currentOptions; + private AtomicReference> previouslySelectedRowAddrs = new AtomicReference<>(); + private AtomicBoolean updateInProgressFlag = new AtomicBoolean(); + private AtomicReference rowToSelect = new AtomicReference<>(); + + private GCheckBox requireValidStringCB; + private GhidraComboBox stringModelFilenameComboBox; + private TrigramStringValidator stringValidator; + private String trigramModelFilename; + + private int optionsPanelRowCount; + private int advOptsRow1; + private int advOptsRow2; + private int stringModelRow; + private int scriptRow; + private int translateRow; + + private EncodedStringsFilterStats prevStats = new EncodedStringsFilterStats(); + private ItemListener itemListener = this::comboboxItemListener; + + public EncodedStringsDialog(EncodedStringsPlugin plugin, Program program, + AddressSetView selectedAddresses) { + super(makeTitleString(selectedAddresses), false, true, true, true); + setRememberSize(false); + + this.plugin = plugin; + this.tool = plugin.getTool(); + this.program = program; + this.selectedAddresses = selectedAddresses; + setHelpLocation(EncodedStringsPlugin.HELP_LOCATION); + + build(); + } + + /** + * For test/screen shot use + * + * @param charsetName set the charset + */ + public void setSelectedCharset(String charsetName) { + charsetComboBox.setSelectedItem(charsetName); + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setRequireValidStringOption(boolean b) { + requireValidStringCB.setSelected(b); + updateOptionsAndRefresh(); + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setAllowLatinScriptOption(boolean b) { + if (allowLatinScriptButton.isSelected() != b) { + allowLatinScriptButton.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setAllowCommonScriptOption(boolean b) { + if (allowCommonScriptButton.isSelected() != b) { + allowCommonScriptButton.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setAllowAnyScriptOption(boolean b) { + if (allowAnyScriptButton.isSelected() != b) { + allowAnyScriptButton.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param requiredScript unicode script + */ + public void setRequiredScript(UnicodeScript requiredScript) { + requiredUnicodeScript.setSelectedItem(requiredScript); + updateOptionsAndRefresh(); + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setShowAdvancedOptions(boolean b) { + if (showAdvancedOptionsButton.isSelected() != b) { + showAdvancedOptionsButton.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setShowScriptOptions(boolean b) { + if (showScriptOptionsButton.isSelected() != b) { + showScriptOptionsButton.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setExcludeCodecErrors(boolean b) { + if (excludeStringsWithCodecErrorCB.isSelected() != b) { + excludeStringsWithCodecErrorCB.doClick(); + } + } + + /** + * For test/screen shot use + * + * @param b boolean + */ + public void setExcludeNonStdCtrlChars(boolean b) { + if (excludeStringsWithNonStdCtrlCharsCB.isSelected() != b) { + excludeStringsWithNonStdCtrlCharsCB.doClick(); + } + } + + /** + * For test/screen shot use + * + * @return table model + */ + public EncodedStringsTableModel getStringModel() { + return tableModel; + } + + /** + * For test/screen shot use + * + * @return button + */ + public JButton getCreateButton() { + return createButton; + } + + public void programClosed(Program p) { + if (program == p) { + close(); + } + } + + private void buildScriptExamplesMap(Font f) { + if (scriptExampleStrings.isEmpty()) { + scriptExampleStrings.putAll(CharacterScriptUtils.getDisplayableScriptExamples(f, 7)); + } + } + + private void build() { + addWorkPanel(buildWorkPanel()); + createButton = new JButton("Create"); + createButton.setName("Create"); + + createButton.addActionListener(e -> { + if (isSingleStringMode()) { + createStringsAndClose(); + } + else { + createStrings(); + } + }); + addButton(createButton); + + addCancelButton(); + cancelButton.setText("Dismiss"); + setDefaultButton(createButton); + } + + private JComponent buildWorkPanel() { + optionsPanel = new JPanel(new PairLayout(5, 5)); + optionsPanel.setBorder(BorderFactory.createTitledBorder("Options")); + + buildCharsetPickerComponents(); + buildOptionsButtonComponents(); + buildAdvancedOptionsComponents(); + buildScriptFilterComponents(); + buildTranslateComponents(); + + boolean ssm = selectedAddresses.getNumAddresses() == 1; + + addRow(new GLabel("Charset:", SwingConstants.RIGHT), charsetComboBox, + showScriptOptionsButton, showTranslateOptionsButton, showAdvancedOptionsButton, + advancedFailedCountLabel); + + GLabel scriptLabel = new GLabel("Script:", SwingConstants.RIGHT); + GLabel allowAddLabel = new GLabel("Allow Additional:"); + scriptRow = + addRow(scriptLabel, requiredUnicodeScript, scriptFailedCountLabel, allowAddLabel, + allowAnyScriptButton, otherScriptsFailedCountLabel, allowLatinScriptButton, + latinScriptFailedCountLabel, allowCommonScriptButton, commonScriptFailedCountLabel); + + advOptsRow1 = addRow(null, excludeStringsWithCodecErrorCB, codecErrorsCountLabel, + excludeStringsWithNonStdCtrlCharsCB, nonStdCtrlCharsErrorsCountLabel); + stringModelRow = addRow(null, requireValidStringCB, stringModelFailedCountLabel, + stringModelFilenameComboBox); + if (ssm) { + advOptsRow2 = addRow(null, alignStartOfStringCB, breakOnRefCB); + } + else { + GLabel minLenLabel = new GLabel("Min Length:", SwingConstants.RIGHT); + minLenLabel.setToolTipText(minStringLengthSpinner.getSpinner().getToolTipText()); + advOptsRow2 = addRow(null, minLenLabel, minStringLengthSpinner.getSpinner(), + minLenFailedCountLabel, alignStartOfStringCB, breakOnRefCB); + } + translateRow = addRow(new GLabel("Translate:", SwingConstants.RIGHT), translateComboBox); + + setRowVisibility(advOptsRow1, showAdvancedOptionsButton.isSelected()); + setRowVisibility(advOptsRow2, showAdvancedOptionsButton.isSelected()); + setRowVisibility(stringModelRow, showAdvancedOptionsButton.isSelected()); + setRowVisibility(scriptRow, showScriptOptionsButton.isSelected()); + setRowVisibility(translateRow, showTranslateOptionsButton.isSelected()); + + buildPreviewTableComponents(); + + JPanel previewTablePanel = new JPanel(new BorderLayout()); + previewTablePanel.add(threadedTablePanel, BorderLayout.CENTER); + previewTablePanel.add(filterPanel, BorderLayout.SOUTH); + + JPanel panel = new JPanel(new BorderLayout()); + panel.add(optionsPanel, BorderLayout.NORTH); + panel.add(previewTablePanel, BorderLayout.CENTER); + + return panel; + } + + private void buildPreviewTableComponents() { + tableModel = new EncodedStringsTableModel(program, selectedAddresses); + tableModel.addTableModelListener(e -> { + Integer rowNum = rowToSelect.getAndSet(null); + if (rowNum != null) { + table.selectRow(rowNum); + table.requestFocusInWindow(); + } + }); + tableModel.addThreadedTableModelListener(new ThreadedTableModelListener() { + + @Override + public void loadingStarted() { + setStatusText("Filtering strings..."); + setCreateButtonInfo(0, 0); + threadedTablePanel.showEmptyTableOverlay(false); + } + + @Override + public void loadingFinished(boolean wasCancelled) { + EncodedStringsFilterStats stats = tableModel.getStats(); + prevStats = stats.clone(); + int rowCount = tableModel.getRowCount(); + setStatusText("%s strings found, %d strings match, %d excluded%s.".formatted( + stats.total, rowCount, stats.getTotalOmitted(), + wasCancelled ? " (partial results)" : "")); + List

    previousAddrs = previouslySelectedRowAddrs.getAndSet(null); + if (previousAddrs != null) { + setSelectedAddresses(previousAddrs); + } + selectedRowChange(); + + codecErrorsCountLabel.setText(getErrorCountString(stats.codecErrors)); + nonStdCtrlCharsErrorsCountLabel.setText(getErrorCountString(stats.nonStdCtrlChars)); + stringModelFailedCountLabel.setText(getErrorCountString(stats.failedStringModel)); + minLenFailedCountLabel.setText(getErrorCountString(stats.stringLength)); + scriptFailedCountLabel.setText(getErrorCountString(stats.requiredScripts)); + latinScriptFailedCountLabel.setText(getErrorCountString(stats.latinScript)); + commonScriptFailedCountLabel.setText(getErrorCountString(stats.commonScript)); + otherScriptsFailedCountLabel.setText(getErrorCountString(stats.otherScripts)); + advancedFailedCountLabel + .setText(getErrorCountString(stats.getTotalForAdvancedOptions())); + + updateRequiredScriptsList(stats); + threadedTablePanel.showEmptyTableOverlay(rowCount == 0); + } + + @Override + public void loadPending() { + // ignore + } + }); + + JPanel emptyTableOverlay = new JPanel(new GridBagLayout()); + emptyTableOverlay.add(new GHtmlLabel("No strings matched filter criteria..."), + new GridBagConstraints()); + threadedTablePanel = + new EncodedStringsThreadedTablePanel<>(tableModel, 1000, emptyTableOverlay); + threadedTablePanel.setBorder(BorderFactory.createTitledBorder("Preview")); + table = threadedTablePanel.getTable(); + table.setName("DataTable"); + table.setPreferredScrollableViewportSize(new Dimension(350, 150)); + table.getSelectionModel().addListSelectionListener(e -> selectedRowChange()); + + GoToService goToService = tool.getService(GoToService.class); + table.installNavigation(goToService, goToService.getDefaultNavigatable()); + + filterPanel = new GhidraTableFilterPanel<>(table, tableModel); + } + + private void buildCharsetPickerComponents() { + charsetComboBox = new GhidraComboBox<>(); + for (String charsetName : CharsetInfo.getInstance().getCharsetNames()) { + charsetComboBox.addToModel(charsetName); + } + charsetComboBox.setSelectedItem(getDefault(EncodedStringsPlugin.CHARSET_OPTIONNAME, + EncodedStringsPlugin.CHARSET_DEFAULT_VALUE)); + charsetComboBox.addItemListener(itemListener); + charsetComboBox.setToolTipText("Which character set to use to decode the raw bytes."); + charsetComboBox.addKeyListener(new KeyListener() { + + @Override + public void keyTyped(KeyEvent e) { + // empty + } + + @Override + public void keyReleased(KeyEvent e) { + // empty + } + + @Override + public void keyPressed(KeyEvent e) { + // Note: we override the [ENTER] key handling to allow the user to invoke the + // dialog and just hit enter to create the string without having to do any + // clicking (otherwise the charset combobox consumes the keystroke) + if (e.getKeyChar() == '\n') { + e.consume(); + if (charsetComboBox.isPopupVisible()) { + charsetComboBox.setPopupVisible(false); + } + else { + EncodedStringsDialog.this.createButton.doClick(); + } + } + } + }); + + } + + private void buildOptionsButtonComponents() { + showAdvancedOptionsButton = new JToggleButton("Advanced..."); + showAdvancedOptionsButton.setName("SHOW_ADVANCED_OPTIONS"); + showAdvancedOptionsButton.setToolTipText("Show advanced options."); + showAdvancedOptionsButton.addActionListener(e -> { + setRowVisibility(advOptsRow1, showAdvancedOptionsButton.isSelected()); + setRowVisibility(advOptsRow2, showAdvancedOptionsButton.isSelected()); + setRowVisibility(stringModelRow, showAdvancedOptionsButton.isSelected()); + advancedFailedCountLabel.setVisible(!showAdvancedOptionsButton.isSelected()); + }); + + // the empty div ensures the initial preferred width of the dialog includes space to show a fail count + advancedFailedCountLabel = new GDHtmlLabel("
    "); + advancedFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + advancedFailedCountLabel.setToolTipText( + "Number of strings excluded due to filtering options in advanced options."); + advancedFailedCountLabel.setVisible(!showAdvancedOptionsButton.isSelected()); + + showScriptOptionsButton = new JToggleButton("A-Z,\u6211\u7684,\u062d\u064e\u0648\u0651"); + showScriptOptionsButton.setName("SHOW_SCRIPT_OPTIONS"); + showScriptOptionsButton.setToolTipText("Filter by character scripts (alphabets)."); + showScriptOptionsButton.addActionListener(e -> { + setRowVisibility(scriptRow, showScriptOptionsButton.isSelected()); + updateOptionsAndRefresh(); + }); + + showTranslateOptionsButton = new JToggleButton("Translate"); + showTranslateOptionsButton.setName("SHOW_TRANSLATE_OPTIONS"); + showTranslateOptionsButton.setToolTipText("Translate strings after creation."); + showTranslateOptionsButton.addActionListener(e -> { + setRowVisibility(translateRow, showTranslateOptionsButton.isSelected()); + }); + } + + private void buildAdvancedOptionsComponents() { + boolean singleStringMode = isSingleStringMode(); + excludeStringsWithCodecErrorCB = new GCheckBox("Exclude codec errors"); + excludeStringsWithCodecErrorCB.setSelected(!singleStringMode); + excludeStringsWithCodecErrorCB.addItemListener(this::checkboxItemListener); + excludeStringsWithCodecErrorCB.setToolTipText(""" + Exclude strings that have charset codec errors.
    + (bytes/sequences that are invalid for the chosen charset)"""); + + codecErrorsCountLabel = new GDHtmlLabel(); + codecErrorsCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + codecErrorsCountLabel.setToolTipText("Number of strings excluded due to codec errors."); + + excludeStringsWithNonStdCtrlCharsCB = new GCheckBox("Exclude non-std ctrl chars"); + excludeStringsWithNonStdCtrlCharsCB.setSelected(!singleStringMode); + excludeStringsWithNonStdCtrlCharsCB.setToolTipText(""" + Exclude strings that contain non-standard control characters.
    + (ASCII 1..31, not including tab, CR, LF)"""); + excludeStringsWithNonStdCtrlCharsCB.addItemListener(this::checkboxItemListener); + + nonStdCtrlCharsErrorsCountLabel = new GDHtmlLabel(); + nonStdCtrlCharsErrorsCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + nonStdCtrlCharsErrorsCountLabel.setToolTipText( + "Number of strings excluded due to non-standard control characters."); + + alignStartOfStringCB = new GCheckBox("Align start of string"); + alignStartOfStringCB.setToolTipText(""" + If the chosen charset specifies a char size greater than 1, only look for
    + strings that begin on an aligned boundary."""); + alignStartOfStringCB.setSelected(!singleStringMode); + alignStartOfStringCB.addItemListener(this::checkboxItemListener); + + breakOnRefCB = new GCheckBox("Truncate at ref"); + breakOnRefCB.setSelected(true); + breakOnRefCB.addItemListener(this::checkboxItemListener); + breakOnRefCB.setToolTipText("Truncate strings at references."); + + minStringLengthSpinner = new IntegerSpinner(new SpinnerNumberModel( // spinner + Long.valueOf(Math.min(5, selectedAddresses.getNumAddresses())), // initial + Long.valueOf(0), // min + Long.valueOf(Math.min(99, selectedAddresses.getNumAddresses())), // max + Long.valueOf(1)), // inc + 3 /* columns */); + minStringLengthSpinner.getSpinner() + .setToolTipText( + "Exclude strings that are shorter (in characters, not bytes) than this minimum"); + minStringLengthSpinner.getTextField().setShowNumberMode(false); + minStringLengthSpinner.getSpinner().addChangeListener(e -> updateOptionsAndRefresh()); + + minLenFailedCountLabel = new GDHtmlLabel(); + minLenFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + minLenFailedCountLabel.setToolTipText("Number of strings excluded due to length."); + + stringModelFilenameComboBox = new GhidraComboBox<>(); + stringModelFilenameComboBox.setEditable(true); + for (String builtinStringModelFilename : getBuiltinStringModelFilenames()) { + stringModelFilenameComboBox.addToModel(builtinStringModelFilename); + } + + stringModelFilenameComboBox + .setText(getDefault(EncodedStringsPlugin.STRINGMODEL_FILENAME_OPTIONNAME, + EncodedStringsPlugin.STRINGMODEL_FILENAME_DEFAULT)); + stringModelFilenameComboBox.addItemListener(itemListener); + stringModelFilenameComboBox.setToolTipText(""" + Select the name of a built-in string model,
    + or
    + Enter the full path to a user-supplied .sng model file,
    + or
    + Clear the field for no string model."""); + + requireValidStringCB = new GCheckBox("Exclude invalid strings"); + requireValidStringCB.setSelected(false); + requireValidStringCB.setToolTipText("Verify strings against the string model."); + requireValidStringCB.addItemListener(this::checkboxItemListener); + + stringModelFailedCountLabel = new GDHtmlLabel(); + stringModelFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + stringModelFailedCountLabel + .setToolTipText("Number of strings excluded due to failing string model check."); + } + + private void buildScriptFilterComponents() { + + requiredUnicodeScript = new CharScriptComboBox(); + requiredUnicodeScript.setSelectedItem(CharacterScriptUtils.ANY_SCRIPT_ALIAS); + requiredUnicodeScript.addItemListener(itemListener); + requiredUnicodeScript.setToolTipText( + """ + Require at least one character of this script (alphabet) to be present in the string.

    +

    + Use the Allow Additional toggle buttons (if currently not enabled) to
    + allow more strings to match.

    +

    + Note: character scripts that are drawable using the current font will have
    + some example characters displayed to the right of the name."""); + + scriptFailedCountLabel = new GDHtmlLabel(); + scriptFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + scriptFailedCountLabel + .setToolTipText("Number of strings excluded due to failing script requirements."); + + Font font = new JPanel().getFont().deriveFont(10.0f); + allowLatinScriptButton = new JToggleButton("A-Z"); + allowLatinScriptButton.setName("ALLOW_LATIN_SCRIPT"); + allowLatinScriptButton.setFont(font); + allowLatinScriptButton.setToolTipText( + "Allow Latin characters (e.g. A-Z, etc) to also be present in the string."); + allowLatinScriptButton.setSelected(true); + allowLatinScriptButton.addItemListener(this::checkboxItemListener); + + latinScriptFailedCountLabel = new GDHtmlLabel(); + latinScriptFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + latinScriptFailedCountLabel.setToolTipText( + "Number of strings excluded because they contained Latin characters."); + + allowCommonScriptButton = new JToggleButton("0-9,!?"); + allowCommonScriptButton.setName("ALLOW_COMMON_SCRIPT"); + allowCommonScriptButton.setFont(font); + allowCommonScriptButton.setToolTipText( + "Allow common characters (e.g. 0-9, space, punctuation, etc) to also be present in the string."); + allowCommonScriptButton.setSelected(true); + allowCommonScriptButton.addItemListener(this::checkboxItemListener); + + commonScriptFailedCountLabel = new GDHtmlLabel(); + commonScriptFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + commonScriptFailedCountLabel.setToolTipText( + "Number of strings excluded because they contained Common (0-9, space, punctuation, etc) characters."); + + allowAnyScriptButton = new JToggleButton("Any"); + allowAnyScriptButton.setName("ALLOW_ANY_SCRIPT"); + allowAnyScriptButton.setFont(font); + allowAnyScriptButton.setToolTipText( + "Allow all other character scripts to also be present in the string."); + allowAnyScriptButton.setSelected(true); + allowAnyScriptButton.addItemListener(this::checkboxItemListener); + + otherScriptsFailedCountLabel = new GDHtmlLabel(); + otherScriptsFailedCountLabel.setForeground(GThemeDefaults.Colors.Messages.ERROR); + otherScriptsFailedCountLabel.setToolTipText( + "Number of strings excluded because they contained characters from other scripts (alphabets)."); + } + + private List getBuiltinStringModelFilenames() { + return List.of(EncodedStringsPlugin.STRINGMODEL_FILENAME_DEFAULT); + } + + private void fixupComboBoxSizes() { + // Make the charset and script comboboxes the same width + requiredUnicodeScript.setPreferredSize(charsetComboBox.getPreferredSize()); + } + + private void buildTranslateComponents() { + List translationServices = + StringTranslationService.getCurrentStringTranslationServices(tool); + translateComboBox = new GhidraComboBox<>(translationServices); + StringTranslationService defaultSTS = getDefaultTranslationService(translationServices); + if (defaultSTS != null) { + translateComboBox.setSelectedItem(defaultSTS); + } + + translateComboBox.setRenderer(GListCellRenderer.createDefaultCellTextRenderer( + sts -> sts != null ? sts.getTranslationServiceName() : "")); + } + + private void setRowVisibility(int rowNum, boolean b) { + Component leftComp = optionsPanel.getComponent(rowNum * 2); + leftComp.setVisible(b); + Component rightComp = optionsPanel.getComponent(rowNum * 2 + 1); + rightComp.setVisible(b); + if (b) { + // if a row is going to take space from the dialog, check the table + Swing.runLater(this::fixTooSmallTablePanel); + } + } + + private void fixTooSmallTablePanel() { + int rowHeight = table.getRowHeight(); + Dimension tableSize = threadedTablePanel.getSize(); + int desiredMinTableHt = rowHeight * 4; // aprox 2 rows + header + filter input + if (tableSize.height < desiredMinTableHt) { + Dimension dlgSize = getDialogSize(); + dlgSize.height += (desiredMinTableHt - tableSize.height); + setDialogSize(dlgSize); + } + } + + private int addRow(Component leftComponent, Component... rightComponents) { + if (leftComponent == null) { + leftComponent = new GLabel(); + } + Component rightComponent; + if (rightComponents.length > 1) { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + for (Component c : rightComponents) { + if (c != null) { + panel.add(c); + } + } + rightComponent = panel; + } + else { + rightComponent = rightComponents[0]; + } + optionsPanel.add(leftComponent); + optionsPanel.add(rightComponent); + return optionsPanelRowCount++; + } + + @Override + protected void dialogShown() { + fixupComboBoxSizes(); + updateOptionsAndRefresh(); + } + + @Override + protected void cancelCallback() { + saveDefaults(); + close(); + } + + @Override + public void close() { + super.close(); + dispose(); + } + + @Override + public void dispose() { + plugin.dialogClosed(this); + table.dispose(); + super.dispose(); + } + + private void createStrings() { + setActionItemEnablement(false); + executeMonitoredRunnable("Creating Strings", true, true, 100, this::createStrings); + } + + private void createStringsAndClose() { + saveDefaults(); + setActionItemEnablement(false); + executeMonitoredRunnable("Creating Strings", true, true, 100, this::createStringsAndClose); + } + + private void setActionItemEnablement(boolean b) { + createButton.setEnabled(b); + cancelButton.setEnabled(b); + table.removeNavigation(); + if (b) { + GoToService goToService = tool.getService(GoToService.class); + table.installNavigation(goToService, goToService.getDefaultNavigatable()); + } + } + + private void createStringsHelper(TaskMonitor monitor) { + int count = 0; + setStatusText("Creating strings..."); + int txId = program.startTransaction("Create Strings"); + boolean success = false; + try { + List stringsToCreate = new ArrayList<>(); + int[] selectedRowNums = table.getSelectedRows(); + if (selectedRowNums.length == 0) { + stringsToCreate.addAll(tableModel.getUnfilteredData()); + } + else { + stringsToCreate.addAll(tableModel.getRowObjects(selectedRowNums)); + } + + monitor.initialize(stringsToCreate.size()); + monitor.setMessage("Creating strings..."); + Settings settings = currentOptions.settings(); + List newStrings = new ArrayList<>(); + for (EncodedStringsRow row : stringsToCreate) { + if (monitor.isCancelled()) { + break; + } + monitor.incrementProgress(1); + try { + Data data = DataUtilities.createData(program, row.sdi().getAddress(), + currentOptions.stringDT(), row.sdi().getDataLength(), false, + ClearDataMode.CLEAR_ALL_DEFAULT_CONFLICT_DATA); + + // copy settings to new data instance + for (String settingName : settings.getNames()) { + Object settingValue = settings.getValue(settingName); + data.setValue(settingName, settingValue); + } + + count++; + newStrings.add(new ProgramLocation(program, row.sdi().getAddress())); + } + catch (CodeUnitInsertionException e) { + Msg.warn(this, "Failed to create string at " + row.sdi().getAddress()); + } + } + tool.setStatusInfo("Created %d strings.".formatted(count)); + tableModel.removeRows(stringsToCreate); + if (selectedRowNums.length > 0 && selectedRowNums[0] < tableModel.getRowCount()) { + // Re-select the current row after table update to enbiggen the user experience. + // See table listener for the other end of this + rowToSelect.set(selectedRowNums[0]); + } + StringTranslationService sts = getSelectedStringTranslationService(true); + if (sts != null) { + Swing.runLater( + () -> sts.translate(program, newStrings, new TranslateOptions(true))); + } + success = true; + } + finally { + program.endTransaction(txId, success); + } + } + + private void createStrings(TaskMonitor monitor) { + createStringsHelper(monitor); + Swing.runLater(() -> setActionItemEnablement(true)); + } + + private void createStringsAndClose(TaskMonitor monitor) { + createStringsHelper(monitor); + Swing.runLater(this::close); + } + + private void setCreateButtonInfo(int rowCount, int selectedRowCount) { + if (rowCount == 0) { + createButton.setText("Create"); + createButton.setEnabled(false); + return; + } + String createMessage = isSingleStringMode() + ? "Create" + : "Create %s".formatted(rowCount == selectedRowCount || selectedRowCount == 0 + ? "All" + : "Selected (%d)".formatted(selectedRowCount)); + createButton.setEnabled(true); + createButton.setText(createMessage); + + } + + private void selectedRowChange() { + int rowCount = table.getRowCount(); + int selectedRowCount = table.getSelectedRowCount(); + setCreateButtonInfo(rowCount, selectedRowCount); + if (selectedRowCount == 1) { + int[] selectedRows = table.getSelectedRows(); + table.navigate(selectedRows[0], 0 /* location col */); + } + } + + private List

    getSelectedAddresses() { + List
    result = new ArrayList<>(); + for (EncodedStringsRow row : tableModel.getRowObjects(table.getSelectedRows())) { + result.add(row.sdi().getAddress()); + } + return result; + } + + private void setSelectedAddresses(List
    addrs) { + Set
    addrSet = new HashSet<>(addrs); + for (EncodedStringsRow row : tableModel.getModelData()) { + if (addrSet.contains(row.sdi().getAddress())) { + int viewIndex = tableModel.getViewIndex(row); + if (viewIndex >= 0) { + table.getSelectionManager().addSelectionInterval(viewIndex, viewIndex); + } + } + } + } + + private void suppressRecursiveCallbacks(AtomicBoolean flag, Runnable r) { + if (flag.compareAndSet(false, true)) { + r.run(); + flag.set(false); + } + } + + private void updateOptionsAndRefresh() { + suppressRecursiveCallbacks(updateInProgressFlag, () -> { + List
    selectedAddrs = getSelectedAddresses(); + if (!selectedAddrs.isEmpty()) { + previouslySelectedRowAddrs.set(selectedAddrs); + } + + updateOptions(); + tableModel.setOptions(currentOptions); + selectedRowChange(); + }); + } + + private String getErrorCountString(int count) { + return count > 0 ? "[%d]".formatted(count) : null; + } + + private void updateOptions() { + String charsetName = charsetComboBox.getSelectedItem().toString(); + if (!charsetExists(charsetName)) { + charsetName = CharsetInfo.USASCII; + } + + boolean scriptOptions = showScriptOptionsButton.isSelected(); + boolean excludeStringsWithErrors = excludeStringsWithCodecErrorCB.isSelected(); + boolean excludeStringsWithNonStdCtrlChars = + excludeStringsWithNonStdCtrlCharsCB.isSelected(); + boolean alignStartofString = alignStartOfStringCB.isSelected(); + + // override the strminlen if the address range selection would be too small + int minStrLen = !isSingleStringMode() + ? (int) Math.min(minStringLengthSpinner.getTextField().getIntValue(), + selectedAddresses.getNumAddresses()) + : -1; // single string mode - no min len + + AbstractStringDataType stringDT = CHARSET_TO_DT_MAP.get(charsetName); + Settings settings = SettingsImpl.NO_SETTINGS; + if (stringDT == null) { + stringDT = StringDataType.dataType; + settings = new SettingsImpl(); + CharsetSettingsDefinition.CHARSET.setCharset(settings, charsetName); + } + int charSize = CharsetInfo.getInstance().getCharsetCharSize(charsetName); + + updateTrigramStringValidator(stringModelFilenameComboBox.getText()); + boolean requireValidStrings = requireValidStringCB.isSelected(); + boolean breakOnRef = breakOnRefCB.isSelected(); + + currentOptions = new EncodedStringsOptions(stringDT, settings, charsetName, + scriptOptions ? getRequiredScripts() : null, scriptOptions ? getAllowedScripts() : null, + excludeStringsWithErrors, excludeStringsWithNonStdCtrlChars, alignStartofString, + charSize, minStrLen, breakOnRef, stringValidator, requireValidStrings); + } + + private void updateTrigramStringValidator(String newTrigramModelFilename) { + if (!newTrigramModelFilename.equals(trigramModelFilename)) { + trigramModelFilename = newTrigramModelFilename; + ResourceFile file = getTrigramStringModelFile(trigramModelFilename); + try { + stringValidator = file != null ? TrigramStringValidator.read(file) : null; + } + catch (IOException e) { + Msg.error(this, "Error reading string model file", e); + stringValidator = null; + } + } + } + + private ResourceFile getTrigramStringModelFile(String filename) { + if (filename == null || filename.isBlank()) { + return null; + } + File f = new File(filename); + ResourceFile rf = f.isAbsolute() && f.isFile() + ? new ResourceFile(f) + : Application.findDataFileInAnyModule(filename); + if (rf == null) { + Msg.error(this, "Unable to find string model file: %s".formatted(filename)); + } + return rf; + } + + private Set getRequiredScripts() { + Set scripts = EnumSet.noneOf(UnicodeScript.class); + UnicodeScript selectedUnicodeScript = + (UnicodeScript) requiredUnicodeScript.getSelectedItem(); + if (selectedUnicodeScript != null && + selectedUnicodeScript != CharacterScriptUtils.ANY_SCRIPT_ALIAS) { + scripts.add(selectedUnicodeScript); + } + return scripts; + } + + private Set getAllowedScripts() { + Set results = EnumSet.noneOf(UnicodeScript.class); + if (allowAnyScriptButton.isSelected()) { + results.addAll(EnumSet.allOf(UnicodeScript.class)); + results.remove(UnicodeScript.LATIN); + results.remove(UnicodeScript.COMMON); + } + + if (allowLatinScriptButton.isSelected()) { + results.add(UnicodeScript.LATIN); + } + if (allowCommonScriptButton.isSelected()) { + results.add(UnicodeScript.COMMON); + } + return results; + } + + private String getDefault(String optionName, String defaultValue) { + ToolOptions stringOptions = tool.getOptions(EncodedStringsPlugin.STRINGS_OPTION_NAME); + return stringOptions.getString(optionName, defaultValue); + } + + private StringTranslationService getDefaultTranslationService( + List translationServices) { + String translationServiceName = + getDefault(EncodedStringsPlugin.TRANSLATE_SERVICE_OPTIONNAME, null); + if (translationServiceName != null) { + for (StringTranslationService sts : translationServices) { + if (translationServiceName.equals(sts.getTranslationServiceName())) { + return sts; + } + } + } + return null; + } + + private StringTranslationService getSelectedStringTranslationService(boolean ifEnabled) { + boolean enabled = showTranslateOptionsButton.isSelected(); + return ifEnabled && !enabled + ? null + : (StringTranslationService) translateComboBox.getSelectedItem(); + } + + private void saveDefaults() { + if (currentOptions == null) { + return; + } + ToolOptions stringOptions = tool.getOptions(EncodedStringsPlugin.STRINGS_OPTION_NAME); + + stringOptions.setString(EncodedStringsPlugin.CHARSET_OPTIONNAME, + currentOptions.charsetName()); + + StringTranslationService sts = getSelectedStringTranslationService(false); + stringOptions.setString(EncodedStringsPlugin.TRANSLATE_SERVICE_OPTIONNAME, + sts != null ? sts.getTranslationServiceName() : null); + + stringOptions.setString(EncodedStringsPlugin.STRINGMODEL_FILENAME_OPTIONNAME, + trigramModelFilename); + } + + private void comboboxItemListener(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + updateOptionsAndRefresh(); + } + } + + private void checkboxItemListener(ItemEvent e) { + updateOptionsAndRefresh(); + } + + private void updateRequiredScriptsList(EncodedStringsFilterStats stats) { + requiredUnicodeScript.removeItemListener(itemListener); + + UnicodeScript currentSelectedScript = + (UnicodeScript) requiredUnicodeScript.getSelectedItem(); + + requiredUnicodeScript.setModel(getScriptListModel(stats)); + if (stats.foundScriptCounts.containsKey(currentSelectedScript)) { + requiredUnicodeScript.setSelectedItem(currentSelectedScript); + } + else { + requiredUnicodeScript.setSelectedItem(CharacterScriptUtils.ANY_SCRIPT_ALIAS); + } + requiredUnicodeScript.addItemListener(itemListener); + + } + + private ComboBoxModel getScriptListModel(EncodedStringsFilterStats stats) { + List scripts = new ArrayList<>(stats.foundScriptCounts.keySet()); + Collections.sort(scripts, (us1, us2) -> us1.name().compareTo(us2.name())); + scripts.add(0, CharacterScriptUtils.ANY_SCRIPT_ALIAS); + + return new DefaultComboBoxModel<>(new Vector<>(scripts)); + } + + private boolean isSingleStringMode() { + return selectedAddresses.getNumAddresses() == 1; + } + + /** + * Execute a non-modal task that has progress and can be cancelled. + *

    + * See {@link #executeProgressTask(Task, int)}. + * + * @param taskTitle String title of task + * @param canCancel boolean flag, if true task can be canceled by the user + * @param hasProgress boolean flag, if true the task has a progress meter + * @param delay int number of milliseconds to delay before showing the task's + * progress + * @param runnable {@link MonitoredRunnable} to run + */ + private void executeMonitoredRunnable(String taskTitle, boolean canCancel, boolean hasProgress, + int delay, MonitoredRunnable runnable) { + Task task = new Task(taskTitle, canCancel, hasProgress, false) { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + runnable.monitoredRun(monitor); + } + }; + executeProgressTask(task, delay); + } + + private static String makeTitleString(AddressSetView addrs) { + return "Search For Encoded Strings - %s (%s - %s)".formatted( + formatLength(addrs.getNumAddresses(), "addresses"), addrs.getMinAddress(), + addrs.getMaxAddress()); + } + + private static boolean charsetExists(String charsetName) { + try { + Charset charset = Charset.forName(charsetName); + return charset != null; + } + catch (RuntimeException e) { + return false; + } + } + + private static String formatLength(long length, String unitSuffix) { + int divisor = 1; + String unitPrefix = ""; + if (length < 1000) { + // nothing + } + else if (length < 1000000) { + divisor = 1000; + unitPrefix = "K"; + } + else { + divisor = 1000000; + unitPrefix = "M"; + } + + return "%d%s %s".formatted(length / divisor, unitPrefix, unitSuffix); + } + + private class CharScriptComboBox extends GhidraComboBox { + + CharScriptComboBox() { + super(List.of(CharacterScriptUtils.ANY_SCRIPT_ALIAS)); + + Function cellToTextMappingFunction = unicodeScript -> { + buildScriptExamplesMap(getFont()); + if (unicodeScript == null) { + return ""; + } + if (unicodeScript == CharacterScriptUtils.ANY_SCRIPT_ALIAS) { + return ""; + } + String name = unicodeScript.name(); + String example = scriptExampleStrings.getOrDefault(unicodeScript, ""); + if (!example.isEmpty()) { + example = " \u2014 " + example; + } + int count = prevStats.foundScriptCounts.getOrDefault(unicodeScript, 0); + return "%s%s (%d)".formatted(name, example, count); + }; + + setRenderer(GListCellRenderer.createDefaultCellTextRenderer(cellToTextMappingFunction)); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsFilterStats.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsFilterStats.java new file mode 100644 index 0000000000..cd48f31887 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsFilterStats.java @@ -0,0 +1,67 @@ +/* ### + * 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.strings; + +import java.lang.Character.UnicodeScript; +import java.util.HashMap; +import java.util.Map; + +/** + * Holds counts of reasons for filter rejection + */ +class EncodedStringsFilterStats { + int total; + int codecErrors; + int nonStdCtrlChars; + int failedStringModel; + int stringLength; + int requiredScripts; + int otherScripts; + int latinScript; + int commonScript; + Map foundScriptCounts = new HashMap<>(); + + public EncodedStringsFilterStats() { + // empty + } + + public EncodedStringsFilterStats(EncodedStringsFilterStats other) { + this.total = other.total; + this.codecErrors = other.codecErrors; + this.nonStdCtrlChars = other.nonStdCtrlChars; + this.failedStringModel = other.failedStringModel; + this.stringLength = other.stringLength; + this.requiredScripts = other.requiredScripts; + this.otherScripts = other.otherScripts; + this.latinScript = other.latinScript; + this.commonScript = other.commonScript; + this.foundScriptCounts.putAll(other.foundScriptCounts); + } + + int getTotalForAdvancedOptions() { + return codecErrors + nonStdCtrlChars + failedStringModel + stringLength; + } + + int getTotalOmitted() { + return codecErrors + nonStdCtrlChars + failedStringModel + stringLength + requiredScripts; + } + + @Override + public EncodedStringsFilterStats clone() { + return new EncodedStringsFilterStats(this); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsOptions.java new file mode 100644 index 0000000000..507504abf6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsOptions.java @@ -0,0 +1,67 @@ +/* ### + * 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.strings; + +import java.lang.Character.UnicodeScript; +import java.util.Set; + +import ghidra.app.services.StringValidatorService; +import ghidra.docking.settings.Settings; +import ghidra.program.model.data.AbstractStringDataType; +import ghidra.util.SystemUtilities; + +record EncodedStringsOptions( + AbstractStringDataType stringDT, + Settings settings, + String charsetName, + Set requiredScripts, + Set allowedScripts, + boolean excludeStringsWithErrors, + boolean excludeNonStdCtrlChars, + boolean alignStartOfString, + int charSize, + int minStringLength, + boolean breakOnRef, + StringValidatorService stringValidator, + boolean requireValidString) { + + boolean equivalentStringCreationOptions(EncodedStringsOptions other) { + // check only the options that would change how strings are created / read from memory + // or produce values that are immutable in the table Row object + return other != null && stringDT.equals(other.stringDT) && + equalValues(settings, other.settings) && charsetName.equals(other.charsetName) && + alignStartOfString == other.alignStartOfString && charSize == other.charSize && + stringValidator == other.stringValidator && breakOnRef == other.breakOnRef; + + } + + private static boolean equalValues(Settings s1, Settings s2) { + Set s1names = Set.of(s1.getNames()); + Set s2names = Set.of(s2.getNames()); + if (!s1names.equals(s2names)) { + return false; + } + for (String name : s1.getNames()) { + Object s1val = s1.getValue(name); + Object s2val = s2.getValue(name); + if (!SystemUtilities.isEqual(s1val, s2val)) { + return false; + } + } + return true; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java new file mode 100644 index 0000000000..3e48ab35a5 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java @@ -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.app.plugin.core.strings; + +import docking.DockingWindowManager; +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.tool.ToolConstants; +import ghidra.app.CorePluginPackage; +import ghidra.app.context.NavigatableActionContext; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.services.GoToService; +import ghidra.app.util.HelpTopics; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.data.CharsetInfo; +import ghidra.program.model.listing.Program; +import ghidra.util.HelpLocation; +import ghidra.util.datastruct.WeakDataStructureFactory; +import ghidra.util.datastruct.WeakSet; + +//@formatter:off +@PluginInfo( + status = PluginStatus.RELEASED, + packageName = CorePluginPackage.NAME, + category = PluginCategoryNames.SEARCH, + shortDescription = "Search For Encoded Strings", + description = "Searches for strings using a specific character set and allows filtering " + + "results using the Unicode scripts (alphabets) used and other criteria. This feature " + + "is being evaluated for it's effectiveness.", + servicesRequired = { GoToService.class } +) +//@formatter:on +public class EncodedStringsPlugin extends ProgramPlugin { + private static final String ACTIONNAME = "Search For Encoded Strings"; + static final String STRINGS_OPTION_NAME = "Strings"; + static final String CHARSET_OPTIONNAME = "Default Charset"; + static final String CHARSET_DEFAULT_VALUE = CharsetInfo.USASCII; + static final String TRANSLATE_SERVICE_OPTIONNAME = "Default Translation Service Name"; + static final String STRINGMODEL_FILENAME_OPTIONNAME = "Default String Model Filename"; + static final String STRINGMODEL_FILENAME_DEFAULT = "stringngrams/StringModel.sng"; + static final HelpLocation HELP_LOCATION = + new HelpLocation(HelpTopics.SEARCH, "Encoded_Strings_Dialog"); + + private WeakSet openDialogs = + WeakDataStructureFactory.createCopyOnWriteWeakSet(); + private DockingAction searchForEncodedStringsAction; + + public EncodedStringsPlugin(PluginTool tool) { + super(tool); + } + + public DockingAction getSearchForEncodedStringsAction() { + return searchForEncodedStringsAction; + } + + @Override + protected void init() { + super.init(); + registerOptions(); + createActions(); + } + + private void registerOptions() { + ToolOptions options = tool.getOptions(STRINGS_OPTION_NAME); + options.registerOption(CHARSET_OPTIONNAME, CHARSET_DEFAULT_VALUE, null, + "Name of default charset."); + options.registerOption(STRINGMODEL_FILENAME_OPTIONNAME, STRINGMODEL_FILENAME_DEFAULT, null, + "Name of default string model file."); + options.registerOption(TRANSLATE_SERVICE_OPTIONNAME, "", null, + "Name of default translation service."); + } + + @Override + protected void programClosed(Program program) { + for (EncodedStringsDialog openDialog : openDialogs) { + openDialog.programClosed(program); + } + } + + void dialogClosed(EncodedStringsDialog dialog) { + openDialogs.remove(dialog); + } + + private void createActions() { + searchForEncodedStringsAction = + new ActionBuilder(ACTIONNAME, getName()) // menu + .withContext(NavigatableActionContext.class, true) + .onAction(this::showSearchForEncodedStrings) + .enabledWhen(ac -> ac.getLocation() != null) + .menuPath(ToolConstants.MENU_SEARCH, "For Encoded Strings...") + .menuGroup("search for", "Strings2") + .helpLocation(HELP_LOCATION) + .buildAndInstall(tool); + } + + private void showSearchForEncodedStrings(NavigatableActionContext lac) { + AddressSetView addrs = lac.hasSelection() + ? lac.getSelection() + : lac.getProgram().getMemory().getAllInitializedAddressSet(); + EncodedStringsDialog dlg = new EncodedStringsDialog(this, lac.getProgram(), addrs); + openDialogs.add(dlg); + DockingWindowManager.showDialog(dlg); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsRow.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsRow.java new file mode 100644 index 0000000000..3d58ae9005 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsRow.java @@ -0,0 +1,82 @@ +/* ### + * 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.strings; + +import java.lang.Character.UnicodeScript; +import java.util.EnumSet; +import java.util.Set; + +import ghidra.program.model.data.StringDataInstance; + +record EncodedStringsRow(StringDataInstance sdi, StringInfo stringInfo, int refCount, + int offcutCount, boolean validString) { + + boolean matches(EncodedStringsOptions options, EncodedStringsFilterStats stats) { + stats.total++; + + String str = stringInfo.stringValue(); + if (options.minStringLength() > 0 && str.length() < options.minStringLength()) { + stats.stringLength++; + return false; + } + if (options.excludeStringsWithErrors() && stringInfo.hasCodecError()) { + stats.codecErrors++; + return false; + } + if (options.excludeNonStdCtrlChars() && stringInfo.hasNonStdCtrlChars()) { + stats.nonStdCtrlChars++; + return false; + } + + stringInfo.scripts() + .forEach(foundScript -> stats.foundScriptCounts.merge(foundScript, 1, + (prevValue, newValue) -> prevValue + newValue)); + + if (options.requiredScripts() != null && !options.requiredScripts().isEmpty()) { + if (!stringInfo.scripts().containsAll(options.requiredScripts())) { + stats.requiredScripts++; + return false; + } + } + if (options.allowedScripts() != null) { + Set scripts = EnumSet.copyOf(stringInfo.scripts()); + scripts.removeAll(CharacterScriptUtils.IGNORED_SCRIPTS); + scripts.removeAll(options.requiredScripts()); + + boolean hadLatin = scripts.remove(UnicodeScript.LATIN); + boolean hadCommon = scripts.remove(UnicodeScript.COMMON); + scripts.removeAll(options.allowedScripts()); + if (!scripts.isEmpty()) { + stats.otherScripts += 1; + return false; + } + if (hadLatin && !options.allowedScripts().contains(UnicodeScript.LATIN)) { + stats.latinScript++; + return false; + } + if (hadCommon && !options.allowedScripts().contains(UnicodeScript.COMMON)) { + stats.commonScript++; + return false; + } + } + if (options.requireValidString() && options.stringValidator() != null && !validString) { + stats.failedStringModel++; + return false; + } + return true; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsTableModel.java new file mode 100644 index 0000000000..78e5248a0a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsTableModel.java @@ -0,0 +1,448 @@ +/* ### + * 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.strings; + +import java.lang.Character.UnicodeScript; +import java.util.*; +import java.util.stream.Collectors; + +import javax.swing.JTable; +import javax.swing.table.TableModel; + +import docking.widgets.table.TableColumnDescriptor; +import generic.theme.GThemeDefaults; +import ghidra.app.services.StringValidatorQuery; +import ghidra.app.services.StringValidityScore; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.program.model.address.*; +import ghidra.program.model.data.StringDataInstance; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +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.AbstractProgramLocationTableColumn; +import ghidra.util.table.field.AddressBasedLocation; +import ghidra.util.task.TaskMonitor; + +class EncodedStringsTableModel extends AddressBasedTableModel { + + private UnicodeScriptColumn unicodeScriptColumn; + private ValidStringColumn validStringColumn; + + private AddressSetView selectedAddresses; + private AddressSetView filteredAddresses; + private boolean singleStringMode; + + private ModelState state; + + EncodedStringsTableModel(Program program, AddressSetView selectedAddresses) { + super("Encoded Strings Table", new ServiceProviderStub(), program, null, true); + this.selectedAddresses = selectedAddresses; + this.singleStringMode = selectedAddresses.getNumAddresses() == 1; + this.state = new ModelState(null, null); + } + + public EncodedStringsFilterStats getStats() { + return state.stats; + } + + @Override + public void dispose() { + state = new ModelState(null, null); + super.dispose(); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + this.validStringColumn = new ValidStringColumn(); + this.unicodeScriptColumn = new UnicodeScriptColumn(); + + descriptor.addVisibleColumn(new DataLocationColumn(), 1, true); + descriptor.addVisibleColumn(new StringRepColumn()); + descriptor.addHiddenColumn(new RefCountColumn()); + descriptor.addHiddenColumn(new OffcutRefCountColumn()); + descriptor.addVisibleColumn(unicodeScriptColumn); + descriptor.addVisibleColumn(validStringColumn); + descriptor.addVisibleColumn(new LengthColumn()); + descriptor.addHiddenColumn(new ByteLengthColumn()); + + return descriptor; + } + + @Override + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + throws CancelledException { + + Program localProgram = program; + ModelState state = this.state; + + if (state == null || localProgram == null || state.options == null) { + return; + } + + if (state.previousData != null) { + // used cached strings and re-filter + EncodedStringsFilterStats newStats = new EncodedStringsFilterStats(); + for (EncodedStringsRow row : state.previousData) { + if (row.matches(state.options, newStats)) { + accumulator.add(row); + } + } + state.stats = newStats; + return; + } + + Listing listing = localProgram.getListing(); + if (filteredAddresses == null) { + filteredAddresses = singleStringMode + ? UndefinedStringIterator.getSingleStringEndAddrRange(localProgram, + selectedAddresses) + : new AddressSet(selectedAddresses); + filteredAddresses = + filteredAddresses.intersect(localProgram.getMemory().getAllInitializedAddressSet()); + + monitor.setIndeterminate(true); + monitor.initialize(0, "Finding undefined address ranges"); + // Note: this can be slow for large programs + filteredAddresses = listing.getUndefinedRanges(filteredAddresses, false, monitor); + monitor.setIndeterminate(false); + } + + int align = 1; + if (state.options.alignStartOfString()) { + align = localProgram.getDataTypeManager() + .getDataOrganization() + .getSizeAlignment(state.options.charSize()); + } + + List allStrings = new ArrayList<>(); + EncodedStringsFilterStats newStats = new EncodedStringsFilterStats(); + UndefinedStringIterator usi = new UndefinedStringIterator(localProgram, filteredAddresses, + state.options.charSize(), align, state.options.breakOnRef(), singleStringMode, + state.options.stringDT(), state.options.settings(), monitor); + + for (StringDataInstance sdi : usi) { + monitor.checkCancelled(); + + StringInfo stringInfo = StringInfo.fromString(sdi.getStringValue()); + int refCount = localProgram.getReferenceManager().getReferenceCountTo(sdi.getAddress()); + int offcutRefCount = getOffcutRefCount(localProgram, + new AddressRangeImpl(sdi.getAddress(), sdi.getEndAddress())); + boolean isValid = true; + if (state.options.stringValidator() != null) { + StringValidatorQuery svq = + new StringValidatorQuery(stringInfo.stringValue(), stringInfo); + StringValidityScore score = + state.options.stringValidator().getStringValidityScore(svq); + isValid = score.isScoreAboveThreshold(); + } + + EncodedStringsRow row = + new EncodedStringsRow(sdi, stringInfo, refCount, offcutRefCount, isValid); + + allStrings.add(row); + + if (row.matches(state.options, newStats)) { + accumulator.add(row); + } + if (singleStringMode) { + break; + } + } + + state.stats = newStats; + state.previousData = allStrings; + } + + @Override + public ProgramSelection getProgramSelection(int[] rows) { + AddressSet set = new AddressSet(); + for (int elementIndex : rows) { + EncodedStringsRow row = filteredData.get(elementIndex); + set.add(row.sdi().getAddressRange()); + } + return new ProgramSelection(set); + } + + public void removeRows(List rows) { + for (EncodedStringsRow row : rows) { + removeObject(row); + } + } + + public void setOptions(EncodedStringsOptions options) { + boolean canReusePrevData = options.equivalentStringCreationOptions(state.options); + ModelState newState = new ModelState(options, canReusePrevData ? state.previousData : null); + this.state = newState; + clearData(); + reload(); + } + + @Override + public Address getAddress(int row) { + return getRowObject(row).sdi().getAddress(); + } + + private int getOffcutRefCount(Program localProgram, AddressRange range) { + int offcutRefCount = 0; + Address prevAddr = range.getMinAddress(); // this also allows us to skip the first addr of the range + for (Address address : localProgram.getReferenceManager() + .getReferenceDestinationIterator(new AddressSet(range), true)) { + if (!address.equals(prevAddr)) { + offcutRefCount++; + prevAddr = address; + } + } + return offcutRefCount; + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private static class DataLocationColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Location"; + } + + @Override + public AddressBasedLocation getValue(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) throws IllegalArgumentException { + return new AddressBasedLocation(program, rowObject.sdi().getAddress()); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + + } + + private static class StringRepColumn + extends AbstractProgramLocationTableColumn { + + private StringRepCellRenderer renderer = new StringRepCellRenderer(); + + @Override + public String getColumnName() { + return "String"; + } + + @Override + public EncodedStringsRow getValue(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject; + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + private class StringRepCellRenderer extends AbstractGColumnRenderer { + + @Override + protected String getText(Object value) { + return value instanceof EncodedStringsRow rowValue + ? rowValue.sdi().getStringRepresentation() + : ""; + } + + @Override + public String getFilterString(EncodedStringsRow t, Settings settings) { + return getText(t); + } + + @Override + protected void setForegroundColor(JTable table, TableModel model, Object value) { + if (value instanceof EncodedStringsRow rowValue && + rowValue.stringInfo().hasCodecError()) { + setForeground(GThemeDefaults.Colors.Tables.ERROR_UNSELECTED); + } + else { + super.setForegroundColor(table, model, value); + } + } + } + } + + private static class UnicodeScriptColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Unicode Script"; + } + + @Override + public String getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + Set scripts = rowObject.stringInfo().scripts(); + String formattedColStr = + scripts.stream().map(UnicodeScript::name).collect(Collectors.joining(",")); + + return formattedColStr; + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class RefCountColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Reference Count"; + } + + @Override + public Integer getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + return rowObject.refCount(); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class OffcutRefCountColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Offcut Reference Count"; + } + + @Override + public Integer getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + return rowObject.offcutCount(); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class ValidStringColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Is Valid String"; + } + + @Override + public Boolean getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + return rowObject.validString(); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class LengthColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Length"; + } + + @Override + public Integer getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + return rowObject.stringInfo().stringValue().length(); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class ByteLengthColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Byte Length"; + } + + @Override + public Integer getValue(EncodedStringsRow rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + return rowObject.sdi().getDataLength(); + } + + @Override + public ProgramLocation getProgramLocation(EncodedStringsRow rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return new ProgramLocation(program, rowObject.sdi().getAddress()); + } + } + + private static class ModelState { + final EncodedStringsOptions options; + Collection previousData; + EncodedStringsFilterStats stats = new EncodedStringsFilterStats(); + + ModelState(EncodedStringsOptions options, Collection previousData) { + this.options = options; + this.previousData = previousData; + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsThreadedTablePanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsThreadedTablePanel.java new file mode 100644 index 0000000000..3c134bdd18 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsThreadedTablePanel.java @@ -0,0 +1,64 @@ +/* ### + * 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.strings; + +import java.awt.BorderLayout; +import java.awt.Component; + +import docking.widgets.table.threaded.ThreadedTableModel; +import ghidra.util.table.GhidraThreadedTablePanel; + +/** + * A Ghidra table panel that can show a custom overlay instead of an empty table. + * + * @param table row type + */ +class EncodedStringsThreadedTablePanel extends GhidraThreadedTablePanel { + + Component emptyTableOverlayComponent; + Component previousCenterComponent; + + public EncodedStringsThreadedTablePanel(ThreadedTableModel model, int minUpdateDelay, + Component emptyTableOverlayComponent) { + super(model, minUpdateDelay); + this.emptyTableOverlayComponent = emptyTableOverlayComponent; + } + + public void showEmptyTableOverlay(boolean b) { + BorderLayout layout = (BorderLayout) getLayout(); + if (previousCenterComponent == null) { + previousCenterComponent = layout.getLayoutComponent(BorderLayout.CENTER); + } + Component currentCenterComponent = layout.getLayoutComponent(BorderLayout.CENTER); + + if (b) { + if (currentCenterComponent != emptyTableOverlayComponent) { + remove(previousCenterComponent); + add(emptyTableOverlayComponent, BorderLayout.CENTER); + } + } + else { + if (currentCenterComponent != previousCenterComponent) { + remove(emptyTableOverlayComponent); + add(previousCenterComponent, BorderLayout.CENTER); + } + } + + invalidate(); + repaint(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfo.java new file mode 100644 index 0000000000..6a175c88aa --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfo.java @@ -0,0 +1,76 @@ +/* ### + * 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.strings; + +import java.lang.Character.UnicodeScript; +import java.util.*; + +import ghidra.util.StringUtilities; + +/** + * Information about a string. + * + * @param stringValue string itself + * @param scripts set of scripts (alphabets) that the string is made of + * @param stringFeatures set of informational flags about various conditions found in the string + */ +public record StringInfo( + String stringValue, + Set scripts, + Set stringFeatures) { + + private static final Set STD_CTRL_CHARS = Set.of('\n', '\t', '\r'); + + /** + * Creates a {@link StringInfo} instance + * + * @param s string + * @return new {@link StringInfo} instance + */ + public static StringInfo fromString(String s) { + s = Objects.requireNonNullElse(s, ""); + EnumSet scripts = EnumSet.noneOf(UnicodeScript.class); + EnumSet features = EnumSet.noneOf(StringInfoFeature.class); + + s.codePoints().forEach(codePoint -> { + try { + UnicodeScript script = Character.UnicodeScript.of(codePoint); + scripts.add(script); + + if (codePoint == StringUtilities.UNICODE_REPLACEMENT) { + features.add(StringInfoFeature.CODEC_ERROR); + } + if ((codePoint < 32 && !STD_CTRL_CHARS.contains((char) codePoint)) || + !Character.isDefined(codePoint)) { + features.add(StringInfoFeature.NON_STD_CTRL_CHARS); + } + } + catch (IllegalArgumentException e) { + // ignore this codepoint + } + }); + return new StringInfo(s, scripts, features); + } + + public boolean hasCodecError() { + return stringFeatures.contains(StringInfoFeature.CODEC_ERROR); + } + + public boolean hasNonStdCtrlChars() { + return stringFeatures.contains(StringInfoFeature.NON_STD_CTRL_CHARS); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfoFeature.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfoFeature.java new file mode 100644 index 0000000000..4210e4b803 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringInfoFeature.java @@ -0,0 +1,21 @@ +/* ### + * 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.strings; + +public enum StringInfoFeature { + CODEC_ERROR, + NON_STD_CTRL_CHARS +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringTrigramIterator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringTrigramIterator.java new file mode 100644 index 0000000000..c401c13a43 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/StringTrigramIterator.java @@ -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.app.plugin.core.strings; + +import java.util.Iterator; + +/** + * Splits a string into trigrams + */ +public class StringTrigramIterator implements Iterator { + private final String s; + private int index = 0; + private int prevCodePoints[] = new int[2]; + + public StringTrigramIterator(String s) { + // throw away string if length is less than 3 + this.s = s.codePointCount(0, s.length()) >= 3 ? s : null; + if (hasNext()) { + next(); // throw away first value which will be "\0, \0, first char" + } + } + + @Override + public boolean hasNext() { + return s != null && index <= s.length(); + } + + @Override + public Trigram next() { + int codePoint = index >= s.length() ? '\0' : s.codePointAt(index); + index += Character.charCount(codePoint); + + Trigram result = + new Trigram(new int[] { prevCodePoints[0], prevCodePoints[1], codePoint }); + prevCodePoints[0] = prevCodePoints[1]; + prevCodePoints[1] = codePoint; + return result; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/Trigram.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/Trigram.java new file mode 100644 index 0000000000..73750967a1 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/Trigram.java @@ -0,0 +1,174 @@ +/* ### + * 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.strings; + +import java.io.IOException; +import java.util.*; + +/** + * Three (3) adjacent characters, with \0 being reserved for start and end of string magic values. + * + * @param codePoints 3 characters (as int32 code points) + */ +public record Trigram(int[] codePoints) implements Comparable { + + public static Trigram of(int cp1, int cp2, int cp3) { + return new Trigram(new int[] { cp1, cp2, cp3 }); + } + + public static Trigram fromStringRep(String s1, String s2, String s3) + throws NumberFormatException, IOException { + return Trigram.of(decodeCodePoint(s1), decodeCodePoint(s2), decodeCodePoint(s3)); + } + + public static StringTrigramIterator iterate(String s) { + return new StringTrigramIterator(s); + } + + public String toCharSeq() { + return getCodePointRepresentation(codePoints[0]) + + getCodePointRepresentation(codePoints[1]) + + getCodePointRepresentation(codePoints[2]); + } + + @Override + public String toString() { + return toCharSeq(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(codePoints); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Trigram other = (Trigram) obj; + return Arrays.equals(codePoints, other.codePoints); + } + + @Override + public int compareTo(Trigram o) { + int result = Integer.compare(codePoints[0], o.codePoints[0]); + result = result == 0 ? Integer.compare(codePoints[1], o.codePoints[1]) : result; + result = result == 0 ? Integer.compare(codePoints[2], o.codePoints[2]) : result; + return result; + } + //-------------------------------------------------------------------------------------------- + private static final String START_OF_STRING = "[^]"; + + private static final String END_OF_STRING = "[$]"; + private static final Set META_CHARS = Set.of(START_OF_STRING, END_OF_STRING); + private static final Map descriptionToCodePoint = new HashMap<>(); + private static final Map codePointToDescription = new HashMap<>(); + + private static void mapCP(String desc, int codePoint) { + descriptionToCodePoint.put(desc, codePoint); + codePointToDescription.put(codePoint, desc); + } + static { + mapCP("[NUL]", 0); + mapCP("[SOH]", 1); + mapCP("[STX]", 2); + mapCP("[ETX]", 3); + mapCP("[EOT]", 4); + mapCP("[ENQ]", 5); + mapCP("[ACK]", 6); + mapCP("[BEL]", 7); + mapCP("[BS]", 8); + mapCP("[HT]", 9); + mapCP("[LF]", 10); + mapCP("[VT]", 11); + mapCP("[FF]", 12); + mapCP("[CR]", 13); + mapCP("[SO]", 14); + mapCP("[SI]", 15); + mapCP("[DLE]", 16); + mapCP("[DC1]", 17); + mapCP("[DC2]", 18); + mapCP("[DC3]", 19); + mapCP("[DC4]", 20); + mapCP("[NAK]", 21); + mapCP("[SYN]", 22); + mapCP("[ETB]", 23); + mapCP("[CAN]", 24); + mapCP("[EM]", 25); + mapCP("[SUB]", 26); + mapCP("[ESC]", 27); + mapCP("[FS]", 28); + mapCP("[GS]", 29); + mapCP("[RS]", 30); + mapCP("[US]", 31); + mapCP("[SP]", 32); + mapCP("[DEL]", 127); + } + + static String getCodePointRepresentation(int codePoint) { + if (codePoint >= 33 && codePoint <= 126) { + return Character.toString(codePoint); + } + String result = codePointToDescription.get(codePoint); + if (result != null) { + return result; + } + return codePoint > 0 && codePoint <= 0xFFFF + ? "\\u%04X".formatted(codePoint) + : "\\U%08X".formatted(codePoint); + } + + private static int decodeCodePoint(String rep) throws IOException, NumberFormatException { + if (rep == null || rep.isEmpty()) { + throw new IOException("Invalid character symbol in model file"); + } + if (rep.codePointCount(0, rep.length()) == 1) { + return rep.codePointAt(0); + } + if (rep.length() == 3 && META_CHARS.contains(rep)) { + // convert $, ^ (start-of-line, end-of-line) to null char + return '\0'; + } + if (rep.length() == 6 && rep.startsWith("\\u")) { + // "\uFFFF" + return Integer.parseUnsignedInt(rep, 2, 6, 16); + } + if (rep.length() == 10 && rep.startsWith("\\U")) { + // "\uFFFFFFFF" + return Integer.parseUnsignedInt(rep, 2, 10, 16); + } + if (rep.startsWith("[")) { + // one of the "[xx]" codes + Integer codePoint = descriptionToCodePoint.get(rep); + if (codePoint == null) { + throw new IOException("Can not parse character " + rep + " in model file"); + } + return codePoint; + } + return rep.codePointAt(0); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/TrigramStringValidator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/TrigramStringValidator.java new file mode 100644 index 0000000000..9cb94a714e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/TrigramStringValidator.java @@ -0,0 +1,250 @@ +/* ### + * 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.strings; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Function; + +import generic.jar.ResourceFile; +import ghidra.app.services.*; + +/** + * A {@link StringValidatorService} that uses precomputed trigram frequencies from + * a ".sng" model file to score strings. + */ +public class TrigramStringValidator implements StringValidatorService { + /** + * Remove this flag when the trigram model thresholds have been recalculated + */ + @Deprecated(forRemoval = true, since = "10.3") + private static final boolean PRESERVE_BUG_SKIP_TRIGRAM = true; + + // "Bad" log to be used as default score when we know the string is bad + private static final double DEFAULT_LOG_VALUE = -20d; + private static final double INVALID_THRESHOLD = 10.0; + + public static TrigramStringValidator read(ResourceFile f) throws IOException { + return readModel(f); + } + + private ResourceFile sourceFile; + private Map trigramLogs; + private long totalNumTrigrams; + private Function modelValueTransformer; + private double[] thresholds; // for string lengths [4..nn] + + public TrigramStringValidator(Map trigramLogs, long totalNumTrigrams, + Function modelValueTransformer, double[] thresholds, + ResourceFile sourceFile) { + this.trigramLogs = trigramLogs; + this.totalNumTrigrams = totalNumTrigrams; + this.modelValueTransformer = modelValueTransformer; + this.thresholds = thresholds; + this.sourceFile = sourceFile; + } + + public ResourceFile getSourceFile() { + return sourceFile; + } + + @Override + public String getValidatorServiceName() { + return "ngram"; + } + + @Override + public StringValidityScore getStringValidityScore(StringValidatorQuery query) { + String transformedString = modelValueTransformer.apply(query.stringValue()); + double score = DEFAULT_LOG_VALUE; + int trigramCount = 0; + + StringTrigramIterator it = Trigram.iterate(transformedString); + if (it.hasNext()) { + double missingTrigramScore = Math.log10(1d / totalNumTrigrams); + score = 0; + for (; it.hasNext();) { + Trigram trigram = it.next(); + trigramCount++; + if (PRESERVE_BUG_SKIP_TRIGRAM && trigramCount == 2) { + // compatibility hack to replicate trigram bug in old code + continue; + } + Double logProb = trigramLogs.get(trigram); + if (logProb == null) { + logProb = missingTrigramScore; + } + score += logProb; + } + score = score / trigramCount; + } + + return new StringValidityScore(query.stringValue(), transformedString, score, + getThresholdForStringOfLength(trigramCount)); + } + + public long getTotalNumTrigrams() { + return totalNumTrigrams; + } + + + public Iterator dumpModel() { + return trigramLogs.keySet() + .stream() + .sorted() + .map(trigram -> "%s=%s".formatted(trigram.toCharSeq(), trigramLogs.get(trigram))) + .iterator(); + } + + private double getThresholdForStringOfLength(int len) { + int index = len - 4; + if (index < 0) { + return INVALID_THRESHOLD; + } + if (index >= thresholds.length) { + index = thresholds.length - 1; + } + return thresholds[index]; + } + + //--------------------------------------------------------------------------------------------- + + private static TrigramStringValidator readModel(ResourceFile sourceFile) throws IOException { + Map counts = new HashMap<>(); + long totalTrigrams = 0; + String modelType = null; + double[] thresholds = null; + int symbolSize = 128; // default + int lineNum = 0; + boolean inFileHeaderSection = true; + + String currString = ""; + try (BufferedReader br = new BufferedReader( + new InputStreamReader(sourceFile.getInputStream(), StandardCharsets.UTF_8))) { + while ((currString = br.readLine()) != null) { + lineNum++; + if (currString.isBlank()) { + continue; + } + if (inFileHeaderSection && currString.startsWith("#")) { + String[] headerFields = parseHeaderLine(currString.substring(1).trim()); + if (headerFields != null) { + switch (headerFields[0]) { + case "Model Type": + modelType = headerFields[1]; + break; + case "Thresholds": + thresholds = parseThresholds(headerFields[1]); + break; + case "Symbol Size": + symbolSize = Integer.parseInt(headerFields[1]); + break; + } + } + continue; + } + + inFileHeaderSection = false; + + String[] lineParts = currString.split("\\t"); + if (lineParts.length != 4) { + throw new IOException("Invalid field count in ngram %s:%d: %s" + .formatted(sourceFile.getName(), lineNum, currString)); + } + + Trigram trigram = Trigram.fromStringRep(lineParts[0], lineParts[1], lineParts[2]); + int currCount = Integer.parseInt(lineParts[3]); + + int[] codePoints = trigram.codePoints(); + if (codePoints[1] == 0 || (codePoints[0] == 0 && codePoints[2] == 0)) { + // if invalid combination of start-of-string, end-of-string markers + continue; + } + + counts.merge(trigram, currCount, (oldVal, newVal) -> oldVal + newVal); + totalTrigrams += currCount; + } + + // fixup missing trigram elements + int trigramEntryCount = counts.size(); + + // fully populated trigram mappings would be symbolsize^3, but due to quirk of old + // code, we also have the special start-of-string and end-of-string doublets to count. + int expectedEntryCount = // symbolSize^3 + (symbolSize^2)*2 + (symbolSize * symbolSize * symbolSize) + (symbolSize * symbolSize * 2); + + totalTrigrams += (expectedEntryCount - trigramEntryCount); + + Map logProb = calculateLogProbs(counts, totalTrigrams); + modelType = Objects.requireNonNullElse(modelType, ""); + Function transformer = getStringTransformer(modelType); + + // normalize whitespace (in addition to whatever the transformer does) + transformer = transformer + .andThen(s -> s.trim().replaceAll(" {2,}", " ").replaceAll("\t{2,}", "\t")); + + return new TrigramStringValidator(logProb, totalTrigrams, transformer, thresholds, + sourceFile); + } + catch (NumberFormatException nfe) { + throw new IOException( + "Error parsing string ngram %s:%d: %s".formatted(sourceFile.getName(), lineNum, + currString)); + } + } + + private static Function getStringTransformer(String modelTypeName) { + Function transformer = switch (modelTypeName) { + case "lowercase" -> String::toLowerCase; + default -> Function.identity(); + }; + return transformer; + } + + private static String[] parseHeaderLine(String s) { + int colon = s.indexOf(':'); + return colon > 0 + ? new String[] { s.substring(0, colon).trim(), s.substring(colon + 1).trim() } + : null; + } + + private static double[] parseThresholds(String s) { + String[] parts = s.split(","); + double[] results = new double[parts.length]; + for (int i = 0; i < parts.length; i++) { + String thresholdValStr = parts[i]; + double d = Double.parseDouble(thresholdValStr.trim()); + results[i] = d; + } + return results; + } + + private static Map calculateLogProbs(Map counts, + long totalTrigrams) { + + double totalTrigramsD = totalTrigrams; + Map logTrigrams = new HashMap<>(); + for (Entry entry : counts.entrySet()) { + Trigram trigram = entry.getKey(); + Integer count = entry.getValue(); + logTrigrams.put(trigram, Math.log10(count / totalTrigramsD)); + } + return logTrigrams; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/UndefinedStringIterator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/UndefinedStringIterator.java new file mode 100644 index 0000000000..dc5a2e0418 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/UndefinedStringIterator.java @@ -0,0 +1,260 @@ +/* ### + * 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.strings; + +import java.util.Iterator; + +import ghidra.docking.settings.Settings; +import ghidra.program.model.address.*; +import ghidra.program.model.data.AbstractStringDataType; +import ghidra.program.model.data.StringDataInstance; +import ghidra.program.model.listing.*; +import ghidra.program.model.mem.*; +import ghidra.util.task.TaskMonitor; + +/** + * Iterator that searches for locations that could be strings and returns + * {@link StringDataInstance}s representing those locations. + */ +public class UndefinedStringIterator + implements Iterator, Iterable { + private static final int MAX_SANE_STRING_LENGTH = 1024 * 1024; // 1mb + + static AddressSet getSingleStringEndAddrRange(Program program, AddressSetView addrs) { + Address minAddr = addrs.getMinAddress(); + MemoryBlock memblock = program.getMemory().getBlock(minAddr); + Address endAddr = memblock != null ? memblock.getEnd() : minAddr; + if (endAddr.subtract(minAddr) > MAX_SANE_STRING_LENGTH) { + endAddr = minAddr.add(MAX_SANE_STRING_LENGTH); + } + return new AddressSet(minAddr, endAddr); + } + + private final TaskMonitor monitor; + private final Listing listing; + private final Program program; + private final Memory memory; + private final AddressSet addrs; + private final int charSize; + private final int charAlignment; + private final boolean breakOnRef; + private final Address singleStringStart; + private final AbstractStringDataType stringDataType; + private final Settings stringSettings; + private final long origAddrCount; + private final byte[] buffer = new byte[64]; + private StringDataInstance currentItem; + + /** + * Creates a new UndefinedStringIterator instance. + * + * @param program {@link Program} + * @param addrs set of {@link Address}es to search. + * @param charSize size of the characters (and the null-terminator) that make up the string + * @param charAlignment alignment requirements for the start of the string + * @param breakOnRef boolean flag, if true strings will be terminated early at locations that + * have an in-bound memory reference + * @param singleStringMode boolean flag, if true only one string will be returned, and it must + * be located at the start of the specified address set (after alignment tweaks) + * @param stringDataType a string data type that corresponds to the type of string being + * searched for + * @param stringSettings {@link Settings} for the string data type + * @param monitor {@link TaskMonitor} + */ + public UndefinedStringIterator(Program program, AddressSetView addrs, int charSize, + int charAlignment, boolean breakOnRef, boolean singleStringMode, + AbstractStringDataType stringDataType, Settings stringSettings, TaskMonitor monitor) { + this.program = program; + this.listing = program.getListing(); + this.memory = program.getMemory(); + this.addrs = new AddressSet(addrs); + this.charSize = charSize; + this.charAlignment = charAlignment; + this.breakOnRef = breakOnRef; + this.singleStringStart = singleStringMode ? addrs.getMinAddress() : null; + this.stringDataType = stringDataType; + this.stringSettings = stringSettings; + this.monitor = monitor; + this.origAddrCount = addrs.getNumAddresses(); + monitor.initialize(origAddrCount); + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + if (currentItem == null) { + currentItem = findNext(); + } + return currentItem != null; + } + + @Override + public StringDataInstance next() { + StringDataInstance result = currentItem; + currentItem = null; + return result; + } + + private StringDataInstance findNext() { + forceAlignment(); + while (!addrs.isEmpty()) { + if (monitor.isCancelled()) { + return null; + } + + if (!findStartOfString()) { + break; + } + + monitor.setProgress(origAddrCount - addrs.getNumAddresses()); + + Address addr = addrs.getMinAddress(); + Data undefData = listing.getDataAt(addr); + if (undefData == null) { + break; + } + Address eos = findEndOfString(); + if (monitor.isCancelled()) { + return null; + } + + addrs.deleteFromMin(eos); + long length = eos.subtract(addr) + 1; + if (length < charSize || length > MAX_SANE_STRING_LENGTH) { + // throw away, try next string + continue; + } + + StringDataInstance sdi = + stringDataType.getStringDataInstance(undefData, stringSettings, (int) length); + return sdi; + + } + return null; + } + + private void forceAlignment() { + while (!addrs.isEmpty() && addrs.getMinAddress().getOffset() % charAlignment != 0) { + addrs.deleteFromMin(addrs.getMinAddress()); + } + } + + private boolean findStartOfString() { + return consumeNullTerms() && !addrs.isEmpty(); + } + + private Address findEndOfString() { + // search for an end-of-string location + // 1) null terminator + // 2) inbound ref + // 3) end-of-memory-block + Address max = addrs.getFirstRange().getMaxAddress(); + Address bufStart = addrs.getFirstRange().getMinAddress(); + try { + do { + Address refdAddr = breakOnRef ? getNextRefdAddr(bufStart, max) : null; + if (refdAddr != null) { + max = refdAddr; + } + + int bytesToRead = (int) Math.min(buffer.length, max.subtract(bufStart) + 1); + int bytesRead = memory.getBytes(bufStart, buffer, 0, bytesToRead); + if (bytesRead <= 0) { + break; + } + for (int nullIndex = 0; nullIndex <= bytesRead - charSize; nullIndex += charSize) { + if (isNullChar(nullIndex)) { + // found a null term char, return it (inclusive) + return bufStart.addNoWrap(nullIndex + charSize - 1); + } + } + + if (refdAddr != null) { + // always terminate if there was a inbound ref + return refdAddr.previous(); + } + + // loop and read next chunk and try again + bufStart = bufStart.addNoWrap(bytesRead); + } + while (bufStart.compareTo(max) <= 0); + } + catch (MemoryAccessException | AddressOverflowException e) { + // terminate loop/method + } + return max; + } + + private boolean isNullChar(int index) { + for (int i = 0; i < charSize; i++) { + if (buffer[index + i] != 0) { + return false; + } + } + return true; + } + + private Address getNextRefdAddr(Address start, Address end) { + AddressIterator it = program.getReferenceManager() + .getReferenceDestinationIterator(new AddressSet(start, end), true); + Address refdAddr = null; + if (it.hasNext()) { + refdAddr = it.next(); + if (start.equals(refdAddr)) { + refdAddr = it.hasNext() ? it.next() : null; + } + } + return refdAddr; + } + + private boolean consumeNullTerms() { + try { + if (memory.getByte(addrs.getMinAddress()) == 0) { + int bytesRead; + while (!addrs.isEmpty() && !monitor.isCancelled() && + (bytesRead = memory.getBytes(addrs.getMinAddress(), buffer, 0, + (int) Math.min(buffer.length, addrs.getFirstRange().getLength()))) > 0) { + + int nonNullIndex; + for (nonNullIndex = 0; nonNullIndex < bytesRead; nonNullIndex++) { + if (buffer[nonNullIndex] != 0) { + nonNullIndex -= nonNullIndex % charSize; + break; + } + } + if (nonNullIndex > 0) { + addrs.deleteFromMin(addrs.getMinAddress().add(nonNullIndex - 1)); + } + if (nonNullIndex < bytesRead) { + break; + } + } + } + } + catch (MemoryAccessException e) { + // terminate loop/method + } + if (singleStringStart != null && + Math.abs(singleStringStart.subtract(addrs.getMinAddress())) >= charAlignment) { + return false; + } + return true; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsPlugin.java index d840d2a72d..635a4598b4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsPlugin.java @@ -113,56 +113,57 @@ public class ViewStringsPlugin extends ProgramPlugin implements DomainObjectList DockingAction editDataSettingsAction = new DockingAction("Data Settings", getName(), KeyBindingType.SHARED) { - @Override - public void actionPerformed(ActionContext context) { - try { - DataSettingsDialog dialog = provider.getSelectedRowCount() == 1 - ? new DataSettingsDialog(provider.getSelectedData()) - : new DataSettingsDialog(currentProgram, provider.getProgramSelection()); + @Override + public void actionPerformed(ActionContext context) { + try { + DataSettingsDialog dialog = provider.getSelectedRowCount() == 1 + ? new DataSettingsDialog(provider.getSelectedData()) + : new DataSettingsDialog(currentProgram, + provider.getProgramSelection()); - tool.showDialog(dialog); - dialog.dispose(); + tool.showDialog(dialog); + dialog.dispose(); + } + catch (CancelledException e) { + // do nothing + } } - catch (CancelledException e) { - // do nothing - } - } - }; + }; editDataSettingsAction.setPopupMenuData(new MenuData(new String[] { "Settings..." }, "R")); editDataSettingsAction.setHelpLocation(new HelpLocation("DataPlugin", "Data_Settings")); DockingAction editDefaultSettingsAction = new DockingAction("Default Settings", getName(), KeyBindingType.SHARED) { - @Override - public void actionPerformed(ActionContext context) { - DataType dt = getSelectedDataType(); - if (dt == null) { - return; + @Override + public void actionPerformed(ActionContext context) { + DataType dt = getSelectedDataType(); + if (dt == null) { + return; + } + DataTypeSettingsDialog dataSettingsDialog = + new DataTypeSettingsDialog(dt, dt.getSettingsDefinitions()); + tool.showDialog(dataSettingsDialog); + dataSettingsDialog.dispose(); } - DataTypeSettingsDialog dataSettingsDialog = - new DataTypeSettingsDialog(dt, dt.getSettingsDefinitions()); - tool.showDialog(dataSettingsDialog); - dataSettingsDialog.dispose(); - } - @Override - public boolean isEnabledForContext(ActionContext context) { - if (provider.getSelectedRowCount() != 1) { - return false; + @Override + public boolean isEnabledForContext(ActionContext context) { + if (provider.getSelectedRowCount() != 1) { + return false; + } + DataType dt = getSelectedDataType(); + if (dt == null) { + return false; + } + return dt.getSettingsDefinitions().length != 0; } - DataType dt = getSelectedDataType(); - if (dt == null) { - return false; - } - return dt.getSettingsDefinitions().length != 0; - } - private DataType getSelectedDataType() { - Data data = provider.getSelectedData(); - return data != null ? data.getDataType() : null; - } - }; + private DataType getSelectedDataType() { + Data data = provider.getSelectedData(); + return data != null ? data.getDataType() : null; + } + }; editDefaultSettingsAction.setPopupMenuData( new MenuData(new String[] { "Default Settings..." }, "R")); editDefaultSettingsAction.setHelpLocation( diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsTableModel.java index 9f1d95aff9..6afbb48000 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/ViewStringsTableModel.java @@ -15,8 +15,9 @@ */ package ghidra.app.plugin.core.strings; -import java.util.HashMap; -import java.util.Map; +import java.lang.Character.UnicodeScript; +import java.util.*; +import java.util.stream.Collectors; import docking.widgets.table.DynamicTableColumn; import docking.widgets.table.TableColumnDescriptor; @@ -60,7 +61,8 @@ class ViewStringsTableModel extends AddressBasedTableModel { DATA_TYPE_COL, IS_ASCII_COL, CHARSET_COL, - HAS_ENCODING_ERROR + HAS_ENCODING_ERROR, + UNICODE_SCRIPT } ViewStringsTableModel(PluginTool tool) { @@ -95,6 +97,7 @@ class ViewStringsTableModel extends AddressBasedTableModel { descriptor.addHiddenColumn(new IsAsciiColumn()); descriptor.addHiddenColumn(new CharsetColumn()); descriptor.addHiddenColumn(new HasEncodingErrorColumn()); + descriptor.addHiddenColumn(new UnicodeScriptColumn()); return descriptor; } @@ -362,8 +365,9 @@ class ViewStringsTableModel extends AddressBasedTableModel { Data data = DataUtilities.getDataAtLocation(rowObject); String s = StringDataInstance.getStringDataInstance(data).getStringValue(); - return (s != null) && s.chars().anyMatch( - codePoint -> codePoint == StringUtilities.UNICODE_REPLACEMENT); + return (s != null) && s.codePoints() + .anyMatch( + codePoint -> codePoint == StringUtilities.UNICODE_REPLACEMENT); } @Override @@ -398,4 +402,35 @@ class ViewStringsTableModel extends AddressBasedTableModel { } + private static class UnicodeScriptColumn + extends AbstractProgramLocationTableColumn { + + @Override + public String getColumnName() { + return "Unicode Script"; + } + + @Override + public String getValue(ProgramLocation rowObject, Settings settings, Program program, + ServiceProvider serviceProvider) throws IllegalArgumentException { + + Data data = DataUtilities.getDataAtLocation(rowObject); + String s = StringDataInstance.getStringDataInstance(data).getStringValue(); + s = Objects.requireNonNullElse(s, ""); + StringInfo stringInfo = StringInfo.fromString(s); + Set scripts = stringInfo.scripts(); + scripts.removeAll(CharacterScriptUtils.IGNORED_SCRIPTS); + String formattedColStr = + scripts.stream().map(UnicodeScript::name).collect(Collectors.joining(",")); + + return formattedColStr; + } + + @Override + public ProgramLocation getProgramLocation(ProgramLocation rowObject, Settings settings, + Program program, ServiceProvider serviceProvider) { + return rowObject; + } + + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringTranslationService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringTranslationService.java index 923de0e772..8164a28ff1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringTranslationService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringTranslationService.java @@ -15,9 +15,10 @@ */ package ghidra.app.services; -import java.util.List; +import java.util.*; import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginDescription; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; @@ -30,6 +31,21 @@ import ghidra.util.HelpLocation; * and then registered via {@link Plugin}'s registerServiceProvided(). */ public interface StringTranslationService { + /** + * Returns a sorted list of the currently enabled StringTranslationService service providers. + * + * @param tool {@link PluginTool} + * @return sorted list of currently enabled StringTranslationServices + */ + public static List getCurrentStringTranslationServices( + PluginTool tool) { + List translationServices = + new ArrayList<>(Arrays.asList(tool.getServices(StringTranslationService.class))); + Collections.sort(translationServices, + (s1, s2) -> s1.getTranslationServiceName().compareTo(s2.getTranslationServiceName())); + return translationServices; + } + /** * Returns the name of this translation service. Used when building menus to allow * the user to pick a translation service. @@ -56,7 +72,12 @@ public interface StringTranslationService { * @param program the program containing the data instances. * @param stringLocations {@link List} of string locations. */ - public void translate(Program program, List stringLocations); + public void translate(Program program, List stringLocations, + TranslateOptions options); + + public record TranslateOptions(boolean autoTranslate) { + public static TranslateOptions NONE = new TranslateOptions(false); + }; /** * Helper that creates a {@link HelpLocation} based on the plugin and sts. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorQuery.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorQuery.java new file mode 100644 index 0000000000..cf9125e1fd --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorQuery.java @@ -0,0 +1,32 @@ +/* ### + * 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.services; + +import ghidra.app.plugin.core.strings.StringInfo; + +public record StringValidatorQuery( + String stringValue, + StringInfo stringCharInfo) { + + public StringValidatorQuery(String stringValue) { + this(stringValue, StringInfo.fromString(stringValue)); + } + + public StringValidatorQuery(String stringValue, StringInfo stringCharInfo) { + this.stringValue = stringValue; + this.stringCharInfo = stringCharInfo; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorService.java new file mode 100644 index 0000000000..84d86ca22f --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidatorService.java @@ -0,0 +1,73 @@ +/* ### + * 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.services; + +import java.util.*; + +import ghidra.framework.plugintool.PluginTool; + +/** + * A service that judges the validity of a string + */ +public interface StringValidatorService { + /** + * Returns a list of string validator services + * + * @param tool {@link PluginTool} + * @return list of services + */ + static List getCurrentStringValidatorServices( + PluginTool tool) { + + List results = + new ArrayList<>(List.of(tool.getServices(StringValidatorService.class))); + Collections.sort(results, + (s1, s2) -> s1.getValidatorServiceName().compareTo(s2.getValidatorServiceName())); + + return results; + } + + StringValidatorService DUMMY = new DummyStringValidator(); + + /** + * Returns the name of the service + * + * @return + */ + String getValidatorServiceName(); + + /** + * Judges a string (specified in the query instance). + * + * @param query {@link StringValidatorQuery} + * @return {@link StringValidityScore} + */ + StringValidityScore getStringValidityScore(StringValidatorQuery query); + + static class DummyStringValidator implements StringValidatorService { + + @Override + public String getValidatorServiceName() { + return "Dummy"; + } + + @Override + public StringValidityScore getStringValidityScore(StringValidatorQuery query) { + return StringValidityScore.makeDummyFor(query.stringValue()); + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidityScore.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidityScore.java new file mode 100644 index 0000000000..fb1f9e02d0 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/StringValidityScore.java @@ -0,0 +1,40 @@ +/* ### + * 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.services; + +/** + * Result of a {@link StringValidatorService}'s judgment about a string. + * + * @param originalString string being scored + * @param transformedString original string, after being tweaked + * @param score string's validity score, larger values are more valid + * @param threshold score that this string would need to exceed to be considered valid + */ +public record StringValidityScore( + String originalString, + String transformedString, + double score, + double threshold) { + + public static StringValidityScore makeDummyFor(String s) { + return new StringValidityScore(s, s, 0, 100); + } + + public boolean isScoreAboveThreshold() { + return score > threshold; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/strings/TrigramStringValidatorTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/strings/TrigramStringValidatorTest.java new file mode 100644 index 0000000000..7fac082265 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/strings/TrigramStringValidatorTest.java @@ -0,0 +1,65 @@ +/* ### + * 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.strings; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.junit.Before; + +import generic.jar.ResourceFile; +import ghidra.app.plugin.core.string.NGramUtils; +import ghidra.app.plugin.core.string.StringAndScores; +import ghidra.app.services.StringValidatorQuery; +import ghidra.app.services.StringValidityScore; +import ghidra.framework.Application; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import utilities.util.FileUtilities; + +public class TrigramStringValidatorTest extends AbstractGhidraHeadlessIntegrationTest { + + TrigramStringValidator ngramValidator; + + @Before + public void setup() throws IOException { + ResourceFile stringModelFile = + Application.findDataFileInAnyModule("stringngrams/StringModel.sng"); + NGramUtils.startNewSession("StringModel.sng", true); + ngramValidator = TrigramStringValidator.read(stringModelFile); + } + + private void assertSameStringScore(String s) { + StringValidityScore score = + ngramValidator.getStringValidityScore(new StringValidatorQuery(s)); + + StringAndScores sas = new StringAndScores(s, true); + NGramUtils.scoreString(sas); + + assertEquals(sas.getScoreThreshold(), score.threshold(), 0.0); + assertEquals(sas.isScoreAboveThreshold(), score.isScoreAboveThreshold()); + } + + //@Test + public void testCompareOldAndNewScoring() throws IOException { + List lines = FileUtilities.getLines(new File("lotsofstrings.txt")); + for (String s : lines) { + assertSameStringScore(s); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java index 98f8c732f1..c08e4aaaa4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java @@ -15,11 +15,12 @@ */ package docking.widgets.spinner; +import java.util.ArrayList; +import java.util.List; + import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; @@ -43,10 +44,20 @@ public class IntegerSpinner { * @param spinnerModel the spinner model to use in the JSpinner. */ public IntegerSpinner(SpinnerNumberModel spinnerModel) { + this(spinnerModel, 10); + } + + /** + * Creates a new IntegerSpinner using the given spinner model. + * + * @param spinnerModel the spinner model to use in the JSpinner. + */ + public IntegerSpinner(SpinnerNumberModel spinnerModel, int columns) { spinner = new JSpinner(spinnerModel); - integerTextField = new IntegerTextField(10, ((Number) spinnerModel.getValue()).longValue()); + integerTextField = + new IntegerTextField(columns, ((Number) spinnerModel.getValue()).longValue()); integerTextField.getComponent().setName("integer.spinner.editor"); Number maximum = (Number) spinnerModel.getMaximum(); integerTextField.setMaxValue( diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java index 971269c96f..ad9238cc66 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java @@ -142,7 +142,7 @@ public abstract class GDynamicColumnTableModel } } - private TableColumnDescriptor getTableColumnDescriptor() { + protected TableColumnDescriptor getTableColumnDescriptor() { if (columnDescriptor == null) { columnDescriptor = createTableColumnDescriptor(); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java index cb21172b1b..b07349087f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java @@ -26,8 +26,7 @@ import java.util.*; import generic.stl.Pair; import ghidra.docking.settings.*; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressOutOfBoundsException; +import ghidra.program.model.address.*; import ghidra.program.model.data.RenderUnicodeSettingsDefinition.RENDER_ENUM; import ghidra.program.model.data.StringRenderParser.StringParseException; import ghidra.program.model.lang.Endian; @@ -380,6 +379,19 @@ public class StringDataInstance { return buf.getAddress(); } + public Address getEndAddress() { + try { + return length > 0 ? buf.getAddress().addNoWrap(length - 1) : buf.getAddress(); + } + catch (AddressOverflowException e) { + return buf.getAddress(); + } + } + + public AddressRange getAddressRange() { + return new AddressRangeImpl(getAddress(), getEndAddress()); + } + private boolean isBadCharSize() { return (paddedCharSize < 1 || paddedCharSize > 8) || !(charSize == 1 || charSize == 2 || charSize == 4) || (paddedCharSize < charSize); diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/EncodedStringsDialogScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/EncodedStringsDialogScreenShots.java new file mode 100644 index 0000000000..d7553f7c20 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/EncodedStringsDialogScreenShots.java @@ -0,0 +1,114 @@ +/* ### + * 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 help.screenshot; + +import java.nio.charset.StandardCharsets; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.app.plugin.core.strings.EncodedStringsDialog; +import ghidra.app.plugin.core.strings.EncodedStringsPlugin; +import ghidra.app.services.ProgramManager; +import ghidra.app.util.HelpTopics; +import ghidra.test.ToyProgramBuilder; +import ghidra.util.Swing; + +public class EncodedStringsDialogScreenShots extends GhidraScreenShotGenerator { + + private EncodedStringsPlugin plugin; + + public EncodedStringsDialogScreenShots() { + super(); + } + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + plugin = env.addPlugin(EncodedStringsPlugin.class); + } + + @Override + public void loadProgram() throws Exception { + + ToyProgramBuilder builder = new ToyProgramBuilder("String Examples", false); + builder.createMemory("RAM", "0x0", 0x2000); + + builder.createString("0x100", "Hello World!\n", StandardCharsets.US_ASCII, true, null); + + builder.createString("0x150", bytes(0, 1, 2, 3, 4, 0x80, 0x81, 0x82, 0x83), + StandardCharsets.US_ASCII, null); + + builder.createString("0x200", "\u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54", + StandardCharsets.UTF_16, true, null); + + builder.createString("0x250", "Exception %s\n\tline: %d\n", StandardCharsets.US_ASCII, true, + null); + + builder.createString("0x330", "A: \u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54", + StandardCharsets.UTF_8, true, null); + + builder.createString("0x450", + "Roses are \u001b[0;31mred\u001b[0m, violets are \u001b[0;34mblue. Hope you enjoy terminal hue", + StandardCharsets.US_ASCII, true, null); + + program = builder.getProgram(); + + runSwing(() -> { + ProgramManager pm = tool.getService(ProgramManager.class); + pm.openProgram(program.getDomainFile()); + }); + + } + + @Override + protected String getHelpTopicName() { + return HelpTopics.SEARCH; + } + + @Test + public void testEncodedStringsDialog_initial() { + positionListingTop(0x50); + makeSelection(0x50, 0x500); + performAction(plugin.getSearchForEncodedStringsAction()); + + EncodedStringsDialog dialog = waitForDialogComponent(EncodedStringsDialog.class); + waitForTableModel(dialog.getStringModel()); + + captureDialog(600, 300); + } + + @Test + public void testEncodedStringsDialog_advancedoptions() { + positionListingTop(0x50); + makeSelection(0x50, 0x500); + performAction(plugin.getSearchForEncodedStringsAction()); + + EncodedStringsDialog dialog = waitForDialogComponent(EncodedStringsDialog.class); + Swing.runNow(() -> { + dialog.setShowAdvancedOptions(true); + dialog.setShowScriptOptions(true); + dialog.setAllowAnyScriptOption(true); + dialog.setRequireValidStringOption(false); + dialog.setSelectedCharset("UTF-8"); + }); + waitForTableModel(dialog.getStringModel()); + + captureDialog(600, 450); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/strings/EncodedStringsDialogTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/strings/EncodedStringsDialogTest.java new file mode 100644 index 0000000000..22d0ded041 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/strings/EncodedStringsDialogTest.java @@ -0,0 +1,183 @@ +/* ### + * 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.strings; + +import static org.junit.Assert.*; + +import java.lang.Character.UnicodeScript; +import java.nio.charset.StandardCharsets; + +import org.junit.*; + +import docking.action.DockingActionIf; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.*; +import ghidra.program.model.data.AbstractStringDataType; +import ghidra.program.model.listing.Data; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.test.*; +import ghidra.util.Swing; + +public class EncodedStringsDialogTest extends AbstractGhidraHeadedIntegrationTest { + private TestEnv env; + private ProgramDB program; + private PluginTool tool; + private MemoryBlock ram; + private DockingActionIf encodedStringsAction; + private EncodedStringsDialog dialog; + private EncodedStringsTableModel tableModel; + private EncodedStringsPlugin plugin; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + program = buildProgram(); + ram = program.getMemory().getBlock("RAM"); + tool = env.launchDefaultTool(program); + plugin = env.addPlugin(EncodedStringsPlugin.class); + encodedStringsAction = plugin.getSearchForEncodedStringsAction(); + } + + private ProgramDB buildProgram() throws Exception { + ToyProgramBuilder builder = new ToyProgramBuilder("String Examples", false); + builder.createMemory("RAM", "0x0", 0x500); + + builder.createString("0x100", "Hello World!\n", StandardCharsets.US_ASCII, true, null); + builder.createString("0x10e", "Next string", StandardCharsets.US_ASCII, true, null); + + builder.createString("0x150", bytes(0, 1, 2, 3, 4, 0x80, 0x81, 0x82, 0x83), + StandardCharsets.US_ASCII, null); + + builder.createString("0x200", "\u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54", + StandardCharsets.UTF_16, true, null); + + builder.createString("0x250", "Exception %s\n\tline: %d\n", StandardCharsets.US_ASCII, true, + null); + + builder.createString("0x330", "A: \u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54", + StandardCharsets.UTF_8, true, null); + + builder.createString("0x450", + "Roses are \u001b[0;31mred\u001b[0m, violets are \u001b[0;34mblue. Hope you enjoy terminal hue", + StandardCharsets.US_ASCII, true, null); + + return builder.getProgram(); + } + + private Address addr(long offset) { + return ram.getStart().getNewAddress(offset); + } + + @After + public void tearDown() throws Exception { + closeDialog(); + env.dispose(); + } + + private void closeDialog() { + if (dialog != null) { + close(dialog); + dialog = null; + tableModel = null; + } + } + + private void showDialog(AddressRange range) { + closeDialog(); + makeSelection(tool, program, range.getMinAddress(), range.getMaxAddress()); + performAction(encodedStringsAction, false); + dialog = waitForDialogComponent(EncodedStringsDialog.class); + tableModel = dialog.getStringModel(); + waitForTableModel(tableModel); + } + + @Test + public void testDefaultUSASCII() { + showDialog(ram.getAddressRange()); + assertEquals(3, tableModel.getRowCount()); + } + + @Test + public void testSingleString() { + showDialog(new AddressRangeImpl(addr(0x100), addr(0x100))); + assertEquals(1, tableModel.getRowCount()); + EncodedStringsRow row0 = tableModel.getRowObject(0); + assertEquals("Hello World!\n", row0.stringInfo().stringValue()); + } + + @Test + public void testUTF8() { + showDialog(ram.getAddressRange()); + Swing.runNow(() -> { + dialog.setSelectedCharset("UTF-8"); + }); + waitForTableModel(tableModel); + assertEquals(4, tableModel.getRowCount()); + } + + @Test + public void testUTF8_Nonstdctrlchars() { + showDialog(ram.getAddressRange()); + Swing.runNow(() -> { + dialog.setShowAdvancedOptions(true); + dialog.setExcludeNonStdCtrlChars(false); + dialog.setSelectedCharset("UTF-8"); + }); + waitForTableModel(tableModel); + assertEquals(5, tableModel.getRowCount()); + } + + @Test + public void testUTF8_HanScript() { + showDialog(ram.getAddressRange()); + Swing.runNow(() -> { + dialog.setShowAdvancedOptions(true); + dialog.setRequireValidStringOption(false); + dialog.setSelectedCharset("UTF-8"); + }); + waitForTableModel(tableModel); + assertEquals(4, tableModel.getRowCount()); + Swing.runNow(() -> { + dialog.setShowScriptOptions(true); + dialog.setShowAdvancedOptions(true); + dialog.setRequireValidStringOption(false); + dialog.setAllowAnyScriptOption(false); + dialog.setAllowLatinScriptOption(true); + dialog.setAllowCommonScriptOption(true); + dialog.setRequiredScript(UnicodeScript.HAN); + }); + waitForTableModel(tableModel); + assertEquals(1, tableModel.getRowCount()); + } + + @Test + public void testCreateString() { + Data data = program.getListing().getDataAt(addr(0x100)); + assertFalse(data.isDefined()); + + showDialog(ram.getAddressRange()); + assertEquals(3, tableModel.getRowCount()); + dialog.getCreateButton().doClick(); + + waitForSwing(); + + data = program.getListing().getDataAt(addr(0x100)); + assertNotNull(data); + assertTrue(data.getDataType() instanceof AbstractStringDataType); + } + +}