• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Gallopsled / pwntools / 4e81f6fbbac705beda7eb5e31268d72e5c2d113a

pending completion
4e81f6fbbac705beda7eb5e31268d72e5c2d113a

push

github-actions

gogo
Fix testing.

3931 of 6497 branches covered (60.5%)

16 of 16 new or added lines in 1 file covered. (100.0%)

12334 of 16864 relevant lines covered (73.14%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.53
/pwnlib/radare2.py
1
# -*- coding: utf-8 -*-
2
"""
1✔
3
During exploit development, it is frequently useful to debug the
4
target binary under Radare2.
5

6
Pwntools makes this easy-to-do with a handful of helper routines, designed
7
to make your exploit-debug-update cycles much faster.
8

9
Useful Functions
10
----------------
11

12
- :func:`attach` - Attach to an existing process
13
- :func:`debug` - Start a new process under a debugger, stopped at the first instruction
14
- :func:`debug_shellcode` - Build a binary with the provided shellcode, and start it under a debugger
15

16
Debugging Tips
17
--------------
18

19
The :func:`attach` and :func:`debug` functions will likely be your bread and
20
butter for debugging.
21

22
Both allow you to provide a script to pass to Radare2 when it is started, so that
23
it can automatically set your breakpoints.
24

25
Attaching to Processes
26
~~~~~~~~~~~~~~~~~~~~~~
27

28
To attach to an existing process, just use :func:`attach`.  It is surprisingly
29
versatile, and can attach to a :class:`.process` for simple
30
binaries, or will automatically find the correct process to attach to for a
31
forking server, if given a :class:`.remote` object.
32

33
Spawning New Processes
34
~~~~~~~~~~~~~~~~~~~~~~
35

36
Attaching to processes with :func:`attach` is useful, but the state the process
37
is in may vary.  If you need to attach to a process very early, and debug it from
38
the very first instruction (or even the start of ``main``), you instead should use
39
:func:`debug`.
40

41
When you use :func:`debug`, the return value is a :class:`.tube` object
42
that you interact with exactly like normal.
43

44
Using Radare2 Python API
45
~~~~~~~~~~~~~~~~~~~~~~~~
46

47
GDB provides Python API, which is documented at
48
https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html. Pwntools allows you
49
to call it right from the exploit, without having to write a gdbscript. This is
50
useful for inspecting program state, e.g. asserting that leaked values are
51
correct, or that certain packets trigger a particular code path or put the heap
52
in a desired state.
53

54
Pass ``api=True`` to :func:`attach` or :func:`debug` in order to enable GDB
55
Python API access. Pwntools will then connect to GDB using RPyC library:
56
https://rpyc.readthedocs.io/en/latest/.
57

58
At the moment this is an experimental feature with the following limitations:
59

60
- Only Python 3 is supported.
61

62
  Well, technically that's not quite true. The real limitation is that your
63
  GDB's Python interpreter major version should be the same as that of
64
  Pwntools. However, most GDBs use Python 3 nowadays.
65

66
  Different minor versions are allowed as long as no incompatible values are
67
  sent in either direction. See
68
  https://rpyc.readthedocs.io/en/latest/install.html#cross-interpreter-compatibility
69
  for more information.
70

71
  Use
72

73
  ::
74

75
      $ gdb -batch -ex 'python import sys; print(sys.version)'
76

77
  in order to check your GDB's Python version.
78
- If your GDB uses a different Python interpreter than Pwntools (for example,
79
  because you run Pwntools out of a virtualenv), you should install ``rpyc``
80
  package into its ``sys.path``. Use
81

82
  ::
83

84
      $ gdb -batch -ex 'python import rpyc'
85

86
  in order to check whether this is necessary.
87
- Only local processes are supported.
88
- It is not possible to tell whether ``gdb.execute('continue')`` will be
89
  executed synchronously or asynchronously (in gdbscripts it is always
90
  synchronous). Therefore it is recommended to use either the explicitly
91
  synchronous :func:`pwnlib.gdb.Gdb.continue_and_wait` or the explicitly
92
  asynchronous :func:`pwnlib.gdb.Gdb.continue_nowait` instead.
93

94
Tips and Troubleshooting
95
------------------------
96

97
``NOPTRACE`` magic argument
98
~~~~~~~~~~~~~~~~~~~~~~~~~~~
99

100
It's quite cumbersom to comment and un-comment lines containing `attach`.
101

102
You can cause these lines to be a no-op by running your script with the
103
``NOPTRACE`` argument appended, or with ``PWNLIB_NOPTRACE=1`` in the environment.
104

105
::
106

107
    $ python exploit.py NOPTRACE
108
    [+] Starting local process '/bin/bash': Done
109
    [!] Skipping debug attach since context.noptrace==True
110
    ...
111

112
Kernel Yama ptrace_scope
113
~~~~~~~~~~~~~~~~~~~~~~~~
114

115
The Linux kernel v3.4 introduced a security mechanism called ``ptrace_scope``,
116
which is intended to prevent processes from debugging eachother unless there is
117
a direct parent-child relationship.
118

119
This causes some issues with the normal Pwntools workflow, since the process
120
hierarchy looks like this:
121

122
::
123

124
    python ---> target
125
           `--> gdb
126

127
Note that ``python`` is the parent of ``target``, not ``gdb``.
128

129
In order to avoid this being a problem, Pwntools uses the function
130
``prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY)``.  This disables Yama
131
for any processes launched by Pwntools via :class:`.process` or via
132
:meth:`.ssh.process`.
133

134
Older versions of Pwntools did not perform the ``prctl`` step, and
135
required that the Yama security feature was disabled systemwide, which
136
requires ``root`` access.
137

138
Member Documentation
139
===============================
140
"""
141

142
import six
1✔
143
import os
1✔
144
import tempfile
1✔
145
from pwnlib.context import LocalContext, context
1✔
146
from pwnlib.timeout import Timeout
1✔
147
from pwnlib.util import misc
1✔
148
from pwnlib.util import proc
1✔
149
from pwnlib.util import packing
1✔
150
from pwnlib.log import getLogger
1✔
151
from pwnlib import tubes
1✔
152

153

154
log = getLogger(__name__)
1✔
155

156

157

158
def binary():
1✔
159
    """binary() -> str
160
    Returns:
161
        str: Path to the appropriate ``radare2`` binary to use.
162
    Example:
163
        >>> radare2.binary() # doctest: +SKIP
164
        '/usr/local/bin/r2'
165
    """
166
    radare2 = misc.which('radare2')
1✔
167

168
    if not radare2:
1!
169
        log.error('radare2 is not installed\n'
×
170
                  '$ git clone https://github.com/radareorg/radare2 ; cd radare2 ; sh sys/install.sh ; cd .. ;')
171

172
    return radare2
1✔
173
    
174

175
@LocalContext
1✔
176
def attach(target, radare2_script = "", exe = None, radare2_args = None, ssh = None, sysroot = None, api = False):
1✔
177
    r"""
178
    Start Radare2 in a new terminal and attach to `target`.
179

180
    Arguments:
181
        target: The target to attach to.
182
        radare2_script(:obj:`str` or :obj:`file`): Radare2 script to run after attaching.
183
        exe(str): The path of the target binary.
184
        arch(str): Architechture of the target binary.  If `exe` known Radare2 will
185
          detect the architechture automatically (if it is supported).
186
        radare2_args(list): List of additional arguments to pass to Radare2.
187
        sysroot(str): Set an alternate system root. The system root is used to
188
            load absolute shared library symbol files. This is useful to instruct
189
            Radare2 to load a local version of binaries/libraries instead of downloading
190
            them from the gdbserver, which is faster
191
        api(bool): Enable access to Radare2 Python API.
192

193
    Returns:
194
        PID of the Radare2 process (or the window which it is running in).
195
        When ``api=True``, a (PID, :class:`Radare2`) tuple.
196

197
    Notes:
198

199
        The ``target`` argument is very robust, and can be any of the following:
200

201
        :obj:`int`
202
            PID of a process
203
        :obj:`str`
204
            Process name.  The youngest process is selected.
205
        :obj:`tuple`
206
            Host, port pair of a listening ``gdbserver``
207
        :class:`.process`
208
            Process to connect to
209
        :class:`.sock`
210
            Connected socket. The executable on the other end of the connection is attached to.
211
            Can be any socket type, including :class:`.listen` or :class:`.remote`.
212
        :class:`.ssh_channel`
213
            Remote process spawned via :meth:`.ssh.process`.
214
            This will use the Radare2 installed on the remote machine.
215
            If a password is required to connect, the ``sshpass`` program must be installed.
216

217
    Examples:
218

219
        Attach to a process by PID
220

221
        >>> pid = radare2.attach(1234) # doctest: +SKIP
222

223
        Attach to the youngest process by name
224

225
        >>> pid = radare2.attach('bash') # doctest: +SKIP
226

227
        Attach a debugger to a :class:`.process` tube and automate interaction
228
        
229
        >>> io = process('bash')
230
        >>> pid = radare2.attach(io, radare2_script='''
231
        ... pd 10 @ main
232
        ... q!!
233
        ... ''')
234
        >>> io = process(['echo', 'ABC'])#['ls', '-lh', '--time-style=+""']
235
        >>> pid = radare2.attach(io, radare2_script='dc')
236
        >>> io.recvline()
237
        b'ABC\n'
238
        >>> io = process('bash')
239
        >>> pid = radare2.attach(io, radare2_script='dc')
240
        >>> io.sendline(b'echo Hello from bash && exit')
241
        >>> io.recvall()
242
        b'Hello from bash\n'
243

244
        Using Radare2 Python API:
245

246
        .. doctest
247
           :skipif: six.PY2
248

249
            >>> io = process('bash')
250

251
            Attach a debugger
252

253
            >>> pid, io_radare2 = radare2.attach(io, api=True)
254

255
            Force the program to write something it normally wouldn't
256

257
            >>> io_radare2.execute('call puts("Hello from process debugger!")')
258

259
            Resume the program
260

261
            >>> io_radare2.continue_nowait()
262

263
            Observe the forced line
264

265
            # >>> io.recvline()
266
            # b'Hello from process debugger!\n'
267

268
            Interact with the program in a regular way
269

270
            >>> io.sendline(b'echo Hello from bash && exit')
271

272
            Observe the results
273

274
            >>> io.recvall()
275
            b'Hello from bash\n'
276

277
        Attach to the remote process from a :class:`.remote` or :class:`.listen` tube,
278
        as long as it is running on the same machine.
279

280
        >>> server = process(['socat', 'tcp-listen:12345,reuseaddr,fork', 'exec:/bin/bash,nofork'])
281
        >>> sleep(1) # Wait for socat to start
282
        >>> io = remote('127.0.0.1', 12345)
283
        >>> sleep(1) # Wait for process to fork
284
        >>> pid = radare2.attach(io, radare2_script='''
285
        ... call puts("Hello from remote debugger!")
286
        ... detach
287
        ... quit
288
        ... ''')
289
        
290
        # >>> io.recvline()
291
        # b'Hello from remote debugger!\n'
292
        # >>> io.sendline(b'echo Hello from bash && exit')
293
        # >>> io.recvall()
294
        # b'Hello from bash\n'
295

296
        Attach to processes running on a remote machine via an SSH :class:`.ssh` process
297

298
        >>> shell = ssh('travis', 'example.pwnme', password='demopass')
299
        >>> io = shell.process(['cat'])
300
        >>> pid = radare2.attach(io, radare2_script='''
301
        ... px @ main
302
        ... ''')
303
        
304
        # >>> io.recvline(timeout=5)  # doctest: +SKIP
305
        # b'Hello from ssh debugger!\n'
306
        >>> io.sendline(b'This will be echoed back')
307
        # >>> io.recvline()
308
        b'This will be echoed back\n'
309
        >>> io.close()
310
    """
311
    if context.noptrace:
1!
312
        log.warn_once("Skipping debug attach since context.noptrace==True")
×
313
        return
×
314

315
    # enable radare2.attach(p, 'continue')
316
    if radare2_script and not radare2_script.endswith('\n'):
1✔
317
        radare2_script += '\n'
1✔
318

319
    # radare2 script to run before `radare2_script`
320

321
    # let's see if we can find a pid to attach to
322
    pid = None
1✔
323
    if   isinstance(target, six.integer_types):
1!
324
        # target is a pid, easy peasy
325
        pid = target
×
326
    elif isinstance(target, str):
1!
327
        # pidof picks the youngest process
328
        pidof = proc.pidof
×
329

330
        if context.os == 'android':
×
331
            pidof = adb.pidof
×
332

333
        pids = list(pidof(target))
×
334
        if not pids:
×
335
            log.error('No such process: %s', target)
×
336
        pid = pids[0]
×
337
        log.info('Attaching to youngest process "%s" (PID = %d)' %
×
338
                 (target, pid))
339
    elif isinstance(target, tubes.ssh.ssh_channel):
1✔
340
        if not target.pid:
1!
341
            log.error("PID unknown for channel")
×
342

343
        shell = target.parent
1✔
344

345
        tmpfile = shell.mktemp()
1✔
346
        radare2_script = b'shell rm %s\n%s' % (tmpfile, packing._need_bytes(radare2_script, 2, 0x80))
1✔
347
        shell.upload_data(radare2_script or b'', tmpfile)
1✔
348

349
        cmd = ['ssh', '-C', '-t', '-p', str(shell.port), '-l', shell.user, shell.host]
1✔
350
        if shell.password:
1!
351
            if not misc.which('sshpass'):
1!
352
                log.error("sshpass must be installed to debug ssh processes")
×
353
            cmd = ['sshpass', '-p', shell.password] + cmd
1✔
354
        if shell.keyfile:
1!
355
            cmd += ['-i', shell.keyfile]
×
356
        cmd += ['gdb', '-q', target.executable, str(target.pid), '-x', tmpfile]
1✔
357

358
        misc.run_in_new_terminal(cmd)
1✔
359
        return
1✔
360
    elif isinstance(target, tubes.sock.sock):
1✔
361
        pids = proc.pidof(target)
1✔
362
        if not pids:
1!
363
            log.error('Could not find remote process (%s:%d) on this machine' %
×
364
                      target.sock.getpeername())
365
        pid = pids[0]
1✔
366

367
        # Specifically check for socat, since it has an intermediary process
368
        # if you do not specify "nofork" to the EXEC: argument
369
        # python(2640)───socat(2642)───socat(2643)───bash(2644)
370
        if proc.exe(pid).endswith('/socat') and time.sleep(0.1) and proc.children(pid):
1!
371
            pid = proc.children(pid)[0]
×
372

373
        # We may attach to the remote process after the fork but before it performs an exec.  
374
        # If an exe is provided, wait until the process is actually running the expected exe
375
        # before we attach the debugger.
376
        t = Timeout()
1✔
377
        with t.countdown(2):
1✔
378
            while exe and os.path.realpath(proc.exe(pid)) != os.path.realpath(exe) and t.timeout:
1!
379
                time.sleep(0.1)
×
380

381
    elif isinstance(target, tubes.process.process):
1!
382
        pid = proc.pidof(target)[0]
1✔
383
        exe = exe or target.executable
1✔
384
    elif isinstance(target, tuple) and len(target) == 2:
×
385
        host, port = target
×
386

387
        if context.os != 'android':
×
388
            pre += 'target remote %s:%d\n' % (host, port)
×
389
        else:
390
            # Android debugging is done over gdbserver, which can't follow
391
            # new inferiors (tldr; follow-fork-mode child) unless it is run
392
            # in extended-remote mode.
393
            pre += 'target extended-remote %s:%d\n' % (host, port)
×
394
            pre += 'set detach-on-fork off\n'
×
395

396
        def findexe():
×
397
            for spid in proc.pidof(target):
×
398
                sexe = proc.exe(spid)
×
399
                name = os.path.basename(sexe)
×
400
                # XXX: parse cmdline
401
                if name.startswith('qemu-') or name.startswith('gdbserver'):
×
402
                    exe = proc.cmdline(spid)[-1]
×
403
                    return os.path.join(proc.cwd(spid), exe)
×
404

405
        exe = exe or findexe()
×
406
    elif isinstance(target, elf.corefile.Corefile):
×
407
        pre += 'target core "%s"\n' % target.path
×
408
    else:
409
        log.error("don't know how to attach to target: %r", target)
×
410

411
    # if we have a pid but no exe, just look it up in /proc/
412
    if pid and not exe:
1✔
413
        exe_fn = proc.exe
1✔
414
        if context.os == 'android':
1!
415
            exe_fn = adb.proc_exe
×
416
        exe = exe_fn(pid)
1✔
417

418
    if not pid and not exe and not ssh:
1!
419
        log.error('could not find target process')
×
420

421
    
422
    cmd = []
1✔
423
    pre = ""
1✔
424

425
    if context.os == 'android' and pid:
1!
426
        runner  = _get_runner()
×
427
        which   = _get_which()
×
428
        gdb_cmd = _gdbserver_args(pid=pid, which=which)
×
429
        gdbserver = runner(gdb_cmd)
×
430
        port    = _gdbserver_port(gdbserver, None)
×
431
        host    = context.adb_host
×
432
        pre    += 'target extended-remote %s:%i\n' % (context.adb_host, port)
×
433

434
        # gdbserver on Android sets 'detach-on-fork on' which breaks things
435
        # when you're trying to debug anything that forks.
436
        pre += 'set detach-on-fork off\n'
×
437

438
    if api:
1!
439
        # create a UNIX socket for talking to GDB
440
        socket_dir = tempfile.mkdtemp()
×
441
        socket_path = os.path.join(socket_dir, 'socket')
×
442
        bridge = os.path.join(os.path.dirname(__file__), 'gdb_api_bridge.py')
×
443

444
        # inject the socket path and the GDB Python API bridge
445
        pre = 'python socket_path = ' + repr(socket_path) + '\n' + \
×
446
              'source ' + bridge + '\n' + \
447
              pre
448

449
    radare2_script = pre + (radare2_script or '')
1✔
450
    
451
    
452
    radare2_binary = binary()
1✔
453
    
454
    cmd = [radare2_binary, "-e", "dbg.exe.path=%s" % exe]
1✔
455

456
    if radare2_script:
1!
457
        tmp = tempfile.NamedTemporaryFile(prefix = 'pwn', suffix = '.r2',
1✔
458
                                          delete = False, mode = 'w+')
459
        log.debug('Wrote radare2 script to %r\n%s', tmp.name, radare2_script)
1✔
460
        radare2_script = '!rm %s\n%s' % (tmp.name, radare2_script)
1✔
461

462
        tmp.write(radare2_script)
1✔
463
        tmp.close()
1✔
464
        cmd = cmd + ["-i", tmp.name]
1✔
465

466

467
    if exe and context.native and not pid:
1!
468
        if not ssh and not os.path.isfile(exe):
×
469
            log.error('No such file: %s', exe)
×
470
        cmd += [exe]
×
471
        
472
    if pid and not context.os == 'android':
1!
473
        cmd += ["-d", str(pid)]
1✔
474
        
475
    if radare2_args:
1!
476
        cmd += radare2_args
×
477
    log.info('running in new terminal with arguments: %s', radare2_args)
1✔
478
        
479
    log.info('running in new terminal: %s', cmd)
1✔
480

481
    if api:
1!
482
        # prevent gdb_faketerminal.py from messing up api doctests
483
        def preexec_fn():
×
484
            os.environ['GDB_FAKETERMINAL'] = '0'
×
485
    else:
486
        preexec_fn = None
1✔
487
    gdb_pid = misc.run_in_new_terminal(cmd, preexec_fn = preexec_fn)
1✔
488

489
    if pid and context.native:
1!
490
        proc.wait_for_debugger(pid, gdb_pid)
1✔
491

492
    if not api:
1!
493
        return gdb_pid
1✔
494

495
    # connect to the GDB Python API bridge
496
    from rpyc import BgServingThread
×
497
    from rpyc.utils.factory import unix_connect
×
498
    if six.PY2:
×
499
        retriable = socket.error
×
500
    else:
501
        retriable = ConnectionRefusedError, FileNotFoundError
×
502

503
    t = Timeout()
×
504
    with t.countdown(10):
×
505
        while t.timeout:
×
506
            try:
×
507
                conn = unix_connect(socket_path)
×
508
                break
×
509
            except retriable:
×
510
                time.sleep(0.1)
×
511
        else:
512
            # Check to see if RPyC is installed at all in GDB
513
            rpyc_check = [gdb_binary, '--nx', '-batch', '-ex',
×
514
                          'python import rpyc; import sys; sys.exit(123)']
515

516
            if 123 != tubes.process.process(rpyc_check).poll(block=True):
×
517
                log.error('Failed to connect to GDB: rpyc is not installed')
×
518

519
            # Check to see if the socket ever got created
520
            if not os.path.exists(socket_path):
×
521
                log.error('Failed to connect to GDB: Unix socket %s was never created', socket_path)
×
522

523
            # Check to see if the remote RPyC client is a compatible version
524
            version_check = [gdb_binary, '--nx', '-batch', '-ex',
×
525
                            'python import platform; print(platform.python_version())']
526
            gdb_python_version = tubes.process.process(version_check).recvall().strip()
×
527
            python_version = str(platform.python_version())
×
528

529
            if gdb_python_version != python_version:
×
530
                log.error('Failed to connect to GDB: Version mismatch (%s vs %s)',
×
531
                           gdb_python_version,
532
                           python_version)
533

534
            # Don't know what happened
535
            log.error('Failed to connect to GDB: Unknown error')
×
536

537
    # now that connection is up, remove the socket from the filesystem
538
    os.unlink(socket_path)
×
539
    os.rmdir(socket_dir)
×
540

541
    # create a thread for receiving breakpoint notifications
542
    BgServingThread(conn, callback=lambda: None)
×
543

544
    return gdb_pid, Gdb(conn)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc