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

Gallopsled / pwntools / 7250413177

18 Dec 2023 03:44PM UTC coverage: 71.866% (-2.7%) from 74.55%
7250413177

Pull #2297

github

web-flow
Merge fbc1d8e0b into c7649c95e
Pull Request #2297: add "retguard" property and display it with checksec

4328 of 7244 branches covered (0.0%)

5 of 6 new or added lines in 1 file covered. (83.33%)

464 existing lines in 9 files now uncovered.

12381 of 17228 relevant lines covered (71.87%)

0.72 hits per line

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

70.33
/pwnlib/tubes/process.py
1
# -*- coding: utf-8 -*-
2
from __future__ import absolute_import
1✔
3
from __future__ import division
1✔
4

5
import ctypes
1✔
6
import errno
1✔
7
import logging
1✔
8
import os
1✔
9
import platform
1✔
10
import select
1✔
11
import signal
1✔
12
import six
1✔
13
import stat
1✔
14
import subprocess
1✔
15
import sys
1✔
16
import time
1✔
17

18
if sys.platform != 'win32':
1!
19
    import fcntl
1✔
20
    import pty
1✔
21
    import resource
1✔
22
    import tty
1✔
23

24
from pwnlib import qemu
1✔
25
from pwnlib.context import context
1✔
26
from pwnlib.log import getLogger
1✔
27
from pwnlib.timeout import Timeout
1✔
28
from pwnlib.tubes.tube import tube
1✔
29
from pwnlib.util.hashes import sha256file
1✔
30
from pwnlib.util.misc import parse_ldd_output
1✔
31
from pwnlib.util.misc import which
1✔
32
from pwnlib.util.misc import normalize_argv_env
1✔
33
from pwnlib.util.packing import _need_bytes
1✔
34

35
log = getLogger(__name__)
1✔
36

37
class PTY(object): pass
1✔
38
PTY=PTY()
1✔
39
STDOUT = subprocess.STDOUT
1✔
40
PIPE = subprocess.PIPE
1✔
41

42
signal_names = {-v:k for k,v in signal.__dict__.items() if k.startswith('SIG')}
1✔
43

44
class process(tube):
1✔
45
    r"""
46
    Spawns a new process, and wraps it with a tube for communication.
47

48
    Arguments:
49

50
        argv(list):
51
            List of arguments to pass to the spawned process.
52
        shell(bool):
53
            Set to `True` to interpret `argv` as a string
54
            to pass to the shell for interpretation instead of as argv.
55
        executable(str):
56
            Path to the binary to execute.  If :const:`None`, uses ``argv[0]``.
57
            Cannot be used with ``shell``.
58
        cwd(str):
59
            Working directory.  Uses the current working directory by default.
60
        env(dict):
61
            Environment variables to add to the environment.
62
        ignore_environ(bool):
63
            Ignore Python's environment.  By default use Python's environment iff env not specified.
64
        stdin(int):
65
            File object or file descriptor number to use for ``stdin``.
66
            By default, a pipe is used.  A pty can be used instead by setting
67
            this to ``PTY``.  This will cause programs to behave in an
68
            interactive manner (e.g.., ``python`` will show a ``>>>`` prompt).
69
            If the application reads from ``/dev/tty`` directly, use a pty.
70
        stdout(int):
71
            File object or file descriptor number to use for ``stdout``.
72
            By default, a pty is used so that any stdout buffering by libc
73
            routines is disabled.
74
            May also be ``PIPE`` to use a normal pipe.
75
        stderr(int):
76
            File object or file descriptor number to use for ``stderr``.
77
            By default, ``STDOUT`` is used.
78
            May also be ``PIPE`` to use a separate pipe,
79
            although the :class:`pwnlib.tubes.tube.tube` wrapper will not be able to read this data.
80
        close_fds(bool):
81
            Close all open file descriptors except stdin, stdout, stderr.
82
            By default, :const:`True` is used.
83
        preexec_fn(callable):
84
            Callable to invoke immediately before calling ``execve``.
85
        raw(bool):
86
            Set the created pty to raw mode (i.e. disable echo and control
87
            characters).  :const:`True` by default.  If no pty is created, this
88
            has no effect.
89
        aslr(bool):
90
            If set to :const:`False`, disable ASLR via ``personality`` (``setarch -R``)
91
            and ``setrlimit`` (``ulimit -s unlimited``).
92

93
            This disables ASLR for the target process.  However, the ``setarch``
94
            changes are lost if a ``setuid`` binary is executed.
95

96
            The default value is inherited from ``context.aslr``.
97
            See ``setuid`` below for additional options and information.
98
        setuid(bool):
99
            Used to control `setuid` status of the target binary, and the
100
            corresponding actions taken.
101

102
            By default, this value is :const:`None`, so no assumptions are made.
103

104
            If :const:`True`, treat the target binary as ``setuid``.
105
            This modifies the mechanisms used to disable ASLR on the process if
106
            ``aslr=False``.
107
            This is useful for debugging locally, when the exploit is a
108
            ``setuid`` binary.
109

110
            If :const:`False`, prevent ``setuid`` bits from taking effect on the
111
            target binary.  This is only supported on Linux, with kernels v3.5
112
            or greater.
113
        where(str):
114
            Where the process is running, used for logging purposes.
115
        display(list):
116
            List of arguments to display, instead of the main executable name.
117
        alarm(int):
118
            Set a SIGALRM alarm timeout on the process.
119

120
    Examples:
121

122
        >>> p = process('python')
123
        >>> p.sendline(b"print('Hello world')")
124
        >>> p.sendline(b"print('Wow, such data')")
125
        >>> b'' == p.recv(timeout=0.01)
126
        True
127
        >>> p.shutdown('send')
128
        >>> p.proc.stdin.closed
129
        True
130
        >>> p.connected('send')
131
        False
132
        >>> p.recvline()
133
        b'Hello world\n'
134
        >>> p.recvuntil(b',')
135
        b'Wow,'
136
        >>> p.recvregex(b'.*data')
137
        b' such data'
138
        >>> p.recv()
139
        b'\n'
140
        >>> p.recv() # doctest: +ELLIPSIS
141
        Traceback (most recent call last):
142
        ...
143
        EOFError
144

145
        >>> p = process('cat')
146
        >>> d = open('/dev/urandom', 'rb').read(4096)
147
        >>> p.recv(timeout=0.1)
148
        b''
149
        >>> p.write(d)
150
        >>> p.recvrepeat(0.1) == d
151
        True
152
        >>> p.recv(timeout=0.1)
153
        b''
154
        >>> p.shutdown('send')
155
        >>> p.wait_for_close()
156
        >>> p.poll()
157
        0
158

159
        >>> p = process('cat /dev/zero | head -c8', shell=True, stderr=open('/dev/null', 'w+b'))
160
        >>> p.recv()
161
        b'\x00\x00\x00\x00\x00\x00\x00\x00'
162

163
        >>> p = process(['python','-c','import os; print(os.read(2,1024).decode())'],
164
        ...             preexec_fn = lambda: os.dup2(0,2))
165
        >>> p.sendline(b'hello')
166
        >>> p.recvline()
167
        b'hello\n'
168

169
        >>> stack_smashing = ['python','-c','open("/dev/tty","wb").write(b"stack smashing detected")']
170
        >>> process(stack_smashing).recvall()
171
        b'stack smashing detected'
172

173
        >>> process(stack_smashing, stdout=PIPE).recvall()
174
        b''
175

176
        >>> getpass = ['python','-c','import getpass; print(getpass.getpass("XXX"))']
177
        >>> p = process(getpass, stdin=PTY)
178
        >>> p.recv()
179
        b'XXX'
180
        >>> p.sendline(b'hunter2')
181
        >>> p.recvall()
182
        b'\nhunter2\n'
183

184
        >>> process('echo hello 1>&2', shell=True).recvall()
185
        b'hello\n'
186

187
        >>> process('echo hello 1>&2', shell=True, stderr=PIPE).recvall()
188
        b''
189

190
        >>> a = process(['cat', '/proc/self/maps']).recvall()
191
        >>> b = process(['cat', '/proc/self/maps'], aslr=False).recvall()
192
        >>> with context.local(aslr=False):
193
        ...    c = process(['cat', '/proc/self/maps']).recvall()
194
        >>> a == b
195
        False
196
        >>> b == c
197
        True
198

199
        >>> process(['sh','-c','ulimit -s'], aslr=0).recvline()
200
        b'unlimited\n'
201

202
        >>> io = process(['sh','-c','sleep 10; exit 7'], alarm=2)
203
        >>> io.poll(block=True) == -signal.SIGALRM
204
        True
205

206
        >>> binary = ELF.from_assembly('nop', arch='mips')
207
        >>> p = process(binary.path)
208
        >>> binary_dir, binary_name = os.path.split(binary.path)
209
        >>> p = process('./{}'.format(binary_name), cwd=binary_dir)
210
        >>> p = process(binary.path, cwd=binary_dir)
211
        >>> p = process('./{}'.format(binary_name), cwd=os.path.relpath(binary_dir))
212
        >>> p = process(binary.path, cwd=os.path.relpath(binary_dir))
213
    """
214

215
    STDOUT = STDOUT
1✔
216
    PIPE = PIPE
1✔
217
    PTY = PTY
1✔
218

219
    #: Have we seen the process stop?  If so, this is a unix timestamp.
220
    _stop_noticed = 0
1✔
221

222
    proc = None
1✔
223

224
    def __init__(self, argv = None,
1!
225
                 shell = False,
226
                 executable = None,
227
                 cwd = None,
228
                 env = None,
229
                 ignore_environ = None,
230
                 stdin  = PIPE,
231
                 stdout = PTY,
232
                 stderr = STDOUT,
233
                 close_fds = True,
234
                 preexec_fn = lambda: None,
235
                 raw = True,
236
                 aslr = None,
237
                 setuid = None,
238
                 where = 'local',
239
                 display = None,
240
                 alarm = None,
241
                 *args,
242
                 **kwargs
243
                 ):
244
        super(process, self).__init__(*args,**kwargs)
1✔
245

246
        # Permit using context.binary
247
        if argv is None:
1✔
248
            if context.binary:
1!
249
                argv = [context.binary.path]
1✔
250
            else:
251
                raise TypeError('Must provide argv or set context.binary')
×
252

253

254
        #: :class:`subprocess.Popen` object that backs this process
255
        self.proc = None
1✔
256

257
        # We need to keep a copy of the un-_validated environment for printing
258
        original_env = env
1✔
259

260
        if shell:
1✔
261
            executable_val, argv_val, env_val = executable or '/bin/sh', argv, env
1✔
262
        else:
263
            executable_val, argv_val, env_val = self._validate(cwd, executable, argv, env)
1✔
264

265
        # Avoid the need to have to deal with the STDOUT magic value.
266
        if stderr is STDOUT:
1✔
267
            stderr = stdout
1✔
268

269
        # Determine which descriptors will be attached to a new PTY
270
        handles = (stdin, stdout, stderr)
1✔
271

272
        #: Which file descriptor is the controlling TTY
273
        self.pty          = handles.index(PTY) if PTY in handles else None
1✔
274

275
        #: Whether the controlling TTY is set to raw mode
276
        self.raw          = raw
1✔
277

278
        #: Whether ASLR should be left on
279
        self.aslr         = aslr if aslr is not None else context.aslr
1✔
280

281
        #: Whether setuid is permitted
282
        self._setuid      = setuid if setuid is None else bool(setuid)
1✔
283

284
        # Create the PTY if necessary
285
        stdin, stdout, stderr, master, slave = self._handles(*handles)
1✔
286

287
        #: Arguments passed on argv
288
        self.argv = argv_val
1✔
289

290
        #: Full path to the executable
291
        self.executable = executable_val
1✔
292

293
        if ignore_environ is None:
1!
294
            ignore_environ = env is not None  # compat
1✔
295

296
        #: Environment passed on envp
297
        self.env = {} if ignore_environ else dict(getattr(os, "environb", os.environ))
1✔
298

299
        # Add environment variables as needed
300
        self.env.update(env_val or {})
1✔
301

302
        self._cwd = os.path.realpath(cwd or os.path.curdir)
1✔
303

304
        #: Alarm timeout of the process
305
        self.alarm        = alarm
1✔
306

307
        self.preexec_fn = preexec_fn
1✔
308
        self.display    = display or self.program
1✔
309
        self._qemu      = False
1✔
310
        self._corefile  = None
1✔
311

312
        message = "Starting %s process %r" % (where, self.display)
1✔
313

314
        if self.isEnabledFor(logging.DEBUG):
1!
315
            if argv != [self.executable]: message += ' argv=%r ' % self.argv
×
316
            if original_env not in (os.environ, None):  message += ' env=%r ' % self.env
×
317

318
        with self.progress(message) as p:
1✔
319

320
            if not self.aslr:
1✔
321
                self.warn_once("ASLR is disabled!")
1✔
322

323
            # In the event the binary is a foreign architecture,
324
            # and binfmt is not installed (e.g. when running on
325
            # Travis CI), re-try with qemu-XXX if we get an
326
            # 'Exec format error'.
327
            prefixes = [([], self.executable)]
1✔
328
            exception = None
1✔
329

330
            for prefix, executable in prefixes:
1!
331
                try:
1✔
332
                    args = self.argv
1✔
333
                    if prefix:
1!
334
                        args = prefix + args
×
335
                    self.proc = subprocess.Popen(args = args,
1✔
336
                                                 shell = shell,
337
                                                 executable = executable,
338
                                                 cwd = cwd,
339
                                                 env = self.env,
340
                                                 stdin = stdin,
341
                                                 stdout = stdout,
342
                                                 stderr = stderr,
343
                                                 close_fds = close_fds,
344
                                                 preexec_fn = self.__preexec_fn)
345
                    break
1✔
346
                except OSError as exception:
×
347
                    if exception.errno != errno.ENOEXEC:
×
348
                        raise
×
349
                    prefixes.append(self.__on_enoexec(exception))
×
350

351
            p.success('pid %i' % self.pid)
1✔
352

353
        if self.pty is not None:
1✔
354
            if stdin is slave:
1✔
355
                self.proc.stdin = os.fdopen(os.dup(master), 'r+b', 0)
1✔
356
            if stdout is slave:
1!
357
                self.proc.stdout = os.fdopen(os.dup(master), 'r+b', 0)
1✔
358
            if stderr is slave:
1✔
359
                self.proc.stderr = os.fdopen(os.dup(master), 'r+b', 0)
1✔
360

361
            os.close(master)
1✔
362
            os.close(slave)
1✔
363

364
        # Set in non-blocking mode so that a call to call recv(1000) will
365
        # return as soon as a the first byte is available
366
        if self.proc.stdout:
1!
367
            fd = self.proc.stdout.fileno()
1✔
368
            fl = fcntl.fcntl(fd, fcntl.F_GETFL)
1✔
369
            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
1✔
370

371
        # Save off information about whether the binary is setuid / setgid
372
        self.suid = self.uid = os.getuid()
1✔
373
        self.sgid = self.gid = os.getgid()
1✔
374
        st = os.stat(self.executable)
1✔
375
        if self._setuid:
1!
376
            if (st.st_mode & stat.S_ISUID):
×
377
                self.suid = st.st_uid
×
378
            if (st.st_mode & stat.S_ISGID):
×
379
                self.sgid = st.st_gid
×
380

381
    def __preexec_fn(self):
1✔
382
        """
383
        Routine executed in the child process before invoking execve().
384

385
        Handles setting the controlling TTY as well as invoking the user-
386
        supplied preexec_fn.
387
        """
388
        if self.pty is not None:
×
389
            self.__pty_make_controlling_tty(self.pty)
×
390

391
        if not self.aslr:
×
392
            try:
×
393
                if context.os == 'linux' and self._setuid is not True:
×
394
                    ADDR_NO_RANDOMIZE = 0x0040000
×
395
                    ctypes.CDLL('libc.so.6').personality(ADDR_NO_RANDOMIZE)
×
396

397
                resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
398
            except Exception:
×
399
                self.exception("Could not disable ASLR")
×
400

401
        # Assume that the user would prefer to have core dumps.
402
        try:
×
403
            resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
×
404
        except Exception:
×
405
            pass
×
406

407
        # Given that we want a core file, assume that we want the whole thing.
408
        try:
×
409
            with open('/proc/self/coredump_filter', 'w') as f:
×
410
                f.write('0xff')
×
411
        except Exception:
×
412
            pass
×
413

414
        if self._setuid is False:
×
415
            try:
×
416
                PR_SET_NO_NEW_PRIVS = 38
×
417
                ctypes.CDLL('libc.so.6').prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
×
418
            except Exception:
×
419
                pass
×
420

421
        # Avoid issues with attaching to processes when yama-ptrace is set
422
        try:
×
423
            PR_SET_PTRACER = 0x59616d61
×
424
            PR_SET_PTRACER_ANY = -1
×
425
            ctypes.CDLL('libc.so.6').prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0)
×
426
        except Exception:
×
427
            pass
×
428

429

430
        if self.alarm is not None:
×
431
            signal.alarm(self.alarm)
×
432

433
        self.preexec_fn()
×
434

435
    def __on_enoexec(self, exception):
1✔
436
        """We received an 'exec format' error (ENOEXEC)
437

438
        This implies that the user tried to execute e.g.
439
        an ARM binary on a non-ARM system, and does not have
440
        binfmt helpers installed for QEMU.
441
        """
442
        # Get the ELF binary for the target executable
443
        with context.quiet:
×
444
            # XXX: Cyclic imports :(
445
            from pwnlib.elf import ELF
×
446
            binary = ELF(self.executable)
×
447

448
        # If we're on macOS, this will never work.  Bail now.
449
        # if platform.mac_ver()[0]:
450
            # self.error("Cannot run ELF binaries on macOS")
451

452
        # Determine what architecture the binary is, and find the
453
        # appropriate qemu binary to run it.
454
        qemu_path = qemu.user_path(arch=binary.arch)
×
455

456
        if not qemu_path:
×
457
            raise exception
×
458

459
        qemu_path = which(qemu_path)
×
460
        if qemu_path:
×
461
            self._qemu = qemu_path
×
462

463
            args = [qemu_path]
×
464
            if self.argv:
×
465
                args += ['-0', self.argv[0]]
×
466
            args += ['--']
×
467

468
            return [args, qemu_path]
×
469

470
        # If we get here, we couldn't run the binary directly, and
471
        # we don't have a qemu which can run it.
472
        self.exception(exception)
×
473

474
    @property
1✔
475
    def program(self):
1✔
476
        """Alias for ``executable``, for backward compatibility.
477

478
        Example:
479

480
            >>> p = process('/bin/true')
481
            >>> p.executable == '/bin/true'
482
            True
483
            >>> p.executable == p.program
484
            True
485

486
        """
487
        return self.executable
1✔
488

489
    @property
1✔
490
    def cwd(self):
1✔
491
        """Directory that the process is working in.
492

493
        Example:
494

495
            >>> p = process('sh')
496
            >>> p.sendline(b'cd /tmp; echo AAA')
497
            >>> _ = p.recvuntil(b'AAA')
498
            >>> p.cwd == '/tmp'
499
            True
500
            >>> p.sendline(b'cd /proc; echo BBB;')
501
            >>> _ = p.recvuntil(b'BBB')
502
            >>> p.cwd
503
            '/proc'
504
        """
505
        try:
1✔
506
            self._cwd = os.readlink('/proc/%i/cwd' % self.pid)
1✔
507
        except Exception:
1✔
508
            pass
1✔
509

510
        return self._cwd
1✔
511

512

513
    def _validate(self, cwd, executable, argv, env):
1✔
514
        """
515
        Perform extended validation on the executable path, argv, and envp.
516

517
        Mostly to make Python happy, but also to prevent common pitfalls.
518
        """
519

520
        orig_cwd = cwd
1✔
521
        cwd = cwd or os.path.curdir
1✔
522

523
        argv, env = normalize_argv_env(argv, env, self, 4)
1✔
524
        if env:
1✔
525
            env = {bytes(k): bytes(v) for k, v in env}
1✔
526
        if argv:
1!
527
            argv = list(map(bytes, argv))
1✔
528

529
        #
530
        # Validate executable
531
        #
532
        # - Must be an absolute or relative path to the target executable
533
        # - If not, attempt to resolve the name in $PATH
534
        #
535
        if not executable:
1!
536
            if not argv:
1!
537
                self.error("Must specify argv or executable")
×
538
            executable = argv[0]
1✔
539

540
        if not isinstance(executable, str):
1✔
541
            executable = executable.decode('utf-8')
1✔
542

543
        path = env and env.get(b'PATH')
1✔
544
        if path:
1✔
545
            path = path.decode()
1✔
546
        else:
547
            path = os.environ.get('PATH')
1✔
548
        # Do not change absolute paths to binaries
549
        if executable.startswith(os.path.sep):
1✔
550
            pass
1✔
551

552
        # If there's no path component, it's in $PATH or relative to the
553
        # target directory.
554
        #
555
        # For example, 'sh'
556
        elif os.path.sep not in executable and which(executable, path=path):
1✔
557
            executable = which(executable, path=path)
1✔
558

559
        # Either there is a path component, or the binary is not in $PATH
560
        # For example, 'foo/bar' or 'bar' with cwd=='foo'
561
        elif os.path.sep not in executable:
1!
562
            tmp = executable
×
563
            executable = os.path.join(cwd, executable)
×
564
            self.warn_once("Could not find executable %r in $PATH, using %r instead" % (tmp, executable))
×
565

566
        # There is a path component and user specified a working directory,
567
        # it must be relative to that directory. For example, 'bar/baz' with
568
        # cwd='foo' or './baz' with cwd='foo/bar'
569
        elif orig_cwd:
1!
570
            executable = os.path.join(orig_cwd, executable)
1✔
571

572
        if not os.path.exists(executable):
1!
573
            self.error("%r does not exist"  % executable)
×
574
        if not os.path.isfile(executable):
1!
575
            self.error("%r is not a file" % executable)
×
576
        if not os.access(executable, os.X_OK):
1!
577
            self.error("%r is not marked as executable (+x)" % executable)
×
578

579
        return executable, argv, env
1✔
580

581
    def _handles(self, stdin, stdout, stderr):
1✔
582
        master = slave = None
1✔
583

584
        if self.pty is not None:
1✔
585
            # Normally we could just use PIPE and be happy.
586
            # Unfortunately, this results in undesired behavior when
587
            # printf() and similar functions buffer data instead of
588
            # sending it directly.
589
            #
590
            # By opening a PTY for STDOUT, the libc routines will not
591
            # buffer any data on STDOUT.
592
            master, slave = pty.openpty()
1✔
593

594
            if self.raw:
1!
595
                # By giving the child process a controlling TTY,
596
                # the OS will attempt to interpret terminal control codes
597
                # like backspace and Ctrl+C.
598
                #
599
                # If we don't want this, we set it to raw mode.
600
                tty.setraw(master)
1✔
601
                tty.setraw(slave)
1✔
602

603
            if stdin is PTY:
1✔
604
                stdin = slave
1✔
605
            if stdout is PTY:
1!
606
                stdout = slave
1✔
607
            if stderr is PTY:
1✔
608
                stderr = slave
1✔
609

610
        return stdin, stdout, stderr, master, slave
1✔
611

612
    def __getattr__(self, attr):
1✔
613
        """Permit pass-through access to the underlying process object for
614
        fields like ``pid`` and ``stdin``.
615
        """
616
        if not attr.startswith('_') and hasattr(self.proc, attr):
1!
617
            return getattr(self.proc, attr)
1✔
618
        raise AttributeError("'process' object has no attribute '%s'" % attr)
×
619

620
    def kill(self):
1✔
621
        """kill()
622

623
        Kills the process.
624
        """
625
        self.close()
1✔
626

627
    def poll(self, block = False):
1✔
628
        """poll(block = False) -> int
629

630
        Arguments:
631
            block(bool): Wait for the process to exit
632

633
        Poll the exit code of the process. Will return None, if the
634
        process has not yet finished and the exit code otherwise.
635
        """
636

637
        # In order to facilitate retrieving core files, force an update
638
        # to the current working directory
639
        _ = self.cwd
1✔
640

641
        if block:
1✔
642
            self.wait_for_close()
1✔
643

644
        self.proc.poll()
1✔
645
        returncode = self.proc.returncode
1✔
646

647
        if returncode is not None and not self._stop_noticed:
1✔
648
            self._stop_noticed = time.time()
1✔
649
            signame = ''
1✔
650
            if returncode < 0:
1✔
651
                signame = ' (%s)' % (signal_names.get(returncode, 'SIG???'))
1✔
652

653
            self.info("Process %r stopped with exit code %d%s (pid %i)" % (self.display,
1✔
654
                                                                  returncode,
655
                                                                  signame,
656
                                                                  self.pid))
657
        return returncode
1✔
658

659
    def communicate(self, stdin = None):
1✔
660
        """communicate(stdin = None) -> str
661

662
        Calls :meth:`subprocess.Popen.communicate` method on the process.
663
        """
664

665
        return self.proc.communicate(stdin)
×
666

667
    # Implementation of the methods required for tube
668
    def recv_raw(self, numb):
1✔
669
        # This is a slight hack. We try to notice if the process is
670
        # dead, so we can write a message.
671
        self.poll()
1✔
672

673
        if not self.connected_raw('recv'):
1!
674
            raise EOFError
×
675

676
        if not self.can_recv_raw(self.timeout):
1✔
677
            return ''
1✔
678

679
        # This will only be reached if we either have data,
680
        # or we have reached an EOF. In either case, it
681
        # should be safe to read without expecting it to block.
682
        data = ''
1✔
683

684
        try:
1✔
685
            data = self.proc.stdout.read(numb)
1✔
686
        except IOError:
1✔
687
            pass
1✔
688

689
        if not data:
1✔
690
            self.shutdown("recv")
1✔
691
            raise EOFError
1✔
692

693
        return data
1✔
694

695
    def send_raw(self, data):
1✔
696
        # This is a slight hack. We try to notice if the process is
697
        # dead, so we can write a message.
698
        self.poll()
1✔
699

700
        if not self.connected_raw('send'):
1!
701
            raise EOFError
×
702

703
        try:
1✔
704
            self.proc.stdin.write(data)
1✔
705
            self.proc.stdin.flush()
1✔
706
        except IOError:
×
707
            raise EOFError
×
708

709
    def settimeout_raw(self, timeout):
1✔
710
        pass
1✔
711

712
    def can_recv_raw(self, timeout):
1✔
713
        if not self.connected_raw('recv'):
1!
714
            return False
×
715

716
        try:
1✔
717
            if timeout is None:
1✔
718
                return select.select([self.proc.stdout], [], []) == ([self.proc.stdout], [], [])
1✔
719

720
            return select.select([self.proc.stdout], [], [], timeout) == ([self.proc.stdout], [], [])
1✔
721
        except ValueError:
×
722
            # Not sure why this isn't caught when testing self.proc.stdout.closed,
723
            # but it's not.
724
            #
725
            #   File "/home/user/pwntools/pwnlib/tubes/process.py", line 112, in can_recv_raw
726
            #     return select.select([self.proc.stdout], [], [], timeout) == ([self.proc.stdout], [], [])
727
            # ValueError: I/O operation on closed file
728
            raise EOFError
×
729
        except select.error as v:
×
730
            if v.args[0] == errno.EINTR:
×
731
                return False
×
732

733
    def connected_raw(self, direction):
1✔
734
        if direction == 'any':
1✔
735
            return self.poll() is None
1✔
736
        elif direction == 'send':
1✔
737
            return self.proc.stdin and not self.proc.stdin.closed
1✔
738
        elif direction == 'recv':
1!
739
            return self.proc.stdout and not self.proc.stdout.closed
1✔
740

741
    def close(self):
1✔
742
        if self.proc is None:
1!
743
            return
×
744

745
        # First check if we are already dead
746
        self.poll()
1✔
747

748
        # close file descriptors
749
        for fd in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
1✔
750
            if fd is not None:
1!
751
                try:
1✔
752
                    fd.close()
1✔
753
                except IOError as e:
×
754
                    if e.errno != errno.EPIPE:
×
755
                        raise
×
756

757
        if not self._stop_noticed:
1✔
758
            try:
1✔
759
                self.proc.kill()
1✔
760
                self.proc.wait()
1✔
761
                self._stop_noticed = time.time()
1✔
762
                self.info('Stopped process %r (pid %i)' % (self.program, self.pid))
1✔
763
            except OSError:
×
764
                pass
×
765

766

767
    def fileno(self):
1✔
768
        if not self.connected():
×
769
            self.error("A stopped process does not have a file number")
×
770

771
        return self.proc.stdout.fileno()
×
772

773
    def shutdown_raw(self, direction):
1✔
774
        if direction == "send":
1✔
775
            self.proc.stdin.close()
1✔
776

777
        if direction == "recv":
1✔
778
            self.proc.stdout.close()
1✔
779

780
        if all(fp is None or fp.closed for fp in [self.proc.stdin, self.proc.stdout]):
1✔
781
            self.close()
1✔
782

783
    def __pty_make_controlling_tty(self, tty_fd):
1✔
784
        '''This makes the pseudo-terminal the controlling tty. This should be
785
        more portable than the pty.fork() function. Specifically, this should
786
        work on Solaris. '''
787

788
        child_name = os.ttyname(tty_fd)
×
789

790
        # Disconnect from controlling tty. Harmless if not already connected.
791
        try:
×
792
            fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
×
793
            if fd >= 0:
×
794
                os.close(fd)
×
795
        # which exception, shouldnt' we catch explicitly .. ?
796
        except OSError:
×
797
            # Already disconnected. This happens if running inside cron.
798
            pass
×
799

800
        os.setsid()
×
801

802
        # Verify we are disconnected from controlling tty
803
        # by attempting to open it again.
804
        try:
×
805
            fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
×
806
            if fd >= 0:
×
807
                os.close(fd)
×
808
                raise Exception('Failed to disconnect from '
×
809
                    'controlling tty. It is still possible to open /dev/tty.')
810
        # which exception, shouldnt' we catch explicitly .. ?
811
        except OSError:
×
812
            # Good! We are disconnected from a controlling tty.
813
            pass
×
814

815
        # Verify we can open child pty.
816
        fd = os.open(child_name, os.O_RDWR)
×
817
        if fd < 0:
×
818
            raise Exception("Could not open child pty, " + child_name)
×
819
        else:
820
            os.close(fd)
×
821

822
        # Verify we now have a controlling tty.
823
        fd = os.open("/dev/tty", os.O_WRONLY)
×
824
        if fd < 0:
×
825
            raise Exception("Could not open controlling tty, /dev/tty")
×
826
        else:
827
            os.close(fd)
×
828

829
    def libs(self):
1✔
830
        """libs() -> dict
831

832
        Return a dictionary mapping the path of each shared library loaded
833
        by the process to the address it is loaded at in the process' address
834
        space.
835
        """
836
        try:
1✔
837
            maps_raw = open('/proc/%d/maps' % self.pid).read()
1✔
838
        except IOError:
×
839
            maps_raw = None
×
840

841
        if not maps_raw:
1!
842
            import pwnlib.elf.elf
×
843

844
            with context.quiet:
×
845
                return pwnlib.elf.elf.ELF(self.executable).maps
×
846

847
        # Enumerate all of the libraries actually loaded right now.
848
        maps = {}
1✔
849
        for line in maps_raw.splitlines():
1✔
850
            if '/' not in line: continue
1✔
851
            path = line[line.index('/'):]
1✔
852
            path = os.path.realpath(path)
1✔
853
            if path not in maps:
1✔
854
                maps[path]=0
1✔
855

856
        for lib in maps:
1✔
857
            path = os.path.realpath(lib)
1✔
858
            for line in maps_raw.splitlines():
1!
859
                if line.endswith(path):
1✔
860
                    address = line.split('-')[0]
1✔
861
                    maps[lib] = int(address, 16)
1✔
862
                    break
1✔
863

864
        return maps
1✔
865

866
    @property
1✔
867
    def libc(self):
1✔
868
        """libc() -> ELF
869

870
        Returns an ELF for the libc for the current process.
871
        If possible, it is adjusted to the correct address
872
        automatically.
873

874
        Example:
875

876
        >>> p = process("/bin/cat")
877
        >>> libc = p.libc
878
        >>> libc # doctest: +SKIP
879
        ELF('/lib64/libc-...so')
880
        >>> p.close()
881
        """
882
        from pwnlib.elf import ELF
1✔
883

884
        for lib, address in self.libs().items():
1!
885
            if 'libc.so' in lib or 'libc-' in lib:
1✔
886
                e = ELF(lib)
1✔
887
                e.address = address
1✔
888
                return e
1✔
889

890
    @property
1✔
891
    def elf(self):
1✔
892
        """elf() -> pwnlib.elf.elf.ELF
893

894
        Returns an ELF file for the executable that launched the process.
895
        """
896
        import pwnlib.elf.elf
×
897
        return pwnlib.elf.elf.ELF(self.executable)
×
898

899
    @property
1✔
900
    def corefile(self):
1✔
901
        """corefile() -> pwnlib.elf.elf.Core
902

903
        Returns a corefile for the process.
904

905
        If the process is alive, attempts to create a coredump with GDB.
906

907
        If the process is dead, attempts to locate the coredump created
908
        by the kernel.
909
        """
910
        # If the process is still alive, try using GDB
911
        import pwnlib.elf.corefile
1✔
912
        import pwnlib.gdb
1✔
913

914
        try:
1✔
915
            if self.poll() is None:
1✔
916
                corefile = pwnlib.gdb.corefile(self)
1✔
917
                if corefile is None:
1!
918
                    self.error("Could not create corefile with GDB for %s", self.executable)
×
919
                return corefile
1✔
920

921
            # Handle race condition against the kernel or QEMU to write the corefile
922
            # by waiting up to 5 seconds for it to be written.
923
            t = Timeout()
1✔
924
            finder = None
1✔
925
            with t.countdown(5):
1✔
926
                while t.timeout and (finder is None or not finder.core_path):
1✔
927
                    finder = pwnlib.elf.corefile.CorefileFinder(self)
1✔
928
                    time.sleep(0.5)
1✔
929

930
            if not finder.core_path:
1!
931
                self.error("Could not find core file for pid %i" % self.pid)
×
932

933
            core_hash = sha256file(finder.core_path)
1✔
934

935
            if self._corefile and self._corefile._hash == core_hash:
1✔
936
                return self._corefile
1✔
937

938
            self._corefile = pwnlib.elf.corefile.Corefile(finder.core_path)
1✔
939
        except AttributeError as e:
×
940
            raise RuntimeError(e) # AttributeError would route through __getattr__, losing original message
×
941
        self._corefile._hash = core_hash
1✔
942

943
        return self._corefile
1✔
944

945
    def leak(self, address, count=1):
1✔
946
        r"""Leaks memory within the process at the specified address.
947

948
        Arguments:
949
            address(int): Address to leak memory at
950
            count(int): Number of bytes to leak at that address.
951

952
        Example:
953

954
            >>> e = ELF(which('bash-static'))
955
            >>> p = process(e.path)
956

957
            In order to make sure there's not a race condition against
958
            the process getting set up...
959

960
            >>> p.sendline(b'echo hello')
961
            >>> p.recvuntil(b'hello')
962
            b'hello'
963

964
            Now we can leak some data!
965

966
            >>> p.leak(e.address, 4)
967
            b'\x7fELF'
968
        """
969
        # If it's running under qemu-user, don't leak anything.
970
        if 'qemu-' in os.path.realpath('/proc/%i/exe' % self.pid):
1!
971
            self.error("Cannot use leaker on binaries under QEMU.")
×
972

973
        with open('/proc/%i/mem' % self.pid, 'rb') as mem:
1✔
974
            mem.seek(address)
1✔
975
            return mem.read(count) or None
1✔
976

977
    readmem = leak
1✔
978

979
    def writemem(self, address, data):
1✔
980
        r"""Writes memory within the process at the specified address.
981

982
        Arguments:
983
            address(int): Address to write memory
984
            data(bytes): Data to write to the address
985

986
        Example:
987
        
988
            Let's write data to  the beginning of the mapped memory of the  ELF.
989

990
            >>> context.clear(arch='i386')
991
            >>> address = 0x100000
992
            >>> data = cyclic(32)
993
            >>> assembly = shellcraft.nop() * len(data)
994

995
            Wait for one byte of input, then write the data to stdout
996

997
            >>> assembly += shellcraft.write(1, address, 1)
998
            >>> assembly += shellcraft.read(0, 'esp', 1)
999
            >>> assembly += shellcraft.write(1, address, 32)
1000
            >>> assembly += shellcraft.exit()
1001
            >>> asm(assembly)[32:]
1002
            b'j\x01[\xb9\xff\xff\xef\xff\xf7\xd1\x89\xdaj\x04X\xcd\x801\xdb\x89\xe1j\x01Zj\x03X\xcd\x80j\x01[\xb9\xff\xff\xef\xff\xf7\xd1j Zj\x04X\xcd\x801\xdbj\x01X\xcd\x80'
1003

1004
            Assemble the binary and test it
1005

1006
            >>> elf = ELF.from_assembly(assembly, vma=address)
1007
            >>> io = elf.process()
1008
            >>> _ = io.recvuntil(b'\x90')
1009
            >>> _ = io.writemem(address, data)
1010
            >>> io.send(b'X')
1011
            >>> io.recvall()
1012
            b'aaaabaaacaaadaaaeaaafaaagaaahaaa'
1013
        """
1014

1015
        if 'qemu-' in os.path.realpath('/proc/%i/exe' % self.pid):
1!
1016
            self.error("Cannot use leaker on binaries under QEMU.")
×
1017

1018
        with open('/proc/%i/mem' % self.pid, 'wb') as mem:
1✔
1019
            mem.seek(address)
1✔
1020
            return mem.write(data)
1✔
1021

1022

1023
    @property
1✔
1024
    def stdin(self):
1✔
1025
        """Shorthand for ``self.proc.stdin``
1026

1027
        See: :obj:`.process.proc`
1028
        """
1029
        return self.proc.stdin
1✔
1030
    @property
1✔
1031
    def stdout(self):
1✔
1032
        """Shorthand for ``self.proc.stdout``
1033

1034
        See: :obj:`.process.proc`
1035
        """
UNCOV
1036
        return self.proc.stdout
×
1037
    @property
1✔
1038
    def stderr(self):
1✔
1039
        """Shorthand for ``self.proc.stderr``
1040

1041
        See: :obj:`.process.proc`
1042
        """
1043
        return self.proc.stderr
×
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