LEAST COMMON GENOMINATOR?

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

from secret import config
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, isPrime

class LCG:
    lcg_m = config.m
    lcg_c = config.c
    lcg_n = config.n

    def __init__(self, lcg_s):
        self.state = lcg_s

    def next(self):
        self.state = (self.state * self.lcg_m + self.lcg_c) % self.lcg_n
        return self.state

if __name__ == '__main__':

    assert 4096 % config.it == 0
    assert config.it == 8
    assert 4096 % config.bits == 0
    assert config.bits == 512

    # Find prime value of specified bits a specified amount of times
    seed = 211286818345627549183608678726370412218029639873054513839005340650674982169404937862395980568550063504804783328450267566224937880641772833325018028629959635
    lcg = LCG(seed)
    primes_arr = []
    
    dump = True
    items = 0
    dump_file = open("dump.txt", "w")

    primes_n = 1
    while True:
        for i in range(config.it):
            while True:
                prime_candidate = lcg.next()
                if dump:
                    dump_file.write(str(prime_candidate) + '\n')
                    items += 1
                    if items == 6:
                        dump = False
                        dump_file.close()
                if not isPrime(prime_candidate):
                    continue
                elif prime_candidate.bit_length() != config.bits:
                    continue
                else:
                    primes_n *= prime_candidate
                    primes_arr.append(prime_candidate)
                    break
        
        # Check bit length
        if primes_n.bit_length() > 4096:
            print("bit length", primes_n.bit_length())
            primes_arr.clear()
            primes_n = 1
            continue
        else:
            break

    # Create public key 'n'
    n = 1
    for j in primes_arr:
        n *= j
    print("[+] Public Key: ", n)
    print("[+] size: ", n.bit_length(), "bits")

    # Calculate totient 'Phi(n)'
    phi = 1
    for k in primes_arr:
        phi *= (k - 1)

    # Calculate private key 'd'
    d = pow(config.e, -1, phi)

    # Generate Flag
    assert config.flag.startswith(b"CTF{")
    assert config.flag.endswith(b"}")
    enc_flag = bytes_to_long(config.flag)
    assert enc_flag < n

    # Encrypt Flag
    _enc = pow(enc_flag, config.e, n)

    with open ("flag.txt", "wb") as flag_file:
        flag_file.write(_enc.to_bytes(n.bit_length(), "little"))

    # Export RSA Key
    rsa = RSA.construct((n, config.e))
    with open ("public.pem", "w") as pub_file:
        pub_file.write(rsa.exportKey().decode())
  • Tóm tắt đoạn code trên:
    • Đoạn code trên tạo một object là LCG, trong đó có 3 số được giữ bí mật là m,n,c nhưng bù lại ta biết được giá trị seed. LCG có phương thức next() nhằm tạo ra các số có giá trị = (state*m + c) % n và lưu nó vào state mới cho các lần tính tiếp theo.
    • Đề bài đã tạo ra các số 512 bits bằng LCG, 6 giá trị đầu tiên tạo ra được lưu vào file dump.txt, nếu tạo được số nguyên tố thì lưu vào mảng primes_arr, nó sẽ generate cho đến khi primes_arr đủ 8 số.
    • Dùng primes_arr làm private key để mã hóa RSA, ghi kết quả mã hóa vào file flag.txt và export public key ra file public.pem.
  • Để làm bài trên, ta phải tìm được 3 số m,n,c trong LCG, từ 6 số trong file dump.txt
  • Tìm n: Giả sử 6 số trong dump.txt là x0 tới x5, ta có:
    {x1(x0.m+c)(mod n)x2(x1.m+c)(mod n)x3(x2.m+c)(mod n)

    {(x2x1)m(x1x0)(mod n)(x3x2)m(x2x1)(mod n)(x2x1)2(x1x0)(x3x2)0(mod n)

    (x2x1)2(x3x2)(x1x0)=kn

    Tương tự:
    (x4x3)2(x3x2)(x5x4)=tn

    Do đó n là ước của gcd 2 số trên.
    Ở bài này ta may mắn tìm được luôn n là gcd 2 số đó.
  • Tìm m: Vì
    (x2x1)m(x1x0)(mod n)
    nên
    m=(x2x1).(x1x0)1(mod n)
  • Tìm c: Vì
    x1(x0m+c)(mod n)
    nên
    c=x1m.x0 (mod n)
  • Tìm được m,c,n rồi, ta chỉ cần nạp chúng vào LCG và để nó tự generate private key.
  • Source code:
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes, isPrime
from math import gcd
# from secret import config

# Đọc file .pem chứa public key RSA
with open('public.pem', 'r') as f:
    key = RSA.importKey(f.read())

# Lấy public key RSA từ file .pem
n = key.publickey().n
e = key.publickey().e

with open("flag.txt", 'rb') as ct:
    ct = ct.read()
ct = int.from_bytes(ct, byteorder='little')
with open("dump.txt", "r") as dump:
    dump = dump.read().strip().split('\n')
    dump = [int(d) for d in dump]
seed = 211286818345627549183608678726370412218029639873054513839005340650674982169404937862395980568550063504804783328450267566224937880641772833325018028629959635
n_lcg = gcd((dump[2] - dump[1])**2 - (dump[1] - dump[0]) * (dump[3] - dump[2]), (dump[4] - dump[3])**2 - (dump[3]-dump[2])*(dump[5]-dump[4]))
m_lcg = (dump[2]-dump[1])*pow((dump[1]-dump[0]), -1, n_lcg) % n_lcg
c_lcg = (dump[1] - dump[0]*m_lcg) % n_lcg

class LCG:
    lcg_m = m_lcg
    lcg_c = c_lcg
    lcg_n = n_lcg

    def __init__(self):
        self.state = seed
    def next(self):
        self.state = (self.state * self.lcg_m + self.lcg_c) % self.lcg_n
        return self.state
lcg = LCG()
primes_arr = []
bdump = True
items = 0
primes_n = 1
while True:
    for i in range(8):
        while True:
            prime_candidate = lcg.next()
            if bdump:
                items += 1
                if items == 6:
                    bdump = False
            if not isPrime(prime_candidate):
                continue
            elif prime_candidate.bit_length() != 512:
                continue
            else:
                primes_n *= prime_candidate
                primes_arr.append(prime_candidate)
                break
    
    # Check bit length
    if primes_n.bit_length() > 4096:
        print("bit length", primes_n.bit_length())
        primes_arr.clear()
        primes_n = 1
        continue
    else:
        break
assert n == primes_n
phi = 1
for k in primes_arr:
    phi *= (k - 1)
print(long_to_bytes(pow(ct,pow(e,-1,phi),n)))
# b'CTF{C0nGr@tz_RiV35t_5h4MiR_nD_Ad13MaN_W0ulD_b_h@pPy}'
  • Flag: CTF{C0nGr@tz_RiV35t_5h4MiR_nD_Ad13MaN_W0ulD_b_h@pPy}

Primes

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

def to_bits(m):
    _bin = lambda b : [1 if b & (1 << n) else 0 for n in range(7)]
    return sum([_bin(b) for b in m], [])

def gen_primes(r, n):
    primes = Primes()[:n]
    bound = prod(primes[n - r:])
    return primes, next_prime(bound)

def prod_exp(p, q, b):
    return prod([p[i]^b[i] for i in range(len(p))]) % q

def encode(r, n, m):
    p, q = gen_primes(r, n)
    return p, q, prod_exp(p, q, to_bits(m))

m = b"I have a sweet flag for you: CTF{YkDOLIStjpjP5Am1SXDt5d2r9es3b5KZP47v8rXF}"
p, q, x = encode(131, 7*len(m), m)
print(f'q = 0x{q:X}\nx = 0x{x:X}')

# q = 0xD2F8711CB5502C512ACEA59BE181A8FCF12F183B540D9A6998BF66370F9538F7E39FC507545DAD9AA2E71D3313F0B4408695A0A2C03A790662A9BD01650533C584C90779B73604FB8157F0AB7C9A82E724700E5937D9FF5FCF1EE3BE1EDD7E07B4C0F035A58CC2B9DB8B79F176F595C1B0E90B7957309B96106A50A01B78171599B41C8744BCB1C0E6A24F60AE8946D37F4D4BD8CF286A336E1022996B3BA3918E4D808627D0315BFE291AEB884CBE98BB620DAA735B0467F3287D158231D
# x = 0x947062E712C031ADD0B60416D3B87D54B50C1EFBC8DBB87346F960B242AF3DF6DD47406FEC98053A967D28FE91B130FF0FE93689122931F0BA6E73A3E9E6C873B8E2344A459244D1295E99A241E59E1EEA796E9738E6B1EDEED3D91AE6747E8ECA634C030B90B02BAF8AE0088058F6994C7CAC232835AC72D8B23A96F10EF03D74F82C49D4513423DAC298698094B5C631B9C7C62850C498330E9D112BB9CAA574AEE6B0E5E66D5B234B23C755AC1719B4B68133E680A7BCF48B4CFD0924D
  • Tóm tắt đề:
    • Đầu tiên, chuỗi bytes m sẽ được đưa vào hàm to_bits để đưa về chuỗi nhị phân bằng cách mỗi ký tự trong plaintext được đổi ra chuỗi nhị phân 7 bits theo thứ tự little edian và nối lại.
    • Số p chính là dãy gồm 7*len(flag) = 518 số nguyên tố đầu tiên, số bound trong hàm next_primes là tích của 131 số nguyên tố cuối cùng trong p, còn q là số nguyên tố nhỏ nhất lớn hơn bound (từ đó ta thấy p,q cố định do không phụ thuộc vào flag).
    • Tiếp theo hàm prod_exp sẽ tính x bằng cách lấy tích các p[i]^b[i] rồi mod q với b = to_bits(m).
  • Ở đây do có 1 phần plaintext đã biết nên mình sẽ làm các thao tác y như đề bài đã làm với plaintext, ta sẽ thu được số k (k = prod_exp(p,q,to_bits(known_text)))
  • Tới đây mình nhận ra k * prod_exp(p,q, to_bits(unknown_text)) = x mod q, do đó mình sẽ tính
    xk1mod q
    để tính giá trị kia, nhưng thực ra nó không phải giá trị ta muốn tìm :') mà nó chỉ là đồng dư với giá trị cần tìm. Giả sử kết quả phép toán trên là tmp, ta cần tìm giá trị của ct = prod_exp(p,q, to_bits(unknown_text)), ta có:
    • tmpct(mod q)
      (1)(như đã nói ở trên)
    • ct là ước của maxx với maxx là tích của 7*len(unknow_text) số nguyên tố cuối cùng trong p, điều này được suy ra bởi ct là một subset product của list các số nguyên tố trên. Do đó
      maxx=t.ct
    • Nhân vào 2 vế của (1) với t, ta được
      maxxt.tmp(mod q)t=maxx.tmp1(mod q)
      . Sau khi tìm được t, mình dễ dàng tìm được ct = maxx // t
    • Việc tìm trên cũng có khả năng ra kết quả không như mong muốn (cũng vì tìm nghiệm trong phương trình đồng dư có thể cho nhiều đáp án), tuy nhiên khi tính được giá trị ct mình đã check lại 2 điều kiện trên và nó pass :)) do đó việc ta phải làm bây giờ là khôi phục lại chuỗi bits và tìm plaintext thôi.
  • solve.sage
q = 0xD2F8711CB5502C512ACEA59BE181A8FCF12F183B540D9A6998BF66370F9538F7E39FC507545DAD9AA2E71D3313F0B4408695A0A2C03A790662A9BD01650533C584C90779B73604FB8157F0AB7C9A82E724700E5937D9FF5FCF1EE3BE1EDD7E07B4C0F035A58CC2B9DB8B79F176F595C1B0E90B7957309B96106A50A01B78171599B41C8744BCB1C0E6A24F60AE8946D37F4D4BD8CF286A336E1022996B3BA3918E4D808627D0315BFE291AEB884CBE98BB620DAA735B0467F3287D158231D
x = 0x947062E712C031ADD0B60416D3B87D54B50C1EFBC8DBB87346F960B242AF3DF6DD47406FEC98053A967D28FE91B130FF0FE93689122931F0BA6E73A3E9E6C873B8E2344A459244D1295E99A241E59E1EEA796E9738E6B1EDEED3D91AE6747E8ECA634C030B90B02BAF8AE0088058F6994C7CAC232835AC72D8B23A96F10EF03D74F82C49D4513423DAC298698094B5C631B9C7C62850C498330E9D112BB9CAA574AEE6B0E5E66D5B234B23C755AC1719B4B68133E680A7BCF48B4CFD0924D
# x = 0x38FC218F4357A4211E5B658BFEDFC0FE390248200DE63E898F9965C369B50CD246AE6953A687BD8753F0FC2D279139C1B0F3889F81C2A12AFB5B70B68F16EF05E80D534DBD04A10B6EC0B0032D739C041D7E126D93C5F7CED5162456B45760173BED1FAD043DEE833F452B96356C46AF34AFCF616EBE261E625E3DBA60E1550460E037CE2B79FAD9FA01B8B6970D6F59B819B5198C9F351D1D0B30A2AD029D18BBD334A4CEE7AF14E01B893D37AA2854CF1DAAC81E7034756C6458B76B45D

def to_bits(m):
    _bin = lambda b : [1 if b & (1 << n) else 0 for n in range(7)]
    return sum([_bin(b) for b in m], [])
len_flag = 74
def gen_primes(r, n):
    primes = Primes()[:n]
    bound = prod(primes[n - r:])
    return primes, next_prime(bound)

def prod_exp(p, q, b):
    return prod([p[i]^b[i] for i in range(len(b))])

p,_ = gen_primes(131, 7*len_flag)
print(len(p))
known_text = b'I have a sweet flag for you: CTF{'
k = prod_exp(p,q,to_bits(known_text))

tmp = x * pow(int(k),-1,q)
bit = ""
maxx = prod(p[len(known_text)*7:])

# print((int(maxx) - int(tmp))//q)
t = maxx * pow(tmp,-1,q)
assert int(maxx) % int(t) == 0 
ct = int(maxx) // int(t)
assert int(ct) * int(k) % q == x
assert (int(tmp) - int(ct)) % q == 0
for i in p[len(known_text) * 7:]:
	if int(ct) % int(i) == 0:
		bit += '1'
	else:
		bit += '0'
print(bit)
flag = known_text
for i in range(0,len(bit),7):
	flag += int(bit[i:i+7][::-1],2).to_bytes()
print(flag)
# b'I have a sweet flag for you: CTF{w0W_c0Nt1nUed_fr4Ct10ns_suR3_Ar3_fUn_Huh}'
  • Flag: CTF{w0W_c0Nt1nUed_fr4Ct10ns_suR3_Ar3_fUn_Huh}

Bài này mình làm ra nhờ vào may mắn là nhiều :D. Có lẽ tác giả ra bài này mong muốn người chơi sử dụng kiến thức của "continued fraction" (thông qua việc đề cập trong flag). Có thể trong tương lai gần, mình sẽ update thêm cách làm mới cho bài này :p
Official write-up: https://github.com/google/google-ctf/tree/master/2023/quals/crypto-primes/solution