Skip to content

Commit 2b7ec5e

Browse files
authored
Fix gdb and other doctests (#2355)
* Fix gdb.debug doctests * Accept bytes in ssh.which But still return a `str` return for backwards-compatibility. * CI: Make rpyc visible in gdb's python * Fix not running all doctests They appear to have to be a free standing block of `>>>` with an empty line above. * Remove unnecessary text type check in ssh.which
1 parent c17f835 commit 2b7ec5e

27 files changed

+159
-29
lines changed

.github/workflows/ci.yml

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ jobs:
112112
113113
- name: Coverage doctests
114114
run: |
115+
# Python version installed using setup-python interferes with gdb's python
116+
# by setting LD_LIBRARY_PATH and gdb's python becoming unable to load built-in modules
117+
# like _socket. This is a workaround.
118+
unset LD_LIBRARY_PATH
115119
PWNLIB_NOTERM=1 python -bb -m coverage run -m sphinx -b doctest docs/source docs/build/doctest
116120
117121
- name: Coverage running examples

docs/source/conf.py

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def filter(self, record):
7171
import sys, os
7272
os.environ['PWNLIB_NOTERM'] = '1'
7373
os.environ['PWNLIB_RANDOMIZE'] = '0'
74+
import six
7475
import pwnlib.update
7576
import pwnlib.util.fiddling
7677
import logging
@@ -98,6 +99,7 @@ def __setattr__(self, name, value):
9899
travis_ci = os.environ.get('USER') == 'travis'
99100
local_doctest = os.environ.get('USER') == 'pwntools'
100101
skip_android = True
102+
is_python2 = six.PY2
101103
'''
102104

103105
autoclass_content = 'both'

docs/source/index.rst

-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ Each of the ``pwntools`` modules is documented here.
8282
:hidden:
8383

8484
testexample
85-
rop/call
8685

8786
.. only:: not dash
8887

docs/source/install.rst

+4-6
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ Command-Line Tools
5151

5252
When installed with ``sudo`` the above commands will install Pwntools' command-line tools to somewhere like ``/usr/bin``.
5353

54-
However, if you run as an unprivileged user, you may see a warning message that looks like this:
54+
However, if you run as an unprivileged user, you may see a warning message that looks like this::
5555

56-
.. code-block::
57-
58-
WARNING: The scripts asm, checksec, common, constgrep, cyclic, debug, disablenx, disasm,
59-
elfdiff, elfpatch, errno, hex, main, phd, pwn, pwnstrip, scramble, shellcraft, template,
60-
unhex, update and version are installed in '/home/user/.local/bin' which is not on PATH.
56+
WARNING: The scripts asm, checksec, common, constgrep, cyclic, debug, disablenx, disasm,
57+
elfdiff, elfpatch, errno, hex, main, phd, pwn, pwnstrip, scramble, shellcraft, template,
58+
unhex, update and version are installed in '/home/user/.local/bin' which is not on PATH.
6159

6260
Follow the instructions listed and add ``~/.local/bin`` to your ``$PATH`` environment variable.
6361

pwnlib/context/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ def bits(self, bits):
830830
The default value is ``32``, but changes according to :attr:`arch`.
831831
832832
Examples:
833+
833834
>>> context.clear()
834835
>>> context.bits == 32
835836
True

pwnlib/elf/elf.py

+3
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ def vaddr_to_offset(self, address):
13461346
or :const:`None`.
13471347
13481348
Examples:
1349+
13491350
>>> bash = ELF(which('bash'))
13501351
>>> bash.vaddr_to_offset(bash.address)
13511352
0
@@ -1496,6 +1497,7 @@ def write(self, address, data):
14961497
that it stays in the same segment.
14971498
14981499
Examples:
1500+
14991501
>>> bash = ELF(which('bash'))
15001502
>>> bash.read(bash.address+1, 3)
15011503
b'ELF'
@@ -2387,6 +2389,7 @@ def set_interpreter(exepath, interpreter_path):
23872389
A new ELF instance is returned after patching the binary with the external ``patchelf`` tool.
23882390
23892391
Example:
2392+
23902393
>>> tmpdir = tempfile.mkdtemp()
23912394
>>> ls_path = os.path.join(tmpdir, 'ls')
23922395
>>> _ = shutil.copy(which('ls'), ls_path)

pwnlib/fmtstr.py

+14
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def normalize_writes(writes):
129129
such that all values are raw bytes and consecutive writes are merged to a single key.
130130
131131
Examples:
132+
132133
>>> context.clear(endian="little", bits=32)
133134
>>> normalize_writes({0x0: [p32(0xdeadbeef)], 0x4: p32(0xf00dface), 0x10: 0x41414141})
134135
[(0, b'\xef\xbe\xad\xde\xce\xfa\r\xf0'), (16, b'AAAA')]
@@ -215,6 +216,7 @@ def compute_padding(self, counter):
215216
given the current format string write counter (how many bytes have been written until now).
216217
217218
Examples:
219+
218220
>>> hex(pwnlib.fmtstr.AtomWrite(0x0, 0x2, 0x2345).compute_padding(0x1111))
219221
'0x1234'
220222
>>> hex(pwnlib.fmtstr.AtomWrite(0x0, 0x2, 0xaa00).compute_padding(0xaabb))
@@ -246,6 +248,7 @@ def union(self, other):
246248
Combine adjacent writes into a single write.
247249
248250
Example:
251+
249252
>>> context.clear(endian = "little")
250253
>>> pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0x1, 0xff).union(pwnlib.fmtstr.AtomWrite(0x1, 0x1, 0x2, 0x77))
251254
AtomWrite(start=0, size=2, integer=0x201, mask=0x77ff)
@@ -285,11 +288,13 @@ def make_atoms_simple(address, data, badbytes=frozenset()):
285288
286289
This function is simple and does not try to minimize the number of atoms. For example, if there are no
287290
bad bytes, it simply returns one atom for each byte:
291+
288292
>>> pwnlib.fmtstr.make_atoms_simple(0x0, b"abc", set())
289293
[AtomWrite(start=0, size=1, integer=0x61, mask=0xff), AtomWrite(start=1, size=1, integer=0x62, mask=0xff), AtomWrite(start=2, size=1, integer=0x63, mask=0xff)]
290294
291295
If there are bad bytes, it will try to bypass by skipping addresses containing bad bytes, otherwise a
292296
RuntimeError will be raised:
297+
293298
>>> pwnlib.fmtstr.make_atoms_simple(0x61, b'abc', b'\x62')
294299
[AtomWrite(start=97, size=2, integer=0x6261, mask=0xffff), AtomWrite(start=99, size=1, integer=0x63, mask=0xff)]
295300
>>> pwnlib.fmtstr.make_atoms_simple(0x61, b'a'*0x10, b'\x62\x63\x64\x65\x66\x67\x68')
@@ -325,6 +330,7 @@ def merge_atoms_writesize(atoms, maxsize):
325330
This function simply merges adjacent atoms as long as the merged atom's size is not larger than ``maxsize``.
326331
327332
Examples:
333+
328334
>>> from pwnlib.fmtstr import *
329335
>>> merge_atoms_writesize([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1), AtomWrite(2, 1, 2)], 2)
330336
[AtomWrite(start=0, size=2, integer=0x101, mask=0xffff), AtomWrite(start=2, size=1, integer=0x2, mask=0xff)]
@@ -364,6 +370,7 @@ def find_min_hamming_in_range_step(prev, step, carry, strict):
364370
A tuple (score, value, mask) where score equals the number of matching bytes between the returned value and target.
365371
366372
Examples:
373+
367374
>>> initial = {(0,0): (0,0,0), (0,1): None, (1,0): None, (1,1): None}
368375
>>> pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0, 0xFF, 0x1), 0, 0)
369376
(1, 1, 255)
@@ -419,6 +426,7 @@ def find_min_hamming_in_range(maxbytes, lower, upper, target):
419426
target(int): the target value that should be approximated
420427
421428
Examples:
429+
422430
>>> pp = lambda svm: (svm[0], hex(svm[1]), hex(svm[2]))
423431
>>> pp(pwnlib.fmtstr.find_min_hamming_in_range(1, 0x0, 0x100, 0xaa))
424432
(1, '0xaa', '0xff')
@@ -470,6 +478,7 @@ def merge_atoms_overlapping(atoms, sz, szmax, numbwritten, overflows):
470478
overflows(int): how many extra overflows (of size sz) to tolerate to reduce the number of atoms
471479
472480
Examples:
481+
473482
>>> from pwnlib.fmtstr import *
474483
>>> merge_atoms_overlapping([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1)], 2, 8, 0, 1)
475484
[AtomWrite(start=0, size=2, integer=0x101, mask=0xffff)]
@@ -557,13 +566,15 @@ def overlapping_atoms(atoms):
557566
Finds pairs of atoms that write to the same address.
558567
559568
Basic examples:
569+
560570
>>> from pwnlib.fmtstr import *
561571
>>> list(overlapping_atoms([AtomWrite(0, 2, 0), AtomWrite(2, 10, 1)])) # no overlaps
562572
[]
563573
>>> list(overlapping_atoms([AtomWrite(0, 2, 0), AtomWrite(1, 2, 1)])) # single overlap
564574
[(AtomWrite(start=0, size=2, integer=0x0, mask=0xffff), AtomWrite(start=1, size=2, integer=0x1, mask=0xffff))]
565575
566576
When there are transitive overlaps, only the largest overlap is returned. For example:
577+
567578
>>> list(overlapping_atoms([AtomWrite(0, 3, 0), AtomWrite(1, 4, 1), AtomWrite(2, 4, 1)]))
568579
[(AtomWrite(start=0, size=3, integer=0x0, mask=0xffffff), AtomWrite(start=1, size=4, integer=0x1, mask=0xffffffff)), (AtomWrite(start=1, size=4, integer=0x1, mask=0xffffffff), AtomWrite(start=2, size=4, integer=0x1, mask=0xffffffff))]
569580
@@ -629,6 +640,7 @@ def sort_atoms(atoms, numbwritten):
629640
numbwritten(int): the value at which the counter starts
630641
631642
Examples:
643+
632644
>>> from pwnlib.fmtstr import *
633645
>>> sort_atoms([AtomWrite(0, 1, 0xff), AtomWrite(1, 1, 0xfe)], 0) # the example described above
634646
[AtomWrite(start=1, size=1, integer=0xfe, mask=0xff), AtomWrite(start=0, size=1, integer=0xff, mask=0xff)]
@@ -694,6 +706,7 @@ def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4, no_dol
694706
no_dollars(bool) : flag to generete the payload with or w/o $ notation
695707
696708
Examples:
709+
697710
>>> pwnlib.fmtstr.make_payload_dollar(1, [pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0xff)])
698711
(b'%255c%1$hhn', b'\x00\x00\x00\x00')
699712
'''
@@ -840,6 +853,7 @@ def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_
840853
The payload in order to do needed writes
841854
842855
Examples:
856+
843857
>>> context.clear(arch = 'amd64')
844858
>>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int')
845859
b'%322419390c%4$llnaaaabaa\x00\x00\x00\x00\x00\x00\x00\x00'

pwnlib/gdb.py

+23-17
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,11 @@
141141
from __future__ import absolute_import
142142
from __future__ import division
143143

144-
from contextlib import contextmanager
145144
import os
146-
import sys
147145
import platform
148146
import psutil
149147
import random
150148
import re
151-
import shlex
152149
import six
153150
import six.moves
154151
import socket
@@ -512,27 +509,30 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
512509
>>> io.close()
513510
514511
Start a new process with modified argv[0]
515-
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], executable="/bin/sh")
512+
513+
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh")
516514
>>> io.sendline(b"echo $0")
517515
>>> io.recvline()
518-
b'$ \xde\xad\xbe\xef\n'
516+
b'\xde\xad\xbe\xef\n'
519517
>>> io.close()
520518
521519
Demonstrate that LD_PRELOAD is respected
520+
522521
>>> io = process(["grep", "libc.so.6", "/proc/self/maps"])
523522
>>> real_libc_path = io.recvline().split()[-1]
523+
>>> io.close()
524524
>>> import shutil
525-
>>> shutil.copy(real_libc_path, "./libc.so.6") # make a copy of libc to demonstrate that it is loaded
526-
>>> io = gdb.debug(["grep", "libc.so.6", "/proc/self/maps"], env={"LD_PRELOAD": "./libc.so.6"})
527-
>>> io.recvline().split()[-1]
528-
b"./libc.so.6"
529-
>>> os.remove("./libc.so.6") # cleanup
525+
>>> local_path = shutil.copy(real_libc_path, "./local-libc.so") # make a copy of libc to demonstrate that it is loaded
526+
>>> io = gdb.debug(["grep", "local-libc.so", "/proc/self/maps"], gdbscript="continue", env={"LD_PRELOAD": "./local-libc.so"})
527+
>>> io.recvline().split()[-1] # doctest: +ELLIPSIS
528+
b'.../local-libc.so'
529+
>>> os.remove("./local-libc.so") # cleanup
530530
531531
532532
Using GDB Python API:
533533
534-
.. doctest
535-
:skipif: six.PY2
534+
.. doctest::
535+
:skipif: is_python2
536536
537537
Debug a new process
538538
@@ -577,18 +577,21 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
577577
>>> io.sendline(b"echo hello")
578578
579579
Interact with the process
580+
580581
>>> io.interactive() # doctest: +SKIP
581582
>>> io.close()
582583
583584
Using a modified args[0] on a remote process
584-
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh", ssh=shell)
585+
586+
>>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh", ssh=shell)
585587
>>> io.sendline(b"echo $0")
586588
>>> io.recvline()
587589
b'$ \xde\xad\xbe\xef\n'
588590
>>> io.close()
589591
590592
Using an empty args[0] on a remote process
591-
>>> io = gdb.debug(args=[], gdbscript='continue', exe="/bin/sh", ssh=shell)
593+
594+
>>> io = gdb.debug(args=[], gdbscript='continue', exe="/bin/sh", ssh=shell)
592595
>>> io.sendline(b"echo $0")
593596
>>> io.recvline()
594597
b'$ \n'
@@ -681,7 +684,10 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=
681684
garbage = gdbserver.recvline(timeout=1)
682685

683686
# Some versions of gdbserver output an additional message
684-
garbage2 = gdbserver.recvline_startswith(b"Remote debugging from host ", timeout=2)
687+
try:
688+
garbage2 = gdbserver.recvline_startswith(b"Remote debugging from host ", timeout=2)
689+
except EOFError:
690+
pass
685691

686692
return gdbserver
687693

@@ -944,8 +950,8 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr
944950
945951
Using GDB Python API:
946952
947-
.. doctest
948-
:skipif: six.PY2
953+
.. doctest::
954+
:skipif: is_python2
949955
950956
>>> io = process('bash')
951957

pwnlib/libcdb.py

+7
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def unstrip_libc(filename):
249249
:const:`True` if binary was unstripped, :const:`False` otherwise.
250250
251251
Examples:
252+
252253
>>> filename = search_by_build_id('69389d485a9793dbe873f0ea2c93e02efaa9aa3d', unstrip=False)
253254
>>> libc = ELF(filename)
254255
>>> 'main_arena' in libc.symbols
@@ -432,6 +433,7 @@ def download_libraries(libc_path, unstrip=True):
432433
The path to the cached directory containing the downloaded libraries.
433434
434435
Example:
436+
435437
>>> libc_path = ELF(which('ls'), checksec=False).libc.path
436438
>>> lib_path = download_libraries(libc_path)
437439
>>> lib_path is not None
@@ -545,6 +547,7 @@ def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, return_as
545547
is returned instead.
546548
547549
Examples:
550+
548551
>>> filename = search_by_symbol_offsets({'puts': 0x420, 'printf': 0xc90}, select_index=1)
549552
>>> libc = ELF(filename)
550553
>>> libc.sym.system == 0x52290
@@ -597,6 +600,7 @@ def search_by_build_id(hex_encoded_id, unstrip=True):
597600
Path to the downloaded library on disk, or :const:`None`.
598601
599602
Examples:
603+
600604
>>> filename = search_by_build_id('fe136e485814fee2268cf19e5c124ed0f73f4400')
601605
>>> hex(ELF(filename).symbols.read)
602606
'0xda260'
@@ -622,6 +626,7 @@ def search_by_md5(hex_encoded_id, unstrip=True):
622626
Path to the downloaded library on disk, or :const:`None`.
623627
624628
Examples:
629+
625630
>>> filename = search_by_md5('7a71dafb87606f360043dcd638e411bd')
626631
>>> hex(ELF(filename).symbols.read)
627632
'0xda260'
@@ -647,6 +652,7 @@ def search_by_sha1(hex_encoded_id, unstrip=True):
647652
Path to the downloaded library on disk, or :const:`None`.
648653
649654
Examples:
655+
650656
>>> filename = search_by_sha1('34471e355a5e71400b9d65e78d2cd6ce7fc49de5')
651657
>>> hex(ELF(filename).symbols.read)
652658
'0xda260'
@@ -673,6 +679,7 @@ def search_by_sha256(hex_encoded_id, unstrip=True):
673679
Path to the downloaded library on disk, or :const:`None`.
674680
675681
Examples:
682+
676683
>>> filename = search_by_sha256('5e877a8272da934812d2d1f9ee94f73c77c790cbc5d8251f5322389fc9667f21')
677684
>>> hex(ELF(filename).symbols.read)
678685
'0xda260'

pwnlib/rop/rop.py

+1
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,7 @@ def ret2csu(self, edi=Padding('edi'), rsi=Padding('rsi'),
14991499
.dynamic section. .got.plt entries are a good target. Required
15001500
for PIE binaries.
15011501
Test:
1502+
15021503
>>> context.clear(binary=pwnlib.data.elf.ret2dlresolve.get("amd64"))
15031504
>>> r = ROP(context.binary)
15041505
>>> r.ret2csu(1, 2, 3, 4, 5, 6, 7, 8, 9)

pwnlib/shellcraft/templates/aarch64/pushstr_array.asm

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Arguments:
1717
ends with exactly one NULL byte.
1818

1919
Example:
20+
2021
>>> assembly = shellcraft.execve("/bin/sh", ["sh", "-c", "echo Hello string $WORLD"], {"WORLD": "World!"})
2122
>>> ELF.from_assembly(assembly).process().recvall()
2223
b'Hello string World!\n'

pwnlib/shellcraft/templates/arm/ret.asm

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Args:
55
return_value: Value to return
66

77
Examples:
8+
89
>>> with context.local(arch='arm'):
910
... print(enhex(asm(shellcraft.ret())))
1011
... print(enhex(asm(shellcraft.ret(0))))

pwnlib/shellcraft/templates/i386/xor.asm

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Args:
1919
the number of bytes to XOR.
2020

2121
Example:
22+
2223
>>> sc = shellcraft.read(0, 'esp', 32)
2324
>>> sc += shellcraft.xor(0xdeadbeef, 'esp', 32)
2425
>>> sc += shellcraft.write(1, 'esp', 32)

pwnlib/shellcraft/templates/thumb/linux/findpeer.asm

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
against the peer port. Resulting socket is left in r6.
99

1010
Example:
11+
1112
>>> enhex(asm(shellcraft.findpeer(1337)))
1213
'6ff00006ee4606f101064ff001074fea072707f11f07f54630461fb401a96a4601df0130efdd01994fea11414ff039024fea022202f105029142e4d1'
1314
</%docstring>

0 commit comments

Comments
 (0)