From 57ad2517c155ff2652ff3e6cb59b4728ee9ae058 Mon Sep 17 00:00:00 2001 From: Dreg Date: Fri, 21 Jul 2023 21:26:40 +0200 Subject: [PATCH] add new nop command features --- docs/commands/nop.md | 43 ++++++-- gef.py | 66 +++++++++--- tests/commands/nop.py | 241 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 302 insertions(+), 48 deletions(-) diff --git a/docs/commands/nop.md b/docs/commands/nop.md index 8f8b75c76..fd4be2476 100644 --- a/docs/commands/nop.md +++ b/docs/commands/nop.md @@ -3,21 +3,50 @@ The `nop` command allows you to easily patch instructions with nops. ``` -nop [LOCATION] [--n NUM_ITEMS] [--b] +nop [LOCATION] [--i ITEMS] [--f] [--n] [--b] ``` -`LOCATION` address/symbol to patch +`LOCATION` address/symbol to patch (by default this command replaces whole instructions) -`--n NUM_ITEMS` Instead of writing one instruction/nop, patch the specified number of -instructions/nops (full instruction size by default) +`--i ITEMS` number of items to insert (default 1) -`--b` Instead of writing full instruction size, patch the specified number of nops +`--f` Force patch even when the selected settings could overwrite partial instructions +`--n` Instead of replacing whole instructions, insert ITEMS nop instructions, no matter how many instructions it overwrites + +`--b` Instead of replacing whole instructions, fill ITEMS bytes with nops + +nop the current instruction ($pc): ```bash gef➤ nop +``` + +nop an instruction at $pc+3 address: +```bash gef➤ nop $pc+3 -gef➤ nop --n 2 $pc+3 +``` + +nop two instructions at address $pc+3: +```bash +gef➤ nop --i 2 $pc+3 +``` + +Replace 1 byte with nop at current instruction ($pc): +```bash gef➤ nop --b +``` + +Replace 1 byte with nop at address $pc+3: +```bash gef➤ nop --b $pc+3 -gef➤ nop --b --n 2 $pc+3 +``` + +Replace 2 bytes with nop(s) (breaking the last instruction) at address $pc+3: +```bash +gef➤ nop --f --b --i 2 $pc+3 +``` + +Patch 2 nops at address $pc+3: +```bash +gef➤ nop --n --i 2 $pc+3 ``` diff --git a/gef.py b/gef.py index e66ec0677..cfdd42fd6 100644 --- a/gef.py +++ b/gef.py @@ -6025,54 +6025,86 @@ class NopCommand(GenericCommand): aware.""" _cmdline_ = "nop" - _syntax_ = ("{_cmdline_} [LOCATION] [--n NUM_ITEMS] [--b]" - "\n\tLOCATION\taddress/symbol to patch" - "\t--n NUM_ITEMS\tInstead of writing one instruction/nop, patch the specified number of instructions/nops (full instruction size by default)" - "\t--b\tInstead of writing full instruction size, patch the specified number of nops") - _example_ = f"{_cmdline_} $pc" - + _syntax_ = ("{_cmdline_} [LOCATION] [--i ITEMS] [--f] [--n] [--b]" + "\n\tLOCATION\taddress/symbol to patch (by default this command replaces whole instructions)" + "\t--i ITEMS\tnumber of items to insert (default 1)" + "\t--f\tForce patch even when the selected settings could overwrite partial instructions" + "\t--n\tInstead of replacing whole instructions, insert ITEMS nop instructions, no matter how many instructions it overwrites" + "\t--b\tInstead of replacing whole instructions, fill ITEMS bytes with nops") _example_ = [f"{_cmdline_}", f"{_cmdline_} $pc+3", - f"{_cmdline_} --n 2 $pc+3", + f"{_cmdline_} --i 2 $pc+3", f"{_cmdline_} --b", f"{_cmdline_} --b $pc+3", - f"{_cmdline_} --b --n 2 $pc+3",] + f"{_cmdline_} --f --b --i 2 $pc+3" + f"{_cmdline_} --n --i 2 $pc+3",] def __init__(self) -> None: super().__init__(complete=gdb.COMPLETE_LOCATION) return @only_if_gdb_running - @parse_arguments({"address": "$pc"}, {"--n": 0, "--b": False}) + @parse_arguments({"address": "$pc"}, {"--i": 1, "--b": True, "--f": True, "--n": True}) def do_invoke(self, _: List[str], **kwargs: Any) -> None: args : argparse.Namespace = kwargs["arguments"] address = parse_address(args.address) nop = gef.arch.nop_insn - num_items = args.n or 1 - as_nops_flags = not args.b + num_items = args.i or 1 + fill_bytes = args.b + fill_nops = args.n + force_flag = args.f or False + + if fill_nops and fill_bytes: + err("only is possible specify --b or --n at same time") + return total_bytes = 0 - if as_nops_flags: + if fill_bytes: + total_bytes = num_items + elif fill_nops: total_bytes = num_items * len(nop) else: try: last_addr = gdb_get_nth_next_instruction_address(address, num_items) except: - err(f"Cannot patch instruction at {address:#x}: MAYBE reaching unmapped area") + err(f"Cannot patch instruction at {address:#x} reaching unmapped area") return total_bytes = (last_addr - address) + gef_get_instruction_at(last_addr).size() - if total_bytes % len(nop): - warn(f"Patching {total_bytes} bytes at {address:#x} will result in a partially patched instruction and may break disassembly") + if len(nop) > total_bytes or total_bytes % len(nop): + warn(f"Patching {total_bytes} bytes at {address:#x} will result in LAST-NOP " + f"(byte nr {total_bytes % len(nop):#x}) broken and may cause a crash or " + f"break disassembly. Use --f (force) to ignore this warning") + if not force_flag: + return + + target_end_address = address + total_bytes + curr_ins = gef_current_instruction(address) + while curr_ins.address + curr_ins.size() < target_end_address: + if not Address(value=curr_ins.address + 1).valid: + err(f"Cannot patch instruction at {address:#x}: reaching unmapped area") + return + curr_ins = gef_next_instruction(curr_ins.address) - nops = bytearray(nop * (total_bytes // len(nop))) + final_ins_end_addr = curr_ins.address + curr_ins.size() + + if final_ins_end_addr != target_end_address: + warn(f"Patching {total_bytes} bytes at {address:#x} will result in LAST-INSTRUCTION " + f"({curr_ins.address:#x}) being partial overwritten and may cause a crash or " + f"break disassembly. You must use --f to allow misaligned patching.") + if not force_flag: + return + + nops = bytearray(nop * total_bytes) end_address = Address(value=address + total_bytes - 1) if not end_address.valid: - err(f"Cannot patch instruction at {address:#x}: reaching unmapped area") + err(f"Cannot patch instruction at {address:#x}: reaching unmapped " + f"area: {end_address:#x}") return ok(f"Patching {total_bytes} bytes from {address:#x}") gef.memory.write(address, nops, total_bytes) + return diff --git a/tests/commands/nop.py b/tests/commands/nop.py index 9805c9b3e..35f6a03a7 100644 --- a/tests/commands/nop.py +++ b/tests/commands/nop.py @@ -19,38 +19,86 @@ def test_cmd_nop_inactive(self): res = gdb_run_cmd(f"{self.cmd}") self.assertFailIfInactiveSession(res) + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_no_arg(self): res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))", # 2 short jumps to pc + "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))", after=( self.cmd, - "pi print(gef.memory.read(gef.arch.pc, 4))", # read 4 bytes + "pi print(gef.memory.read(gef.arch.pc, 4))", + ) + ) + self.assertNoException(res) + self.assertIn(r"\x90\x90\xeb\xfe", res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_no_arg_break_instruction(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"), + + after=( + self.cmd, + "pi print(gef.memory.read(gef.arch.pc, 4))", + ) + ) + self.assertNoException(res) + self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) + self.assertNotIn(r"\x90\x90\xeb\xfe", res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_force_arg_break_instruction(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"), + + after=( + f"{self.cmd} --f", + "pi print(gef.memory.read(gef.arch.pc, 4))", + ) + ) + self.assertNoException(res) + self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) + self.assertIn(r"\x90\x91\xeb\xfe", res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_i_arg(self): + + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))", + after=( + f"{self.cmd} --i 2 $pc+1", + "pi print(gef.memory.read(gef.arch.pc+1, 8))", ) ) self.assertNoException(res) - self.assertIn(r"\x90\x90\xeb\xfe", res) # 2 nops + 1 short jump + self.assertIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") - def test_cmd_nop_arg(self): + def test_cmd_nop_i_arg_reaching_unmapped_area(self): res = gdb_start_silent_cmd( - "pi gef.memory.write(gef.arch.sp, p64(0xfeebfeebfeebfeeb))", # 4 short jumps to stack + "pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))", after=( - f"{self.cmd} --n 2 $sp", - "pi print(gef.memory.read(gef.arch.sp, 8))", # read 8 bytes + f"{self.cmd} --i 2000000000000000000000000000000000000 $pc+1", + "pi print(gef.memory.read(gef.arch.pc+1, 8))", ) ) + self.assertIn(r"reaching unmapped area", res) self.assertNoException(res) - self.assertIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res) # 4 nops + 2 short jumps + self.assertNotIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res) @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_invalid_end_address(self): res = gdb_run_silent_cmd( - f"{self.cmd} --n 5 0x1337000+0x1000-4", + f"{self.cmd} --i 5 0x1337000+0x1000-4", target=_target("mmap-known-address") ) self.assertNoException(res) @@ -58,40 +106,185 @@ def test_cmd_nop_invalid_end_address(self): @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") - def test_cmd_nop_as_bytes_no_arg(self): + def test_cmd_nop_nop(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p32(0x9191))", + after=( + f"{self.cmd} --n", + "pi print(gef.memory.read(gef.arch.pc, 2))", + ) + ) + self.assertIn(r"\x90\x91", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_nop_break_instruction(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", + after=( + f"{self.cmd} --n", + "pi print(gef.memory.read(gef.arch.pc, 2))", + ) + ) + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\xeb\xfe'", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_nop_break_instruction_force(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", + after=( + f"{self.cmd} --n --f", + "pi print(gef.memory.read(gef.arch.pc, 2))", + ) + ) + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\x90\xfe'", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_nop_arg(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))", + after=( + f"{self.cmd} --i 4 --n", + "pi print(gef.memory.read(gef.arch.pc, 8))", + ) + ) + self.assertIn(r"b'\x90\x90\x90\x90\xeb\xfe\xeb\xfe'", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_nop_arg_multibnop_breaks(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), + + after=( + f"{self.cmd} --n", + "pi print(gef.memory.read(gef.arch.pc, 8))", + ) + ) + self.assertNoException(res) + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\xeb\xfe\xeb\xfe\xeb\xfe\xeb\xfe'", res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_nop_arg_multibnop_breaks_force(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), + + after=( + f"{self.cmd} --n --f", + "pi print(gef.memory.read(gef.arch.pc, 8))", + ) + ) + self.assertNoException(res) + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\x90\x91\x92\xfe\xeb\xfe\xeb\xfe'", res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_bytes(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p16(0x9191))", + after=( + f"{self.cmd} --b", + "pi print(gef.memory.read(gef.arch.pc, 2))", + ) + ) + + self.assertIn(r"\x90\x91", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_bytes_break_instruction(self): res = gdb_start_silent_cmd( - "pi print(f'*** *pc={u8(gef.memory.read(gef.arch.pc, 1))}')", + "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", after=( f"{self.cmd} --b", - "pi print(f'*** *pc={u8(gef.memory.read(gef.arch.pc, 1)):#x}')", + "pi print(gef.memory.read(gef.arch.pc, 2))", ) ) + + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\xeb\xfe'", res) self.assertNoException(res) - lines = findlines("*** *pc=", res) - self.assertEqual(len(lines), 2) - self.assertEqual(lines[1], "*** *pc=0x90") @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") - def test_cmd_nop_as_bytes_arg(self): + def test_cmd_nop_bytes_break_instruction_force(self): res = gdb_start_silent_cmd( - "pi print(f'*** *sp={u32(gef.memory.read(gef.arch.sp, 4))}')", + "pi gef.memory.write(gef.arch.pc, p16(0xfeeb))", + after=( + f"{self.cmd} --b --f", + "pi print(gef.memory.read(gef.arch.pc, 2))", + ) + ) + self.assertIn(r"will result in LAST-INSTRUCTION", res) + self.assertIn(r"b'\x90\xfe'", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_bytes_arg(self): + res = gdb_start_silent_cmd( + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))", + after=( + f"{self.cmd} --i 2 --b --f", + "pi print(gef.memory.read(gef.arch.pc, 8))", + ) + ) + self.assertIn(r"b'\x90\x90\xeb\xfe\xeb\xfe\xeb\xfe'", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_bytes_arg_nops_no_fit(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), + + after=( + f"{self.cmd} --i 4 --b", + "pi print(gef.memory.read(gef.arch.pc, 8))", + ) + ) + self.assertIn(r"b'\xeb\xfe\xeb\xfe\xeb\xfe\xeb\xfe'", res) + self.assertIn(r"will result in LAST-NOP (byte nr 0x1)", res) + self.assertNoException(res) + + + @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") + def test_cmd_nop_bytes_arg_nops_no_fit_force(self): + res = gdb_start_silent_cmd( + (r"pi gef.arch.nop_insn=b'\x90\x91\x92'", + "pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"), + after=( - f"{self.cmd} --b --n 4 $sp", - "pi print(f'*** *sp={u32(gef.memory.read(gef.arch.sp, 4)):#x}')", + f"{self.cmd} --i 5 --b --f", + "pi print(gef.memory.read(gef.arch.pc, 8))", ) ) + self.assertIn(r"b'\x90\x91\x92\x90\x91\xfe\xeb\xfe'", res) + self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res) + self.assertIn(r"will result in LAST-INSTRUCTION", res) self.assertNoException(res) - lines = findlines("*** *sp=", res) - self.assertEqual(len(lines), 2) - self.assertEqual(lines[1], "*** *sp=0x90909090") @pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}") def test_cmd_nop_as_bytes_invalid_end_address(self): # Make sure we error out if writing nops into an unmapped or RO area res = gdb_run_silent_cmd( - f"{self.cmd} --b --n 5 0x1337000+0x1000-4", + f"{self.cmd} --b --i 5 0x1337000+0x1000-4", target=_target("mmap-known-address") ) self.assertNoException(res) @@ -100,7 +293,7 @@ def test_cmd_nop_as_bytes_invalid_end_address(self): # We had an off-by-one bug where we couldn't write the last byte before # an unmapped area. Make sure that we can now. res = gdb_run_silent_cmd( - f"{self.cmd} --b --n 4 0x1337000+0x1000-4", + f"{self.cmd} --b --i 4 0x1337000+0x1000-4", target=_target("mmap-known-address"), after="pi print(f'*** *mem={u32(gef.memory.read(0x1337ffc, 4)):#x}')", )