Modern Binary Exploitation: Laboratório de Engenharia Reversa

Olá! Nesse post, resolveremos o primeiro laboratório do Modern Binary Exploitation da RPISEC que aborda a Engenharia Reversa. Caso tenha interesse no assunto, temos diversos posts no blog que abordam o tema, basta acessar a tag!

Laboratório 01

Todos os laboratórios do curso residem dentro de uma máquina virtual disponibilizada no material através de uma imagem de disco para Ubuntu 14.04, que possui toda a configuração necessária para o Wargame. Os desafios são separados por laboratório e dificuldade, sendo C o mais fácil e A o mais difícil. Além disso, você acessa o challenge mediante ao usuário do respectivo desafio. Portanto, começando no C, o seu objetivo é exploitar o desafio para spawnar o terminal logado no usuário da próxima challenge e pegar a senha dele (que está em /home/labXX/.pass).

O laboratório 01 aborda a Engenharia Reversa, tópico, esse, que é trabalhado durante as três primeiras lectures do material. Para uma melhor compreensão do que está ocorrendo, é necessário saber um pouco sobre:

  • Programação em C
  • Assembly x86
  • Stack

Todas as ferramentas utilizadas estarão listadas ao final do write-up.

Lab1C

Conferindo algumas informações do arquivo:

lab1C@warzone:~$ file /levels/lab01/lab1C
/levels/lab01/lab1C: setuid ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=1522352e3de50fd6d180831ba18e2bca16be4204, not stripped

Nosso código está rodando em 32 bits. Isso é importante para entendermos como são passados os parâmetros das funções, dadas as características da arquitetura. Nesse caso, os parâmetros são passados através da Stack.

Acessando com a senha padrão lab01start, o primeiro problema nos prompta com uma interface para inserir uma senha:

-----------------------------
--- RPISEC - CrackMe v1.0 ---
-----------------------------

Password: 

Realizando o disassembly da função main():

Dump of assembler code for function main:
   0x080486ad <+0>:	push   ebp
   0x080486ae <+1>:	mov    ebp,esp
   0x080486b0 <+3>:	and    esp,0xfffffff0
   0x080486b3 <+6>:	sub    esp,0x20
   0x080486b6 <+9>:	mov    DWORD PTR [esp],0x80487d0
   0x080486bd <+16>:	call   0x8048560 <puts@plt>
   0x080486c2 <+21>:	mov    DWORD PTR [esp],0x80487ee
   0x080486c9 <+28>:	call   0x8048560 <puts@plt>
   0x080486ce <+33>:	mov    DWORD PTR [esp],0x80487d0
   0x080486d5 <+40>:	call   0x8048560 <puts@plt>
   0x080486da <+45>:	mov    DWORD PTR [esp],0x804880c
   0x080486e1 <+52>:	call   0x8048550 <printf@plt>
   0x080486e6 <+57>:	lea    eax,[esp+0x1c]
   0x080486ea <+61>:	mov    DWORD PTR [esp+0x4],eax
   0x080486ee <+65>:	mov    DWORD PTR [esp],0x8048818
   0x080486f5 <+72>:	call   0x80485a0 <__isoc99_scanf@plt>
   0x080486fa <+77>:	mov    eax,DWORD PTR [esp+0x1c]
   0x080486fe <+81>:	cmp    eax,0x149a
   0x08048703 <+86>:	jne    0x8048724 <main+119>
   0x08048705 <+88>:	mov    DWORD PTR [esp],0x804881b
   0x0804870c <+95>:	call   0x8048560 <puts@plt>
   0x08048711 <+100>:	mov    DWORD PTR [esp],0x804882b
   0x08048718 <+107>:	call   0x8048570 <system@plt>
   0x0804871d <+112>:	mov    eax,0x0
   0x08048722 <+117>:	jmp    0x8048735 <main+136>
   0x08048724 <+119>:	mov    DWORD PTR [esp],0x8048833
   0x0804872b <+126>:	call   0x8048560 <puts@plt>
   0x08048730 <+131>:	mov    eax,0x1
   0x08048735 <+136>:	leave  
   0x08048736 <+137>:	ret    
End of assembler dump.

Nota-se, em <main+81>:

    0x080486ea <+61>:    mov    DWORD PTR [esp+0x4],eax
    0x080486ee <+65>:    mov    DWORD PTR [esp],0x8048818
    0x080486f5 <+72>:    call   0x80485a0 <__isoc99_scanf@plt>
    0x080486fa <+77>:    mov    eax,DWORD PTR [esp+0x1c]
    0x080486fe <+81>:    cmp    eax,0x149a
    0x08048703 <+86>:    jne    0x8048724 <main+119>

Há uma chamada de scanf() e uma comparação entre o valor inputado e 0x149a (5274 em decimal). Portanto, ao inserir 5274 como senha, devemos satisfazer a comparação e evitar o jump subsequente.Assim, então, acessamos o usuário lab1B e recuperamos a senha n0_str1ngs_n0_pr0bl3m.

#!/bin/bash

file="/tmp/lab1B_pass.txt"

(python3 -c "print(str(5274))"; echo "cat /home/lab1B/.pass > $file") | /levels/lab01/lab1C

echo "Password stored at $file"

Lab1B

Conferindo as informações do arquivo:

lab1B@warzone:/levels/lab01$ file lab1B
lab1B: setuid ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=c9f07b581bd8d97cdc7c0ff1a288e20aea2df0f5, stripped
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Esse arquivo de 32 bits, agora, possui algumas proteções de stack.

Análise das funções

Dessa vez, somos promptados com a mesma inteface de login:

-----------------------------
--- RPISEC - CrackMe v1.0 ---
-----------------------------

Password: 

Realizaremos, então, o disassembly da main:

Dump of assembler code for function main:
   0x08048be4 <+0>:	push   ebp
   0x08048be5 <+1>:	mov    ebp,esp
   0x08048be7 <+3>:	and    esp,0xfffffff0
   0x08048bea <+6>:	sub    esp,0x20
   0x08048bed <+9>:	push   eax
   0x08048bee <+10>:	xor    eax,eax
   0x08048bf0 <+12>:	je     0x8048bf5 <main+17>
   0x08048bf2 <+14>:	add    esp,0x4
   0x08048bf5 <+17>:	pop    eax
   0x08048bf6 <+18>:	mov    DWORD PTR [esp],0x0
   0x08048bfd <+25>:	call   0x80487b0 <time@plt>
   0x08048c02 <+30>:	mov    DWORD PTR [esp],eax
   0x08048c05 <+33>:	call   0x8048800 <srand@plt>
   0x08048c0a <+38>:	mov    DWORD PTR [esp],0x8048d88
   0x08048c11 <+45>:	call   0x80487d0 <puts@plt>
   0x08048c16 <+50>:	mov    DWORD PTR [esp],0x8048da6
   0x08048c1d <+57>:	call   0x80487d0 <puts@plt>
   0x08048c22 <+62>:	mov    DWORD PTR [esp],0x8048dc4
   0x08048c29 <+69>:	call   0x80487d0 <puts@plt>
   0x08048c2e <+74>:	mov    DWORD PTR [esp],0x8048de2
   0x08048c35 <+81>:	call   0x8048780 <printf@plt>
   0x08048c3a <+86>:	lea    eax,[esp+0x1c]
   0x08048c3e <+90>:	mov    DWORD PTR [esp+0x4],eax
   0x08048c42 <+94>:	mov    DWORD PTR [esp],0x8048dee
   0x08048c49 <+101>:	call   0x8048840 <__isoc99_scanf@plt>
   0x08048c4e <+106>:	mov    eax,DWORD PTR [esp+0x1c]
   0x08048c52 <+110>:	mov    DWORD PTR [esp+0x4],0x1337d00d
   0x08048c5a <+118>:	mov    DWORD PTR [esp],eax
   0x08048c5d <+121>:	call   0x8048a74 <test>     # chama test()
   0x08048c62 <+126>:	mov    eax,0x0
   0x08048c67 <+131>:	leave  
   0x08048c68 <+132>:	ret    
End of assembler dump.

Ao efetuar o disassembly, reparamos que a função test() é chamada em <main+121>:

   0x08048c3e <+90>:    mov    DWORD PTR [esp+0x4],eax
   0x08048c42 <+94>:    mov    DWORD PTR [esp],0x8048dee
   0x08048c49 <+101>:   call   0x8048840 <__isoc99_scanf@plt>
   0x08048c4e <+106>:   mov    eax,DWORD PTR [esp+0x1c]
   0x08048c52 <+110>:   mov    DWORD PTR [esp+0x4],0x1337d00d
   0x08048c5a <+118>:   mov    DWORD PTR [esp],eax
   0x08048c5d <+121>:   call   0x8048a74 <test>     # chama test()

Repare que o input do usuário é inserido como parametro, além de um valor fixo 0x1337d00d. Conferindo a função test():

Dump of assembler code for function test:
   0x08048a74 <+0>:	push   ebp
   0x08048a75 <+1>:	mov    ebp,esp
   0x08048a77 <+3>:	sub    esp,0x28
   0x08048a7a <+6>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048a7d <+9>:	mov    edx,DWORD PTR [ebp+0xc]
   0x08048a80 <+12>:	sub    edx,eax                  #subtrai os inputs
   0x08048a82 <+14>:	mov    eax,edx
   0x08048a84 <+16>:	mov    DWORD PTR [ebp-0xc],eax
   0x08048a87 <+19>:	cmp    DWORD PTR [ebp-0xc],0x15
   0x08048a8b <+23>:	ja     0x8048bd5 <test+353>         
   0x08048a91 <+29>:	mov    eax,DWORD PTR [ebp-0xc]      
   0x08048a94 <+32>:	shl    eax,0x2
   0x08048a97 <+35>:	add    eax,0x8048d30
   0x08048a9c <+40>:	mov    eax,DWORD PTR [eax]
   0x08048a9e <+42>:	jmp    eax
   0x08048aa0 <+44>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048aa3 <+47>:	mov    DWORD PTR [esp],eax
   0x08048aa6 <+50>:	call   0x80489b7 <decrypt>
   0x08048aab <+55>:	jmp    0x8048be2 <test+366>
   0x08048ab0 <+60>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048ab3 <+63>:	mov    DWORD PTR [esp],eax
   0x08048ab6 <+66>:	call   0x80489b7 <decrypt>
   .
   .
   .
   0x08048b85 <+273>:	jmp    0x8048be2 <test+366>
   0x08048b87 <+275>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048b8a <+278>:	mov    DWORD PTR [esp],eax
   0x08048b8d <+281>:	call   0x80489b7 <decrypt>
   0x08048b92 <+286>:	jmp    0x8048be2 <test+366>
   0x08048b94 <+288>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048b97 <+291>:	mov    DWORD PTR [esp],eax
   0x08048b9a <+294>:	call   0x80489b7 <decrypt>
   0x08048b9f <+299>:	jmp    0x8048be2 <test+366>
   0x08048ba1 <+301>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048ba4 <+304>:	mov    DWORD PTR [esp],eax
   0x08048ba7 <+307>:	call   0x80489b7 <decrypt>
   0x08048bac <+312>:	jmp    0x8048be2 <test+366>
   0x08048bae <+314>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048bb1 <+317>:	mov    DWORD PTR [esp],eax
   0x08048bb4 <+320>:	call   0x80489b7 <decrypt>
   0x08048bb9 <+325>:	jmp    0x8048be2 <test+366>
   0x08048bbb <+327>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048bbe <+330>:	mov    DWORD PTR [esp],eax
   0x08048bc1 <+333>:	call   0x80489b7 <decrypt>
   0x08048bc6 <+338>:	jmp    0x8048be2 <test+366>
   0x08048bc8 <+340>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048bcb <+343>:	mov    DWORD PTR [esp],eax
   0x08048bce <+346>:	call   0x80489b7 <decrypt>
   0x08048bd3 <+351>:	jmp    0x8048be2 <test+366>
   0x08048bd5 <+353>:	call   0x8048830 <rand@plt>
   0x08048bda <+358>:	mov    DWORD PTR [esp],eax
   0x08048bdd <+361>:	call   0x80489b7 <decrypt>
   0x08048be2 <+366>:	leave  
   0x08048be3 <+367>:	ret    
End of assembler dump.

Investigando a função test(), vemos que existe um grande if-else (ou switch-case) que realiza a chamada de uma função decrypt() em todos os seus casos. O único caso que se diferencia é o último, cujo é usada a função rand() para ser o parametro dessa chamada.

Em partes, vamos analizar o comportamento da função para entendê-la. Primeiramente:

   0x08048a7a <+6>: mov    eax,DWORD PTR [ebp+0x8] # input do user
   0x08048a7d <+9>: mov    edx,DWORD PTR [ebp+0xc] # 0x1337d00d
   0x08048a80 <+12>:    sub    edx,eax          # subtrai os valores

Nesse momento, é calculada a diferença entre a senha inputada e o valor hexadecimal. Após isso:

   0x08048a82 <+14>:    mov    eax,edx
   0x08048a84 <+16>:    mov    DWORD PTR [ebp-0xc],eax
   0x08048a87 <+19>:    cmp    DWORD PTR [ebp-0xc],0x15
   0x08048a8b <+23>:    ja     0x8048bd5 <test+353>

Aqui, vemos o que provoca o caso do switch-case com o rand(): a diferença entre os valores ser maior que 21. Então, caso a diferença seja igual ou menor que 21:

   0x08048a91 <+29>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048a94 <+32>:    shl    eax,0x2
   0x08048a97 <+35>:    add    eax,0x8048d30
   0x08048a9c <+40>:    mov    eax,DWORD PTR [eax]
   0x08048a9e <+42>:    jmp    eax

Em todo caso, esse jump é tomado para um dos blocos do switch-case (que são todos iguais). Conferindo eles:

   0x08048aa0 <+XX>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048aa3 <+XX>:    mov    DWORD PTR [esp],eax
   0x08048aa6 <+XX>:    call   0x80489b7 <decrypt>

Basicamente, a diferença entre 0x1337d00d e o input é enviada como parametro para decrypt(). Vejamos, então, a função:

Dump of assembler code for function decrypt:
   0x080489b7 <+0>:	push   ebp
   0x080489b8 <+1>:	mov    ebp,esp
   0x080489ba <+3>:	sub    esp,0x38
   0x080489bd <+6>:	mov    eax,gs:0x14
   0x080489c3 <+12>:	mov    DWORD PTR [ebp-0xc],eax
   0x080489c6 <+15>:	xor    eax,eax
   0x080489c8 <+17>:	mov    DWORD PTR [ebp-0x1d],0x757c7d51
   0x080489cf <+24>:	mov    DWORD PTR [ebp-0x19],0x67667360
   0x080489d6 <+31>:	mov    DWORD PTR [ebp-0x15],0x7b66737e
   0x080489dd <+38>:	mov    DWORD PTR [ebp-0x11],0x33617c7d
   0x080489e4 <+45>:	mov    BYTE PTR [ebp-0xd],0x0
   0x080489e8 <+49>:	push   eax
   0x080489e9 <+50>:	xor    eax,eax
   0x080489eb <+52>:	je     0x80489f0 <decrypt+57>
   0x080489ed <+54>:	add    esp,0x4
   0x080489f0 <+57>:	pop    eax
   0x080489f1 <+58>:	lea    eax,[ebp-0x1d]
   0x080489f4 <+61>:	mov    DWORD PTR [esp],eax
   0x080489f7 <+64>:	call   0x8048810 <strlen@plt>
   0x080489fc <+69>:	mov    DWORD PTR [ebp-0x24],eax
   0x080489ff <+72>:	mov    DWORD PTR [ebp-0x28],0x0
   0x08048a06 <+79>:	jmp    0x8048a28 <decrypt+113>
   0x08048a08 <+81>:	lea    edx,[ebp-0x1d]
   0x08048a0b <+84>:	mov    eax,DWORD PTR [ebp-0x28]
   0x08048a0e <+87>:	add    eax,edx
   0x08048a10 <+89>:	movzx  eax,BYTE PTR [eax]
   0x08048a13 <+92>:	mov    edx,eax
   0x08048a15 <+94>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048a18 <+97>:	xor    eax,edx
   0x08048a1a <+99>:	lea    ecx,[ebp-0x1d]
   0x08048a1d <+102>:	mov    edx,DWORD PTR [ebp-0x28]
   0x08048a20 <+105>:	add    edx,ecx
   0x08048a22 <+107>:	mov    BYTE PTR [edx],al
   0x08048a24 <+109>:	add    DWORD PTR [ebp-0x28],0x1
   0x08048a28 <+113>:	mov    eax,DWORD PTR [ebp-0x28]
   0x08048a2b <+116>:	cmp    eax,DWORD PTR [ebp-0x24]
   0x08048a2e <+119>:	jb     0x8048a08 <decrypt+81>
   0x08048a30 <+121>:	mov    DWORD PTR [esp+0x4],0x8048d03
   0x08048a38 <+129>:	lea    eax,[ebp-0x1d]
   0x08048a3b <+132>:	mov    DWORD PTR [esp],eax
   0x08048a3e <+135>:	call   0x8048770 <strcmp@plt>
   0x08048a43 <+140>:	test   eax,eax
   0x08048a45 <+142>:	jne    0x8048a55 <decrypt+158>
   0x08048a47 <+144>:	mov    DWORD PTR [esp],0x8048d14
   0x08048a4e <+151>:	call   0x80487e0 <system@plt>
   0x08048a53 <+156>:	jmp    0x8048a61 <decrypt+170>
   0x08048a55 <+158>:	mov    DWORD PTR [esp],0x8048d1c
   0x08048a5c <+165>:	call   0x80487d0 <puts@plt>
   0x08048a61 <+170>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048a64 <+173>:	xor    eax,DWORD PTR gs:0x14
   0x08048a6b <+180>:	je     0x8048a72 <decrypt+187>
   0x08048a6d <+182>:	call   0x80487c0 <__stack_chk_fail@plt>
   0x08048a72 <+187>:	leave  
   0x08048a73 <+188>:	ret    
End of assembler dump.

Em decrypt(), vemos a existência de uma string hardcoded:

   0x080489c8 <+17>:    mov    DWORD PTR [ebp-0x1d],0x757c7d51
   0x080489cf <+24>:    mov    DWORD PTR [ebp-0x19],0x67667360
   0x080489d6 <+31>:    mov    DWORD PTR [ebp-0x15],0x7b66737e
   0x080489dd <+38>:    mov    DWORD PTR [ebp-0x11],0x33617c7d

Transformando em caracteres, resulta em Q}|u\`sfg~sf{}|a3 (tenha em mente que estamos tratando de Little Endian!).

Além disso, existe um loop onde todos os caracteres dessa string estranha são decodificados (usando XOR) baseados no valor da diferença calculado em test():

   0x08048a08 <+81>:    lea    edx,[ebp-0x1d]
   0x08048a0b <+84>:    mov    eax,DWORD PTR [ebp-0x28]
   0x08048a0e <+87>:    add    eax,edx
   0x08048a10 <+89>:    movzx  eax,BYTE PTR [eax]
   0x08048a13 <+92>:    mov    edx,eax
   0x08048a15 <+94>:    mov    eax,DWORD PTR [ebp+0x8]
   0x08048a18 <+97>:    xor    eax,edx
   0x08048a1a <+99>:    lea    ecx,[ebp-0x1d]
   0x08048a1d <+102>:   mov    edx,DWORD PTR [ebp-0x28]
   0x08048a20 <+105>:   add    edx,ecx
   0x08048a22 <+107>:   mov    BYTE PTR [edx],al
   0x08048a24 <+109>:   add    DWORD PTR [ebp-0x28],0x1
   0x08048a28 <+113>:   mov    eax,DWORD PTR [ebp-0x28]
   0x08048a2b <+116>:   cmp    eax,DWORD PTR [ebp-0x24]
   0x08048a2e <+119>:   jb     0x8048a08 <decrypt+81>

Após a decriptação, a string final é vista novamente em main<+132>:

   0x08048a30 <+121>:   mov    DWORD PTR [esp+0x4],0x8048d03 # string nova?
   0x08048a38 <+129>:   lea    eax,[ebp-0x1d]
   0x08048a3b <+132>:   mov    DWORD PTR [esp],eax # string final
   0x08048a3e <+135>:   call   0x8048770 <strcmp@plt>

Ela é enviada com uma outra string (do endereço 0x8048d03) para ser comparada. Afinal, que string é essa?

gdb-peda$ x/s 0x8048d03
0x8048d03:	"Congratulations!"

Ok! Temos, então, a string Congratulations! como alvo!

Objetivo

Como controlamos apenas o input, devemos inputar um número que, ao subtrair 0x1337d00d, resulte em uma chave válida para a decriptação de Q}|u\`sfg~sf{}|a3 em Congratulations!. Nosso objetivo é descobrir essa chave e saber o payload válido.

Resolvendo

Dado o funcionamento do XOR, podemos realizar XOR entre a string original e a encriptada e, assim, sabemos a chave simétrica. Usando Python:

>>> ord("Q") ^ ord("C")
18

Precisamos, então, de uma diferença de 18! Como fazemos 0x1337D00D - input, sabemos que 0x1337D00D + 0x12 é o input desejado. Assim, chegamos a 322424827 e voilà! Temos a senha 1337_3nCRyptI0n_br0.

Exploit

#!/usr/bin/python

import pwn

# XOR allow us to do that :P
key = 0x1337d00d - (ord("Q") ^ ord("C"))

# Path to the save file
save_file = "/tmp/lab1A_pass.txt"


# Initiating process
p = pwn.process('/levels/lab01/lab1B')

# Sending the payloads
p.sendline(str(key))
p.sendline("cat /home/lab1A/.pass > "+ save_file)

print("[!] Password saved on " + save_file)

p.close()

Lab1A

Conferindo as informações do arquivo:

lab1A@warzone:/levels/lab01$ file lab1A
lab1A: setuid ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=8207a5ad5821e47f25412161c60dc24fd2f3386e, stripped
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Repare que todos os programas estão em 32 bits. Fiz todas essas verificações por prática, mas todos os programas desse curso são em 32 bits.

Análise das funções

Realizando o disassembly da main(), verificamos que há 2 inputs pertinentes: um usuário e um serial.

.---------------------------.
|---------  RPISEC  --------|
|+ SECURE LOGIN SYS v. 3.0 +|
|---------------------------|
|~- Enter your Username:  ~-|
'---------------------------'
UsernameTeste
.---------------------------.
| !! NEW ACCOUNT DETECTED !!|
|---------------------------|
|~- Input your serial:    ~-|
'---------------------------'
123456

Realizando o disassembly da função main(), temos:

Dump of assembler code for function main:
   0x08048b44 <+0>:	push   ebp
   0x08048b45 <+1>:	mov    ebp,esp
   0x08048b47 <+3>:	and    esp,0xfffffff0
   0x08048b4a <+6>:	sub    esp,0x40
   0x08048b4d <+9>:	mov    eax,DWORD PTR [ebp+0xc]
   0x08048b50 <+12>:	mov    DWORD PTR [esp+0xc],eax
   0x08048b54 <+16>:	mov    eax,gs:0x14
   0x08048b5a <+22>:	mov    DWORD PTR [esp+0x3c],eax
   0x08048b5e <+26>:	xor    eax,eax
   0x08048b60 <+28>:	push   eax
   0x08048b61 <+29>:	xor    eax,eax
   0x08048b63 <+31>:	je     0x8048b68 <main+36>
   0x08048b65 <+33>:	add    esp,0x4
   0x08048b68 <+36>:	pop    eax
   0x08048b69 <+37>:	mov    DWORD PTR [esp],0x8048d73
   0x08048b70 <+44>:	call   0x8048810 <puts@plt>
   0x08048b75 <+49>:	mov    DWORD PTR [esp],0x8048d91
   0x08048b7c <+56>:	call   0x8048810 <puts@plt>
   0x08048b81 <+61>:	mov    DWORD PTR [esp],0x8048daf
   0x08048b88 <+68>:	call   0x8048810 <puts@plt>
   0x08048b8d <+73>:	mov    DWORD PTR [esp],0x8048dcd
   0x08048b94 <+80>:	call   0x8048810 <puts@plt>
   0x08048b99 <+85>:	mov    DWORD PTR [esp],0x8048deb
   0x08048ba0 <+92>:	call   0x8048810 <puts@plt>
   0x08048ba5 <+97>:	mov    DWORD PTR [esp],0x8048e09
   0x08048bac <+104>:	call   0x8048810 <puts@plt>
   0x08048bb1 <+109>:	mov    eax,ds:0x804b060
   0x08048bb6 <+114>:	mov    DWORD PTR [esp+0x8],eax 
   0x08048bba <+118>:	mov    DWORD PTR [esp+0x4],0x20
   0x08048bc2 <+126>:	lea    eax,[esp+0x1c]
   0x08048bc6 <+130>:	mov    DWORD PTR [esp],eax
   0x08048bc9 <+133>:	call   0x80487d0 <fgets@plt> # Coleta o username como string
   0x08048bce <+138>:	mov    DWORD PTR [esp],0x8048d73
   0x08048bd5 <+145>:	call   0x8048810 <puts@plt>
   0x08048bda <+150>:	mov    DWORD PTR [esp],0x8048e27
   0x08048be1 <+157>:	call   0x8048810 <puts@plt>
   0x08048be6 <+162>:	mov    DWORD PTR [esp],0x8048dcd
   0x08048bed <+169>:	call   0x8048810 <puts@plt>
   0x08048bf2 <+174>:	mov    DWORD PTR [esp],0x8048e45
   0x08048bf9 <+181>:	call   0x8048810 <puts@plt>
   0x08048bfe <+186>:	mov    DWORD PTR [esp],0x8048e09
   0x08048c05 <+193>:	call   0x8048810 <puts@plt>
   0x08048c0a <+198>:	lea    eax,[esp+0x18]
   0x08048c0e <+202>:	mov    DWORD PTR [esp+0x4],eax
   0x08048c12 <+206>:	mov    DWORD PTR [esp],0x8048d00
   0x08048c19 <+213>:	call   0x8048860 <__isoc99_scanf@plt> # Coleta o serial como int
   0x08048c1e <+218>:	mov    eax,DWORD PTR [esp+0x18]
   0x08048c22 <+222>:	mov    DWORD PTR [esp+0x4],eax
   0x08048c26 <+226>:	lea    eax,[esp+0x1c]
   0x08048c2a <+230>:	mov    DWORD PTR [esp],eax
   0x08048c2d <+233>:	call   0x8048a0f <auth>
   0x08048c32 <+238>:	test   eax,eax
   0x08048c34 <+240>:	jne    0x8048c55 <main+273>
   0x08048c36 <+242>:	mov    DWORD PTR [esp],0x8048e63
   0x08048c3d <+249>:	call   0x8048810 <puts@plt>
   0x08048c42 <+254>:	mov    DWORD PTR [esp],0x8048e72
   0x08048c49 <+261>:	call   0x8048820 <system@plt>
   0x08048c4e <+266>:	mov    eax,0x0
   0x08048c53 <+271>:	jmp    0x8048c5a <main+278>
   0x08048c55 <+273>:	mov    eax,0x1
   0x08048c5a <+278>:	mov    edx,DWORD PTR [esp+0x3c]
   0x08048c5e <+282>:	xor    edx,DWORD PTR gs:0x14
   0x08048c65 <+289>:	je     0x8048c6c <main+296>
   0x08048c67 <+291>:	call   0x8048800 <__stack_chk_fail@plt>
   0x08048c6c <+296>:	leave  
   0x08048c6d <+297>:	ret    
End of assembler dump.

Sendo um pouco mais breve, os inputs são armazenados na stack e enviados para auth(), em <main+233>. Realizando o disassembly de auth():

Dump of assembler code for function auth:
   0x08048a0f <+0>:	push   ebp
   0x08048a10 <+1>:	mov    ebp,esp
   0x08048a12 <+3>:	sub    esp,0x28
   0x08048a15 <+6>:	mov    DWORD PTR [esp+0x4],0x8048d03
   0x08048a1d <+14>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048a20 <+17>:	mov    DWORD PTR [esp],eax
   0x08048a23 <+20>:	call   0x80487a0 <strcspn@plt>
   0x08048a28 <+25>:	mov    edx,DWORD PTR [ebp+0x8]
   0x08048a2b <+28>:	add    eax,edx
   0x08048a2d <+30>:	mov    BYTE PTR [eax],0x0
   0x08048a30 <+33>:	mov    DWORD PTR [esp+0x4],0x20
   0x08048a38 <+41>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048a3b <+44>:	mov    DWORD PTR [esp],eax
   0x08048a3e <+47>:	call   0x8048850 <strnlen@plt>
   0x08048a43 <+52>:	mov    DWORD PTR [ebp-0xc],eax
   0x08048a46 <+55>:	push   eax
   0x08048a47 <+56>:	xor    eax,eax
   0x08048a49 <+58>:	je     0x8048a4e <auth+63>
   0x08048a4b <+60>:	add    esp,0x4
   0x08048a4e <+63>:	pop    eax
   0x08048a4f <+64>:	cmp    DWORD PTR [ebp-0xc],0x5
   0x08048a53 <+68>:	jg     0x8048a5f <auth+80>
   0x08048a55 <+70>:	mov    eax,0x1
   0x08048a5a <+75>:	jmp    0x8048b42 <auth+307>
   0x08048a5f <+80>:	mov    DWORD PTR [esp+0xc],0x0
   0x08048a67 <+88>:	mov    DWORD PTR [esp+0x8],0x1
   0x08048a6f <+96>:	mov    DWORD PTR [esp+0x4],0x0
   0x08048a77 <+104>:	mov    DWORD PTR [esp],0x0
   0x08048a7e <+111>:	call   0x8048870 <ptrace@plt>
   0x08048a83 <+116>:	cmp    eax,0xffffffff
   0x08048a86 <+119>:	jne    0x8048ab6 <auth+167>
   0x08048a88 <+121>:	mov    DWORD PTR [esp],0x8048d08
   0x08048a8f <+128>:	call   0x8048810 <puts@plt>
   0x08048a94 <+133>:	mov    DWORD PTR [esp],0x8048d2c
   0x08048a9b <+140>:	call   0x8048810 <puts@plt>
   0x08048aa0 <+145>:	mov    DWORD PTR [esp],0x8048d50
   0x08048aa7 <+152>:	call   0x8048810 <puts@plt>
   0x08048aac <+157>:	mov    eax,0x1
   0x08048ab1 <+162>:	jmp    0x8048b42 <auth+307>
   0x08048ab6 <+167>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048ab9 <+170>:	add    eax,0x3
   0x08048abc <+173>:	movzx  eax,BYTE PTR [eax]
   0x08048abf <+176>:	movsx  eax,al
   0x08048ac2 <+179>:	xor    eax,0x1337
   0x08048ac7 <+184>:	add    eax,0x5eeded
   0x08048acc <+189>:	mov    DWORD PTR [ebp-0x10],eax
   0x08048acf <+192>:	mov    DWORD PTR [ebp-0x14],0x0
   0x08048ad6 <+199>:	jmp    0x8048b26 <auth+279>
   0x08048ad8 <+201>:	mov    edx,DWORD PTR [ebp-0x14]
   0x08048adb <+204>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048ade <+207>:	add    eax,edx
   0x08048ae0 <+209>:	movzx  eax,BYTE PTR [eax]
   0x08048ae3 <+212>:	cmp    al,0x1f
   0x08048ae5 <+214>:	jg     0x8048aee <auth+223>
   0x08048ae7 <+216>:	mov    eax,0x1
   0x08048aec <+221>:	jmp    0x8048b42 <auth+307>
   0x08048aee <+223>:	mov    edx,DWORD PTR [ebp-0x14]
   0x08048af1 <+226>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048af4 <+229>:	add    eax,edx
   0x08048af6 <+231>:	movzx  eax,BYTE PTR [eax]
   0x08048af9 <+234>:	movsx  eax,al
   0x08048afc <+237>:	xor    eax,DWORD PTR [ebp-0x10]
   0x08048aff <+240>:	mov    ecx,eax
   0x08048b01 <+242>:	mov    edx,0x88233b2b
   0x08048b06 <+247>:	mov    eax,ecx
   0x08048b08 <+249>:	mul    edx
   0x08048b0a <+251>:	mov    eax,ecx
   0x08048b0c <+253>:	sub    eax,edx
   0x08048b0e <+255>:	shr    eax,1
   0x08048b10 <+257>:	add    eax,edx
   0x08048b12 <+259>:	shr    eax,0xa
   0x08048b15 <+262>:	imul   eax,eax,0x539
   0x08048b1b <+268>:	sub    ecx,eax
   0x08048b1d <+270>:	mov    eax,ecx
   0x08048b1f <+272>:	add    DWORD PTR [ebp-0x10],eax
   0x08048b22 <+275>:	add    DWORD PTR [ebp-0x14],0x1
   0x08048b26 <+279>:	mov    eax,DWORD PTR [ebp-0x14]
   0x08048b29 <+282>:	cmp    eax,DWORD PTR [ebp-0xc]
   0x08048b2c <+285>:	jl     0x8048ad8 <auth+201>
   0x08048b2e <+287>:	mov    eax,DWORD PTR [ebp+0xc]
   0x08048b31 <+290>:	cmp    eax,DWORD PTR [ebp-0x10]
   0x08048b34 <+293>:	je     0x8048b3d <auth+302>
   0x08048b36 <+295>:	mov    eax,0x1
   0x08048b3b <+300>:	jmp    0x8048b42 <auth+307>
   0x08048b3d <+302>:	mov    eax,0x0
   0x08048b42 <+307>:	leave  
   0x08048b43 <+308>:	ret    
End of assembler dump.

O assembly dessa função está muito extenso. Portanto, para facilitar, utilizei o decompilador do Ghidra para entender melhor o que estava ocorrendo:

/* WARNING: Removing unreachable block (ram,0x08048a4b) */
/* WARNING: Restarted to delay deadcode elimination for space: stack */

undefined4 auth(char *param_1,uint param_2)

{
  size_t sVar1;
  undefined4 uVar2;
  long lVar3;
  int local_18;
  uint local_14;
  
  sVar1 = strcspn(param_1,"\n");
  param_1[sVar1] = '\0';
  sVar1 = strnlen(param_1,0x20);
  if ((int)sVar1 < 6) {
    uVar2 = 1;
  }
  else {
    lVar3 = ptrace(PTRACE_TRACEME);
    if (lVar3 == -1) {
      puts("\x1b[32m.---------------------------.");
      puts("\x1b[31m| !! TAMPERING DETECTED !!  |");
      puts("\x1b[32m\'---------------------------\'");
      uVar2 = 1;
    }
    else {
      local_14 = ((int)param_1[3] ^ 0x1337U) + 0x5eeded;
      for (local_18 = 0; local_18 < (int)sVar1; local_18 = local_18 + 1) {
        if (param_1[local_18] < ' ') {
          return 1;
        }
        local_14 = local_14 + ((int)param_1[local_18] ^ local_14) % 0x539;
      }
      if (param_2 == local_14) {
        uVar2 = 0;
      }
      else {
        uVar2 = 1;
      }
    }
  }
  return uVar2;
}

Adaptando os nomes de algumas variáveis para facilitar a leitura, resultamos em:

int auth(char *username,uint serial)

{
  size_t inputLength;
  undefined4 flag;
  long ptrace?;
  int i;
  uint temp;
  
  inputLength = strcspn(username,"\n");
  username[inputLength] = '\0';
  inputLength = strnlen(username,0x20);
  if ((int)inputLength < 6) {
    flag = 1;
  }
  else {
    ptrace? = ptrace(PTRACE_TRACEME);
    if (ptrace? == -1) {
      puts("\x1b[32m.---------------------------.");
      puts("\x1b[31m| !! TAMPERING DETECTED !!  |");
      puts("\x1b[32m\'---------------------------\'");
      flag = 1;
    }
    else {
      temp = ((int)username[3] ^ 0x1337U) + 0x5eeded;
      for (i = 0; i < (int)inputLength; i = i + 1) {
        if (username[i] < ' ') {
          return 1;
        }
        temp = temp + ((int)username[i] ^ temp) % 0x539;
      }
      if (serial == temp) {
        flag = 0;
      }
      else {
        flag = 1;
      }
    }
  }
  return flag;
}

Em auth(), podemos verificar que o programa retira, do nome do usuário, o caracter \n e substitui por \0, além de calcular e armazenar o comprimento da string. Cabe salientar que usernames com menos de 6 caracteres não são aceitos pelo programa e resultam em retorno imediato com valor 1 (que invalida a sessão). Após isso, ele faz algo em uma lógica fixa com o username e compara com o serial inputado para validar.

Resolução

Para resolver o problema, criei um pequeno keygen a partir da lógica exibida pelo decompilador.

#!/usr/bin/python

import pwn

#input
username = str(raw_input("[!] Insert the username that you want to use: "))

# Serial logic
num = (ord(username[3]) ^ 0x1337) + 6221293

for char in username:
    if char == ' ':
        print("lol")
        exit(3)

    if char == '\n':
        continue

    num = num + (ord(char) ^ num) % 1337

# Printing out the serial generated for the inputed username
print("[!] Serial: " + str(num))

 # Starting process
p = pwn.process("/levels/lab01/lab1A")

# Sending payload
p.sendline(username)

p.sendline(str(num))

# Give the user the shell
p.interactive()

# Close the program
p.close()

A partir de um username inputado, ele cria um serial válido e executa o programa. Basta utilizá-lo e voilà! Fim de laboratório!

Ferramentas utilizadas:

  • GEF, uma extensão para o GDB
  • GHIDRA, uma ferramenta de análise de binários.
  • pwntools, uma biblioteca de Python para fabricação de exploits.