Browse files

Midnightsun 2018 finals

  • Loading branch information...
rollsafe committed Jun 18, 2018
1 parent e3352dd commit f99e26ac6996e5859b425f13be0953c87f974a30
@@ -1 +1,2 @@
@@ -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:
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<SECRET>(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.
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.
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': [
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:
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 = zero # additive identity = one # multiplicative identity
def _zeros(self, deg):
res = [0]*(deg)
for i in range(0,deg):
res[i] =
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,,
res = self.coeff[:]
for i,a in enumerate(res):
res[i] = a*other
return Poly(res,,
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,,
def __div__(self, scalar):
res = self.coeff[:]
for i,a in enumerate(res):
res[i] = a/scalar
return Poly(res,,
def __call__(self, scalar):
res =
xi =
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:
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,,
if not (yx == y):
ok = False
if ok:
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
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
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}

0 comments on commit f99e26a

Please sign in to comment.