Secure Signing
4 minutos de lectura
Se nos da el código fuente en Python del servidor que cifra la flag:
from hashlib import sha256
from secret import FLAG
WELCOME_MSG = """
Welcome to my Super Secure Signing service which uses unbreakable hash function.
We combine your Cipher with our secure key to make sure that it is more secure than it should be.
"""
def menu():
print("1 - Sign Your Message")
print("2 - Verify Your Message")
print("3 - Exit")
def xor(a, b):
return bytes([i ^ j for i, j in zip(a, b)])
def H(m):
return sha256(m).digest()
def main():
print(WELCOME_MSG)
while True:
try:
menu()
choice = int(input("> "))
except:
print("Try again.")
continue
if choice == 1:
message = input("Enter your message: ").encode()
hsh = H(xor(message, FLAG))
print(f"Hash: {hsh.hex()}")
elif choice == 2:
message = input("Enter your message: ").encode()
hsh = input("Enter your hash: ")
if H(xor(message, FLAG)).hex() == hsh:
print("[+] Signature Validated!\n")
else:
print(f"[!] Invalid Signature!\n")
else:
print("Good Bye")
exit(0)
if __name__ == "__main__":
main()
Análisis del código fuente
El servidor ofrece dos opciones, aunque la segunda es inútil:
def menu():
print("1 - Sign Your Message")
print("2 - Verify Your Message")
print("3 - Exit")
if choice == 1:
message = input("Enter your message: ").encode()
hsh = H(xor(message, FLAG))
print(f"Hash: {hsh.hex()}")
elif choice == 2:
message = input("Enter your message: ").encode()
hsh = input("Enter your hash: ")
if H(xor(message, FLAG)).hex() == hsh:
print("[+] Signature Validated!\n")
else:
print(f"[!] Invalid Signature!\n")
En resumen, podemos proporcionar cualquier mensaje, se aplicará una operación XOR entre este y la flag, y al resultado se le aplica un hash SHA256:
$$ h = \mathrm{SHA256}(m \oplus \mathrm{FLAG}) $$
def xor(a, b):
return bytes([i ^ j for i, j in zip(a, b)])
def H(m):
return sha256(m).digest()
Hay un problema, porque la función xor
utiliza la función zip
de Python. Entonces, la longitud del resultado será igual a la de la cadena más corta (a
o b
).
Solución
Con esto, tenemos un oráculo donde podemos probar cualquier byte hasta que encontremos el resultado esperado.
Oráculo
Asumiendo que la flag es HTB{f4k3_fl4g_f0r_t3st1ng}
, entonces:
- Si enviamos
H
, el servidor devolverásha256(b'\0').digest()
- Si enviamos
HT
, el servidor devolverásha256(b'\0\0').digest()
- Si enviamos
HTB
, el servidor devolverásha256(b'\0\0\0').digest()
- Si enviamos
HTB{
, el servidor devolverásha256(b'\0\0\0\0').digest()
- …
Podemos probarlo en la instancia remota y comprobarlo con Python:
$ python3 -q
>>> from hashlib import sha256
>>> sha256(b'\0').hexdigest()
'6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d'
>>> sha256(b'\0\0').hexdigest()
'96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7'
>>> sha256(b'\0\0\0').hexdigest()
'709e80c88487a2411e1ee4dfb9f22a861492d20c4765150c0c794abd70f8147c'
>>> sha256(b'\0\0\0\0').hexdigest()
'df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119'
>>> exit()
$ nc 94.237.58.148 55912
Welcome to my Super Secure Signing service which uses unbreakable hash function.
We combine your Cipher with our secure key to make sure that it is more secure than it should be.
1 - Sign Your Message
2 - Verify Your Message
3 - Exit
> 1
Enter your message: H
Hash: 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
1 - Sign Your Message
2 - Verify Your Message
3 - Exit
> 1
Enter your message: HT
Hash: 96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7
1 - Sign Your Message
2 - Verify Your Message
3 - Exit
> 1
Enter your message: HTB
Hash: 709e80c88487a2411e1ee4dfb9f22a861492d20c4765150c0c794abd70f8147c
1 - Sign Your Message
2 - Verify Your Message
3 - Exit
> 1
Enter your message: HTB{
Hash: df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119
Este enfoque es correcto, podemos probar carácter por carácter hasta que obtengamos el hash SHA256 de un número determinado de bytes nulos. Sin embargo, podemos mejorarlo.
En lugar de consultar el servidor varias veces, solo consultaremos una vez por carácter. Para que esto funcione, haremos lo siguiente:
- Enviamos
\0
, el servidor devolverásha256(b'H').digest()
. Calculamos todos los hashes SHA256 deb'\0'
hastab'\xff'
y tomamos el que coincida con el resultado (H
). - Enviamos
\0\0
, el servidor devolverásha256(b'HT').digest()
. Calculamos todos los hashes SHA256 deb'H\0'
hastab'H\xff'
y tomamos el que coincida con el resultado (HT
). - Enviamos
\0\0\0
, el servidor devolverásha256(b'HTB').digest()
. Calculamos todos los hashes SHA256 deb'HT\0'
hastab'HT\xff'
y tomamos el que coincida con el resultado (HTB
). - Enviamos
\0\0\0\0
, el servidor devolverásha256(b'HTB{').digest()
. Calculamos todos los hashes SHA256 deb'HTB\0'
hastab'HTB\xff'
y tomamos el que coincida con el resultado (HTB{
). - …
Como resultado, todo el proceso de fuerza bruta se realiza localmente y no se emplea el servidor remoto.
Implementación
Esta vez, estoy usando Go con mi módulo gopwntools
. Este es el código relevante para resolver el reto:
func main() {
io := getProcess()
defer io.Close()
var flag []byte
prog := pwn.Progress("Flag")
for !bytes.ContainsRune(flag, '}') {
prog.Status(string(flag))
h := sendHash(io, bytes.Repeat([]byte{'\x00'}, len(flag)+1))
for c := byte(0x20); c < 0x7f; c++ {
s := sha256.Sum256(append(flag, c))
if pwn.Hex(s[:]) == h {
flag = append(flag, c)
break
}
}
}
prog.Success(string(flag))
}
Flag
Si ejecutamos el script, obtendremos la flag:
$ go run solve.go 94.237.58.148:55912
[+] Opening connection to 94.237.58.148 on port 55912: Done
[+] Flag: HTB{r0ll1n6_0v3r_x0r_w17h_h@5h1n6_0r@cl3_15_n07_s3cur3!@#}
[*] Closed connection to 94.237.58.148 port 55912
El script completo se puede encontrar aquí: solve.go
.