Berlian Gabriel

DownUnder CTF (DUCTF) 2024 Writeup

V for Vieta

Implementing Vieta jumping algorithm

Problem

The following source code is given.

#!/usr/bin/env python3

import os
import random
import json
from enum import Enum


FLAG = os.getenv("FLAG", "DUCTF{dummy_flag}")


class State(Enum):
    INITIAL = 1
    TEST = 2
    QUIT = 3


class Server:
    def __init__(self):
        self.level = 2048
        self.target = 2048
        self.finish = 8
        self.state = State.INITIAL

    def win(self):
        return {
            "flag": FLAG,
        }

    def generate_k(self):
        self.k = random.getrandbits(self.level) ** 2
        self.state = State.TEST
        return {
            "k": self.k,
            "level": self.level,
        }

    def test(self, challenge):
        a, b = challenge["a"], challenge["b"]
        if a <= 0 or b <= 0:
            self.state = State.QUIT
            return {"error": "Your answer must be positive!"}

        if a.bit_length() <= self.target or b.bit_length() <= self.target:
            self.state = State.QUIT
            return {"error": "Your answer is too small!"}

        num = a**2 + a * b + b**2
        denom = 2 * a * b + 1

        if num % denom != 0 or num // denom != self.k:
            self.state = State.QUIT
            return {"error": "Your answer wasn't a solution!"}

        self.level -= self.level // 5

        if self.level <= self.finish:
            self.state = State.QUIT
            return self.win()
        else:
            return self.generate_k()


def main():
    server = Server()
    print("V equals negative V plus or minus the squareroot of V squared minus 4 V V all divided by 2 V!")

    while True:
        if server.state == State.INITIAL:
            print(json.dumps(server.generate_k()))
        elif server.state == State.TEST:
            challenge = json.loads(input())
            print(json.dumps(server.test(challenge)))
        elif server.state == State.QUIT:
            exit(0)


if __name__ == "__main__":
    main()

The essence is, we need to find a pair of (a,b) which satisfies the requirement:

  • (a**2 + a * b + b**2) / (2 * a * b + 1) == k
  • along the way, k will get smaller, until it reaches 8 bit, but we still need to always supply a and be that are atleast 2048 bit

Approach

Re-arrange the quadratic equation in terms of a:

a**2 - (2*k-1)*b * a + (b^2 - k) == 0

The first few pairs of (a,b) that satisfy the quadratic equation above can be derived from the Wikipedia page on Vieta jumping, where r is the square root of the given k:

1st pair: (0              , r)
2nd pair: (r              , 2 * r ** 3 - r)
3rd pair: (2 * r ** 3 - r , 4 * r ** 5 - 4 * r ** 3)

After observing the pattern, notice that

a[i] = b[i-1]
b[i] = (2 * r ** 2 - 1)*b[i-1] - a 

where (a[i-1],b[i-1]) is the previous working pair before (a[i],b[i]). This means, we can determine the elements of the next pair from the previous elements.

Solution

The key thing to understand is, because k will keep getting smaller in each iteration, we cannot use the same form of (a,b) pair, as the earlier form of (a,b) will end up being smaller than 2048 bit for smaller k. This is why we need to jump to the next (bigger) form of (a,b), in order to be able to hit the 2048 bit requirement with smaller k.

Below is the solver script.

from pwn import *
from math import isqrt
import json

def calculate_a_b(k):
    r = isqrt(k)
    a = 2 * r ** 3 - r
    b = 4 * r ** 5 - 4 * r ** 3
    c = (2 * r ** 2 - 1)*b - a
    while(a.bit_length() < 2048):
        a=b
        b=c
        c = (2 * r ** 2 - 1)*b - a
    return a, b

def main():
    # Connect to the server
    conn = remote('2024.ductf.dev', 30018)
    #conn = process(['python', 'server.py'])
    
    # Read the introductory message
    intro_message = conn.recvline()
    print(intro_message.decode().strip())
    
    while True:
        # Receive the challenge from the server
        challenge = conn.recvline().decode().strip()
        print("Received challenge:", challenge)
        
        # Parse the challenge to get the value of k
        challenge_json = json.loads(challenge)
        k = challenge_json['k']
        # Calculate a and b based on k
        a, b = calculate_a_b(k)
        
        # Prepare the solution in JSON format
        solution = {"a": a, "b": b}
        solution_json = json.dumps(solution)
        print("Sending solution:", solution_json)
        
        # Send the solution back to the server
        conn.sendline(solution_json)
        
        # Receive the server's response

    
    # Close the connection
    conn.close()

if __name__ == "__main__":
    main()

FLAG: DUCTF{jump1n6_4nd_fl1pp1n6_up_7h3_hyp3r8011c_14dd3r}


Social Media

Thanks for reading! Follow me on Twitter and LinkedIn