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