# perfectblue/ctf-writeups

Midnightsun 2018 finals

rollsafe committed Jun 18, 2018
1 parent e3352dd commit f99e26ac6996e5859b425f13be0953c87f974a30
 @@ -1 +1,2 @@ # *.pyc
 @@ -0,0 +1 @@ # Midnight Sun CTF 2018 Finals Writeups
 @@ -0,0 +1,45 @@ # badchair **Category**: Reverse 161 Points 11 Solves **Problem description**: ``` Try sitting on it and see if it breaks ``` --- This was a crypto problem involving [Shamir's Secret Sharing Scheme (SSS)][1] problem over the finite field GF(256). The way SSS works is that your secret is the constant term of a polynomial, and the shares are points on the polynomial. Since `k` points are necessary to define a polynomial of degree `k-1`, you can split up the secret in this manner while revealing no information about the secret unless you have enough (`>= k`) points. In our problem we're given only 4 shares (points) when the threshold is 5 (degree 4 polynomial). So somehow we would need to deduce the polynomial. What's interesting is the secret splitting implementation: ```python class KeySplitter: def __init__(self, numshares, threshold): self.splitter = Shamir(numshares, threshold) self.numshares = numshares self.threshold = threshold def split(self, key): xshares = [''] * self.numshares yshares = [''] * self.numshares for char in key: xcords, ycords = self.splitter.split(ord(char)) for idx in range(self.numshares): xshares[idx] += chr(xcords[idx]) yshares[idx] += chr(ycords[idx]) return zip(xshares, yshares) ``` The key observation here is that secret is split up byte-for-byte, and the non-constant terms of the secret polynomial is the same for every byte. That means that for two bytes in the secret with the same value but different position, the corresponding secret polynomial would be the same. If we knew the positions of two identical bytes in the secret, we could combine the 4 (insufficient) shares of each byte into 8 (sufficient) shares of the secret. Moreover, we can even calculate the full polynomial which would allow us to use only 4 shares to deduce all the other bytes of the secret too. Luckily, we know the flag format is `midnight{..}` and we have two `i`s at positions 1 and 4. So using that, we can figure out the polynomial used was `f(x) = SECRET + db*x^1 + 6f*x^2 + ad*x^3 + 1f*x^4`. Then, for the other bytes of the secret, we can simply try all field elements 0-255 as the secret constant term in the polynomial `f(x)`, and check whether `f(x_i)` matches `y_i` for each of the 4 known share points. If they all match, that means we probably guessed our constant term correctly. `midnight{ehhh_n0t_3ven_cl0se}` Note: I believe the algebra can all be done in sage, but I was in a hurry just to solve the problem so I just reinvented the wheel. [1]:https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing
Binary file not shown.
 @@ -0,0 +1,193 @@ import os import json import base64 # from flag import FLAG from point import Point from shamir import Shamir class KeySplitter: def __init__(self, numshares, threshold): self.splitter = Shamir(numshares, threshold) self.numshares = numshares self.threshold = threshold def split(self, key): xshares = [''] * self.numshares yshares = [''] * self.numshares for char in key: xcords, ycords = self.splitter.split(ord(char)) for idx in range(self.numshares): xshares[idx] += chr(xcords[idx]) yshares[idx] += chr(ycords[idx]) return zip(xshares, yshares) def jsonify(self, shares, threshold, split): data = { 'shares': shares, 'threshold': threshold, 'split': [ base64.b64encode(split[0]), base64.b64encode(split[1]) ] } return json.dumps(data) def lagrange_interpolate_l0(x, x_s, y_s): k = len(x_s) l0 = Point(0) for j in range(0, k): prod = Point(1) for m in range(0, k): if m == j: continue prod = prod * x_s[m] / (x_s[m] + x_s[j]) l0 = l0 + y_s[j] * prod return l0 # we have to be very careful to treat our ring members like they are immutable class Poly(object): def __init__(self, coeff,zero,one): self.coeff = coeff self.zero = zero # additive identity self.one = one # multiplicative identity def _zeros(self, deg): res = [0]*(deg) for i in range(0,deg): res[i] = self.zero return res def __mul__(self, other): if type(other) == Poly: res = self._zeros(len(self.coeff)+len(other.coeff)-1) for o1,i1 in enumerate(self.coeff): for o2,i2 in enumerate(other.coeff): res[o1+o2] = res[o1+o2] + i1*i2 return Poly(res,self.zero,self.one) else: res = self.coeff[:] for i,a in enumerate(res): res[i] = a*other return Poly(res,self.zero,self.one) def __add__(self, other): res = self._zeros(max(len(self.coeff), len(other.coeff))) for o1,i1 in enumerate(self.coeff): res[o1] = res[o1] + i1 for o1,i1 in enumerate(other.coeff): res[o1] = res[o1] + i1 return Poly(res,self.zero,self.one) def __div__(self, scalar): res = self.coeff[:] for i,a in enumerate(res): res[i] = a/scalar return Poly(res,self.zero,self.one) def __call__(self, scalar): res = self.zero xi = self.one for i,a in enumerate(self.coeff): res = res + a * xi xi = xi * scalar return res def __str__(self): result = [] for i,a in enumerate(self.coeff): result.append('%sx^%s' % (a,i)) return ' + '.join(result) def lagrange_interpolate_poly(x_s, y_s, deg, zero,one): assert(len(x_s) == len(y_s)) l_s = [zero] * (deg+1) for j in range(0, deg+1): num = Poly([one],zero,one) den = one for i in range(0, len(x_s)): if i == j: continue num = num * Poly([-x_s[i], one],zero,one) den = den * (x_s[j] - x_s[i]) l_s[j] = num / den f = Poly([],zero,one) for j in range(0, deg+1): f = f + l_s[j] * y_s[j] return f def extract_coords(xs,ys,i): xs_i = map(lambda fuck: Point(ord(fuck[i])), xs) ys_i = map(lambda fuck: Point(ord(fuck[i])), ys) return xs_i, ys_i def decode(xs, ys): l = len(xs[0]) result = '' for i in range(0, l): xs_i,ys_i = extract_coords(xs,ys,i) result += chr(lagrange_interpolate_l0(0, xs_i, ys_i).value) return result def decode_brute(poly, xs, ys): l = len(xs[0]) result = '' for i in range(0, l): xs_i,ys_i = extract_coords(xs,ys,i) result += chr(brute_base(poly, xs_i, ys_i).value) return result def brute_base(poly, xs, ys): cands = [] for x0 in range(32, 128): coeffs = poly.coeff[:] coeffs[0] = Point(x0) ok = True for x,y in zip(xs,ys): yx = Poly(coeffs, poly.zero,poly.one)(x) if not (yx == y): ok = False break if ok: cands.append(coeffs[0]) assert(len(cands) == 1) return cands[0] # print lagrange_interpolate_poly([2.,4.,5.],[1942.,3402.,4414.], 2,0.0,1.) if __name__ == "__main__": shares = map(json.loads, open('shares.txt','r').readlines()) splits = [] for share in shares: splits.append(map(base64.b64decode, share['split'])) xs,ys= zip(*splits) # now here is the trick. since they are both split-up `i`s we can combine the shares to deduce the poly. # midnight{ # 01234567 # ^ ^ xs_1,ys_1 = extract_coords(xs,ys,1) xs_4,ys_4 = extract_coords(xs,ys,4) xs_full = xs_1[0:3] + xs_4[0:3] ys_full = ys_1[0:3] + ys_4[0:3] # print interpolate(0, xs_full, ys_full) P = lagrange_interpolate_poly(xs_full, ys_full, 5, Point(0), Point(1)) print P # print P(Point(xs_full[0])),ys_full[0] print decode_brute(P, xs, ys) # if __name__ == "__main__": # splitter = KeySplitter(9, 5) # splits = splitter.split('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') # xs,ys= zip(*splits) # xs_0,ys_0 = extract_coords(xs,ys,0) # xs_5,ys_5 = extract_coords(xs,ys,5) # print str(interpolate(0, xs_0[0:3] + xs_5[0:3],ys_0[0:3] + ys_5[0:3])) # for i in range(0, 4): # print splitter.jsonify(9, 5, splits[i])
 @@ -0,0 +1,68 @@ logTable = [ 0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36, 0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18, 0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f, 0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e, 0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53, 0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3, 0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21, 0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74, 0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4, 0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1, 0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13, 0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80, 0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12, 0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5, 0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56, 0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba, 0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3, 0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47, 0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf, 0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05, 0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67, 0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd, 0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34, 0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec, 0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7, 0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e, 0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a, 0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d, 0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c, 0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d, 0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0, 0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38] # this is a table of GF(2^8) using g = 0xe5 expTable = [ 0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12, 0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36, 0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a, 0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee, 0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29, 0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b, 0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d, 0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c, 0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f, 0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a, 0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85, 0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94, 0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7, 0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2, 0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d, 0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17, 0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39, 0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b, 0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd, 0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c, 0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84, 0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97, 0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2, 0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd, 0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c, 0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24, 0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c, 0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4, 0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7, 0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52, 0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6, 0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01]
 @@ -0,0 +1,39 @@ from log import logTable, expTable class Point(object): def __init__(self, value): if type(value) == Point: self.value = value.value else: self.value = value % 256 def __add__(self, point): return Point(self.value ^ point.value) def __neg__(self): return Point(self) def __sub__(self, point): return self + point def __mul__(self, point): if point.value == 0 or self.value == 0: return Point(0) return Point(expTable[(logTable[self.value] + logTable[point.value]) % 255]) def __div__(self, point): if point.value == 0: raise ArithmeticError('Division by zero') return Point(expTable[(255 + logTable[self.value] - logTable[point.value]) % 255]) def __eq__(self, point): return self.value == point.value def __ne__(self, point): return not self == point def __str__(self): return '%02x' % (self.value,) def __repr__(self): return chr(self.value)
 @@ -0,0 +1,28 @@ import random from point import Point class Shamir: def __init__(self, shares, threshold): self.shares = shares self.rng = random.SystemRandom() self.base_poly = [self.rng.randint(0, 256) for _ in range(threshold - 1)] def split(self, secret): coeffs = [secret] + self.base_poly coords = [] result = [] while len(coords) < self.shares: drawn = self.rng.randint(1, 255) if not drawn in coords: coords += [drawn] for coord in coords: B = Point(1) S = Point(0) X = Point(coord) for coeff in coeffs: S += (B * Point(coeff)) B *= X result.append(S.value) return coords, result
 @@ -0,0 +1,4 @@ {"threshold": 5, "split": ["jUEumBCZY6GRlXbB/uobM53gis/RldnMAfBAnkg=", "e3WhwYy6YUQGedMXkGnxJO6v0ov4cgteapL17wI="], "shares": 9} {"threshold": 5, "split": ["mrygQzFoasgDY23te4MGqTFXjpS/pMalQiN9Sks=", "XjpXSuBzyfRAbKj7hODzcf0cv0NZsXQDEQiIdtA="], "shares": 9} {"threshold": 5, "split": ["U64tceO4Ddtr4V6FMXSTJre4f6t4nPczWgtIkfo=", "t3hrQmsqonoBCl7T4f3bLqMShchtOtF3WesMGEs="], "shares": 9} {"threshold": 5, "split": ["TcOzpm1jNtwsj0cdiLfd6oVHLGMMKRt52t9kU4E=", "RqJ1WKlufadMFwFLbIjvBs82BH/yP8CAT6kyv3M="], "shares": 9}