From d98befe85aad7f4737a828fbb1d412e3179b8131 Mon Sep 17 00:00:00 2001 From: xenia Date: Tue, 19 Sep 2023 04:17:07 -0400 Subject: [PATCH] csawq23 --- 2023/csawq/.gitignore | 1 + 2023/csawq/00-intro.md | 14 ++ 2023/csawq/01-crypto-blocky.md | 393 +++++++++++++++++++++++++++++++++ 2023/csawq/02-crypto-poker.md | 371 +++++++++++++++++++++++++++++++ 2023/csawq/03-end.md | 5 + 2023/csawq/attack_blocky.sage | 133 +++++++++++ 2023/csawq/attack_poker.py | 161 ++++++++++++++ 2023/csawq/build.ninja | 16 ++ 8 files changed, 1094 insertions(+) create mode 100644 2023/csawq/.gitignore create mode 100644 2023/csawq/00-intro.md create mode 100644 2023/csawq/01-crypto-blocky.md create mode 100644 2023/csawq/02-crypto-poker.md create mode 100644 2023/csawq/03-end.md create mode 100644 2023/csawq/attack_blocky.sage create mode 100644 2023/csawq/attack_poker.py create mode 100644 2023/csawq/build.ninja diff --git a/2023/csawq/.gitignore b/2023/csawq/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/2023/csawq/.gitignore @@ -0,0 +1 @@ +/build diff --git a/2023/csawq/00-intro.md b/2023/csawq/00-intro.md new file mode 100644 index 0000000..94d9793 --- /dev/null +++ b/2023/csawq/00-intro.md @@ -0,0 +1,14 @@ +# CSAW quals 2023 post + +i played CSAW quals this past weekend + +there were some good parts and some bad parts. overall i think CSAW has needed a lot more QC in the +2 years that i've been playing it + +anyway, given that there are official writeups (looking at them, the quality varies greatly by +challenge author, but y'know whatever) i'm going to limit this to two of the challenges i thought +were actually good and we can solve them dragon style :) + +writeup index: +- [crypto: blocky noncense](#copying-and-pasting-code-you-don-t-entirely-understand-for-fun-and-profit-blocky-noncense) +- [crypto: mental poker](#poker-i-barely-even-mental-poker) diff --git a/2023/csawq/01-crypto-blocky.md b/2023/csawq/01-crypto-blocky.md new file mode 100644 index 0000000..dac815a --- /dev/null +++ b/2023/csawq/01-crypto-blocky.md @@ -0,0 +1,393 @@ +## copying and pasting code you don't entirely understand for fun and profit: *blocky noncense* + +> I designed this foolproof signing blockchain. I'll even let you sign as many signatures as you +> want! + +it's time for everyone's favorite part of crypto challenges, *source review* + +### `blocks.sage` + +```python +from Crypto.Cipher import AES +import sig_sage as sig # this is generated from sig.sage +import hashlib + +class Chain: + def __init__(self, seed): + self.flag = b"csaw{[REDACTED]}" + self.ecdsa = sig.ECDSA(seed) + self.blocks = {0: [hashlib.sha1(self.flag).digest(), self.ecdsa.sign(self.flag)]} + + def commit(self, message, num): + formatted = self.blocks[num-1][0] + message + sig = self.ecdsa.sign(formatted) + self.blocks[num] = [hashlib.sha256(message).digest(), sig] + + def view_messages(self): + return self.blocks + + def verify_sig(self, r, s, message): + t = self.ecdsa.verify(r, s, message) + return t +``` + +this is the definition of the blockchain, we can see that the first block encodes the SHA-1 hash of +the flag, and every subsequent block is the SHA-256 (!) hash of the previous block hash combined +with the message, and a signature of the same data + +### `sig.sage` + +```python +from Crypto.Util.number import * +from Crypto.Cipher import AES +import random +import hashlib + +def _hash(msg): + return bytes_to_long(hashlib.sha1(msg).digest()) +``` + +the hashes used for signature calculations are SHA-1. tbh i'm not sure why some hashes are SHA-1 and +others are SHA-256, and it's not relevant for the solution but we need to keep it in mind to be able +to replicate operations in the solution code + +```python +class LCG: + def __init__(self, seed, q): + self.q = q + self.a = random.randint(2,self.q) + self.b = random.randint(2,self.a) + self.c = random.randint(2,self.b) + self.d = random.randint(2,self.c) + self.seed = seed + + def next(self): + seed = (self.a*self.seed^3 + self.b*self.seed^2 + self.c*self.seed + self.d) % self.q + self.seed = seed + return seed +``` + +this defines a Linear Congruential Generator, a type of PRNG that uses a polynomial over previous +state to generate new state. this one uses a cubic polynomial with random coefficients + +```python +class ECDSA: + def __init__(self, seed): + self.p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F + self.E = EllipticCurve(GF(self.p), [0, 7]) + self.G = self.E.lift_x(55066263022277343669578718895168534326250603453777594175500187360389116729240) + self.order = 115792089237316195423570985008687907852837564279074904382605163141518161494337 + + self.priv_key = random.randint(2,self.order) + self.pub_key = self.G*self.priv_key + + self.lcg = LCG(seed, self.order) +``` + +it's always useful to double check elliptic curve params, so if we look up these numbers we can find +out that this is the curve `secp256k1` (also used in bitcoin). so it's just a standard curve, no +cheese going on + +the other interesting bit here is the LCG is initialized with a modulus equal to the order of +`secp256k1` - this is sort of convenient for later + +```python + def sign(self, msg): + nonce = self.lcg.next() + hashed = _hash(msg) + + r = int((self.G*nonce)[0]) % self.order + assert r != 0 + s = (pow(nonce,-1,self.order)*(hashed + r*self.priv_key)) % self.order + return (r,s) +``` + +the sign function generates an ECDSA signature using the SHA-1 hash of the given message, and a +nonce value is computed using the LCG + +one important fact you might know about ECDSA is that it's very dangerous to reuse nonces because it +allows recovery of the private key. however, nonce reuse doesn't seem to be an issue here, so we +have to find something else + +also i cut out the `verify` function because we don't actually care + +### `chall.sage` + +it basically just makes a menu to interact with the challenge, it's not super interesting + +### elliptic curve crypto + +[skip this section](#duckduckbing-solve-this-for-me) + +i'm going to actually go over ECC even though you really don't actually need to know what's going on +at all for this. just for context + +**Group**: it's kind of like a generalization of addition (or multiplication??? don't think about it +too hard). for example, the integers with the integer addition operation are a group. specifically, +groups are a **set of elements** with a **binary operator** that takes two elements and produces a +third element that's in the set, and there's an **identity element** and an **inverse for every +element**. also the operation doesn't have to be commutative + +**Field**: a generalization of addition, subtraction, multiplication, and division. something we're +interested in here is a *finite field* which is also called a *Galois field* (after this weird +french revolutionary who solved all of math in his teens and then died in a duel -- fucking +frenchmen am i right). a convenient type of finite field is integers modulo a prime number `p`. you +can take a moment to convince yourself that all arithmetic still works kinda like normal when it's +all modulo a prime number (also think of division as a multiplicative inverse). or don't, i'm not a +textbook author. this is called `GF(p)` + +**Elliptic Curve**: an elliptic curve is defined by the equation `y^2 = x^3 + ax + b`. elliptic +curves can also be defined over *Galois fields*, in which case all the coordinates of points on the +curve will be integers. elliptic curves can conveniently form *groups* with the group operation of +point multiplication. don't worry about point multiplication except knowing that it takes two curve +points and produces a third point + +when considering some elliptic curve `E` on `GF(p)`, a group can be formed using a generator point +`G` that is on the curve, and that is capable of producing every other point in the group by using +the group operator some number of times. so we say that `G` "multiplied" by some scalar `s` as in +`G*s` means that you are applying the group operation on `G` with itself `s` times. confusingly, we +notate this `G*s` despite the fact that it's really more analogous to "raising to a power". but +sometimes, if you have two elliptic curve points `A` and `B`, then `A*B` means simply doing the +group operation on `A` and `B` once. math people are normal, don't worry about it + +the elliptic curve group also has an *order* `q`, which is the number of elements in the group. the +order is not necessarily the same as the size of the field that the curve is defined on (`p != q`) + +there are a bunch of standard curves which are predefined for use in cryptography, so you don't have +to generate your own curves from scratch, which is hard. NIST produces several, and `secp256k1` is +one of the NIST curves + +the property that makes this useful for cryptography is the difficulty of the *Discrete Logarithm +Problem*. basically, if you do `G*s` for some secret `s`, it's computationally Very Hard™ to recover +what `s` was + +and now, finally, we get to signatures. first, generate a keypair. `d` is the private key, a scalar +in the range of `0..q`, and the public key is `Q = G*d`, a curve point + +to sign a message `m`, take its hash `H(m)`. then, generate a random nonce `k` (the RNG must be very +strong). produce two values +```text +r = (G*k).x % q +s = (k^-1) * (H(m) + r*d) % q +``` + +(btw here `(expr).x` means the x-coordinate of a curve point. also sorry for the confusing mixture +of group operation and regular arithmetic in `Zmod(q)`. for further questions, consult the nearest +wikipedia and/or crypto girl infodump) + +the signature consists of the pair `(r, s)`, which you can also see in the challenge code + +next, to verify a signature + +```text +u1 = H(m)*s^-1 % n +u2 = r*s^-1 % n + +r == (u1*G + u2*Q).x +``` + +extracting the private key from a signature would require solving the discrete log problem, and +signatures cannot be forged without the private key + +and that's basically it. now you know about elliptic curves + +### duckduckbing solve this for me + +top 10 CTF skills you need to have in 2023 -- search online for the solution to a problem + +so i immediately went and searched for something like "ECDSA LCG attack", y'know just based on the +algorithms that are present here and what we want to do, and find this 2023 paper: + + +that's pretty convenient, but we don't have time to read a crypto paper, so scroll down to where +they list the PoC repo: + + +(the key takeaway is that you can construct a polynomial out of the signatures, knowing the +structure of the recurrence relation for the nonces, and then one of the roots of the polynomial is +the private key used for the signatures) + +we need to adapt this to the challenge implementation of #blockchain, so next we take a look at the +basic implementation of the attack, ignoring all the stuff that is specific to bitcoin and ethereum, +which is in `original-attack/recurrence_nonces.py` + +first, there's a comment explaining how many signatures we need to collect for our case + +```python +# N = the number of signatures to use, N >= 4 +# the degree of the recurrence relation is N-3 +# the number of unknown coefficients in the recurrence equation is N-2 +# the degree of the final polynomial in d is 1 + Sum_(i=1)^(i=N-3)i +``` + +since the LCG has 4 unknown coefficients, we need `N=6` + +now, we simply need to do the pro gamer strategy of copying and pasting code we barely understand + +```python +N = 6 +order=115792089237316195423570985008687907852837564279074904382605163141518161494337 + +Z = GF(order) +R = PolynomialRing(Z, names=('dd',)) +(dd,) = R._first_ngens(1) + +# the polynomial we construct will have degree 1 + Sum_(i=1)^(i=N-3)i in dd +# our task here is to compute this polynomial in a constructive way starting from the N signatures in the given list order +# the generic formula will be given in terms of differences of nonces, i.e. k_ij = k_i - k_j where i and j are the signature indexes +# each k_ij is a first-degree polynomial in dd +# this function has the goal of returning it given i and j +def k_ij_poly(i, j): + hi = Z(h[i]) + hj = Z(h[j]) + s_invi = Z(s_inv[i]) + s_invj = Z(s_inv[j]) + ri = Z(r[i]) + rj = Z(r[j]) + poly = dd*(ri*s_invi - rj*s_invj) + hi*s_invi - hj*s_invj + return poly + +# the idea is to compute the polynomial recursively from the given degree down to 0 +# the algorithm is as follows: +# for 4 signatures the second degree polynomial is: +# k_12*k_12 - k_23*k_01 +# so we can compute its coefficients. +# the polynomial for N signatures has degree 1 + Sum_(i=1)^(i=N-3)i and can be derived from the one for N-1 signatures + +# let's define dpoly(i, j) recursively as the dpoly of degree i starting with index j + +def dpoly(n, i, j): + if i == 0: + return (k_ij_poly(j+1, j+2))*(k_ij_poly(j+1, j+2)) - (k_ij_poly(j+2, j+3))*(k_ij_poly(j+0, j+1)) + else: + left = dpoly(n, i-1, j) + for m in range(1,i+2): + left = left*(k_ij_poly(j+m, j+i+2)) + right = dpoly(n, i-1, j+1) + for m in range(1,i+2): + right = right*(k_ij_poly(j, j+m)) + return (left - right) +``` + +### rest of the fucking owl + +now with the "hard" stuff out of the way we're actually gaming + +let's build the rest of the solution + +```python +def _hex(x): + return binascii.hexlify(x).decode() + +def _hash(msg): + return bytes_to_long(hashlib.sha1(msg).digest()) + +conn = nclib.Netcat(("crypto.csaw.io", 5002)) +``` + +so the strategy is: we make 6 messages with known contents, and then we retrieve the contents of the +blockchain, which includes the message hashes (note: SHA-256 hashes, the ones used for block +chaining, not the SHA-1 hashes used for the signature itself) and signatures. this also retrieves +the original block containing the flag, but since we don't know the flag we don't include that in +the attack algorithm + +```python +h = [] +sgns = [] +msgs = [] + +def get_messages(count): + all_msgs = [] + + conn.recv_until(b": ") + conn.send_line(b"2") + + for i in range(count): + conn.recv_until(f"Block {i}\n".encode()) + msg_line = conn.recv_line() + print(msg_line) + msg = binascii.unhexlify(msg_line.strip().split(b" ")[1]) + sig_line = conn.recv_line() + sig_rs = sig_line.decode().split("(")[1].split(")")[0] + r, s = sig_rs.split(", ") + r = int(r) + s = int(s) + all_msgs.append((msg, r, s)) + + return all_msgs + +for i in range(N): + msg = f"meow{i}".encode() + msgs.append(msg) + conn.recv_until(b": ") + conn.send_line(b"1") + conn.send_line(_hex(msg)) + +all_msgs = get_messages(N+1) +``` + +next, we process the data into a form suitable for the paper's PoC code + +```python +for i in range(N): + (_, r, s) = all_msgs[i+1] + prev_msg = all_msgs[i][0] + real_hash = _hash(prev_msg + msgs[i]) + h.append(real_hash) + sgns.append((r,s)) + +s_inv = [] +s = [] +r = [] +for i in range(N): + s.append(sgns[i][1]) + r.append(sgns[i][0]) + s_inv.append(ecdsa.numbertheory.inverse_mod(s[i], order)) +``` + +finally, do the attack + +```python +poly_target = dpoly(N-4, N-4, 0) +d_guesses = poly_target.roots() +print("results!!!!!\n\n\n") +print(d_guesses) +``` + +and send it to the server, which should then give back the flag + +```python +if len(d_guesses) == 1: + conn.recv_until(b": ") + conn.send_line(b"4") + res = str(d_guesses[0][0]).encode() + print("SENDING", res) + conn.send_line(res) + print(conn.recv_all(timeout=5)) +``` + +let's run this on the local copy and you can see it prints the flag + +```bash +ncat --exec "/usr/bin/sage chall.sage" -lp 1337 & + +sage attack.sage + +b'Message 0058d5ebdef0730f71787e6ff2617ffdd3d0060f\n' +b'Message 5be17c92f7ecb50ab5a44969957c3c036122f7dbd8e2cd4fd7a5c1974ba22f66\n' +b'Message fec07ac858649497dedb5136d9c3eff1e61394c549286888fb0a893d74f035f2\n' +b'Message 1dbbae23136fd579d032d52cb6c5b516ead212103f0d2285092b9c5f1412bced\n' +b'Message 4231d9b332338107def060b2a271bf6d6aded7b643b54744538e99901b38886a\n' +b'Message 2014ed4b6e2ba09e8d3c5be90d81881d4ac30531adc7644436122386d2ee42b3\n' +b'Message 844005fbfdd684f7e9e07f751a48d2550b288f702bb3ed0004735471a41b1229\n' +(((k12*k12-k23*k01)*k13*k23-(k23*k23-k34*k12)*k01*k02)*k14*k24*k34-((k23*k23-k34*k12)*k24*k34-(k34*k34-k45*k23)*k12*k13)*k01*k02*k03)results!!!!! + + + +[(109627846673527107412168645315617142862787762347616623632546915862323406286018, 1)] +SENDING b'109627846673527107412168645315617142862787762347616623632546915862323406286018' +b"So you think you can get the flag huh? Try your luck.\nPrivate Key: You must be our admin. Here's the flag b'csaw{[REDACTED]}'\n" +``` + +full attack script: + diff --git a/2023/csawq/02-crypto-poker.md b/2023/csawq/02-crypto-poker.md new file mode 100644 index 0000000..a913faf --- /dev/null +++ b/2023/csawq/02-crypto-poker.md @@ -0,0 +1,371 @@ +## poker? i barely even- (mental poker) + +> Let's play some mental poker. + +it's another crypto challenge ! + +let's look at the source + +```python +class PRNG: + def __init__(self, seed = int(os.urandom(8).hex(),16)): + self.seed = seed + self.state = [self.seed] + self.index = 64 + for i in range(63): + self.state.append((3 * (self.state[i] ^ (self.state[i-1] >> 4)) + i+1)%64) + + def __str__(self): + return f"{self.state}" + + def getnum(self): + if self.index >= 64: + for i in range(64): + y = (self.state[i] & 0x20) + (self.state[(i+1)%64] & 0x1f) + val = y >> 1 + val = val ^ self.state[(i+42)%64] + if y & 1: + val = val ^ 37 + self.state[i] = val + self.index = 0 + seed = self.state[self.index] + self.index += 1 + return (seed*15 + 17)%(2**6) +``` + +this implements a Mersenne Twister PRNG, seeded with `os.urandom`. the output is limited to 6 bits, +and seemingly it uses 64 bits of randomness to initialize the PRNG + +then we have a fairly uninteresting implementation of poker which i'm going to skip over. there +isn't anything particularly wrong with it + +```python +def shuffle(deck): + new_deck = [] + for i in range(len(deck)): + x = rng.getnum() + if deck[x] not in new_deck: + new_deck.append(deck[x]) + elif deck[i] not in new_deck: + new_deck.append(deck[i]) + else: + for card in deck: + if card not in new_deck: + new_deck.append(card) + break + return new_deck +``` + +this shuffles the deck based on the PRNG above + +ok and now for the weird stuff + +```python +p = getPrime(300) +q = getPrime(300) +phi = (p-1)*(q-1) +N = p*q +print(f"Let's play some mental poker!\nIf you win 10 consecutive times, I'll give you a prize!\nHere are the primes we will use to generate our RSA public and private exponents --> {p}, {q}") +``` + +we get an RSA *private key* just like, for free. then (not shown here), we're prompted for a public +and private exponent for RSA, and the computer also comes up with its own public and private +exponent like this + +```python +while computer_e < 2 or computer_d < 1: + e_array = [] + for _ in range(6): + e_array.append(str(rng.getnum())) + computer_e = int(''.join(e_array)) + if gcd(computer_e, phi) == 1: + computer_d = pow(computer_e,-1,phi) +``` + +interestingly, the card shuffling for the first round is done before the computer's exponents are +generated + +then, on each round, we get an encrypted deck -- that's encrypted with our public exponent as well +as the computer's + +```python +enc_deck = [] +for card in deck: + enc_deck.append(pow(pow(bytes_to_long(str(card).encode()),computer_e,N),player_e,N)) +assert len(set(enc_deck)) == len(deck_str) +print(f"Here is the shuffled encrypted deck --> {enc_deck}") +``` + +we are tasked with shuffling the deck and returning it, still in encrypted form. then, the computer +decrypts the deck and runs the poker algorithm to determine a winner + +if you win 10 rounds, you get the (encrypted) flag + +### the bug + +the flaw in this happens in the PRNG. since this is used to generate the computer's private +exponent, if we can guess PRNG outputs then we can recover enough info to be able to decrypt the +deck fully + +but first, there's a kind of pointless but convenient bug in the way your exponent is inputted. the +only checks done on your input are + +```python +if gcd(player_e, phi) == 1: + # accept + +if (player_e*player_d)%phi == 1: + # accept +``` + +therefore, we can provide a `player_e` and `player_d` that are both `1`. this makes the logic to +decrypt the cards slightly easier + +the issue with the PRNG is that despite at first glance looking like it is seeded with 8 bytes of +`urandom`, it's actually only seeded with 10 real bits of randomness + +in the constructor, the state array is initialized with + +```python +for i in range(63): + self.state.append((3 * (self.state[i] ^ (self.state[i-1] >> 4)) + i+1)%64) +``` + +`state[0]` initially contains the full seed, but according to this code we can see only the lower 6 +bits (due to the modulo) combined with 4 more bits (due to the bit shift) actually influence the +values of the other items in the state array. then, when the first call to `getnum` happens, the +twist operation is immediately performed, which normalizes all the values to be modulo 64, so +`state[0]` also ends up only dependent on the lower 6 bits of the seed value + +we can verify this for ourselves + +```python +[ins] In [1]: from server import PRNG + +[ins] In [2]: PRNG(0x1337c00 | 420).getnum() +Out[2]: 35 + +[nav] In [3]: PRNG(420).getnum() +Out[3]: 35 +``` + +so basically, we have an extremely bruteforceable search space of 10 bits. that's a lot better than +64!! + +### it's pokin' time / poked all over the place + +given some copying and pasting of server code as well as importing it directly from the server +module, the solution script can basically simulate the same thing the server is doing each round + +```python +from server import PRNG, Card, card_str_to_Card, determine_winner + +def sim_round(deck, rng, phi, computer_e, computer_d): + deck = shuffle(rng, deck) + print("local hacks 1", rng.seed, rng.index, rng.state) + while computer_e < 2 or computer_d < 1: + e_array = [] + for _ in range(6): + e_array.append(str(rng.getnum())) + computer_e = int(''.join(e_array)) + if gcd(computer_e, phi) == 1: + computer_d = pow(computer_e,-1,phi) + + return computer_e, computer_d, deck + +def shuffle(rng, deck): + new_deck = [] + for i in range(len(deck)): + x = rng.getnum() + if deck[x] not in new_deck: + new_deck.append(deck[x]) + elif deck[i] not in new_deck: + new_deck.append(deck[i]) + else: + for card in deck: + if card not in new_deck: + new_deck.append(card) + break + return new_deck +``` + +next, we need to generate the deck + +```python +deck = [] +deck_str = [] +for suit in ["Spades", "Hearts", "Diamonds", "Clubs"]: + for i in range(16): + deck.append(Card(suit, i)) + deck_str.append(str(deck[-1])) +deck_str = set(deck_str) + +server_deck = list(deck) +``` + +`server_deck` will hold the copy of the deck that we think the server currently holds + +we initialize the game by receiving `p` and `q` and sending `player_e` and `player_d` + +```python +r = remote("crypto.csaw.io", 5001) + +r.recvuntil(b"Here are") +r.recvuntil(b"--> ") + +p, q = r.recvline().decode().strip().split(", ") +p = int(p) +q = int(q) +phi = (p-1)*(q-1) +N = p*q + +print("p=", p) +print("q=", q) + +player_e = 1 +player_d = 1 + +print("player_e=", player_e) +print("player_d=", player_d) + +r.recvuntil(b">>") +r.sendline(str(player_e).encode()) +r.recvuntil(b">>") +r.sendline(str(player_d).encode()) +``` + +next, we get the deck for the first round + +```python +r.recvuntil(b"***") +print(r.recvuntil(b"--> ")) +deck = r.recvline().strip().decode() +deck = ast.literal_eval(deck) +``` + +now, we do the brute force to guess what the server's PRNG was seeded with. this is done over the 10 +bits of seed search space, and the offset to the beginning of the PRNG outputs that are used to +generate the exponent + +```python +test_item = deck[0] +found_seed = None +for seed in tqdm.trange(2**10): + prng = PRNG(seed) + rand_values = [] + for i in range(256): + rand_values.append(prng.getnum()) + + found = False + + for offset in range(249): + e_array = [str(x) for x in rand_values[offset:offset+6]] + computer_e = int(''.join(e_array)) + if gcd(computer_e, phi) != 1: + continue + computer_d = pow(computer_e, -1, phi) + + test_card = long_to_bytes(pow(test_item, computer_d, N)).decode('iso-8859-1') + if test_card in deck_str: + print("found seed", test_card, seed) + found = True + found_seed = seed + break + + if found: + break +``` + +now we just play the game, simulating the server's steps as we go and decrypting the cards we +receive using the recovered keys + +```python +prng = PRNG(found_seed) +computer_e, computer_d = -1, 0 + +for rndnum in range(10): + if rndnum > 0: + print(r.recvuntil(b"***")) + roundline = r.recvline() + print("roundline", roundline) + print(r.recvuntil(b"--> ")) + deck = r.recvline().strip().decode() + deck = ast.literal_eval(deck) + # print("recv deck", deck) + + computer_e, computer_d, server_deck = sim_round(server_deck, prng, phi, computer_e, computer_d) + print(computer_e, computer_d, prng.seed, prng.index, prng.state) + + dec_cards = [] + for enc_card in deck: + dec_card = long_to_bytes(pow(enc_card, computer_d, N)).decode() + dec_cards.append(card_str_to_Card(dec_card)) + + print([str(c) for c in dec_cards]) + print([str(c) for c in server_deck]) +``` + +since i don't actually know how to make a computer play poker, we simply ask the server functions +whether a deck would be favorable for us, and keep shuffling until it is + +(i love some good ctf code cheese) + +```python + while True: + random.shuffle(dec_cards) + computer_cards, player_cards = dec_cards[0:5], dec_cards[5:10] + computer_winner, player_winner = determine_winner(computer_cards, player_cards) + if player_winner and not computer_winner: + break +``` + +encrypt with the computer exponent and send our cooked deck order to the server + +```python + server_deck = list(dec_cards) + + enc_deck = [] + for card in dec_cards: + enc_deck.append(pow(bytes_to_long(str(card).encode()),computer_e,N)) + + for c in enc_deck: + r.recvuntil(b">> ") + r.sendline(str(c).encode()) +``` + +and after 10 rounds we should have the flag, which we can decrypt with the same computer exponent as +we've been using + +```python +r.recvuntil(b"HAHAHA") +r.recvline() + +val = ast.literal_eval(r.recvline().decode()) +print(long_to_bytes(pow(bytes_to_long(val),computer_d,N))) +``` + +let's run this locally, + +```bash +ncat --exec "/usr/bin/python server.py" -lp 1337 & + +python3 exploit.py + +[+] Opening connection to localhost on port 1337: Done +p= 1272817120605235218138374468827494843305725876454046855144283728239720564668615378794696017 +q= 1119723139502382006362751135685189913928207952277558814504020165396321544235726326572905597 +player_e= 1 +player_d= 1 +roundline b'******* Round 1, current streak is 0 **********\n' +b'Here is the shuffled encrypted deck --> ' + 28%|██████████████████ | 284/1024 [00:24<01:05, 11.37it/s]found seed One of Clubs 284 + 28%|██████████████████ | 284/1024 [00:24<01:04, 11.43it/s] +[... lots of spam ...] +My hand is Seven of Hearts, Six of Diamonds, Four of Clubs, Two of Clubs, Zero of Clubs --> I have a High card +Your hand is Joker of Hearts, Ace of Diamonds, Ten of Clubs, Nine of Spades, Six of Spades --> You have a High card +b'csawctf{test_test_test_test}\n' +[*] Closed connection to localhost port 1337 +``` + +full attack script: + diff --git a/2023/csawq/03-end.md b/2023/csawq/03-end.md new file mode 100644 index 0000000..9c78639 --- /dev/null +++ b/2023/csawq/03-end.md @@ -0,0 +1,5 @@ +## that's it + +``` +:wq +``` diff --git a/2023/csawq/attack_blocky.sage b/2023/csawq/attack_blocky.sage new file mode 100644 index 0000000..0f8c5cd --- /dev/null +++ b/2023/csawq/attack_blocky.sage @@ -0,0 +1,133 @@ +import nclib +import ecdsa +import hashlib +import binascii +from Crypto.Util.number import * + +def _hex(x): + return binascii.hexlify(x).decode() + +def _hash(msg): + return bytes_to_long(hashlib.sha1(msg).digest()) + +conn = nclib.Netcat(("crypto.csaw.io", 5002)) +# conn = nclib.Netcat(("localhost", 1337)) + +N = 6 +h = [] +sgns = [] +msgs = [] + +def get_messages(count): + all_msgs = [] + + conn.recv_until(b": ") + conn.send_line(b"2") + + for i in range(count): + conn.recv_until(f"Block {i}\n".encode()) + msg_line = conn.recv_line() + print(msg_line) + msg = binascii.unhexlify(msg_line.strip().split(b" ")[1]) + sig_line = conn.recv_line() + sig_rs = sig_line.decode().split("(")[1].split(")")[0] + r, s = sig_rs.split(", ") + r = int(r) + s = int(s) + all_msgs.append((msg, r, s)) + + return all_msgs + +for i in range(N): + msg = f"meow{i}".encode() + msgs.append(msg) + conn.recv_until(b": ") + conn.send_line(b"1") + conn.send_line(_hex(msg)) + +all_msgs = get_messages(N+1) + +for i in range(N): + (_, r, s) = all_msgs[i+1] + prev_msg = all_msgs[i][0] + real_hash = _hash(prev_msg + msgs[i]) + h.append(real_hash) + sgns.append((r,s)) + +order=115792089237316195423570985008687907852837564279074904382605163141518161494337 + +s_inv = [] +s = [] +r = [] +for i in range(N): + s.append(sgns[i][1]) + r.append(sgns[i][0]) + s_inv.append(ecdsa.numbertheory.inverse_mod(s[i], order)) + +Z = GF(order) +R = PolynomialRing(Z, names=('dd',)) +(dd,) = R._first_ngens(1) + +# the polynomial we construct will have degree 1 + Sum_(i=1)^(i=N-3)i in dd +# our task here is to compute this polynomial in a constructive way starting from the N signatures in the given list order +# the generic formula will be given in terms of differences of nonces, i.e. k_ij = k_i - k_j where i and j are the signature indexes +# each k_ij is a first-degree polynomial in dd +# this function has the goal of returning it given i and j +def k_ij_poly(i, j): + hi = Z(h[i]) + hj = Z(h[j]) + s_invi = Z(s_inv[i]) + s_invj = Z(s_inv[j]) + ri = Z(r[i]) + rj = Z(r[j]) + poly = dd*(ri*s_invi - rj*s_invj) + hi*s_invi - hj*s_invj + return poly + +# the idea is to compute the polynomial recursively from the given degree down to 0 +# the algorithm is as follows: +# for 4 signatures the second degree polynomial is: +# k_12*k_12 - k_23*k_01 +# so we can compute its coefficients. +# the polynomial for N signatures has degree 1 + Sum_(i=1)^(i=N-3)i and can be derived from the one for N-1 signatures + +# let's define dpoly(i, j) recursively as the dpoly of degree i starting with index j + +def dpoly(n, i, j): + if i == 0: + return (k_ij_poly(j+1, j+2))*(k_ij_poly(j+1, j+2)) - (k_ij_poly(j+2, j+3))*(k_ij_poly(j+0, j+1)) + else: + left = dpoly(n, i-1, j) + for m in range(1,i+2): + left = left*(k_ij_poly(j+m, j+i+2)) + right = dpoly(n, i-1, j+1) + for m in range(1,i+2): + right = right*(k_ij_poly(j, j+m)) + return (left - right) + +def print_dpoly(n, i, j): + if i == 0: + print('(k', j+1, j+2, '*k', j+1, j+2, '-k', j+2, j+3, '*k', j+0, j+1, ')', sep='', end='') + else: + print('(', sep='', end='') + print_dpoly(n, i-1, j) + for m in range(1,i+2): + print('*k', j+m, j+i+2, sep='', end='') + print('-', sep='', end='') + print_dpoly(n, i-1, j+1) + for m in range(1,i+2): + print('*k', j, j+m, sep='', end='') + print(')', sep='', end='') + +print_dpoly(N-4, N-4, 0) +poly_target = dpoly(N-4, N-4, 0) +d_guesses = poly_target.roots() +print("results!!!!!\n\n\n") +print(d_guesses) + +if len(d_guesses) == 1: + conn.recv_until(b": ") + conn.send_line(b"4") + res = str(d_guesses[0][0]).encode() + print("SENDING", res) + conn.send_line(res) + print(conn.recv_all(timeout=5)) diff --git a/2023/csawq/attack_poker.py b/2023/csawq/attack_poker.py new file mode 100644 index 0000000..a99a921 --- /dev/null +++ b/2023/csawq/attack_poker.py @@ -0,0 +1,161 @@ +from pwn import * +import ast +import random +from math import gcd +from Crypto.Util.number import bytes_to_long, long_to_bytes + +from server import PRNG, Card, card_str_to_Card, determine_winner + +import tqdm + +def sim_round(deck, rng, phi, computer_e, computer_d): + deck = shuffle(rng, deck) + print("local hacks 1", rng.seed, rng.index, rng.state) + while computer_e < 2 or computer_d < 1: + e_array = [] + for _ in range(6): + e_array.append(str(rng.getnum())) + computer_e = int(''.join(e_array)) + if gcd(computer_e, phi) == 1: + computer_d = pow(computer_e,-1,phi) + + return computer_e, computer_d, deck + +def shuffle(rng, deck): + new_deck = [] + for i in range(len(deck)): + x = rng.getnum() + if deck[x] not in new_deck: + new_deck.append(deck[x]) + elif deck[i] not in new_deck: + new_deck.append(deck[i]) + else: + for card in deck: + if card not in new_deck: + new_deck.append(card) + break + return new_deck + +def exploit(): + deck = [] + deck_str = [] + for suit in ["Spades", "Hearts", "Diamonds", "Clubs"]: + for i in range(16): + deck.append(Card(suit, i)) + deck_str.append(str(deck[-1])) + deck_str = set(deck_str) + + server_deck = list(deck) + + # r = process(["python3", "server.py"]) + r = remote("crypto.csaw.io", 5001) + + r.recvuntil(b"Here are") + r.recvuntil(b"--> ") + + p, q = r.recvline().decode().strip().split(", ") + p = int(p) + q = int(q) + phi = (p-1)*(q-1) + N = p*q + + print("p=", p) + print("q=", q) + + # player_e = 65537 + # player_d = pow(player_e, -1, phi) + player_e = 1 + player_d = 1 + + print("player_e=", player_e) + print("player_d=", player_d) + + r.recvuntil(b">>") + r.sendline(str(player_e).encode()) + r.recvuntil(b">>") + r.sendline(str(player_d).encode()) + + r.recvuntil(b"***") + roundline = r.recvline() + print("roundline", roundline) + print(r.recvuntil(b"--> ")) + deck = r.recvline().strip().decode() + deck = ast.literal_eval(deck) + # print("recv deck", deck) + + test_item = deck[0] + found_seed = None + for seed in tqdm.trange(2**10): + prng = PRNG(seed) + rand_values = [] + for i in range(256): + rand_values.append(prng.getnum()) + + found = False + + for offset in range(249): + e_array = [str(x) for x in rand_values[offset:offset+6]] + computer_e = int(''.join(e_array)) + if gcd(computer_e, phi) != 1: + continue + computer_d = pow(computer_e, -1, phi) + + test_card = long_to_bytes(pow(test_item, computer_d, N)).decode('iso-8859-1') + if test_card in deck_str: + print("found seed", test_card, seed) + found = True + found_seed = seed + break + + if found: + break + + prng = PRNG(found_seed) + computer_e, computer_d = -1, 0 + + for rndnum in range(10): + if rndnum > 0: + print(r.recvuntil(b"***")) + roundline = r.recvline() + print("roundline", roundline) + print(r.recvuntil(b"--> ")) + deck = r.recvline().strip().decode() + deck = ast.literal_eval(deck) + # print("recv deck", deck) + + computer_e, computer_d, server_deck = sim_round(server_deck, prng, phi, computer_e, computer_d) + print(computer_e, computer_d, prng.seed, prng.index, prng.state) + + dec_cards = [] + for enc_card in deck: + dec_card = long_to_bytes(pow(enc_card, computer_d, N)).decode() + dec_cards.append(card_str_to_Card(dec_card)) + + print([str(c) for c in dec_cards]) + print([str(c) for c in server_deck]) + + while True: + random.shuffle(dec_cards) + computer_cards, player_cards = dec_cards[0:5], dec_cards[5:10] + computer_winner, player_winner = determine_winner(computer_cards, player_cards) + if player_winner and not computer_winner: + break + + server_deck = list(dec_cards) + + enc_deck = [] + for card in dec_cards: + enc_deck.append(pow(bytes_to_long(str(card).encode()),computer_e,N)) + + for c in enc_deck: + r.recvuntil(b">> ") + r.sendline(str(c).encode()) + + r.recvuntil(b"HAHAHA") + r.recvline() + + val = ast.literal_eval(r.recvline().decode()) + print(long_to_bytes(pow(bytes_to_long(val),computer_d,N))) + +if __name__ == "__main__": + exploit() diff --git a/2023/csawq/build.ninja b/2023/csawq/build.ninja new file mode 100644 index 0000000..465f3bb --- /dev/null +++ b/2023/csawq/build.ninja @@ -0,0 +1,16 @@ +builddir = build + +pandocflags = --filter writefreely-validate --wrap=none + +postid = bdg7m2ck2d + +rule pandoc + command = pandoc $pandocflags -o $out $in + description = pandoc $out + +rule upload + command = writefreely-cli $postid $in + description = upload $postid + +build $builddir/post-full.md: pandoc 00-intro.md 01-crypto-blocky.md 02-crypto-poker.md 03-end.md +build upload: upload $builddir/post-full.md