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

Gallopsled / pwntools / 11532290288

26 Oct 2024 01:42PM UTC coverage: 73.702% (-0.04%) from 73.738%
11532290288

push

github

peace-maker
Fix gzip compression on Python 2

3793 of 6394 branches covered (59.32%)

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

385 existing lines in 13 files now uncovered.

13304 of 18051 relevant lines covered (73.7%)

0.74 hits per line

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

82.4
/pwnlib/asm.py
1
# -*- coding: utf-8 -*-
2
r"""
3
Utilities for assembling and disassembling code.
4

5
Architecture Selection
6
------------------------
7

8
    Architecture, endianness, and word size are selected by using :mod:`pwnlib.context`.
9

10
    Any parameters which can be specified to ``context`` can also be specified as
11
    keyword arguments to either :func:`asm` or :func:`disasm`.
12

13
Assembly
14
------------------------
15

16
    To assemble code, simply invoke :func:`asm` on the code to assemble.
17

18
        >>> asm('mov eax, 0')
19
        b'\xb8\x00\x00\x00\x00'
20

21
    Additionally, you can use constants as defined in the :mod:`pwnlib.constants`
22
    module.
23

24
        >>> asm('mov eax, SYS_execve')
25
        b'\xb8\x0b\x00\x00\x00'
26

27
    Finally, :func:`asm` is used to assemble shellcode provided by ``pwntools``
28
    in the :mod:`shellcraft` module.
29

30
        >>> asm(shellcraft.nop())
31
        b'\x90'
32

33
Disassembly
34
------------------------
35

36
    To disassemble code, simply invoke :func:`disasm` on the bytes to disassemble.
37

38
    >>> disasm(b'\xb8\x0b\x00\x00\x00')
39
    '   0:   b8 0b 00 00 00          mov    eax, 0xb'
40

41
"""
42
from __future__ import absolute_import
1✔
43
from __future__ import division
1✔
44

45
import errno
1✔
46
import os
1✔
47
import platform
1✔
48
import re
1✔
49
import shutil
1✔
50
import subprocess
1✔
51
import tempfile
1✔
52
from collections import defaultdict
1✔
53
from glob import glob
1✔
54
from os import environ
1✔
55
from os import path
1✔
56

57
from pwnlib import atexit
1✔
58
from pwnlib import shellcraft
1✔
59
from pwnlib.context import LocalContext
1✔
60
from pwnlib.context import context
1✔
61
from pwnlib.log import getLogger
1✔
62
from pwnlib.util.hashes import sha1sumhex
1✔
63
from pwnlib.util.packing import _encode
1✔
64
from pwnlib.version import __version__
1✔
65

66
log = getLogger(__name__)
1✔
67

68
__all__ = ['asm', 'cpp', 'disasm', 'make_elf', 'make_elf_from_assembly']
1✔
69

70
_basedir = path.split(__file__)[0]
1✔
71
_incdir  = path.join(_basedir, 'data', 'includes')
1✔
72

73
def dpkg_search_for_binutils(arch, util):
1✔
74
    """Use dpkg to search for any available assemblers which will work.
75

76
    Returns:
77
        A list of candidate package names.
78

79
    ::
80

81
        >>> pwnlib.asm.dpkg_search_for_binutils('aarch64', 'as')
82
        ['binutils-aarch64-linux-gnu']
83
    """
84

85
    # Example output:
86
    # $ dpkg -S 'arm*linux*-as'
87
    # binutils-arm-linux-gnu: /usr/bin/arm-linux-gnu-as
88
    # binutils-arm-linux-gnueabihf: /usr/bin/arm-linux-gnueabihf-as
89
    # binutils-arm-linux-gnueabihf: /usr/x86_64-linux-gnu/arm-linux-gnueabihf/include/dis-asm.h
90
    # binutils-arm-linux-gnu: /usr/x86_64-linux-gnu/arm-linux-gnu/include/dis-asm.h
91
    packages = []
1✔
92

93
    try:
1✔
94
        filename = 'bin/%s*linux*-%s' % (arch, util)
1✔
95
        output = subprocess.check_output(['dpkg','-S',filename], universal_newlines = True)
1✔
96
        for line in output.strip().splitlines():
1✔
97
            package, path = line.split(':', 1)
1✔
98
            packages.append(package)
1✔
99
    except OSError:
×
100
        pass
×
101
    except subprocess.CalledProcessError:
×
102
        pass
×
103

104
    return packages
1✔
105

106
def print_binutils_instructions(util, context):
1✔
107
    """On failure to find a binutils utility, inform the user of a way
108
    they can get it easily.
109

110
    Doctest:
111

112
        >>> context.clear(arch = 'amd64')
113
        >>> pwnlib.asm.print_binutils_instructions('as', context)
114
        Traceback (most recent call last):
115
        ...
116
        PwnlibException: Could not find 'as' installed for ContextType(arch = 'amd64', bits = 64, endian = 'little')
117
        Try installing binutils for this architecture:
118
        $ sudo apt-get install binutils
119
    """
120
    # This links to our instructions on how to manually install binutils
121
    # for several architectures.
122
    instructions = 'https://docs.pwntools.com/en/stable/install/binutils.html'
1✔
123

124
    # However, if we can directly provide a useful command, go for it.
125
    binutils_arch = {
1✔
126
        'amd64': 'x86_64',
127
        'arm':   'armeabi',
128
        'thumb': 'armeabi',
129
    }.get(context.arch, context.arch)
130

131
    packages = dpkg_search_for_binutils(binutils_arch, util)
1✔
132

133
    if packages:
1!
134
        instructions = '$ sudo apt-get install %s' % packages[0]
1✔
135

136
    log.error("""
1✔
137
Could not find %(util)r installed for %(context)s
138
Try installing binutils for this architecture:
139
%(instructions)s
140
""".strip() % locals())
141

142

143
def check_binutils_version(util):
1✔
144
    if util_versions[util]:
1✔
145
        return util_versions[util]
1✔
146
    result = subprocess.check_output([util, '--version','/dev/null'],
1✔
147
                                     stderr=subprocess.STDOUT, universal_newlines=True)
148
    if 'clang' in result:
1!
149
        log.warn_once('Your binutils is clang-based and may not work!\n'
×
150
            'Try installing with: https://docs.pwntools.com/en/stable/install/binutils.html\n'
151
            'Reported version: %r', result.strip())
152
    version = re.search(r' (\d+\.\d+)', result).group(1)
1✔
153
    util_versions[util] = version = tuple(map(int, version.split('.')))
1✔
154
    return version
1✔
155

156

157
@LocalContext
1✔
158
def which_binutils(util, check_version=False):
1✔
159
    """
160
    Finds a binutils in the PATH somewhere.
161
    Expects that the utility is prefixed with the architecture name.
162

163
    Examples:
164

165
        >>> import platform
166
        >>> which_binutils = pwnlib.asm.which_binutils
167
        >>> which_binutils('as', arch=platform.machine())
168
        '.../bin/...as'
169
        >>> which_binutils('as', arch='arm') #doctest: +ELLIPSIS
170
        '.../bin/arm-...-as'
171
        >>> which_binutils('as', arch='powerpc') #doctest: +ELLIPSIS
172
        '.../bin/powerpc...-as'
173
        >>> which_binutils('as', arch='msp430') #doctest: +SKIP
174
        ...
175
        Traceback (most recent call last):
176
        ...
177
        Exception: Could not find 'as' installed for ContextType(arch = 'msp430')
178
    """
179
    arch = context.arch
1✔
180

181
    # Fix up pwntools vs Debian triplet naming, and account
182
    # for 'thumb' being its own pwntools architecture.
183
    arches = [arch] + {
1✔
184
        'thumb':  ['arm',    'aarch64'],
185
        'i386':   ['x86_64', 'amd64'],
186
        'i686':   ['x86_64', 'amd64'],
187
        'amd64':  ['x86_64', 'i386'],
188
        'mips64': ['mips'],
189
        'powerpc64': ['powerpc'],
190
        'sparc64': ['sparc'],
191
        'riscv32': ['riscv32', 'riscv64', 'riscv'],
192
        'riscv64': ['riscv64', 'riscv32', 'riscv'],
193
    }.get(arch, [])
194

195
    # If one of the candidate architectures matches the native
196
    # architecture, use that as a last resort.
197
    machine = platform.machine()
1✔
198
    machine = 'i386' if machine == 'i686' else machine
1✔
199
    try:
1✔
200
        with context.local(arch = machine):
1✔
201
            if context.arch in arches:
1✔
202
                arches.append(None)
1✔
203
    except AttributeError:
×
204
        log.warn_once("Your local binutils won't be used because architecture %r is not supported." % machine)
×
205

206
    utils = [util]
1✔
207

208
    # hack for homebrew-installed binutils on mac
209
    if platform.system() == 'Darwin':
1!
210
        utils = ['g'+util, util]
×
211

212
    if platform.system() == 'Windows':
1!
213
        utils = [util + '.exe']
×
214

215
    for arch in arches:
1!
216
        for gutil in utils:
1✔
217
            # e.g. objdump
218
            if arch is None:
1!
219
                patterns = [gutil]
×
220

221
            # e.g. aarch64-linux-gnu-objdump, avr-objdump
222
            else:
223
                patterns = ['%s*linux*-%s' % (arch, gutil),
1✔
224
                            '%s*-elf-%s' % (arch, gutil),
225
                            '%s-none*-%s' % (arch, gutil),
226
                            '%s-%s' % (arch, gutil)]
227

228
            for pattern in patterns:
1✔
229
                for dir in environ['PATH'].split(os.pathsep):
1✔
230
                    for res in sorted(glob(path.join(dir, pattern))):
1✔
231
                        if check_version:
1✔
232
                            ver = check_binutils_version(res)
1✔
233
                            return res, ver
1✔
234
                        return res
1✔
235

236
    # No dice!
237
    print_binutils_instructions(util, context)
×
238

239
util_versions = defaultdict(tuple)
1✔
240

241
def _assembler():
1✔
242
    gas, version = which_binutils('as', check_version=True)
1✔
243
    if version < (2, 19):
1!
244
        log.warn_once('Your binutils version is too old and may not work!\n'
×
245
            'Try updating with: https://docs.pwntools.com/en/stable/install/binutils.html\n'
246
            'Reported version: %r', version)
247

248
    E = {
1✔
249
        'big':    '-EB',
250
        'little': '-EL'
251
    }[context.endianness]
252

253
    B = '-%s' % context.bits
1✔
254

255
    assemblers = {
1✔
256
        'i386'   : [gas, B],
257
        'amd64'  : [gas, B],
258

259
        # Most architectures accept -EL or -EB
260
        'thumb'  : [gas, '-mthumb', E],
261
        'arm'    : [gas, E],
262
        'aarch64': [gas, E],
263
        'mips'   : [gas, E, B],
264
        'mips64' : [gas, E, B],
265
        'sparc':   [gas, E, B],
266
        'sparc64': [gas, E, B],
267

268
        # Powerpc wants -mbig or -mlittle, and -mppc32 or -mppc64
269
        'powerpc':   [gas, '-m%s' % context.endianness, '-mppc%s' % context.bits],
270
        'powerpc64': [gas, '-m%s' % context.endianness, '-mppc%s' % context.bits],
271

272
        # ia64 only accepts -mbe or -mle
273
        'ia64':    [gas, '-m%ce' % context.endianness[0]],
274

275
        # riscv64-unknown-elf-as supports riscv32 as well as riscv64
276
        'riscv32': [gas, '-march=rv32gc', '-mabi=ilp32'],
277
        'riscv64': [gas, '-march=rv64gc', '-mabi=lp64'],
278
    }
279

280
    assembler = assemblers.get(context.arch, [gas])
1✔
281

282
    return assembler
1✔
283

284
def _linker():
1✔
285
    ld, _ = which_binutils('ld', check_version=True)
1✔
286
    bfd = ['--oformat=' + _bfdname()]
1✔
287

288
    E = {
1✔
289
        'big':    '-EB',
290
        'little': '-EL'
291
    }[context.endianness]
292

293
    arguments = {
1✔
294
        'i386': ['-m', 'elf_i386'],
295
    }.get(context.arch, [])
296

297
    return [ld] + bfd + [E] + arguments
1✔
298

299

300
def _execstack(linker):
1✔
301
    ldflags = ['-z', 'execstack']
1✔
302
    version = util_versions[linker[0]]
1✔
303
    if version >= (2, 39):
1!
304
        return ldflags + ['--no-warn-execstack', '--no-warn-rwx-segments']
×
305
    return ldflags
1✔
306

307

308
def _objcopy():
1✔
309
    return [which_binutils('objcopy')]
1✔
310

311
def _objdump():
1✔
312
    path = [which_binutils('objdump')]
1✔
313

314
    if context.arch in ('i386', 'amd64'):
1✔
315
        path += ['-Mintel']
1✔
316

317
    return path
1✔
318

319
def _include_header():
1✔
320
    os   = context.os
1✔
321
    arch = context.arch
1✔
322
    include = '%s/%s.h' % (os, arch)
1✔
323

324
    if not include or not path.exists(path.join(_incdir, include)):
1✔
325
        log.warn_once("Could not find system include headers for %s-%s" % (arch,os))
1✔
326
        return '\n'
1✔
327

328
    return '#include <%s>\n' % include
1✔
329

330

331
def _arch_header():
1✔
332
    prefix  = ['.section .shellcode,"awx"',
1✔
333
                '.global _start',
334
                '.global __start',
335
                '_start:',
336
                '__start:']
337
    headers = {
1✔
338
        'i386'  :  ['.intel_syntax noprefix', '.p2align 0'],
339
        'amd64' :  ['.intel_syntax noprefix', '.p2align 0'],
340
        'arm'   : ['.syntax unified',
341
                   '.arch armv7-a',
342
                   '.arm',
343
                   '.p2align 2'],
344
        'thumb' : ['.syntax unified',
345
                   '.arch armv7-a',
346
                   '.thumb',
347
                   '.p2align 2'
348
                   ],
349
        'mips'  : ['.set mips2',
350
                   '.set noreorder',
351
                   '.p2align 2'
352
                   ],
353
    }
354

355
    return '\n'.join(prefix + headers.get(context.arch, [])) + '\n'
1✔
356

357
def _bfdname():
1✔
358
    arch = context.arch
1✔
359
    E    = context.endianness
1✔
360

361
    bfdnames = {
1✔
362
        'i386'    : 'elf32-i386',
363
        'aarch64' : 'elf64-%saarch64' % E,
364
        'amd64'   : 'elf64-x86-64',
365
        'arm'     : 'elf32-%sarm' % E,
366
        'thumb'   : 'elf32-%sarm' % E,
367
        'avr'     : 'elf32-avr',
368
        'mips'    : 'elf32-trad%smips' % E,
369
        'mips64'  : 'elf64-trad%smips' % E,
370
        'alpha'   : 'elf64-alpha',
371
        'cris'    : 'elf32-cris',
372
        'ia64'    : 'elf64-ia64-%s' % E,
373
        'm68k'    : 'elf32-m68k',
374
        'msp430'  : 'elf32-msp430',
375
        'powerpc' : 'elf32-powerpc',
376
        'powerpc64' : 'elf64-powerpc',
377
        'riscv32' : 'elf%d-%sriscv' % (context.bits, E),
378
        'riscv64' : 'elf%d-%sriscv' % (context.bits, E),
379
        'vax'     : 'elf32-vax',
380
        's390'    : 'elf%d-s390' % context.bits,
381
        'sparc'   : 'elf32-sparc',
382
        'sparc64' : 'elf64-sparc',
383
    }
384

385
    if arch in bfdnames:
1!
386
        return bfdnames[arch]
1✔
387
    else:
388
        raise Exception("Cannot find bfd name for architecture %r" % arch)
×
389

390

391
def _bfdarch():
1✔
392
    arch = context.arch
1✔
393
    convert = {
1✔
394
        'amd64':     'i386:x86-64',
395
        'i386':      'i386',
396
        'ia64':      'ia64-elf64',
397
        'mips64':    'mips',
398
        'powerpc64': 'powerpc',
399
        'sparc64':   'sparc',
400
        'thumb':     'arm',
401
        'riscv32':   'riscv',
402
        'riscv64':   'riscv',
403
    }
404

405
    if arch in convert:
1✔
406
        return convert[arch]
1✔
407

408
    return arch
1✔
409

410
def _run(cmd, stdin = None):
1✔
411
    log.debug('%s', subprocess.list2cmdline(cmd))
1✔
412
    try:
1✔
413
        proc = subprocess.Popen(
1✔
414
            cmd,
415
            stdin  = subprocess.PIPE,
416
            stdout = subprocess.PIPE,
417
            stderr = subprocess.PIPE,
418
            universal_newlines = True
419
        )
420
        stdout, stderr = proc.communicate(stdin)
1✔
421
        exitcode = proc.wait()
1✔
422
    except OSError as e:
×
423
        if e.errno == errno.ENOENT:
×
424
            log.exception('Could not run %r the program', cmd[0])
×
425
        else:
426
            raise
×
427

428
    if (exitcode, stderr) != (0, ''):
1!
429
        msg = 'There was an error running %r:\n'
×
430
        args = cmd,
×
431
        if exitcode != 0:
×
432
            msg += 'It had the exitcode %d.\n'
×
433
            args += exitcode,
×
434
        if stderr != '':
×
435
            msg += 'It had this on stdout:\n%s\n'
×
436
            args += stderr,
×
437
        log.error(msg, *args)
×
438

439
    return stdout
1✔
440

441
@LocalContext
1✔
442
def cpp(shellcode):
1✔
443
    r"""cpp(shellcode, ...) -> str
444

445
    Runs CPP over the given shellcode.
446

447
    The output will always contain exactly one newline at the end.
448

449
    Arguments:
450
        shellcode(str): Shellcode to preprocess
451

452
    Kwargs:
453
        Any arguments/properties that can be set on ``context``
454

455
    Examples:
456

457
        >>> cpp("mov al, SYS_setresuid", arch = "i386", os = "linux")
458
        'mov al, 164\n'
459
        >>> cpp("weee SYS_setresuid", arch = "arm", os = "linux")
460
        'weee (0+164)\n'
461
        >>> cpp("SYS_setresuid", arch = "thumb", os = "linux")
462
        '(0+164)\n'
463
        >>> cpp("SYS_setresuid", os = "freebsd")
464
        '311\n'
465
    """
466
    if platform.system() == 'Windows':
1!
467
        cpp = which_binutils('cpp')
×
468
    else:
469
        cpp = 'cpp'
1✔
470

471
    code = _include_header() + shellcode
1✔
472
    cmd  = [
1✔
473
        cpp,
474
        '-C',
475
        '-nostdinc',
476
        '-undef',
477
        '-P',
478
        '-I' + _incdir,
479
    ]
480
    return _run(cmd, code).strip('\n').rstrip() + '\n'
1✔
481

482

483
@LocalContext
1✔
484
def make_elf_from_assembly(assembly,
1✔
485
                           vma=None,
486
                           extract=False,
487
                           shared=False,
488
                           strip=False,
489
                           **kwargs):
490
    r"""make_elf_from_assembly(assembly, vma=None, extract=None, shared=False, strip=False, **kwargs) -> str
491

492
    Builds an ELF file with the specified assembly as its executable code.
493

494
    This differs from :func:`.make_elf` in that all ELF symbols are preserved,
495
    such as labels and local variables.  Use :func:`.make_elf` if size matters.
496
    Additionally, the default value for ``extract`` in :func:`.make_elf` is
497
    different.
498

499
    Note:
500
        This is effectively a wrapper around :func:`.asm`. with setting
501
        ``extract=False``, ``vma=0x10000000``, and marking the resulting
502
        file as executable (``chmod +x``).
503

504
    Note:
505
        ELF files created with `arch=thumb` will prepend an ARM stub
506
        which switches to Thumb mode.
507

508
    Arguments:
509
        assembly(str): Assembly code to build into an ELF
510
        vma(int): Load address of the binary
511
            (Default: ``0x10000000``, or ``0`` if ``shared=True``)
512
        extract(bool): Extract the full ELF data from the file.
513
            (Default: ``False``)
514
        shared(bool): Create a shared library
515
            (Default: ``False``)
516
        kwargs(dict): Arguments to pass to :func:`.asm`.
517

518
    Returns:
519

520
        The path to the assembled ELF (extract=False), or the data
521
        of the assembled ELF.
522

523
    Example:
524

525
        This example shows how to create a shared library, and load it via
526
        ``LD_PRELOAD``.
527

528
        >>> context.clear()
529
        >>> context.arch = 'amd64'
530
        >>> sc = 'push rbp; mov rbp, rsp;'
531
        >>> sc += shellcraft.echo('Hello\n')
532
        >>> sc += 'mov rsp, rbp; pop rbp; ret'
533
        >>> solib = make_elf_from_assembly(sc, shared=1)
534
        >>> subprocess.check_output(['echo', 'World'], env={'LD_PRELOAD': solib}, universal_newlines = True)
535
        'Hello\nWorld\n'
536

537
        The same thing can be done with :func:`.make_elf`, though the sizes
538
        are different.  They both
539

540
        >>> file_a = make_elf(asm('nop'), extract=True)
541
        >>> file_b = make_elf_from_assembly('nop', extract=True)
542
        >>> file_a[:4] == file_b[:4]
543
        True
544
        >>> len(file_a) < len(file_b)
545
        True
546
    """
547
    if shared and vma:
1!
548
        log.error("Cannot specify a VMA for a shared library.")
×
549

550
    if vma is None:
1✔
551
        if shared:
1✔
552
            vma = 0
1✔
553
        else:
554
            vma = 0x10000000
1✔
555

556
    if context.arch == 'thumb':
1✔
557
        to_thumb = shellcraft.arm.to_thumb()
1✔
558

559
        if not assembly.startswith(to_thumb):
1✔
560
            assembly = to_thumb + assembly
1✔
561

562
    result = asm(assembly, vma = vma, shared = shared, extract = False, **kwargs)
1✔
563

564
    if not extract:
1✔
565
        os.chmod(result, 0o755)
1✔
566
    else:
567
        with open(result, 'rb') as io:
1✔
568
            result = io.read()
1✔
569

570
    return result
1✔
571

572
@LocalContext
1✔
573
def make_elf(data,
1✔
574
             vma=None,
575
             strip=True,
576
             extract=True,
577
             shared=False):
578
    r"""make_elf(data, vma=None, strip=True, extract=True, shared=False, **kwargs) -> str
579

580
    Builds an ELF file with the specified binary data as its executable code.
581

582
    Arguments:
583
        data(str): Assembled code
584
        vma(int):  Load address for the ELF file
585
        strip(bool): Strip the resulting ELF file. Only matters if ``extract=False``.
586
            (Default: ``True``)
587
        extract(bool): Extract the assembly from the ELF file.
588
            If ``False``, the path of the ELF file is returned.
589
            (Default: ``True``)
590
        shared(bool): Create a Dynamic Shared Object (DSO, i.e. a ``.so``)
591
            which can be loaded via ``dlopen`` or ``LD_PRELOAD``.
592

593
    Examples:
594
        This example creates an i386 ELF that just does
595
        execve('/bin/sh',...).
596

597
        >>> context.clear(arch='i386')
598
        >>> bin_sh = unhex('6a68682f2f2f73682f62696e89e331c96a0b5899cd80')
599
        >>> filename = make_elf(bin_sh, extract=False)
600
        >>> p = process(filename)
601
        >>> p.sendline(b'echo Hello; exit')
602
        >>> p.recvline()
603
        b'Hello\n'
604
    """
605
    retval = None
1✔
606

607
    if shared and vma:
1!
608
        log.error("Cannot specify a VMA for a shared library.")
×
609

610
    if context.arch == 'thumb':
1✔
611
        to_thumb = asm(shellcraft.arm.to_thumb(), arch='arm')
1✔
612

613
        if not data.startswith(to_thumb):
1!
614
            data = to_thumb + data
1✔
615

616

617
    assembler = _assembler()
1✔
618
    linker    = _linker()
1✔
619
    code      = _arch_header()
1✔
620
    code      += '.string "%s"' % ''.join('\\x%02x' % c for c in bytearray(data))
1✔
621
    code      += '\n'
1✔
622

623
    log.debug("Building ELF:\n" + code)
1✔
624

625
    tmpdir    = tempfile.mkdtemp(prefix = 'pwn-asm-')
1✔
626
    step1     = path.join(tmpdir, 'step1-asm')
1✔
627
    step2     = path.join(tmpdir, 'step2-obj')
1✔
628
    step3     = path.join(tmpdir, 'step3-elf')
1✔
629

630
    try:
1✔
631
        with open(step1, 'w') as f:
1✔
632
            f.write(code)
1✔
633

634
        _run(assembler + ['-o', step2, step1])
1✔
635

636
        linker_options = _execstack(linker)
1✔
637
        if vma is not None:
1✔
638
            linker_options += ['--section-start=.shellcode=%#x' % vma,
1✔
639
                               '--entry=%#x' % vma]
640
        elif shared:
1!
641
            linker_options += ['-shared', '-init=_start']
×
642

643
        linker_options += ['-o', step3, step2]
1✔
644

645
        _run(linker + linker_options)
1✔
646

647
        if strip:
1!
648
            _run([which_binutils('objcopy'), '-Sg', step3])
1✔
649
            _run([which_binutils('strip'), '--strip-unneeded', step3])
1✔
650

651
        if not extract:
1✔
652
            os.chmod(step3, 0o755)
1✔
653
            retval = step3
1✔
654

655
        else:
656
            with open(step3, 'rb') as f:
1✔
657
                retval = f.read()
1✔
658
    except Exception:
×
659
        log.exception("An error occurred while building an ELF:\n%s" % code)
×
660
    else:
661
        atexit.register(lambda: shutil.rmtree(tmpdir))
1✔
662

663
    return retval
1✔
664

665

666
@LocalContext
1✔
667
def make_macho_from_assembly(shellcode):
1✔
668
    return make_macho(shellcode, is_shellcode=True)
×
669

670

671
@LocalContext
1✔
672
def make_macho(data, is_shellcode=False):
1✔
673
    prefix = []
×
674
    if context.arch == 'amd64':
×
675
        prefix = [
×
676
            '.intel_syntax noprefix',
677
        ]
678
    prefix.extend([
×
679
        '.text',
680
        '.global _start',
681
        '_start:',
682
        '.p2align 2',
683
    ])
684
    code = ''
×
685
    code += '\n'.join(prefix) + '\n'
×
686
    if is_shellcode:
×
687
        code += cpp(data)
×
688
    else:
689
        code += '.string "%s"' % ''.join('\\x%02x' % c for c in bytearray(data))
×
690

691
    log.debug('Assembling\n%s' % code)
×
692

693
    tmpdir = tempfile.mkdtemp(prefix = 'pwn-asm-')
×
694
    step1 = path.join(tmpdir, 'step1')
×
695
    step2 = path.join(tmpdir, 'step2')
×
696
    step3 = path.join(tmpdir, 'step3')
×
697

698
    with open(step1, 'w') as fd:
×
699
        fd.write(code)
×
700

701
    assembler = [
×
702
        '/usr/bin/as',
703
    ]
704
    asflags = [
×
705
        '-mmacosx-version-min=11.0',
706
        '-o', step2, step1,
707
    ]
708
    _run(assembler + asflags)
×
709

710
    linker = [
×
711
        '/usr/bin/ld',
712
    ]
713
    ldflags = [
×
714
        '-macos_version_min', '11.0',
715
        '-l', 'System',
716
        '-e', '_start',
717
        '-L', '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib',
718
        '-o', step3, step2,
719
    ]
720
    _run(linker + ldflags)
×
721

722
    os.chmod(step3, 0o755)
×
723

724
    return step3
×
725

726

727
@LocalContext
1✔
728
def asm(shellcode, vma = 0, extract = True, shared = False):
1✔
729
    r"""asm(code, vma = 0, extract = True, shared = False, ...) -> str
730

731
    Runs :func:`cpp` over a given shellcode and then assembles it into bytes.
732

733
    To see which architectures or operating systems are supported,
734
    look in :mod:`pwnlib.context`.
735

736
    Assembling shellcode requires that the GNU assembler is installed
737
    for the target architecture.
738
    See :doc:`Installing Binutils </install/binutils>` for more information.
739

740
    Arguments:
741
        shellcode(str): Assembler code to assemble.
742
        vma(int):       Virtual memory address of the beginning of assembly
743
        extract(bool):  Extract the raw assembly bytes from the assembled
744
                        file.  If :const:`False`, returns the path to an ELF file
745
                        with the assembly embedded.
746
        shared(bool):   Create a shared object.
747
        kwargs(dict):   Any attributes on :data:`.context` can be set, e.g.set
748
                        ``arch='arm'``.
749

750
    Examples:
751

752
        >>> asm("mov eax, SYS_select", arch = 'i386', os = 'freebsd')
753
        b'\xb8]\x00\x00\x00'
754
        >>> asm("mov eax, SYS_select", arch = 'amd64', os = 'linux')
755
        b'\xb8\x17\x00\x00\x00'
756
        >>> asm("mov rax, SYS_select", arch = 'amd64', os = 'linux')
757
        b'H\xc7\xc0\x17\x00\x00\x00'
758
        >>> asm("mov r0, #SYS_select", arch = 'arm', os = 'linux', bits=32)
759
        b'R\x00\xa0\xe3'
760
        >>> asm("mov #42, r0", arch = 'msp430')
761
        b'0@*\x00'
762
        >>> asm("la %r0, 42", arch = 's390', bits=64)
763
        b'A\x00\x00*'
764

765
        The output is cached:
766

767
        >>> start = time.time()
768
        >>> asm("lea rax, [rip+0]", arch = 'amd64', cache_dir = None) # force uncached time
769
        b'H\x8d\x05\x00\x00\x00\x00'
770
        >>> uncached_time = time.time() - start
771
        >>> asm("lea rax, [rip+0]", arch = 'amd64') # cache it
772
        b'H\x8d\x05\x00\x00\x00\x00'
773
        >>> start = time.time()
774
        >>> asm("lea rax, [rip+0]", arch = 'amd64')
775
        b'H\x8d\x05\x00\x00\x00\x00'
776
        >>> cached_time = time.time() - start
777
        >>> uncached_time > cached_time
778
        True
779
    """
780
    result = b''
1✔
781

782
    assembler = _assembler()
1✔
783
    linker    = _linker()
1✔
784
    objcopy   = _objcopy() + ['-j', '.shellcode', '-Obinary']
1✔
785
    code      = ''
1✔
786
    code      += _arch_header()
1✔
787
    code      += cpp(shellcode)
1✔
788

789
    log.debug('Assembling\n%s' % code)
1✔
790

791
    cache_file = None
1✔
792
    if context.cache_dir:
1!
793
        cache_dir = os.path.join(context.cache_dir, 'asm-cache')
1✔
794
        if not os.path.isdir(cache_dir):
1✔
795
            os.makedirs(cache_dir)
1✔
796

797
        # Include the context in the hash in addition to the shellcode
798
        hash_params = '{}_{}_{}_{}'.format(vma, extract, shared, __version__)
1✔
799
        fingerprint_params = _encode(code) + _encode(hash_params) + _encode(' '.join(assembler)) + _encode(' '.join(linker)) + _encode(' '.join(objcopy))
1✔
800
        asm_hash = sha1sumhex(fingerprint_params)
1✔
801
        cache_file = os.path.join(cache_dir, asm_hash)
1✔
802
        if os.path.exists(cache_file):
1✔
803
            log.debug('Using cached assembly output from %r', cache_file)
1✔
804
            if extract:
1✔
805
                with open(cache_file, 'rb') as f:
1✔
806
                    return f.read()
1✔
807

808
            # Create a temporary copy of the cached file to avoid modification.
809
            tmpdir = tempfile.mkdtemp(prefix = 'pwn-asm-')
1✔
810
            atexit.register(shutil.rmtree, tmpdir)
1✔
811
            step3 = os.path.join(tmpdir, 'step3')
1✔
812
            shutil.copy(cache_file, step3)
1✔
813
            return step3
1✔
814

815
    tmpdir    = tempfile.mkdtemp(prefix = 'pwn-asm-')
1✔
816
    step1     = path.join(tmpdir, 'step1')
1✔
817
    step2     = path.join(tmpdir, 'step2')
1✔
818
    step3     = path.join(tmpdir, 'step3')
1✔
819
    step4     = path.join(tmpdir, 'step4')
1✔
820

821
    try:
1✔
822
        with open(step1, 'w') as fd:
1✔
823
            fd.write(code)
1✔
824

825
        _run(assembler + ['-o', step2, step1])
1✔
826

827
        if not vma:
1✔
828
            shutil.copy(step2, step3)
1✔
829

830
        if vma or not extract:
1✔
831
            ldflags = _execstack(linker) + ['-o', step3, step2]
1✔
832
            if vma:
1✔
833
                ldflags += ['--section-start=.shellcode=%#x' % vma,
1✔
834
                            '--entry=%#x' % vma]
835
            elif shared:
1✔
836
                ldflags += ['-shared', '-init=_start']
1✔
837

838
            # In order to ensure that we generate ELF files with 4k pages,
839
            # and not e.g. 65KB pages (AArch64), force the page size.
840
            # This is a result of GNU Gold being silly.
841
            #
842
            # Introduced in commit dd58f409 without supporting evidence,
843
            # this shouldn't do anything except keep consistent page granularity
844
            # across architectures.
845
            ldflags += ['-z', 'max-page-size=4096',
1✔
846
                        '-z', 'common-page-size=4096']
847

848
            _run(linker + ldflags)
1✔
849

850
        elif open(step2,'rb').read(4) == b'\x7fELF':
1!
851
            # Sanity check for seeing if the output has relocations
852
            relocs = subprocess.check_output(
1✔
853
                [which_binutils('readelf'), '-r', step2],
854
                universal_newlines = True
855
            ).strip()
856
            if extract and len(relocs.split('\n')) > 1:
1!
857
                log.error('Shellcode contains relocations:\n%s' % relocs)
×
858
        else:
UNCOV
859
            shutil.copy(step2, step3)
×
860

861
        if not extract:
1✔
862
            if cache_file is not None:
1!
863
                shutil.copy(step3, cache_file)
1✔
864
            return step3
1✔
865

866
        _run(objcopy + [step3, step4])
1✔
867

868
        with open(step4, 'rb') as fd:
1✔
869
            result = fd.read()
1✔
870

871
    except Exception:
×
UNCOV
872
        lines = '\n'.join('%4i: %s' % (i+1,line) for (i,line) in enumerate(code.splitlines()))
×
UNCOV
873
        log.exception("An error occurred while assembling:\n%s" % lines)
×
874
    else:
875
        atexit.register(lambda: shutil.rmtree(tmpdir))
1✔
876

877
    if cache_file is not None and result != b'':
1!
878
        with open(cache_file, 'wb') as f:
1✔
879
            f.write(result)
1✔
880

881
    return result
1✔
882

883
@LocalContext
1✔
884
def disasm(data, vma = 0, byte = True, offset = True, instructions = True):
1✔
885
    """disasm(data, ...) -> str
886

887
    Disassembles a bytestring into human readable assembler.
888

889
    To see which architectures are supported,
890
    look in :mod:`pwnlib.contex`.
891

892
    Arguments:
893
      data(str): Bytestring to disassemble.
894
      vma(int): Passed through to the --adjust-vma argument of objdump
895
      byte(bool): Include the hex-printed bytes in the disassembly
896
      offset(bool): Include the virtual memory address in the disassembly
897

898
    Kwargs:
899
      Any arguments/properties that can be set on ``context``
900

901
    Examples:
902

903
        >>> print(disasm(unhex('b85d000000'), arch = 'i386'))
904
           0:   b8 5d 00 00 00          mov    eax, 0x5d
905
        >>> print(disasm(unhex('b85d000000'), arch = 'i386', byte = 0))
906
           0:   mov    eax, 0x5d
907
        >>> print(disasm(unhex('b85d000000'), arch = 'i386', byte = 0, offset = 0))
908
        mov    eax, 0x5d
909
        >>> print(disasm(unhex('b817000000'), arch = 'amd64'))
910
           0:   b8 17 00 00 00          mov    eax, 0x17
911
        >>> print(disasm(unhex('48c7c017000000'), arch = 'amd64'))
912
           0:   48 c7 c0 17 00 00 00    mov    rax, 0x17
913
        >>> print(disasm(unhex('04001fe552009000'), arch = 'arm'))  # doctest: +ELLIPSIS
914
           0:   e51f0004        ldr     r0, [pc, #-4]   ...
915
           4:   00900052        addseq  r0, r0, r2, asr r0
916
        >>> print(disasm(unhex('4ff00500'), arch = 'thumb', bits=32))
917
           0:   f04f 0005       mov.w   r0, #5
918
        >>> print(disasm(unhex('656664676665400F18A4000000000051'), byte=0, arch='amd64'))
919
           0:   gs data16 fs rex nop WORD PTR gs:[eax+eax*1+0x0]
920
           f:   push   rcx
921
        >>> print(disasm(unhex('01000000'), arch='sparc64'))
922
           0:   01 00 00 00     nop
923
        >>> print(disasm(unhex('60000000'), arch='powerpc64'))
924
           0:   60 00 00 00     nop
925
        >>> print(disasm(unhex('00000000'), arch='mips64'))
926
           0:   00000000        nop
927
        >>> print(disasm(unhex('48b84141414141414100c3'), arch='amd64'))
928
           0:   48 b8 41 41 41 41 41 41 41 00   movabs rax, 0x41414141414141
929
           a:   c3                      ret
930
        >>> print(disasm(unhex('00000000'), vma=0x80000000, arch='mips'))
931
        80000000:       00000000        nop
932
    """
933
    result = ''
1✔
934

935
    arch   = context.arch
1✔
936

937
    tmpdir = tempfile.mkdtemp(prefix = 'pwn-disasm-')
1✔
938
    step1  = path.join(tmpdir, 'step1')
1✔
939
    step2  = path.join(tmpdir, 'step2')
1✔
940

941
    bfdarch = _bfdarch()
1✔
942
    bfdname = _bfdname()
1✔
943
    objdump = _objdump() + ['-w', '-d', '--adjust-vma', str(vma), '-b', bfdname]
1✔
944
    objcopy = _objcopy() + [
1✔
945
        '-I', 'binary',
946
        '-O', bfdname,
947
        '-B', bfdarch,
948
        '--set-section-flags', '.data=code',
949
        '--rename-section', '.data=.text',
950
    ]
951

952
    if not byte:
1✔
953
        objdump += ['--no-show-raw-insn']
1✔
954

955
    if arch == 'thumb':
1✔
956
        objcopy += ['--prefix-symbol=$t.']
1✔
957
    else:
958
        objcopy += ['-w', '-N', '*']
1✔
959

960
    try:
1✔
961

962
        with open(step1, 'wb') as fd:
1✔
963
            fd.write(data)
1✔
964

965
        _run(objcopy + [step1, step2])
1✔
966

967
        output0 = _run(objdump + [step2])
1✔
968
        output1 = re.split(r'<\.text(?:\+0x0)?>:\n', output0, flags=re.MULTILINE)
1✔
969

970
        if len(output1) != 2:
1!
UNCOV
971
            log.error('Could not find .text in objdump output:\n%s' % output0)
×
972

973
        result = output1[1].strip('\n').rstrip().expandtabs()
1✔
UNCOV
974
    except Exception:
×
UNCOV
975
        log.exception("An error occurred while disassembling:\n%s" % data)
×
976
    else:
977
        atexit.register(lambda: shutil.rmtree(tmpdir))
1✔
978

979

980
    lines = []
1✔
981

982
    # Note: those patterns are also used in pwnlib/commandline/disasm.py
983
    pattern = '^( *[0-9a-f]+: *)', '((?:[0-9a-f]+ )+ *)', '(.*)'
1✔
984
    if not byte:
1✔
985
        pattern = pattern[::2]
1✔
986
    pattern = ''.join(pattern)
1✔
987
    for line in result.splitlines():
1✔
988
        match = re.search(pattern, line)
1✔
989
        if not match:
1✔
990
            lines.append(line)
1✔
991
            continue
1✔
992

993
        groups = match.groups()
1✔
994
        if byte:
1✔
995
            o, b, i = groups
1✔
996
        else:
997
            o, i = groups
1✔
998

999
        line = ''
1✔
1000

1001
        if offset:
1✔
1002
            line += o
1✔
1003
        if byte:
1✔
1004
            line += b
1✔
1005
        if instructions:
1!
1006
            line += i
1✔
1007
        lines.append(line)
1✔
1008

1009
    return re.sub(',([^ ])', r', \1', '\n'.join(lines))
1✔
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