Written by aaSSfxxx -
Suite à ma participation au CTF AeroCTF 2020, j'en profite pour faire un writeup sur un des challenges "simples", "1000 and 1 night".
Pour ce challenge, on récupère une archive comportant beaucoup de fichiers, dont le nom semble être un hash. Chaque fichier est fichier un programme ELF x86_64. De plus, un serveur écoute, et demande:
Enter valid token to binary with name <8c235f89a8143a28a1d6067e959dd858>
Token:
à la connexion. On comprend donc assez rapidement qu'il va falloir automatiser le reversing de tous ces ELF pour renvoyer le bon token au serveur, et ainsi avoir le flag, le serveur demandant une série de tokens avant de cracher le flag.
Fort heureusement pour nous, ces ELF ont une structure très similaire, et l'automatisation ne devrait pas être trop difficile (d'autant plus que les binaires ne sont pas strippés). La partie intéressante se trouve donc dans la fonction "sym.check" sous radare2:
; CALL XREF from main @ 0x1219
┌ 171: sym.check (void *arg1);
│ ; var void *s1 @ rbp-0x38
│ ; var void *s2 @ rbp-0x30
│ ; var int64_t var_28h @ rbp-0x28
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_18h @ rbp-0x18
│ ; var signed int64_t var_4h @ rbp-0x4
│ ; arg void *arg1 @ rdi
│ 0x000012a4 55 push rbp
│ 0x000012a5 4889e5 mov rbp, rsp
│ 0x000012a8 4883ec40 sub rsp, 0x40
│ 0x000012ac 48897dc8 mov qword [s1], rdi ; arg1
│ 0x000012b0 48b8110e5655. movabs rax, 0xe57581255560e11
│ 0x000012ba 48ba0e585544. movabs rdx, 0x114758114455580e
│ 0x000012c4 488945d0 mov qword [s2], rax
│ 0x000012c8 488955d8 mov qword [var_28h], rdx
│ 0x000012cc 48b80d131244. movabs rax, 0x5614410e4412130d
│ 0x000012d6 48ba470d5755. movabs rdx, 0x430d424155570d47
│ 0x000012e0 488945e0 mov qword [var_20h], rax
│ 0x000012e4 488955e8 mov qword [var_18h], rdx
│ 0x000012e8 c745fc000000. mov dword [var_4h], 0
│ ┌─< 0x000012ef eb2e jmp 0x131f
│ │ ; CODE XREF from sym.check @ 0x1323
│ ┌──> 0x000012f1 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x000012f4 4863d0 movsxd rdx, eax
│ ╎│ 0x000012f7 488b45c8 mov rax, qword [s1]
│ ╎│ 0x000012fb 4801d0 add rax, rdx
│ ╎│ 0x000012fe 0fb600 movzx eax, byte [rax]
│ ╎│ 0x00001301 83c00a add eax, 0xa
│ ╎│ 0x00001304 83f022 xor eax, 0x22
│ ╎│ 0x00001307 8d48f5 lea ecx, [rax - 0xb]
│ ╎│ 0x0000130a 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x0000130d 4863d0 movsxd rdx, eax
│ ╎│ 0x00001310 488b45c8 mov rax, qword [s1]
│ ╎│ 0x00001314 4801d0 add rax, rdx
│ ╎│ 0x00001317 89ca mov edx, ecx
│ ╎│ 0x00001319 8810 mov byte [rax], dl
│ ╎│ 0x0000131b 8345fc01 add dword [var_4h], 1
│ ╎│ ; CODE XREF from sym.check @ 0x12ef
│ ╎└─> 0x0000131f 837dfc1f cmp dword [var_4h], 0x1f
│ └──< 0x00001323 7ecc jle 0x12f1
│ 0x00001325 488d4dd0 lea rcx, [s2]
│ 0x00001329 488b45c8 mov rax, qword [s1]
│ 0x0000132d ba20000000 mov edx, 0x20 ; "@" ; size_t n
│ 0x00001332 4889ce mov rsi, rcx ; const void *s2
│ 0x00001335 4889c7 mov rdi, rax ; const void *s1
│ 0x00001338 e833fdffff call sym.imp.memcmp ; int memcmp(const void *s1, const void *s2, size_t n)
│ 0x0000133d 85c0 test eax, eax
│ ┌─< 0x0000133f 7407 je 0x1348
│ │ 0x00001341 b800000000 mov eax, 0
│ ┌──< 0x00001346 eb05 jmp 0x134d
│ ││ ; CODE XREF from sym.check @ 0x133f
│ │└─> 0x00001348 b801000000 mov eax, 1
│ │ ; CODE XREF from sym.check @ 0x1346
│ └──> 0x0000134d c9 leave
└ 0x0000134e c3 ret
On peut voir que le crackme remplit un buffer avec 4 qwords (le "token" chiffré), avant de récupérer le serial saisi par l'utilisateur, et effectuer des calculs dessus avant de le comparer avec le premier buffer.
L'algorithme de chiffrement de l'input est donc pour chaque octet:
out[i] = ((serial[i] + 0xa) ^ 0x22) - 0xb
Tous les exécutables ont le même algorithme, seuls les paramètres, c'est-à-dire le buffer, et les constantes 0xa, 0x22 et 0xb changent pour chaque binaire. Il nous suffit donc de sortir python et r2pipe pour extraire ces valeurs et communiquer avec le serveur, ce qui donne le python ci-dessous (les binaires sont placés dans un sous-dossier "files"):
#!/usr/bin/python
import r2pipe
import sys
import struct
import pexpect
import socket
buf = b""
def get_tok(file):
r2 = r2pipe.open("files/" + file)
# Extraction du buffer
buf = struct.pack("<Q", r2.cmdj("pdj 1 @0x000012b0")[0]["val"])
buf += struct.pack("<Q", r2.cmdj("pdj 1 @0x000012ba")[0]["val"])
buf += struct.pack("<Q", r2.cmdj("pdj 1 @0x000012cc")[0]["val"])
buf += struct.pack("<Q", r2.cmdj("pdj 1 @0x000012d6")[0]["val"])
# Extraction des params de chiffrement
add_operand = r2.cmdj("pdj 1 @0x00001301")[0]["val"]
xor_operand = r2.cmdj("pdj 1 @0x00001304")[0]["val"]
final_sub = r2.cmdj("pdj 1 @0x00001307")[0]["esil"].split(",")[0]
final_sub = int(final_sub[2:], 16)
out = []
for i in buf:
out.append(((final_sub + i) ^ xor_operand) - add_operand)
return "".join([chr(x) for x in out])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("tasks.aeroctf.com", 44324))
toto = sock.makefile()
while True:
line = toto.readline()
print(line)
if line.find("Enter valid token") > -1:
name = line[line.find("<")+1:-2]
token = get_tok(name)
sock.send(bytes(token + "\n", "ascii"))
La seule partie "compliquée" ici étant d'extraire la bonne valeur du "lea ecx, [rax - 0xb]", où je me suis basé sur l'évaluation ESIL faite par radare2 pour récupérer la bonne valeur. Enfin, dernière subtilité, le serveur renvoie une séquence ANSI de reset du terminal après avoir envoyé le flag, il a donc fallu rediriger la sortie vers un fichier, pour obtenir le flag.
That's all folks ! :þ