service.py:
#! /usr/bin/env python import string import random import SocketServer import threading import hashlib import ecdsa import base64 import os #our own password database access = {"f56334fbe02eaa05218c31b01a80f2f6":0, "00b37cb56bb57705348610253b1b82e4":0, "f2131629ea6c08f7f5f326d8bb6eb5fd":0, "6fa95b1427af77b3d769ae9cb853382f":0, "58cd57027cf126fcc9bd93dea9d74c1a":0, "f1cd318e412b5f7226e5f377a9544ff7":1, "98c131f9fb31f732b136f87e64ff686a":1, "6f3249aa304055d63828af3bfab778f6":2} #alphabet for passwords alphabet = string.lowercase + string.uppercase #read the curve parameters and flag from file p,b,q,_sk,flag = open("secrets").read().split(',') _p = int(p,16) _b = int(b,16) _q = int(q,16) _sk = int(_sk,16) ec = ecdsa.CurveFp( _p, _p-3, _b ) #some points that really should be on the curve we're going to use _Gx = 0x337ef2115b4595fbd60e2ffb5ee6409463609e0e5a6611b105443e02cb82edd8L _Gy = 0x1879b8d7a68a550f58166f0d6b4e86a0873d7b709e28ee318ddadd4ccf505e1aL _Qx = 0x2a40fd522f73dc9f7c40b2420e39e62c5742ff2f11805a1577ed7f60153a0be1L _Qy = 0x3085e99246006b71b4211eff47ff3efc0f93103ee7379dc3bcc6decdc46073a3L _Rx = 0xbd0a442367bdc24cb09c49404e3d307ba99122e7b78e14f0d84870d0df97aa59L _Ry = 0x22c88612db6b6af6f196cd815fc5f57fe871d3b6588b0c7a59e06cc759d736b2L #check the curve is loaded ok if not ec.contains_point(_Gx,_Gy): exit() if not ec.contains_point(_Qx,_Qy): exit() if not ec.contains_point(_Rx,_Ry): exit() g = ecdsa.Point( ec, _Gx, _Gy, _q ) seed = os.urandom(32) #construct the server key material server_public_key = ecdsa.Public_key( g, g * _sk) server_secret_key = ecdsa.Private_key(server_public_key, _sk ) #some extended interfaces to Lis's ecdsa script def sign(hashed_data, privkey): h = int(hashed_data,16) k = get_ephemeral_key() signature = privkey.sign( h, k ) return encode_signature(signature) def encode_signature(signature): rhex = "%064x" % signature.r shex = "%064x" % signature.s return base64.b64encode(rhex.decode('hex') + shex.decode('hex')) def verify(signature, hashed_input, pubkey): tmp = signature.replace("\n", "") try: tmp = base64.b64decode(tmp).encode('hex') except: return False if len(tmp) != 128: return False sig = ecdsa.Signature(int(tmp[:64],16),int(tmp[64:],16)) h = int(hashed_input,16) return pubkey.verifies(h, sig) def get_ephemeral_key(): k = "" while len(k) < 32: k += hashlib.md5(seed + str(random.randint(0,255))).digest() return int(k.encode('hex'),16) #server class threadedserver(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass class incoming(SocketServer.BaseRequestHandler): def handle(self): cur_thread = threading.current_thread() #hashcash cash = os.urandom(4).encode('hex') self.request.send("Please send a string of 64 characters, consisting of only 0-9a-f that starts with %s which md5 digest starts with 0000:\n" % cash) resp = self.request.recv(128).strip() if len(resp) != 64 or not all(c in '0123456789abcdef' for c in resp) or not resp.startswith(cash) or not hashlib.md5(resp).hexdigest().startswith('0000'): self.request.send('Sorry :(') return #request password self.request.send("Please enter your password:\n") password = self.request.recv(32) password = password.replace("\n", "") #we only understand simple passwords if not all(c in alphabet for c in password): self.request.send("Access denied\n") return token = hashlib.md5(password).digest().encode('hex') #you better be in our database if not token in access: self.request.send("Access denied\n") return #retrieve allowed access level level = access[token] if level == 2: #you really really deserve this self.request.send("There you go: " + flag) if level == 1: #you seem to belong here self.request.send("Please present your security token for today:\n" ) sec_token = self.request.recv(1024) if verify(sec_token, token, server_public_key): self.request.send("There you go: " + flag) else: self.request.send("Access denied\n") if level == 0: self.request.send("Sorry level 0 has been temporarily disabled while we investigate a potential security breach.\n") # Code disabled while we investigate the breach #self.request.send("Here is your security token for today:\n" ) #self.request.send( sign(token, server_secret_key) + "\n" ) server = threadedserver(("127.0.0.1", 3016), incoming) server.allow_reuse_address = True server.timeout = 4 server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() server_thread.join()
ecdsa.py:
#! /usr/bin/env python # # credits to Lis # taken from https://bitcointalk.org/index.php?topic=23241.0 # class CurveFp( object ): def __init__( self, p, a, b ): self.__p = p self.__a = a self.__b = b def p( self ): return self.__p def a( self ): return self.__a def b( self ): return self.__b def contains_point( self, x, y ): return ( y * y - ( x * x * x + self.__a * x + self.__b ) ) % self.__p == 0 class Point( object ): def __init__( self, curve, x, y, order = None ): self.__curve = curve self.__x = x self.__y = y self.__order = order if self.__curve: assert self.__curve.contains_point( x, y ) if order: assert self * order == INFINITY def __add__( self, other ): if other == INFINITY: return self if self == INFINITY: return other assert self.__curve == other.__curve if self.__x == other.__x: if ( self.__y + other.__y ) % self.__curve.p() == 0: return INFINITY else: return self.double() p = self.__curve.p() l = ( ( other.__y - self.__y ) * \ inverse_mod( other.__x - self.__x, p ) ) % p x3 = ( l * l - self.__x - other.__x ) % p y3 = ( l * ( self.__x - x3 ) - self.__y ) % p return Point( self.__curve, x3, y3 ) def __mul__( self, other ): def leftmost_bit( x ): assert x > 0 result = 1L while result <= x: result = 2 * result return result / 2 e = other if self.__order: e = e % self.__order if e == 0: return INFINITY if self == INFINITY: return INFINITY assert e > 0 e3 = 3 * e negative_self = Point( self.__curve, self.__x, -self.__y, self.__order ) i = leftmost_bit( e3 ) / 2 result = self while i > 1: result = result.double() if ( e3 & i ) != 0 and ( e & i ) == 0: result = result + self if ( e3 & i ) == 0 and ( e & i ) != 0: result = result + negative_self i = i / 2 return result def __rmul__( self, other ): return self * other def __str__( self ): if self == INFINITY: return "infinity" return "(%d,%d)" % ( self.__x, self.__y ) def double( self ): if self == INFINITY: return INFINITY p = self.__curve.p() a = self.__curve.a() l = ( ( 3 * self.__x * self.__x + a ) * \ inverse_mod( 2 * self.__y, p ) ) % p x3 = ( l * l - 2 * self.__x ) % p y3 = ( l * ( self.__x - x3 ) - self.__y ) % p return Point( self.__curve, x3, y3 ) def x( self ): return self.__x def y( self ): return self.__y def curve( self ): return self.__curve def order( self ): return self.__order INFINITY = Point( None, None, None ) def inverse_mod( a, m ): if a < 0 or m <= a: a = a % m c, d = a, m uc, vc, ud, vd = 1, 0, 0, 1 while c != 0: q, c, d = divmod( d, c ) + ( c, ) uc, vc, ud, vd = ud - q*uc, vd - q*vc, uc, vc assert d == 1 if ud > 0: return ud else: return ud + m class Signature( object ): def __init__( self, r, s ): self.r = r self.s = s class Public_key( object ): def __init__( self, generator, point ): self.curve = generator.curve() self.generator = generator self.point = point n = generator.order() if not n: raise RuntimeError, "Generator point must have order." if not n * point == INFINITY: raise RuntimeError, "Generator point order is bad." if point.x() < 0 or n <= point.x() or point.y() < 0 or n <= point.y(): raise RuntimeError, "Generator point has x or y out of range." def verifies( self, hash, signature ): G = self.generator n = G.order() r = signature.r s = signature.s if r < 1 or r > n-1: return False if s < 1 or s > n-1: return False c = inverse_mod( s, n ) u1 = ( hash * c ) % n u2 = ( r * c ) % n xy = u1 * G + u2 * self.point v = xy.x() % n return v == r class Private_key( object ): def __init__( self, public_key, secret_multiplier ): self.public_key = public_key self.secret_multiplier = secret_multiplier def sign( self, hash, random_k ): G = self.public_key.generator n = G.order() k = random_k % n p1 = k * G r = p1.x() if r == 0: raise RuntimeError, "amazingly unlucky random number r" s = ( inverse_mod( k, n ) * \ ( hash + ( self.secret_multiplier * r ) % n ) ) % n if s == 0: raise RuntimeError, "amazingly unlucky random number s" return Signature( r, s )And captured session (1000 requests):
Bushing N2H4IbYdza03JAPqQMSClxFCWaZbw3qMT1btugW9lAMm7jPtethECcB64qSwiNDT2GAUPNcZe+LHC32q73PmSQ== Hotz wNpp5moxL1+Z0I40PJQCC/LegcOQocP+8Y9GV7Mv+5xCxqlIuZkMQDSpRWffZGG4NdkaOvAfttTtQuCaJZonxA== Hotz l3O9KitquB8KznEtDn4NJnWOxk1oJfYxMp3A9MjMOrezkBIzao9RQnWqF+ARC61MVNXRnzCypm3/RCPCT3XtFQ== [...] Bushing Ny/2AWoDfM70LbVBrnUQw85Wr2aV0pNihTimg6aYCnqw0jxp2VTQ/Xkss3jNXSLWX4u2/G2RplnMKDbIia1DVQ== Bushing aPL6Qh+7UQwZ0EIEvPgPHW+YkcqIrpULimmgt4xdqCgHwOb7LGxyGh7hNT5b/Yyo1crvsQ5AxEZZiuQ80QJBmg== Bushing P3uwUNm7bDwpgRe6KgZXPmk1PmQOpLeVPtr+cQjI5yxb17YBZw8wh1jp24O5avzZ4cXdMeJvC0jABBCkMtX9PA== Hotz PEoOFXZD/Qs4b/4Omsn3O87UUpFsd8i25mJvo91hp+K0W3bX7bbhHU/i2hSSmGLAXuNS7dIoO3Sxn/liqNGNlg==To get flag we need to:
1. Break MD5 level2 hash (6f3249aa304055d63828af3bfab778f6)
or
2. Break one of MD5 level1 hashes (f1cd318e412b5f7226e5f377a9544ff7, 98c131f9fb31f732b136f87e64ff686a) and sign it with ECDSA private key.
Level2 hash seems to be unbreakable, level1 hashes are easy to break but we don't have the private key and all captured data are level0
ECDSA is safe if all messages are signed with different random value of ephemeral key (k), otherwise when we have 2 messages that used the same value of k we can calculate k using formula $$k = \frac{hash_1 - hahs_2}{s_1-s_2}\ (mod\ n)$$ and then the private key: $$d_A = \frac{s_1*k-hash_1}{r_1}\ (mod\ n)$$ And then we can sign our hash: $$s = \frac{hash+d_A*r_1}{k}\ (mod\ n)$$ and the signature is $$(r_1,s)$$ Let's look at get_ephemeral_key() function:
def get_ephemeral_key(): k = "" while len(k) < 32: k += hashlib.md5(seed + str(random.randint(0,255))).digest() return int(k.encode('hex'),16)There are only 255*255 possible outputs of this function in one session (seed value is random but stays the same for the whole session)! And we have 1000 captured samples, so there is a huge probability that two messages were using the same k (birthday paradox). We can calculate k and the private key value from all pairs and if two disjoint pairs will give us the same private key, we can assume that this is true private_key.
import base64 import hashlib n = G_ORDER def inverse_mod( a, m ): if a < 0 or m <= a: a = a % m c, d = a, m uc, vc, ud, vd = 1, 0, 0, 1 while c != 0: q, c, d = divmod( d, c ) + ( c, ) uc, vc, ud, vd = ud - q*uc, vd - q*vc, uc, vc assert d == 1 if ud > 0: return ud else: return ud + m f = open('captured-sessions.txt','r') data = f.read() lines = data.split("\n") sigs = [] zs = [] for line in lines: dwa = line.split(' ') if len(dwa) <> 2: continue tmp = base64.b64decode(dwa[1]).encode('hex') r = int(tmp[:64],16) s = int(tmp[64:],16) sigs.append( (r,s) ) hashed_input = hashlib.md5(dwa[0]).digest().encode('hex') zs.append(int(hashed_input,16)) keys = dict() for i in range(0,1000): for j in range(i+1,1000): (r1,s1) = sigs[i] (r2,s2) = sigs[j] z1 = zs[i] z2 = zs[j] if z1 == z2: continue k = (z1 - z2)*inverse_mod(s1-s2,n) % n d = (s1*k - z1)*inverse_mod(r1,n) % n ii = i jj =j if d in keys: (ii,jj) = keys[d] if ii <> i and jj <> j and i<>jj and j<>ii: print d,k,r1 keys[d]=(i,j)But there is a problem, we need Order of G. Let's look at verify function:
def verifies( self, hash, signature ): G = self.generator n = G.order() r = signature.r s = signature.s if r < 1 or r > n-1: return False if s < 1 or s > n-1: return False c = inverse_mod( s, n ) u1 = ( hash * c ) % n u2 = ( r * c ) % n xy = u1 * G + u2 * self.point v = xy.x() % n return v == rBefore expensive (time) calculations there is a check if values of r and s are between 1 and n-1. So we can use binary search and time checks to find value of n.
import base64 import socket import re import random import hashlib import datetime hostport = ('54.216.116.38', 3016) def int2bin(d): s="" for i in range(0,32): s+=chr(d%256) d/=256 return s[::-1] ordera = 0 orderb = 2**256 - 1 while 1: s = socket.create_connection(hostport) d= s.recv(1024) m = re.search('.*starts with (.*) which md5.*', d) pref = m.group(1) while 1: st = "01234567"*7 resp = pref + ''.join(random.sample(st,len(st))) if hashlib.md5(resp).hexdigest().startswith('0000'): break s.send(resp) d= s.recv(1024) password = "Kevin"+("\n"*27) s.send(password) d= s.recv(1024) si=2 orderc = ordera + (orderb - ordera)/2 ri = orderc tmp = int2bin(ri) + int2bin(si) sig = base64.b64encode(tmp) s.send(sig) n1=datetime.datetime.now() d= s.recv(1024) n2=datetime.datetime.now() x=(n2-n1).microseconds if x<100000: orderb = orderc else: ordera = orderc print x,ordera, orderb s.close()And that is everything what we need to solve this task.
import base64 import socket import re import random import hashlib import datetime hostport = ('54.216.116.38', 3016) def int2bin(d): s="" for i in range(0,32): s+=chr(d%256) d/=256 return s[::-1] ordera = 0 orderb = 2**256 - 1 s = socket.create_connection(hostport) d= s.recv(1024) m = re.search('.*starts with (.*) which md5.*', d) pref = m.group(1) while 1: st = "01234567"*7 resp = pref + ''.join(random.sample(st,len(st))) if hashlib.md5(resp).hexdigest().startswith('0000'): break s.send(resp) d= s.recv(1024) password = "Kevin"+("\n"*27) s.send(password) d= s.recv(1024) n = 89953523493328636138979614835438769106005948670998555217484157791369906305783 da =68503307448214310387573639006216872681840007669594105206515313184282784925849 k = 36738773201348520209625638351073339244477604751576415245956940910111085841389 ri = 59596085284558034169187633095832466866630673714956966387930595096089236063353 hash = int('f1cd318e412b5f7226e5f377a9544ff7',16) si = ( inverse_mod( k, n ) * ( hash + ( da * ri ) % n ) ) % n tmp = int2bin(ri) + int2bin(si) sig = base64.b64encode(tmp) s.send(sig) d= s.recv(1024) print d
No comments:
Post a Comment