Skip to content

Commit cda0db4

Browse files
committed
Participate Google CTF 2023 Quals
1 parent 8728cf1 commit cda0db4

File tree

9 files changed

+668
-0
lines changed

9 files changed

+668
-0
lines changed

GoogleCTF/2023 Quals/mhk2/MHK2.sage

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import secrets
2+
3+
random = secrets.SystemRandom()
4+
5+
MSG = "CTF{????}"
6+
7+
8+
class PrivateKey:
9+
def __init__(self, length: int = 256, keytup: tuple = ()):
10+
if keytup:
11+
self.s1, self.s2, self.s, self.p1, self.p2, self.e1, self.e2 = keytup
12+
else:
13+
while True:
14+
self.s1 = self._gen_sequence(length)
15+
self.p1 = sum(self.s1) + 2
16+
self.e1 = self._gen_pos_ints(self.p1)
17+
if is_prime(self.p1): break
18+
19+
while True:
20+
self.s2 = self._gen_sequence(length)
21+
self.p2 = sum(self.s2) + 2
22+
self.e2 = self._gen_pos_ints(self.p2)
23+
if is_prime(self.p2): break
24+
25+
self.s = [self.s1[i] + self.s2[i] for i in range(length)]
26+
assert self.p1 != self.p2
27+
28+
def _gen_sequence(self, length: int) -> list[int]:
29+
return [random.getrandbits(128) for _ in range(length)]
30+
31+
def _gen_pos_ints(self, p) -> int:
32+
return random.randint((p-1)//2, p-1)
33+
34+
def export_secret(self):
35+
return {"s1": self.s1, "s2": self.s2, "s": self.s,
36+
"p1": self.p1, "p2": self.p2, "e1": self.e1, "e2": self.e2}
37+
38+
39+
class PublicKey:
40+
def __init__(self, private_key: PrivateKey):
41+
self.a1 = [(private_key.e1 * s) % private_key.p1 for s in private_key.s1]
42+
self.a2 = [(private_key.e2 * s) % private_key.p2 for s in private_key.s2]
43+
44+
self.b1 = [i % 2 for i in private_key.s1]
45+
self.b2 = [i % 2 for i in private_key.s2]
46+
self.b = [i % 2 for i in private_key.s]
47+
48+
self.t = random.randint(1, 2)
49+
self.c = self.b1 if self.t == 1 else self.b2
50+
51+
def public_key_export(self):
52+
return {"a1": self.a1, "a2": self.a2, "b": self.b, "c": self.c}
53+
54+
55+
class MHK2:
56+
def __init__(
57+
self,
58+
length: int,
59+
private_key: PrivateKey = PrivateKey,
60+
public_key: PublicKey = PublicKey,
61+
):
62+
self.private_key = private_key(length)
63+
self.public_key = public_key(self.private_key)
64+
65+
def _random_bin_sequence(self, n):
66+
return [random.randint(0, 1) for _ in range(n)]
67+
68+
def encrypt(self, msg: str):
69+
ciphertext = []
70+
msg_int = f'{(int.from_bytes(str.encode(msg), "big")):b}'
71+
for i in msg_int:
72+
ciphertext.append(self.encrypt_bit(int(i)))
73+
return ciphertext
74+
75+
def decrypt(self, ciphertext):
76+
plaintext_bin = ""
77+
for i in ciphertext:
78+
plaintext_bin += str(self.decrypt_bit(i))
79+
80+
split_bin = [plaintext_bin[i : i + 7] for i in range(0, len(plaintext_bin), 8)]
81+
82+
plaintext = ""
83+
for seq in split_bin:
84+
plaintext += chr(int(seq, 2))
85+
return plaintext
86+
87+
# single bit {0,1}
88+
def encrypt_bit(self, bit):
89+
r1 = self._random_bin_sequence(len(self.public_key.b))
90+
r2 = self._random_bin_sequence(len(self.public_key.b))
91+
92+
m1 = sum([(self.public_key.b[i] * r1[i]) for i in range(len(r1))]) % 2
93+
m2 = sum([(self.public_key.b[i] * r2[i]) for i in range(len(r2))]) % 2
94+
95+
eq = sum([(self.public_key.c[i] * r1[i]) for i in range(len(r1))]) == sum(
96+
[(self.public_key.c[i] * r2[i]) for i in range(len(r2))]
97+
)
98+
99+
while m1 != bit or m2 != bit or not eq or r1 == r2:
100+
r1 = self._random_bin_sequence(len(self.public_key.b))
101+
r2 = self._random_bin_sequence(len(self.public_key.b))
102+
103+
m1 = (
104+
sum(
105+
[
106+
(self.public_key.b[i] * r1[i])
107+
for i in range(len(self.public_key.b))
108+
]
109+
)
110+
% 2
111+
)
112+
m2 = (
113+
sum(
114+
[
115+
(self.public_key.b[i] * r2[i])
116+
for i in range(len(self.public_key.b))
117+
]
118+
)
119+
% 2
120+
)
121+
122+
eq = sum(
123+
[(self.public_key.c[i] * r1[i]) for i in range(len(self.public_key.b))]
124+
) == sum(
125+
[(self.public_key.c[i] * r2[i]) for i in range(len(self.public_key.b))]
126+
)
127+
128+
C1 = sum([(self.public_key.a1[i] * r1[i]) for i in range(len(r1))])
129+
C2 = sum([(self.public_key.a2[i] * r2[i]) for i in range(len(r2))])
130+
return C1, C2
131+
132+
def decrypt_bit(self, ciphertext: tuple[int, int]) -> int:
133+
C1, C2 = ciphertext
134+
M1 = (
135+
pow(self.private_key.e1, -1, self.private_key.p1) * C1 % self.private_key.p1
136+
)
137+
M2 = (
138+
pow(self.private_key.e2, -1, self.private_key.p2) * C2 % self.private_key.p2
139+
)
140+
m = (M1 + M2) % 2
141+
return m
142+
143+
144+
def main():
145+
crypto = MHK2(256)
146+
ciphertext = crypto.encrypt(MSG)
147+
plaintext = crypto.decrypt(ciphertext)
148+
149+
print(crypto.public_key.public_key_export())
150+
print(ciphertext)
151+
152+
assert plaintext == MSG
153+
154+
155+
if __name__ == "__main__":
156+
main()

GoogleCTF/2023 Quals/mhk2/output.py

+2
Large diffs are not rendered by default.

GoogleCTF/2023 Quals/mhk2/output.txt

+2
Large diffs are not rendered by default.

GoogleCTF/2023 Quals/mhk2/solver.sage

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from output import cipher, params
2+
3+
N = 256
4+
LAMBDA = 32 # LLL scaling
5+
6+
a1 = params["a1"]
7+
a2 = params["a2"]
8+
9+
b = params["b"]
10+
11+
b1 = params["c"]
12+
b2 = [(b1[i] + b[i]) % 2 for i in range(N)]
13+
14+
assert N == len(a1) == len(a2) == len(b) == len(b1) == len(b2)
15+
16+
17+
def solve_one(v, sum_v, bits):
18+
m = []
19+
for i in range(N):
20+
t = [0] * (N + 2)
21+
t[i] = 1
22+
t[N] = -LAMBDA * v[i]
23+
m.append(t)
24+
# Final row
25+
t = [0] * (N + 2)
26+
t[N] = LAMBDA * sum_v
27+
t[N + 1] = 1
28+
m.append(t)
29+
30+
mat = matrix(ZZ, m)
31+
lll = mat.LLL()
32+
33+
for row in lll:
34+
if row[N + 1] == 1 and row[N] == 0:
35+
coeff_row = row
36+
break
37+
else:
38+
print("No row found...")
39+
exit(1)
40+
41+
bit = 0
42+
for i in range(N):
43+
bit = (bit + bits[i] * coeff_row[i]) % 2
44+
45+
return bit
46+
47+
48+
if len(cipher) % 8 == 0:
49+
plaintext_bin = ""
50+
else:
51+
plaintext_bin = "0" * (8 - len(cipher) % 8)
52+
53+
for (i, c_bit) in enumerate(cipher):
54+
m1 = solve_one(a1, c_bit[0], b2)
55+
m2 = solve_one(a2, c_bit[1], b1)
56+
print(m1, m2)
57+
58+
plaintext_bin += str((m1 + m2) % 2)
59+
print(f"{i:03}/{len(cipher)}: {plaintext_bin}")
60+
61+
split_bin = [plaintext_bin[i : i + 8] for i in range(0, len(plaintext_bin), 8)]
62+
63+
plaintext = ""
64+
for seq in split_bin:
65+
plaintext += chr(int(seq, 2))
66+
67+
# CTF{faNNYPAcKs_ARe_4maZiNg_AnD_und3Rr@t3d}
68+
print(plaintext)
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
diff --git a/include/Attack.hpp b/include/Attack.hpp
2+
index 0171073..5be5673 100644
3+
--- a/include/Attack.hpp
4+
+++ b/include/Attack.hpp
5+
@@ -54,6 +54,10 @@ class Attack
6+
u32arr<CONTIGUOUS_SIZE> zlist;
7+
u32arr<CONTIGUOUS_SIZE> ylist; // the first two elements are not used
8+
u32arr<CONTIGUOUS_SIZE> xlist; // the first four elements are not used
9+
+
10+
+ // Custom verification logic
11+
+ bytevec extraCipher, extraPlain;
12+
+ std::size_t extraPlainPosition;
13+
};
14+
15+
/// \brief Iterate on Zi[2,32) candidates to try and find complete internal keys
16+
diff --git a/src/Attack.cpp b/src/Attack.cpp
17+
index 21c2272..6938851 100644
18+
--- a/src/Attack.cpp
19+
+++ b/src/Attack.cpp
20+
@@ -4,9 +4,44 @@
21+
#include "KeystreamTab.hpp"
22+
#include "MultTab.hpp"
23+
24+
+#include <iomanip>
25+
+
26+
Attack::Attack(const Data& data, std::size_t index, std::vector<Keys>& solutions, bool exhaustive, Progress& progress)
27+
: data(data), index(index + 1 - Attack::CONTIGUOUS_SIZE), solutions(solutions), exhaustive(exhaustive), progress(progress)
28+
-{}
29+
+{
30+
+ extraPlainPosition = 11;
31+
+ for (auto c : "\x9F\x61\x80\xB0\xE2\xFB\x7C\x52\x9E\x28\x51\x02\x5F\x12\x1E\x89") {
32+
+ if (c == 0) break;
33+
+ extraCipher.push_back(c);
34+
+ }
35+
+ for (auto c : "\x3b\xe2\xf2\xba\x77") {
36+
+ if (c == 0) break;
37+
+ extraPlain.push_back(c);
38+
+ }
39+
+
40+
+
41+
+ std::cout << std::setfill('0') << std::hex;
42+
+
43+
+ std::cout << "cipherText: ";
44+
+ for(byte c : data.ciphertext) {
45+
+ std::cout << std::setw(2) << static_cast<int>(c) << ' ';
46+
+ }
47+
+ std::cout << std::endl;
48+
+
49+
+ std::cout << "extraCipher: ";
50+
+ for(byte c : extraCipher) {
51+
+ std::cout << std::setw(2) << static_cast<int>(c) << ' ';
52+
+ }
53+
+ std::cout << std::endl;
54+
+
55+
+ std::cout << "extraPlain: ";
56+
+ for(byte c : extraPlain) {
57+
+ std::cout << std::setw(2) << static_cast<int>(c) << ' ';
58+
+ }
59+
+ std::cout << std::endl;
60+
+
61+
+ std::cout << std::setfill(' ') << std::dec;
62+
+}
63+
64+
void Attack::carryout(uint32 z7_2_32)
65+
{
66+
@@ -155,6 +190,31 @@ void Attack::testXlist()
67+
return;
68+
}
69+
70+
+ // Custom check for junk.dat
71+
+ Keys customKey(x, ylist[3], zlist[3]);
72+
+ using rit = std::reverse_iterator<bytevec::const_iterator>;
73+
+ for(rit p = rit(data.plaintext.begin() + index + 3),
74+
+ c = rit(data.ciphertext.begin() + data.offset + index + 3);
75+
+ p != data.plaintext.rend();
76+
+ ++p, ++c)
77+
+ {
78+
+ customKey.updateBackward(*c);
79+
+ }
80+
+
81+
+ // First, run backward to reset the state
82+
+ customKey.updateBackward(data.ciphertext, data.offset, 0);
83+
+
84+
+ // Then, run it forward again with `junk.dat` data
85+
+ for (std::size_t idxWithHeader = 0; idxWithHeader < extraCipher.size(); idxWithHeader++) {
86+
+ byte c = extraCipher[idxWithHeader];
87+
+ byte expectedP = c ^ customKey.getK();
88+
+ if (idxWithHeader >= extraPlainPosition
89+
+ && expectedP != extraPlain[idxWithHeader - extraPlainPosition]) {
90+
+ return;
91+
+ }
92+
+ customKey.update(expectedP);
93+
+ }
94+
+
95+
// all tests passed so the keys are found
96+
97+
// get the keys associated with the initial state
98+
diff --git a/src/Data.cpp b/src/Data.cpp
99+
index 194c298..5a14cce 100644
100+
--- a/src/Data.cpp
101+
+++ b/src/Data.cpp
102+
@@ -141,8 +141,8 @@ Data::Data(bytevec ciphertextArg, bytevec plaintextArg, int offsetArg, const std
103+
// check that there is enough known plaintext
104+
if(plaintext.size() < Attack::CONTIGUOUS_SIZE)
105+
throw Error("not enough contiguous plaintext ("+std::to_string(plaintext.size())+" bytes available, minimum is "+std::to_string(Attack::CONTIGUOUS_SIZE)+")");
106+
- if(plaintext.size() + extraPlaintext.size() < Attack::ATTACK_SIZE)
107+
- throw Error("not enough plaintext ("+std::to_string(plaintext.size() + extraPlaintext.size())+" bytes available, minimum is "+std::to_string(Attack::ATTACK_SIZE)+")");
108+
+ // if(plaintext.size() + extraPlaintext.size() < Attack::ATTACK_SIZE)
109+
+ // throw Error("not enough plaintext ("+std::to_string(plaintext.size() + extraPlaintext.size())+" bytes available, minimum is "+std::to_string(Attack::ATTACK_SIZE)+")");
110+
111+
// reorder remaining extra plaintext for filtering
112+
{

0 commit comments

Comments
 (0)