[ Autor: Nicholas Ferreira ]
[0x7] Bruteforcer de ROT Cypher em assembly x64
06/03/2022Neste texto eu mostrarei uma implementação que fiz em assembly x64 de um bruteforcer de ROT Cypher - também conhecida como Cifra de Cesar. O código não foi feito com o intuito de ser o menor ou mais otimizado possível. Esta é apenas a primeira forma que encontrei de construir o programa (e eu sei que tem outras maneiras mais eficientes de fazê-lo), e a escrita do texto é uma "documentação" do código e faz parte do meu aprendizado em assembly. Portanto, não utilize o código para fins acadêmicos a menos que você tenha certeza do que está fazendo. Dito isso, podemos começar.
ROT
O ROT - do inglês rotate - é um dos algoritmos de criptografia mais antigos e conhecidos - e simples - do mundo. O algoritmo foi originalmente usado para a troca de mensagens secretas entre exércitos durante o império de Julio Cesar - de onde vem o nome da cifra.Trata-se de uma cifra de substituição bastante simples, baseada na rotação do alfabeto em um determinado índice n, sendo n ∈ ℕ, n <= 26. Consideremos o seguintes alfabetos:
Original: ABCDEFGHIJKLMNOPQRSTUVWXYZ
ROT 0: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Aqui, o íncide da cifra é 0. Neste caso, não há qualquer criptografia, pois o alfabeto da cifra é idêntico ao original. No entanto, consideremos os seguintes alfabetos, com o índice sendo 1:
Original: ABCDEFGHIJKLMNOPQRSTUVWXYZ
ROT 1: BCDEFGHIJKLMNOPQRSTUVWXYZA
Note que agora o alfabeto da cifra foi movido para a esquerda em uma posição, de modo que a letra A do alfabeto original agora é mapeada à letra B do alfabeto cifrado, a letra B é mapeada à letra C, e assim sucessivamente até a letra Z, que é mapeada à letra A, fechando o ciclo.
A criptografia agora pode ser feita remapeando todas as letras do texto a ser criptografado às letras correspondentes no alfabeto cifrado. Portanto, se quisermos criptografar a palavra "cebola", basta procurarmos pela letra "c" no alfabeto original e anotar a letra em que ela é mapeada no alfabeto cifrado, e depois fazer o mesmo com as letras "e", "b", "o", "l" e "a". Neste caso, ficaremos com "dfcpmb".
Para fazer a descriptografia, basta fazer a mesma coisa, porém, fazendo a rotação para a direita. Ou, como o alfabeto é cíclico, basta fazer a mesma coisa com o índice 26-n, onde n é o índice original.
Rot13
O caso de uso mais famoso é o ROT13, já que a rotação de 13 posições leva para o meio do alfabeto. Neste caso, para descriptografar, podemos aplicar exatamente o mesmo algoritmo com o mesmo índice (13), pois 13+13=26 (que é o número de letras no alfabeto). Temos, então, que a função ROT13 é a inversa dela própria, ou seja, ROT13(ROT13(x))=x. Por exemplo:ROT13("boa tarde") = "obn gneqr"
ROT13("obn gneqr") = "boa tarde"
É fácil notar que não há qualquer aplicação do ROT na criptografia atualmente, já que há pouquíssimas combinações (no máximo 25) é extremamente fácil testar todas as combinações até encontrarmos aquela que descriptografa a mensagem original. E é precisamente esse teste, escrito em assembly, que será apresentado aqui.
Como o programa funcionará
O intuito é escrever um programa em linha de comando que possa receber inputs de duas formas: através do stdin ou através de argumentos. No primeiro caso, o programa receberá diretamente a string que será testada (por exemplo, via pipe, como em echo -n obn gneqr | ./rotbrute). No segundo caso, o programa receberá o nome de um arquivo que será aberto e cujo conteúdo será testado.Precisamos, então, criar a seguinte rotina (exibida em pseudo-código), onde rotbrute é a função principal, que escreveremos depois:
if(argv.count > 1)
if(file = open(argv[1]))
var content = read(file);
else
exit "Erro ao abrir arquivo";
else
if(stdin.length > 0)
var content = stdin;
else
print "Usage: echo -n 'string' | ./rotbrute"
rotbrute(content)
Lendo o arquivo
A primeira coisa que precisamos fazer é analisar se algum argumento foi passado e, se este for o caso, tentar ler o conteúdo do arquivo passado.Quando um programa é iniciado, a primeira coisa que fica no topo da pilha é um valor numérico contendo a quantidade de argumentos passados para o programa. Esse valor sempre será pelo menos 1, já que o primeiro item é sempre o nome do próprio programa. Precisamos verificar, então, se esse valor é maior ou igual a 2 - dessa forma, saberemos que algum argumento foi passado (o nome do arquivo a ser aberto).
Como esse valor fica no topo da stack, podemos usar a instrução pop para retirá-lo para algum registrador e usar a instrução cmp para saber se o valor é maior ou igual a 2. Caso seja maior ou igual a 2, pularemos para a rotina que tentará abrir o arquivo. Caso contrário, chamaremos a rotina que verifica se há algo no stdin:
global _start
section .text
_start:
pop rax ;guarda o número de argumentos
cmp rax, 2 ;compara esse número com 2
jge _hasArgument ;se for >=2, tenta abrir o arquivo
call _checkStdin ;se não, verifica o stdin
jmp _begin ;e então pula pro _begin
Note que caso o programa não tenha recebido um argumento, ele checará pelo stdin e então pulará para a rotina _begin, que ainda não foi definida. Essa rotina, como o próprio nome sugere, é que iniciará o processo de bruteforce, e quando entrarmos nela, já teremos carregado a string que será testada. O ponteiro para essa string estará no rdx, e o número de bytes da string estará no rax, independentemente de ela ter sido carregada via leitura de arquivo ou via stdin. Isso será importante para mais tarde.
Como precisaremos usar várias funções do sistema, defini as constantes de cada syscall no começo do código. Você pode ver o número das syscalls no arquivo /usr/include/x86_64-linux-gnu/asm/unistd_64.h.
%define READ 0
%define WRITE 1
%define OPEN 2
%define FSTAT 5
%define MMAP 9
%define EXIT 60
%define MREMAP 25
Agora devemos fazer o código que tenta abrir o arquivo passado via argumento. Após o primeiro item da stack, que é a quantidade de argumentos passados, estão os argumentos propriamente ditos. Antes disso, porém, vamos fazer uma rotina e um macro para a abertura do arquivo. Na syscall open(), são passados três argumentos: o path do arquivo a ser aberto, as flags de abertura e as permissões.
%macro open 3 ;open filepath, flags, perm
push %1 ;filepath
push %2 ;flags(ro)
push %3 ;perm
call _open ;chama a rotina de abertura
add rsp, 24 ;limpa a pilha
%endmacro
_open:
push rbp ;cria novo stack frame
mov rbp, rsp
mov rax, OPEN
mov rdx, [rbp+16] ;perm
mov rsi, [rbp+24] ;flags
mov rdi, [rbp+32] ;filepath
syscall
leave
ret ;salva o resultado no rdi
Agora podemos abrir o arquivo. Para pegarmos o primeiro argumento, precisamos chegar em rsp+8.
_hasArgument:
mov rax, [rsp+8] ;pega o argv[1] da stack
push rax ;salva no topo da stack
open rax, 0, 0 ;abre o arquivo c/ permissão de leitura e escrita
cmp rax, 0 ;compara o file descriptor aberto com 0
jl _notfound ;se for menor que zero, temos um erro
push rax ;se não for menor que zero, salva na stack
A rotina _notfound simplesmente printa uma mensagem de erro e fecha o programa.
_notfound:
write 1, error, 15
exit
Vamos aproveitar e definir as strings de instruções de uso e o newline. Essas definições de string serão feitas na seção .data do executável.
section .data
usage: db 'Usage: ./rotbrute [filename]', 0
error: db 'File not found',0
nl: db 0xA,0
Após abrir o arquivo, precisamos alocar um espaço na memória que será preenchido com seu conteúdo. Para isso, usaremos a syscall mmap, que nesse caso receberá o número de bytes a serem alocados. Para descobrirmos o número de bytes a serem alocados (que é o tamanho do arquivo aberto), usaremos a syscall fstat, que retorna algumas informações sobre o arquivo, dentre elas o seu tamanho. Eis os macros e as rotinas do mmap e do fstat:
%macro mmap 2 ;argumento: tamanho do map
push %1 ;endereço (deixe o kernel decidir)
push %2 ;tamanho
push 0x2 ;prot (PROT_WRITE)
push 33 ;flags (MAP_SHARED|MAP_ANONYMOUS)
push -1 ;fd (ignore)
push 0 ;offset (0, por conta do MAP_ANONYMOUS)
call _mmap
add rsp, 48 ;limpa a pilha
%endmacro
%macro filesize 1 ;filesize fd
mov rdi, %1 ;salva o file descriptor no rdi
call _filesize
%endmacro
_filesize:
push rbp ;novo stack frame
mov rbp, rsp
sub rsp, 192 ;reservado para o retorno de stat()
mov rax, FSTAT ;constante c/ a syscall
mov rsi, rsp ;statbuf
syscall
mov rax, [rsp+48] ;o tamanho do arquivo estará aqui na stack
leave
ret
_mmap:
push rbp ;novo stack frame
mov rbp, rsp
mov rax, MMAP ;constante c/ a syscall
mov r9, [rbp+16] ;offset
mov r8, [rbp+24] ;fd
mov r10, [rbp+32] ;flags
mov rdx, [rbp+40] ;prot
mov rsi, [rbp+48] ;length
mov rdi, [rbp+54] ;addr
syscall
leave
ret
Agora, podemos usar o macro filesize e passar o file descriptor do arquivo cujo tamanho queremos saber. Esse file descriptor já está salvo no rax, que contém o retorno da chamada do open().
filesize rax ;retorna o tamanho do arquivo
cmp rax, 0 ;compara o tamanho com 0
jz _exit ;quita se o tamanho for 0
A rotina _exit é bastante simples; é apenas uma chamada para a própria syscall exit():
_exit:
mov rax, 60 ;código da syscall exit()
mov rdi, 0 ;código de saída
syscall
Agora já temos o tamanho do arquivo salvo em rax. Podemos passar esse tamanho para o nosso macro do mmap para alocarmos o tamanho necessário em um buffer na memória. Após isso, poderemos ler o conteúdo do arquivo e salvar nesse novo buffer.
push rax ;salva o tamanho do arquivo (k) na stack
mmap 0, rax ;mapeia k bytes na memória
O mmap nos retorna o endereço onde o novo buffer foi criado. Tendo em mãos esse endereço (que está no rax), o file descriptor do arquivo a ser lido (que está na stack) e o tamanho do buffer (que é o tamanho do arquivo, que também está na stack), podemos chamar a função read() para ler o conteúdo do arquivo para o novo buffer:
mov rcx, [rsp] ;tamanho do arquivo
mov rbx, [rsp+8] ;file descriptor
mov rdx, rax ;pointeiro pro buffer criado
push rdx ;salva o ponteiro na stack
push rdx
read rbx, rax, rcx ;lê o arquivo pro buffer
mov r9, rax ;salva o número de bytes lidos no r9
pop rdx ;retorna o ponteiro pro buffer
Ao fim desse trecho, teremos no rax o número de bytes que foram lidos (ou seja, o tamanho do arquivo) e no rdx o ponteiro para o buffer onde se encontra o conteúdo do arquivo.
Lendo do stdin
Fizemos o código que lê o conteúdo de um arquivo passado via argumento (ex: ./rotbrute arquivo.txt). Agora, precisamos fazer o código que lê o stdin passado pelo usuário (ex: echo -n string | ./rotbrute). No começo da rotina principal, a _start, já declaramos uma chamada para a função _checkStdin, e é ela que escreveremos agora.A função terá mais ou menos o seguinte fluxo (em pseudo-código):
buf = mmap(x); //mapeia x bytes na memória
r = read(STDIN, buf, x); //lê x bytes do STDIN para esse buffer
if(r>0) //número de bytes lidos
while(r != 0){ //enquanto houver conteúdo p/ ser lido
buf = mremap(buf, x); //aumenta o tamanho do buffer em x bytes
r = read(STDIN, buf, x);//lê mais x bytes pro buffer
}
else
print "Usage: echo -n 'string' | ./rotbrute";
Ou seja, vamos criar um buffer na memória com um tamanho x e ler x bytes do STDIN para esse buffer. Se o tamanho do conteúdo lido for maior que zero, a função remapeia o buffer para aumentar seu tamanho e tenta ler mais conteúdo do STDIN, até que ele se esvazie. Do contrário (ou seja, se não tiver STDIN), a função printa as instruções de uso.
_checkStdin: ;verifica se len(stdin)>0
push rbp ;novo stack frame
mov rbp, rsp
mmap 0, BUF ;aloca o buffer p/ receber STDIN
mov r14, rax ;salva o endereço do buffer criado
mov r15, rax ;salvamos novamente, pois ele será incrementado
push rax ;salva o endereço de memória mapeado
read 0, rax, BUF ;tenta ler do STDIN
cmp rax, 0 ;se ler mais que 0 bytes
jnz _hasStdin ;então tem STDIN
jmp _usage ;se não, printa usage
Aqui, chamamos o macro mmap e passamos BUF como parâmetro. Este seria o tamanho do buffer que será alocado. Precisamos, então, definir essa constante BUF no começo do código; neste caso, ela terá 256 bytes (ou seja, o STDIN será lido em blocos de 256 bytes até acabar).
%define BUF 256
Após chamar o mmap, o rax conterá o endereço de memória do novo buffer criado. Nós, então, salvamos esse endereço em dois registradores, r14 e r15. O segundo será alterado ao longo da execução da função - ele será somado com os 256 bytes para criar o novo endereço para onde os novos 256 bytes do STDIN devem ser lidos. O primeiro será guardado e movido para o rdx ao fim da função (lembre-se que ao fim desse trecho, precisaremos que o rdx seja o ponteiro para o buffer onde se encontra o conteúdo a ser testado). Esse endereço de memória mapeado também é salvo na stack para ser consultado depois.
Feito isso, chamamos o macro read para ler 256 bytes do STDIN para o rax, ou seja, para esse novo buffer que criamos com mmap.
Depois de chamada a função read, o número de bytes lidos é salvo em rax. Nós, então, comparamos rax com 0 para verificar se algo foi lido. Se for maior que 0, então nós temos algo no STDIN e podemos pular para a rotina _hasStdin. Se não, então pulamos para a rotina que printa as instruções de uso (_usage).
A rotina _hasStdin iniciará o código que terá a função de ler o STDIN a cada 256 bytes para o buffer criado e aumentar o tamanho do buffer conforme necessário. Para fazer isso, usaremos a syscall mremap, que recebe os seguintes parâmetros: o endereço da memória a ser redimensionada, o tamanho antigo, o novo tamanho, flags e o novo endereço. No caso das flags, não forneceremos nada. No caso do novo endereço, também deixaremos como 0, para que o endereço seja o mesmo. Eis, então, o macro e a rotina dessa função:
%macro mremap 3
push %1 ;endereço antigo
push %2 ;tamanho antigo
push %3 ;novo tamanho
push 0 ;flags
push 0 ;novo endereço
call _mremap
add rsp, 40 ;limpa a stack
%endmacro
_mremap:
push rbp ;novo stack frame
mov rbp, rsp
mov rax, MREMAP ;código da syscall
mov r8, [rbp+16] ;endereço antigo
mov r10, [rbp+24] ;tamanho antigo
mov rdx, [rbp+32] ;novo tamanho
mov rsi, [rbp+40] ;flags
mov rdi, [rbp+48] ;novo endereço
syscall
leave
ret
Uma vez definida o macro e a rotina da syscall mremap, podemos usá-la em nosso código. De início, salvaremos na stack o tamanho inicial do buffer (256 bytes). Esse tamanho será alterado a cada iteração do loop, então esse primeiro push funcionará quase como um "vetor de inicialização".
_hasStdin:
push BUF ;salva o tamanho inicial do buffer
_loop: ;lê o STDIN a cada 256 bytes
;e aumenta o tamanho da memória alocada conforme necessário
pop rcx ;tamanho antigo
pop rax ;endereço da memória mapeada
mov rbx, rcx ;salva o tamanho antigo
add rcx, BUF ;novo tamanho = tamanho antigo + BUF (256)
push rcx ;salva o novo tamanho (será recuperado depois)
mremap rax, rbx, rcx ;remapeia (aumenta o tamanho da memória alocada)
Logo após a rotina _hasStdin, que salva na stack o tamanho inicial do buffer, iniciamos a rotina _loop, que, como o nome indica, fará o loop de leitura do STDIN até que ele se acabe. De início, recuperamos para rcx o tamanho atual do buffer que está na stack (que, na primeira iteração, será 256).
Após isso, recuperamos para rax o endereço da memória mapeada pelo mmap (que havia sido salvo na stack após a chamada dessa syscall). Salvamos, então, em rbx o tamanho atual do buffer e adicionamos BUF (256) a rcx, que agora tem o novo tamanho que o buffer terá ao ser realocado. Salvamos esse novo tamanho em rcx e chamamos nosso macro mremap rax, rbx, rcx, onde o primeiro argumento é o endereço antigo do buffer, o segundo é o tamanho antigo do buffer e o terceiro é o tamanho novo que o buffer terá.
Agora podemos dar continuidade e ler o STDIN para esse novo buffer:
pop rcx ;recuperamos o tamanho atual do buffer
push rax ;endereço da memória mapeada
push rcx ;endereço atual do buffer
add r15, BUF ;aumenta o lugar para onde o STDIN será lido
read 0, r15, BUF ;lê 256 bytes do STDIN p/ a nova localizaçao do buffer
Note que as primeiras instruções do loop são pop rcx e pop rax. Ou seja, quando o loop resetar, ele tirará dois itens da stack para esses registradores. Como a syscall mremap retornará em rax o endereço da memória mapeada, podemos salvá-la na stack junto com o tamanho atual do buffer (que está em rcx), para que eles sejam recuperados no início da próxima iteração do loop. Por isso nós colocamos o rax e rcx na pilha, nessa ordem. Após isso, podemos ler mais 256 bytes do STDIN para r15+256, ou seja, para o início da parte vazia do buffer (a parte que acabou de ser remapeada).
Agora, só precisamos verificar se o número de bytes lidos foi 0. Se não tiver sido, então voltamos para o começo do loop, para aumentarmos o tamanho do buffer e tentarmos ler mais 256 bytes. Se foi 0, então não há mais dados a serem lidos e podemos terminar essa função. Antes de terminar, porém, nós salvamos o tamanho do buffer (o total de bytes lidos) em rax. Lembre-se que após carregar a string a ser testada, precisamos que seu tamanho esteja no rax e o ponteiro para ela esteja em rdx, como falei anteriormente, e como fizemos no _hasArgument.
cmp rax, 0 ;compara o número de bytes lidos com 0
jnz _loop ;se for diferente de 0, volta pro início do loop
mov rax, rbx ;se não, move o tamanho final do buffer pra rax
mov rdx, r14 ;move o endereço do começo do buffer pra rdx
leave
ret
Preparando para o bruteforce
Como definimos no _start, o programa primeiro lerá a string a ser testada - seja através da leitura de um arquivo, seja pela leitura do STDIN - e então pulará par a rotina _begin, que dará início ao brute force. De início, printaremos a própria string que foi carregada e, então, começaremos a função de brute force. Para printar isso, definiremos um macro para a syscall write, que recebe três parâmetros: o file descriptor para onde o conteúdo será escrito, um ponteiro para o buffer contendo o conteúdo que será escrito e o tamanho do conteúdo que será escrito. Eis o macro e a rotina:%macro write 3
push %1 ;file descriptor
push %2 ;buffer
push %3 ;tamanho do buffer
call _write
add rsp, 24 ;limpa a stack
%endmacro
_write:
push rbp
mov rbp, rsp
mov rax, WRITE
mov rdx, [rbp+16] ;tamanho do buffer
mov rsi, [rbp+24] ;buffer
mov rdi, [rbp+32] ;file descriptor
syscall
leave
ret
Como nós temos o tamanho da string lida em rax e o ponteiro para ela em rdx, basta chamarmos o macro write passando esses registradores como parâmetros e escrevermos o conteúdo no file descriptor 1, que é o STDOUT, ou seja, a saída do terminal. Além disso, salvaremos na stack esses valores (o ponteiro e o tamanho da string) para que possam ser usados dentro da função de bruteforce.
_begin:
push rax
push rdx
write 1, rdx, rax ;printa a string passada
write 1, nl, 1 ;printa newline
jmp _bruteforce
Bruteforce
Agora sim, finalmente, podemos começar a escrever a função que fará o bruteforce da string passada e retornará todos os 25 possíveis resultados da aplicação do algoritmo ROT a ela.A função terá mais ou menos o seguinte fluxo, tanto para letras maiúsculas quanto para minúsculas (em pseudo-código):
str = 'string lida do arquivo ou do stdin';
charset = 'abcdefghijklmnopqrstuvxwyz'; //no código, usaremos os correspondentes ascii
i=0; //começa com índice 0
while(i<27) //loop para todas as 27 combinações
for(j=0;j<=strlen(str);j++) //para cada caractere da string
if(str[j] == 'z') //se ele for o último (z)
str[j] = 'a'; //troca pelo primeiro (a)
else //se não
str[i] = charset[j+1]; //adiciona 1
i++; //próximo índice
Ou seja, para cada índice de 0 a 27, o código percorrerá todos os caracteres da string e somará 1 a ele, com base no alfabeto ascii. Ou seja, quando o código encontrar a letra h, ele somará 1 e retornará i; quando encontrar a letra x, ele somará 1 e retornará w, e assim por diante. O pseudo-código acima não é 100% preciso porque, como veremos, há algumas outras exceções:
- Caso o caractere seja z, ele retornará a,
- Caso o caractere seja Z, ele retornará A,
- Caso o caractere seja um espaço ou número, printe-o normalmente,
Dito isso, podemos começar:
_bruteforce:
push rbp ;novo stack frame
mov rbp, rsp
mov rax, [rbp+8] ;endereço com a string a ser testada
mov rbx, [rbp+16] ;tamanho da string
push rbx ;salva o tamanho para mais tarde
add rbx, rax ;endereço da string+tamanho da string = endereço do final
push rax ;será popped no final
mov rcx, 1 ;índice do ROT
Após criarmos o novo stack frame, recuperamos o endereço com a string (que está na stack) para o rax e o tamanho dela (também na stack) para o rbx. Nós, então, adicionamos um ao outro e salvamos o resultado no rbx. Esse resultado é o ponteiro para o final da string, e será utilizado mais tarde para verificarmos se já chegamos ao final da string.
Agora, precisamos fazer as verificações das exceções que falei. Em resumo, caso o caractere atual seja z ou Z, o retorno precisa ser a ou A, respectivamente. Caso seja um espaço ou um número, o retorno será o próprio espaço ou o próprio número.
_checkz:
cmp byte [rax],0x7A ;verifica se o caractere atual é 'z' (0x7A)
jnz _checkZ ;se não for, verifica se é 'Z' (maiúsculo)
mov byte [rax],0x60 ;se for 'z', seta ele pro char antes de "a" (0x60 = `)
Note que se o caractere for 'z', ele será trocado pelo caractere que vem antes do 'a' na tabela ascii. Isso ocorre porque posteriormente iremos adicionar 1 a esse caractere, de modo que, neste caso, o 'z' será convertido no 'a', e o mesmo ocorre para o 'Z' maiúsculo. As verificações aqui são feitas usando o código ascii correspondente do caractere. Para descobri-lo, basta consultar a tabela ascii e obter o valor em hexadecimal do caractere.
O mesmo é feito para o "Z" maiúsculo:
_checkZ:
cmp byte [rax],0x5A ;verifica se o caractere atual é 'Z' (0x5A)
jnz _checkSpace ;se não for, verifica se é um espaço
mov byte [rax],0x40 ;se for 'Z', seta ele pro char antes de 'A' (0x40 = @)
Verificando se o caractere é um espaço:
_checkSpace:
cmp byte [rax],0x20 ;verifica se o caractere atual é um espaço (0x20)
jnz _checkNL ;se não for, verifica se é um newline
jmp _next ;se for um espaço, pula pro próximo
Precisamos, agora, fazer essas duas rotinas: a que verifica se o caractere é um newline e a que pulará para o próximo caractere. Faremos esta última depois de fazermos as rotinas de verificação de números.
_checkNL:
cmp byte [rax], 0xA ;verifica se o caractere atual é um newline (0xA)
jnz _checkNum0 ;se não for, verifica se é um número maior que 0
jmp _next ;se for um newline, pula pro próximo
Agora, as rotinas que verificam se o caractere é um número. Na tabela ascii, os números estão entre 0x30 e 0x39. Então, para validarmos se o caractere atual é um número, basta verificarmos se ele está nesse intervalo([0x30 - 0x39]). Para isso, usaremos as instruções jge (jump if greater than or equal to) e jle (jump if lesser than or equal to):
_checkNum0:
cmp byte [rax], 0x30 ;verifica se o caractere atual é 0x30 (0)
jge _checkNum9 ;se for maior ou igual a 0, verifica se é <= 9
_checkNum9:
cmp byte [rax], 0x39 ;verifica se o caractere atual é 0x39 (9)
jle _next ;se for menor ou igual a 9, pula pro próximo
add byte [rax], 1 ;se não, adiciona 1 ao caractere (ex: a+1 = b)
Agora podemos pular para o próximo caractere e verificar se já chegamos no final da string. Lembre-se que no começo da função, salvamos o endereço do fim da string em rbx. Como a cada iteração nós adicionamos 1 ao endereço atual (rax), para que possamos percorrer todos os caracteres da string, basta que comparemos rax com rbx para sabermos se chegamos ao final da string:
_next:
inc rax ;incrementa a posição atual em 1
cmp rax, rbx ;a posição atual é igual à final?
jle _checkz ;se não for, volta pro começo do loop
Após terminado esse loop, podemos printar a string (de)cifrada (que neste momento está no topo da stack) e verificar se nosso índice é menor que ou igual a 25 (e não 26, porque a string original já foi printada). Se for, voltaremos para o _checkz, repetindo o loop. Se for maior que 25, então já percorremos por todos os índices possíveis e podemos sair da função e do programa.
mov rdx, [rsp] ;move a string (de)cifrada para rdx
mov r9, [rsp+8] ;filesize
push rax ;saving...
push rcx ;saving...
push rdx ;saving...
write 1, rdx, r9 ;printa a string (de)cifrada
write 1, nl, 2 ;printa um newline
pop rdx ;recuperando...
pop rcx ;recuperando...
pop rax ;recuperando...
Note que salvamos o rax, rcx e rdx na stack antes de chamarmos as syscalls write, e os recuperamos depois. Fazemos isso porque esses registradores serão alterados após o retorno da syscall, e precisaremos deles depois. Eles contém, respectivamente, a posição atual na string, o índice atual e o endereço da string.
Depois de recuperar os valores para os registradores, podemos verificar se o índice é menor ou igual a 25 e, caso seja, incrementar o índice (para irmos para o próximo índice do agoritmo ROT) e rodar o loop novamente, agora com a nova string. Do contrário, pulamos para a rotina de saída do programa.
inc rcx ;incrementa o índice
mov rax, rdx ;rax agora contém a nova string (de)cifrada
cmp rcx, 25 ;compara o índice atual com 25
jle _checkz ;se índice <=25, repete o loop (com o novo índice incrementado)
jmp _exit ;se não, já testamos tudo; feche o programa
leave
ret
Finalizando
Terminamos, assim, o código do brute-forcer de ROT cypher. Para rodarmos, precisamos compilar o código e linkar o executável. Fazemos isso da seguinte maneira:nich0las@0x7359:~$ nasm -felf64 rotbrute.nasm -g -F dwarf
nich0las@0x7359:~$ ld rotbrute.o -o rotbrute
nich0las@0x7359:~$ ld rotbrute.o -o rotbrute
Após gerado o executável, podemos rodá-lo das duas formas que programamos. Note que passei o -n no echo para ele não adicionar um newline ao fim da string. Se não passarmos isso, o newline será duplicado a cara iteração do loop, o que não é interessante. (Adicionei o grifo em negrito apenas para facilitar a visualização do resultado correto do bruteforce):
1. Lendo o conteúdo de um arquivo:
nich0las@0x7359:~$ echo -n "bmfbw kzqxbwozinilw" > file.txt
nich0las@0x7359:~$ ./rotbrute file.txt
bmfbw kzqxbwozinilw
cngcx larycxpajojmx
dohdy mbszdyqbkpkny
epiez nctaezrclqloz
fqjfa odubfasdmrmpa
grkgb pevcgbtensnqb
hslhc qfwdhcufotorc
itmid rgxeidvgpupsd
junje shyfjewhqvqte
kvokf tizgkfxirwruf
lwplg ujahlgyjsxsvg
mxqmh vkbimhzktytwh
nyrni wlcjnialuzuxi
ozsoj xmdkojbmvavyj
patpk ynelpkcnwbwzk
qbuql zofmqldoxcxal
rcvrm apgnrmepydybm
sdwsn bqhosnfqzezcn
texto criptografado
ufyup dsjquphsbgbep
vgzvq etkrvqitchcfq
whawr fulswrjudidgr
xibxs gvmtxskvejehs
yjcyt hwnuytlwfkfit
zkdzu ixovzumxglgju
aleav jypwavnyhmhkv
nich0las@0x7359:~$ ./rotbrute file.txt
bmfbw kzqxbwozinilw
cngcx larycxpajojmx
dohdy mbszdyqbkpkny
epiez nctaezrclqloz
fqjfa odubfasdmrmpa
grkgb pevcgbtensnqb
hslhc qfwdhcufotorc
itmid rgxeidvgpupsd
junje shyfjewhqvqte
kvokf tizgkfxirwruf
lwplg ujahlgyjsxsvg
mxqmh vkbimhzktytwh
nyrni wlcjnialuzuxi
ozsoj xmdkojbmvavyj
patpk ynelpkcnwbwzk
qbuql zofmqldoxcxal
rcvrm apgnrmepydybm
sdwsn bqhosnfqzezcn
texto criptografado
ufyup dsjquphsbgbep
vgzvq etkrvqitchcfq
whawr fulswrjudidgr
xibxs gvmtxskvejehs
yjcyt hwnuytlwfkfit
zkdzu ixovzumxglgju
aleav jypwavnyhmhkv
2. Lendo o conteúdo do STDIN:
nich0las@0x7359:~$ echo -n "bmfbw kzqxbwozinilw" | ./rotbrute
bmfbw kzqxbwozinilw
cngcx larycxpajojmx
dohdy mbszdyqbkpkny
epiez nctaezrclqloz
fqjfa odubfasdmrmpa
grkgb pevcgbtensnqb
hslhc qfwdhcufotorc
itmid rgxeidvgpupsd
junje shyfjewhqvqte
kvokf tizgkfxirwruf
lwplg ujahlgyjsxsvg
mxqmh vkbimhzktytwh
nyrni wlcjnialuzuxi
ozsoj xmdkojbmvavyj
patpk ynelpkcnwbwzk
qbuql zofmqldoxcxal
rcvrm apgnrmepydybm
sdwsn bqhosnfqzezcn
texto criptografado
ufyup dsjquphsbgbep
vgzvq etkrvqitchcfq
whawr fulswrjudidgr
xibxs gvmtxskvejehs
yjcyt hwnuytlwfkfit
zkdzu ixovzumxglgju
aleav jypwavnyhmhkv
bmfbw kzqxbwozinilw
cngcx larycxpajojmx
dohdy mbszdyqbkpkny
epiez nctaezrclqloz
fqjfa odubfasdmrmpa
grkgb pevcgbtensnqb
hslhc qfwdhcufotorc
itmid rgxeidvgpupsd
junje shyfjewhqvqte
kvokf tizgkfxirwruf
lwplg ujahlgyjsxsvg
mxqmh vkbimhzktytwh
nyrni wlcjnialuzuxi
ozsoj xmdkojbmvavyj
patpk ynelpkcnwbwzk
qbuql zofmqldoxcxal
rcvrm apgnrmepydybm
sdwsn bqhosnfqzezcn
texto criptografado
ufyup dsjquphsbgbep
vgzvq etkrvqitchcfq
whawr fulswrjudidgr
xibxs gvmtxskvejehs
yjcyt hwnuytlwfkfit
zkdzu ixovzumxglgju
aleav jypwavnyhmhkv
Veja o código completo no github.