x86gdbgdb-pythonno-opcode-patching

Overwriting segfaulting instructions with NOPs doesn't fix segfault in gdb


I thought it would be cool to add a command to gdb that will look at the instruction pointer and overwrite the current instruction with NOPs. The idea being that if you are debugging, and your program segfaults due to memory access you could use this to temporarily ignore it and continue execution (useful if your recompiles take a really long time and you know there are more issues).

However, when I do trigger a segfault in a simple test program, overwrite the bytes, and try to resume execution, it just segfaults again and the process dies. Why?

Test program:

#include <stdio.h>

int main(int argc, char** argv)
{
    if(argc == 1) {
        *((char*)0) = 42;
        printf("Success.\n");
        return 0;
    }
    printf("Failure.\n");
    return 1;
}

Gdb plugin (you can put source /path/to/nopify.py in your ~/.gdbinit to enable):

import gdb
from gdb import Command

class NopifyCommand(Command):
    """Nopify the instruction at the current instruction pointer (rip/eip)."""

    def __init__(self):
        super(NopifyCommand, self).__init__("nopify", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        # Get the current instruction pointer
        rip = int(gdb.parse_and_eval("$rip"))

        # Disassemble the current and the next instruction
        asm_output = gdb.execute("x/2i $rip", to_string=True)

        # Extract the address of the next instruction
        lines = asm_output.strip().split('\n')
        if len(lines) > 1:
            next_instr_line = lines[1]
            next_addr_str = next_instr_line.split()[0]
            next_addr = int(next_addr_str, 16)
            instr_length = next_addr - rip

            # Replace the instruction with NOPs
            for i in range(instr_length):
                cmd = f"set *((char*)({rip}+{i})) = 0x90"
                print(f"Running: {cmd}")
                gdb.execute(cmd)

            print(f"Replaced {instr_length} bytes with NOPs")
        else:
            print("Could not determine the length of the instruction.")

# Register the command
NopifyCommand()

Example gdb session:

(gdb) run
Starting program: /tmp/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555167 in main ()

(gdb) x/2i $rip
=> 0x555555555167 <main+30>:    movb   $0x2a,(%rax)
   0x55555555516a <main+33>:    lea    0xe93(%rip),%rdi        # 0x555555556004

(gdb) nopify
Running: set *((char*)(93824992235879+0)) = 0x90
Running: set *((char*)(93824992235879+1)) = 0x90
Running: set *((char*)(93824992235879+2)) = 0x90
Replaced 3 bytes with NOPs

(gdb) x/2i $rip
=> 0x555555555167 <main+30>:    nop
   0x555555555168 <main+31>:    nop

(gdb) cont
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

I get no other output, I expect to see it print "Success." Running flush icache before continuing makes no difference. Any ideas?

Edit: Updated version of the script incorporating the fix from below can be found here: https://github.com/jgarvin/joe-etc/blob/master/gdb/nopify.py


Solution

  • You don't have a signal handler for SIGSEGV. GDB is just pausing at the moment between the signal being delivered and the process dying (the default action for SIGSEGV).

    This is useful when debugging to see what led to a fatal signal, but I'm not sure there's any way to set $pc = something to have GDB change the handling of that signal after the OS has started to deliver it.

    So you need your process to ignore SIGSEGV or catch it with an empty handler, so it will retry / return to the faulting instruction address. e.g. with the obsolete but simple signal(2) or the now-standard sigaction(2). (I'm not sure if SIG_IGN is allowed as the disposition for SIGSEGV, since that would lead to infinite loops in programs not being debugged.)

    Apparently it is possible to have GDB intercept signals if you run a GDB command first: Continue debugging after SegFault in GDB? - handle SIGSEGV stop nopass. Thanks @ssbssa for pointing that out.