PwnTools

PwnTools

A PwnTools é uma biblioteca python que permite a criação de scripts que ajudam na resolução de desafios CTFs, automatização, análise de binários e desenvolvimento de exploits.

Nesse documento, será apresentado algumas das principais funções disponíveis da biblioteca e alguns exemplos de uso.

Instalação

Utilizando pip:

pip3 install pwntools

Após instalação, basta apenas colocar a seguinte linha para importar a biblioteca:

from pwn import *

Documentação

Nesta seção, serão apresentadas as principais funções da biblioteca.

Conectar a processos

É possível interagir com processos locais ou remotos:

# Cria um processo do binário vuln
p = process("./vuln")

# Cria um processo remoto. Exemplo: Se o servidor for "nc chall.ctf 1234"
p = remote("chall.ctf", 1234)
# É possível interagir com esse processo criado

# Irá ler todos os dados gerados pelo processo ou pode receber como parâmetro
# a quantidade de bytes que deve ler
p.recv()

# Irá ler até a quebra de linha '\n'
p.recvline()

# Irá ler até encontrar a string passada como parâmetro
# Nesse caso, irá ler até encontrar a string 'digite:'
p.recvuntil('digite:')

# Irá ler tudo, o decode() no final é apenas para formatar a string corretamente
p.clean().decode()

# Irá enviar a string passada como parâmetro
p.send(b'abc')

# Irá enviar a string passada como parâmetro mas com um '\n' no final
p.sendline(b'abc')

# Irá enviar a a string 'abc' após receber a string 'Digite:'
p.sendafter('Digite:', b'abc')

# Irá abrir uma instância do terminal do processo para interagir manualmente
p.interactive()

# Espera o processo terminar
p.wait()
# É possível instanciar o gdb no processo executado
p = process('./exec')

# É possível definir parâmetros como comandos gdb, como breakpoints e se tem ou não aslr
gdb.attach(p, gdbscript='''
b *main+45
''', aslr=False)

Ambiente e contexto

É possível definir o ambiente que executará o processo.

# É possível definir a arquitetura
# Valores possíveis 'aarch64', 'arm', 'i386', 'amd64'
# Padrão 'i386'
context.arch = 'amd64'

# É possível definir se o processo é little endian ou big endian
# Padrão 'little'
context.endian = 'big'

Codificação e utilidades

Há algumas funções de codificação, decodificação e utilidade gerais

# Transforma de caracteres para hexadecimal e vice-versa
enhex(b'/bin/sh')        # = '2f62696e2f7368'
unhex('2f62696e2f7368')  # = b'/bin/sh'

# Codifica e decodifica em base64
b64e(b'basemeiaquatro')       # = 'YmFzZW1laWFxdWF0cm8='
b64d('YmFzZW1laWFxdWF0cm8=')  # = b'basemeiaquatro'

# É possível calcular md5 e sha1
# Para a 'md5filehex' e 'sha1filehex' é preciso passar o caminho do arquivo
md5sumhex(b'hash')     # = '0800fc577294c34e0b28ad2839435945'
md5filehex('./exec')   # = '9f27142d5424be26f280a50c1ed1b5af'
sha1sumhex(b'hash')    # = '2346ad27d7568ba9896f1b7da6b5991251debdf2'
sha1filehex('./exec')  # = '28b1419d35ab80a8d49015760fcbafee641e83eb'

# É possível converter de representação inteira
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'

# É possível converter para representação inteira
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

# É possível fazer essa conversão com string com tamanhos não usuais
pack(0x2F62696E2F7368, 'all')  # = b'\x02CBA@'
unpack(b'\x02CBA@', 'all')     # = 275972768514 = 0x4041424302

# É possível gerar sequências cíclicas. Útil para encontrar que parte da string
# que ocorre algum buffer overflow
cyclic(32)              # = b'aaaabaaacaaadaaaeaaafaaagaaahaaa'
cyclic_find(0x61616166) # = 20

# É possível gerar hexdump de strings de bytes
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
'''

Assembly

É possível escrever código assembly para enviar para o processo. Útil para exploits como shellcode.

shellcode = asm('''
mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, SYS_execve
syscall
''')

shellcode = shellcraft.pushstr('/bin/sh')
shellcode += shellcraft.syscall('SYS_execve', 'rsp', 0, 0)

# É preciso converter para bytes para enviar
payload = bytes(asm(shellcode))
# É possível usar um shellcode padrão do pwntools
shellcode = shellcraft.sh()
payload = bytes(asm(shellcode))

'''
* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80
'''

ELFs, Strings e símbolos

É possível encontrar diversas informações a respeito do binário, como endereços de algumas funções, de string específicas, etc.

# Instanciar um elf
elf = ELF('./vuln')
# Acessando símbolos
elf.plt  # Todos os símbolos da PLT
elf.got  # Todos os símbolos da GOT

elf.sym # Todos os símbolos conhecidos

# Exemplos
elf.plt['printf']  # = 4160
elf.got['printf']  # = 16384
elf.sym['system']  # = 4144

# É possível setar o endereço base do processo
elf.address = 0x400000

# É possível instanciar a libc
libc = ELF('./libc.so.6')

# É possível setar o endereço base da libc
libc.address = 0x40404000

# É possível procurar por strings no arquivo elf
# Exemplo: Pega a primeira instância da string '/bin/sh'
bin_sh = next(elf.search(b'/bin/sh'))

Return Oriented Programming (ROP)

É possível instanciar um analisador ROP.

elf = ELF('./target')
rop = ROP(elf)
# É possível procurar por gadgets específicos
pop_rax = rop.find_gadget(['pop rax', 'ret']).address
syscall = rop.find_gadget(['syscall', 'ret']).address

# Outra possibilidade é procurar por gadgets do tipo 'pop registrador; ret'
pop_rdi = rop.rdi.address
pop_rsi = rop.rsi.address

rop.call(elf.sym.puts, [0xdeadbeef])

# A função call, é o mesmo que fazer as seguintes três intruções
rop.raw(rop.rdi.address)  # pop rdi; ret
rop.raw(0xdeadbeef)
rop.raw(elf.sym.puts)

# Converte a sequência de rop para bytes
payload = rop.chain()

# Imprime a sequência
print(rop.dump())

SROP

É possível criar frame para exploits de SROP.

# Endereço de uma syscall
syscall = 0xdeadbeef

# Endereço de uma string "/bin/sh"
bin_sh = 0xcafebabe

# Instancia uma frame de um objeto sigreturn 
frame = SigreturnFrame()

# Setando os valores dos registradores, com rip sendo o endereço de retorno
frame.rax = constants.SYS_execve
frame.rdi = bin_sh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

# Convertendo o frame pra bytes
payload = bytes(frame)

Format String Exploits

É possível utilizar funções prontas para realizar exploits de format string.

# Função recebe o offset, que é qual argumento da printf o input do usuário começa
# e recebe um dicionário de chave valor, sendo a chave o endereço que deve ser escrito o valor

offset = 5  # O input do usuário começa no argumento 5

write = { 0x40010 : 0xdeadbeef} # Escreve o valor 0xdeadbeef no endereço 0x40010

payload = fmtstr_payload(offset, writes)

Carregar libc

Para garantir que a libc carregada no script seja a libc disponibilizada pelo desafio, você pode usar o seguinte script

from pwn import *

exe = ELF("./hunter_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.23.so")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r)
    else:
        r = remote("addr", 1337)

    return r


def main():
    r = conn()

    # good luck pwning :)

    r.interactive()


if __name__ == "__main__":
    main()

Referências

pwntools cheatsheet: https://gist.github.com/anvbis/64907e4f90974c4bdd930baeb705dedf#program-interaction

Documentação pwntools: https://docs.pwntools.com/en/stable/intro.html#making-connections

Pwninit: https://github.com/io12/pwninit

Atualizado