Ce programme est trop grand mais il est inutile. Pouvez-vous extraire le flag ?
Solution
Ici, on ne peut pas débugger directement. On va passer par une attaque par bruteforce grâce à un side-channel : le nombre d'instructions CPU.
Quand on décompile le binaire, on s'aperçoit que les caractères sont vérifiés 1 par 1 et dès qu'un est mauvais, le processus se termine. Autrement dit, si on a la bonne lettre, on fera un tour de boucle en plus et nécessairement, on exécutera plus d'instructions. C'est ça qu'on peut détecter :
En jaune, on déduit que notre input doit faire 15 de longueur.
En bleu, c'est la condition qui quitte la fonctionne si le caractère n'est pas le bon.
On va passer par valgrind pour compter le nombre d'instructions puisque perf stat ne fonctionne pas sur la majorité des VM (pour ceux comme moi qui sont sur VMWare ou autre).
#!/usr/bin/python3
import argparse
import sys
import re
import subprocess
import tempfile
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
not_allowed_chars = ['"', "'", "(", ")", ">", "<", "`", "|", "\\",
"#", ";", "&"]
punctuation = list(punctuation.strip(" "))
for char in not_allowed_chars:
punctuation.remove(char)
punctuation = ''.join(punctuation)
def make_bold(text, index):
return text[:index] + '\033[1m' + text[index] + '\033[0m' + text[index+1:]
def get_charset(code):
return "NZKqdWgZZUfEUiT9lV2f"
def get_instruction_count(test_str, binary_filename):
with tempfile.NamedTemporaryFile() as tmp:
command = f'echo "{test_str}" | valgrind --tool=callgrind --callgrind-out-file={tmp.name}\
{binary_filename}'
try:
with subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) as valout:
valout = valout.stderr.read()
valout = int(re.findall('Collected : \d+',
valout.decode())[0][12:])
return valout
except IndexError:
print('here')
pass
def find_char_at(test_str, location, binary_filename, verbose, charset_code):
if verbose:
print("Testing: ", make_bold(test_str, location), "at", location)
charset = get_charset(charset_code)
maximum = 0
bestchoice = ""
for char in charset:
test = test_str[:location] + char + test_str[location+1:]
val = get_instruction_count(test, binary_filename)
if val is not None:
if val > maximum:
maximum = val
bestchoice = test
if verbose:
print(" ", char, ":", val)
return bestchoice
def find_length(binary_filename, maxlen, verbose):
maximum = -1
init_counts = get_instruction_count("a", binary_filename)
length = -1
for i in range(1, maxlen+1):
val = get_instruction_count("a"*i, binary_filename)
if verbose:
print("a"*i, ":", val)
if val is not None:
if val > maximum:
maximum = val
length = i
if init_counts != maximum:
return length
def find_string(binary_filename, maxlen=35,
verbose=False, reverse=False,
lengthgiven=False, length=0,
charset_code=0,
flag_format=""):
if not lengthgiven:
length = find_length(binary_filename, maxlen, verbose)
if length is None:
print("[taCTF] I couldn't guess the length, sorry. Try -l LENGTH.")
sys.exit()
print("Length guess:", length)
length_diff = length - len(flag_format)
candidate = flag_format + 'a'*length_diff
if not reverse:
for i in range(len(flag_format), length):
candidate = find_char_at(candidate, i, binary_filename, verbose,
charset_code)
print(candidate)
else:
for i in range(length-1, len(flag_format)-1, -1):
candidate = find_char_at(candidate, i, binary_filename, verbose,
charset_code)
print(candidate)
return candidate
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Run an instruction counting based timing attack on a given binary.')
parser.add_argument('filename', type=str,
help='The file to run taCTF against')
parser.add_argument('-v', '--verbose', action='store_true',
help="Verbose mode, prints out what it's up to at every iteration")
parser.add_argument('-r', '--reverse', action='store_true',
help='Try the timing attack starting at the end of the string.')
parser.add_argument('-f', '--flag-format', type=str, default="",
help='Flag format: first N known characters in string.')
parser.add_argument('-ml', '--max-length', type=int, default=35,
help='Maximum length to check strings till, if length unknown.')
parser.add_argument('-l', '--length', type=int,
help='Length of string, if known.')
parser.add_argument('-c', '--charset', type=int, default=0,
help='Set the charset code. Default is 0 (all chars).')
args = parser.parse_args()
lengthgivenbool = False
if args.length is not None:
lengthgivenbool = True
find_string(args.filename, maxlen=args.max_length,
verbose=args.verbose,
reverse=args.reverse,
lengthgiven=lengthgivenbool, length=args.length,
charset_code=args.charset,
flag_format=args.flag_format)
valgrind ne fonctionne pas dans un terminal émulé (comme celui-ci de VSCode), veillez bien à lancer le script depuis votre "vrai" terminal pour ne pas avoir de surprise de segfault.
On lance avec python3 taCTF.py "./task2" -l 15.
┌──(kali㉿kali)-[~/Documents/Reverse]
└─$ python3 taCTF.py "./task2" -l 15 130 ⨯
Length guess: 15
Kaaaaaaaaaaaaaa
Kdaaaaaaaaaaaaa
Kdgaaaaaaaaaaaa
KdgWaaaaaaaaaaa
KdgWZaaaaaaaaaa
KdgWZUaaaaaaaaa
KdgWZUZaaaaaaaa
KdgWZUZfaaaaaaa
KdgWZUZfUaaaaaa
KdgWZUZfUEaaaaa
KdgWZUZfUEiaaaa
KdgWZUZfUEi9aaa
KdgWZUZfUEi9Taa
KdgWZUZfUEi9Tla
KdgWZUZfUEi9Tl2
┌──(kali㉿kali)-[~/Documents/Reverse]
└─$ ./task2
give me The correct flag: KdgWZUZfUEi9Tl2
Good Job
Go ahead and submit
StarHack{KdgWZUZfUEi9Tl2}
J'ai simplement modifié le script de pour mettre notre charset et éviter de tester des caractères intuiles, ça donne :