当前位置:   article > 正文

[SECCON CTF 2022] crypto 部分复现_symbolic_mersenne_cracker

symbolic_mersenne_cracker

目录

janken vs kurenaif

pqpq 以前写过了,官方方法一样

witches_symmetric_exam

this is not lsb

insufficient CVP

BBB


当时仅pqpq差点完成,有个小错。其它都无从下手。终于看到wp一一试一下。原文

janken vs kurenaif

这个情况第一回见,题目给出一个种子,并通过种子生成666个[0,1,2]的随机数,来玩石头-剪刀-布,要求给出一个种子,生成的随机数都是对应的[1,2,0]。

python的随机数使用梅森旋转MT19937来生成,以前的题基本都是复现序列,通过已知的随机数部分来恢复state,从而得到与原题相同的伪随机数。这个题是在一个不满秩的情况下求种子。

  1. import os
  2. import signal
  3. import random
  4. import secrets
  5. FLAG = os.getenv("FLAG", "fake{cast a special spell}")
  6. def janken(a, b):
  7. return (a - b + 3) % 3
  8. signal.alarm(1000)
  9. print("kurenaif: Hi, I'm a crypto witch. Let's a spell battle with me.")
  10. witch_spell = secrets.token_hex(16)
  11. witch_rand = random.Random()
  12. witch_rand.seed(int(witch_spell, 16))
  13. print(f"kurenaif: My spell is {witch_spell}. What about your spell?")
  14. your_spell = input("your spell: ")
  15. your_random = random.Random()
  16. your_random.seed(int(your_spell, 16))
  17. for _ in range(666):
  18. witch_hand = witch_rand.randint(0, 2)
  19. your_hand = your_random.randint(0, 2)
  20. if janken(your_hand, witch_hand) != 1:
  21. print("kurenaif: Could you come here the day before yesterday?")
  22. quit()
  23. print("kurenaif: Amazing! Your spell is very powerful!!")
  24. print(f"kurenaif: OK. The flag is here. {FLAG}")

第一步就是先求 state

  1. from pwn import *
  2. import random
  3. io = remote("janken-vs-kurenaif.seccon.games", 8080)
  4. context.log_level = 'debug'
  5. io.recvuntil(b"kurenaif: My spell is ")
  6. u_spell = io.recvuntil(b'.', drop= True).decode()
  7. print(f"{u_spell = }")
  8. #u_spell = 'df74f6c5c096355a87ec544c1ac865e8'
  9. u_rand = random.Random()
  10. u_rand.seed(int(u_spell, 16))
  11. u_hand = [u_rand.randint(0,2) for _ in range(666)]
  12. i_hand = [(i+1)%3 for i in u_hand]
  13. #solve prng
  14. from Untwister import Untwister as _Untwister
  15. import logging
  16. logging.disable()
  17. # Superclass Untwister to obtain PRNG state after seeding
  18. # instead of PRNG state after outputs generated
  19. class Untwister(_Untwister):
  20. def __init__(self):
  21. super().__init__()
  22. self.first_MT = self.MT
  23. # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L201
  24. # Index is 624 after seeding
  25. self.index = 624
  26. def get_random(self):
  27. # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L232
  28. # First MT state is set to 0x80000000 after seeding
  29. self.solver.add(self.first_MT[0] == 0x80000000)
  30. print(self.solver.check())
  31. model = self.solver.model()
  32. state = [
  33. model[x].as_long() if model[x] is not None else 0
  34. for x in self.first_MT
  35. ]
  36. result_state = (3, tuple(state+[624]), None)
  37. rand = random.Random()
  38. rand.setstate(result_state)
  39. return rand
  40. ut = Untwister()
  41. for me in i_hand:
  42. res = bin(me)[2:].zfill(2) + "?" * 30 #fill to 32bits
  43. ut.submit(res)
  44. ut_rand = ut.get_random()
  45. ut_rand_state = ut_rand.getstate()[1][:-1]
  46. # sanity check
  47. for me in i_hand:
  48. assert me == ut_rand.randint(0, 2)

这里边用到一个模块Untwister 可以到github下载,其实本身很短,直接放在这。

  1. #https://raw.githubusercontent.com/icemonster/symbolic_mersenne_cracker/main/main.py
  2. from z3 import *
  3. from random import Random
  4. from itertools import count
  5. from time import time
  6. import logging
  7. logging.basicConfig(format='STT> %(message)s')
  8. logger = logging.getLogger()
  9. logger.setLevel(logging.DEBUG)
  10. SYMBOLIC_COUNTER = count()
  11. class Untwister:
  12. def __init__(self):
  13. name = next(SYMBOLIC_COUNTER)
  14. self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)]
  15. self.index = 0
  16. self.solver = Solver()
  17. #This particular method was adapted from https://www.schutzwerk.com/en/43/posts/attacking_a_random_number_generator/
  18. def symbolic_untamper(self, solver, y):
  19. name = next(SYMBOLIC_COUNTER)
  20. y1 = BitVec(f'y1_{name}', 32)
  21. y2 = BitVec(f'y2_{name}' , 32)
  22. y3 = BitVec(f'y3_{name}', 32)
  23. y4 = BitVec(f'y4_{name}', 32)
  24. equations = [
  25. y2 == y1 ^ (LShR(y1, 11)),
  26. y3 == y2 ^ ((y2 << 7) & 0x9D2C5680),
  27. y4 == y3 ^ ((y3 << 15) & 0xEFC60000),
  28. y == y4 ^ (LShR(y4, 18))
  29. ]
  30. solver.add(equations)
  31. return y1
  32. def symbolic_twist(self, MT, n=624, upper_mask=0x80000000, lower_mask=0x7FFFFFFF, a=0x9908B0DF, m=397):
  33. '''
  34. This method models MT19937 function as a Z3 program
  35. '''
  36. MT = [i for i in MT] #Just a shallow copy of the state
  37. for i in range(n):
  38. x = (MT[i] & upper_mask) + (MT[(i+1) % n] & lower_mask)
  39. xA = LShR(x, 1)
  40. xB = If(x & 1 == 0, xA, xA ^ a) #Possible Z3 optimization here by declaring auxiliary symbolic variables
  41. MT[i] = MT[(i + m) % n] ^ xB
  42. return MT
  43. def get_symbolic(self, guess):
  44. name = next(SYMBOLIC_COUNTER)
  45. ERROR = 'Must pass a string like "?1100???1001000??0?100?10??10010" where ? represents an unknown bit'
  46. assert type(guess) == str, ERROR
  47. assert all(map(lambda x: x in '01?', guess)), ERROR
  48. assert len(guess) <= 32, "One 32-bit number at a time please"
  49. guess = guess.zfill(32)
  50. self.symbolic_guess = BitVec(f'symbolic_guess_{name}', 32)
  51. guess = guess[::-1]
  52. for i, bit in enumerate(guess):
  53. if bit != '?':
  54. self.solver.add(Extract(i, i, self.symbolic_guess) == bit)
  55. return self.symbolic_guess
  56. def submit(self, guess):
  57. '''
  58. You need 624 numbers to completely clone the state.
  59. You can input less than that though and this will give you the best guess for the state
  60. '''
  61. if self.index >= 624:
  62. name = next(SYMBOLIC_COUNTER)
  63. next_mt = self.symbolic_twist(self.MT)
  64. self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)]
  65. for i in range(624):
  66. self.solver.add(self.MT[i] == next_mt[i])
  67. self.index = 0
  68. symbolic_guess = self.get_symbolic(guess)
  69. symbolic_guess = self.symbolic_untamper(self.solver, symbolic_guess)
  70. self.solver.add(self.MT[self.index] == symbolic_guess)
  71. self.index += 1
  72. def get_random(self):
  73. '''
  74. This will give you a random.Random() instance with the cloned state.
  75. '''
  76. logger.debug('Solving...')
  77. start = time()
  78. self.solver.check()
  79. model = self.solver.model()
  80. end = time()
  81. logger.debug(f'Solved! (in {round(end-start,3)}s)')
  82. #Compute best guess for state
  83. state = list(map(lambda x: model[x].as_long(), self.MT))
  84. result_state = (3, tuple(state+[self.index]), None)
  85. r = Random()
  86. r.setstate(result_state)
  87. return r

得到state后可以验证真能生成指定的序列。

第二步是根据这个state计算种子,这个用z3来实现,在复现过程中发现计算这东西不是每次都成功,有的数据可能会运算很久也不出结果。所以不行的时候可以重来换个spell。这块也是没有能力写的,只能原文复制下来。然后保存后用。

  1. #2.get seed
  2. from z3 import *
  3. prng_N = 624
  4. def u32(x):
  5. return x & 0xffffffff
  6. # Note that LShR is used instead of ">>" operator
  7. # unsigned 32 bit integers use logical bitshift, not arithmetic bitshift.
  8. # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L186
  9. def init_genrand(s):
  10. mt = [0 for _ in range(prng_N)]
  11. mt[0] = BitVecVal(s, 32)
  12. mti = 1
  13. while mti < prng_N:
  14. mt[mti] = u32(
  15. 1812433253 * (mt[mti-1] ^ LShR(mt[mti-1], 30)) + mti
  16. # 1812433253 * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti
  17. )
  18. mti += 1
  19. return mt, mti
  20. # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L209
  21. def init_by_array(init_key):
  22. key_length = len(init_key)
  23. mt, mti = init_genrand(19650218)
  24. i, j = 1, 0
  25. k = prng_N if prng_N > key_length else key_length
  26. while k:
  27. mt[i] = u32(
  28. (mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1664525)) + init_key[j] + j
  29. )
  30. i, j = i + 1, j + 1
  31. if i >= prng_N:
  32. mt[0] = mt[prng_N-1]
  33. i = 1
  34. if j >= key_length:
  35. j = 0
  36. k -= 1
  37. k = prng_N - 1
  38. while k:
  39. mt[i] = u32(
  40. (mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1566083941)) - i
  41. )
  42. i += 1
  43. if i >= prng_N:
  44. mt[0] = mt[prng_N-1]
  45. i = 1
  46. k -= 1
  47. mt[0] = 0x80000000;
  48. return mt
  49. seed_vars = [
  50. BitVec(f"seed_{i}", 32) for i in range(624)
  51. ]
  52. seed_rand_state = init_by_array(seed_vars)
  53. s = Solver()
  54. for a, b in zip(seed_rand_state, ut_rand_state):
  55. s.add(a == b)
  56. print('check:',s.check())
  57. model = s.model()
  58. print(f"{model = }")
  59. seed_init_key = [
  60. model[x].as_long() if model[x] is not None else 0
  61. for x in seed_vars
  62. ]
  63. print(f"{seed_init_key[:10] = }")

最后把所有计算得到的key连起来(每个32位,4字节,一个非常长的数)

  1. #3,get my spell
  2. seed_rand_seed = sum([x * (2 ** (idx * 32))for idx, x in enumerate(seed_init_key)])
  3. print("seed_rand_seed =", hex(seed_rand_seed)[2:52], "...")
  4. seed_rand = random.Random()
  5. seed_rand.seed(seed_rand_seed)
  6. # sanity check
  7. assert seed_rand.getstate()[1][:-1] == ut_rand_state
  8. for me in i_hand:
  9. assert me == seed_rand.randint(0, 2)
  10. print(f"I spell:{hex(seed_rand_seed)[2:]}")
  11. io.sendline(f"{hex(seed_rand_seed)[2:]}".encode())
  12. print(io.recvall().decode())

pqpq 以前写过了,官方方法一样

witches_symmetric_exam

题目很明了,题目先将secret_spell用AES_GCM加密,然后将tag,nonce与cipher连一起,再进行AES_OFB方式加密,返回iv+cipher。

后台提供解密功能,但仅返回错在哪。

  1. from Crypto.Cipher import AES
  2. from Crypto.Random import get_random_bytes
  3. from Crypto.Util.Padding import pad, unpad
  4. from flag import flag, secret_spell
  5. key = get_random_bytes(16)
  6. nonce = get_random_bytes(16)
  7. def encrypt():
  8. data = secret_spell
  9. gcm_cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
  10. gcm_ciphertext, gcm_tag = gcm_cipher.encrypt_and_digest(data) #CTR+消息验证码(?)
  11. ofb_input = pad(gcm_tag + gcm_cipher.nonce + gcm_ciphertext, 16)
  12. ofb_iv = get_random_bytes(16)
  13. ofb_cipher = AES.new(key, AES.MODE_OFB, iv=ofb_iv) #OFB
  14. ciphertext = ofb_cipher.encrypt(ofb_input)
  15. return ofb_iv + ciphertext
  16. def decrypt(data):
  17. ofb_iv = data[:16]
  18. ofb_ciphertext = data[16:]
  19. ofb_cipher = AES.new(key, AES.MODE_OFB, iv=ofb_iv)
  20. try:
  21. m = ofb_cipher.decrypt(ofb_ciphertext)
  22. temp = unpad(m, 16) #从最后一位padding
  23. except:
  24. return b"ofb error"
  25. try:
  26. gcm_tag = temp[:16]
  27. gcm_nonce = temp[16:32]
  28. gcm_ciphertext = temp[32:]
  29. gcm_cipher = AES.new(key, AES.MODE_GCM, nonce=gcm_nonce)
  30. plaintext = gcm_cipher.decrypt_and_verify(gcm_ciphertext, gcm_tag)
  31. except:
  32. return b"gcm error"
  33. if b"give me key" == plaintext:
  34. your_spell = input("ok, please say secret spell:").encode()
  35. if your_spell == secret_spell:
  36. return flag
  37. else:
  38. return b"Try Harder"
  39. return b"ok"
  40. print(f"ciphertext: {encrypt().hex()}")
  41. while True:
  42. c = input("ciphertext: ")
  43. print(decrypt(bytes.fromhex(c)))

这显然是个padding oracle的题,ofb和gcm都是通过与固定的流异或来得到密文。通过尾部爆破看解密状态得到加密用的流。

如果padding为1字节那么尾字节就是01,当用0-255爆破时,恰好没报错的那个数与1异或就是加密流的最后一个字节,依次向前两字节就是0202,直到16。

第一段我还是明白的,leak_enc用来爆破加密流,每次发送256种情况,然后从返回中取状态,这样可以加快爆破速度,每次recvuntil是非常费时的。

  1. from pwn import *
  2. context.log_level = "error"
  3. r = remote("witches-symmetric-exam.seccon.games", 8080)
  4. context.log_level = 'debug'
  5. r.recvuntil(b"ciphertext: ")
  6. enc = bytes.fromhex(r.recvline().decode())
  7. print(f"{len(enc) = }")
  8. print(f"{enc.hex() = }")
  9. #get ofb stream
  10. def bxor(*args):
  11. if len(args) == 1:
  12. return args[0]
  13. a, b = args[0], args[1:]
  14. return bytes([i ^ j for i, j in zip(a, bxor(*b))])
  15. def query(xs):
  16. r.sendline("\n".join([x.hex() for x in xs]).encode())
  17. ress = []
  18. for _ in xs:
  19. r.recvuntil(b"ciphertext: ")
  20. res = r.recv(5).strip().decode()
  21. if "b'ofb" in res:
  22. ress.append("pad")
  23. elif "b'gcm" in res:
  24. ress.append("gcm")
  25. elif "o', p" in res:
  26. ress.append("spell")
  27. elif "b'ok'" in res:
  28. ress.append("ok")
  29. else:
  30. raise ValueError(res)
  31. return ress
  32. def leak_enc(pln):
  33. enc = [0 for _ in range(16)]
  34. for pos in range(16):
  35. print(".", end="", flush=True)
  36. expected_padding = ([0] * 16 + [pos+1] * (pos+1))[-16:]
  37. found = []
  38. probe = [0] * 16
  39. xs = []
  40. for i in range(256):
  41. probe[15-pos] = i
  42. try_block_ofb = pln + bxor(enc, expected_padding, probe)
  43. xs.append(try_block_ofb)
  44. #每次发送256个,然后再取返回信息,加快爆破速度
  45. ress = query(xs)
  46. for i, res in enumerate(ress):
  47. if res == "gcm":
  48. found.append(i)
  49. if len(found) != 1:
  50. raise ValueError("???", pos, found)
  51. enc[15-pos] = found[0]
  52. print()
  53. return bytes(enc)

然后把4段的加密流得到,异或得到gcm的tag,nonce,cipher_gcm

  1. #get ofb
  2. from Crypto.Util.Padding import unpad
  3. ofb_iv = enc[:16]
  4. ofb_enc = enc[16:]
  5. print(f"{ofb_iv.hex() = }")
  6. ofb_stream = ofb_iv
  7. for block in range(4):
  8. ofb_stream += leak_enc(ofb_stream[-16:])
  9. ofb_stream = ofb_stream[16:]
  10. print(f"{ofb_stream.hex() = }")
  11. ofb_dec = bxor(ofb_enc, ofb_stream)
  12. gcm_tag, gcm_nonce, gcm_enc = ofb_dec[:16], ofb_dec[16:32], unpad(ofb_dec[32:], 16)
  13. print(f"{gcm_tag.hex() = }")
  14. print(f"{gcm_nonce.hex() = }")
  15. print(f"{gcm_enc.hex() = }")

第三步是爆破 gcm的流,这块目前还不大明白,慢慢来,GCM需要计算tag,tag是通过h0和密文通过_ghash_clmul方法运算得到,除了这个ofb,gcm基本上一样。

  1. #GCM parameters
  2. from Crypto.Util.number import long_to_bytes, bytes_to_long
  3. from Crypto.Cipher._mode_gcm import _GHASH as GHASH, _ghash_clmul as ghash_c
  4. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L223-L226
  5. h0 = leak_enc(b"\x00" * 16)
  6. print(f"{h0.hex() = }")
  7. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L232-L236
  8. nonce = gcm_nonce
  9. fill = (16 - (len(nonce) % 16)) % 16 + 8
  10. ghash_in = (nonce + b'\x00' * fill + long_to_bytes(8 * len(nonce), 8))
  11. j0 = GHASH(h0, ghash_c).update(ghash_in).digest()
  12. print(f"{j0.hex() = }")
  13. #Decrypt GCM
  14. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L239-L245
  15. # nonce_ctr = j0[:12]
  16. # iv_ctr = (bytes_to_long(j0) + 1) & 0xFFFFFFFF
  17. # nonce_iv_bytes = nonce_ctr + bytes.fromhex(hex(iv_ctr)[2:].zfill(8))
  18. block_one_ctr = bytes.fromhex(hex(int(j0.hex(), 16) + 1)[2:].zfill(32))
  19. block_two_ctr = bytes.fromhex(hex(int(j0.hex(), 16) + 2)[2:].zfill(32))
  20. gcm_stream = leak_enc(block_one_ctr) + leak_enc(block_two_ctr)
  21. gcm_pln = bxor(gcm_stream, gcm_enc)
  22. secret_spell = gcm_pln
  23. print(f"{secret_spell = }")

得到secret_spell就可以包装gcm,ofb包了,发送即返回flag

  1. #3, encode 'give me key'
  2. from Crypto.Util.Padding import pad
  3. signer = GHASH(h0, ghash_c)
  4. token_dec = b"give me key"
  5. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L372-L379
  6. # > encrypt and digest > encrypt
  7. token_enc = bxor(token_dec, gcm_stream)
  8. msg_len = len(token_enc)
  9. # signer.update(token_enc) # defer to later for block alignment
  10. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L459-L462
  11. # > encrypt and digest > digest
  12. signer.update(token_enc + b"\x00" * (16 - msg_len))
  13. signer.update(long_to_bytes(8 * 0, 8) + long_to_bytes(8 * msg_len, 8))
  14. tag_dec = signer.digest()
  15. # https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L465
  16. # > encrypt and digest > digest > self._tag_cipher.encrypt
  17. j0_stream = leak_enc(j0)
  18. tag_enc = bxor(tag_dec, j0_stream)
  19. print(f"{tag_enc.hex() = }")
  20. payload = ofb_iv + bxor(pad(tag_enc + gcm_nonce + token_enc, 16), ofb_stream)
  21. print(f"{payload.hex() = }")
  22. r.sendline(payload.hex().encode())
  23. r.sendline(secret_spell)
  24. flag = r.recvuntil(b"}").decode().strip()
  25. print(flag)
  26. #SECCON{you_solved_this!?I_g1ve_y0u_symmetr1c_cipher_mage_certificate}

this is not lsb

LSB都整来,对于c = m^e mod n 来说,不断的将m乘2(c*2^e mod n)判断末位是0还是1,以确定2m与n的关系。这个题有点类似,返回的不是末位而是第1位是不是0xff

  1. from Crypto.Util.number import *
  2. #from flag import flag
  3. flag = bytes_to_long(b'A'*55)
  4. p = getStrongPrime(512)
  5. q = getStrongPrime(512)
  6. print(p,q)
  7. e = 65537
  8. n = p * q
  9. phi = (p - 1) * (q - 1)
  10. d = pow(e, -1, phi)
  11. print(f"n = {n}")
  12. print(f"e = {e}")
  13. print(f"flag_length = {flag.bit_length()}")
  14. # Oops! encrypt without padding!
  15. c = pow(flag, e, n)
  16. print(f"c = {c}")
  17. # padding format: 0b0011111111........
  18. def check_padding(c):
  19. padding_pos = n.bit_length() - 2
  20. m = pow(c, d, n)
  21. return (m >> (padding_pos - 8)) == 0xFF
  22. while True:
  23. c = int(input("c = "))
  24. print(check_padding(c))

 看了WP,感觉啊,还可以这样。方法依然是二分法。

先确定flag的上下边界(已知flag以SECCON{开头,后补全0和全1),然后确定一个除法的最小值:int('00'+'1'*8+'0'*(b.bit_length()-10),2),然后每发送 dec//flag_md(上下边界的中间值)的e 次幂再乘c ,当返回是0xff时则说明flag在上半部(比一半大)。修改边界循环即可。

  1. from pwn import *
  2. context.log_level = "error"
  3. # ====== Get parameters ======
  4. r = remote("this-is-not-lsb.seccon.games", 8080)
  5. r.recvuntil(b"n = ")
  6. n = int(r.recvline().strip().decode())
  7. r.recvuntil(b"e = ")
  8. e = int(r.recvline().strip().decode())
  9. r.recvuntil(b"flag_length = ")
  10. flag_length = int(r.recvline().strip().decode())
  11. r.recvuntil(b"c = ")
  12. c = int(r.recvline().strip().decode())
  13. print(f"{n = }")
  14. print(f"{e = }")
  15. print(f"{flag_length = }")
  16. print(f"{c = }")
  17. def query(k):
  18. y = (pow(k, e, n) * c) % n
  19. r.recvuntil(b"c = ")
  20. r.sendline(f"{y}".encode())
  21. res = r.recvline().decode().strip()
  22. if res == "True":
  23. return True
  24. elif res == "False":
  25. return False
  26. else:
  27. raise ValueError(res)
  28. dec_lb = int("0011111111".ljust(n.bit_length(), "0"), 2)
  29. dec_ub = int("0011111111".ljust(n.bit_length(), "1"), 2)
  30. flag_lb = int((b"SECCON{"+ b"\x00" * 48).hex(), 16)
  31. flag_ub = int((b"SECCON{"+ b"\xff" * 48).hex(), 16)
  32. # ====== Binary search ======
  33. while flag_ub - flag_lb > 10:
  34. flag_md = (flag_lb + flag_ub) // 2
  35. coef = dec_lb // flag_md
  36. if query(coef):
  37. print("-", end="", flush=True)
  38. flag_lb = flag_md
  39. else:
  40. print("+", end="", flush=True)
  41. flag_ub = flag_md
  42. print()
  43. flag = bytes.fromhex(hex(flag_lb)[2:])
  44. print(f"{flag = }")

insufficient CVP

又是一个看着大概认识又不会的题。求系数用格基约减。但这里差一点,就是有两个未知的情况。

  1. from random import randint
  2. from Crypto.Util.number import getPrime, bytes_to_long
  3. from secret import FLAG
  4. # f(x,y,z) = a1*x + a2*x^2 + a3*x^3
  5. # + b1*y + b2*y^2 + b3*y^3
  6. # + c*z + s mod p
  7. def calc_f(coeffs, x, y, z, p):
  8. ret = 0
  9. ret += x * coeffs[0] + pow(x, 2, p) * coeffs[1] + pow(x, 3, p)*coeffs[2]
  10. ret += y * coeffs[3] + pow(y, 2, p) * coeffs[4] + pow(y, 3, p)*coeffs[5]
  11. ret += z * coeffs[6]
  12. ret += coeffs[7]
  13. return ret % p
  14. p = getPrime(512)
  15. # [a1, a2, a3, b1, b2, b3, c, s]
  16. coeffs = [randint(0, 2**128) for _ in range(8)]
  17. key = 0
  18. for coeff in coeffs:
  19. key <<= 128
  20. key ^= coeff
  21. cipher_text = bytes_to_long(FLAG) ^ key
  22. print(cipher_text)
  23. shares = []
  24. for _ in range(4):
  25. x = randint(0, p)
  26. y = randint(0, p)
  27. z = randint(0, 2**128)
  28. w = calc_f(coeffs, x, y, z, p)
  29. packed_share = ((x,y), w)
  30. shares.append(packed_share)
  31. print(p)
  32. print(shares)

这里4个x,y已经给出,但z没给,假设没有6,7两个系数的情况下用

B = \begin{bmatrix} x_{0} & x_{1} & ... & x_{3} & 1 & & & \\ x_{0}^{2} & x_{1}^{2} & ... & x_{3}^{2} & & 1 & & \\ ... & ... & ... & ... & & & ... & \\ y_{0}^{3} & y_{1}^{3} & ... & y_{3}^{3} & & & & 1\\ -w_{0} & -w_{1} & ... & -w_{3} & & & & \end{bmatrix}

这里多的c7,c7,z都只有128位,与已知的x,y,p的512比很小,这里先将其忽略求出近似值,将右侧的单位对角阵乘2^128,在左上与w之间补一个对角阵p,并在最后加一列写一个极大值,这样可以用CVP算法求得前6个参数*2^128的挖值,再除去2^128即可得到前6个参数。

  1. #read msg
  2. f = open('output.txt')
  3. cipher_text = int(f.readline())
  4. p = int(f.readline())
  5. shares = eval(f.readline())
  6. '''
  7. | A B O | A= x0,x1,... y0^3,y1^3,...
  8. M = | C O O | B= I*2^128 C= I*p
  9. | D E L | D= -w0,-w1.. L= large
  10. E= O, 对M进行格基约减后,得到的前6个参数
  11. '''
  12. r, c = 11, 11
  13. two_127 = 2**127
  14. two_128 = 2**128
  15. large = 2**1024
  16. A = matrix(ZZ, 6,4)
  17. B = identity_matrix(ZZ,6)*2^128
  18. C = identity_matrix(ZZ,4)*p
  19. D = matrix(ZZ, 1,4)
  20. L = matrix(ZZ, 1,1, [large])
  21. for i in range(4):
  22. xi,yi = shares[i][0]
  23. A[0,i],A[1,i],A[2,i] = xi, xi**2, xi**3
  24. A[3,i],A[4,i],A[5,i] = yi, yi**2, yi**3
  25. D[0,i] = -shares[i][1]
  26. M = block_matrix(3, [[A,B,zero_matrix(ZZ,6,1)],
  27. [C,zero_matrix(ZZ,4,6),zero_matrix(ZZ,4,1)],
  28. [D,zero_matrix(ZZ,1,6),L]])
  29. vec = vector(ZZ,11)
  30. for i in range(6):
  31. vec[i] = two_127*two_128
  32. vec[10] = large
  33. # https://jgeralnik.github.io/writeups/2021/08/12/Lattices/
  34. def closest_vector(B, target):
  35. # Babai's Nearest Plane algorithm
  36. M = B.LLL()
  37. G = M.gram_schmidt()[0]
  38. small = target
  39. for _ in range(1):
  40. for i in reversed(range(M.nrows())):
  41. c = ((small * G[i]) / (G[i] * G[i])).round()
  42. small -= M[i] * c
  43. return target - small
  44. cv = closest_vector(M, vec)
  45. assert cv[10] == large
  46. for idx, cvi in enumerate(cv):
  47. print(f"cv[{idx}] = {cvi}")
  48. coeffs = [i//two_128 for i in cv[4:10]]
  49. czs = [int((w - (x * coeffs[0] + pow(x, 2, p) * coeffs[1] + pow(x, 3, p)*coeffs[2]
  50. + y * coeffs[3] + pow(y, 2, p) * coeffs[4] + pow(y, 3, p)*coeffs[5]))%p) for (x,y),w in shares] #模值需要转整
  51. c6 = gcd(czs[1]-czs[0], czs[2]-czs[0])
  52. c7 = czs[0]%c6
  53. coeffs += [c6,c7]
  54. key = 0
  55. for coeff in coeffs:
  56. key <<= 128
  57. key ^^= coeff
  58. flag = cipher_text ^^ key
  59. flag = bytes.fromhex(hex(flag)[2:])
  60. #SECCON{Unfortunately_I_could_not_come_up_with_a_more_difficult_problem_than_last_year_sorry...-6fc18307d3ed2e7673a249abc2e0e22c}

这里在求czs的时候没有用WP里的方法,而是直接用的式子,在sage里由于作了取模,所以czs[1]-czs[0]会自动取模得到模p的值,从而无法用gcd,所以在取模后需要用int对数字进行转换,转成数字型再运算。看别人几句话就写出来,自己一练才发现功夫还着得远。

BBB

最后一个BBB就相对简单点了,远端是个RNG,a,p已知,b可以输入,那这个RNG就可以控制了,但运算次数不定,在10-100之间。要求输入5个种子,用生成的值作为e,并且每次都生成n,并对flag加密后返回。

  1. from Crypto.Util.number import bytes_to_long, getPrime
  2. from random import randint
  3. from math import gcd
  4. from secret import FLAG
  5. from os import urandom
  6. assert len(FLAG) < 100
  7. def generate_key(rng, seed):
  8. e = rng(seed)
  9. while True:
  10. for _ in range(randint(10,100)):
  11. e = rng(e)
  12. p = getPrime(1024)
  13. q = getPrime(1024)
  14. phi = (p-1)*(q-1)
  15. if gcd(e, phi) == 1:
  16. break
  17. n = p*q
  18. return (n, e)
  19. def generate_params():
  20. p = getPrime(1024)
  21. a = randint(0, p-1)
  22. return (p,a)
  23. def main():
  24. p,a = generate_params()
  25. print("[+] The parameters of RNG:")
  26. print(f"{a=}")
  27. print(f"{p=}")
  28. b = int(input("[+] Inject [b]ackdoor!!: "))
  29. rng = lambda x: (x**2 + a*x + b) % p
  30. keys = []
  31. seeds = []
  32. for i in range(5):
  33. seed = int(input("[+] Please input seed: "))
  34. seed %= p
  35. if seed in seeds:
  36. print("[!] Same seeds are not allowed!!")
  37. exit()
  38. seeds.append(seed)
  39. n, e = generate_key(rng, seed)
  40. if e <= 10:
  41. print("[!] `e` is so small!!")
  42. exit()
  43. keys.append((n,e))
  44. flag = bytes_to_long(FLAG + urandom(16))
  45. for n,e in keys:
  46. c = pow(flag, e, n)
  47. print("[+] Public Key:")
  48. print(f"{n=}")
  49. print(f"{e=}")
  50. print("[+] Cipher Text:", c)
  51. if __name__ == "__main__":
  52. main()

思路也简单,首先就是这个题n肯定是没法分解,给5组值也就是用中国剩余定理了,这就要求e要相同,当然e必需相同,不然rng会出来不确定的值。

首先通过输入b来确定e,也就是让f(x)==x 由于有要求e必需大于10所以这里e选11,先求得b

e == e^{2} + a*e + b (mod p)

这样不管运行多少次都能保证结果是11也就能确定e

然后要求输入5组x那就要算出 f(x1) == e,f(x2)== x1,...

因为前边有10次运算,并且大概率这个运算会超过20次,所以空间很大,不行就重爆破。

当然这里因为有x^2所以每步都可能有两个解,凑够4个即可。

最后将得到的c,n用 crt得到m再开11次方。

  1. from pwn import *
  2. io = remote('BBB.seccon.games', 8080)
  3. context.log_level = 'debug'
  4. io.recvuntil(b'a=')
  5. a = int(io.recvline().strip())
  6. io.recvuntil(b'p=')
  7. p = int(io.recvline().strip())
  8. #rng(11) = 11^2 + a*11 + b mod p
  9. P.<x> = PolynomialRing(Zmod(p), 'x')
  10. e = 11
  11. eq = e^2 + a*e + x - e
  12. b = eq.roots()[0][0]
  13. print(f'{b=}')
  14. #rng(a1) = a2 ;rng(a2) = a3 ; ... rng(a4) = 11; rng(11) = 11
  15. ra = [e]
  16. while len(ra)<5:
  17. eq = x^2 + a*x + b - ra[-1]
  18. d = eq.roots()
  19. for j,_ in d:
  20. if j not in ra:
  21. ra.append(j)
  22. print(f"{j = }")
  23. io.sendlineafter(b'[+] Inject [b]ackdoor!!:', str(b).encode())
  24. for i in range(5):
  25. io.sendlineafter(b"[+] Please input seed: ", str(ra[i]).encode())
  26. n_a ,e_a ,c_a = [], [], []
  27. for i in range(5):
  28. io.recvuntil(b'n=')
  29. n_a.append(int(io.recvline().strip()))
  30. io.recvuntil(b'[+] Cipher Text: ')
  31. c_a.append(int(io.recvline().strip()))
  32. f11 = crt(c_a,n_a)
  33. m = f11.nth_root(11)
  34. flag = bytes.fromhex(hex(m)[2:])
  35. print(flag)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/226425
推荐阅读
相关标签
  

闽ICP备14008679号