Last weekend I participated in zer0pts 2022 CTF online as a team member of Never Stop Exploiting
(abbreviation: NeSE
).We’re happy to get the first place in this high level competition. I focused on the crypto challenges and spent two days struggling with five challenges : Anti-Fermat
、CurveCrypto
、EDDH
、OK
、Karen
, finally four of them solved. They are all well-designed and here are the writeups for Anti-Fermat
、CurveCrypto
、EDDH
、OK
.
Anti-Fermat
Challenge name | Author | Points | Solves |
---|---|---|---|
crypto/Anti-Fermat | ptr-yudai | 90pts | 125 |
Source code : task.py.
This is a warm-up challenge. Observe that p,q are generated in a special way : . It’s equivalent to : , where y is a small number. Therefore we can brute force the value of y with n=p×q known and construct a quadratic equation to factor n.
Here is my solver : solve.ipynb.
CurveCrypto
Challenge name | Author | Points | Solves |
---|---|---|---|
crypto/CurveCrypto | theoremoon | 156pts | 36 |
Source code : task.py.
It seems like a curve problem? Haha, it’s just a blindfold. The ecc curve itself is secure. The crucial part of this problem is the unmatched block size. The modulus of ecc curve is 1024-bit but the cipher block length is just 16×8=128 bits, which means that we already have the 896 most significant bits leakage. Right, it’s coppersmith! Applying the bivariate coppersmith method to the ecc equations : where x0 and y0 are the known high 896 bits of point (x,y) on ecc curve. Obliviously, has small roots (x,y) upper bounded by .
The solver in sagemath :
1 | n = 144119247523820514307319742558945817289524321678464785828165262389987364282241677120346992289602773032781170623185859522408681068717004227361637296377314973988883717763449514502353544535632434189976809320943402560377421207936239458384129077990667822889168041784489265932700188699685494064706711885776064499497 |
EDDH
Challenge name | Author | Points | Solves |
---|---|---|---|
crypto/EDDH | theoremoon | 188pts | 24 |
Source code : server.py.
The author implements a DH protocol based on Edwards Curve
and we’re supposed to break the DH protocol to get the server’s private key. Of course , the DH protocol on a valid curve with a non-smooth order q is hard to hack. So we do some observation on the modulus p and order q:
1 | sage: factor(q) |
Oops, is deliberately smooth ! The first idea appears in my mind is to transform the Edwards Curve to a standard weierstrass curve. However the weierstrass curve’s order is still q. Then, we find that the author doesn’t check the validity of the point we submit, which means we can apply an invalid curve attack! In Edwards curve, if we submit an invalid point , the mul
operation becomes simply scalar power : . In GF(p) field, the multiplication order is exactly p-1, smooth! We can use Pohlig–Hellman algorithm to solve the discrete log problem in a smooth order algebraic group. However, if we submit an invalid point, we’re not able to compute given and thus cannot get the shared secret. Luckily, the author provides us with a decryption oracle. The padding is also strange:
1 | def pad(b, l): |
The shared secret is in the form of : "\x00"*32 + long_to_bytes(y^s)
.If we upload a message m
in form of : "\x00"*32 + 32*random bytes
, after operation and unpadding ,with a high probability, the message becomes m'="\x00"*31
, as long as the 32 random bytes don’t have the same byte with long_to_bytes(y^s)
in the same position. Therefore the results we receive from the server are exactly and we can recover from the results. Then compute the discrete log of to get server’s private key. It’s all done.
The python solver :
1 | from pwn import * |
OK
This is a standard 1-2 oblivious transfer with almost no vulnerability. We can’t get the two messages simultaneously and therefore we focus on recovering . This writeup is written by thiner ThinerDAS
and roadicing
of NeSE.
1 | v = int(input("v: ")) |
We can easily observe that, if we set , then , so , where secret := pow(flag, e, P)
.
(Note: since and the RHS of this equation is odd,we deduce that d must be odd number.)
Here we naturally ask the question: if we collect enough , can we recover s
?
The answer seems to be yes. By intuition we can find out, for each digit i
, if s[i]
is 1
, then it only contributes 1
in this digit; otherwise in this digit it can contribute 0
or 2
.
This intuition extends naturally for several digits.
1 | def calc_distribution(k=4): |
By experimenting we find out that, the more “1” digits the s
have, the more likely the result is restricted to a small subset of numbers. So for a bit interval, we can count its statistics and easily find out what values of s
are impossible to be here - for example, if we see a 14
appear in one of our samples, then this part in s
cannot be 15
since 15
in s
must entail either 15
or 0
here.
We can expect that as long as we have collected enough , we can recover s
. However, the case is different in this challenge, where we have . Actually it is no different. We can guess the LSB of s
. If we believe that the LSB for s
is 0, then we will add N
to all samples whose LSB is 1; if we believe that the LSB for s
is 1, the same ways around. Therefore, we have divided the problem in this challenge into 2 sub-problems of .
Now finally we handle the . Here I did not investigate the gory detail of a perfect algorithm, and only used a heuristic way of determining the answer. For each bit of s
, I look ahead 12 bits of samples and find the largest number that satisfy all the samples, and I take the LSB of the number to be the corresponding digit of s
. The proof of concept is as follows:
1 | import random |
I am surprised to find out that NN=64
is enough to deduce s
without error most of the time; NN=16
even seems to work.
Here is the final exp.py.