Ouchie Revenge
The revenge of Ouchie — this time Get Flag encrypts the challenge, so we abuse the decryption oracle to recover p.
Overview
Same Paillier-like cryptosystem as Ouchie, over n = p**2 * q
with 384-bit primes. The difference is in option 3 (Get Flag): the challenge value is now
encrypted before being printed, so we can no longer just echo it back.
elif choice == 3 :
challenge = randint(p >> 10, p - 1)
print(f"Challenge: {encrypt(challenge)}")
response = int(input("Plaintext: "))
if challenge == response :
print("Congrats!")
print(FLAG)
exit()
else :
print("Wrong!")
exit()
The Encrypt and Decrypt menu options remain available, giving us both an encryption oracle
and a decryption oracle.
Analysis
Key structure: n = p² · q, with p being 384 bits. Look closely at the decryption routine:
L = lambda x : (x - 1) // p
def decrypt(c):
a = pow(c, p - 1, p**2)
b = pow(g, p - 1, p**2)
a = L(a)
b = L(b)
m = (a * pow(b, -1, p)) % p
return m
Both a and b are reduced modulo p, so decrypt(c) returns m' = m mod p. If we ask the
oracle to encrypt a plaintext m that is much larger than p (for example m = 2^385), then
decrypting that ciphertext gives us m' = m mod p. The difference m − m' is therefore an
exact multiple of p.
By collecting several such differences mᵢ − mᵢ' and taking their GCD, the common factor p
falls out:
gcd(m₁ − m₁', m₂ − m₂', …) = p (with high probability)
Once we know p, the challenge ciphertext from option 3 decrypts deterministically with the same
formula the server uses, and we submit the recovered plaintext to get the flag.
Exploitation
from pwn import *
from Crypto.Util.number import GCD
def get_p(io):
m_list = [2**385, 2**385 + 1, 2**386, 2**386 + 1]
diffs = []
for m in m_list:
io.sendlineafter(b">> ", b"1")
io.sendlineafter(b"Enter Your Plaintext: ", str(m).encode())
io.recvuntil(b"Encrypted: ")
c = int(io.recvline().strip())
io.sendlineafter(b">> ", b"2")
io.sendlineafter(b"Enter Your Ciphertext: ", str(c).encode())
io.recvuntil(b"Decrypted: ")
m_prime = int(io.recvline().strip())
diffs.append(m - m_prime)
p = GCD(diffs[0], diffs[1])
for d in diffs[2:]:
p = GCD(p, d)
return p
def main():
io = remote("20.198.224.34", 8666)
# Read g from the banner
io.recvuntil(b"g = ")
g = int(io.recvline().strip())
# Recover p via the decryption oracle
p = get_p(io)
# Grab the encrypted challenge
io.sendlineafter(b">> ", b"3")
io.recvuntil(b"Challenge: ")
c = int(io.recvline().strip())
# Decrypt the challenge using p
a = pow(c, p-1, p**2)
b = pow(g, p-1, p**2)
a = (a - 1) // p
b = (b - 1) // p
m = (a * pow(b, -1, p)) % p
# Submit the plaintext
io.sendlineafter(b"Plaintext: ", str(m).encode())
io.interactive()
if __name__ == "__main__":
main()
Recovering p from the GCD of the decryption differences lets us replicate the server's
decryption and answer the encrypted challenge, yielding the flag.
Flag
CTFITB{i_hate_myself_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_OUCHIE_OUCHIE_OUCH_OUCH_ME}