Quando o binário vem stripped desanima um pouco porque sou noob. Mas enquanto caminho para um dia deixar de ser, vou aprendendo como analisar e lidar com esses casos. Esse desafio foi um desses que com um truque simples deu para pegar a flag.
Vou usar muito o GDB nesse write-up e não irei entrar nos detalhes de comandos e bla bla porque já escrevi sobre isso aqui. Passe lá se nunca usou o debugger e não faz ideia o que é PEDA.
A descrição dizia: “Just one byte makes all the difference.” e me lembrou de One toke over the line por motivos que não me recordo.
Para fazer a engenharia reversa, temos um simples elf stripped que você pode baixar aqui.
b@b ~/h> file a-byte
a-byte: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=88fe0ee8aed1a070d6555c7e9866e364a40f686c, stripped
Nenhuma pista na execução.
b@b ~/h> ./a-byte
u do not know da wae
b@b ~/h> ./a-byte asdfa
u do not know da wae
Executando com ltrace se detecta uma chamada para strlen, uma função que conta o número de caracteres numa string.
b@b ~/h> ltrace ./a-byte
puts("u do not know da wae"u do not know da wae
) = 21
+++ exited (status 255) +++
b@b ~/h> ltrace ./a-byte asdfjasd
strlen("asdfjasd") = 8
puts("u do not know da wae"u do not know da wae
) = 21
+++ exited (status 255) +++
Ao começar a analisar com o gdb, vemos que um binário stripped não dá muita bandeira:
b@b ~/h> gdb -q a-byte
Reading symbols from a-byte...(no debugging symbols found)...done.
gdb-peda$ info functions
All defined functions:
Non-debugging symbols:
0x00000000000005e0 puts@plt
0x00000000000005f0 strlen@plt
0x0000000000000600 __stack_chk_fail@plt
0x0000000000000610 strcmp@plt
0x0000000000000620 __cxa_finalize@plt
Então, depois de descobrir que era possível, decidi inserir um breakpoint em strlen.
gdb-peda$ b strlen@plt
Breakpoint 1 at 0x5f0
gdb-peda$ run AAAAAAAAAAAA
Executo strlen linha a linha usando o comando n e observo onde o código para assim que strlen retorna:
0x55555555478e: call 0x5555555545f0 <strlen@plt>
=> 0x555555554793: mov DWORD PTR [rbp-0x3c],eax
0x555555554796: cmp DWORD PTR [rbp-0x3c],0x23
0x55555555479a: jne 0x555555554761
Essa instrução vai mover EAX para a stack, para a posição rbp-0x3c para ser mais exato. E a seguinte vai comparar exatamente a posição rbp-0x3c com o valor 0x23, 35 em decimal.
Você provavelmente pode adivinhar essa. Sim, strlen retorna seu resultado para o registrador EAX e as instruções seguintes movem o resultado para a stack e o compara com 35. Significa que a primeira validação verifica se o argumento passado tem o tamanho 35.
Estamos em terra de 64 bits por aqui, então EAX na verdade é RAX e nesse momento guarda a quantidade de A’s que inseri:
RAX: 0xc ('\x0c')
\x0C nada mais é que C em hexadecimal que em decimal representa os 12 A’s que enviei como argumento.
Se ainda não trocou de aba no navegador você pode estar se perguntando: “por que a instrução é MOV EAX e não MOV RAX? Qual a diferença?”
É uma boa questão. A explicação é que MOV EAX só usará os últimos 32 bits de RAX (e automaticamente vai zerar os primeiros 32 bits). É possível descobrir mais dessas nuances indo estudar assembly. E se você não sabia disso, deveria ir.
E a última instrução, jne 0x555555554761, é uma instrução condiconal que vai verificar a flag zero e pular ou não para esse endereço na memória.
Depois de descobrir que o input precisa ter 35 caracteres, passei essa quantidade como argumento, removi o breakpoint de strlen e setei um breakpoint em strcmp, porque ela é uma função que compara duas strings e isso soa como uma grande candidata para guardar flag. Poderia ter continuado debuggando o código de onde strlen retornou e entendendo o programa? Poderia, mas no ctf a gente quer resolver as coisas rápido e nem sempre é o modo mais bonito e didático.
Ao reiniciar a execução do binário passando os 35 A’s, caio direto dentro da primeira instrução de strcmp:
gdb-peda$ b strcmp@plt
Breakpoint 1 at 0x610
gdb-peda$ run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/b/h/a-byte AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
RBX: 0x0
RCX: 0x40 ('@')
RDX: 0x7fffffffe3bb ('@' <repeats 35 times>)
RSI: 0x7fffffffe3bb ('@' <repeats 35 times>)
RDI: 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
RBP: 0x7fffffffdf70 --> 0x5555555548b0 (push r15)
RSP: 0x7fffffffdf18 --> 0x555555554878 (test eax,eax)
RIP: 0x555555554610 (<strcmp@plt>: jmp QWORD PTR [rip+0x2009ba] # 0x555555754fd0)
R8 : 0x4000 ('')
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x0
R11: 0x0
R12: 0x555555554630 (xor ebp,ebp)
R13: 0x7fffffffe050 --> 0x2
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554600 <__stack_chk_fail@plt>:
jmp QWORD PTR [rip+0x2009c2] # 0x555555754fc8
0x555555554606 <__stack_chk_fail@plt+6>: push 0x2
0x55555555460b <__stack_chk_fail@plt+11>: jmp 0x5555555545d0
=> 0x555555554610 <strcmp@plt>: jmp QWORD PTR [rip+0x2009ba] # 0x555555754fd0
| 0x555555554616 <strcmp@plt+6>: push 0x3
| 0x55555555461b <strcmp@plt+11>: jmp 0x5555555545d0
| 0x555555554620 <__cxa_finalize@plt>: jmp QWORD PTR [rip+0x2009d2] # 0x555555754ff8
| 0x555555554626 <__cxa_finalize@plt+6>: xchg ax,ax
|-> 0x7ffff7a8de70 <__strcmp_sse2_unaligned>: mov eax,edi
0x7ffff7a8de72 <__strcmp_sse2_unaligned+2>: xor edx,edx
0x7ffff7a8de74 <__strcmp_sse2_unaligned+4>: pxor xmm7,xmm7
0x7ffff7a8de78 <__strcmp_sse2_unaligned+8>: or eax,esi
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdf18 --> 0x555555554878 (test eax,eax)
0008| 0x7fffffffdf20 --> 0x7fffffffe058 --> 0x7fffffffe3aa ("/home/b/h/a-byte")
0016| 0x7fffffffdf28 --> 0x200f0b0ff
0024| 0x7fffffffdf30 --> 0x2300000023 ('#')
0032| 0x7fffffffdf38 --> 0x7fffffffe3bb ('@' <repeats 35 times>)
0040| 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
0048| 0x7fffffffdf48 ("v^x1t^jo1v^e5^v@2^9i3c@138|")
0056| 0x7fffffffdf50 ("1v^e5^v@2^9i3c@138|")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000555555554610 in strcmp@plt ()
Se não me engano, strcmp recebe dois argumentos, duas strings, para fazer a comparação. Assim que bati o olho no registrador RAX e vi que ele apontava para uma string diferenciada. Reiniciei a execução passando esse valor como argumento e vi a flag nascer na stack.
gdb-peda$ run irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138\|
Starting program: /home/b/h/a-byte irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138\|
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
RBX: 0x0
RCX: 0x7d ('}')
RDX: 0x7fffffffe3bb ("hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}")
RSI: 0x7fffffffe3bb ("hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}")
RDI: 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
RBP: 0x7fffffffdf70 --> 0x5555555548b0 (push r15)
RSP: 0x7fffffffdf18 --> 0x555555554878 (test eax,eax)
RIP: 0x555555554610 (<strcmp@plt>: jmp QWORD PTR [rip+0x2009ba] # 0x555555754fd0)
R8 : 0x4000 ('')
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x0
R11: 0x0
R12: 0x555555554630 (xor ebp,ebp)
R13: 0x7fffffffe050 --> 0x2
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555554600 <__stack_chk_fail@plt>:
jmp QWORD PTR [rip+0x2009c2] # 0x555555754fc8
0x555555554606 <__stack_chk_fail@plt+6>: push 0x2
0x55555555460b <__stack_chk_fail@plt+11>: jmp 0x5555555545d0
=> 0x555555554610 <strcmp@plt>: jmp QWORD PTR [rip+0x2009ba] # 0x555555754fd0
| 0x555555554616 <strcmp@plt+6>: push 0x3
| 0x55555555461b <strcmp@plt+11>: jmp 0x5555555545d0
| 0x555555554620 <__cxa_finalize@plt>: jmp QWORD PTR [rip+0x2009d2] # 0x555555754ff8
| 0x555555554626 <__cxa_finalize@plt+6>: xchg ax,ax
|-> 0x7ffff7a8de70 <__strcmp_sse2_unaligned>: mov eax,edi
0x7ffff7a8de72 <__strcmp_sse2_unaligned+2>: xor edx,edx
0x7ffff7a8de74 <__strcmp_sse2_unaligned+4>: pxor xmm7,xmm7
0x7ffff7a8de78 <__strcmp_sse2_unaligned+8>: or eax,esi
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdf18 --> 0x555555554878 (test eax,eax)
0008| 0x7fffffffdf20 --> 0x7fffffffe058 --> 0x7fffffffe3aa ("/home/b/h/a-byte")
0016| 0x7fffffffdf28 --> 0x200f0b0ff
0024| 0x7fffffffdf30 --> 0x2300000023 ('#')
0032| 0x7fffffffdf38 --> 0x7fffffffe3bb ("hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}")
0040| 0x7fffffffdf40 ("irbugzv1v^x1t^jo1v^e5^v@2^9i3c@138|")
0048| 0x7fffffffdf48 ("v^x1t^jo1v^e5^v@2^9i3c@138|")
0056| 0x7fffffffdf50 ("1v^e5^v@2^9i3c@138|")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000555555554610 in strcmp@plt ()
Não tenho certeza se esse era o caminho da flag que o criador do desafio visionou porque minha análise foi bem porca, mas flag é o que importa.
hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}
tags: Sem Categoria, reversing, engenharia reversa,