RSA parity oracle

一、举例

即RSA奇偶性预言(oracle意为神谕)

这说的是,服务器可以解密任何密文,并至少返回密文的最低位(判断奇偶性),除了某个不可知的密文

那我们可以通过构造密文,进行二分查找,只需要次搜索就能恢复密文

假设 我们构造 那么有 我们把这个发给服务器,服务器就会返回解密结果,注意到2m一定是偶数

且由于 那么有两种情况

那么有这两种情况这两种情况分别得到范围 无论哪种情况都可以把m的范围缩小一半,然后我们继续操作 这时候我们发现没有那么显然了,有2种情况, 前者的话,注意这里要-3N,因为必须要减去奇数个N,而1个N不够

分别把范围缩小到 我们发现有一个情况直接出了m=N//2

即使其他情况,每次构造都能使得范围变为原来的1/2,这就是经典的二分搜索

那么时间复杂度是

二、数学归纳法

使用数学归纳法进行证明每次都能缩短1/2的区间

n=1的时候是显然的

假设第i次发送,那么有 于是有 那么第i+1次就会得到 为了方便对比,我们把上面第i的结果分子分母乘以2,通分一下 如果服务器返回奇数,那说明k'是奇数,那么就有 由于m是存在的,所以不等式一定有交集,

那么 上界只能变小,下界只能变大,因为前面的不等式是已经被确认成立的

根据 那么j只能小于等于k,又由于2j+1>2k,那么j又大于等于k,所以j必然等于k

于是这种情况上界没变,下界变大了 原来的区间是 变为原来的一半,且变化规律和二分法一样,即取上界和下界的中点,如果查找的数比中点大(在这里这个逻辑被换为,如果返回的数是奇数),那就把下界换成中点

如果服务器返回偶数,那说明k’是偶数 同理可得j=k,那么就有 这样上界就被缩小了

原来的区间是 变为原来的一半,且变化规律和二分法一样,即取上界和下界的中点,如果查找的数比中点小(在这里这个逻辑被换为,如果返回的数是奇数),那就把上界换成中点

综上证明完毕

三、Python实现

server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/local/bin/python3
import json
import socket
import threading
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from secrets import randbelow, token_bytes
from hashlib import sha256

HOST = "0.0.0.0"
PORT = 1337
flag = 'flag{fdfdb6c7-5fb4-4681-9cd0-a2e7d42652fa}'
p = getPrime(512)
q = getPrime(512)
n = p * q
e = 65537
d = pow(e, -1, (p-1)*(q-1))
c = pow(bytes_to_long(flag),e,n)
def handle_client(conn, addr):
try:
param = {
"n": n,
"c": c
}
conn.sendall(json.dumps(param).encode() + b"\n")
for _ in range(1024):
data = conn.recv(1024)
if not data:
break
try:
response = json.loads(data.strip())
c_in = response['c'] % n
assert c_in != c
m = pow(c_in, d, n)
b = m % 2
except (json.JSONDecodeError, KeyError, AssertionError, TypeError, ValueError):
b = 2
conn.sendall(json.dumps({"b": b}).encode() + b"\n")
finally:
conn.close()

def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f"[+] Listening on {HOST}:{PORT}")
while True:
conn, addr = s.accept()
print(f"[+] Connection from {addr}")
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()

if __name__ == "__main__":
main()

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
from Crypto.Util.number import *
import json
from tqdm import *

p = remote("127.0.0.1",1337)
param = p.recvline()[:-1]
param = json.loads(param)
n = int(param['n'])
c = int(param['c'])
from fractions import Fraction
lb = Fraction(0)
ub = Fraction(n)

e = 65537
for i in trange(1024):
cipher = (pow(2,e*(i+1),n) * c) % n
input = b'{"c": '+str(cipher).encode()+b'}'
p.sendline(input)
b = int(json.loads(p.recvline().strip())['b'])
plain = (lb+ub)/2
if b == 0:
ub = plain
else:
lb = plain
m = [int(ub),int(lb)]
print([long_to_bytes(int(i)) for i in m])

注意要用分数,这里如果用整数得到的结果不精确

实验结果

image-20250909204952450