*CTF-Qualifier-2023/Crypto/gcccd writeup
tags: ctf,crypto
*CTF-Qualifier-2023/Crypto/gcccd writeup
本题某种意义上不完全是密码学,还有一些侧信道攻击。
是独立解的,无奈手太慢,比赛9点结束,12点才跑出来
1. challenge
Can you leak my secert using gcd?
nc 122.9.153.69 23333
#!/usr/bin/env python3
from secret import flag
import socketserver
import hashlib
import signal
import random
import string
import os
p=20973268502876719886012765513713011996343752519737224550553652605696573094756255499211333096502971357908939298357512380813773140436677393056575164230564778609423872301899323721040416852230597466288892977839300189625522429038289083381035647126860128821615664730513694930502000903655609105029016636999073477487851081722316115785141
enc=lambda x:pow(17,x,p)
m=int(flag.encode().hex(),16)
def gcd(a,b,f=enc):
if b:
return gcd(b,a%b,f)
else:
return f(a)
class Task(socketserver.BaseRequestHandler):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def timeout_handler(self, signum, frame):
self.request.close()
def proof_of_work(self):
random.seed(os.urandom(8))
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
_hexdigest = hashlib.sha256(proof.encode()).hexdigest()
self.request.send(f"sha256(XXXX+{proof[4:]}) == {_hexdigest}\n".encode()+b'Give me XXXX: ')
x = self.request.recv(1024).strip(b'\n')
if len(x) != 4 or hashlib.sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
return False
return True
def handle(self):
signal.alarm(60)
if not self.proof_of_work():
return
while True:
try:
self.request.send(b'type:')
t=int(self.request.recv(1024).strip(b'\n'))
self.request.send(b'a:')
a=int(self.request.recv(1024).strip(b'\n'))
self.request.send(b'b:')
b=int(self.request.recv(1024).strip(b'\n'))
assert a>0 and b>0
if t==1:#enc test
self.request.send(b'%d\n'%gcd(a,b))
elif t==2:#leak try1
self.request.send(b'%d\n'%gcd(a,m))
elif t==3:#leak try2
self.request.send(b'%d\n'%gcd(a,b,f=lambda x:gcd(x,m)))
elif t==4:#leak try3
self.request.send(b'%d\n'%gcd(a,m,f=lambda x:gcd(x,b)))
else:
self.request.close()
break
except BrokenPipeError:
break
except:
self.request.send(b'Bad input!\n')
class ThreadedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 23333
server = ThreadedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
2. 分析
允许选手控制a, b, 提供4个选项,返回
- pow(17, gcd(a, b), p)
- pow(17, gcd(a, m), p)
- pow(17, gcd(gcd(a, b), m), p)
- pow(17, gcd(gcd(a, m), b), p)
2.1 思路一:构造包含m的DLP
初步的思路是,构造包含m的DLP,例如,令a = 0, 则 gcd(a, m) = pow(17, m, p) 。 使问题转化为离散对数问题。
本题的GF(p)关于g元素的乘法阶是光滑的,易求离散对数问题
p=20973268502876719886012765513713011996343752519737224550553652605696573094756255499211333096502971357908939298357512380813773140436677393056575164230564778609423872301899323721040416852230597466288892977839300189625522429038289083381035647126860128821615664730513694930502000903655609105029016636999073477487851081722316115785141
F = GF(p)
g = F.multiplicative_generator()
order = g.multiplicative_order()
order == p-1, factor(order)
(False,
5 * 7^2 * 11 * 13 * 29 * 31 * 37^4 * 41^2 * 43^3 * 61^2 * 67 * 71^3 * 73 * 79^2 * 83 * 89 * 97^3 * 101^4 * 103 * 107 * 109 * 113 * 127^2 * 131^2 * 137 * 139^5 * 151^2 * 157 * 163 * 167 * 173 * 179^2 * 181^3 * 191 * 193 * 197^3 * 199^2 * 211^2 * 223^3 * 227 * 229 * 239^2 * 251^2 * 257^3 * 263 * 269 * 271^5 * 281 * 283^2 * 293^2 * 307^2 * 311 * 317 * 331^3 * 337^3 * 347 * 353^2 * 359^5 * 367 * 389^2 * 397^3 * 409 * 421 * 431^3 * 433^2 * 439^2 * 443^2 * 457^2 * 461 * 467^2 * 479^4 * 487 * 499 * 503 * 509 * 521^3 * 541)
但本题限制了 a, b > 0 , 也未找到其他办法使 m 出现在方程中。
2.2 思路二:爆破m的素因子
考虑 gcd(a, m) 大部分情况都为 1, 在 a|m 时为 a, 可以根据这一特征对m因式分解。
但因为比赛时与远程的交互较慢,实际执行很困难。
实际仅爆破了第一个素因子 6079
2.3 提示:Why recursion?
赛程过半时,题目仍然0解,主办方公布了提示 “Why recursion?”
对啊,为什么gcd要使用递归呢?会导致什么问题?我考虑主要有:
- 时间复杂度
- 递归过多导致程序崩溃
2.4 思路三:gcd的时间复杂度问题
经过实验,我发现,两个数较为接近时,gcd的时间复杂度相对较高。
可以通过比较 gcd(a, b) 的时间复杂度和 gcd(gcd(a, b), m) 的时间复杂度的差,来计算 m 与某个数(gcd(a, b)的最大公约数,可控)的计算量,则可能可以恢复 m 。
但经过实验,发现主要的时间差异体现在网络波动上,无法体现计算量差异。
2.5 思路四:python的默认递归次数限制: 1000
使用递归的另一个缺点是,过多可能导致程序崩溃。
考虑,构造两个数a, b, 其计算 x = gcd(a, b) 的递归次数与 计算 gcd(x, m)的递归次数之和,刚好等于最大递归次数。于是得知,gcd(x, m) 需要递归的次数,这个特点可能恢复m
自己构造了一段时间,发现 $gcd(a, b),a,b < 2^1024$ 的递归次数一般在600左右徘徊,自己构造不出来1000左右的情况。
搜索了一下,发现 gcd 的最坏时间复杂度为 $log_2n$ , 在连续两个斐波那契数产生。
https://www.baeldung.com/cs/euclid-time-complexity
def gcd_count(a, b, count=0):
if b:
return gcd_count(b,a%b,count+1)
else:
return a, count
gcd_count(fibonacci(1000), fibonacci(1001))
(1, 1000)
实验还发现,两个连续的斐波那契数,他们的最大公约数为1, 递归次数为较小数在斐波那契数列中的位置。两个数同乘一个数,不影响gcd的递归次数
gcd_count(fibonacci(1000)*pow(2, 100), fibonacci(1001)*pow(2, 100))
(1267650600228229401496703205376, 1000)
而且,斐波那契数的长度很短,适合用于我的攻击思路
fibonacci(1000).nbits()
694
于是,可以:
- 控制: x = gcd(a, b) 的递归次数
- 通过程序崩溃时x = gcd(a, b) 的递归次数, 获得: gcd(x, m) 的递归次数
2.6 如何根据 gcd(x, m) 的递归次数 恢复m?
实验 gcd(prime, m) 的递归次数,过程中发现:
- gcd(prime, m) = 0 的递归次数,必然为3次
- gcd(prime, m) = 1 的递归次数,必然为4次
- 其余情况,可能存在重复,不同递归次数可能对应多种模方程
于是,可以考虑,遍历 gcd(prime, m) 的递归次数,根据上述特点,计算多组同余式,根据中国剩余定理求解m
m % 2 = 0: 2次递归
- gcd(2, m)
- 1 (m, 2)
- 2 (2, 0)
- -> 2
m % 2 = 1: 3次递归
- gcd(2, m)
- 1 (m, 2)
- 2 (2, 1)
- 3 (1, 0)
- -> 1
m % 5 = 0: 2次递归
- gcd(5, m)
- 1 (m, 5)
- 2 (5, 0)
- -> 0
m % 5 = 1: 3次递归
- gcd(5, m)
- 1 (m, 5)
- 2 (5, 1)
- 3 (1, 0)
- -> 1
m % 5 = 2: 4次递归
- gcd(5, m)
- 1 (m, 5)
- 2 (5, 2)
- 3 (2, 1)
- 4 (1, 0)
- -> 1
m % 5 = 3: 5次递归
- gcd(5, m)
- 1 (m, 5)
- 2 (5, 3)
- 3 (3, 2)
- 4 (2, 1)
- 5 (1, 0)
- -> 1
m % 5 = 4: 4次递归
- gcd(5, m)
- 1 (m, 5)
- 2 (5, 4)
- 3 (4, 1)
- 4 (1, 0)
- -> 1
3. solve
确认使用思路四:python的默认递归次数限制: 1000
3.1 IO
服务器环境经常断连,断了之后可重新建立连接。
import itertools
from pwn import *
context.log_level = 'info'
r = None
local = False
def init_connect(local):
if local:
r = remote("127.0.0.1", 2333)
else:
r = remote("122.9.153.69", 23333)
return r
def proof_of_work():
print("pow doing.")
def receive_proof_of_work():
r.recvuntil(b"XXXX+")
p = r.recvuntil(b")").rstrip(b")")
r.recvuntil(b"== ")
h = r.recvuntil(b'\n').rstrip(b'\n')
return p.decode(), h.decode()
def solve_proof_of_work(p, h):
for c in itertools.product(string.ascii_letters+string.digits, repeat=4):
proof = "".join(c) + p
if hashlib.sha256(proof.encode()).hexdigest() == h:
return "".join(c)
p, h = receive_proof_of_work()
proof = solve_proof_of_work(p, h)
r.sendlineafter(b'Give me XXXX: ', proof.encode())
print("pow done.")
def enc_io(t, a, b):
global r
if r is None:
r = init_connect(local)
if not local:
proof_of_work()
try:
r.recvuntil(b"type:")
r.sendline(str(t).encode())
r.recvuntil(b"a:")
r.sendline(str(a).encode())
r.recvuntil(b"b:")
r.sendline(str(b).encode())
return int(r.recvuntil(b'\n').rstrip(b'\n'))
except EOFError as err:
r = None
return enc_io(t, a, b)
def test(a, b):
return enc_io(1, a, b)
def try1(a):
"""
gcd(a,m)
"""
return enc_io(2, a, 1)
def try2(a, b):
"""
gcd(a,b,f=lambda x:gcd(x,m))
"""
return enc_io(3, a, b)
def try3(a, b):
"""
gcd(a,m,f=lambda x:gcd(x,b))
"""
return enc_io(4, a, b)
3.2 oracle: 触发递归崩溃
def oracle(t, a, b, debug=False, msg=''):
try:
enc_io(t, a, b)
except ValueError as err:
if debug:
print(err, msg)
return True
return False
oracle(1, fibonacci(1024), fibonacci(1024+1), True)
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
invalid literal for int() with base 10: b'Bad input!'
True
3.3 获得递归次数
fibs = {}
def fib(i):
if i not in fibs:
fibs[i] = fibonacci(i)
return fibs[i]
def guess(a, base=900):
"""
return remote gcd iter count
"""
for i in range(base, 1000):
if oracle(3, fib(i)*a, fib(i+1)*a):
return i
count_cache = {}
def clean_cache():
global count_cache
count_cache = {}
def gcd_count_cache(a, b, count=0):
"""
return gcd(a, b), itercount
"""
if (a, b) in count_cache:
g, c = count_cache[(a, b)]
return g, count + c
if b:
return gcd_count(b,a%b,count+1)
else:
return a, count
def gcd_counts_remainders(b):
"""
base = gcd(1, b)
return {itercount - base: remainders}
"""
# _, base = gcd_count_cache(b, 1)
counts = {}
for i in range(b):
g, count = gcd_count_cache(b, b+i)
count_cache[(b+i, b)] = (g, count)
# count -= base
if count not in counts:
counts[count] = []
counts[count].append(i)
return counts
gcd(1, m) + gcd(fib(985),fib(986)) 时崩溃, 作为计算递归次数的基准
remote_base = guess(1); remote_base
985
gcd_counts_remainders返回不同递归次数,对应的可能同余值。
counts_remainders3 = gcd_counts_remainders(3); counts_remainders3
{1: [0], 3: [1], 4: [2]}
gcd(3, m) 在服务器递归了 2(remote_base中相同步骤)+2 次,对应的可能同余值为 2
remote_count3 = guess(3, remote_base-max(counts_remainders3))
remote_count3 = remote_base - remote_count3 + 2; remote_count3
4
remote_remainder3 = counts_remainders3[remote_count3]; remote_remainder3
[2]
gcd(5, m) 在服务器递归了 3 次,对应的可能同余值为 1
counts_remainders5 = gcd_counts_remainders(5)
remote_count5 = guess(5, remote_base-max(counts_remainders5))
remote_count5 = remote_base - remote_count5 + 2
remote_remainder5 = counts_remainders5[remote_count5]
counts_remainders5, remote_count5, remote_remainder5
({1: [0], 3: [1], 4: [2, 4], 5: [3]}, 3, [1])
gcd(7, m) 在服务器递归了 4 次,对应的可能同余值为 2, 3, 6
counts_remainders7 = gcd_counts_remainders(7)
remote_count7 = guess(7, remote_base-max(counts_remainders7))
remote_count7 = remote_base - remote_count7 + 2
remote_remainder7 = counts_remainders7[remote_count7]
counts_remainders7, remote_count7, remote_remainder7
({1: [0], 3: [1], 4: [2, 3, 6], 5: [4, 5]}, 4, [2, 3, 6])
3.4 约束可能同余值
同余值不唯一,将导致使用中国剩余定理时要排列组合过多可能。
已知 gcd(2, m) = 1, 能否根据前面已知的同余方程,验证有效解?
考虑:
$m \equiv crt \pmod {\Pi p}$
gcd(pi, m):
- gcd($\Pi p$ , m)
- 1 (m, $\Pi p$)
- 2 ($\Pi p$, crt)
有:
gcd(pi, m) = gcd(pi, crt) + 2
def check_valid(remainders, primes, debug=False):
assert len(remainders) > 1 and len(primes) > 1
# if len(remainders) > 20:
# remainders = remainders[-20:]
# primes = primes[-20:]
c = crt(remainders, primes)
pi = product(primes)
_, local_count = gcd_count_cache(pi, c)
remote_count = guess(pi, remote_base-local_count-2)
remote_count = remote_base - remote_count + 2
if debug:
print(primes[-1], remainders[-1], remote_count, local_count, remote_count == local_count + 2)
return remote_count == local_count + 2
for remainder in [2, 3, 6]:
check_valid([1, 2, 1, remainder], [2, 3, 5, 7], True)
7 2 7 3 False
7 3 8 6 True
7 6 7 3 False
对素数的排列组合都进行这种验证,可以尽可能地约束同余式。
但是这里有一个问题:
参与计算的同余式数量不能太多,否则上面的结论不再成立。具体原因我也没弄明白,可能是 crt, 素数乘积不能太大,可能当这个数较大时,影响了gcd的递归次数?
另外,减少参与计算的同余式数量,也是减少计算复杂度的需要。在同余式过多时,这个函数的计算量将非常大。
def check_valid_brute(remainders, primes, debug=False):
if len(remainders) > 5:
remainders = remainders[:4] + remainders[-1:]
primes = primes[:4] + primes[-1:]
new_remainders = [remainders[-1]]
new_primes = [primes[-1]]
for remainder, prime in zip(remainders[:-1], primes[:-1]):
new_remainders.append(remainder)
new_primes.append(prime)
if not check_valid(new_remainders, new_primes, debug):
return False
return True
for remainder in [2, 3, 6]:
check_valid_brute([1, 2, 1, remainder], [2, 3, 5, 7], True)
2 1 5 4 False
2 1 5 3 True
3 2 5 3 True
5 1 8 6 True
2 1 5 2 False
3.5 遍历 m 的同余式
local=False
r.close()
r=init_connect(local)
clean_cache()
remote_base = guess(1)
[*] Closed connection to 122.9.153.69 port 23333
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
def generate_candidate_primes():
candidate_primes = []
pi = 1
prime = 2
while pi.nbits() < 250:
candidate_primes.append(prime)
prime = prime.next_prime()
pi *= prime
return candidate_primes
from tqdm.notebook import trange
pi = 1
prime = 2
remainders = []
primes = []
candidate_primes = generate_candidate_primes()
for _ in trange(len(candidate_primes)):
if pi.nbits() > 250:
break
print("bits:", pi.nbits())
counts_remainders = gcd_counts_remainders(prime)
remote_count = guess(prime, remote_base-max(counts_remainders))
remote_count = remote_base - remote_count + 2
remote_remainders = counts_remainders[remote_count]
valid_remainders = []
if len(remote_remainders) == 1:
valid_remainders = remote_remainders
else:
for remainder in remote_remainders:
if check_valid_brute(remainders + [remainder], primes + [prime], False):
valid_remainders.append(remainder)
if len(valid_remainders) == 1:
print("m %", prime, "=", valid_remainders[0])
remainders.append(valid_remainders[0])
primes.append(prime)
pi *= prime
else:
print(prime, valid_remainders)
prime = prime.next_prime()
0%| | 0/43 [00:00<?, ?it/s]
bits: 1
m % 2 = 1
bits: 2
m % 3 = 2
bits: 3
m % 5 = 1
bits: 5
m % 7 = 3
bits: 8
m % 11 = 1
bits: 12
m % 13 = 4
bits: 15
m % 17 = 8
bits: 19
m % 19 = 6
bits: 24
m % 23 = 19
bits: 28
m % 29 = 23
bits: 33
m % 31 = 14
bits: 38
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 37 = 4
bits: 43
m % 41 = 39
bits: 49
m % 43 = 13
bits: 54
m % 47 = 33
bits: 60
m % 53 = 30
bits: 65
m % 59 = 54
bits: 71
m % 61 = 7
bits: 77
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 67 = 63
bits: 83
m % 71 = 38
bits: 89
m % 73 = 31
bits: 96
m % 79 = 6
bits: 102
m % 83 = 3
bits: 108
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 89 = 1
bits: 115
m % 97 = 5
bits: 121
m % 101 = 6
bits: 128
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 103 = 7
bits: 135
m % 107 = 2
bits: 141
m % 109 = 4
bits: 148
m % 113 = 32
bits: 155
m % 127 = 22
bits: 162
m % 131 = 57
bits: 169
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 137 = 54
bits: 176
m % 139 = 64
bits: 183
m % 149 = 101
bits: 190
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 151 = 71
bits: 198
m % 157 = 124
bits: 205
m % 163 = 119
bits: 212
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 167 = 7
bits: 220
m % 173 = 45
bits: 227
[x] Opening connection to 122.9.153.69 on port 23333
[x] Opening connection to 122.9.153.69 on port 23333: Trying 122.9.153.69
[+] Opening connection to 122.9.153.69 on port 23333: Done
pow doing.
pow done.
m % 179 = 85
bits: 235
m % 181 = 172
bits: 242
m % 191 = 20
3.6 中国剩余定理还原flag
from Crypto.Util.number import long_to_bytes
long_to_bytes(crt(remainders,primes))
b'*CTF{Y0U_ar3_gO.d_@_gCD_1EAk!!}'
4个选项只用到了第三个,不确定是不是我遗漏了什么