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

Gallopsled / pwntools / 12797806857

15 Jan 2025 10:04PM UTC coverage: 71.089% (-2.6%) from 73.646%
12797806857

push

github

peace-maker
Drop Python 2.7 support / Require Python 3.10

Only test on Python 3 and bump minimal required python version to 3.10.

3628 of 6402 branches covered (56.67%)

12848 of 18073 relevant lines covered (71.09%)

0.71 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']
1✔
305
    return ldflags
×
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
        '-Wno-unused-command-line-argument',
475
        '-C',
476
        '-nostdinc',
477
        '-undef',
478
        '-P',
479
        '-I' + _incdir,
480
    ]
481
    return _run(cmd, code).strip('\n').rstrip() + '\n'
1✔
482

483

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

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

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

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

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

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

519
    Returns:
520

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

524
    Example:
525

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

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

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

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

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

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

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

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

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

571
    return result
1✔
572

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

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

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

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

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

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

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

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

617

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

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

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

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

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

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

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

646
        _run(linker + linker_options)
1✔
647

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

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

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

664
    return retval
1✔
665

666

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

671

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

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

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

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

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

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

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

725
    return step3
×
726

727

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

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

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

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

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

751
    Examples:
752

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

766
        The output is cached:
767

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

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

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

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

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

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

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

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

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

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

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

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

849
            _run(linker + ldflags)
1✔
850

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

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

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

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

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

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

882
    return result
1✔
883

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

888
    Disassembles a bytestring into human readable assembler.
889

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

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

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

902
    Examples:
903

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

936
    arch   = context.arch
1✔
937

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

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

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

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

961
    try:
1✔
962

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

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

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

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

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

980

981
    lines = []
1✔
982

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

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

1000
        line = ''
1✔
1001

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

1010
    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

© 2026 Coveralls, Inc