Skip to content

Commit dd63f90

Browse files
committed
happy writeup
1 parent 4d2cf09 commit dd63f90

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

Diff for: 2019-09-02-tokyowesterns/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Team: c7, shalom, nazywam, rev, chivay, Eternal, rodbert, msm, des, psrok1, nied
88
* [Simple logic (crypto)](simple_logic)
99
* [Meow (re/crypto)](meow)
1010
* [M-Poly-Cipher (re/crypto)](mpolycipher)
11+
* [Happy (crypto)](happy)

Diff for: 2019-09-02-tokyowesterns/happy/README.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Happy (crypto, 242p, 36 solved)
2+
3+
In the challenge we get [encrypted flag](flag.enc), [rsa public key](pub.key) and [encryption code](happy).
4+
The code performs generation of RSA-CRT key (although in a bit convoluted way, to make sure there are safe primes used).
5+
Then encryption of the flag is performed using RSA with OAEP SHA1 padding.
6+
Unusually the modulus `N` is here `p*q**k`, but on its own it doesn't yet cause any issues.
7+
From the modulus size of 2300 bits, and minimum bitsize for primes set for 700, we can deduce that `p` and `q` are about 765 bits long and `k=2`.
8+
9+
The key to solve the problem is to notice this:
10+
11+
```ruby
12+
cf = p.pow(q ** (k - 1) * (q - 1) - 1, q ** k)
13+
```
14+
15+
and then
16+
17+
```ruby
18+
def public_key
19+
Key.new(@attr.reject{|k, v| [:p, :q, :d1, :d2, :ce].include?(k)})
20+
end
21+
```
22+
23+
The `cf` parameter is what is normally known as `qInv`, and in our case `cf = modinv(p,q^2)`.
24+
But if you look closely at the public key construction, the omitted parameters are `:p, :q, :d1, :d2, :ce`.
25+
There is no parameter `ce` but there was `cf`!
26+
27+
This means that actually the value of `cf` is still stored in the public key, alongside `e` and `N`.
28+
29+
In conclusion, we know a small part of RSA-CRT private key -> we know value `qInv` such that `qInv*p == 1 mod q**2`
30+
31+
What we would like to calculate is `p` (or `q`).
32+
Let's rephrase that into an equation:
33+
34+
`f(x) = qInv*x - 1 == 0 mod q**2`
35+
36+
We want to solve such equation, because non-trivial root of this polynomial has to be `p`.
37+
Once it's written like this, it's pretty clear that it look a lot like polynomial for Coppersmith theorem.
38+
Just to recap:
39+
40+
Given polynomial `f(x)` such that `f(x) == 0 mod N` there is a polynomial algorithm to calculate `small roots` of such polynomial mod some factor of `N` bigger than `N^beta` (where beta can be anything `0<beta<1`).
41+
Small in this case mean that for polynomial of degree `d` they have to be smaller than `N^(beta^2)/d`.
42+
43+
In our case `p` is of similar order to `q`, so about `N^1/3` and degree of the polynomial is `1`.
44+
We know that `q^2` is about `N^2/3` and is a factor of `N`, so we can look for roots modulo `N^2/3` thus `beta = 0.6`.
45+
And with this constraint we should be able to find roots smaller than `N^(4/9)`, and we know `p` is about `N^1/3` so `N^3/9`, so it's smaller than the bound, and we should be able to find it.
46+
47+
We proceed with the code:
48+
49+
```python
50+
def main():
51+
e = 65537
52+
N = 5452318773620154613572502669913080727339917760196646730652258556145398937256752632887555812737783373177353194432136071770417979324393263857781686277601413222025718171529583036919918011865659343346014570936822522629937049429335236497295742667600448744568785484756006127827416640477334307947919462834229613581880109765730148235236895292544500644206990455843770003104212381715712438639535055758354549980537386992998458659247267900481624843632733660905364361623292713318244751154245275273626636275353542053068704371642619745495065026372136566314951936609049754720223393857083115230045986813313700617859091898623345607326632849260775745046701800076472162843326078037832455202509171395600120638911
53+
qinv = 25895436290109491245101531425889639027975222438101136560069483392652360882638128551753089068088836092997653443539010850513513345731351755050869585867372758989503310550889044437562615852831901962404615732967948739458458871809980240507942550191679140865230350818204637158480970417486015745968144497190368319745738055768539323638032585508830680271618024843807412695197298088154193030964621282487334463994562290990124211491040392961841681386221639304429670174693151
54+
55+
P.<x> = PolynomialRing(Zmod(N), implementation='NTL')
56+
pol = x*qinv - 1
57+
pol = pol.monic()
58+
roots = pol.small_roots(X=2**765, beta=0.6)
59+
print("Potential solutions:")
60+
for p in roots:
61+
q = isqrt(int(N)/int(p))
62+
phi = (p-1)*(q-1)*q
63+
d = inverse_mod(e, phi)
64+
print(d)
65+
66+
main()
67+
```
68+
69+
And almost immediately we get back `d = 313643312579885910144930879740792443079046797319702735470940304815114423813387207622962378717692956907985131193206173468032955155911357015790117906931310982300638685119345225585365379933984401550490180088069653940748930777249398681018529181837718088338410634951815720591986027326920386342449211862769317826747179543111987382083071211027548820393280953703100868439675930431579069835763288141197755585262721361909904809472100941962440764955942607730932895387899109482973057485978370088173899965076238641801547197089631820258212320243267074873219925727866388979716767504927253295557727184298435208619716766017226260721754210366993951047440280419099305076176216010566878368093440228299300269753`
70+
71+
Now we only need to decrypt the flag.
72+
It's a bit tricky, since unlike in most CTFs, it's not textbook RSA but OAEP.
73+
74+
To decrypt the flag we need:
75+
76+
```python
77+
from Crypto.PublicKey import RSA
78+
from cryptography.hazmat.backends import default_backend
79+
from cryptography.hazmat.primitives import hashes, serialization
80+
from cryptography.hazmat.primitives.asymmetric import padding
81+
82+
83+
def main():
84+
e = 65537L
85+
N = 5452318773620154613572502669913080727339917760196646730652258556145398937256752632887555812737783373177353194432136071770417979324393263857781686277601413222025718171529583036919918011865659343346014570936822522629937049429335236497295742667600448744568785484756006127827416640477334307947919462834229613581880109765730148235236895292544500644206990455843770003104212381715712438639535055758354549980537386992998458659247267900481624843632733660905364361623292713318244751154245275273626636275353542053068704371642619745495065026372136566314951936609049754720223393857083115230045986813313700617859091898623345607326632849260775745046701800076472162843326078037832455202509171395600120638911
86+
d = 313643312579885910144930879740792443079046797319702735470940304815114423813387207622962378717692956907985131193206173468032955155911357015790117906931310982300638685119345225585365379933984401550490180088069653940748930777249398681018529181837718088338410634951815720591986027326920386342449211862769317826747179543111987382083071211027548820393280953703100868439675930431579069835763288141197755585262721361909904809472100941962440764955942607730932895387899109482973057485978370088173899965076238641801547197089631820258212320243267074873219925727866388979716767504927253295557727184298435208619716766017226260721754210366993951047440280419099305076176216010566878368093440228299300269753
87+
key = RSA.construct((N, e, d))
88+
pemkey = key.exportKey()
89+
encrypted = open("flag.enc", 'rb').read()
90+
private_key = serialization.load_pem_private_key(
91+
pemkey,
92+
password=None,
93+
backend=default_backend()
94+
)
95+
original_message = private_key.decrypt(
96+
encrypted,
97+
padding.OAEP(
98+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
99+
algorithm=hashes.SHA1(),
100+
label=None
101+
)
102+
)
103+
print(original_message)
104+
105+
106+
main()
107+
```
108+
109+
Which finally gives `TWCTF{I_m_not_sad__I_m_happy_always}`

Diff for: 2019-09-02-tokyowesterns/happy/flag.enc

287 Bytes
Binary file not shown.

Diff for: 2019-09-02-tokyowesterns/happy/happy

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env ruby
2+
require 'openssl/oaep'
3+
require 'optparse'
4+
5+
class Key
6+
def initialize(attr)
7+
@attr = attr
8+
end
9+
10+
def self.generate_key(bits, k)
11+
while true
12+
p = OpenSSL::BN::generate_prime(bits, true).to_i
13+
q = OpenSSL::BN::generate_prime(bits, true).to_i
14+
e = 65537
15+
next if e.gcd((p - 1) * (q - 1) * q ** (k - 1)) > 1
16+
d1 = e.pow((p - 1) / 2 - 2, (p - 1))
17+
fail unless d1 * e % (p - 1) == 1
18+
d2 = e.pow(((q - 1) / 2 - 1) * (q - 1) * (k > 1 ? q ** (k - 2) : 1) - 1, q ** (k - 1) * (q - 1))
19+
fail unless d2 * e % (q ** (k - 1) * (q - 1)) == 1
20+
cf = p.pow(q ** (k - 1) * (q - 1) - 1, q ** k)
21+
return Key.new({
22+
n: p * q ** k,
23+
e: e,
24+
p: p,
25+
q: q ** k,
26+
d1: d1,
27+
d2: d2,
28+
cf: cf,
29+
})
30+
break
31+
end
32+
end
33+
34+
def private?
35+
@attr.key?(:d1) && @attr.key?(:d2) && @attr.key?(:p) && @attr.key?(:q)
36+
end
37+
38+
def public?
39+
@attr.key?(:n) && @attr.key?(:e)
40+
end
41+
42+
def public_key
43+
Key.new(@attr.reject{|k, v| [:p, :q, :d1, :d2, :ce].include?(k)})
44+
end
45+
46+
def self.import(str)
47+
Key.new(Marshal.load(str))
48+
end
49+
50+
def to_s
51+
Marshal.dump(@attr)
52+
end
53+
54+
def public_encrypt(str, pad = true)
55+
raise StandardError.new('NotPublicKey') unless public?
56+
n, e = @attr[:n], @attr[:e]
57+
if pad
58+
msg = OpenSSL::PKCS1.add_oaep_mgf1(str, (n.to_s(16).size + 1) / 2)
59+
else
60+
msg = str
61+
end
62+
long_to_bytes(msg.unpack1('H*').to_i(16).pow(e, n))
63+
end
64+
65+
def private_decrypt(str, pad = true)
66+
raise StandardError.new('NotPrivateKey') unless private?
67+
n, p, q, d1, d2, c = @attr[:n], @attr[:p], @attr[:q], @attr[:d1], @attr[:d2], @attr[:cf]
68+
enc = str.unpack1('H*').to_i(16)
69+
e1 = enc.pow(d1, p)
70+
e2 = enc.pow(d2, q)
71+
ret = long_to_bytes(e1 + p * ((c * (e2 - e1)) % q))
72+
ret = "\0" + ret while ret.size < (n.to_s(16).size + 1) / 2
73+
pad ? OpenSSL::PKCS1.check_oaep_mgf1(ret) : ret
74+
end
75+
76+
private
77+
78+
def long_to_bytes(l)
79+
r = '%x' % l
80+
r = '0' + r if r.size.odd?
81+
return [r].pack('H*')
82+
end
83+
end
84+
85+
def main
86+
if ARGV[0] == 'keygen'
87+
if ARGV.size != 5
88+
STDERR.puts "Usage: happy keygen <bits> <k> <path_to_private_key> <path_to_public_key>"
89+
exit 1
90+
end
91+
bits = ARGV[1].to_i
92+
k = ARGV[2].to_i
93+
priv = ARGV[3]
94+
pub = ARGV[4]
95+
if bits < 700 || k < 1
96+
STDERR.puts "Invalid parameter"
97+
exit 1
98+
end
99+
key = Key.generate_key(bits, k)
100+
File.binwrite(ARGV[3], key.to_s)
101+
File.binwrite(ARGV[4], key.public_key.to_s)
102+
elsif ARGV[0] == 'encrypt'
103+
if ARGV.size != 4
104+
STDERR.puts "Usage: happy encrypt <path_to_key_file> <path_to_input> <path_to_output>"
105+
exit 1
106+
end
107+
key = Key.import(File.binread(ARGV[1]))
108+
File.binwrite(ARGV[3], key.public_encrypt(File.binread(ARGV[2])))
109+
elsif ARGV[0] == 'decrypt'
110+
if ARGV.size != 4
111+
STDERR.puts "Usage: happy decrypt <path_to_key_file> <path_to_input> <path_to_output>"
112+
exit 1
113+
end
114+
key = Key.import(File.binread(ARGV[1]))
115+
File.binwrite(ARGV[3], key.private_decrypt(File.binread(ARGV[2])))
116+
else
117+
STDERR.puts "Usage: happy keygen/encrypt/decrypt"
118+
exit 1
119+
end
120+
end
121+
122+
main if __FILE__ == $0

Diff for: 2019-09-02-tokyowesterns/happy/pub.key

506 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)