fix: make mprotect command truly multi-arch (#1162)

* fix: make mprotect command truly multi-arch

Added register saving based on reg_sets defined for each processor architecture, additionally shellcraft is used to generate the arch-specific shellcode.

Unfortunately this command is not currently tested on platforms other than x86_64.

* Update pwndbg/commands/mprotect.py

Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>

* mprotect: Add parsing, alignment to the addr argument

This change makes sure that the addr argument is parsed as an gdb expression (so you can use registers for example) and aligns it to the nearest page boundary.

* mprotect: Clean up register saving, print the result

Cleaned up saving of registers and added printing of the results, as per disconnect's sugesstions.

* Simplify the test for mprotect

Simplify the code and remove the useless binary

* Update tests/test_mprotect.py

Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>
This commit is contained in:
alufers 2022-10-18 02:12:10 +02:00 committed by GitHub
parent c50ba4612e
commit 1c06c52b47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 58 additions and 69 deletions

View File

@ -11,9 +11,24 @@ import pwndbg.gdblib.file
import pwndbg.lib.which import pwndbg.lib.which
import pwndbg.wrappers.checksec import pwndbg.wrappers.checksec
import pwndbg.wrappers.readelf import pwndbg.wrappers.readelf
from pwndbg.lib.regs import reg_sets
parser = argparse.ArgumentParser(description="Calls mprotect. x86_64 only.") parser = argparse.ArgumentParser(
parser.add_argument("addr", help="Page-aligned address to all mprotect on.", type=int) description="""
Calls the mprotect syscall and prints its result value
Note that the mprotect syscall may fail for various reasons
(see `man mprotect`) and a non-zero error return value
can be decoded with the `errno <value>` command.
Examples:
mprotect $rsp PROT_READ|PROT_WRITE|PROT_EXEC
mprotect some_symbol PROT_NONE
"""
)
parser.add_argument(
"addr", help="Page-aligned address to all mprotect on.", type=pwndbg.commands.sloppy_gdb_parse
)
parser.add_argument( parser.add_argument(
"length", "length",
help="Count of bytes to call mprotect on. Needs " "to be multiple of page size.", help="Count of bytes to call mprotect on. Needs " "to be multiple of page size.",
@ -44,33 +59,42 @@ def prot_str_to_val(protstr):
@pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.ArgparsedCommand(parser)
@pwndbg.commands.OnlyWhenRunning @pwndbg.commands.OnlyWhenRunning
@pwndbg.commands.OnlyAmd64
def mprotect(addr, length, prot): def mprotect(addr, length, prot):
"""Only x86_64."""
saved_rax = pwndbg.gdblib.regs.rax
saved_rbx = pwndbg.gdblib.regs.rbx
saved_rcx = pwndbg.gdblib.regs.rcx
saved_rdx = pwndbg.gdblib.regs.rdx
saved_rip = pwndbg.gdblib.regs.rip
prot_int = prot_str_to_val(prot) prot_int = prot_str_to_val(prot)
shellcode_asm = pwnlib.shellcraft.syscall("SYS_mprotect", int(addr), int(length), int(prot_int)) # generate a shellcode that executes the mprotect syscall
shellcode_asm = pwnlib.shellcraft.syscall(
"SYS_mprotect", int(pwndbg.lib.memory.page_align(addr)), int(length), int(prot_int)
)
shellcode = asm.asm(shellcode_asm) shellcode = asm.asm(shellcode_asm)
saved_instruction_bytes = pwndbg.gdblib.memory.read(pwndbg.gdblib.regs.rip, len(shellcode)) # obtain the registers that need to be saved for the current platform
# we save the registers that are used for arguments, return value and the program counter
current_regs = reg_sets[pwndbg.gdblib.arch.current]
regs_to_save = current_regs.args + (current_regs.retval, current_regs.pc)
pwndbg.gdblib.memory.write(pwndbg.gdblib.regs.rip, shellcode) # save the registers
saved_registers = {reg: pwndbg.gdblib.regs[reg] for reg in regs_to_save}
# save the memory which will be overwritten by the shellcode
saved_instruction_bytes = pwndbg.gdblib.memory.read(
saved_registers[current_regs.pc], len(shellcode)
)
pwndbg.gdblib.memory.write(saved_registers[current_regs.pc], shellcode)
# execute syscall # execute syscall
gdb.execute("nextsyscall") gdb.execute("nextsyscall")
gdb.execute("stepi") gdb.execute("stepi")
# restore registers and memory # get the return value
pwndbg.gdblib.memory.write(saved_rip, saved_instruction_bytes) ret = pwndbg.gdblib.regs[current_regs.retval]
pwndbg.gdblib.regs.rax = saved_rax print("mprotect returned %d (%s)" % (ret, current_regs.retval))
pwndbg.gdblib.regs.rbx = saved_rbx
pwndbg.gdblib.regs.rcx = saved_rcx # restore registers and memory
pwndbg.gdblib.regs.rdx = saved_rdx pwndbg.gdblib.memory.write(saved_registers[current_regs.pc], saved_instruction_bytes)
pwndbg.gdblib.regs.rip = saved_rip
# restore the registers
for register, value in saved_registers.items():
setattr(pwndbg.gdblib.regs, register, value)

View File

@ -1,17 +0,0 @@
#include <stdlib.h>
#include <stdint.h>
//
// pwnlib.asm.asm(pwnlib.shellcraft.amd64.linux.echo("mprotect_ok"), arch='amd64').hex()
uint8_t func[] = {
0x36, 0x38, 0x35, 0x65, 0x36, 0x65, 0x36, 0x61, 0x30, 0x31, 0x38, 0x31, 0x33, 0x34, 0x32, 0x34, 0x30, 0x31, 0x30, 0x31, 0x30, 0x31, 0x30, 0x31, 0x34, 0x38, 0x62, 0x38, 0x36, 0x64, 0x37, 0x30, 0x37, 0x32, 0x36, 0x66, 0x37, 0x34, 0x36, 0x35, 0x36, 0x33, 0x37, 0x34, 0x35, 0x30, 0x36, 0x61, 0x30, 0x31, 0x35, 0x38, 0x36, 0x61, 0x30, 0x31, 0x35, 0x66, 0x36, 0x61, 0x30, 0x62, 0x35, 0x61, 0x34, 0x38, 0x38, 0x39, 0x65, 0x36, 0x30, 0x66, 0x30, 0x35, 0x33, 0x31, 0x66, 0x66, 0x36, 0x61, 0x33, 0x63, 0x35, 0x38, 0x30, 0x66, 0x30, 0x35
};
int main() {
void (*f)() = (void (*)())func;
printf("ptr = %p", f);
// f();
return 1;
}

View File

@ -3,46 +3,28 @@ import gdb
import pwndbg import pwndbg
import tests import tests
MPROTECT_BINARY = tests.binaries.get("mprotect.out") SMALL_BINARY = tests.binaries.get("crash_simple.out.hardcoded")
def test_mprotect(start_binary): def test_mprotect_executes_properly(start_binary):
""" """
Tests mprotect command Tests the mprotect command
It will mark some memory as executable, then this binary will print "mprotect_ok"
""" """
start_binary(MPROTECT_BINARY) start_binary(SMALL_BINARY)
gdb.execute("starti") pc = pwndbg.gdblib.regs.pc
# get addr of func
addr = int(gdb.parse_and_eval("&func"))
addr_aligned = pwndbg.lib.memory.page_align(addr)
# sizeof # Check if we can use mprotect with address provided as value
size = int(gdb.parse_and_eval("sizeof(func)")) # and to set page permissions to RWX
size_aligned = pwndbg.lib.memory.page_align(size) gdb.execute("mprotect %d 4096 PROT_EXEC|PROT_READ|PROT_WRITE" % pc)
vm = pwndbg.gdblib.vmmap.find(pc)
assert vm.read and vm.write and vm.execute
vmmaps_before = gdb.execute("vmmap -x", to_string=True).splitlines() # Check if we can use mprotect with address provided as register
# and to set page permissions to none
# mark memory as executable gdb.execute("mprotect $pc 0x1000 PROT_NONE")
gdb.execute( vm = pwndbg.gdblib.vmmap.find(pc)
"mprotect {} {} PROT_EXEC|PROT_READ|PROT_WRITE".format( assert not (vm.read and vm.write and vm.execute)
hex(addr_aligned), pwndbg.lib.memory.PAGE_SIZE
)
)
vmmaps_after = gdb.execute("vmmap -x", to_string=True).splitlines()
# expect vmmaps_after to be one element longer than vmmaps_before
assert len(vmmaps_after) == len(vmmaps_before) + 1
# get the changed vmmap entry
vmmap_entry = [x for x in vmmaps_after if x not in vmmaps_before][0]
assert vmmap_entry.split()[2] == "rwxp"
# continue execution
gdb.execute("continue")
def test_cannot_run_mprotect_when_not_running(start_binary): def test_cannot_run_mprotect_when_not_running(start_binary):