ASC - Wargames 2022 Filtration Phase

This is my writeup and walkthrough for crypto challenges in ASC wargames 2022 gualification round

August 4, 2022 - 21 minute read -
Security ASC-Wargames CTF Infosec Python Hacking Cryptography cracking RSA ECC

ASC

Table of Content


Intro

Arab Security Cyber Wargames qualification round started this year from 5th till 6th of August and as always the top 10 teams will be qualified to the finals. In this write-up, we’re going to discuss and solve the 5 crypto challenges in the competition . The challenges was easy-medium ones, but full of tricks for those who aren’t up-todate with the encryption algorithms and techniques. However, I’ll show you my thought process and how to divide the challenge into sub-challenges in order to find a correct solution.

Challenge Difficulty Points
RSA in the wild Warmup 100
OSP Easy 300
Teaser Medium 600
FHE Medium 600
Vault Hard 900

RSA in the wild

Description:

Our security team found encrypted messages between members of an active threat actor in the wild.

This is a warmup straightforward challenge, so let’s have a look at the given files.

[
    {
        "John": {
            "key": 9019465093803586877472891652042526017244423267918585684572141459337752636017501282398583984846819147555479788255766221465302452334708306581657478087163498882790399392556915932241903819600243256898710512837026330099749891149718206725456165654975013707057350042189177818505148923810842478214626652504947902299,
            "msg": 1490803635449005835981793387807741830923148060654731278738509797435451285285034156065878921946571927216460900511251526914548382779631334897120457669789539503101428807041786196779372071069328112093285546177856847259662170258558289415211977744184992082066716124590295955026240499770848142550445898094801157061
        }
    },
    {
        "Sara": {
            "key": 23938372162005523177999938438562451374665546708075664883194200608993841377868039780046395969369898805670203008718315917149246468698236445400730491330343376568175458641957123986113999188370741703681470314365261825831443108787421922073023609145294588353146041309964285454626205876016177576199911694583578054203,
            "msg": 22749819412562980254888072091849267912732443428386048160973061880684541213086828626572165698999242738218862790810468234743381315109206140529974157828521448038033521924876023520452664012990798718216059461079083465212248636474782874296227844898049141588947385927713571390311824063367782855645524892655431088232
        }
    },
    {
        "Y4mm1": {
            "key": 7492176105815056287406737107861152687669914817188441973876375606125509278843647128053495385472184164273276753734355681888283710630052589292533918258041321561584337044160204288159261124250897895150472928088930420119607423773142875636276401786832850472958085716356092462792054479554714349979034664376850407259,
            "msg": 2544223511735543039595079752083782272939464573374775456475586531619250161960313372895971808675158274512437185309522676978160116122909124405173644335952401335143161289490254404665940426997169777822971888908315046502903142588256830588219713706207832651682400227233863085882991692803261801301182265503150372301
        }
    },
    {
        "Gary": {
            "key": 19226181445602743460246708025013176246822001005948560833211736039157554695246287037030410489087800335076044816379819628670911825715971233704410525113162113042540729331798511555022529148709471705473637189586448652726834752638590559219127165638752435997278633564685349397058307290548363125722837867180940021419,
            "msg": 12100625282820382536088469677465402939756857865013288698256765193122801312845842440176118885229553306158666539700152355154084650895509376550887918252093180450562973419960250796728283309496027020169076272415675948089735523228946553123649235016377673362851198236398841047345542435309022329198769047584615575574
        }
    }
]

The output file contains an array of kinda users messages with some keys. Let’s look at the script:

# WildRSA.py

from Crypto.Util.number import getRandomNBitInteger, bytes_to_long, long_to_bytes, getStrongPrime, isPrime

BITS = 512
SEED = 1
while SEED & 1:
SEED = getRandomNBitInteger(BITS//2)

class WildRSA:
def __init__(self, seed=getRandomNBitInteger(BITS//2)):
self.P = getStrongPrime(BITS)
self.seed = seed

    def __get_q(self, s):
        Q = s
        while not isPrime(Q+1):
            Q = s
            for _ in range(8):
                Q *= getRandomNBitInteger(32)
        return Q + 1

    def pub(self):
        N = self.P * self.__get_q(self.seed)
        return N, 0x10001

from secret import APT
obj = WildRSA(SEED)

for _, user in APT.items():
    N, E = obj.pub()
    key = N
    msg = pow(bytes_to_long(user["msg"]),E, N)
    print(key, msg)

The script has one class called WildRSA and it first generates a random 256-bit even integer seed. After that, it instantiate an object from the class with the generated seed as input, then it iterate over the APT array and encrypt the messages inside.

Solution

Obviousley, the class generates a strong 512-bit prime in the constructor and there’re two functions pub and __get_q. The trick here is in the pub method. It generates only one random prime from __get_q and the other prime is still the same for each message.

To visualiza, we have for each message \(N\) and \(E\):

\[N_i = PQ_i\]

This is a basic algebra problem to get \(P\) we have to find the GCD between each pair of the \(N_i\).

\[P = gcd(N_1, N_2, ...)\]

Finally, we can get the private key of the RSA.

from Crypto.Util.number import getRandomNBitInteger, bytes_to_long, long_to_bytes, GCD

N = [9019465093803586877472891652042526017244423267918585684572141459337752636017501282398583984846819147555479788255766221465302452334708306581657478087163498882790399392556915932241903819600243256898710512837026330099749891149718206725456165654975013707057350042189177818505148923810842478214626652504947902299,23938372162005523177999938438562451374665546708075664883194200608993841377868039780046395969369898805670203008718315917149246468698236445400730491330343376568175458641957123986113999188370741703681470314365261825831443108787421922073023609145294588353146041309964285454626205876016177576199911694583578054203,7492176105815056287406737107861152687669914817188441973876375606125509278843647128053495385472184164273276753734355681888283710630052589292533918258041321561584337044160204288159261124250897895150472928088930420119607423773142875636276401786832850472958085716356092462792054479554714349979034664376850407259,19226181445602743460246708025013176246822001005948560833211736039157554695246287037030410489087800335076044816379819628670911825715971233704410525113162113042540729331798511555022529148709471705473637189586448652726834752638590559219127165638752435997278633564685349397058307290548363125722837867180940021419]

messages = [1490803635449005835981793387807741830923148060654731278738509797435451285285034156065878921946571927216460900511251526914548382779631334897120457669789539503101428807041786196779372071069328112093285546177856847259662170258558289415211977744184992082066716124590295955026240499770848142550445898094801157061,6350249974685514311455731678779522359350354799468017596988644954406012738159501505851851861514932395179333372434804220392980343950894714606458923379054304802233466609403548752751709359872922491353578150109676550914201161697356048954377466378161795747517549045847439371181670308693139841054101664947749441303,2544223511735543039595079752083782272939464573374775456475586531619250161960313372895971808675158274512437185309522676978160116122909124405173644335952401335143161289490254404665940426997169777822971888908315046502903142588256830588219713706207832651682400227233863085882991692803261801301182265503150372301,12100625282820382536088469677465402939756857865013288698256765193122801312845842440176118885229553306158666539700152355154084650895509376550887918252093180450562973419960250796728283309496027020169076272415675948089735523228946553123649235016377673362851198236398841047345542435309022329198769047584615575574]
E = 0x10001
P = GCD(N[1], N[3])
print(P)
Q1 = N[1] // P
Q2 = N[3] // P
phi = (P - 1) * (Q1 - 1)
d = pow(E, -1, phi)
msg = pow(messages[1], d, N[1])
print(long_to_bytes(msg))

Flag: ASCWG{7h3_c0mM0n_9re4t_P0W3r_0f_6r0k3N_R$A}

OSP

Description:

Have you heard of OTP? This is OSP (One Secret Password).

This challenge was an easy one given the OSP.py script and output.txt file.

# OSP.py

#!/usr/bin/env python3
from Crypto.Util.number import getPrime, long_to_bytes, bytes_to_long, isPrime
from string import printable, ascii_letters
from secret import FLAG
import os

secret = os.urandom(len(FLAG))

def OSP(plain, secret):
    assert len(plain) == len(secret), 'The length has to be idenntical!'
    ct = []
    p = getPrime(256)
    for f, k in zip(FLAG, secret):
        ct.append((f * p + k))
        return ct, p

ct, p = OSP(FLAG, secret)
print(ct)

Basically, the script does some OTP on the flag with a randomly generated secret bytes and an odd 256-bit prime using the following equation \(C_i = F_ip + k\) where \(F_i\) are the flag bytes.

And the other file contains the cipher bytes:

5447072546591309544167389173397699795993168970119080464536675615517059887871841
6955492636416595264090666175261678201037431146459748900869908862891014933743799
5614674778794119068603308840271475174331420323045821401907034865225277115190905
7290697100822214312962505509009228957713933852313230775610627362307449388382150
5949879243199738117475148174019025931007923028899303276647753364641711569829134
10307537280472785752809059512737185767802458204994567648277093857055359480126320
...

Solution

Obviously, you can notice that \(k\) is in the byte range \([0, 255]\) and \(F_{0...5}\) all are known bytes ASCWG{. So, we can rearrange the equation above in order to get the prime number generated during the encryption.

\[p = \frac{C_i - k}{F_0}\]

We’ll calculate this by bruteforcing the \(k\) and check the result is it a prime or not, then we can do a bruteforce on the flag bytes itself.

flag = b''
p = 0
for i in range(1,256):
    p = (ct[0] - i) // ord('A')
    if isPrime(p):
    break
for c in ct:
    for i in range(1,256):
        for j in range(1,256):
            if i * p + j == c:
                flag += long_to_bytes(i)
                break
                print(flag)

Flag: ASCWG{Wh47_1f_17's_N07_@_Pr1M3!-f0ffa3657e}

Teaser

Description:

The APT hasn’t stopped their operations yet and decided to change the technique they use.

Given 2 files a python script Teaser.py and text output.txt file. The script contains 2 layers of encryption RSA with some modular polynomial equations.

from Crypto.Util.number import bytes_to_long, long_to_bytes, getStrongPrime, getRandomNBitInteger
from time import time_ns
from secret import p, q, FLAG

BITS = 512

N = p * q

a, b, x = [getRandomNBitInteger(64) for _ in range(3)]
s = x*(a * x + b) + 1
inf = -x*(a * x + b)
FLAG = bytes_to_long(FLAG)

c1 = pow(FLAG, s, N)
c2 = pow(FLAG, inf, N)

q1 = (x*a*c1 + b*c2 + a*b) % N
q2 = (a*c2 - x*b*c1 + a*b) % N

print(f"{N=}\n{q1=}\n{q2=}")
hint = x**5 + a*b*x**4 + b*x**3 - (a*b**2)*x**2 + (b*a**2)*x - (b**2)*(a**2)
print(f"{hint=}\n{a=}\n{b=}")

And the text files contains some values

N=136172654412975696672277699911326568930906766030832704596331587851913580572236893811129555165206299271122442434714570487317929463308920741438574074555215146250641537240478488340634101606111041980010829107689359885402204793049862837890400636654700457420935273510878322624221066308139237364134968330182982837219
q1=129700851577911833951546779649879386781363652757273181746564105468579589496221524015498600435486574591787516500081344266887101103166695795763627773287485087193020550446476318981445160008866376604043109183290785818082565090275386881955913967486969291033661943236500735834562836343585517551647242966723167449078
q2=106187520824836916140530981655653639328217502775479926753617707156322730832324365295797538549177802527036773048825921792473140235119964421139149577764124882650810667324422218800463880704315570077101568050089773823894917651766606738325184195516375067069083002909431495903494276122403106230525955048238146574777
hint=6573544964235663795110387821358621068738264530355319754834598296204350028845729399053875214556575503920004379593112
a=12011053116152205388
b=11423234452039057359

After analyzing the script, it computes the modulus \(N=pq\), then generates three 64-bit random numbers a, b, x. Aftet that, it calculates \(s = x( ax + b) + 1\) and \(inf = -x(ax + b)\) and they’re additive inverse of each other except the 1.

Then here comes to the first layer with the following

\[C_1 = Flag^s \pmod{N}\] \[C_2 = Flag^{inf} \pmod{N}\]

Second layer calculates two modular polynomial linear eqautions

\[Q_1 = xaC_1 + bC_2 + ab \pmod{N}\] \[Q_2 = aC_2 - xbC_1 + ab \pmod{N}\]

Finally, it scrambles the value of x in some sort of another 5th degree polynomial equation \(hint = x^5 + abx^4 + bx^3 - ab^2x^2 + xba^2 - b^2a^2\)

Solution

We can start from the end, so the goal first is to calculate the unknown variable \(x\), then try to solve the two modular equations to find \(C_1\) and \(C_2\) after that we can multiply each other to get the flag.

\[C_1C_2 = Flag^sFlag^{inf} = Flag^{s(inf)} = Flag^{x(ax+b) + 1 - x(ax+b)} = Flag \pmod{N}\]

We’ll use SageMath in order to recover the encrypted flag.

N=136172654412975696672277699911326568930906766030832704596331587851913580572236893811129555165206299271122442434714570487317929463308920741438574074555215146250641537240478488340634101606111041980010829107689359885402204793049862837890400636654700457420935273510878322624221066308139237364134968330182982837219
q1=129700851577911833951546779649879386781363652757273181746564105468579589496221524015498600435486574591787516500081344266887101103166695795763627773287485087193020550446476318981445160008866376604043109183290785818082565090275386881955913967486969291033661943236500735834562836343585517551647242966723167449078
q2=106187520824836916140530981655653639328217502775479926753617707156322730832324365295797538549177802527036773048825921792473140235119964421139149577764124882650810667324422218800463880704315570077101568050089773823894917651766606738325184195516375067069083002909431495903494276122403106230525955048238146574777
hint=6573544964235663795110387821358621068738264530355319754834598296204350028845729399053875214556575503920004379593112
a=12011053116152205388
b=11423234452039057359
x = var('x')
pol = x^5 + a*b*x^4 + b*x^3 - (a*b^2)*x^2 + (b*a^2)_x - (b^2)_(a^2) - hint
x = int(pol.roots()[-1][0])

Then we’ll construct PolynomialRing with a field module \(N\), then we can use Ideals and Groebner Basis.

F = Zmod(N)
PR.<c1, c2> = PolynomialRing(F)
f1 = x*a*c1 + b*c2 + a*b - q1
f2 = a*c2 - x*b*c1 + a*b - q2

I = Ideal([f1, f2])
I.groebner_basis()
# [c1 + 129605315639493448970075136922290937178908733835389122798667679999645002511918751225679112934299574414218902904120640354628679425092067997512344836684845210103107098629862452090849238650480459973169748953515560552426758751304034613294461809691584124214680152686132787281209362547560156811179702797894368189923, c2 + 16301823837274342549830986630234325738949194338370818370588794724975224426894656835985621236373909782588949334404482254039646785783224270540302423929586850992446342322491817804966558550730248640999810574168456842642895073832374078601103825246815150330281607599953134437905377383158998850822351263862695676571]

And we know that they’re basis so the result has to equal zero \(C_1 + 12960... \pmod{N}= 0\) and \(C_2 + 16301... \pmod{N}= 0\).

C1 = F(-129605315639493448970075136922290937178908733835389122798667679999645002511918751225679112934299574414218902904120640354628679425092067997512344836684845210103107098629862452090849238650480459973169748953515560552426758751304034613294461809691584124214680152686132787281209362547560156811179702797894368189923)
C2 = F(-16301823837274342549830986630234325738949194338370818370588794724975224426894656835985621236373909782588949334404482254039646785783224270540302423929586850992446342322491817804966558550730248640999810574168456842642895073832374078601103825246815150330281607599953134437905377383158998850822351263862695676571)
print(int.to_bytes(int(F(C1*C2)), 64, 'big'))

Note that: There’re multiple ways to solve multivariate polynomials I’ll leave it for you to discover, but most of them are limited to for example the degree. However, Ideals and Groebner Basis are general ways to find roots of multivairate non-linear modular polynomials under certain conditions. You can learn more from this book.

Flag: ASCWG{8r4in_T3s$s1n9_7h3_Ba51s_0f_9r036n3r}

Fully Heuristic Encryption

Description:

We’re looking for briliant mathematicians to decrypt this cipher. If you’re not the one, go away!

This one is a bit tricky and it’s all about basic homomorphic encryption. Basically, you can look at the homomorphic encryption as an extension of public-key cryptography. Given the FHE.py script and a text output.txt file.

from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes, isPrime, getRandomNBitInteger
from random import getrandbits, randint

LEN = 25
BITS = 256

with open('flag.txt', 'rb') as file:
FLAG = file.read()

bin_flag = bin(bytes_to_long(FLAG))[2:]

def gen():
    _p = 1
    while not isPrime(_p):
    _p = 2**255 + 2**127 + getRandomNBitInteger(LEN)
    pub = []
    for _ in range(LEN):
    r, q = getrandbits(BITS//2), getrandbits(BITS)
    pub.append(_p*q + 2*r)
    return pub, _p

def encrypt(public, bins):
    ciphertext = []
    for bit in bins:
    ids = [public[randint(0, len(public)-1)] for _ in range(5)]
    r = getrandbits(BITS//2)
    ciphertext.append(sum(ids) + 2*r + int(bit))
    return ciphertext

public, secret = gen()
ct = encrypt(public, bin_flag)

with open('output.txt', 'w') as h:
    for c in ct:
        h.write(str(c)+"\n")

The script has two functions gen that retunrs a public key with a secret from and encrypt that does some sort of encryption on the flag bits with the generated public key.

The first function gen, it generates a 256-bit prime number with the last 25 bits are random \(p = 2^{255} + 2^{127} + rand(2^{25})\). After that, it generates two random numbers \(q\) and \(r\) 25 times, then computes the public-key using the following equation \(pub_i = pq_i + 2r_i\).

The encrypt functions takes the public key and the flag bits, then it get 5 random values from the public-key as ids with a random 128-bit r. After that, it encrypts the each bit of the flag with the following formula \(C_i = 2r + Flag_{bit} + \sum{ids}\).

Solution

The attack here is pretty simple just you need to focus on what is given by looking at the equtions used. First you can notice that the last few bits of prime are low so we can bruteforce those bits, then after finding \(p\) and looking at the \(ids\) it’s just some values of the public-key \(\sum{ids} = \sum{pq_i + 2r_i}\).

Now, here comes the interesting question: How can we get rid off the values \(2r + \sum{2r_i + pq_i}\) to recover the flag bits?

In order to make the above value equals to zero, the property of module somes into place by first take it module the recovered prime and this will get rid off all values multiplied by \(p\), then the remaining value is equal to some even number added to the flag bits.

\[2r + Flag_i + \sum{2r_i + pq_i} \pmod{p} = 2r + Flag_i + \sum{2r_i}\] \[Flag_i = 2(r + \sum{r_i}) + Flag_i \pmod{2}\]
for i in range(2**(LEN-1), 2**LEN-1): # Look at getRandomNBitInteger docs 😉
    p = 2**255 + 2**127 + i
    if isPrime(p):
    flag = ''.join([str((x % p) % 2) for x in ct])
    flag = long_to_bytes(int(flag, 2))
    if b"ASCWG{" in flag:
        print(p, flag)
        break

Flag: ASCWG{DiV1n9_1n70_7h3_H0m0M0rpH1c_W0rl6_0f_We4k_Pr1m3_3nCryp7i0N}

Vault

Description:

After hunting down the APT over the darkweb we’ve finally found their encrypted vault.

The last challenge was about ECC and especially on SECP256k1 bitcoin curve, but with some tweeks on the curve params. Given vault.py script and vault.enc an encrypted file.

from Crypto.Util.number import (
getPrime,
getRandomRange,
getRandomNBitInteger,
long_to_bytes,
inverse,
)
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from collections import namedtuple
from hashlib import sha1

Point = namedtuple("Point", "x y")
O = Point(0, 1)

class ECC:
    def __init__(self, BITS=128) -> None:
        assert BITS >= 128
        self.p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
        self.a = -32
        self.b = 7
        self._private = getRandomNBitInteger(BITS) % self.p

    def encrypt(self, P: Point, message):
        C = self.mul(P, self._private)
        key = sha1(long_to_bytes(C.x * self.inv_mod_p(C.y) % self.p)).digest()[:16]
        cipher = AES.new(key, AES.MODE_CBC)
        return key.hex(), cipher.iv.hex(), cipher.encrypt(pad(message, 16)).hex()

    def add(self, P: Point, Q: Point) -> Point:
        # Adding 2 Points P and Q on the curve

        if not (self.valid(P) and self.valid(Q)):
            raise ValueError("Invalid points")

        if P == O:
            return Q
        if Q == O:
            return P
        if P.x == Q.x and P.y == -Q.y % self.p:
            return O
        else:
            if P == Q:
                y_diff = (3 * (P.x**2) + self.a) % self.p
                x_diff = (2 * P.y) % self.p

            else:
                y_diff = (Q.y - P.y) % self.p
                x_diff = (Q.x - P.x) % self.p

            slope = (y_diff * self.inv_mod_p(x_diff)) % self.p
            x3 = (slope**2 - P.x - Q.x) % self.p
            y3 = (slope * (P.x - x3) - P.y) % self.p
            return Point(x3, y3)

    def inv_mod_p(self, x):
        if x % self.p == 0:
            raise ZeroDivisionError("Impossible inverse")
        return pow(x, self.p - 2, self.p)

    def valid(self, P) -> bool:
        if P == O:
            return True
        return (
            (P.y ** 2 - (P.x ** 3 + self.a * P.x + self.b)) % self.p == 0
            and 0 <= P.x < self.p
            and 0 <= P.y < self.p
        )

    def mul(self, P: Point, n: int) -> Point:
        # Adding a Point P to itself n times
        Q = P
        R = O
        while n:
            if n & 1:
                R = self.add(Q, R)
            Q = self.add(Q, Q)
            n >>= 1
        return R

ecc = ECC()
G = Point(39613264652991136516316121365481995043381995333181795575614979310610916877953, 14487697298980196960516056623210206756245610286316104440426923097340932458937)

with open("vault.json", "rb") as h1, open("vault.enc", "wb") as h2:
    vault = h1.read()
    key, iv, enc = ecc.encrypt(G,vault)
    h2.write(bytes.fromhex(enc))

Looking at the script, it has a class called ECC with some functions operates on the curve adding points or multiplying constants. After that, it creates an instance from the class and a given generator point \(G\), then encrypts the vault.json file with encrypt function in the class.

The class ECC has the following functions:

  • add: Add 2 points on the defined curve.
  • mul: Add a point to itself n times.
  • valid: Validate a point on the curve.
  • inv_mod_p: Find modular inverse of a coordinates with the curve prime.
  • encrypt: Encrypt a message with a secret point \(C = privG\) using AES-CBC mode.

The curve is using the following parameters:

p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
a, b = -32, 7
E = EllipticCurve(FiniteField(p), [a,b]); E
# Elliptic Curve defined by y^2 = x^3 + 115792089237316195423570985008687907853269984665640564039457584007908834671631\*x + 7 over Finite Field of size 115792089237316195423570985008687907853269984665640564039457584007908834671663

Solution

First thing you need to focus while working with elliptic curves is the curve structure. Is it secure or not with the defined parameters?

Let’s first look at the order of the curve is it secure? then we can llok at the order of the given generator point.

E.order().factor()
# 23 * 9817 * 4286059 * 12708491 * 172460042771 * 54592331553134402818263581321437118002282885169
G = E(39613264652991136516316121365481995043381995333181795575614979310610916877953,14487697298980196960516056623210206756245610286316104440426923097340932458937)
G.order()
# 12708491

Apparently, the order is factored int small primes and one of the them is exactly the order of the generator point, so we can assume that the given point is generated by dividing the curve order by 12708491 then multiplied by the original generator G = (o//12708491)*E.gens()[0] that will produce a point with order or 12708491.

Notice that, any generated points from G will have the same order of 12708491, so we can brute force the point from the range \([1,12708491]\), try to decrypt the message, and check if it contains the flag or not.

from Crypto.Util.number import (
getPrime,
getRandomRange,
getRandomNBitInteger,
long_to_bytes,
inverse,
)
from Crypto.Cipher import AES
from hashlib import sha1
p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
a, b = -32, 7
E = EllipticCurve(FiniteField(p), [a,b])
G = E(39613264652991136516316121365481995043381995333181795575614979310610916877953,14487697298980196960516056623210206756245610286316104440426923097340932458937)
for i in range(12708491):
    x, y = (i*G).xy()
    inv = pow(y,-1,p)
    key = sha1(long_to_bytes(int(int(x * inv) % p))).digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC)
    flag = cipher.decrypt(ct)
    if b'ASCWG{' in flag:
        print(flag)
        break

Flag: ASCWG{Curv3_0u7_7h3_3111pt1c_5m4l1_0rd3r_0f_8i7c0in_$3cp256k1}

Conclusion

In this writeup, I’ve conducted a detailed analysis of all the crypto challenges I’ve created in the competition as well as, my thought process of how I can look over and examine the problem. I hope you’ve enjoyed the competition and learned something from the challenges that are created by me or even my teammates. For those who are still stuck or willing to learn more about cryptography, I’ve made a small roadmap here that can guide you through your journey and of course you can hit me up any time on my socials. Finally, congrats to every team who made it until the end and wish you all the best in the finals.