NationalSecurityAgency / ghidra

Ghidra is a software reverse engineering (SRE) framework
https://www.nsa.gov/ghidra
Apache License 2.0
52.06k stars 5.9k forks source link

Ghidra debugger throws exceptions when debugging STM32F103 target though OpenOCD and StlinkV2 #4345

Closed lolwheel closed 2 years ago

lolwheel commented 2 years ago

EDIT: For those who bump into the same issue and don't want to scan through the comments, follow these steps to fix it. Add following lines to a text file and source it from Ghidra GDB window. You have to source them from a file as running define info proc mappings requires interactive input which Ghidra GDB console doesn't support yet:

set arch armv7
set arm fallback-mode thumb
set arm force-mode thumb
define info proc mappings
echo 0x0 0xFFFFFFFF 0x100000000 0x0 mem \n
end
tar ext :3333

The last line points to the default OpenOCD gdb port. Tweak if you need to. The last step to link the Dynamic and Static listings is to open the Modules window and hit the static mapping button on it. Clicking it will start tracking the current execution point in the Listings and Decompiler window. That's it.

END EDIT

When trying to connect to the local OpenOCD instance via IN-VM GDB local debugger, a couple things happen: The GDB window shows several errors right after I connect to the OpenOCD via target ext :3333:

Remote debugging using :3333
 warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command. 
 0x08002b26 in ?? ()
 (gdb) 0x08002b26 in ?? ()
 Not supported on this target.
 Not supported on this target.

The "Debug Console" pane immediately shows following errors:

 Could not list regions. Using default. | [] | Mon Jun 13 20:49:12 PDT 2022
 Listener ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsProvider$MyObjectListener@7084cb38 caused exception processing attributesChanged | [] | Mon Jun 13 20:49:12 PDT 2022
 Could not list regions. Using default. | [] | Mon Jun 13 20:49:12 PDT 2022

The "Objects" pane does show the stack and registers. Every other window stays in its default state - all of them are empty and the Listings pane in scrolled to the entry point. Hitting "Step into" or "Step Over" buttons makes more of the same errors appear in both the GDB logs and the "Debug Console"

To Reproduce

  1. Launch OpenOCD
  2. Load the debugged binary in the Debugger.
  3. Create the new debugging connection via IN-VM local GDB. I'm using gdb from gcc-arm-none-eabi-10.3-2021.10 package
  4. In the debugger console type tar ext :3333
  5. Witness the errors above.

Expected behavior No errors, functional Stepping, generally other windows behaving as if the target is connected to.

Screenshots image

Attachments If applicable, please attach any files that caused problems or log files generated by the software.

Environment (please complete the following information):

Additional context Using the gdb in console works totally fine, with the following .gdbinit and using the same exact gdb binary that I'm pointing Ghidra to, I'm able to step through the assembly without any errors:

tar ext :3333
tui enable
layout asm
monitor reset halt
nsadeveloper789 commented 2 years ago

OK, so except for the "...caused exception processing attributesChanged", all of these are pretty normal errors for a remote target. I suspect we can still ignore that one, though. As an aside, I'd take GDB's advice and use the "file" command, if you have the image on disk. The errors about "Not supported on this target" and "Could not list regions" are typical for remote targets. We use "info proc mappings" to obtain a process memory map, but remote targets don't support it, so it just falls back to one or two entries covering the entire address space. The other behaviors are all symptoms of not having a "recording." We're actively working on relaxing this requirement, but it seems Ghidra was unable to identify the remote platform. It's pretty straightforward to fix. We'll try remedies in this order:

  1. Manually set the architecture from gdb
  2. Manually set the architecture from Ghidra
  3. Modify Ghidra to recognize your target

To get started, expand the "Environment" node in your "Objects" tree. Note the values for os and arch (debugger should be "gdb"). You can also type show os and show arch into the "Interpreter" window, if you prefer. 1) If arch is not what you expect, then disconnect and reconnect Ghidra to gdb, set the arch, then reconnect to the remote. If that works, then we're all done. Just remember to set your arch each time or add it to your .gdbinit. Sometimes setting it to something more specific will help Ghidra, even if it appears to have no affect on GDB.

2) If that didn't work, then in the "Objects" pane, select Inferior 1, right-click and select "Record." It should present a langauge/platform selection box. Choose the one that best matches your platform and hit OK. If you have a choice between "ARM/GDB for..." and "Default GDB for...," choose the former. If that worked, then you might consider proceeding to step 3 to make it automatic in the future. If it didn't work, then let us know.

3) Edit Ghidra's ARM.ldefs file (should be Ghidra/Processors/ARM/data/languages/ARM.ldefs). In there you should see external_name attributes which provide a mapping from Ghidra's languages to GNU/GDB's arch names. Presumably there is no entry which exactly matches what show arch reports for your target. Find the Ghidra language that worked from step 2 and add the appropriate external_name element to it, restart Ghidra, then retry connecting to your target.

lolwheel commented 2 years ago

Hey Dan, thanks for all the hand-holding.

  1. Os is set to none, arch is arm and endianness is little in the "Environment" node so no surprises here.
  2. Hitting "Record" in "Objects" pane pops up a window which is initially empty. When I uncheck the "Show only recommended offers" I do get whole list of options. I picked my platform which is Arm Cortex 32 bit, little endian. This makes Ghidra throw a pretty verbose exception:
    
    Offset must be between 0x0 and 0xffffffff, got 0x7ffffffffffffffe instead!
    ghidra.program.model.address.AddressOutOfBoundsException: Offset must be between 0x0 and 0xffffffff, got 0x7ffffffffffffffe instead!
    at ghidra.program.model.address.AbstractAddressSpace.makeValidOffset(AbstractAddressSpace.java:626)
    at ghidra.program.model.address.GenericAddressSpace.makeValidOffset(GenericAddressSpace.java:21)
    at ghidra.program.model.address.GenericAddress.<init>(GenericAddress.java:55)
    at ghidra.program.model.address.GenericAddressSpace.getAddress(GenericAddressSpace.java:88)
    at ghidra.app.plugin.core.debug.mapping.DefaultDebuggerMemoryMapper.toSameNamedSpace(DefaultDebuggerMemoryMapper.java:41)
    at ghidra.app.plugin.core.debug.mapping.DefaultDebuggerMemoryMapper.targetToTrace(DefaultDebuggerMemoryMapper.java:66)
    at ghidra.app.plugin.core.debug.mapping.DefaultDebuggerMemoryMapper.targetToTrace(DefaultDebuggerMemoryMapper.java:72)
    at ghidra.app.plugin.core.debug.service.model.RecorderSimpleMemory.getAccessibleMemory(RecorderSimpleMemory.java:97)
    at ghidra.app.plugin.core.debug.service.model.DefaultProcessRecorder.getAccessibleProcessMemory(DefaultProcessRecorder.java:65)
    at ghidra.app.plugin.core.debug.service.model.DefaultTraceRecorder.getAccessibleProcessMemory(DefaultTraceRecorder.java:528)
    at ghidra.app.plugin.core.debug.gui.action.VisibleROOnceAutoReadMemorySpec.readMemory(VisibleROOnceAutoReadMemorySpec.java:61)
    at ghidra.app.plugin.core.debug.gui.action.DebuggerReadsMemoryTrait.doAutoRead(DebuggerReadsMemoryTrait.java:253)
    at ghidra.app.plugin.core.debug.gui.action.DebuggerReadsMemoryTrait.goToCoordinates(DebuggerReadsMemoryTrait.java:248)
    at ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesProvider.goToCoordinates(DebuggerMemoryBytesProvider.java:312)
    at ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesProvider.coordinatesActivated(DebuggerMemoryBytesProvider.java:319)
    at ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin.lambda$processEvent$1(DebuggerMemoryBytesPlugin.java:172)
    at ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin.allProviders(DebuggerMemoryBytesPlugin.java:161)
    at ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin.processEvent(DebuggerMemoryBytesPlugin.java:172)
    at ghidra.framework.plugintool.Plugin.eventSent(Plugin.java:329)
    at ghidra.framework.plugintool.mgr.EventManager.sendEvents(EventManager.java:286)
    at ghidra.framework.plugintool.mgr.EventManager.lambda$new$3(EventManager.java:49)
    at ghidra.util.Swing.doRun(Swing.java:292)
    at ghidra.util.Swing.runNow(Swing.java:208)
    at ghidra.util.Swing.runNow(Swing.java:163)
    at ghidra.framework.plugintool.mgr.EventManager.fireEvent(EventManager.java:216)
    at ghidra.framework.plugintool.PluginTool.firePluginEvent(PluginTool.java:475)
    at ghidra.framework.plugintool.Plugin.firePluginEvent(Plugin.java:481)
    at ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.fireLocationEvent(DebuggerTraceManagerServicePlugin.java:700)
    at ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.lambda$prepareViewAndFireEvent$18(DebuggerTraceManagerServicePlugin.java:695)
    at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:718)
    at java.base/java.util.concurrent.CompletableFuture$Completion.run(CompletableFuture.java:482)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:771)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:716)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:741)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

Build Date: 2022-May-19 0956 EDT Ghidra Version: 10.1.4 Java Home: /usr/lib/jvm/java-17-openjdk-amd64 JVM Version: Private Build 17.0.1 OS: Linux 5.11.0-49-generic amd64


The address seems 64 bit while the platform I picked is 32 bit so there's that. Attaching screenshot too:
![image](https://user-images.githubusercontent.com/96507237/173767297-4ce488f7-73f7-42a4-a550-e2221b656314.png)

Thanks once again.
nsadeveloper789 commented 2 years ago

Oiy, OK. Not sure if you can dismiss that dialog long enough to check the "Regions" tab in the bottom left? My guess is it contains a defaultLow and defaultHigh entry, covering 64-bits of space rather than just the 32 supported by your platform? If that's the case and you have a dev environment, you might try patching GdbModelTargetProcessMemory.java at or about line 76. Just hardcode one GdbMemoryMapping(0, 1L<<32, 1L<<32, "default"). In any case, I'll see about a proper fix.

lolwheel commented 2 years ago

The "Regions" tab is empty: image

lolwheel commented 2 years ago

OK so I think I got it working. What did the trick is the instruction that I found here: https://gist.github.com/aldelaro5/d532b21b5e2ec48d5f78e81846c1c1b7 I added following lines to the .gdbinit that I source from Ghidra:

define info proc mappings
echo 0x0 0xFFFFFFFF 0x100000000 0x0 mem \n
end

After this, I also had to do what you suggested in the first comment, which is to hit "Record" button and picked the correct architecture which finally made Ghidra happy.

I would lie if I fully understood what's going on here but my impression is that Ghidra is polling GDB for the memory layout it has from the device. I was wondering if it's possible to feed GDB the memory layout defined in the Ghidra project itself as a fallback, if GDB doesn't have it? The rationale for this is that most of the Ghidra tutorials for embedded reverse engineering suggest using the SVD loader plugin which ends up producing a very accurate memory map of the system.

Anyways, thank you once again Dan, Cheers!

EDIT: Oh I also figured out the external_name issue. arm seems to be too generic. When I changed set arch arm to set arch armv7, I don't have to even hit the "Record" button and pick the architecture there. Awesome!

nsadeveloper789 commented 2 years ago

Glad you got it working!

As far as feeding the memory map from Ghidra back to GDB, that's not built-in. We aim for one interface to model several debuggers, so we have few, if any, special provisions for GDB. In general, the communication pattern is: commands go from Ghidra to the debugger, data is sent from debugger to Ghidra. That said, you may be able to write a GDB-specific script to accomplish your goal.

  1. Use currentProgram's memory map to write out a script that defines info proc mappings. The example .gdbinit you gave actually does this, faking out the full 32-bit address space. You would generate an echo line for each ALLOCED block in the memory map. Just write this out to a script file.
  2. Send a command to GDB to execute that script file.

Step 1 can get complicated depending on how well you know Ghidra's scripting API. Use getMemoryBlocks() and then filter for those in "ram" and whose name is not EXTERNAL. You may need some additional tweaking, but that should get you only the real ALLOCED ones.

Step 2 is definitely more complicated, because we don't have "script conveniences" for the debugger, yet. Use state.getTool().getService(DebuggerModelService.class) to get the "model service". This manages the debugger's connections. Use modelService.getCurrentModel() to get the current connection. Assuming it's GDB, you can use (TargetInterpreter) model.getModelRoot(). This will get the "session" object and cast it to an interpreter. The cast doesn't work in general, but it works for GDB. That interface has execute(String) and executeCapture(String), which you can use to run arbitrary GDB CLI commands. Due to a bug, it can't be used for multi-line commands, which is why you have to write the temporary script file. Remember to call .get() on the CompletableFuture, so that you can handle any errors.

lolwheel commented 2 years ago

Thanks for the tips and debugging Dan.

In retrospect, it would be nice if Gghidra logged at least some errors when its core expectations such as defined "Recorder" or proc mapping are absent. I am very lucky to have someone as helpful as you explaining this to me, I bet this discouraged others from using this amazing tool more.

In the meantime I bumped into another issue: My Dynamic and Static listings aren't synchronized. I looked at https://github.com/NationalSecurityAgency/ghidra/issues/2578 where you suggested that the culprit might be unmapped module. The thing is, there are no modules present or known to gdb when connected to OpenOCD.

I tried looking up potential commands that Ghidra might be issuing to gdb to spoof the output but couldn't find anything useful after 15 mins of Googling so I'm back for help.

nsadeveloper789 commented 2 years ago

We actually have some changes in the pipe to mitigate and at least warn users when the "launch" process fails or only completes partially. It's still awaiting review, though. It addresses "record failures" as well as "mapping failures". So that should be coming soon.

As far mapping/syncing in your case, I think the only option is to "map identically". Since gdb produces neither a memory map nor a module list, it has no idea what program to sync with in the static listing. From the "Modules" window's drop-down menu, select "Map Identically." This assumes your current program is loaded in the current target without relocation. It'll sync the two listings by exact address.

If there is relocation, you can add the mapping entry manually: Window -> Debugger -> Static Mappings. There's an "Add" button in the toolbar.

lolwheel commented 2 years ago

Thanks Dan, this solved it all. I'm going to edit my first comment in the issue so that others stumbling into the same issues find the answers easily.

I'm not sure if I should just close the issue at this point or you want to do it after addressing the https://github.com/NationalSecurityAgency/ghidra/issues/4345#issuecomment-1156457726. I'll leave this up to you.

nsadeveloper789 commented 2 years ago

Yes, please leave it open. We'll close it once we decide what to do re/ the GdbModelTargetProcessMemory.java issue. Thanks!