SSEcret
FCSC
Reverse Engineering
SSEcret
Le binaire est un ELF 64 bits et prend un secret en paramêtre
plean@ubuntu:~/ctf/fcsc-2020/reverse/SSEcreT$ file ssecret.bin
ssecret.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=bc0fff26311919cb6ebc99a23c993763db59299e, stripped
plean@ubuntu:~/ctf/fcsc-2020/reverse/SSEcreT$ ./ssecret.bin
Usage: ./ssecret.bin <secret>
Énumération:
Nous lançons DIA et analysons le programme.
__int64 __fastcall main(int argc, char **argv, char **envp)
{
void *v4; // rax@4
__int64 v5; // [sp+0h] [bp-18h]@4
__int64 v6; // [sp+8h] [bp-10h]@1
v6 = *MK_FP(__FS__, 40LL);
if ( argc == 2 )
{
v4 = sub_400860(argv[1], strlen(argv[1]), &v5);
sub_601050(v4, v5);
}
else
{
__printf_chk(1LL, "Usage: %s <secret>\n", *argv);
}
return 0LL;
}
Jusqu’à là rien de bien complexe nous passons à la fonction sub_400860
.
Celle-ci un peu plus longue est rapidement reconnue, en effet elle effectue un b64decode
sur la chaine passé en paramêtre, en revoie le résultat et stock le nombre de bytes écrit dans v5
.
Le seconde fonction quand a elle est bien plus complexe. Elle peut être découpé en deux parties:
La première partie charge deux valeurs, une dans rax
et une dans rbx
, puis les xor avec les 0x10
premiers bytes de l’entrée utilisateur, une fois base64 decodé, les highbits pour rbx
et les lowbits pour rax
.
Puis l’instruction popcnt
est utilisé sur les deux registres, cette operation va compter le nombre de bits non zéros et ce résultat sera & 1
.
Au final si les deux résultats sont égaux, c’est à dire que les nombres de bits sont tous les deux soit pair, soit impair, on jump au block suivant, sinon le nième bit de xmm3
est set à 1
.
loc_601373:
psrlq xmm2, 1
mov rax, 64CE525FF893C2Ch
mov rbx, 6CE07930082C6E66h
movq xmm1, rax
pinsrq xmm1, rbx, 1
pand xmm1, xmm0
vmovq rax, xmm1
vpextrq rbx, xmm1, 1
popcnt rax, rax
and rax, 1
popcnt rbx, rbx
and rbx, 1
xor rax, rbx
test rax, rax
jz short loc_6013C5
pxor xmm3, xmm2
loc_6013C5:
Ce block se répète un certain nombre de fois avec des valeurs différentes pour rax
et rbx
à chaques fois, jusqu’à ce que tout les bits de xmm3
soient set ou non.
Une fois cela fait deux valeurs sont à nouveaux chargé dans rax
et rbx
et sont comparés à xmm3
, si les valeurs sont différentes on exit
le programme.
Il nous faut donc trouver 16
bytes pour lesquels le check final sera juste.
Pour cela on écrit un petit algorithme qui calculera l’input pour nous.
blank = []
final_list = list(map(int, bin(final)[2:].zfill(128))) # final is the final number we want to reach
arr = [
]
for _ in range(128):
arr.append([])
for nb1, nb2 in nbs: # ns is all the couples for rax and rbx
res = (popcount(nb1 & 0x0) & 1) ^ (popcount(nb2 & 0x0) & 1)
blank.append(res)
for i in range(128):
tmp = 1 << i
arr[i].append((popcount(nb1 & tmp) & 1) ^
(popcount(nb2 & (tmp >> 64)) & 1) ^ res)
pass
usables = [ [arr[i], 1 << i] for i in range(128) ]
transform = [ blank, 0x0 ]
for i in range(128):
for item in usables:
if transform[0][i] ^ item[0][i] == final_list[i]:
for j in range(128):
transform[0][j] ^= item[0][j]
transform[1] ^= item[1]
break
else:
if transform[0][i] != final_list[i]:
print("[-] no solution")
exit()
continue
new_usables = []
for item in usables:
if item[0][i]:
for item2 in usables:
if item2[0][i]:
item_tmp = [ [], item[1] ^ item2[1] ]
for j in range(128):
item_tmp[0].append(item[0][j] ^ item2[0][j])
new_usables.append(item_tmp)
break
else:
new_usables.append(item)
usables = new_usables
assert cmp(final_list, transform[0])
print("[+] input:", long_to_bytes(transform[1])[::-1])
S’ensuit la génération d’une clé aes puis le déchiffrage du block de code suivant avec. Une fois le block déchiffré, on jump dessus et on continue.
Les blocks suivant sont à chaques fois similaire au précédents, si ce n’est les valeurs pour rax
et rbx
et le check final, jusqu’au block affichant le flag.
Afin d’automatiser tout ceci, nous utilison un script gdb pour récupérer les valeurs qui seront notre final
et nos nbs
gdb.execute("tb *0x400588")
gdb.execute('r "%s"' % b64encode(inpt).decode())
for i in range(itr):
addr = start + 0x2c00 * i
gdb.execute("tb *%x" % addr)
gdb.execute("c")
gdb.execute("b *%x" % (start + 0x2bee + 0x2c00 * i))
gdb.execute("c")
gdb.execute("c 0x2bf")
addr = start + 0x2c00 * itr
gdb.execute("tb *%x" % addr)
gdb.execute("c")
nbs = []
for i in range(64):
nb1 = gdb.execute("x/i %x" % (addr + 0x46 + i * (0xe8 - 0x96)), to_string=True)
nb2 = gdb.execute("x/i %x" % (addr + 0x50 + i * (0xe8 - 0x96)), to_string=True)
nbs.append((int(nb1[nb1.index(',') + 1:-1], 16), int(nb2[nb2.index(',') + 1:-1], 16)))
for i in range(64):
nb1 = gdb.execute("x/i %x" % (addr + 0x14e3 + i * (0xe8 - 0x96)), to_string=True)
nb2 = gdb.execute("x/i %x" % (addr + 0x14ed + i * (0xe8 - 0x96)), to_string=True)
nbs.append((int(nb1[nb1.index(',') + 1:-1], 16), int(nb2[nb2.index(',') + 1:-1], 16)))
rax = gdb.execute("x/i %x" % (addr + 0x2963), to_string=True)
rbx = gdb.execute("x/i %x" % (addr + 0x296D), to_string=True)
final = (int(rbx[rbx.index(',') + 1:-1], 16) << 64) | int(rax[rax.index(',') + 1:-1], 16)
Nous mettons les deux parties en commun et lançons le script et resolvons le challenge.