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.
-
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
-
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()
-
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:
-
disasm
(address, n_bytes) → str[source]¶ Returns a string of disassembled instructions at the specified virtual memory address
-
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
-
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()
-
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: 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: 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 thisELF
imports any libraries which contain'libc[.-]
, and we can determine the appropriate path to it on the local system, returns a newELF
object pertaining to that library.If not found, the value will be
None
.
-
non_writable_segments
[source]¶ list
– List of all segments which are NOT writeable.- See:
ELF.segments
-
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
-
process
(argv=[], *a, **kw) → process[source]¶ Execute the binary with
process
. Note thatargv
is a list of arguments, and should not includeargv[0]
.Parameters: Returns:
-
read
(address, count) → bytes[source]¶ Read data from the specified virtual address
Parameters: Returns: A
str
object, orNone
.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'
-
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: 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 ofelftools.elf.sections.Section
objects for the segments in the ELF.
-
segments
[source]¶ list
– A list ofelftools.elf.segments.Segment
objects for the segments in the ELF.
-
ubsan
[source]¶ bool
– Whether the current binary was built with Undefined Behavior Sanitizer (UBSAN
).
-
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: 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'
-