Author. EggRoll
[TOC]
# PRaNsomG
## Overview
The ransomware first generates $32$ random $576$-bits numbers and then produces the key and initialization vector for encryptor. On the other hand, it uses a 2-bytes OTP to encrypt the $32$ random numbers and appends them to the corresponding encrpyted files.
## Exploit
Clearly, our goal is to recover the key and iv from the 32 random numbers. The straightforward approach is to crack the PRNG, which is MT19937 in this case. However, we have only $32 \times 576 < 19937$ bits.
Let's study the PRNG deeper, here is the way that MT19937 updates the internal states:
```python
for i in range(0, 623+1):
y = (self.MT[i] & 0x80000000) + (self.MT[(i+1) % 624] & 0x7fffffff)
self.MT[i] = self.MT[(i + 397) % 624] ^ (y >> 1)
if (y % 2) != 0:
self.MT[i] = self.MT[i] ^ (2567483615)
```
and the output is generated as follow:
```python
if self.index == 0:
self.generate_numbers()
y = self.MT[self.index]
y = y ^ (y >> 11)
y = y ^ ((y << 7) & (0x9d2c5680))
y = y ^ ((y << 15) & (0xefc60000))
y = y ^ (y >> 18)
self.index = (self.index + 1) % 624
return y
```
From the output, it's not hard to convery it back to the internal state and most importantly, any new internal state depends on only $3$ previous states. Therefore, we could still figure out the key and iv using less than $19937$-bits output.
```python
from Crypto.Util.number import *
from Crypto.Cipher import AES
import random
import os
def USR(x, shift):
res = x
for i in range(32):
res = x ^ res >> shift
return res
def USL(x, shift, mask):
res = x
for i in range(32):
res = x ^ (res << shift & mask)
return res
def to_MT(v):
v = USR(v, 18)
v = USL(v, 15, 0xefc60000)
v = USL(v, 7, 0x9d2c5680)
v = USR(v, 11)
return v
def to_random(y):
y = y ^ (y >> 11)
y = y ^ ((y << 7) & (0x9d2c5680))
y = y ^ ((y << 15) & (0xefc60000))
y = y ^ (y >> 18)
return y
def xor(a, b):
return b''.join([bytes([a[i] ^ b[i % len(b)]]) for i in range(len(a))])
def recover(a, b, c, otp):
a = bytes_to_long(xor(a, otp))
b = bytes_to_long(xor(b, otp))
c = bytes_to_long(xor(c, otp))
res = []
MT_i, MT_iadd1, MT_iadd397 = to_MT(a), to_MT(b), to_MT(c)
y = (MT_i & 0x80000000) + (MT_iadd1 & 0x7fffffff)
MT_iadd624 = MT_iadd397 ^ (y >> 1)
if (y % 2) != 0:
MT_iadd624 = MT_iadd624 ^ 0x9908b0df
return long_to_bytes(to_random(MT_iadd624))
def pad(s, L):
return (L - len(s)) * b"\x00" + s
DEBUG = False
if not DEBUG:
folder = "./enc_files/"
files = os.listdir(folder)
sorted_files = []
for i in range(32):
for fname in files:
if fname.startswith(str(i) + "_"):
sorted_files += [fname]
files.remove(fname)
break
enc, outputs = [], []
for fname in sorted_files:
with open(folder + fname, "rb") as f:
tmp = f.read()
enc += [tmp[:-72]]
_ = tmp[-72:]
for i in range(17, -1, -1):
outputs += [_[4 * i: 4 * i + 4]]
else:
# random.seed(12345678)
outputs = []
for _ in range(576):
outputs += [long_to_bytes(random.getrandbits(32))]
_key = long_to_bytes(random.getrandbits(1680))[:16]
_iv = long_to_bytes(random.getrandbits(1680))[:16]
"""
0, 1, ..., 575 (576)
576, 577, ..., 627, 628 (1680 / 32 = 52.5)
629, 630, ..., 680, 681
"""
for n in range(1 if DEBUG else 256**2):
otp = pad(long_to_bytes(n), 2)
key = recover(outputs[4], outputs[5], outputs[401], otp)[:2] + \
pad(recover(outputs[3], outputs[4], outputs[400], otp), 4) + \
pad(recover(outputs[2], outputs[3], outputs[399], otp), 4) + \
pad(recover(outputs[1], outputs[2], outputs[398], otp), 4) + \
pad(recover(outputs[0], outputs[1], outputs[397], otp), 4)[:2]
iv = recover(outputs[57], outputs[58], outputs[454], otp)[:2] + \
pad(recover(outputs[56], outputs[57], outputs[453], otp), 4) + \
pad(recover(outputs[55], outputs[56], outputs[452], otp), 4) + \
pad(recover(outputs[54], outputs[55], outputs[451], otp), 4) + \
pad(recover(outputs[53], outputs[54], outputs[450], otp), 4)[:2]
if not DEBUG:
cipher = AES.new(key, AES.MODE_CBC, iv)
for i, e in enumerate(enc):
try:
pt = cipher.decrypt(enc[i])
print(pt.decode())
print(i)
except:
pass
else:
assert key == _key and iv == _iv
```
## FLAG
:::success
HTB{v1t4l1um_h3r3_w3_c0m3___n0_r4ns0mw4r3_c4n_st0p_us}
:::
# Interception
## Overview
The server generates standard RSA parameters $(p, q, e, d, N)$ and the encryption key is defined to be $$ pow(ct, a^{N}, N) $$ where $ct, a$ are constants. Also, we are given three options:
1. Send the message $m$, the server returns $pow(ANS, e, N)$ if $pow(m, d, N)$ is decodable. Otherwise, it gives us $pow(random, e, N)$
2. Provide the key to the server and get the decrypted results back
3. Retrieve high bits of one prime factor of $N$ if the input is exactly the hash of $N$
## Exploit
Obviously, the steps to capture the flag are listed below.
1. Recover the value of RSA modulus $N$ using the oracle
2. Factorize $N$ with the high bits of one prime factor
3. Compute the key and send it to the server
### Step 1. Recover Modulus
Notice that by definition, $m^{e} - pow(m, e, N)$ is a multiple of $N$. If we have several such multiples, then $N$ is likely to be the GCD of them. From this observation, we should make $pow(m, d, N)$ be decodable. In my implementation, I choose $m=2^e$
### Step 2. Factorize N
We have $643$ bits of $q$, which is enough for us to factorize the modulus. See https://github.com/mimoo/RSA-and-LLL-attacks for more reference.
### Step 3. Key Computation
As we have factorization of $N$, we tends to find $$ pow(ct, a^{N}, p) \quad \text{and} \quad pow(ct, a^{N}, q) $$ By Fermat's little theorem, $$pow(ct, a^{N}, p) = pow(ct, a^{N\pmod{p-1}}, p)$$ Finally, using CRT to get the key.
## FLAG
:::success
HTB{th3y_d3f1n1t3ly_d0nt_h4v3_a_sUp3r_c0mPut3r!}
:::
# Vitrium Stash
## Overview
We are asked to forgery a DSA signature of message in a special form. Precisely, we have to provide $m ,r, s$ such that $$ r = (g^{ms^{-1}}y^{rs^{-1}} \pmod{p})\pmod{q} $$ where $y=g^{x}\pmod{p}$ and $q$ is the order of $\mathbb{F}_{p}(g)$
## Exploit
According to the function for verification, it's not hard to come up with a valid signatures $(0, y\pmod{q}, y\pmod{q})$. So it remains to find a message in the special form whose correpsonding integral value is a multiple of $q$. This could be done by LLL. A similar problem is https://github.com/Social-Engineering-Experts/SEETF-2023-Public/tree/main/challs/crypto/onelinecrypto
```python
from Crypto.Util.number import *
import json
q = 26189572440233739420990528170531051459310363621928135990243626537967
# b'{"admin": True, "user": "xxxxxxx"}'
for k in range(30, 70):
c = bytes_to_long(b'{"admin": true, "user": "' + b"\x00" * k + b'"}')
M = Matrix(ZZ, k+2, k+2)
M[:k+1, :k+1] = Matrix.identity(k+1)
for i in range(k):
M[i, -1] = 256 ** (k+1 - i)
M[-2, i] = -80
M[-2, -2] = 1
M[-2, -1] = c
M[-1, -1] = -q
M = M.LLL()
for r in M:
if r[-1] == 0 and r[-2] == 1:
try:
print("Found")
print(k)
user = "".join([chr(r[i] + 80) for i in range(k)])
# check
message = b'{"admin": true, "user": "' + user.encode() + b'"}'
print(bytes_to_long(message) % q)
print(message.decode())
except:
pass
```
## FLAG
:::success
HTB{th3_l0c4t10n_0f_th3_v1t4l1um_1s_4t___37.187561,-115.885322}
:::