## waRSAw, inCTF 2019 ## Description Just another conventional RSA attack, isn't it? The challenge provides an "encrypt.py" source code that contains an RSA LSB decrypt oracle. However, the particularity is that that you can query only one bit per connection and the challenge will send a different encrypted flag, and n value at each connection. Let's first see how a simple LSB RSA oracles works: ## Multiplying by powers of 2 modulo N This is what happends the simple cases of 2 and 4: When you multiply by 2: | Input Range | Output before mod N | Parity after mod N | |--------------|-----------------------------|--------------------------------------------------------------------------------------| | 0 - 0.5N | 0 - N | Even, because we multiplied by a power of 2, and the modulo N did nothing | | 0.5N - N | N - 2N | Odd, because we multiplied by a power of 2, and subtract N once (and N is odd) | When you multiply by 4: | Input Range | Output before mod N | Parity after mod N | |--------------|-----------------------------|--------------------------------------------------------------------------------------| | 0 - 0.25N | 0 - N | Even, because we multiplied by a power of 2, and the modulo N did nothing | | 0.25N - 0.5N | N - 2N | Odd, because we multiplied by a power of 2, and subtract N once (and N is odd) | | 0.5N - 0.75N | 2N - 3N | Even, because we multiplied by a power of 2, and subtract N twice (and N is odd) | | 0.75 - N | 3 - 4N | Odd, because we multiplied by a power of 2, and subtract N tree times (and N is odd) | **General Case** Now we can see, that in the general case when multiplying by $2^k$, we split [0,N] into $2^k$ equal size intervals labeled $I_{0}$ ... $I_{2^k-1}$ * if the result is even, the input was from an evenly labeled interval $I_{2i}$ * if the result is odd, the input was from an oddly labeled interval $I_{2i+1}$ In a normal LSB oracle for RSA, when N stays constant, it is easy solve the challenge by dicotomy. At each step, we pick a larger power of two, and determine if the value is in $I_{2i}$ or $I_{2i+1}$ ## RSA LSB oracle where N changes Now, when N changes at every step, if you know that the input is in the interval [l, u], we can always pick a value $2^k$ such that two successive intervals $I_{2i}$ and $I_{2i+1}$ (build like explained before) fully cover [l,u] and have their joining point (h) in [l,u]. * If the result is even, we conclude that the input was in $I_{2i}$ and we set u = h * If the result is odd, we conclude that the input was in $I_{2i+1}$ and we set l = h We just start with [l, u ] = [0, 2**2048], and apply the rule until we find the solution. ## Source Code The source code we used to solve the challenge is the following: ```python from socket import create_connection import re e = 65537 def get_power(n, l_know, u_know): l = 0 u = n for i in range(2048): h = (l + u) // 2 if l_know <= h <= u_know: return (i, h) if h < l_know: l = h else: u = h def hexencode(i): res = hex(i)[2:] if len(res) % 2: return "0" + res return res def step(l, u): conn = create_connection(("18.217.237.201", 3197)) def recv(l): return conn.recv(l) def send(data): conn.send(data) def recv_until(end=b"\n"): res = recv(1) while res[-len(end):] != end: c = recv(1) if not c: return res res += c return res def readline(): return recv_until(b"\n") # Receive flagenc and modulus readline() flagencline = readline() flagenc = int(re.search(b": ([0-9a-h]+)", flagencline).group(1), 16) modulusline = readline() n = int(re.search(b": ([0-9a-h]+)", modulusline).group(1)) # Choose decrypt recv_until(b'choice: ') send(b"2\n") recv_until(b': ') # Pick the power of 2 to multiply flagenc by i, h = get_power(n, l, u) print (i) # Send the value v = flagenc * pow(2**(i+1), e, n) send(hexencode(v).encode() + b"\n") resultline = readline() response = int(re.search(b": ([0-1]+)", resultline).group(1)) # Adjust upper and lower bound if response == 0: u = h else: l = h return l, u if __name__ == '__main__': l = 0 u = 2**2048 for a in range(2048): print (a, len(str(l)), len(str(u)), l, u) l, u = step( l, u) ```