Secure Signing
4 minutes to read
We are given the Python source code of the server that encrypts the 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()
Source code analysis
The server offers two options, although the second is useless:
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")
In brief, we can provide any message, a XOR operation will be applied on it and the flag, and the result will be hashed with 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()
There is an issue, because the xor
function uses Python’s built-in zip
function. As a result, the length of the result will be equal to the shortest string (a
or b
).
Solution
With this, we have an oracle where we can test any byte until we find the expected result.
Oracle
Let’s assume that the flag is HTB{f4k3_fl4g_f0r_t3st1ng}
, then:
- If we send
H
, the server will returnsha256(b'\0').digest()
- If we send
HT
, the server will returnsha256(b'\0\0').digest()
- If we send
HTB
, the server will returnsha256(b'\0\0\0').digest()
- If we send
HTB{
, the server will returnsha256(b'\0\0\0\0').digest()
- …
We can test it on the remote instance and check with 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
This approach is correct, we can test character by character until we get the SHA256 hash of all null bytes. However, we can make an improvement.
Instead of querying the server multiple times, we will only query one time per character. For this to work, we will do the following:
- Send
\0
, the server will returnsha256(b'H').digest()
. We compute all SHA256 hashes fromb'\0'
tob'\xff'
and take the one that matches the result (H
). - Send
\0\0
, the server will returnsha256(b'HT').digest()
. We compute all SHA256 hashes fromb'H\0'
tob'H\xff'
and take the one that matches the result (HT
). - Send
\0\0\0
, the server will returnsha256(b'HTB').digest()
. We compute all SHA256 hashes fromb'HT\0'
tob'HT\xff'
and take the one that matches the result (HTB
). - Send
\0\0\0\0
, the server will returnsha256(b'HTB{').digest()
. We compute all SHA256 hashes fromb'HTB\0'
tob'HTB\xff'
and take the one that matches the result (HTB{
). - …
As a result, all the brute force process is done locally, and not using the remote server.
Implementation
This time, I am using Go with my gopwntools
module. This is the relevant code to solve the challenge:
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
If we run the script, we will get the 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
The full script can be found in here: solve.go
.