pwnlib.elf — Working with ELF binaries

Most exploitable CTF challenges are provided in the Executable and Linkable Format (ELF). Generally, it is very useful to be able to interact with these files to extract data such as function addresses, ROP gadgets, and writable page addresses.

pwnlib.elf.load(*args, **kwargs)[source]

Compatibility wrapper for pwntools v1

class pwnlib.elf.ELF(path)[source]

Encapsulates information about an ELF file.

Example

>>> bash = ELF(which('bash'))
>>> hex(bash.symbols['read'])
0x41dac0
>>> hex(bash.plt['read'])
0x41dac0
>>> u32(bash.read(bash.got['read'], 4))
0x41dac6
>>> print bash.disasm(bash.plt.read, 16)
0:   ff 25 1a 18 2d 00       jmp    QWORD PTR [rip+0x2d181a]        # 0x2d1820
6:   68 59 00 00 00          push   0x59
b:   e9 50 fa ff ff          jmp    0xfffffffffffffa60
address[source]

int – Address of the lowest segment loaded in the ELF.

When updated, the addresses of the following fields are also updated:

  • symbols
  • got
  • plt
  • functions

However, the following fields are NOT updated:

Example

>>> bash = ELF('/bin/bash')
>>> read = bash.symbols['read']
>>> text = bash.get_section_by_name('.text').header.sh_addr
>>> bash.address += 0x1000
>>> read + 0x1000 == bash.symbols['read']
True
>>> text == bash.get_section_by_name('.text').header.sh_addr
True
asan[source]

bool – Whether the current binary was built with Address Sanitizer (ASAN).

aslr[source]

bool – Whether the current binary is position-independent.

asm(address, assembly)[source]

Assembles the specified instructions and inserts them into the ELF at the specified address.

This modifies the ELF in-pace. The resulting binary can be saved with ELF.save()

bss(offset=0) → int[source]
Returns:Address of the .bss section, plus the specified offset.
buildid[source]

str – GNU Build ID embedded into the binary

canary[source]

bool – Whether the current binary uses stack canaries.

checksec(banner=True)[source]

Prints out information in the binary, similar to checksec.sh.

Parameters:banner (bool) – Whether to print the path to the ELF binary.
data[source]

str – Raw data of the ELF file.

See:
get_data()
debug(argv=[], *a, **kw) → tube[source]

Debug the ELF with gdb.debug().

Parameters:
  • argv (list) – List of arguments to the binary
  • *args – Extra arguments to gdb.debug()
  • **kwargs – Extra arguments to gdb.debug()
Returns:

See gdb.debug()

Return type:

tube

disasm(address, n_bytes) → str[source]

Returns a string of disassembled instructions at the specified virtual memory address

dwarf[source]

DWARF info for the elf

dynamic_by_tag(tag) → tag[source]
Parameters:tag (str) – Named DT_XXX tag (e.g. 'DT_STRTAB').
Returns:elftools.elf.dynamic.DynamicTag
dynamic_string(offset) → bytes[source]

Fetches an enumerated string from the DT_STRTAB table.

Parameters:offset (int) – String index
Returns:String from the table as raw bytes.
Return type:str
elftype[source]

str – ELF type (EXEC, DYN, etc)

entry[source]

int – Address of the entry point for the ELF

entrypoint[source]

int – Address of the entry point for the ELF

execstack[source]

bool – Whether the current binary uses an executable stack.

executable_segments[source]

list – List of all segments which are executable.

See:
ELF.segments
fit(address, *a, **kw)[source]

Writes fitted data into the specified address.

See: packing.fit()

flat(address, *a, **kw)[source]

Writes a full array of values to the specified address.

See: packing.flat()

fortify[source]

bool – Whether the current binary was built with Fortify Source (-DFORTIFY).

static from_assembly(assembly) → ELF[source]

Given an assembly listing, return a fully loaded ELF object which contains that assembly at its entry point.

Parameters:
  • assembly (str) – Assembly language listing
  • vma (int) – Address of the entry point and the module’s base address.

Example

>>> e = ELF.from_assembly('nop; foo: int 0x80', vma = 0x400000)
>>> e.symbols['foo'] = 0x400001
>>> e.disasm(e.entry, 1)
'  400000:       90                      nop'
>>> e.disasm(e.symbols['foo'], 2)
'  400001:       cd 80                   int    0x80'
static from_bytes(bytes) → ELF[source]

Given a sequence of bytes, return a fully loaded ELF object which contains those bytes at its entry point.

Parameters:
  • bytes (str) – Shellcode byte string
  • vma (int) – Desired base address for the ELF.

Example

>>> e = ELF.from_bytes('\x90\xcd\x80', vma=0xc000)
>>> print(e.disasm(e.entry, 3))
    c000:       90                      nop
    c001:       cd 80                   int    0x80
get_data() → bytes[source]

Retrieve the raw data from the ELF file.

>>> bash = ELF(which('bash'))
>>> fd   = open(which('bash'))
>>> bash.get_data() == fd.read()
True
libc[source]

ELF – If this ELF imports any libraries which contain 'libc[.-], and we can determine the appropriate path to it on the local system, returns a new ELF object pertaining to that library.

If not found, the value will be None.

msan[source]

bool – Whether the current binary was built with Memory Sanitizer (MSAN).

non_writable_segments[source]

list – List of all segments which are NOT writeable.

See:
ELF.segments
nx[source]

bool – Whether the current binary uses NX protections.

offset_to_vaddr(offset) → int[source]

Translates the specified offset to a virtual address.

Parameters:offset (int) – Offset to translate
Returns:Virtual address which corresponds to the file offset, or None.
Return type:int

Examples

This example shows that regardless of changes to the virtual address layout by modifying ELF.address, the offset for any given address doesn’t change.

>>> bash = ELF('/bin/bash')
>>> bash.address == bash.offset_to_vaddr(0)
True
>>> bash.address += 0x123456
>>> bash.address == bash.offset_to_vaddr(0)
True
p16(address, data, *a, **kw)[source]

Writes a 16-bit integer data to the specified address

p32(address, data, *a, **kw)[source]

Writes a 32-bit integer data to the specified address

p64(address, data, *a, **kw)[source]

Writes a 64-bit integer data to the specified address

p8(address, data, *a, **kw)[source]

Writes a 8-bit integer data to the specified address

pack(address, data, *a, **kw)[source]

Writes a packed integer data to the specified address

packed[source]

bool – Whether the current binary is packed with UPX.

pie[source]

bool – Whether the current binary is position-independent.

process(argv=[], *a, **kw) → process[source]

Execute the binary with process. Note that argv is a list of arguments, and should not include argv[0].

Parameters:
  • argv (list) – List of arguments to the binary
  • *args – Extra arguments to process
  • **kwargs – Extra arguments to process
Returns:

process

read(address, count) → bytes[source]

Read data from the specified virtual address

Parameters:
  • address (int) – Virtual address to read
  • count (int) – Number of bytes to read
Returns:

A str object, or None.

Examples

The simplest example is just to read the ELF header.

>>> bash = ELF(which('bash'))
>>> bash.read(bash.address, 4)
'\x7fELF'

ELF segments do not have to contain all of the data on-disk that gets loaded into memory.

First, let’s create an ELF file has some code in two sections.

>>> assembly = '''
... .section .A,"awx"
... .global A
... A: nop
... .section .B,"awx"
... .global B
... B: int3
... '''
>>> e = ELF.from_assembly(assembly, vma=False)

By default, these come right after eachother in memory.

>>> e.read(e.symbols.A, 2)
'\x90\xcc'
>>> e.symbols.B - e.symbols.A
1

Let’s move the sections so that B is a little bit further away.

>>> objcopy = pwnlib.asm._objcopy()
>>> objcopy += [
...     '--change-section-vma', '.B+5',
...     '--change-section-lma', '.B+5',
...     e.path
... ]
>>> subprocess.check_call(objcopy)
0

Now let’s re-load the ELF, and check again

>>> e = ELF(e.path)
>>> e.symbols.B - e.symbols.A
6
>>> e.read(e.symbols.A, 2)
'\x90\x00'
>>> e.read(e.symbols.A, 7)
'\x90\x00\x00\x00\x00\x00\xcc'
>>> e.read(e.symbols.A, 10)
'\x90\x00\x00\x00\x00\x00\xcc\x00\x00\x00'

Everything is relative to the user-selected base address, so moving things around keeps everything working.

>>> e.address += 0x1000
>>> e.read(e.symbols.A, 10)
'\x90\x00\x00\x00\x00\x00\xcc\x00\x00\x00'
relro[source]

bool – Whether the current binary uses RELRO protections.

rpath[source]

bool – Whether the current binary has an RPATH.

runpath[source]

bool – Whether the current binary has a RUNPATH.

rwx_segments[source]

list – List of all segments which are writeable and executable.

See:
ELF.segments
save(path=None)[source]

Save the ELF to a file

>>> bash = ELF(which('bash'))
>>> bash.save('/tmp/bash_copy')
>>> copy = file('/tmp/bash_copy')
>>> bash = file(which('bash'))
>>> bash.read() == copy.read()
True
search(needle, writable = False) → generator[source]

Search the ELF’s virtual address space for the specified string.

Notes

Does not search empty space between segments, or uninitialized data. This will only return data that actually exists in the ELF file. Searching for a long string of NULL bytes probably won’t work.

Parameters:
  • needle (str) – String to search for.
  • writable (bool) – Search only writable sections.
Yields:

An iterator for each virtual address that matches.

Examples

An ELF header starts with the bytes \x7fELF, so we sould be able to find it easily.

>>> bash = ELF('/bin/bash')
>>> bash.address + 1 == next(bash.search('ELF'))
True

We can also search for string the binary.

>>> len(list(bash.search('GNU bash'))) > 0
True
section(name) → bytes[source]

Gets data for the named section

Parameters:name (str) – Name of the section
Returns:String containing the bytes for that section
Return type:str
sections[source]

list – A list of elftools.elf.sections.Section objects for the segments in the ELF.

segments[source]

list – A list of elftools.elf.segments.Segment objects for the segments in the ELF.

start[source]

int – Address of the entry point for the ELF

string(address)[source]

Reads a null-terminated string from the specified address

sym[source]

dotdict – Alias for ELF.symbols

u16(address, *a, **kw)[source]

Unpacks an integer from the specified address.

u32(address, *a, **kw)[source]

Unpacks an integer from the specified address.

u64(address, *a, **kw)[source]

Unpacks an integer from the specified address.

u8(address, *a, **kw)[source]

Unpacks an integer from the specified address.

ubsan[source]

bool – Whether the current binary was built with Undefined Behavior Sanitizer (UBSAN).

unpack(address, *a, **kw)[source]

Unpacks an integer from the specified address.

vaddr_to_offset(address) → int[source]

Translates the specified virtual address to a file offset

Parameters:address (int) – Virtual address to translate
Returns:Offset within the ELF file which corresponds to the address, or None.
Return type:int

Examples

>>> bash = ELF(which('bash'))
>>> bash.vaddr_to_offset(bash.address)
0
>>> bash.address += 0x123456
>>> bash.vaddr_to_offset(bash.address)
0
>>> bash.vaddr_to_offset(0) is None
True
writable_segments[source]

list – List of all segments which are writeable.

See:
ELF.segments
write(address, data)[source]

Writes data to the specified virtual address

Parameters:
  • address (int) – Virtual address to write
  • data (str) – Bytes to write

Note

This routine does not check the bounds on the write to ensure that it stays in the same segment.

Examples

>>> bash = ELF(which('bash'))
>>> bash.read(bash.address+1, 3)
'ELF'
>>> bash.write(bash.address, "HELO")
>>> bash.read(bash.address, 4)
'HELO'
class pwnlib.elf.Core(*a, **kw)[source]

Alias for Corefile