⭐La Barre Fixe
404CTF{x86_64-iGnor3s-5TuFF}
Catégorie: Stegano Difficulté: easy Flag: -
Challenge
Description
C'est le grand moment, tous les yeux sont rivés sur vous. Vous vous apprêtez à commencer votre prestation sur l'épreuve de la barre fixe. Mais quelques instants avant de vous élancer, vous remarquez un détail intriguant. Certaines choses sont là... alors qu'elles ne devraient pas l'être... Un message peut être ?
Solution
On commence par chercher ce que l'on a entre les mains, donc soit avec un coup de commande file
, soit via un site en ligne comme Metadata2go.

Il s'agit d'un exécutable x86 en 64bits. On va donc le décompiler pour savoir ce qu'il fait, ici, j'utilise IDA. Direction la fonction _start
qui est le point d'entrée du programme.
On voit plein d'instructions db
. Elles signifient que le logiciel n'a pas su les comprendre, car elles ne sont pas censées être là.

On trouve toujours les mêmes valeurs hexadécimales : 65, 2E et 3E. Il s'agit de préfixes pour indiquer les segments à utiliser.
Pas besoin de comprendre ce que ça veut dire, IDA nous dit qu'elles n'ont rien à faire ici donc c'est surement une bonne piste à explorer vu l'énoncé du challenge.
Recompilation
Pour aller un peu plus loin, on peut s'amuser en Python à décompiler le binaire pour lister chaque instruction, puis à les recompiler. On pourra ainsi voir les différences puisque les instructions inutiles vont partir d'elles-mêmes.
from elftools.elf.elffile import ELFFile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from keystone import Ks, KS_ARCH_X86, KS_MODE_64
def compare(ops):
ks = Ks(KS_ARCH_X86, KS_MODE_64)
md = Cs(CS_ARCH_X86, CS_MODE_64)
print(f'{"INSTRUCTION":^35}\t{"ORIGINAL":^25}\t{"RECOMPILÉ":^20}')
for i in md.disasm(ops, 0):
instruction = f'{i.mnemonic}\t{i.op_str}'
op, _ = ks.asm(instruction)
decompiled = ' '.join([f'{n:0>2x}' for n in i.bytes])
recompiled = ' '.join([f'{n:0>2x}' for n in bytes(op)])
print(f'{instruction:<35}\t{decompiled:<25}\t{recompiled}')
with open('./chall', 'rb') as f:
elf = ELFFile(f)
code = elf.get_section_by_name('.text')
ops = code.data()
compare(ops)
# INSTRUCTION ORIGINAL RECOMPILÉ
# push 0 65 6a 00 6a 00
# mov rax, 1 2e 3e 48 c7 c0 01 00 00 00 48 c7 c0 01 00 00 00
# mov rdi, 1 48 c7 c7 01 00 00 00 48 c7 c7 01 00 00 00
# mov rsi, rsp 2e 3e 48 89 e6 48 89 e6
# mov rdx, 1 65 48 c7 c2 01 00 00 00 48 c7 c2 01 00 00 00
# mov byte ptr ds:[rsp], 0x55 2e 3e c6 04 24 55 c6 04 24 55
# syscall 2e 3e 0f 05 0f 05
# mov byte ptr gs:[rsp], 0x6e 65 c6 04 24 6e 65 c6 04 24 6e
# syscall 65 0f 05 0f 05
# mov byte ptr ds:[rsp], 0x4d 65 3e c6 04 24 4d c6 04 24 4d
# syscall 65 2e 0f 05 0f 05
# ...
# syscall 65 0f 05 0f 05
# mov byte ptr ds:[rsp], 0x61 2e 3e c6 04 24 61 c6 04 24 61
# syscall 40 65 3e 0f 05 0f 05 40 0f 05 0f 05
# ...
On a confirmation que le début de chaque instruction est étrange, et en plus de nos trois valeurs, on voit également 40
qui disparaît.
Ça nous fait un total de 4 valeurs "fantômes", et le plus bizarre, c'est qu'elles semblent toutes respecter un certain ordre.
2E
apparaît toujours avant3E
65
apparaît toujours avant2E
40
apparaît toujours avant65
Ce qui donne l'ordre : 40 65 2E 3E
Éclair de génie
On a 4 valeurs qui apparaissent ou non, dans un ordre précis. Et si elles représentaient les bits du message caché que l'on cherche ?
Le flag commence par 404
, ce qui donne en binaire : 0011 0100 0011 0000 0011 0100
Si l'on prend les instructions une à une et que l'on regarde si nos valeurs y sont :
VALEURS | HEXADECIMAL | ASSEMBLEUR
| |
-- 65 -- -- | 6a 00 | push 0
0 1 0 0 | |
| |
-- -- 2e 3e | 48 c7 c0 01 00 00 00 | mov rax, 1
0 0 1 1 | |
| |
-- -- -- -- | 48 c7 c7 01 00 00 00 | mov rdi, 1
0 0 0 0 | |
| |
-- -- 2e 3e | 48 89 e6 | mov rsi, rsp
0 0 1 1 | |
Ça ressemble à notre flag en binaire. La première instruction correspond aux 4 bits bas du caractère "4", puis la seconde instruction aux 4 bits hauts.
Pour chaque instruction, on va donc écrire un bloc de 4 bits en fonction de si les valeurs fantômes sont présentes. Ensuite, il faudra simplement assembler 2 blocs par 2 blocs pour reformer nos octets (en veillant à ce que le 1ᵉʳ bloc corresponde bien aux bits de poids faibles).
Script de résolution
from elftools.elf.elffile import ELFFile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
def decompile(ops):
md = Cs(CS_ARCH_X86, CS_MODE_64)
decompiled = []
for i in md.disasm(ops, 0):
decompiled.append(i.bytes)
return decompiled
with open('./chall', 'rb') as f:
elf = ELFFile(f)
code = elf.get_section_by_name('.text')
ops = code.data()
instructions = decompile(ops)
blocs = []
for i in instructions:
bloc = ''
for prefix in [0x40, 0x65, 0x2e, 0x3e]:
if i.startswith(bytes([prefix])):
bloc += '1'
i = i[1:]
else:
bloc += '0'
blocs.append(bloc)
print(blocs)
print(bytes([int(blocs[i+1] + blocs[i], 2) for i in range(0, len(blocs)//2*2, 2)]))
# ['0100', '0011', '0000', '0011', '0100', '0011', '0011', '0100',
# '0100', '0101', '0110', '0100', '1011', '0111', '1000', '0111',
# '1000', '0011', '0110', '0011', '1111', '0101', '0110', '0011',
# '0100', '0011', '1101', '0010', '1001', '0110', '0111', '0100',
# '1110', '0110', '1111', '0110', '0010', '0111', '0011', '0011',
# '0011', '0111', '1101', '0010', '0101', '0011', '0100', '0101',
# '0101', '0111', '0110', '0100', '0110', '0100', '1101', '0111',
# '0000', '0000', '0000']
# b'404CTF{x86_64-iGnor3s-5TuFF}\x00'
Dernière mise à jour
Cet article vous a-t-il été utile ?