/* ###
 * 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.
 */
// An example script demonstrating the ability to emulate a specific portion of code within
// a disassembled program to extract return values of interest (deobfuscated data in this case)
// and generate program listing comments.
// This script emulates the "main" function within the deobExample program
// (see docs/GhidraClass/ExerciseFiles/Emulation/Source) built with gcc for x86-64.
// The program's "data" array contains simple obfuscated data and has a function "deobfuscate"
// which is called for each piece of obfuscated data.  The "main" function loops through all
// the data and deobfuscates each one invoking the "use_string" function for each deobfuscated
// data.  Breakpoints are placed on the call (and just after the call)
// to the function "deobfuscate" so that the various return values can be recorded with a comment
// placed just after the call.
//@category Examples.Emulation
import ghidra.app.emulator.EmulatorHelper;
import ghidra.app.script.GhidraScript;
import ghidra.app.util.opinion.ElfLoader;
import ghidra.pcode.emulate.EmulateExecutionState;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.symbol.*;
import ghidra.util.Msg;
import ghidra.util.exception.NotFoundException;

public class EmuX86DeobfuscateExampleScript extends GhidraScript {

	private static String PROGRAM_NAME = "deobExample";

	private EmulatorHelper emuHelper;

	// Important breakpoint locations
	private Address deobfuscateCall;
	private Address deobfuscateReturn;

	// Function locations
	private Address mainFunctionEntry; // start of emulation address

	// Address used as final return location
	// A breakpoint will be set here so we can determine when function execution
	// has completed.
	private static final long CONTROLLED_RETURN_OFFSET = 0;
	private Address controlledReturnAddr; // end of emulation address

	// First argument passed to deobfuscate function on last call (used for comment generation)
	private long lastDeobfuscateArg0;

	@Override
	protected void run() throws Exception {

		String format = currentProgram.getExecutableFormat();

		if (currentProgram == null || !currentProgram.getName().startsWith(PROGRAM_NAME) ||
			!"x86:LE:64:default".equals(currentProgram.getLanguageID().toString()) ||
			!ElfLoader.ELF_NAME.equals(format)) {

			printerr(
				"This emulation example script is specifically intended to be executed against the\n" +
					PROGRAM_NAME +
					" program whose source is contained within the GhidraClass exercise files\n" +
					"(see docs/GhidraClass/ExerciseFiles/Emulation/" + PROGRAM_NAME + ".c).\n" +
					"This program should be compiled using gcc for x86 64-bit, imported into your project, \n" +
					"analyzed and open as the active program before running ths script.");
			return;
		}

		// Identify function to be emulated
		mainFunctionEntry = getSymbolAddress("main");

		// Obtain entry instruction in order to establish initial processor context
		Instruction entryInstr = getInstructionAt(mainFunctionEntry);
		if (entryInstr == null) {
			printerr("Instruction not found at main entry point: " + mainFunctionEntry);
			return;
		}

		// Identify important symbol addresses
		// NOTE: If the sample is recompiled the following addresses may need to be adjusted
		Instruction callSite = getCalledFromInstruction("deobfuscate");
		if (callSite == null) {
			printerr("Instruction not found at call site for: deobfuscate");
			return;
		}

		deobfuscateCall = callSite.getAddress();
		deobfuscateReturn = callSite.getFallThrough(); // instruction address immediately after deobfuscate call

		// Remove prior pre-comment
		setPreComment(deobfuscateReturn, null);

		// Establish emulation helper
		emuHelper = new EmulatorHelper(currentProgram);
		try {

			// Initialize stack pointer (not used by this example)
			long stackOffset =
				(entryInstr.getAddress().getAddressSpace().getMaxAddress().getOffset() >>> 1) -
					0x7fff;
			emuHelper.writeRegister(emuHelper.getStackPointerRegister(), stackOffset);

			// Setup breakpoints
			emuHelper.setBreakpoint(deobfuscateCall);
			emuHelper.setBreakpoint(deobfuscateReturn);

			// Set controlled return location so we can identify return from emulated function
			controlledReturnAddr = getAddress(CONTROLLED_RETURN_OFFSET);
			emuHelper.writeStackValue(0, 8, CONTROLLED_RETURN_OFFSET);
			emuHelper.setBreakpoint(controlledReturnAddr);

			Msg.debug(this, "EMU starting at " + mainFunctionEntry);

			// Execution loop until return from function or error occurs
			while (!monitor.isCancelled()) {
				boolean success =
					(emuHelper.getEmulateExecutionState() == EmulateExecutionState.BREAKPOINT)
							? emuHelper.run(monitor)
							: emuHelper.run(mainFunctionEntry, entryInstr, monitor);
				Address executionAddress = emuHelper.getExecutionAddress();
				if (monitor.isCancelled()) {
					println("Emulation cancelled");
					return;
				}
				if (executionAddress.equals(controlledReturnAddr)) {
					println("Returned from function");
					return;
				}
				if (!success) {
					String lastError = emuHelper.getLastError();
					printerr("Emulation Error: " + lastError);
					return;
				}
				processBreakpoint(executionAddress);
			}
		}
		finally {
			// cleanup resources and release hold on currentProgram
			emuHelper.dispose();
		}
	}

	private Address getAddress(long offset) {
		return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset);
	}

	/**
	 * Perform processing for the various breakpoints.
	 * @param addr current execute address where emulation has been suspended
	 * @throws Exception if an error occurs
	 */
	private void processBreakpoint(Address addr) throws Exception {

		if (addr.equals(deobfuscateCall)) {
			lastDeobfuscateArg0 = emuHelper.readRegister("RDI").longValue();
		}

		else if (addr.equals(deobfuscateReturn)) {
			long deobfuscateReturnValue = emuHelper.readRegister("RAX").longValue();
			String str = "deobfuscate(src=0x" + Long.toHexString(lastDeobfuscateArg0) + ") -> \"" +
				emuHelper.readNullTerminatedString(getAddress(deobfuscateReturnValue), 32) + "\"";
			String comment = getPreComment(deobfuscateReturn);
			if (comment == null) {
				comment = "";
			}
			else {
				comment += "\n";
			}
			comment += str;
			println("Updated pre-comment at " + deobfuscateReturn);
			setPreComment(deobfuscateReturn, comment);
		}
	}

	private Instruction getCalledFromInstruction(String functionName) {
		Symbol s = SymbolUtilities.getExpectedLabelOrFunctionSymbol(currentProgram, functionName,
			m -> printerr(m));
		for (Reference ref : s.getReferences(monitor)) {
			if (ref.getReferenceType().isCall()) {
				return currentProgram.getListing().getInstructionAt(ref.getFromAddress());
			}
		}
		return null;
	}

	private Address getSymbolAddress(String symbolName) throws NotFoundException {
		Symbol symbol = SymbolUtilities.getLabelOrFunctionSymbol(currentProgram, symbolName,
			err -> Msg.error(this, err));
		if (symbol != null) {
			return symbol.getAddress();
		}
		throw new NotFoundException("Failed to locate label: " + symbolName);
	}

}
