shellgdbremote-debugginggdbserver

gdbserver: execute shell commands of the target


E.g. !ls would execute ls command in gdb itself, but how to do it on remote side?

It should be simple, but I can't figure it out. Per documentation something like target remote | ls or target remote | !ls ought to do the trick, but either it's wrong or I don't understand something: such command makes gdb to try to close current session, and start debugging ls binary.

I also found some monitor cmd mentioned, but monitor !ls just triggers Unknown monitor command message.


Solution

  • A workaround is to implement a custom gdb command that does the following:

    1. fork the remote process;
    2. switch to the child inferior;
    3. replace the child process with a shell that executes the user's provided command;
    4. display the remote command's output;
    5. go back to the parent inferior.

    There are several limitations to take into account:

    Example gdb session:

    # Given remote terminal running `gdbserver :2345 ./remote_executable`, we connect to that server.
    target extended-remote 192.168.1.4:2345
    
    # Load our custom gdb command `rcmd`.
    source ./remote-cmd.py
    
    # Run until a point where libc has been loaded on the remote process, e.g. start of main().
    b main
    r
    
    # Don't need the main() breakpoint anymore.
    del 1
    
    # Run the remote command, e.g. `ls`.
    rcmd ls
    

    remote-cmd.py:

    #!/usr/bin/env python3
    
    import gdb
    import re
    import traceback
    import uuid
    
    
    class RemoteCmd(gdb.Command):
        def __init__(self):
            self.addresses = {}
    
            self.tmp_file = f'/tmp/{uuid.uuid4().hex}'
            gdb.write(f"Using tmp output file: {self.tmp_file}.\n")
    
            gdb.execute("set detach-on-fork off")
            gdb.execute("set follow-fork-mode parent")
    
            gdb.execute("set max-value-size unlimited")
            gdb.execute("set pagination off")
            gdb.execute("set print elements 0")
            gdb.execute("set print repeats 0")
    
            super(RemoteCmd, self).__init__("rcmd", gdb.COMMAND_USER)
    
        def preload(self):
            for symbol in [
                "close",
                "execl",
                "fork",
                "free",
                "lseek",
                "malloc",
                "open",
                "read",
            ]:
                self.load(symbol)
    
        def load(self, symbol):
            if symbol not in self.addresses:
                address_string = gdb.execute(f"info address {symbol}", to_string=True)
                match = re.match(
                    f'Symbol "{symbol}" is at ([0-9a-fx]+) .*', address_string, re.IGNORECASE
                )
                if match and len(match.groups()) > 0:
                    self.addresses[symbol] = match.groups()[0]
                else:
                    raise RuntimeError(f'Could not retrieve address for symbol "{symbol}".')
    
            return self.addresses[symbol]
    
        def output(self):
            # From `fcntl-linux.h`
            O_RDONLY = 0
            gdb.execute(
                f'set $fd = (int){self.load("open")}("{self.tmp_file}", {O_RDONLY})'
            )
    
            # From `stdio.h`
            SEEK_SET = 0
            SEEK_END = 2
            gdb.execute(f'set $len = (int){self.load("lseek")}($fd, 0, {SEEK_END})')
            gdb.execute(f'call (int){self.load("lseek")}($fd, 0, {SEEK_SET})')
            if int(gdb.convenience_variable("len")) <= 0:
                gdb.write("No output was captured.")
                return
    
            gdb.execute(f'set $mem = (void*){self.load("malloc")}($len)')
            gdb.execute(f'call (int){self.load("read")}($fd, $mem, $len)')
            gdb.execute('printf "%s\\n", (char*) $mem')
    
            gdb.execute(f'call (int){self.load("close")}($fd)')
            gdb.execute(f'call (int){self.load("free")}($mem)')
    
        def invoke(self, arg, from_tty):
            try:
                self.preload()
    
                is_auto_solib_add = gdb.parameter("auto-solib-add")
                gdb.execute("set auto-solib-add off")
    
                parent_inferior = gdb.selected_inferior()
                gdb.execute(f'set $child_pid = (int){self.load("fork")}()')
                child_pid = gdb.convenience_variable("child_pid")
                child_inferior = list(
                    filter(lambda x: x.pid == child_pid, gdb.inferiors())
                )[0]
                gdb.execute(f"inferior {child_inferior.num}")
    
                try:
                    gdb.execute(
                        f'call (int){self.load("execl")}("/bin/sh", "sh", "-c", "exec {arg} >{self.tmp_file} 2>&1", (char*)0)'
                    )
                except gdb.error as e:
                    if (
                        "The program being debugged exited while in a function called from GDB"
                        in str(e)
                    ):
                        pass
                    else:
                        raise e
                finally:
                    gdb.execute(f"inferior {parent_inferior.num}")
                    gdb.execute(f"remove-inferiors {child_inferior.num}")
    
                self.output()
            except Exception as e:
                gdb.write("".join(traceback.TracebackException.from_exception(e).format()))
                raise e
            finally:
                gdb.execute(f'set auto-solib-add {"on" if is_auto_solib_add else "off"}')
    
    
    RemoteCmd()