/* ### * 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. */ //The script is called from several scripts to use selected instructions with or without operands and constants (depending on the script //that called it) and build a combined mask/value buffer. //Memory is then searched looking for this combined value buffer that represents the selected instructions. //This automates the process of searching through memory for a particular ordering of instructions by hand. //@category Search.InstructionPattern import java.util.ArrayList; import java.util.LinkedHashMap; import ghidra.app.plugin.processors.sleigh.SleighDebugLogger; import ghidra.app.plugin.processors.sleigh.SleighDebugLogger.SleighDebugMode; import ghidra.app.script.GhidraScript; import ghidra.app.script.ImproperUseException; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.program.model.listing.*; import ghidra.program.model.mem.Memory; public class SearchBaseExtended extends GhidraScript { //holds the mask and value for all the mnemonics, or commands like cmp, jmp, jnz etc ArrayList mnemonics = new ArrayList<>(); /* * Holds the masks and values for all the operands. The arraylist portion will correspond to the operand number. An example is * arraylist.get(0) will refer to the first operand. Arraylist.get(1) will return the second operands data. The previous commands will * give you a hashmap that maps the mnemonics MVCase object to its operands, if they exist. For example to get the first operand for the * second mnemonic that was seen you would call arraylist.get(0).get(mnemonicArrayList.get(1)); That will return the MVCase that refers to * operand. I set it up this was to conserve memory and allow for a dynamically growing collection. */ ArrayList> ops = new ArrayList<>();//holds masks and values for all operands. ArrayList db = new ArrayList<>();//holds the search results. //These control the detail at which a scan is performed. //They determine how specific the instructions must match the currently selected ones ArrayList controlList = new ArrayList<>(); @Override public void run() throws Exception { loadSelectedInstructions(); executeSearch(); } public void run(boolean mneonics, boolean op1, boolean op2, boolean constants) { controlList = new ArrayList<>(); controlList.add(new SLMaskControl(mneonics, op1, op2, constants)); loadSelectedInstructions(); executeSearch(); } public void run(ArrayList control) { controlList = control; loadSelectedInstructions(); executeSearch(); } public void clearResults() { db = new ArrayList<>(); } public void setState(SLMaskControl newState) { controlList = new ArrayList<>(); controlList.add(newState); } public void setState(ArrayList newState) { controlList = newState; } public void loadSelectedInstructions() { if (currentProgram == null || currentSelection == null) { return; } try { //Builds object that is used to extract all the instructions masks and values SleighDebugLogger logger = null; //Grabs the AddressRange for the first continuous selection of instructions AddressRange addrRange = currentSelection.getFirstRange(); if (addrRange == null) { return; }//makes sure that something was returned //Creates a list that is used to determine the location of the instructions starting byte Listing list = currentProgram.getListing(); Address tempAddr = addrRange.getMinAddress();//sets the beginning scan address /* * The purpose of this while loop is to iterate through the memory that is currently selected by user. * All the instructions within this selection range are extracted and corresponding masks made from them. * These masks will then be used to search through memory to find instructions of the same type. */ while (tempAddr.compareTo(addrRange.getMaxAddress()) <= 0) { //Determines if current location is start of instruction, tempIns will be null if not beginning of instruction Instruction tempIns = list.getInstructionAt(tempAddr); if (tempIns != null) {//means that current address is start of an instruction logger = new SleighDebugLogger(currentProgram, tempAddr, SleighDebugMode.VERBOSE); if (logger.parseFailed()) { break; } //This takes care of the headers byte[] mask = logger.getInstructionMask(); byte[] value = logger.getMaskedBytes(mask); if (mask == null || value == null) { break; }//need to move on to next byte, not sure if this is right command to use //Builds a structure representing the recently found instruction mask and value Case tCase = new Case(); tCase.mask = mask; tCase.value = value; tCase.textRep = tempIns.getMnemonicString(); mnemonics.add(tCase); //adds the mnemonic mask and value to the arraylist //Gets a code unit which can be used to determine if the operands are constants. CodeUnit cu = list.getCodeUnitAt(tempAddr); //Iterates through all the operands for the currently selected instruction and stores them accordingly for (int x = 1; x <= logger.getNumOperands(); x++) { mask = logger.getOperandValueMask(x - 1); value = logger.getMaskedBytes(mask); if (mask == null || value == null) { break; }//move on to next instruction //Builds case to store the operands mask and value OperandCase otCase = new OperandCase(); otCase.mask = mask; otCase.value = value; //Object hey = tempIns.getDefaultOperandRepresentationList(x-1); otCase.textRep = tempIns.getDefaultOperandRepresentation(x - 1); //Determines if the given operand is a constant value. If it is a constant then proper flag is set. if (cu.getScalar(x - 1) != null) { otCase.constant = true; } //Determines if current structure is large enough to hold new operand, if it isn't increases structure size if (ops.size() < x && ops.size() > -1) { ops.add(new LinkedHashMap()); } //Adds the operand to the data-structure with a mapping to the instruction mnemonic extracted earlier in line 87 ops.get(x - 1).put(tCase, otCase); } //Increments the address pointer to point to the beginning of next instruction. tempAddr = tempAddr.add(tempIns.getLength()); } else { //Increments the address pointer by 1 //This would be hit if the tempAddr didn't get offset correctly. tempAddr.addWrap(1); } } } catch (Exception e) { println(e.getMessage()); } } //performs the application of filters and search instructions for matches. Doesn't load instructions or fill the controlList object. //Must have mnemonics, ops and controlList populated prior to running this method. public void executeSearch() { //Applies the filters provided through the controlList object to the instructions provided through the mnemonics and ops structures. MaskValueCase finalSearchString = getFinalMaskAndValue(mnemonics, ops, controlList); String valueString = new String(); String maskString = new String(); for (byte element : finalSearchString.value) { valueString = valueString.concat(toHexString(element, true, false)) + " "; } for (byte element : finalSearchString.mask) { maskString = maskString.concat(toHexString(element, true, false)) + " "; } printf("Final Search Bytes: "); println(valueString); printf("Final Search Mask: "); println(maskString); //Searches memory for matches to provided instructions with given filters. db structure is populated with the results. findLocations(finalSearchString, db); //Displays results in a table. Address[] tableArray = new Address[db.size()]; for (int x = 0; x < db.size(); x++) { tableArray[x] = db.get(x).addr; } try { show(tableArray); } catch (ImproperUseException iue) { // Do nothing; this code should only be run headed, anyway } } /* * Builds the mask and value byte streams for a single instruction that is represented by the mnemonic entered into the first parameter. * * That mnemonic is used as a key to withdrawal the operands from the data-structure and the SLMaskControl contains the filter information to be * applied to the instruction. */ private MaskValueCase buildSingleInstructionMask(Case mnemonic, ArrayList> localOperands, SLMaskControl localState) { byte[] tempMask = new byte[mnemonic.mask.length]; byte[] tempValue = new byte[mnemonic.value.length]; //Applies the mnemonic's value and mask if needed if (localState.useMnemonic) { tempValue = byteArrayOr(tempValue, mnemonic.value); tempMask = byteArrayOr(tempMask, mnemonic.mask); } //Applies the first operands mask and value to running stream if needed if (localState.useOp1 && localOperands.size() >= 1 && localOperands.get(0).get(mnemonic) != null) { OperandCase temp = localOperands.get(0).get(mnemonic); if (localState.useConst || !temp.constant) { if (temp.value != null && temp.mask != null && tempValue != null && tempMask != null) { tempValue = byteArrayOr(tempValue, temp.value); tempMask = byteArrayOr(tempMask, temp.mask); } } } //Applies the second operands mask and value to running stream if needed if (localState.useOp2 && localOperands.size() >= 2 && localOperands.get(1).get(mnemonic) != null) { OperandCase temp = localOperands.get(1).get(mnemonic); if (localState.useConst || !temp.constant) { if (temp.value != null && temp.mask != null && tempValue != null && tempMask != null) { tempValue = byteArrayOr(tempValue, temp.value); tempMask = byteArrayOr(tempMask, temp.mask); } } } MaskValueCase tempCase = new MaskValueCase(); tempCase.mask = tempMask; tempCase.value = tempValue; return tempCase; } /* * Combines all the masks in the data-structure together into a single byte stream. This stream is used to search memory. */ private MaskValueCase combineInstructionMasks(ArrayList masks, ArrayList values, int length) { if (masks.size() != values.size()) { throw new IllegalArgumentException(); } int index = 0; /* * This portion of code takes the masks and values from each of the different commands and concatenates them together to form * a single mask and value byte array. This byte array is then used to scan memory to look for other sequences of commands. */ byte[] finalValueArray = new byte[length]; byte[] finalMaskArray = new byte[length]; for (int x = 0; x < values.size(); x++) { //iterates through each mnemonic for (int i = 0; i < values.get(x).length && index < length; i++) { //iterate through the mnemonics bytes finalValueArray[index] = values.get(x)[i]; finalMaskArray[index] = masks.get(x)[i]; index++; } } MaskValueCase tempFinalParams = new MaskValueCase(); tempFinalParams.mask = finalMaskArray; tempFinalParams.value = finalValueArray; return tempFinalParams; } /* * Calculates the final mask and value byte streams. These streams are then used to search through memory to look for similar * assembly instructions. */ private MaskValueCase getFinalMaskAndValue(ArrayList privateMnemonics, ArrayList> localOperands, ArrayList control) { ArrayList masks = new ArrayList<>(); ArrayList values = new ArrayList<>(); //used for storing the byte stream currently being work on prior to being added to final data structure int totalLength = 0; boolean continueFilter = true;//controls whether the last mask given will be applied on the rest of the mnemonics if the controlList structure is smaller than mnemonics if (privateMnemonics == null || localOperands == null || control == null) { throw new IllegalArgumentException("Null Structure"); } //TODO Need to do some thorough checking on values entered into the control and mnemonics //TODO Need to make sure that all possible cases of unequal amounts are considered. /* * This loop will scan through the mnemonics and apply the provided MaskControls. * * The masks will be applied from the arraylist until there aren't any remaining in the list. At this point * a mask of all falses will be applied. This will in effect ignore rest of the mnemonics * * TODO Apply a switch that will allow the last mask in the array to be applied to rest of the mnemonics within selection. */ for (int x = 0; x < privateMnemonics.size(); x++) { MaskValueCase result = new MaskValueCase(); if (x < control.size()) { //need to apply the filter from control result = buildSingleInstructionMask(privateMnemonics.get(x), localOperands, control.get(x)); } else if (continueFilter) { //apply the last available mask in the control structure result = buildSingleInstructionMask(privateMnemonics.get(x), localOperands, control.get(control.size() - 1)); } else { //apply an empty filter basically ignoring result = buildSingleInstructionMask(privateMnemonics.get(x), localOperands, new SLMaskControl(false, false, false, false)); } if (result.value.length == result.mask.length) { masks.add(result.mask); values.add(result.value); totalLength += result.value.length; } } return combineInstructionMasks(masks, values, totalLength); } /* * Populates the database with the locations where the specified byte arrays are found. * The first parameter should only have two elements. Element 0 should contain mask and element 1 * contain the value. * * The second parameter gets populated with the results of the search. */ private void findLocations(MaskValueCase searchArrays, ArrayList localDatabase) { if (currentProgram == null || localDatabase == null || searchArrays == null) { throw new IllegalArgumentException("Null Data-Structure"); } if (searchArrays.mask.length != searchArrays.value.length) { throw new IllegalArgumentException("Mask and value lengths are different."); } if (containsOnBit(searchArrays.mask)) { Memory mem = currentProgram.getMemory(); //Gets the start and end address to search through Address endAddress = currentProgram.getMaxAddress(); Address currentPosition = currentProgram.getMinAddress(); while (currentPosition.compareTo(endAddress) < 0) { //Searches memory for the given mask and value. currentPosition = mem.findBytes(currentPosition, endAddress, searchArrays.value, searchArrays.mask, true, monitor); //Determines if a new location was found. if (currentPosition == null) { break; } Case temp = new Case(); temp.mask = searchArrays.mask; temp.value = searchArrays.value; temp.addr = currentPosition; localDatabase.add(temp); currentPosition = currentPosition.add(1); } } else { return; } } /* * Used for determining if there is a "On" bit in a byte stream. * * This is necessary because if you do a memory search for a mask with no "on" bit then it will return every memory address. */ private boolean containsOnBit(byte[] array) { for (byte element : array) { Byte temp = Byte.valueOf(element); int value = temp.intValue(); if (value != 0) { return true; } } return false; } /* * Takes two arrays of bytes and performs a bitwise or operation and returns the result. Returns null if the arguments aren't of same length. */ private byte[] byteArrayOr(byte[] arr1, byte[] arr2) { byte[] result = new byte[arr1.length]; if (arr1.length != arr2.length) { return null; } for (int x = 0; x < arr1.length; x++) { result[x] = (byte) (arr1[x] | arr2[x]); } return result; } /* * Used for storing data about an instructions mnemonic */ public class Case { public Address addr; public byte[] mask; public byte[] value; public String textRep; } public class OperandCase extends Case { public boolean constant = false; } private class MaskValueCase { public byte[] mask; public byte[] value; } /* * Represents a filter for a single instruction. * Controls which portions of the instruction will be used when performing the search through memory. */ public class SLMaskControl { boolean useMnemonic = true; boolean useOp1 = false; boolean useOp2 = false; boolean useConst = false; public SLMaskControl() { } public SLMaskControl(boolean mnemonic, boolean useop1, boolean useop2, boolean constant) { useMnemonic = mnemonic; useOp1 = useop1; useOp2 = useop2; useConst = constant; } } }