Page cover image

PwnSecCTF 24 Writeups

In this page, I'll be sharing the writeups to the challenges i wrote for PwnSecCTF 24

RSNAY - Cryptography (EASY)

We're given this handout:

from Crypto.Util.number import *
from secret import flag,e,n



def encrypt(m):
    return pow(m,e,n)

assert (pow(encrypt(flag),e,n) - flag) % GCD(e,n)  == 0


menu = """Welcome to my Encryption Service! 
Choose an option:
1. Encrypt
2. Get Flag
3. Exit
"""
def main():
    encryption_counter = 0
    print(menu)
    while True:
        choice = input("Enter your choice: ")
        if choice == '1':
            if encryption_counter >= 2:
                print("You have reached the maximum number of encryptions allowed!")
                break
            else:
                encryption_counter += 1
                m = int(input("Enter the message you want to encrypt: "))
                print(f"Here is your encrypted message: {encrypt(m)}")
        elif choice == '2':

            print(f"Here is your flag: {encrypt(flag)}")
        elif choice == '3':
            print("Goodbye!")
            break
        else:
            print("Invalid choice! Please try again.")
            continue


if __name__ == "__main__":
    main()

We can either encrypt a ciphertext we have, encrypt the flag and leave, if we wish to encrypt a chosen plaintext we're limited to only 2 encryptions.

let's analyze the assertion we're given

Understanding and Breaking the Assertion

The given assertion is:

assert (pow(encrypt(flag),e,n) - flag) % GCD(e,n)  == 0

This simplifies to:

flag(ee)flag0(modgcd)(e,N)flag^{(e * e)} - flag ≡ 0 \pmod \gcd(e, N)
  • Output of GCD: The possible values of GCD(e, N) are 1, p, or q, where (N=pq)(N = p \cdot q). Since the assertion wouldn’t hold meaningfully if GCD(e, N) = 1, it implies that (e = p) or (e = q).

  • Simplification: From here onward, assume (e = p). This substitution allows us to analyze the system further.

Finding (N)

To determine (N), leverage the fact that (p) is odd. Sending -1 as input results in the following:

(1)p1N1(modN)(-1)^p \equiv -1 \equiv N - 1\pmod N

From this, we can deduce:

N=(1)pmodN+1N = (-1)^p \mod N + 1


Finding (p)

To extract (p), use Fermat’s Little Theorem:

xpx(modp)for any x coprime with Nx^p \equiv x \pmod{p} \quad \text{for any } x \text{ coprime with } N

Rewriting, we find:

xpx0(modp)x^p - x \equiv 0 \pmod{p}

This implies that (xpx)(x^p - x) is divisible by (p). Using this property, we can compute:

p=GCD(N,xpx)p = \text{GCD}(N, x^p - x)

where (x) is a random number coprime to (N).

Solver

from Crypto.Util.number import *
from pwn import *

HOST = "crypto-rsnay.pwnsec.xyz"
PORT = 33333

context.log_level = 'error'
r = remote(HOST,PORT)
def parse_encrypt(pt):
    
    r.recvuntil(b"choice: ")
    r.sendline(str(1).encode())
    r.recvuntil(b"encrypt: ")
    r.sendline(str(pt).encode())
    r.recvuntil(b"message: ") 
    ct = r.recvline().decode()
    return ct

r.recvuntil(b"choice: ")
r.sendline(str(2).encode())
r.recvuntil(b"flag: ") 
enc_f = int(r.recvline().decode())

N = int(parse_encrypt(-1)) + 1
p = GCD(int(parse_encrypt(2)) - 2, N)
q = N // p
phi = (p - 1) * (q - 1)

d = pow(p,-1,phi)

pt = pow(enc_f,d,N)
print(long_to_bytes(pt).decode())

Calculus Help - Cryptography (EASY)

We're given the following handout:

from secret import flag,f, integrate
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import sha256

key = f'{integrate(f, 2.420, 1.77415).evalf():.2f}'
print(key)

def encrypt(key, data):
    key = sha256(str(key).encode('utf-8')).digest()[16:]
    iv =  sha256(str(key).encode('utf-8')).digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv = iv)
    ct_bytes = cipher.encrypt(pad(data, AES.block_size))
    return ct_bytes

ct = encrypt(key, flag)

def get_hint(x):
    return f(x)
def get_flag():
    return ct.hex()

def main():
    print("Welcome to my secure encryption service!")
    print("")
    hint_ctr = 0
    while True:
        try:
            option = int(input("1. Get hint\n2. Get flag\n3. Exit\n"))
            if option == 1:
                if hint_ctr > 2:
                    print("You have exceeded the maximum number of hints.")
                    continue
                hint_ctr += 1
                x = float(input("Enter a number: "))
                print(get_hint(x))
            elif option == 2:
                print(f"Here is the flag: {get_flag()}")
            elif option == 3:
                print("Goodbye!")
                break
            else:
                print("Invalid option. Please try again.")
        except:
            print("An error occurred. Please try again.")
            continue
if __name__ == '__main__':
    main()

This challenge involves using Simpson’s rule for numerical integration to compute a key for decrypting the flag. Let’s break it down step by step.

What is Simpson’s Rule?

Simpson’s rule is a method to approximate the definite integral of a function f(x) over an interval [a,b]. The formula is:

abf(x)dxba6[f(a)+4f(2a+b)+f(b)]\int_a ^b ​ f(x)dx≈ \frac {b−a} 6 ​ [f(a)+4f( 2 a+b ​ )+f(b)]

In this challenge, the interval is [2.420, 1.77415], and the function is f(x), which you can query using get_hint(x).


Steps to Solve the Challenge

  1. Understand the Function f(x):

    • Use the get_hint(x) option to get the values of f(x) at key points:

    • x1=2.420, x2=(2.420+1.77415)/2, and x3=1.77415.

  2. Apply Simpson’s Rule:

    • Plug the values of f(x) into Simpson’s rule to calculate the integral. This result forms the key, which is rounded to two decimal places.

  3. Decrypt the Flag:

    • Use the computed key to derive the AES encryption key and IV. Then decrypt the ciphertext to get the flag.


Why Simpson’s Rule?

Simpson’s rule is used here because:

  • It gives a quick and accurate way to approximate integrals.

  • The challenge tests your ability to calculate the integral precisely, as small errors can lead to the wrong key.


Summary of Key Steps:

  • Use get_hint(x) to calculate f(2.420), f(1.77415), and f((2.420+1.77415)/2).

  • Compute the integral using Simpson’s rule and round to two decimal places.

  • Use the result as the encryption key to decrypt the flag.

It's essential to remember that this is an approximation and can have a high error level for some functions, hence why it's more practical to brute force the .xx decimal points just incase it doesn't instantly work...

Once we find our key we decrypt using AES-CBC and Retrieve our flag.

Solver

Thanks to masashi for providing the solve script as I was too lazy to write it...

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad ,unpad
from hashlib import sha256

# key = f'{integrate(f, 2.420, 1.77415).evalf():.2f}'

def encrypt(key, data):
    key = sha256(str(key).encode('utf-8')).digest()[16:]
    iv =  sha256(str(key).encode('utf-8')).digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv = iv)
    ct_bytes = cipher.encrypt(pad(data, AES.block_size))
    return ct_bytes

def decrypt(key,data):
    key = sha256(str(key).encode('utf-8')).digest()[16:]
    iv =  sha256(str(key).encode('utf-8')).digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv = iv)
    pt = unpad(cipher.decrypt(data),AES.block_size)
    return pt


x1, y1 = 2.420, -38.8857613294852
x2, y2 = 2.1, -30.245318139720677
x3, y3 = 1.77415, -24.381158068030444

# Simpson's Rule
integral = ((x3 - x1) / 6) * (y1 + 4 * y2 + y3)
print(f"Approximate integral: {integral:.2f}")


decimal = f'{int(integral)}'

ct = '468b1d9e6e18fa2dd8d89d7a2e8942716c11e5ee6c643f0462c6563bc940f66a4121d362d2ac112bbcabc16583d07105'

ct = bytes.fromhex(ct)


for i in range(100):
    frac = str(i)
    frac = frac.zfill(2)
    key = decimal + '.' + frac
    try:
        pt = decrypt(key,ct)
    except:
        continue
    if b'PWN' in pt:
        print(pt)

Unintended Solve

This solve exploits the library issue with evaluating large numbers, if we input a large number as a hint we'll get a funny output giving the secret function we use to evaluate the key.

Also, credit to bebo07_ for finding it, absolute legend!

Personal Reflection

This is only my second time writing challenges for a CTF, and honestly, I’m pretty happy with how they turned out. Sure, there were some flaws—classic skill issues—but hey, we’re all learning here! I hope my weird hints and occasional oops moments didn’t drive anyone too crazy. Huge thanks for all the feedback and support—it means a lot!

Last updated