Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mmap command that executes the mmap syscall in the inferior #1952

Merged
merged 16 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pwndbg/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ def load_commands() -> None:
import pwndbg.commands.leakfind
import pwndbg.commands.memoize
import pwndbg.commands.misc
import pwndbg.commands.mmap
import pwndbg.commands.mprotect
import pwndbg.commands.nearpc
import pwndbg.commands.next
Expand Down
239 changes: 239 additions & 0 deletions pwndbg/commands/mmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
from __future__ import annotations

import argparse

import pwndbg.chain
import pwndbg.color.message as message
import pwndbg.commands
import pwndbg.enhance
import pwndbg.gdblib.file
import pwndbg.gdblib.shellcode
import pwndbg.lib.memory
import pwndbg.wrappers.checksec
import pwndbg.wrappers.readelf
from pwndbg.commands import CommandCategory

parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description="""
Calls the mmap syscall and prints its resulting address.

Note that the mmap syscall may fail for various reasons
(see `man mmap`) and, in case of failure, its return value
will not be a valid pointer.

PROT values: NONE (0), READ (1), WRITE (2), EXEC (4)
MAP values: SHARED (1), PRIVATE (2), SHARED_VALIDATE (3), FIXED (0x10),
ANONYMOUS (0x20)

Flags and protection values can be either a string containing the names of the
flags or permissions or a single number corresponding to the bitwise OR of the
protection and flag numbers.

Examples:
mmap 0x0 4096 PROT_READ|PROT_WRITE|PROT_EXEC MAP_PRIVATE|MAP_ANONYMOUS -1 0
- Maps a new private+anonymous page with RWX permissions at a location
decided by the kernel.

mmap 0x0 4096 PROT_READ MAP_PRIVATE 10 0
- Maps 4096 bytes of the file pointed to by file descriptor number 10 with
read permission at a location decided by the kernel.

mmap 0xdeadbeef 0x1000
- Maps a new private+anonymous page with RWX permissions at a page boundary
near 0xdeadbeef.
""",
)
parser.add_argument(
"addr", help="Address hint to be given to mmap.", type=pwndbg.commands.sloppy_gdb_parse
)
parser.add_argument(
"length",
help="Length of the mapping, in bytes. Needs to be greater than zero.",
type=int,
)
parser.add_argument(
"prot",
help='Prot enum or int as in mmap(2). Eg. "PROT_READ|PROT_EXEC" or 7 (for RWX).',
type=str,
nargs="?",
default="7",
)
parser.add_argument(
"flags",
help='Flags enum or int as in mmap(2). Eg. "MAP_PRIVATE|MAP_ANONYMOUS" or 0x22.',
type=str,
nargs="?",
default="0x22",
)
parser.add_argument(
"fd",
help="File descriptor of the file to be mapped, or -1 if using MAP_ANONYMOUS.",
type=int,
nargs="?",
default=-1,
)
parser.add_argument(
"offset",
help="Offset from the start of the file, in bytes, if using file based mapping.",
type=int,
nargs="?",
default=0,
)
parser.add_argument(
"--quiet", "-q", help="Disable address validity warnings and hints", action="store_true"
)
parser.add_argument(
"--force", "-f", help="Force potentially unsafe actions to happen", action="store_true"
)


prot_dict = {
"PROT_NONE": 0x0,
"PROT_READ": 0x1,
"PROT_WRITE": 0x2,
"PROT_EXEC": 0x4,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe also support RWX or rwx?


flag_dict = {
"MAP_SHARED": 0x1,
"MAP_PRIVATE": 0x2,
"MAP_SHARED_VALIDATE": 0x3,
"MAP_FIXED": 0x10,
"MAP_ANONYMOUS": 0x20,
}


def prot_str_to_val(protstr):
"""Heuristic to convert PROT_EXEC|PROT_WRITE to integer value."""
prot_int = 0

Check warning on line 109 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L109

Added line #L109 was not covered by tests
for k, v in prot_dict.items():
if k in protstr:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw PROT_EXECasdf will also work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, which isn't optimal, but I figured that if it's good enough for mprotect, it's probably good enough here too. But I could make it stricter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its probably okay for now

prot_int |= v
return prot_int

Check warning on line 113 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L112-L113

Added lines #L112 - L113 were not covered by tests


def flag_str_to_val(flagstr):
"""Heuristic to convert MAP_SHARED|MAP_FIXED to integer value."""
flag_int = 0

Check warning on line 118 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L118

Added line #L118 was not covered by tests
for k, v in flag_dict.items():
if k in flagstr:
flag_int |= v
return flag_int

Check warning on line 122 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L121-L122

Added lines #L121 - L122 were not covered by tests


def parse_str_or_int(val, parser):
"""
Try parsing a string with one of the parsers above or by converting it to
an int, or passes the value through if it is already an integer.
"""
if type(val) == str:
disconnect3d marked this conversation as resolved.
Show resolved Hide resolved
candidate = parser(val)

Check warning on line 131 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L131

Added line #L131 was not covered by tests
if candidate != 0:
return candidate
return int(val, 0)

Check warning on line 134 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L133-L134

Added lines #L133 - L134 were not covered by tests
elif type(val) == int:
disconnect3d marked this conversation as resolved.
Show resolved Hide resolved
return val

Check warning on line 136 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L136

Added line #L136 was not covered by tests
else:
# Getting here is a bug, we shouldn't be seeing other types at all.
raise TypeError("invalid type for value: {type(val)}")

Check warning on line 139 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L139

Added line #L139 was not covered by tests
disconnect3d marked this conversation as resolved.
Show resolved Hide resolved


@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.MEMORY)
@pwndbg.commands.OnlyWhenRunning
def mmap(addr, length, prot=7, flags=0x22, fd=-1, offset=0, quiet=False, force=False) -> None:
try:
prot_int = parse_str_or_int(prot, prot_str_to_val)
except ValueError as e:
print(message.error(f'Invalid protection value "{prot}": {e}'))

Check warning on line 148 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L145-L148

Added lines #L145 - L148 were not covered by tests
disconnect3d marked this conversation as resolved.
Show resolved Hide resolved

try:
flag_int = parse_str_or_int(flags, flag_str_to_val)
except ValueError as e:
print(message.error(f'Invalid flags value "{flags}": {e}'))

Check warning on line 153 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L150-L153

Added lines #L150 - L153 were not covered by tests
disconnect3d marked this conversation as resolved.
Show resolved Hide resolved

aligned_addr = int(pwndbg.lib.memory.page_align(addr))

Check warning on line 155 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L155

Added line #L155 was not covered by tests
if flag_int & flag_dict["MAP_FIXED"] != 0:
# When using MAP_FIXED, it's only safe to call mmap(2) when the address
# overlaps no other maps. We want to make sure that, unless the user
# _really_ knows what they're doing, this call will be safe.
#
# Additionally, it's nice to highlight cases where the call is likely
# to fail because the address is not properly aligned.
addr = int(addr)

Check warning on line 163 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L163

Added line #L163 was not covered by tests
if addr != aligned_addr and not quiet:
print(

Check warning on line 165 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L165

Added line #L165 was not covered by tests
message.warn(
f"""\
Address {addr:#x} is not properly aligned. Calling mmap with MAP_FIXED and an
unaligned address is likely to fail. Consider using the address {aligned_addr:#x}
instead.\
"""
)
)

page = pwndbg.lib.memory.Page(addr, int(length), 0, 0)
collisions = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, there should never be more than a single collision (in practice if we have broken vmmap info there will be). We can safely assume a single collision here. Its fine if we print a single one if there are more

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I follow. If any range is permissible for our mmap, it could, in the worst case, technically collide with all of the mappings, no? Nothing's really stopping you from trying to call this with MAP_FIXED for [0, 0xffffffffffffffff[ (I know in practice there are limitations on covering the whole range, but this same logic applies for smaller ranges too).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... you are right. For some reason I was thinking that we map a single address instead of a range. Forgive my stupidness :)

vm = pwndbg.gdblib.vmmap.get()

Check warning on line 177 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L175-L177

Added lines #L175 - L177 were not covered by tests
for i in range(len(vm)):
cand = vm[i]

Check warning on line 179 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L179

Added line #L179 was not covered by tests
if cand.end > page.start and cand.start < page.end:
collisions.append(cand)

Check warning on line 181 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L181

Added line #L181 was not covered by tests
if cand.start >= page.end:
# No more collisions are possible.
break

Check warning on line 184 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L184

Added line #L184 was not covered by tests

if len(collisions) > 0:
m = message.error if not force else message.warn

Check warning on line 187 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L187

Added line #L187 was not covered by tests

if not force or not quiet:
print(

Check warning on line 190 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L190

Added line #L190 was not covered by tests
m(
f"""\
Trying to mmap with MAP_FIXED for an address range that collides with {len(collisions)}
existing range{'s' if len(collisions) > 1 else ''}:\
"""
)
)
for c in collisions:
print(m(f" {c}"))
print(

Check warning on line 200 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L199-L200

Added lines #L199 - L200 were not covered by tests
m(
"""
This operation is destructive and will delete all of the listed mappings.\
"""
)
)
if not force:
print(

Check warning on line 208 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L208

Added line #L208 was not covered by tests
m(
"Run this command again with `--force` if you still \
wish to proceed."
)
)
return

Check warning on line 214 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L214

Added line #L214 was not covered by tests

elif int(addr) != aligned_addr and not quiet:
# Highlight to the user that the address they've specified is likely to
# be changed by the kernel.
print(

Check warning on line 219 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L219

Added line #L219 was not covered by tests
message.warn(
f"""\
Address {addr:#x} is not properly aligned. It is likely to be changed to an
aligned address by the kernel automatically. If this is not desired, consider
using the address {aligned_addr:#x} instead.\
"""
)
)

pointer = pwndbg.gdblib.shellcode.exec_syscall(

Check warning on line 229 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L229

Added line #L229 was not covered by tests
"SYS_mmap",
int(pwndbg.lib.memory.page_align(addr)),
int(length),
prot_int,
flag_int,
int(fd),
int(offset),
)

print(f"mmap returned {pointer:#x}")

Check warning on line 239 in pwndbg/commands/mmap.py

View check run for this annotation

Codecov / codecov/patch

pwndbg/commands/mmap.py#L239

Added line #L239 was not covered by tests
41 changes: 3 additions & 38 deletions pwndbg/commands/mprotect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

import argparse

import gdb
import pwnlib
from pwnlib import asm

import pwndbg.chain
import pwndbg.commands
import pwndbg.enhance
import pwndbg.gdblib.file
import pwndbg.gdblib.shellcode
import pwndbg.wrappers.checksec
import pwndbg.wrappers.readelf
from pwndbg.commands import CommandCategory
from pwndbg.lib.regs import reg_sets

parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
Expand Down Expand Up @@ -65,38 +61,7 @@ def prot_str_to_val(protstr):
def mprotect(addr, length, prot) -> None:
prot_int = prot_str_to_val(prot)

# generate a shellcode that executes the mprotect syscall
shellcode_asm = pwnlib.shellcraft.syscall(
ret = pwndbg.gdblib.shellcode.exec_syscall(
"SYS_mprotect", int(pwndbg.lib.memory.page_align(addr)), int(length), int(prot_int)
)
shellcode = asm.asm(shellcode_asm)

# 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)

# 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
gdb.execute("nextsyscall")
gdb.execute("stepi")

# get the return value
ret = pwndbg.gdblib.regs[current_regs.retval]

print("mprotect returned %d (%s)" % (ret, current_regs.retval))

# restore registers and memory
pwndbg.gdblib.memory.write(saved_registers[current_regs.pc], saved_instruction_bytes)

# restore the registers
for register, value in saved_registers.items():
setattr(pwndbg.gdblib.regs, register, value)
print(f"mprotect returned {ret}")
Loading
Loading