Skip to content

Commit

Permalink
Fix hardcoded NOP instructions for ARM/AARCH64 (#971)
Browse files Browse the repository at this point in the history
* fix nop insn of arm & arm64 to `hint #0`

* `gef.memory.write` should take an optional param for length

* [gef] fixed off by one in gdb_get_nth_next_instruction_address for fixed insn size archs

* replicate error message change to tests

* deprecating `gdb_get_nth_next_instruction_address()` in favor `gef_instruction_n()`

* bit more type hinting
  • Loading branch information
hugsy committed Jul 22, 2023
1 parent e529fbc commit 0461d6f
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 60 deletions.
46 changes: 26 additions & 20 deletions gef.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,11 @@ def is_valid(self) -> bool:
def size(self) -> int:
return len(self.opcodes)

def next(self) -> "Instruction":
address = self.address + self.size()
return gef_get_instruction_at(address)


@deprecated("Use GefHeapManager.find_main_arena_addr()")
def search_for_main_arena() -> int:
return GefHeapManager.find_main_arena_addr()
Expand Down Expand Up @@ -2013,6 +2018,9 @@ def gdb_disassemble(start_pc: int, **kwargs: int) -> Generator[Instruction, None
arch = frame.architecture()

for insn in arch.disassemble(start_pc, **kwargs):
assert isinstance(insn["addr"], int)
assert isinstance(insn["length"], int)
assert isinstance(insn["asm"], str)
address = insn["addr"]
asm = insn["asm"].rstrip().split(None, 1)
if len(asm) > 1:
Expand Down Expand Up @@ -2066,19 +2074,15 @@ def gdb_get_nth_previous_instruction_address(addr: int, n: int) -> Optional[int]
return None


@deprecated(solution="Use `gef_instruction_n().address`")
def gdb_get_nth_next_instruction_address(addr: int, n: int) -> int:
"""Return the address (Integer) of the `n`-th instruction after `addr`."""
# fixed-length ABI
if gef.arch.instruction_length:
return addr + n * gef.arch.instruction_length

# variable-length ABI
insn = list(gdb_disassemble(addr, count=n))[-1]
return insn.address
"""Return the address of the `n`-th instruction after `addr`. """
return gef_instruction_n(addr, n).address


def gef_instruction_n(addr: int, n: int) -> Instruction:
"""Return the `n`-th instruction after `addr` as an Instruction object."""
"""Return the `n`-th instruction after `addr` as an Instruction object. Note that `n` is treated as
an positive index, starting from 0 (current instruction address)"""
return list(gdb_disassemble(addr, count=n + 1))[n]


Expand Down Expand Up @@ -2509,8 +2513,7 @@ class ARM(Architecture):
"$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp",
"$lr", "$pc", "$cpsr",)

# https://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/Caccegih.html
nop_insn = b"\x01\x10\xa0\xe1" # mov r1, r1
nop_insn = b"\x00\xf0\x20\xe3" # hint #0
return_register = "$r0"
flag_register: str = "$cpsr"
flags_table = {
Expand Down Expand Up @@ -2675,6 +2678,7 @@ class AARCH64(ARM):
5: "t32",
4: "m[4]",
}
nop_insn = b"\x1f\x20\x03\xd5" # hint #0
function_parameters = ("$x0", "$x1", "$x2", "$x3", "$x4", "$x5", "$x6", "$x7",)
syscall_register = "$x8"
syscall_instructions = ("svc $x0",)
Expand Down Expand Up @@ -6029,8 +6033,8 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None:
address = parse_address(args.address)
num_instructions = args.n

last_addr = gdb_get_nth_next_instruction_address(address, num_instructions)
total_bytes = (last_addr - address) + gef_get_instruction_at(last_addr).size()
last_insn = gef_instruction_n(address, num_instructions-1)
total_bytes = (last_insn.address - address) + last_insn.size()
target_addr = address + total_bytes

info(f"skipping {num_instructions} instructions ({total_bytes} bytes) from {address:#x} to {target_addr:#x}")
Expand Down Expand Up @@ -6068,13 +6072,13 @@ 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.i or 1
fill_bytes = args.b
fill_nops = args.n
force_flag = args.f or False
num_items = int(args.i) or 1
fill_bytes = bool(args.b)
fill_nops = bool(args.n)
force_flag = bool(args.f) or False

if fill_nops and fill_bytes:
err("only is possible specify --b or --n at same time")
err("--b and --n cannot be specified at the same time.")
return

total_bytes = 0
Expand All @@ -6084,7 +6088,8 @@ def do_invoke(self, _: List[str], **kwargs: Any) -> None:
total_bytes = num_items * len(nop)
else:
try:
last_addr = gdb_get_nth_next_instruction_address(address, num_items)
last_insn = gef_instruction_n(address, num_items-1)
last_addr = last_insn.address
except:
err(f"Cannot patch instruction at {address:#x} reaching unmapped area")
return
Expand Down Expand Up @@ -10274,8 +10279,9 @@ def reset_caches(self) -> None:
self.__maps = None
return

def write(self, address: int, buffer: ByteString, length: int = 0x10) -> None:
def write(self, address: int, buffer: ByteString, length: Optional[int] = None) -> None:
"""Write `buffer` at address `address`."""
length = length or len(buffer)
gdb.selected_inferior().write_memory(address, buffer, length)

def read(self, addr: int, length: int = 0x10) -> bytes:
Expand Down
80 changes: 40 additions & 40 deletions tests/commands/nop.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,45 @@ def test_cmd_nop_inactive(self):
def test_cmd_nop_no_arg(self):

res = gdb_start_silent_cmd(
"pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))",
"pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))",
after=(
self.cmd,
"pi print(gef.memory.read(gef.arch.pc, 4))",
"pi print(gef.memory.read(gef.arch.pc, 4))",
)
)
self.assertNoException(res)
self.assertIn(r"\x90\x90\xeb\xfe", 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_check_b_and_n_same_time(self):

res = gdb_start_silent_cmd(f"{self.cmd} --b --n")
self.assertNoException(res)
self.assertIn(r"-b or --n at same time", res)
self.assertIn(r"--b and --n cannot be specified at the same time.", 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))"),
"pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"),

after=(
self.cmd,
"pi print(gef.memory.read(gef.arch.pc, 4))",
"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)
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))"),
"pi gef.memory.write(gef.arch.pc, p32(0xfeebfeeb))"),

after=(
f"{self.cmd} --f",
Expand All @@ -71,36 +71,36 @@ def test_cmd_nop_force_arg_break_instruction(self):
)
self.assertNoException(res)
self.assertIn(r"will result in LAST-NOP (byte nr 0x2)", res)
self.assertIn(r"\x90\x91\xeb\xfe", 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))",
"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))",
"pi print(gef.memory.read(gef.arch.pc+1, 8))",
)
)
self.assertNoException(res)
self.assertIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res)
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_i_arg_reaching_unmapped_area(self):

res = gdb_start_silent_cmd(
"pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))",
"pi gef.memory.write(gef.arch.pc+1, p64(0xfeebfeebfeebfeeb))",
after=(
f"{self.cmd} --i 2000000000000000000000000000000000000 $pc+1",
"pi print(gef.memory.read(gef.arch.pc+1, 8))",
"pi print(gef.memory.read(gef.arch.pc+1, 8))",
)
)
self.assertIn(r"reaching unmapped area", res)
self.assertNoException(res)
self.assertNotIn(r"\x90\x90\x90\x90\xeb\xfe\xeb\xfe", res)
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}")
Expand All @@ -116,10 +116,10 @@ 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_nop(self):
res = gdb_start_silent_cmd(
"pi gef.memory.write(gef.arch.pc, p32(0x9191))",
"pi gef.memory.write(gef.arch.pc, p32(0x9191))",
after=(
f"{self.cmd} --n",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)
self.assertIn(r"\x90\x91", res)
Expand All @@ -129,10 +129,10 @@ def test_cmd_nop_nop(self):
@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))",
"pi gef.memory.write(gef.arch.pc, p16(0xfeeb))",
after=(
f"{self.cmd} --n",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)
self.assertIn(r"will result in LAST-INSTRUCTION", res)
Expand All @@ -143,10 +143,10 @@ def test_cmd_nop_nop_break_instruction(self):
@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))",
"pi gef.memory.write(gef.arch.pc, p16(0xfeeb))",
after=(
f"{self.cmd} --n --f",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)
self.assertIn(r"will result in LAST-INSTRUCTION", res)
Expand All @@ -157,10 +157,10 @@ def test_cmd_nop_nop_break_instruction_force(self):
@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))",
"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))",
"pi print(gef.memory.read(gef.arch.pc, 8))",
)
)
self.assertIn(r"b'\x90\x90\x90\x90\xeb\xfe\xeb\xfe'", res)
Expand All @@ -171,7 +171,7 @@ def test_cmd_nop_nop_arg(self):
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))"),
"pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"),

after=(
f"{self.cmd} --n",
Expand All @@ -180,14 +180,14 @@ def test_cmd_nop_nop_arg_multibnop_breaks(self):
)
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)
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))"),
"pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"),

after=(
f"{self.cmd} --n --f",
Expand All @@ -196,16 +196,16 @@ def test_cmd_nop_nop_arg_multibnop_breaks_force(self):
)
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)
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))",
"pi gef.memory.write(gef.arch.pc, p16(0x9191))",
after=(
f"{self.cmd} --b",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)

Expand All @@ -216,10 +216,10 @@ def test_cmd_nop_bytes(self):
@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 gef.memory.write(gef.arch.pc, p16(0xfeeb))",
"pi gef.memory.write(gef.arch.pc, p16(0xfeeb))",
after=(
f"{self.cmd} --b",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)

Expand All @@ -231,10 +231,10 @@ def test_cmd_nop_bytes_break_instruction(self):
@pytest.mark.skipif(ARCH not in ("i686", "x86_64"), reason=f"Skipped for {ARCH}")
def test_cmd_nop_bytes_break_instruction_force(self):
res = gdb_start_silent_cmd(
"pi gef.memory.write(gef.arch.pc, p16(0xfeeb))",
"pi gef.memory.write(gef.arch.pc, p16(0xfeeb))",
after=(
f"{self.cmd} --b --f",
"pi print(gef.memory.read(gef.arch.pc, 2))",
"pi print(gef.memory.read(gef.arch.pc, 2))",
)
)
self.assertIn(r"will result in LAST-INSTRUCTION", res)
Expand All @@ -245,10 +245,10 @@ def test_cmd_nop_bytes_break_instruction_force(self):
@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))",
"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))",
"pi print(gef.memory.read(gef.arch.pc, 8))",
)
)
self.assertIn(r"b'\x90\x90\xeb\xfe\xeb\xfe\xeb\xfe'", res)
Expand All @@ -259,11 +259,11 @@ def test_cmd_nop_bytes_arg(self):
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))"),
"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))",
"pi print(gef.memory.read(gef.arch.pc, 8))",
)
)
self.assertIn(r"b'\xeb\xfe\xeb\xfe\xeb\xfe\xeb\xfe'", res)
Expand All @@ -274,12 +274,12 @@ def test_cmd_nop_bytes_arg_nops_no_fit(self):
@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))"),
(r"pi gef.arch.nop_insn=b'\x90\x91\x92'",
"pi gef.memory.write(gef.arch.pc, p64(0xfeebfeebfeebfeeb))"),

after=(
f"{self.cmd} --i 5 --b --f",
"pi print(gef.memory.read(gef.arch.pc, 8))",
"pi print(gef.memory.read(gef.arch.pc, 8))",
)
)
self.assertIn(r"b'\x90\x91\x92\x90\x91\xfe\xeb\xfe'", res)
Expand Down

0 comments on commit 0461d6f

Please sign in to comment.