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

Gallopsled / pwntools / 5f1e8a43414155cfd080df00632ea5c9a0b736d1-PR-1763

pending completion
5f1e8a43414155cfd080df00632ea5c9a0b736d1-PR-1763

Pull #1763

github-actions

web-flow
Merge 8d7613a41 into b73e74f1f
Pull Request #1763: Add env_add to the process module

3640 of 6422 branches covered (56.68%)

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

11697 of 16708 relevant lines covered (70.01%)

0.7 hits per line

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

66.5
/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.  By default, inherits from Python's environment.
62
        env_add(dict):
63
            Environment variables to add to the environment.
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
                 env_add = {},
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, 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
        #: Environment passed on envp
294
        self.env = dict(os.environ) if env is None else env_val
1✔
295

296
        #: Add environmemnt variables as needed
297
        self.env.update(env_add)
1✔
298

299
        if self.executable is None:
1✔
300
            if shell:
1!
301
                self.executable = '/bin/sh'
1✔
302
            else:
303
                self.executable = which(self.argv[0], path=self.env.get('PATH'))
×
304

305
        self._cwd = os.path.realpath(cwd or os.path.curdir)
1✔
306

307
        #: Alarm timeout of the process
308
        self.alarm        = alarm
1✔
309

310
        self.preexec_fn = preexec_fn
1✔
311
        self.display    = display or self.program
1✔
312
        self._qemu      = False
1✔
313
        self._corefile  = None
1✔
314

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

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

321
        with self.progress(message) as p:
1✔
322

323
            if not self.aslr:
1✔
324
                self.warn_once("ASLR is disabled!")
1✔
325

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

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

354
            p.success('pid %i' % self.pid)
1✔
355

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

364
            os.close(master)
1✔
365
            os.close(slave)
1✔
366

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

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

384
    def __preexec_fn(self):
1✔
385
        """
386
        Routine executed in the child process before invoking execve().
387

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

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

400
                resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
×
401
            except Exception:
×
402
                self.exception("Could not disable ASLR")
×
403

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

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

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

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

432

433
        if self.alarm is not None:
×
434
            signal.alarm(self.alarm)
×
435

436
        self.preexec_fn()
×
437

438
    def __on_enoexec(self, exception):
1✔
439
        """We received an 'exec format' error (ENOEXEC)
440

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

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

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

459
        if not qemu_path:
×
460
            raise exception
×
461

462
        qemu_path = which(qemu_path)
×
463
        if qemu_path:
×
464
            self._qemu = qemu_path
×
465

466
            args = [qemu_path]
×
467
            if self.argv:
×
468
                args += ['-0', self.argv[0]]
×
469
            args += ['--']
×
470

471
            return [args, qemu_path]
×
472

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

477
    @property
1✔
478
    def program(self):
479
        """Alias for ``executable``, for backward compatibility.
480

481
        Example:
482

483
            >>> p = process('/bin/true')
484
            >>> p.executable == '/bin/true'
485
            True
486
            >>> p.executable == p.program
487
            True
488

489
        """
490
        return self.executable
1✔
491

492
    @property
1✔
493
    def cwd(self):
494
        """Directory that the process is working in.
495

496
        Example:
497

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

513
        return self._cwd
1✔
514

515

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

520
        Mostly to make Python happy, but also to prevent common pitfalls.
521
        """
522

523
        orig_cwd = cwd
1✔
524
        cwd = cwd or os.path.curdir
1✔
525

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

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

543
        if not isinstance(executable, str):
1!
544
            executable = executable.decode('utf-8')
×
545

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

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

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

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

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

582
        return executable, argv, env
1✔
583

584
    def _handles(self, stdin, stdout, stderr):
1✔
585
        master = slave = None
1✔
586

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

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

606
            if stdin is PTY:
1✔
607
                stdin = slave
1✔
608
            if stdout is PTY:
1!
609
                stdout = slave
1✔
610
            if stderr is PTY:
1✔
611
                stderr = slave
1✔
612

613
        return stdin, stdout, stderr, master, slave
1✔
614

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

623
    def kill(self):
1✔
624
        """kill()
625

626
        Kills the process.
627
        """
628
        self.close()
1✔
629

630
    def poll(self, block = False):
1✔
631
        """poll(block = False) -> int
632

633
        Arguments:
634
            block(bool): Wait for the process to exit
635

636
        Poll the exit code of the process. Will return None, if the
637
        process has not yet finished and the exit code otherwise.
638
        """
639

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

644
        if block:
1✔
645
            self.wait_for_close()
1✔
646

647
        self.proc.poll()
1✔
648
        returncode = self.proc.returncode
1✔
649

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

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

662
    def communicate(self, stdin = None):
1✔
663
        """communicate(stdin = None) -> str
664

665
        Calls :meth:`subprocess.Popen.communicate` method on the process.
666
        """
667

668
        return self.proc.communicate(stdin)
×
669

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

676
        if not self.connected_raw('recv'):
1!
677
            raise EOFError
×
678

679
        if not self.can_recv_raw(self.timeout):
1✔
680
            return ''
1✔
681

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

687
        try:
1✔
688
            data = self.proc.stdout.read(numb)
1✔
689
        except IOError:
1✔
690
            pass
1✔
691

692
        if not data:
1✔
693
            self.shutdown("recv")
1✔
694
            raise EOFError
1✔
695

696
        return data
1✔
697

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

703
        if not self.connected_raw('send'):
1!
704
            raise EOFError
×
705

706
        try:
1✔
707
            self.proc.stdin.write(data)
1✔
708
            self.proc.stdin.flush()
1✔
709
        except IOError:
×
710
            raise EOFError
×
711

712
    def settimeout_raw(self, timeout):
1✔
713
        pass
1✔
714

715
    def can_recv_raw(self, timeout):
1✔
716
        if not self.connected_raw('recv'):
1!
717
            return False
×
718

719
        try:
1✔
720
            if timeout is None:
1✔
721
                return select.select([self.proc.stdout], [], []) == ([self.proc.stdout], [], [])
1✔
722

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

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

744
    def close(self):
1✔
745
        if self.proc is None:
1!
746
            return
×
747

748
        # First check if we are already dead
749
        self.poll()
1✔
750

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

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

769

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

774
        return self.proc.stdout.fileno()
×
775

776
    def shutdown_raw(self, direction):
1✔
777
        if direction == "send":
1✔
778
            self.proc.stdin.close()
1✔
779

780
        if direction == "recv":
1✔
781
            self.proc.stdout.close()
1✔
782

783
        if all(fp is None or fp.closed for fp in [self.proc.stdin, self.proc.stdout]):
1✔
784
            self.close()
1✔
785

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

791
        child_name = os.ttyname(tty_fd)
×
792

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

803
        os.setsid()
×
804

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

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

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

832
    def libs(self):
1✔
833
        """libs() -> dict
834

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

844
        if not maps_raw:
1!
845
            import pwnlib.elf.elf
×
846

847
            with context.quiet:
×
848
                return pwnlib.elf.elf.ELF(self.executable).maps
×
849

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

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

867
        return maps
1✔
868

869
    @property
1✔
870
    def libc(self):
871
        """libc() -> ELF
872

873
        Returns an ELF for the libc for the current process.
874
        If possible, it is adjusted to the correct address
875
        automatically.
876

877
        Example:
878

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

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

893
    @property
1✔
894
    def elf(self):
895
        """elf() -> pwnlib.elf.elf.ELF
896

897
        Returns an ELF file for the executable that launched the process.
898
        """
899
        import pwnlib.elf.elf
×
900
        return pwnlib.elf.elf.ELF(self.executable)
×
901

902
    @property
1✔
903
    def corefile(self):
904
        """corefile() -> pwnlib.elf.elf.Core
905

906
        Returns a corefile for the process.
907

908
        If the process is alive, attempts to create a coredump with GDB.
909

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

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

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

933
            if not finder.core_path:
1!
934
                self.error("Could not find core file for pid %i" % self.pid)
×
935

936
            core_hash = sha256file(finder.core_path)
1✔
937

938
            if self._corefile and self._corefile._hash == core_hash:
1✔
939
                return self._corefile
1✔
940

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

946
        return self._corefile
1✔
947

948
    def leak(self, address, count=1):
1✔
949
        r"""Leaks memory within the process at the specified address.
950

951
        Arguments:
952
            address(int): Address to leak memory at
953
            count(int): Number of bytes to leak at that address.
954

955
        Example:
956

957
            >>> e = ELF(which('bash-static'))
958
            >>> p = process(e.path)
959

960
            In order to make sure there's not a race condition against
961
            the process getting set up...
962

963
            >>> p.sendline(b'echo hello')
964
            >>> p.recvuntil(b'hello')
965
            b'hello'
966

967
            Now we can leak some data!
968

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

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

980
    readmem = leak
1✔
981

982
    def writemem(self, address, data):
1✔
983
        r"""Writes memory within the process at the specified address.
984

985
        Arguments:
986
            address(int): Address to write memory
987
            data(bytes): Data to write to the address
988

989
        Example:
990
        
991
            Let's write data to  the beginning of the mapped memory of the  ELF.
992

993
            >>> context.clear(arch='i386')
994
            >>> address = 0x100000
995
            >>> data = cyclic(32)
996
            >>> assembly = shellcraft.nop() * len(data)
997

998
            Wait for one byte of input, then write the data to stdout
999

1000
            >>> assembly += shellcraft.write(1, address, 1)
1001
            >>> assembly += shellcraft.read(0, 'esp', 1)
1002
            >>> assembly += shellcraft.write(1, address, 32)
1003
            >>> assembly += shellcraft.exit()
1004
            >>> asm(assembly)[32:]
1005
            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'
1006

1007
            Assemble the binary and test it
1008

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

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

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

1025

1026
    @property
1✔
1027
    def stdin(self):
1028
        """Shorthand for ``self.proc.stdin``
1029

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

1037
        See: :obj:`.process.proc`
1038
        """
1039
        return self.proc.stdout
×
1040
    @property
1✔
1041
    def stderr(self):
1042
        """Shorthand for ``self.proc.stderr``
1043

1044
        See: :obj:`.process.proc`
1045
        """
1046
        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