Enhance got command (#1771)

* Refactor the `got` command to support more use cases

- Create some function to parse the information of loaded shared object libraries from `info sharedlibrary`
- Make got command can show the entries of other libraries loaded in memory
- Make got command can show more various relocations to support not only the `JUMP_SLOT` type relocation but also supports `IRELATIVE` and `GLOB_DAT` type relocation.

* Update tests for the `got` command

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update pwndbg/commands/got.py

* Update the comment

https://github.com/pwndbg/pwndbg/pull/1771#discussion_r1251054080

* Update the tests

* Add some hints for the qemu users

---------

Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>
pull/1786/head
Alan Li 2 years ago committed by GitHub
parent 976363a3d8
commit d7d54cb895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,8 +1,9 @@
import pwndbg.commands
import pwndbg.gdblib.file
import pwndbg.wrappers.checksec
@pwndbg.commands.ArgparsedCommand("Prints out the binary security settings using `checksec`.")
@pwndbg.commands.OnlyWithFile
def checksec() -> None:
print(pwndbg.wrappers.checksec.get_raw_out())
print(pwndbg.wrappers.checksec.get_raw_out(pwndbg.gdblib.file.get_proc_exe_file()))

@ -1,54 +1,177 @@
import argparse
from elftools.elf.elffile import ELFFile
import pwndbg.chain
import pwndbg.color.memory as M
import pwndbg.commands
import pwndbg.enhance
import pwndbg.gdblib.arch
import pwndbg.gdblib.file
import pwndbg.gdblib.info
import pwndbg.gdblib.proc
import pwndbg.gdblib.qemu
import pwndbg.gdblib.vmmap
import pwndbg.wrappers.checksec
import pwndbg.wrappers.readelf
from pwndbg.color import message
from pwndbg.commands import CommandCategory
from pwndbg.wrappers.readelf import RelocationType
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description="""Show the state of the Global Offset Table.
parser = argparse.ArgumentParser(description="Show the state of the Global Offset Table.")
Examples:
got
got puts
got -p libc
got -a
""",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p",
"--path",
help="Filter results by library/objfile path.",
type=str,
default="",
dest="path_filter",
)
group.add_argument(
"-a",
"--all",
help="Process all libs/obfjiles including the target executable.",
action="store_true",
default=False,
dest="all_",
)
parser.add_argument(
"-r",
"--show-readonly",
help="Also display read-only entries (which are filtered out by default).",
action="store_true",
default=False,
dest="accept_readonly",
)
parser.add_argument(
"name_filter", help="Filter results by passed name.", type=str, nargs="?", default=""
"symbol_filter", help="Filter results by symbol name.", type=str, nargs="?", default=""
)
@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.LINUX)
@pwndbg.commands.OnlyWhenRunning
def got(name_filter="") -> None:
relro_status = pwndbg.wrappers.checksec.relro_status()
pie_status = pwndbg.wrappers.checksec.pie_status()
jmpslots = list(pwndbg.wrappers.readelf.get_jmpslots())
if not jmpslots:
print(message.error("NO JUMP_SLOT entries available in the GOT"))
def got(path_filter, all_, accept_readonly, symbol_filter) -> None:
if pwndbg.gdblib.qemu.is_qemu_usermode():
print(
"QEMU target detected - the result might not be accurate when checking if the entry is writable and getting the information for libraries/objfiles"
)
print()
# Show the filters we are using
if path_filter:
print("Filtering by lib/objfile path: " + message.hint(path_filter))
if symbol_filter:
print("Filtering by symbol name: " + message.hint(symbol_filter))
if not accept_readonly:
print("Filtering out read-only entries (display them with -r or --show-readonly)")
if path_filter or not accept_readonly or symbol_filter:
print()
# Calculate the base address
if not path_filter:
first_print = False
_got(pwndbg.gdblib.proc.exe, accept_readonly, symbol_filter)
else:
first_print = True
if not all_ and not path_filter:
return
# TODO: We might fail to find shared libraries if GDB can't find them (can't show them in `info sharedlibrary`)
paths = pwndbg.gdblib.info.sharedlibrary_paths()
for path in paths:
if path_filter not in path:
continue
if not first_print:
print()
first_print = False
_got(path, accept_readonly, symbol_filter)
# Maybe user have a typo or something in the path filter, show the available shared libraries
if first_print and path_filter:
print(message.error("No shared library matching the path filter found."))
if paths:
print(message.notice("Available shared libraries:"))
for path in paths:
print(" " + path)
if "PIE enabled" in pie_status:
bin_base = pwndbg.gdblib.proc.binary_base_addr
def _got(path, accept_readonly, symbol_filter) -> None:
# Maybe download the file from remote
local_path = pwndbg.gdblib.file.get_file(path, try_local_path=True)
relro_status = pwndbg.wrappers.checksec.relro_status(local_path)
pie_status = pwndbg.wrappers.checksec.pie_status(local_path)
got_entry = pwndbg.wrappers.readelf.get_got_entry(local_path)
# The following code is inspired by the "got" command of https://github.com/bata24/gef/blob/dev/gef.py by @bata24, thank you!
# TODO/FIXME: Maybe a -v option to show more information will be better
outputs = []
if path == pwndbg.gdblib.proc.exe:
bin_base_offset = pwndbg.gdblib.proc.binary_base_addr if "PIE enabled" in pie_status else 0
else:
# TODO/FIXME: Is there a better way to get the base address of the loaded shared library?
# I guess parsing the vmmap result might also work, but what if it's not reliable or not available? (e.g. debugging with qemu-user)
text_section_addr = pwndbg.gdblib.info.parsed_sharedlibrary()[path][0]
with open(local_path, "rb") as f:
bin_base_offset = (
text_section_addr - ELFFile(f).get_section_by_name(".text").header["sh_addr"]
)
# Parse the output of readelf line by line
for category, lines in got_entry.items():
for line in lines:
# line might be something like:
# 00000000001ec018 0000000000000025 R_X86_64_IRELATIVE a0480
# or something like:
# 00000000001ec030 0000020a00000007 R_X86_64_JUMP_SLOT 000000000009ae80 realloc@@GLIBC_2.2.5 + 0
offset, _, rtype, *rest = line.split()[:5]
if len(rest) == 1:
value = rest[0]
name = ""
else:
value, name = rest
address = int(offset, 16) + bin_base_offset
# TODO/FIXME: This check might not work correctly if we failed to get the correct vmmap result
if not accept_readonly and not pwndbg.gdblib.vmmap.find(address).write:
continue
if not name and category == RelocationType.IRELATIVE:
# TODO/FIXME: I don't know the naming logic behind this yet, I'm just modifying @bata24's code here :p
# We might need to add some comments here to explain the logic in the future, and also fix it if something wrong
if pwndbg.gdblib.arch.name == "i386":
name = "*ABS*"
else:
name = f"*ABS*+0x{int(value, 16):x}"
if symbol_filter not in name:
continue
outputs.append(
{
"name": name or "????",
"address": address,
}
)
# By sorting the outputs by address, we can get a more intuitive output
outputs.sort(key=lambda x: x["address"])
relro_color = message.off
if "Partial" in relro_status:
relro_color = message.warn
elif "Full" in relro_status:
relro_color = message.on
print("GOT protection: %s | GOT functions: %d" % (relro_color(relro_status), len(jmpslots)))
for line in jmpslots:
address, info, rtype, value, name = line.split()[:5]
if name_filter not in name:
continue
address_val = int(address, 16)
if (
"PIE enabled" in pie_status
): # if PIE, address is only the offset from the binary base address
address_val = bin_base + address_val
got_address = pwndbg.gdblib.memory.pvoid(address_val)
print(f"State of the GOT of {message.notice(path)}:")
print(
f"GOT protection: {relro_color(relro_status)} | Found {message.hint(len(outputs))} GOT entries passing the filter"
)
for output in outputs:
print(
"[0x%x] %s -> %s" % (address_val, message.hint(name), pwndbg.chain.format(got_address))
f"[{M.get(output['address'])}] {message.hint(output['name'])} -> {pwndbg.chain.format(pwndbg.gdblib.memory.pvoid(output['address']))}"
)

@ -3,7 +3,10 @@ Runs a few useful commands which are available under "info".
"""
import re
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
import gdb
@ -50,6 +53,34 @@ def sharedlibrary():
return ""
def parsed_sharedlibrary() -> Dict[str, Tuple[int, int]]:
"""
Returns a dictionary of shared libraries with their .text section from and to addresses.
"""
lines = sharedlibrary().splitlines()
if len(lines) <= 1:
return {}
result = {}
for line in lines:
# We only need to parse the lines starting with "0x", for example:
# 0x00007fc8fd01b630 0x00007fc8fd19027d Yes /lib/x86_64-linux-gnu/libc.so.6
# or something like:
# 0x00007fc8fd01b630 0x00007fc8fd19027d Yes (*) /lib/x86_64-linux-gnu/libc.so.6
if not line.startswith("0x"):
continue
from_, to, _, rest = line.split(maxsplit=3)
path = rest.lstrip("(*)").lstrip()
result[path] = (int(from_, 0), int(to, 0))
return result
def sharedlibrary_paths() -> List[str]:
"""
Get the paths of all shared libraries loaded in the process by parsing the output of "info sharedlibrary".
"""
return list(parsed_sharedlibrary().keys())
def address(symbol: str) -> Optional[int]:
try:
res = gdb.execute(f"info address {symbol}", to_string=True)

@ -76,19 +76,8 @@ def get_libc_filename_from_info_sharedlibrary() -> Optional[str]:
"""
Get the filename of the libc by parsing the output of `info sharedlibrary`.
"""
# Try to parse the output of `info sharedlibrary`:
# pwndbg> |info sharedlibrary| grep libc
# 0x00007f9ade418700 0x00007f9ade58f47d Yes ./libc.so.6
# Or:
# pwndbg> |info sharedlibrary| grep libc
# 0x00007f9ade418700 0x00007f9ade58f47d Yes (*) ./libc.so.6
possible_libc_path = []
for line in pwndbg.gdblib.info.sharedlibrary().splitlines()[1:]:
if line.startswith("("):
# footer line:
# (*): Shared library is missing debugging information.
break
path = line.split(maxsplit=3)[-1].lstrip("(*)").lstrip()
for path in pwndbg.gdblib.info.sharedlibrary_paths():
basename = os.path.basename(
path[7:] if path.startswith("target:") else path
) # "target:" prefix is for remote debugging

@ -10,8 +10,7 @@ cmd_pwntools = ["pwn", "checksec"]
@pwndbg.wrappers.OnlyWithCommand(cmd_name, cmd_pwntools)
@pwndbg.lib.cache.cache_until("objfile")
def get_raw_out():
local_path = pwndbg.gdblib.file.get_proc_exe_file()
def get_raw_out(local_path: str) -> str:
try:
return pwndbg.wrappers.call_cmd(get_raw_out.cmd + ["--file=" + local_path])
except CalledProcessError:
@ -24,9 +23,9 @@ def get_raw_out():
@pwndbg.wrappers.OnlyWithCommand(cmd_name, cmd_pwntools)
def relro_status():
def relro_status(local_path: str) -> str:
relro = "No RELRO"
out = get_raw_out()
out = get_raw_out(local_path)
if "Full RELRO" in out:
relro = "Full RELRO"
@ -37,9 +36,9 @@ def relro_status():
@pwndbg.wrappers.OnlyWithCommand(cmd_name, cmd_pwntools)
def pie_status():
def pie_status(local_path) -> str:
pie = "No PIE"
out = get_raw_out()
out = get_raw_out(local_path)
if "PIE enabled" in out:
pie = "PIE enabled"

@ -1,29 +1,33 @@
from enum import Enum
from typing import Dict
from typing import List
import pwndbg.wrappers
cmd_name = "readelf"
@pwndbg.wrappers.OnlyWithCommand(cmd_name)
def get_jmpslots():
local_path = pwndbg.gdblib.file.get_proc_exe_file()
cmd = get_jmpslots.cmd + ["--relocs", local_path]
readelf_out = pwndbg.wrappers.call_cmd(cmd)
return filter(_extract_jumps, readelf_out.splitlines())
class RelocationType(Enum):
# For x86_64, some details about these flag can be found in 4.4.1 Relocation Types in https://www.intel.com/content/dam/develop/external/us/en/documents/mpx-linux64-abi.pdf
# The definitions of these flags can be found in this file: https://elixir.bootlin.com/glibc/glibc-2.37/source/elf/elf.h
JUMP_SLOT = 1 # e.g.: R_X86_64_JUMP_SLOT
GLOB_DAT = 2 # e.g.: R_X86_64_GLOB_DAT
IRELATIVE = 3 # e.g.: R_X86_64_IRELATIVE
def _extract_jumps(line):
"""
Checks for records in `readelf --relocs <binary>` which has type e.g. `R_X86_64_JUMP_SLO`
NOTE: Because of that we DO NOT display entries that are not writeable (due to FULL RELRO)
as they have `R_X86_64_GLOB_DAT` type.
@pwndbg.wrappers.OnlyWithCommand(cmd_name)
def get_got_entry(local_path: str) -> Dict[RelocationType, List[str]]:
# --wide is for showing the full information, e.g.: R_X86_64_JUMP_SLOT instead of R_X86_64_JUMP_SLO
cmd = get_got_entry.cmd + ["--relocs", "--wide", local_path]
readelf_out = pwndbg.wrappers.call_cmd(cmd)
It might be good to display them separately in the future.
"""
try:
if "JUMP" in line.split()[2]:
return line
else:
return False
except IndexError:
return False
entries: Dict[RelocationType, List[str]] = {category: [] for category in RelocationType}
for line in readelf_out.splitlines():
if not line or not line[0].isdigit():
continue
category = line.split()[2]
# TODO/FIXME: There's a bug here, somehow the IRELATIVE relocation might point to somewhere in .data.rel.ro, which is not in .got or .got.plt
for c in RelocationType:
if c.name in category:
entries[c].append(line)
return entries

@ -1,4 +1,5 @@
import re
from pathlib import Path
import gdb
import pytest
@ -18,7 +19,12 @@ def test_commands_plt_gotplt_got_when_no_sections(start_binary):
assert gdb.execute("gotplt", to_string=True) == "Could not find section .got.plt\n"
# got.py command
assert gdb.execute("got", to_string=True) == "NO JUMP_SLOT entries available in the GOT\n"
out = gdb.execute("got", to_string=True).splitlines()
assert len(out) == 4
assert out[0] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[1] == ""
assert out[2] == f"State of the GOT of {Path.cwd() / NO_SECTS_BINARY}:"
assert out[3] == "GOT protection: No RELRO | Found 0 GOT entries passing the filter"
@pytest.mark.parametrize(
@ -51,7 +57,7 @@ def test_command_plt(binary_name, is_pie):
@pytest.mark.parametrize(
"binary_name,is_pie", ((PIE_BINARY_WITH_PLT, True), (NOPIE_BINARY_WITH_PLT, False))
)
def test_command_got(binary_name, is_pie):
def test_command_got_for_target_binary(binary_name, is_pie):
binary = tests.binaries.get(binary_name)
gdb.execute(f"file {binary}")
@ -59,12 +65,177 @@ def test_command_got(binary_name, is_pie):
assert out == ["got: The program is not being run."]
gdb.execute("break main")
gdb.execute("starti")
out2 = gdb.execute("got", to_string=True).splitlines()
out = gdb.execute("got", to_string=True).splitlines()
# TODO/FIXME: We need to verify the addresses are correct or not
# Before resolving symbols' addresses, .got and .got.plt are writable
assert len(out) == 7
assert out[0] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[1] == ""
assert out[2] == f"State of the GOT of {Path.cwd() / binary}:"
assert out[3] == "GOT protection: Full RELRO | Found 3 GOT entries passing the filter"
assert re.match(r"\[0x[0-9a-f]+\] __libc_start_main@GLIBC_[0-9.]+ -> .*", out[4])
assert re.match(r"\[0x[0-9a-f]+\] __gmon_start__ -> .*", out[5])
assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[6])
gdb.execute("continue")
# After resolving symbols' addresses, .got and .got.plt are read-only
out = gdb.execute("got -r", to_string=True).splitlines()
assert len(out) == 5
assert out[0] == f"State of the GOT of {Path.cwd() / binary}:"
assert out[1] == "GOT protection: Full RELRO | Found 3 GOT entries passing the filter"
assert re.match(r"\[0x[0-9a-f]+\] __libc_start_main@GLIBC_[0-9.]+ -> .*", out[2])
assert re.match(r"\[0x[0-9a-f]+\] __gmon_start__ -> .*", out[3])
assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[4])
# Try filtering out entries with "puts"
out = gdb.execute("got -r puts", to_string=True).splitlines()
assert len(out) == 5
assert out[0] == "Filtering by symbol name: puts"
assert out[1] == ""
assert out[2] == f"State of the GOT of {Path.cwd() / binary}:"
assert out[3] == "GOT protection: Full RELRO | Found 1 GOT entries passing the filter"
assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[4])
def test_command_got_for_target_binary_and_loaded_library():
binary = tests.binaries.get(NOPIE_BINARY_WITH_PLT)
gdb.execute(f"file {binary}")
assert out != out2
gdb.execute("break main")
gdb.execute("starti")
assert len(out2) == 2
assert out2[0] == "GOT protection: Full RELRO | GOT functions: 1"
assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out2[1])
# Before loading libc, we can't find .got.plt of libc
out = gdb.execute("got -p libc", to_string=True).splitlines()
assert len(out) == 6
assert out[0] == "Filtering by lib/objfile path: libc"
assert out[1] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[2] == ""
assert out[3] == "No shared library matching the path filter found."
assert out[4] == "Available shared libraries:"
assert out[5].endswith("/ld-linux-x86-64.so.2")
gdb.execute("continue")
# TODO/FIXME: We need to verify the addresses are correct or not
# After loading libc, we can find .got.plt of libc
out = gdb.execute("got -p libc", to_string=True).splitlines()
assert out[0] == "Filtering by lib/objfile path: libc"
assert out[1] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[2] == ""
assert re.match(r"State of the GOT of .*/libc.so.6:", out[3])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[4]
)
got_entries_count = int(m.group(1))
assert got_entries_count > 0
assert len(out) == (5 + got_entries_count)
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[5 + i])
# Try showing read-only entries of libc also
out = gdb.execute("got -p libc -r", to_string=True).splitlines()
assert out[0] == "Filtering by lib/objfile path: libc"
assert out[1] == ""
assert re.match(r"State of the GOT of .*/libc.so.6:", out[2])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[3]
)
assert int(m.group(1)) > got_entries_count # We should have more entries now
got_entries_count = int(m.group(1))
assert len(out) == (4 + got_entries_count)
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[4 + i])
# Try filtering out libc's entries with "ABS"
out = gdb.execute("got -p libc ABS", to_string=True).splitlines()
assert out[0] == "Filtering by lib/objfile path: libc"
assert out[1] == "Filtering by symbol name: ABS"
assert out[2] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[3] == ""
assert re.match(r"State of the GOT of .*/libc.so.6:", out[4])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[5]
)
got_entries_count = int(m.group(1))
assert len(out) == (6 + got_entries_count)
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .*ABS.* -> .*", out[6 + i])
# Try filtering out path with "l", which should match every library
# First should be ld-linux-x86-64.so.2
out = gdb.execute("got -p l", to_string=True).splitlines()
assert out[0] == "Filtering by lib/objfile path: l"
assert out[1] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[2] == ""
assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[3])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[4]
)
got_entries_count = int(m.group(1))
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[5 + i])
assert out[5 + i + 1] == ""
# Second should be libc.so.6
out = out[5 + i + 2 :]
assert re.match(r"State of the GOT of .*/libc.so.6:", out[0])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[1]
)
got_entries_count = int(m.group(1))
assert len(out) == (2 + got_entries_count)
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[2 + i])
# Check -a option list target binary's GOT also all loaded libraries' GOT
# First should be target binary
out = gdb.execute("got -a", to_string=True).splitlines()
assert out[0] == "Filtering out read-only entries (display them with -r or --show-readonly)"
assert out[1] == ""
assert out[2] == f"State of the GOT of {Path.cwd() / binary}:"
assert out[3] == "GOT protection: Full RELRO | Found 0 GOT entries passing the filter"
assert out[4] == ""
out = out[5:]
# Second should be ld-linux-x86-64.so.2
assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[0])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[1]
)
got_entries_count = int(m.group(1))
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[2 + i])
assert out[2 + i + 1] == ""
out = out[2 + i + 2 :]
# Third should be libc.so.6
assert re.match(r"State of the GOT of .*/libc.so.6:", out[0])
m = re.match(
r"GOT protection: Partial RELRO \| Found (\d+) GOT entries passing the filter", out[1]
)
got_entries_count = int(m.group(1))
assert len(out) == (2 + got_entries_count)
for i in range(got_entries_count):
assert re.match(r"\[0x[0-9a-f]+\] .* -> .*", out[2 + i])
# Check got -a -r puts can show the puts entry in target binary's GOT
out = gdb.execute("got -a -r puts", to_string=True).splitlines()
assert out[0] == "Filtering by symbol name: puts"
assert out[1] == ""
assert out[2] == f"State of the GOT of {Path.cwd() / binary}:"
assert out[3] == "GOT protection: Full RELRO | Found 1 GOT entries passing the filter"
assert re.match(r"\[0x[0-9a-f]+\] puts@GLIBC_[0-9.]+ -> .*", out[4])
assert out[5] == ""
assert re.match(r"State of the GOT of .*/ld-linux-x86-64.so.2:", out[6])
assert out[7] == "GOT protection: Partial RELRO | Found 0 GOT entries passing the filter"
assert out[8] == ""
assert re.match(r"State of the GOT of .*/libc.so.6:", out[9])
assert out[10] == "GOT protection: Partial RELRO | Found 0 GOT entries passing the filter"
assert len(out) == 11

Loading…
Cancel
Save