0day_dev/pwntools-tutorial/elf.md
2020-08-07 21:19:09 -07:00

5.7 KiB

Table of Contents

ELFs

Pwntools makes interacting with ELF files relatively straightforward, via the ELF class. You can find the full documentation on RTD.

Loading ELF files

ELF files are loaded by path. Upon being loaded, some security-relevant attributes about the file are printed.

from pwn import *

e = ELF('/bin/bash')
# [*] '/bin/bash'
#     Arch:     amd64-64-little
#     RELRO:    Partial RELRO
#     Stack:    Canary found
#     NX:       NX enabled
#     PIE:      No PIE
#     FORTIFY:  Enabled

Using Symbols

ELF files have a few different sets of symbols available, each contained in a dictionary of {name: data}.

  • ELF.symbols lists all known symbols, including those below. Preference is given the PLT entries over GOT entries.
  • ELF.got only contains GOT entries
  • ELF.plt only contains PLT entries
  • ELF.functions only contains functions (requires DWARF symbols)

This is very useful in keeping exploits robust, by removing the need to hard-code addresses.

from pwn import *

e = ELF('/bin/bash')

print "%#x -> license" % e.symbols['bash_license']
print "%#x -> execve" % e.symbols['execve']
print "%#x -> got.execve" % e.got['execve']
print "%#x -> plt.execve" % e.plt['execve']
print "%#x -> list_all_jobs" % e.functions['list_all_jobs'].address

This would print something like the following:

0x4ba738 -> license
0x41db60 -> execve
0x6f0318 -> got.execve
0x41db60 -> plt.execve
0x446420 -> list_all_jobs

Changing the Base Address

Changing the base address of the ELF file (e.g. to adjust for ASLR) is very straightforward. Let's change the base address of bash, and see all of the symbols change.

from pwn import *

e = ELF('/bin/bash')

print "%#x -> base address" % e.address
print "%#x -> entry point" % e.entry
print "%#x -> execve" % e.symbols['execve']

print "---"
e.address = 0x12340000

print "%#x -> base address" % e.address
print "%#x -> entry point" % e.entry
print "%#x -> execve" % e.symbols['execve']

This should print something like:

0x400000 -> base address
0x42020b -> entry point
0x41db60 -> execve
---
0x12340000 -> base address
0x1236020b -> entry point
0x1235db60 -> execve

Reading ELF Files

We can directly interact with the ELF as if it were loaded into memory, using read, write, and functions named identically to that in the packing module. Additionally, you can see the disassembly via the disasm method.

from pwn import *

e = ELF('/bin/bash')

print repr(e.read(e.address, 4))

p_license = e.symbols['bash_license']
license   = e.unpack(p_license)
print "%#x -> %#x" % (p_license, license)

print e.read(license, 14)
print e.disasm(e.symbols['main'], 12)

This prints something like:

'\x7fELF'
0x4ba738 -> 0x4ba640
License GPLv3+
  41eab0:       41 57                   push   r15
  41eab2:       41 56                   push   r14
  41eab4:       41 55                   push   r13

Patching ELF Files

Patching ELF files is just as simple.

from pwn import *

e = ELF('/bin/bash')

# Cause a debug break on the 'exit' command
e.asm(e.symbols['exit_builtin'], 'int3')

# Disable chdir and just print it out instead
e.pack(e.got['chdir'], e.plt['puts'])

# Change the license
p_license = e.symbols['bash_license']
license = e.unpack(p_license)
e.write(license, 'Hello, world!\n\x00')

e.save('./bash-modified')

We can then run our modified version of bash.

$ chmod +x ./bash-modified
$ ./bash-modified -c 'exit'
Trace/breakpoint trap (core dumped)
$ ./bash-modified --version | grep "Hello"
Hello, world!
$ ./bash-modified -c 'cd "No chdir for you!"'
/home/user/No chdir for you!
No chdir for you!
./bash-modified: line 0: cd: No chdir for you!: No such file or directory

Searching ELF Files

Every once in a while, you just need to find some byte sequence. The most common example is searching for e.g. "/bin/sh\x00" for an execve call. The search method returns an iterator, allowing you to either take the first result, or keep searching if you need something special (e.g. no bad characters in the address). You can optionally pass a writable argument to search, indicating it should only return addresses in writable segments.

from pwn import *

e = ELF('/bin/bash')

for address in e.search('/bin/sh\x00'):
    print hex(address)

The above example prints something like:

0x420b82
0x420c5e

Building ELF Files

ELF files can be created from scratch relatively easy. All of these functions are context-aware. The relevant functions are from_bytes and from_assembly. Each returns an ELF object, which can easily be saved to file.

from pwn import *

ELF.from_bytes('\xcc').save('int3-1')
ELF.from_assembly('int3').save('int3-2')
ELF.from_assembly('nop', arch='powerpc').save('powerpc-nop')

Running and Debugging ELF Files

If you have an ELF object, you can run or debug it directly. The following are equivalent:

>>> io = elf.process()
# vs
>>> io = process(elf.path)

Similarly, you can launch a debugger trivially attached to your ELF. This is super useful when testing shellcode, without the need for a C wrapper to load and debug it.

>>> io = elf.debug()
# vs
>>> io = gdb.debug(elf.path)