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.
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 itselfn
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.