mirror of
https://github.com/Gallopsled/pwntools.git
synced 2026-11-07 09:25:58 -10:00
`pwn errno` only ever consulted the host's errno module, so numbers from a target of a different architecture were reported as unknown (131 is ECONNRESET on mips but unused here). Add -c/--context, built on the same context_arg helper as asm, constgrep and cyclic: number and name lookups now resolve against pwnlib.constants for the requested os/arch, and --list/--search honour the flag too. --search also matches errno names, so the flag stays useful on hosts whose strerror cannot spell out the target's codes. Covered with three inline doctests, as TESTING.md asks for.
This commit is contained in:
1 parent
b83a0ddc8c
commit
01f5cf08d7
3 files changed
+123
-23
No files matched your search
@@ -76,6 +76,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
## 5.0.0 (`dev`)
|
||||
|
||||
- [#2780][2780] fix(encoders): handle unavailable xor encodings and fallback
|
||||
- [#2789][2789] feat(errno): support `-c/--context` to look up errno values for other architectures
|
||||
- [#2784][2784] docs: put plain doctest blocks in the default group for Sphinx 9
|
||||
- [#2762][2762] fix(srop): correct amd64 SigreturnFrame `uc_sigmask` offset
|
||||
- [#2753][2753] docs(args): clarify reserved args (DEBUG/NOASLR) map to context, not args
|
||||
@@ -233,6 +234,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
[2768]: https://github.com/Gallopsled/pwntools/pull/2768
|
||||
[2769]: https://github.com/Gallopsled/pwntools/pull/2769
|
||||
[2780]: https://github.com/Gallopsled/pwntools/pull/2780
|
||||
[2789]: https://github.com/Gallopsled/pwntools/pull/2789
|
||||
[2750]: https://github.com/Gallopsled/pwntools/pull/2750
|
||||
[2761]: https://github.com/Gallopsled/pwntools/pull/2761
|
||||
[2727]: https://github.com/Gallopsled/pwntools/pull/2727
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
.. testcleanup:: *
|
||||
|
||||
context.defaults.copy = old
|
||||
context.defaults.clear()
|
||||
context.defaults.update(old)
|
||||
|
||||
Command Line Tools
|
||||
========================
|
||||
@@ -18,3 +19,7 @@ If these tools do not appear to be installed, make sure that you have added ``~/
|
||||
|
||||
.. autoprogram:: pwnlib.commandline.main:parser
|
||||
:prog: pwn
|
||||
|
||||
.. only:: doctest
|
||||
|
||||
.. automodule:: pwnlib.commandline.errno
|
||||
+115
-22
@@ -1,8 +1,23 @@
|
||||
"""Look up ``errno`` values for the architecture selected by ``-c``.
|
||||
|
||||
The examples below run in the documentation doctest suite.
|
||||
|
||||
>>> from pwnlib.context import context
|
||||
>>> from pwnlib.commandline.errno import _context_table
|
||||
>>> with context.local(arch='amd64', os='linux'):
|
||||
... _context_table()['ECONNRESET']
|
||||
104
|
||||
>>> with context.local(arch='mips', os='linux'):
|
||||
... _context_table()['ECONNRESET']
|
||||
131
|
||||
"""
|
||||
from argparse import Namespace
|
||||
from typing import Generator
|
||||
import os
|
||||
import errno
|
||||
|
||||
import pwnlib.constants as constants
|
||||
from pwnlib.context import context
|
||||
from pwnlib.commandline import common
|
||||
|
||||
parser = common.parser_commands.add_parser(
|
||||
@@ -21,16 +36,76 @@ parser.add_argument(
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--search', action='store_true',
|
||||
help='Treat the argument as a perror substring and print every matching errno.'
|
||||
help='Treat the argument as an errno name or perror substring and print every match.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--context',
|
||||
metavar = 'context',
|
||||
type = common.context_arg,
|
||||
help = 'Look the errno values up for this architecture or os, e.g. `-c aarch64`.',
|
||||
)
|
||||
|
||||
|
||||
def _iter_known_errnos() -> Generator[tuple[int, str, str], None, None]:
|
||||
"""Yield ``(value, name, message)`` tuples for every errno code visible to
|
||||
the standard library on this platform, sorted by numeric value.
|
||||
def _host_table() -> dict[str, int]:
|
||||
"""Return ``{name: value}`` for every errno the host standard library knows."""
|
||||
return {name: getattr(errno, name) for name in dir(errno)
|
||||
if name.isupper() and isinstance(getattr(errno, name), int)}
|
||||
|
||||
|
||||
def _context_table() -> dict[str, int]:
|
||||
"""Return ``{name: value}`` for the errno numbers ``context`` uses."""
|
||||
if context.os == 'freebsd':
|
||||
module = getattr(constants, 'freebsd', None)
|
||||
else:
|
||||
module = getattr(getattr(constants, context.os, None), context.arch, None)
|
||||
|
||||
if module is None:
|
||||
return {}
|
||||
|
||||
table = {}
|
||||
|
||||
for name in _host_table():
|
||||
value = getattr(module, name, None)
|
||||
if value is not None:
|
||||
table[name] = int(getattr(value, 'value', value))
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def _message(name: str) -> str:
|
||||
"""Return the perror string of ``name`` according to the host, or ``''``."""
|
||||
value = getattr(errno, name, None)
|
||||
if not isinstance(value, int):
|
||||
return ''
|
||||
|
||||
try:
|
||||
return os.strerror(value)
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
|
||||
def _iter_known_errnos(table: dict[str, int]) -> Generator[tuple[int, str, str], None, None]:
|
||||
"""Yield ``(value, name, message)`` tuples sorted by numeric value.
|
||||
|
||||
Aliases sharing a number are reported once: the canonical name from
|
||||
`errno.errorcode` wins when it maps to the same value, otherwise the
|
||||
first name alphabetically does.
|
||||
|
||||
>>> with context.local(arch='aarch64', os='linux'):
|
||||
... [(value, name) for value, name, _ in _iter_known_errnos(_context_table())][:3]
|
||||
[(1, 'EPERM'), (2, 'ENOENT'), (3, 'ESRCH')]
|
||||
"""
|
||||
for value, name in sorted(errno.errorcode.items()):
|
||||
yield value, name, os.strerror(value)
|
||||
# ``errno.errorcode`` is the canonical name for a number, so prefer it over the
|
||||
# alphabetically first alias to keep the default listing identical to before.
|
||||
canonical = {value: name for value, name in errno.errorcode.items() if table.get(name) == value}
|
||||
seen = set()
|
||||
|
||||
for name, value in sorted(table.items(), key=lambda item: (item[1], item[0])):
|
||||
if value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
name = canonical.get(value, name)
|
||||
yield value, name, _message(name)
|
||||
|
||||
|
||||
def _print_errno(value: int, name: str | None = None, message: str | None = None) -> None:
|
||||
@@ -42,18 +117,37 @@ def _print_errno(value: int, name: str | None = None, message: str | None = None
|
||||
print('#define %s %d' % (name, value))
|
||||
else:
|
||||
print('#define <unknown> %d' % value)
|
||||
print(message)
|
||||
if message:
|
||||
print(message)
|
||||
|
||||
|
||||
def _search_errnos(needle: str) -> list[tuple[int, str, str]]:
|
||||
"""Return errnos whose ``strerror`` text contains ``needle`` (case-insensitive)."""
|
||||
def _search_errnos(needle: str, table: dict[str, int]) -> list[tuple[int, str, str]]:
|
||||
"""Return errnos whose name or ``strerror`` text contains ``needle``.
|
||||
|
||||
The match is case-insensitive, so ``pwn errno -c mips -s conn`` finds the
|
||||
connection-oriented errnos even when the host cannot spell them out.
|
||||
|
||||
>>> with context.local(arch='mips', os='linux'):
|
||||
... [(value, name) for value, name, _ in _search_errnos('econnreset', _context_table())]
|
||||
[(131, 'ECONNRESET')]
|
||||
"""
|
||||
needle = needle.lower()
|
||||
return [(v, n, m) for v, n, m in _iter_known_errnos() if needle in m.lower()]
|
||||
return [(v, n, m) for v, n, m in _iter_known_errnos(table)
|
||||
if needle in n.lower() or needle in m.lower()]
|
||||
|
||||
|
||||
def main(args: Namespace) -> None:
|
||||
if args.context is None:
|
||||
table = _host_table()
|
||||
else:
|
||||
table = _context_table()
|
||||
|
||||
if not table:
|
||||
print('No errno values known for %s/%s' % (context.os, context.arch))
|
||||
return
|
||||
|
||||
if args.list:
|
||||
for value, name, message in _iter_known_errnos():
|
||||
for value, name, message in _iter_known_errnos(table):
|
||||
print('%-3d %-10s %s' % (value, name, message))
|
||||
return
|
||||
|
||||
@@ -61,7 +155,7 @@ def main(args: Namespace) -> None:
|
||||
parser.error('the following arguments are required: error (or pass --list)')
|
||||
|
||||
if args.search:
|
||||
matches = _search_errnos(args.error)
|
||||
matches = _search_errnos(args.error, table)
|
||||
if not matches:
|
||||
print("No errno matched %r" % args.error)
|
||||
return
|
||||
@@ -79,22 +173,20 @@ def main(args: Namespace) -> None:
|
||||
if 0x100000000 - value < 0x200:
|
||||
value = 0x100000000 - value
|
||||
|
||||
if value not in errno.errorcode:
|
||||
name = next((n for n, v in sorted(table.items()) if v == value), None)
|
||||
|
||||
if name is None:
|
||||
print("No errno for %s" % value)
|
||||
return
|
||||
|
||||
name = errno.errorcode[value]
|
||||
|
||||
except ValueError:
|
||||
candidate = args.error.upper()
|
||||
name = args.error.upper()
|
||||
value = table.get(name)
|
||||
|
||||
if hasattr(errno, candidate):
|
||||
name = candidate
|
||||
value = getattr(errno, name)
|
||||
else:
|
||||
if value is None:
|
||||
# Fall back to a substring search against perror messages so
|
||||
# ``pwn errno 'Cannot allocate memory'`` resolves to ENOMEM.
|
||||
matches = _search_errnos(args.error)
|
||||
matches = _search_errnos(args.error, table)
|
||||
if not matches:
|
||||
print("No errno for %s" % args.error)
|
||||
return
|
||||
@@ -104,8 +196,9 @@ def main(args: Namespace) -> None:
|
||||
print()
|
||||
return
|
||||
|
||||
_print_errno(value, name)
|
||||
_print_errno(value, name, _message(name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
common.main(__file__, main)
|
||||
|
||||
Reference in new issue
Block a user