.d8888b. 8888888888 .d8888b. 888888888 .d8888b. d88P Y88b d88P d88P Y88b 888 d88P Y88b 888 888 d88P .d88P 888 888 888 888 888 888 888 d88P 8888" 8888888b. Y88b. d888 888 888 `Y8bd8P' 88888888 "Y8b. "Y88b "Y888P888 888 888 X88K d88P 888 888 888 888 Y88b d88P .d8""8b. d88P Y88b d88P Y88b d88P Y88b d88P "Y8888P" 8888 8888 d88P "Y8888P" "Y8888P" "Y8888P"

[ Autor: Nicholas Ferreira ]


[0x7] Bruteforcer de ROT Cypher em assembly x64

06/03/2022

assembly ROT13 bruteforcer

Neste 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


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

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


Veja o código completo no github.