Keykoolol (reverse, 500)

Description du challenge

1
On vous demande d'écrire un générateur d'entrées valides pour ce binaire, puis de le valider sur les entrées fournies par le service distant afin d'obtenir le flag.
2
3
Service : nc challenges2.france-cybersecurity-challenge.fr 3000
Copied!

Solution

Ce crackme était une aventure intense et pleine de rebondissements. J'ai du passer environ 8 heures non-stop dessus pour le résoudre. Il y a probablement quelques cracks qui l'ont poutrée beaucoup plus vite, donc je suis curieux de connaître les méthodes qui permettaient d'accélérer le processus de résolution. Quoi qu'il en soit, je suis fier de constater le fruit de mon acharnement et de ma persévérance sur cette longue épreuve !
Rentrons dans le vif du sujet : on nous donne un ELF 64 bits qui nous demande un username ainsi qu'un serial associé :
1
$ ./keykoolol
2
[+] Username: abc
3
[+] Serial: 0123
4
[!] Incorrect serial.
Copied!
Lançons le binaire dans Ghidra. La fonction main ne paraît pas dépaysante, ce qui est rassurant.
1
undefined8 FUN_00100730(void)
2
3
{
4
char cVar1;
5
size_t sVar2;
6
ulong uVar3;
7
ulong uVar4;
8
char *__s;
9
long in_FS_OFFSET;
10
byte bVar5;
11
char local_420 [512];
12
char local_220 [512];
13
long local_20;
14
15
bVar5 = 0;
16
local_20 = *(long *)(in_FS_OFFSET + 0x28);
17
__printf_chk(1,"[+] Username: ");
18
fgets(local_420,0x200,stdin);
19
sVar2 = strcspn(local_420,"\n");
20
local_420[sVar2] = 0;
21
__printf_chk(1,"[+] Serial: ");
22
fgets(local_220,0x200,stdin);
23
sVar2 = strcspn(local_220,"\n");
24
local_220[sVar2] = 0;
25
uVar3 = 0xffffffffffffffff;
26
__s = local_220;
27
do {
28
if (uVar3 == 0) break;
29
uVar3 = uVar3 - 1;
30
cVar1 = *__s;
31
__s = __s + (ulong)bVar5 * -2 + 1;
32
} while (cVar1 != 0);
33
uVar4 = 0xffffffffffffffff;
34
__s = local_420;
35
do {
36
if (uVar4 == 0) break;
37
uVar4 = uVar4 - 1;
38
cVar1 = *__s;
39
__s = __s + (ulong)bVar5 * -2 + 1;
40
} while (cVar1 != 0);
41
uVar3 = FUN_0010096a(&DAT_001024e0,0x400,local_420,~uVar4 - 1,local_220,~uVar3 - 1);
42
__s = "[!] Incorrect serial.";
43
if ((int)uVar3 != 0) {
44
puts("[>] Valid serial!");
45
__s = "[>] Now connect to the remote server and generate serials for the given usernames.";
46
}
47
puts(__s);
48
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
49
/* WARNING: Subroutine does not return */
50
__stack_chk_fail();
51
}
52
return 0;
53
}
Copied!
La ligne importante est la suivante, où j'ai renommé les arguments :
1
r = FUN_0010096a(&DAT_001024e0, 0x400, username, len_username, serial, len_serial);
Copied!
Pour gagner, il faut que cet appel renvoie autre chose que 0. 0x001024e0 est l'adresse d'un grand tableau de 0x400 = 1024 octets hardcodés dans le binaire et qui ne font a priori pas encore sens.
On rentre dans la fonction, et là c'est le drame. D'abord, une petite capture du flow graph largement dézoomé sous IDA :
A ce moment-là c'est simple : on baisse les bras et on va tenter d'autres épreuves 😁
Puis on revient à nouveau dessus en se disant qu'elle vaut quand même 500 points et qu'une fois passée l'étape de tout bien lire ce qu'il se passe, elle doit être franchement faisable.
Examinons d'abord le prologue de cette fonction. J'ai renommé data la référence au bloc de 1024 octets qui et passé en argument.
1
bVar16 = 0;
2
lVar10 = 0x10;
3
puVar13 = &DAT_00303040;
4
while (lVar10 != 0) {
5
lVar10 = lVar10 + -1;
6
*puVar13 = 0;
7
puVar13 = puVar13 + 1;
8
}
9
lVar10 = 0x200;
10
uVar11 = (uint)data_len & 0xfffffff0;
11
puVar13 = &DAT_00303080;
12
while (lVar10 != 0) {
13
lVar10 = lVar10 + -1;
14
*puVar13 = 0;
15
puVar13 = puVar13 + 1;
16
}
17
lVar10 = __memcpy_chk(&DAT_00303080,data,data_len,0x800);
18
_DAT_00303060 = uVar11 + 0x10;
19
_DAT_00303064 = (uint)username_len;
20
_DAT_00303068 = uVar11 + 0x20 + (_DAT_00303064 & 0xfffffff0);
21
uVar9 = (ulong)DAT_0030302c;
22
bVar4 = false;
23
puVar14 = (undefined *)((ulong)_DAT_00303060 + lVar10);
24
while (iVar15 = DAT_00305880, username_len != 0) {
25
username_len = username_len + -1;
26
*puVar14 = *username;
27
username = username + (ulong)bVar16 * -2 + 1;
28
puVar14 = puVar14 + (ulong)bVar16 * -2 + 1;
29
}
30
_DAT_0030306c = (uint)serial_len;
31
_DAT_00303070 = _DAT_00303068 + 0x10 + (_DAT_0030306c & 0xfffffff0);
32
bVar3 = false;
33
bVar2 = false;
34
puVar14 = (undefined *)((ulong)_DAT_00303068 + lVar10);
35
while (uVar11 = DAT_00303030, serial_len != 0) {
36
serial_len = serial_len + -1;
37
*puVar14 = *serial;
38
serial = serial + (ulong)bVar16 * -2 + 1;
39
puVar14 = puVar14 + (ulong)bVar16 * -2 + 1;
40
}
Copied!
Ce qu'il y a à retenir de ce prologue, c'est que :
    Un espace de 0x10 * 4 = 64 octets nuls est réservé en 0x00303040
    Le contenu de data est copié en 0x00303080
    Notre username et serial sont copiés après le bloc alloué à data :
      username est en 0x00303080 + 0x400 + 0x10
      serial est en 0x00303080 + 0x400 + 0x10 + username_len + 0x20
Ensuite vient ce qui ressemble à un monstrueux switch case, qui débute par :
1
uVar1 = *(uint *)(data_ + (ulong)uVar11);
2
uVar5 = uVar1 >> 0x18;
Copied!
Le switch case est effectué sur la valeur de uVar5. uVar11 est un compteur, qui avance la plupart du temps de 4 en 4 (on lit des mots de 32 bits à chaque fois donc). Là ça commence à mettre la puce à l'oreille... Je vais renommer uVar11 "pc", uVar1 "opcode" et uVar5 "type" 😁
Voici un court extrait maintenant de quelques entrées du switch case :
1
if (type == 0) {
2
(&DAT_00303040)[(ulong)(opcode >> 0x14)] = (&DAT_00303040)[(ulong)(opcode >> 0x10 & 0xf)];
3
uVar4 = pc + 4;
4
}
5
else {
6
if (type == 0x1f) {
7
_DAT_00303054 = _DAT_00303054 ^ 0xf7e1560a;
8
uVar4 = pc + 4;
9
}
10
else {
11
if (type == 0x20) {
12
_DAT_00303048 = _DAT_00303048 ^ 0x6ddc660c;
13
uVar4 = pc + 4;
14
}
15
else {
16
if (type == 0x21) {
17
_DAT_00303074 = _DAT_00303074 ^ 0x13e40c56;
18
uVar4 = pc + 4;
19
}
20
[...]
Copied!
En effet, en fonction de la variable type, on va effectuer des opérations différentes, et on va incrémenter le pc de 4. Tout est clair dès à présent ; il s'agit là d'une mini machine virtuelle 32 bits qui lit et interprète un jeu d'instructions (qui ressemble un peu à du RISC ?). Le pc (Program Counter) donne la position courante dans la lecture de data qui n'est rien d'autre que le bytecode du programme que l'on exécute. La zone de 64 octets initialement nuls en 0x00303040 représenta, nous le verrons, les 16 registres du processeur et enfin la zone après les 1024 octets du programme, en 0x00303480, sert à des fins de mémoire (comme une heap).
Le type d'un opcode est donné par opcode >> 0x18, autrement ses 8 bits de poids fort, d'où le switch case à 256 entrées.
Analysons l'extrait. Pour type = 0, on prend opcode >> 0x14 et opcode >> 0x10 & 0xf, autrement dit les 4 bits et 4 bits suivant le type de l'opcode, et ces valeurs (entre 0 et 15 donc en décimal) sont des indices de registres (situés en 0x00303040). Cette instruction effectue donc ce qui s'apparente à un mov tel que l'on le noterait en assembleur classique x86 par exemple.
Les autres types (0x1f, 0x20, 0x21) semblent prendre un certain registre donné, et le XORer avec une constante donnée.
En fait, si l'on analyse tout le code, on se rend compte que ces étranges instructions de XOR très arbitraires occupent tous les types de 0x1f à 0xfd, ce qui diminue pas mal le nombre d'instructions réelles. On pourra coder un script pour extraire toutes ces instructions de XOR à partir du code généré par Ghidra.
Quant-aux autres instructions, il convient de les étudier chacune à la main. C'est un travail fastidieux et je vais directement passer à l'explication de l'ISA.
Tout d'abord, on pose quelques notations :
1
type_op k m p q
2
00000000 0000 0000 0000 000000000000
3
-----------------s ssss
4
5
s = pour les opérations de shift (5 bits)
6
. = concaténation de bits
Copied!
Ensuite on détaille chaque type d'opcode. J'appelle text la mémoire composée du programme et du heap, indicée à partir de zéro (début du programme). La fonction swap_endianness change le boutisme d'un mot de 32 bits, par exemple 0x11223344 devient 0x44332211. La fonction AES est en réalité l'instruction x86 aesenc, qui n'effectue qu'un seul "round" de chiffrement (j'ai perdu beaucoup de temps là-dessus !).
1
00 -> reg[k] = reg[m]
2
01 -> reg[k] = text[reg[m]]
3
02 -> reg[k] = m.p (8 bits)
4
03 -> text[reg[k]] = reg[m]
5
04 -> text[reg[k]] = text[reg[m]]
6
05 -> text[reg[k]] = m . p (8 bits)
7
06 -> sauvegarde PC+4; pc = k.m.p.q; (CALL)
8
07 -> jump_flag = reg[k] - reg[m] (CMP between two registers)
9
08 -> jump_flag = reg[k] - m.p (CMP with immediate)
10
09 -> jump to k.m.p.q if jump_flag = 0 (JE)
11
0a -> jump to k.m.p.q if jump_flag != 0 (JNE)
12
0b -> reg[k] = reg[k] + reg[m]
13
0c -> reg[k] = reg[k] + m.p
14
0d -> reg[k] = reg[k] * reg[m]
15
0e -> reg[k] = reg[k] * m.p
16
0f -> reg[k] += 1
17
10 -> reg[k] = reg[k] % reg[m]
18
11 -> reg[k] = reg[k] % m.p
19
12 -> reg[k] = reg[k] ^ reg[m]
20
13 -> reg[k] = reg[k] ^ m.p
21
14 -> jump to k.m.p.q if jump_flag < 0 (JL)
22
15 -> jump to k.m.p.q if jump_flag > 0 (JG)
23
16 -> reg[k] = reg[k] - reg[m]
24
17 -> reg[k] = reg[k] - m.p
25
18 -> jump to k.m.p.q
26
19 -> reg[k] = reg[k] >> s
27
1a -> reg[k] = PC + 4
28
1b -> reg[k] = swap_endianness(text[reg[m]])
29
1c -> text[reg[k]] = swap_endianness(reg[m])
30
1d -> reg[k] = reg[k] << s
31
1e -> text[reg[k]] = AES(text[reg[m]], text[reg[p]])
32
1f -> fd : reg[something] ^= some hardcoded value
33
fe -> récupère l'adresse de retour et jump (RET)
34
ff -> fin du prog et retourne reg[0]
Copied!
Bon, eh bien il semblerait que nous avons maintenant toutes les clés en main pour... s'amuser à coder un interpréteur, ainsi qu'un désassembleur !
Je passe sur les détails et je vous montre directement mon script. Cela allant de soi, la résolution ne se déroulant pas comme voulue, il a fallu aussi coder un débugger minimaliste (affichage des registres, de la mémoire et des breakpoints).
1
from binascii import hexlify as tohex, unhexlify as unhex
2
import re, struct, sys
3
import aes as crypto # Tiré de https://github.com/p4-team/crypto-commons/blob/master/crypto_commons/symmetrical/aes.py
4
5
disassembly_mode = False
6
debug_mode = False
7
8
if len(sys.argv) > 1 and sys.argv[1] == '--disassembly':
9
disassembly_mode = True
10
if len(sys.argv) > 1 and sys.argv[1] == '--debug':
11
debug_mode = True
12
disassembly_mode = True
13
14
decode = lambda u: (u[3] << 24) | (u[2] << 16) | (u[1] << 8) | u[0]
15
16
# Contient un dump du C généré par Ghidra pour la fonction principale
17
code = open('code.txt', 'r').read()
18
19
code = code.replace(' ', '').replace('\t', '').replace('\n', '')
20
s = re.findall(r'if\(type\_op\=\=((?:0x)?[0-9a-f]{1,3})\)\{(?:\_)?DAT\_003030[0-9a-f]{2}\=(?:\_)?DAT\_003030([0-9a-f]{2})\^0x([0-9a-f]{1,8})\;', code)
21
22
xor_opcodes = {}
23
for opcode, reg_i, magic in s:
24
xor_opcodes[eval(opcode)] = ((int(reg_i, 16) - 0x40) // 4, int(magic, 16))
25
26
def dis(pc, ins):
27
print('{:04x}'.format(pc) + ' ' + ins)
28
29
def read(text, offset):
30
return decode(text[offset:offset + 4])
31
32
def read128(text, offset):
33
return b''.join(bytes([text[offset + i]]) for i in range(16))
34
35
def write(text, offset, value):
36
text[offset] = value & 0xff
37
text[offset + 1] = (value >> 8) & 0xff
38
text[offset + 2] = (value >> 16) & 0xff
39
text[offset + 3] = (value >> 24) & 0xff
40
41
def write_bytes(text, offset, value):
42
for i in range(len(value)):
43
text[offset + i] = value[i]
44
45
def swap(x):
46
return ((x & 0xff) << 24) | (((x >> 8) & 0xff) << 16) | (((x >> 16) & 0xff) << 8) | ((x >> 24) & 0xff)
47
48
text = "6e 18 b0 17 c9 f5 bf 08 74 00 00 0a 37 52 0a 00 98 95 1c 00 74 03 00 06 88 1c 00 08 74 00 00 0a 3f 9e 08 00 56 94 1c 00 ad 06 18 0c c6 0f 20 02 88 02 00 06 89 97 0c 00 7c 02 08 0c c9 73 1c 00 5b 00 19 0c 7c 00 00 06 fa 1b 0c 00 f7 01 10 00 a7 f3 1f 0c 4b 19 10 0c fc 00 00 06 5a 41 0c 00 09 95 1c 00 8e 08 18 0c 28 0b 26 02 e8 02 00 06 64 34 7b ff 05 0c 00 02 af b4 68 ff de 24 f2 1a 05 88 f4 0c fd 5c dd 12 c0 49 df 13 b9 82 d0 1d 5a 3a de 13 ea 8f d0 1d c1 2f dd 13 37 86 d0 1d c0 1f dc 13 02 c4 ef 1b 64 91 ed 12 0a 33 fe 1c db 8a e1 1d 40 81 e1 19 28 fe e7 08 c8 00 00 15 1a 46 f0 0c a4 00 00 18 be e2 e2 c3 b8 2b f2 c1 04 a2 f0 c0 29 de f2 cf c7 18 fd d2 c1 5b c0 c2 f5 30 e5 ce 4c ec e7 c9 0c e3 d2 c8 fc d7 d9 ce 08 b2 cf ce 38 e3 d2 d9 5e 3d 4c 3f 4e 65 ff 1a c0 85 f4 0c eb 87 dd 12 bf 15 da 13 f2 82 d0 1d c3 2c db 13 be 80 d0 1d f0 32 dc 13 1c 8d d0 1d 88 49 dd 13 6f 26 ef 1b 54 13 ed 12 1f ad fe 1c cc 8d e1 1d d8 80 e1 19 23 f2 e7 08 48 01 00 15 44 44 f0 0c 24 01 00 18 11 07 a3 d4 ab bd a5 d8 54 f2 b3 d4 54 bb 33 d6 44 b0 93 d6 66 8d 83 d4 7a 9c 86 df a5 59 f7 d5 03 a0 82 d4 0d b4 86 df d6 f6 80 d7 21 2b 96 db 27 b5 92 dc 25 b3 c3 dd fd b3 c3 cc 75 78 f3 d4 97 b8 f6 d8 3a f3 83 d4 c0 13 94 d4 9b f2 90 ca d2 81 f3 d4 35 bf f7 d8 39 99 85 d4 37 b8 82 d8 b9 e4 94 d4 51 ba 96 d8 50 f4 90 ca 9e 13 f3 d4 19 bd f0 d8 51 33 85 d4 aa be 82 d8 dd 87 94 d4 27 be 97 d8 53 fc 90 ca 7b e7 f3 d4 15 b4 f1 d8 b5 e9 83 d4 63 b1 80 d8 35 d9 94 d4 a6 bc 90 d8 20 fc 90 ca f1 b8 f3 d4 c0 bd f2 d8 41 b3 85 d4 cb 4c 94 d4 d9 b6 91 d8 d1 f0 90 ca 83 17 f2 d4 b7 87 85 d4 e6 65 94 d4 36 b3 92 d8 ff f9 90 ca 31 7c 34 db 6d b3 31 dc 89 b0 c3 dd f9 b3 c3 cc 83 ee 5a 2a 34 f0 0f 0f 85 eb 02 0f 7f 82 0a 0f 44 7d 00 0f 9a 88 03 0f 1a ba 0f 0f 8b 89 0f 0f f4 2d 09 0f 97 0a 08 0f 66 55 0f 0f b3 23 0c 0f fb d6 0b 0f 33 83 09 0f 3f 94 0a 0f e3 c1 00 0f 5f c8 00 0f 88 d4 07 0f 23 5c 06 0f 43 de 0e 0f 25 fa af 62 7c 94 2c cf 8d a5 9c bc 67 70 de f4 c6 7e 70 01 e2 0c 70 08 98 02 00 0a e4 02 00 18 c5 0c 60 02 9b 85 37 00 54 65 36 0b 58 d6 30 0e e7 50 32 13 4b f6 3f 11 9c 4d 46 00 a3 a9 42 0b a6 06 41 11 7e 8a 41 0b f5 ff 54 01 0e 42 53 12 65 92 45 03 57 e2 69 0f ac 0e 61 08 9c 02 00 0a 7c ca 07 0f 99 f1 25 0f 88 02 00 06 fe 32 5b fe e8 59 fd 1a 2f 83 f4 0c 27 b8 dd 12 b0 a8 da 13 36 82 d0 1d 6b bc db 13 b5 84 d0 1d 28 cb dc 13 de 88 d0 1d 39 d2 dd 13 37 3d ef 1b e5 59 ed 12 0c 7d fe 1c 5e 89 e1 1d 74 8f e1 19 c9 f6 e7 08 34 03 00 15 16 4a f0 0c 10 03 00 18 9f bb fc df 30 78 8c dd 32 dc 9d dd b8 dd 8f d6 ad 2c 9f d6 da 35 88 dc 59 b0 99 dc 14 fe 89 da c6 b8 cc d7 81 24 fa d2 f9 6a fe da 92 b8 cc d7 56 ae cc df da b8 cc c5 dd b1 cc df e5 79 c6 23 36 01 30 02 01 c9 20 00 0a 27 23 0b 5f 56 22 01 1c 09 20 08 f0 03 00 09 af 99 23 08 a4 03 00 15 89 09 23 08 f8 03 00 14 7c 08 23 17 b8 03 00 18 68 67 26 08 f8 03 00 15 3e 17 26 08 f8 03 00 14 f9 73 25 17 3a 47 43 00 25 22 40 11 a4 1c 40 08 d4 03 00 09 b5 0c 21 0e 86 93 52 00 e8 03 00 18 29 d6 25 12 59 80 43 00 55 19 40 19 b6 02 41 0b c3 39 42 03 d7 54 35 0f 78 03 00 18 1b 13 00 02 fc 03 00 18 dc 00 00 02 a0 a1 31 fe"
49
text = [int(x, 16) for x in text.split(' ')]
50
51
reg = [0] * 16
52
53
if debug_mode or not disassembly_mode:
54
username = input('Username: ').encode()
55
serial = input('Serial: ').encode()
56
57
text += [0] * 0x400 # Heap
58
59
write_bytes(text, 0x400 + 0x20 + 0x10, serial)
60
61
reg[8] = 0x400 + 0x10 # Adresse username
62
reg[9] = len(username)
63
write_bytes(text, reg[8], username)
64
65
reg[10] = 0x400 + 0x20 + (reg[9] & 0xfffffff0) # Adresse serial
66
reg[11] = len(serial)
67
write_bytes(text, reg[10], serial)
68
69
reg[12] = reg[10] + 0x10 + (reg[11] & 0xfffffff0) # Adresse something
70
71
pc = 0
72
save_pc = []
73
jump_flag = 0
74
75
breakpoints = []
76
no_stop = False
77
78
while (debug_mode or not disassembly_mode) or (pc < len(text)):
79
type_op = decode(text[pc:pc + 4])
80
opcode = type_op >> 24
81
k = (type_op >> 20) & 0xf
82
m = (type_op >> 16) & 0xf
83
p = (type_op >> 12) & 0xf
84
q = type_op & 0xfff
85
kmpq = type_op & 0xffffff
86
imm = (m << 4) | p
87
s = (type_op >> 0xc) & 0x1f
88
89
if no_stop and pc in breakpoints:
90
no_stop = False
91
92
if debug_mode and not no_stop:
93
print("Regs: %s" % (','.join('{:08x}'.format(_) for _ in reg)))
94
print("Heap: %s" % tohex(bytes(text[0x400:0x400+0x300])))
95
96
if opcode == 0x00:
97
if disassembly_mode and not no_stop:
98
dis(pc, 'mov r%s, r%s' % (k, m))
99
if debug_mode or not disassembly_mode:
100
reg[k] = reg[m]
101
pc += 4
102
103
elif opcode == 0x01:
104
if disassembly_mode and not no_stop:
105
dis(pc, 'mov r%s, (char) [r%s]' % (k, m))
106
if debug_mode or not disassembly_mode:
107
reg[k] = read(text, reg[m]) & 0xff
108
pc += 4
109
110
elif opcode == 0x02:
111
if disassembly_mode and not no_stop:
112
dis(pc, 'mov r%s, %s' % (k, imm))
113
if debug_mode or not disassembly_mode:
114
reg[k] = imm
115
pc += 4
116
117
elif opcode == 0x03:
118
if disassembly_mode and not no_stop:
119
dis(pc, 'mov [r%s], (char) r%s' % (k, m))
120
if debug_mode or not disassembly_mode:
121
write_bytes(text, reg[k], bytes([reg[m] & 0xff]))
122
pc += 4
123
124
elif opcode == 0x04:
125
if disassembly_mode and not no_stop:
126
dis(pc, 'mov [r%s], [r%s]' % (k, m))
127
if debug_mode or not disassembly_mode:
128
write(text, reg[k], read(text, reg[m]))
129
pc += 4
130
131
elif opcode == 0x05:
132
if disassembly_mode and not no_stop:
133
dis(pc, 'mov [r%s], (char) %s' % (k, imm))
134
if debug_mode or not disassembly_mode:
135
write_bytes(text, reg[k], bytes([reg[m] & 0xff]))
136
pc += 4
137
138
elif opcode == 0x06:
139
if disassembly_mode and not no_stop:
140
dis(pc, 'call %s' % ('{:04x}'.format(kmpq)))
141
pc += 4
142
if debug_mode or not disassembly_mode:
143
save_pc.append(pc + 4)
144
pc = kmpq
145
146
elif opcode == 0x07:
147
if disassembly_mode and not no_stop:
148
dis(pc, 'cmp r%s, r%s' % (k, m))
149
if debug_mode or not disassembly_mode:
150
jump_flag = reg[k] - reg[m]
151
pc += 4
152
153
elif opcode == 0x08:
154
if disassembly_mode and not no_stop:
155
dis(pc, 'cmp r%s, %s' % (k, imm))
156
if debug_mode or not disassembly_mode:
157
jump_flag = reg[k] - imm
158
pc += 4
159
160
elif opcode == 0x09:
161
if disassembly_mode and not no_stop:
162
dis(pc, 'je %s' % ('{:04x}'.format(kmpq)))
163
if not debug_mode:
164
pc += 4
165
if debug_mode or not disassembly_mode:
166
if jump_flag == 0:
167
pc = kmpq
168
else:
169
pc += 4
170
171
elif opcode == 0x0a:
172
if disassembly_mode and not no_stop:
173
dis(pc, 'jne %s' % ('{:04x}'.format(kmpq)))
174
if not debug_mode:
175
pc += 4
176
if debug_mode or not disassembly_mode:
177
if jump_flag != 0:
178
pc = kmpq
179
else:
180
pc += 4
181
182
elif opcode == 0x0b:
183
if disassembly_mode and not no_stop:
184
dis(pc, 'add r%s, r%s' % (k, m))
185
if debug_mode or not disassembly_mode:
186
reg[k] = (reg[k] + reg[m]) & 0xffffffff
187
pc += 4
188
189
elif opcode == 0x0c:
190
if disassembly_mode and not no_stop:
191
dis(pc, 'add r%s, %s' % (k, imm))
192
if debug_mode or not disassembly_mode:
193
reg[k] = (reg[k] + imm) & 0xffffffff
194
pc += 4
195
196
elif opcode == 0x0d:
197
if disassembly_mode and not no_stop:
198
dis(pc, 'mul r%s, r%s' % (k, m))
199
if debug_mode or not disassembly_mode:
200
reg[k] = (reg[k] * reg[m]) & 0xffffffff
201
pc += 4
202
203
elif opcode == 0x0e:
204
if disassembly_mode and not no_stop:
205
dis(pc, 'mul r%s, %s' % (k, imm))
206
if debug_mode or not disassembly_mode:
207
reg[k] = (reg[k] * imm) & 0xffffffff
208
pc += 4
209
210
elif opcode == 0x0f:
211
if disassembly_mode and not no_stop:
212
dis(pc, 'inc r%s' % k)
213
if debug_mode or not disassembly_mode:
214
reg[k] = (reg[k] + 1) & 0xffffffff
215
pc += 4
216
217
elif opcode == 0x10:
218
if disassembly_mode and not no_stop:
219
dis(pc, 'mod r%s, r%s' % (k, m))
220
if debug_mode or not disassembly_mode:
221
reg[k] %= reg[m]
222
pc += 4
223
224
elif opcode == 0x11:
225
if disassembly_mode and not no_stop:
226
dis(pc, 'mod r%s, %s' % (k, imm))
227
if debug_mode or not disassembly_mode:
228
reg[k] %= imm
229
pc += 4
230
231
elif opcode == 0x12:
232
if disassembly_mode and not no_stop:
233
dis(pc, 'xor r%s, r%s' % (k, m))
234
if debug_mode or not disassembly_mode:
235
reg[k] ^= reg[m]
236
pc += 4
237
238
elif opcode == 0x13:
239
if disassembly_mode and not no_stop:
240
dis(pc, 'xor r%s, %s' % (k, imm))
241
if debug_mode or not disassembly_mode:
242
reg[k] ^= imm
243
pc += 4
244
245
elif opcode == 0x14:
246
if disassembly_mode and not no_stop:
247
dis(pc, 'jl %s' % ('{:04x}'.format(kmpq)))
248
if not debug_mode:
249
pc += 4
250
if debug_mode or not disassembly_mode:
251
if jump_flag < 0:
252
pc = kmpq
253
else:
254
pc += 4
255
256
elif opcode == 0x15:
257
if disassembly_mode and not no_stop:
258
dis(pc, 'jg %s' % ('{:04x}'.format(kmpq)))
259
if not debug_mode:
260
pc += 4
261
if debug_mode or not disassembly_mode:
262
if jump_flag > 0:
263
pc = kmpq
264
else:
265
pc += 4
266
267
elif opcode == 0x16:
268
if disassembly_mode and not no_stop:
269
dis(pc, 'sub r%s, r%s' % (k, m))
270
if debug_mode or not disassembly_mode:
271
reg[k] = (reg[k] - reg[m]) % 2**32
272
pc += 4
273
274
elif opcode == 0x17:
275
if disassembly_mode and not no_stop:
276
dis(pc, 'sub r%s, %s' % (k, imm))
277
if debug_mode or not disassembly_mode:
278
reg[k] = (reg[k] - imm) % 2**32
279
pc += 4
280
281
elif opcode == 0x18:
282
if disassembly_mode and not no_stop:
283
dis(pc, 'jmp %s' % ('{:04x}'.format(kmpq)))
284
pc += 4
285
if debug_mode or not disassembly_mode:
286
pc = kmpq
287
288
elif opcode == 0x19:
289
if disassembly_mode and not no_stop:
290
dis(pc, 'shr r%s, %s' % (k, s))
291
if debug_mode or not disassembly_mode:
292
reg[k] >>= s
293
pc += 4
294
295
elif opcode == 0x1a:
296
if disassembly_mode and not no_stop:
297
dis(pc, 'loadpc r%s' % k)
298
if debug_mode or not disassembly_mode:
299
reg[k] = pc + 4
300
pc += 4
301
302
elif opcode == 0x1b:
303
if disassembly_mode and not no_stop:
304
dis(pc, 'mov r%s, swap([r%s])' % (k, m))
305
if debug_mode or not disassembly_mode:
306
reg[k] = swap(read(text, reg[m]))
307
pc += 4
308
309
elif opcode == 0x1c:
310
if disassembly_mode and not no_stop:
311
dis(pc, 'mov [r%s], swap(r%s)' % (k, m))
312
if debug_mode or not disassembly_mode:
313
write(text, reg[k], swap(reg[m]))
314
pc += 4
315
316
elif opcode == 0x1d:
317
if disassembly_mode and not no_stop:
318
dis(pc, 'shl r%s, %s' % (k, s))
319
if debug_mode or not disassembly_mode:
320
reg[k] = (reg[k] << s) & 0xffffffff
321
pc += 4
322
323
elif opcode == 0x1e:
324
if disassembly_mode and not no_stop:
325
dis(pc, 'mov [r%s], aes([r%s], [r%s])' % (k, m, p))
326
if debug_mode or not disassembly_mode:
327
cipher = crypto.AES()
328
write_bytes(text, reg[k], cipher.AESENC(read128(text, reg[m]), read128(text, reg[p])))
329
pc += 4
330
331
elif opcode in xor_opcodes.keys():
332
reg_i, value = xor_opcodes[opcode]
333
if disassembly_mode and not no_stop:
334
dis(pc, 'xor r%s, %s' % (reg_i, hex(value)))
335
if debug_mode or not disassembly_mode:
336
reg[reg_i] ^= value
337
pc += 4
338
339
elif opcode == 0xfe:
340
if disassembly_mode and not no_stop:
341
dis(pc, 'ret\n')
342
pc += 4
343
if debug_mode or not disassembly_mode:
344
pc = save_pc.pop()
345
346
elif opcode == 0xff:
347
if disassembly_mode and not no_stop:
348
dis(pc, 'end\n')
349
pc += 4
350
if debug_mode or not disassembly_mode:
351
break
352
353
else:
354
print("[-] %s: opcode %s not supported" % (pc, hex(opcode)))
355
break
356
357
if debug_mode and not no_stop:
358
while True:
359
command = input('> ')
360
if command == '' or command == 'n':
361
break
362
elif command == 'c':
363
no_stop = True
364
break
365
elif command[:2] == 'b ':
366
breakpoints.append(int(command[2:], 16))
367
else:
368
print('Unknown command')
369
370
if not disassembly_mode or debug_mode:
371
print('[+] Program ended with %s' % reg[0])
Copied!
Voici le code désassemblé généré :
1
0000 sub r11, 1
2
0004 cmp r11, 255
3
0008 jne 0074
4
000c mov r0, r10
5
0010 mov r1, r12
6
0014 call 0374
7
0018 cmp r0, 1
8
001c jne 0074
9
0020 mov r0, r8
10
0024 mov r1, r12
11
0028 add r1, 128
12
002c mov r2, 0
13
0030 call 0288
14
0034 mov r0, r12
15
0038 add r0, 128
16
003c mov r1, r12
17
0040 add r1, 144
18
0044 call 007c
19
0048 mov r0, r12
20
004c mov r1, r0
21
0050 add r1, 255
22
0054 add r1, 1
23
0058 call 00fc
24
005c mov r0, r12
25
0060 mov r1, r12
26
0064 add r1, 128
27
0068 mov r2, 96
28
006c call 02e8
29
0070 end
30
31
0074 mov r0, 0
32
0078 end
33
34
007c loadpc r15
35
0080 add r15, 72
36
0084 xor r13, r13
37
0088 xor r13, 244
38
008c shl r13, 8
39
0090 xor r13, 227
40
0094 shl r13, 8
41
0098 xor r13, 210
42
009c shl r13, 8
43
00a0 xor r13, 193
44
00a4 mov r14, swap([r15])
45
00a8 xor r14, r13
46
00ac mov [r15], swap(r14)
47
00b0 shl r14, 24
48
00b4 shr r14, 24
49
00b8 cmp r14, 127
50
00bc jg 00c8
51
00c0 add r15, 4
52
00c4 jmp 00a4
53
00c8 xor r12, 0x4110a870
54
00cc xor r11, 0xe2c7c3c3
55
00d0 xor r3, 0x3a7ac323
56
00d4 xor r0, 0x92201356
57
00d8 xor r10, 0x2934e85a
58
00dc xor r9, 0x93048f8b
59
00e0 xor r13, 0xe46099e2
60
00e4 xor r14, 0xd6632aca
61
00e8 xor r4, 0xd3bda74e
62
00ec xor r13, 0xe46099e2
63
00f0 xor r13, 0xe46099e2
64
00f4 xor r14, 0xbfb56256
65
00f8 xor r3, 0xf5acad7d
66
00fc loadpc r15
67
0100 add r15, 72
68
0104 xor r13, r13
69
0108 xor r13, 161
70
010c shl r13, 8
71
0110 xor r13, 178
72
0114 shl r13, 8
73
0118 xor r13, 195
74
011c shl r13, 8
75
0120 xor r13, 212
76
0124 mov r14, swap([r15])
77
0128 xor r14, r13
78
012c mov [r15], swap(r14)
79
0130 shl r14, 24
80
0134 shr r14, 24
81
0138 cmp r14, 127
82
013c jg 0148
83
0140 add r15, 4
84
0144 jmp 0124
85
0148 xor r9, 0x93da34fd
86
014c xor r2, 0xd24eba88
87
0150 xor r9, 0x93da34fd
88
0154 xor r1, 0x71e85cfb
89
0158 xor r1, 0x71e85cfb
90
015c xor r9, 0x93da34fd
91
0160 xor r7, 0xb0f84472
92
0164 xor r8, 0xfcb4cd4a
93
0168 xor r9, 0x93da34fd
94
016c xor r7, 0xb0f84472
95
0170 xor r6, 0xf71a0cab
96
0174 xor r0, 0xaca57ad
97
0178 xor r13, 0xd05cd042
98
017c xor r5, 0xe4573279
99
0180 xor r3, 0x19f0505b
100
0184 xor r9, 0x93da34fd
101
0188 xor r2, 0xd24eba88
102
018c xor r9, 0x93da34fd
103
0190 xor r9, 0x93da34fd
104
0194 xor r10, 0xbc777df5
105
0198 xor r9, 0x93da34fd
106
019c xor r2, 0xd24eba88
107
01a0 xor r9, 0x93da34fd
108
01a4 xor r2, 0xd24eba88
109
01a8 xor r9, 0x93da34fd
110
01ac xor r2, 0xd24eba88
111
01b0 xor r10, 0xbc777df5
112
01b4 xor r9, 0x93da34fd
113
01b8 xor r2, 0xd24eba88
114
01bc xor r9, 0x93da34fd
115
01c0 xor r2, 0xd24eba88
116
01c4 xor r9, 0x93da34fd
117
01c8 xor r2, 0xd24eba88
118
01cc xor r10, 0xbc777df5
119
01d0 xor r9, 0x93da34fd
120
01d4 xor r2, 0xd24eba88
121
01d8 xor r9, 0x93da34fd
122
01dc xor r2, 0xd24eba88
123
01e0 xor r9, 0x93da34fd
124
01e4 xor r2, 0xd24eba88
125
01e8 xor r10, 0xbc777df5
126
01ec xor r9, 0x93da34fd
127
01f0 xor r2, 0xd24eba88
128
01f4 xor r9, 0x93da34fd
129
01f8 xor r9, 0x93da34fd
130
01fc xor r2, 0xd24eba88
131
0200 xor r10, 0xbc777df5
132
0204 xor r9, 0x93da34fd
133
0208 xor r9, 0x93da34fd
134
020c xor r9, 0x93da34fd
135
0210 xor r2, 0xd24eba88
136
0214 xor r10, 0xbc777df5
137
0218 xor r0, 0xaca57ad
138
021c xor r13, 0xd05cd042
139
0220 xor r5, 0xe4573279
140
0224 xor r3, 0x19f0505b
141
0228 xor r0, 0x480035e4
142
022c inc r0
143
0230 inc r0
144
0234 inc r0
145
0238 inc r0
146
023c inc r0
147
0240 inc r0
148
0244 inc r0
149
0248 inc r0
150
024c inc r0
151
0250 inc r0
152
0254 inc r0
153
0258 inc r0
154
025c inc r0
155
0260 inc r0
156
0264 inc r0
157
0268 inc r0
158
026c inc r0
159
0270 inc r0
160
0274 inc r0
161
0278 xor r6, 0x7e0233a2
162
027c xor r0, 0x92201356
163
0280 xor r6, 0x66601391
164
0284 xor r15, 0x727c2426
165
0288 mov r7, (char) [r0]
166
028c cmp r7, 0
167
0290 jne 0298
168
0294 jmp 02e4
169
0298 mov r6, 0
170
029c mov r3, r7
171
02a0 add r3, r6
172
02a4 mul r3, 13
173
02a8 xor r3, 37
174
02ac mod r3, 255
175
02b0 mov r4, r6
176
02b4 add r4, r2
177
02b8 mod r4, 16
178
02bc add r4, r1
179
02c0 mov r5, (char) [r4]
180
02c4 xor r5, r3
181
02c8 mov [r4], (char) r5
182
02cc inc r6
183
02d0 cmp r6, 16
184
02d4 jne 029c
185
02d8 inc r0
186
02dc inc r2
187
02e0 call 0288
188
02e4 ret
189
190
02e8 loadpc r15
191
02ec add r15, 72
192
02f0 xor r13, r13
193
02f4 xor r13, 170
194
02f8 shl r13, 8
195
02fc xor r13, 187
196
0300 shl r13, 8
197
0304 xor r13, 204
198
0308 shl r13, 8
199
030c xor r13, 221
200
0310 mov r14, swap([r15])
201
0314 xor r14, r13
202
0318 mov [r15], swap(r14)
203
031c shl r14, 24
204
0320 shr r14, 24
205
0324 cmp r14, 127
206
0328 jg 0334
207
032c add r15, 4
208
0330 jmp 0310
209
0334 xor r7, 0xb0f84472
210
0338 xor r5, 0xe4573279
211
033c xor r5, 0xe4573279
212
0340 xor r1, 0x71e85cfb
213
0344 xor r1, 0x71e85cfb
214
0348 xor r13, 0xd05cd042
215
034c xor r13, 0xd05cd042
216
0350 xor r14, 0x2802f673
217
0354 xor r6, 0xf71a0cab
218
0358 xor r10, 0x2934e85a
219
035c xor r14, 0x2802f673
220
0360 xor r6, 0xf71a0cab
221
0364 xor r7, 0xb0f84472
222
0368 xor r5, 0xb1653a57
223
036c xor r7, 0xb0f84472
224
0370 xor r7, 0x8bb5b038
225
0374 mov r3, 0
226
0378 mov r2, r0
227
037c add r2, r3
228
0380 mov r2, (char) [r2]
229
0384 cmp r2, 0
230
0388 je 03f0
231
038c cmp r2, 57
232
0390 jg 03a4
233
0394 cmp r2, 48
234
0398 jl 03f8
235
039c sub r2, 48
236
03a0 jmp 03b8
237
03a4 cmp r2, 102
238
03a8 jg 03f8
239
03ac cmp r2, 97
240
03b0 jl 03f8
241
03b4 sub r2, 87
242
03b8 mov r4, r3
243
03bc mod r4, 2
244
03c0 cmp r4, 1
245
03c4 je 03d4
246
03c8 mul r2, 16
247
03cc mov r5, r2
248
03d0 jmp 03e8
249
03d4 xor r2, r5
250
03d8 mov r4, r3
251
03dc shr r4, 1
252
03e0 add r4, r1
253
03e4 mov [r4], (char) r2
254
03e8 inc r3
255
03ec jmp 0378
256
03f0 mov r0, 1
257
03f4 jmp 03fc
258
03f8 mov r0, 0
259
03fc ret
Copied!
On peut voir qu'il y a des zones étranges où les fameuses opérations XOR sont répétées sans aucun sens. C'est là que j'ai tiqué : ces instructions ne servent à rien dans la logique du programme. Elles sont en réalité ici uniquement à des fins d'obfuscation. Prenons par exemple cette routine :
1
02e8 loadpc r15
2
02ec add r15, 72
3
02f0 xor r13, r13
4
02f4 xor r13, 170
5
02f8 shl r13, 8
6
02fc xor r13, 187
7
0300 shl r13, 8
8
0304 xor r13, 204
9
0308 shl r13, 8
10
030c xor r13, 221
11
0310 mov r14, swap([r15])
12
0314 xor r14, r13
13
0318 mov [r15], swap(r14)
14
031c shl r14, 24
15
0320 shr r14, 24
16
0324 cmp r14, 127
17
0328 jg 0334
18
032c add r15, 4
19
0330 jmp 0310
20
0334 xor r7, 0xb0f84472
21
0338 xor r5, 0xe4573279
22
033c xor r5, 0xe4573279
23
0340 xor r1, 0x71e85cfb
24
0344 xor r1, 0x71e85cfb
25
0348 xor r13, 0xd05cd042
26
034c xor r13, 0xd05cd042
27
0350 xor r14, 0x2802f673
28
0354 xor r6, 0xf71a0cab
29
0358 xor r10, 0x2934e85a
30
035c xor r14, 0x2802f673
31
0360 xor r6, 0xf71a0cab
32
0364 xor r7, 0xb0f84472
33
0368 xor r5, 0xb1653a57
34
036c xor r7, 0xb0f84472
35
0370 xor r7, 0x8bb5b038
Copied!
Ce qu'on fait ici, c'est qu'on charge la valeur de PC+4 dans r15 et on lui ajoute 72 : r15 contient l'adresse 0x0334. Puis une boucle va venir déchiffrer ce tableau de mots à l'aide de swaps et de xors. Difficile donc d'analyser directement ce code de façon statique. Heureusement, je peux maintenant poser des points d'arrêts qui me permettront de suivre le comportement réel du programme en direct !
Aperçu d'un début de session de debug :
Le "main" du programme se décompose en plusieurs calls. Le premier permet de s'assurer que la longueur du serial est de 256 octets. Le deuxième n'est pas très difficile à analyser ; il s'assure que le serial soit en fait de l'hexadécimal et le décode, en le stockant un peu plus loin dans la mémoire. Les trois calls suivants sont des routines obfusquées.
Je passe les étapes de reconstitution de la logique des routines, c'était un travail assez fastidieux car des erreurs pouvaient se cacher à tous les niveaux et n'étaient pas toujours évidentes à corriger (mauvaise compréhension du binaire et donc de la logique de certaines instructions de l'ISA, erreur d'implémentation, mauvaise compréhension/visualisation des routines...).
Voici globalement les étapes du programme :
    Le serial doit faire 256 caractères hexadécimaux, puis est décodé
    On dérive une clé de 96 octets à partir de notre username, à l'aide de boucles de multiplications et de xors
    On effectue 32 itérations d'une série de tours de chiffrement AES de différents blocs du serial, dont les clés sont aussi des blocs du serial
    Le résultat obtenu est comparé à la clé de 96 octets dérivée de l'username
Première remarque : y'a 32 octets qui partent dans le vent. Du coup, on peut générer plein de clés valides en paddant le buffer de 96 octets avec des octets arbitraires (ça tombe bien, le serveur demande à chaque fois deux clés valides pour l'username donné !)
Deuxième remarque : il faut faire très attention à l'ordre dans lesquels sont faits les aesenc parce que le serial se réécrit par dessus à chaque itération, et il faut le prendre en compte pour l'algo inverse.
Ceci étant dit, il ne reste plus qu'à coder le fameux keygen.
1
from binascii import unhexlify as unhex, hexlify as tohex
2
from pwn import *
3
import aes as crypto
4
5
def aesd(a, b):
6
aes = crypto.AES()
7
return aes.AESDEC(a, b)
8
9
def write(text, offset, value):
10
for i in range(len(value)):
11
text[offset + i] = value[i]
12
13
def invert(serial):
14
for i in range(32):
15
old_serial = serial[:16][:]
16
write(serial, 0, aesd(serial[16:16+16], serial[96:96+16]))
17
write(serial, 16, aesd(serial[32:32+16], serial[96:96+16]))
18
old_serial48 = serial[48:48+16][:]
19
write(serial, 48, aesd(serial[64:64+16], serial[112:112+16]))
20
write(serial, 32, aesd(old_serial48, serial[48:48+16]))
21
write(serial, 64, aesd(serial[80:80+16], serial[112:112+16]))
22
write(serial, 80, aesd(old_serial, serial[:16]))
23
24
def keyge