Case Study - Openssh Private Key Recovery
Intro
Openssh keys are often used for ssh access. Under the hood, openssh uses RSA algorithm for key pair generation. RSA is an industrial standard algorithm used for encryption/decryption. In this case study, we’ll examine how to re-calculate the openssh private key once necessary bits and pieces information are obtained.
How does RSA work
Reading https://en.wikipedia.org/wiki/RSA_(cryptosystem), RSA operations can be summarised below
* Choose two distinct prime numbers, p and q
* n = pq
* Compute the Carmichael's totient function of n: λ(n) = lcm(p − 1, q − 1)
* Choose any number 1 < e < λ(n) that is coprime to λ(n)
* Compute d, the modular multiplicative inverse of e (mod λ(n))
* Public key contains n and e
* Encryption process: cipher = msg^e mode n
* Private key contains n and d
* Decryption process: msg = cipher^d mod n
So, in principle, if we have successfully obtained the n
and either of p
and q
values, we should be able to re-calculate the necessary values in the private key and recover the openssh private key.
Where to find n
n
value can be found in the public key. If we examine how a public key looks like, we may see something like the following in .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1...........................................................96tr222+Iu/ZbU= root@kali
Using a tool called openssh_key_parser
https://pypi.org/project/openssh-key-parser/, we can decode the public content for better visibility.
> python -m openssh_key ~/.ssh/id_rsa.pub
[
{
"header": {
"key_type": "ssh-rsa"
},
"params": {
"data": {
"e": 65537,
"n": 460042693500104190................032231896501
}
},
"footer": {},
"clear": {
"key_type": "ssh-rsa",
"comment": "root@kali"
}
}
]
From the above, we can see the n
value and the exponent (i.e e
)
Where to find q
q
can be found in the private key. To understand where to look for the q
, we need to understand how the openssh private key is structured.
According to https://coolaj86.com/articles/the-openssh-private-key-format/, openssh has its own format, which is structured below.
"openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string
32-bit length, "none" # ciphername length and string
32-bit length, "none" # kdfname length and string
32-bit length, nil # kdf (0 length, no kdf)
32-bit 0x01 # number of keys, hard-coded to 1 (no length)
32-bit length, sshpub # public key in ssh format
32-bit length, keytype
32-bit length, pub0
32-bit length, pub1
32-bit length for rnd+prv+comment+pad
64-bit dummy checksum? # a random 32-bit int, repeated
32-bit length, keytype # the private key (including public)
32-bit length, pub0 # Public Key parts
32-bit length, pub1
32-bit length, prv0 # Private Key parts
... # (number varies by type)
32-bit length, comment # comment string
padding bytes 0x010203 # pad to blocksize (see notes below)
Note the line that says 32-bit length, prv0 # Private Key parts
, this is where the private key’s q
value begins. It stars with 32-bit data to indicate the length, then followed by the actual q
value.
If we examine a private key example, we can see how it’s structured
> sed -e '1d' -e '$d' ~/.ssh/id_rsa | base64 -d | hexdump -C
00000000 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00 00 |openssh-key-v1..| # Auth Magic
00000010 00 00 04 6e 6f 6e 65 00 00 00 04 6e 6f 6e 65 00 |...none....none.| # ciphername length and string
# kdfname length and string
00000020 00 00 00 00 00 00 01 00 00 01 97 00 00 00 07 73 |...............s| # kdf
# number of keys
# sshpub
00000030 73 68 2d 72 73 61 00 00 00 03 01 00 01 00 00 01 |sh-rsa..........|
00000040 81 00 ca b7 b3 a1 47 a2 f9 b1 d6 b0 ec 29 34 68 |......G......)4h|
......
00000670 7e 7c 19 00 00 00 c1 00 de 56 df a2 36 93 da a9 |~|.......V..6...| # Private Key parts
# key length, 00 00 c1 00
# followed by q value
......
00000730 86 c9 14 a2 f8 ac e9 fd 00 00 00 09 72 6f 6f 74 |............root| # comment and padding
00000740 40 6b 61 6c 69 01 |@kali.|
00000746
After extracting the q
value from the hexdump, we can convert it to a usable format
> python -c 'print(int(0xde56...))'
Recovering the openssh private key
With n
and q
values obtained, the p
value can be calculated using p = n/q
. This may exceed the limit of some OS, so, you can use an online big number calculator to achieve the purpose, eg: https://www.calculator.net/big-number-calculator.html. Then rest of the job is just doing some math, which can be done using the script below.
Simple script for private key recovery
n = ...
q = ...
p = ...
e = 0x010001
phi = (p -1)*(q-1)
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)
def modinv(a, m):
gcd, x, y = egcd(a, m)
if gcd != 1:
return None # modular inverse does not exist
else:
return x % m
d = modinv(e,phi)
dp = modinv(e,(p-1))
dq = modinv(e,(q-1))
qi = modinv(q,p)
import pyasn1.codec.der.encoder
import pyasn1.type.univ
import base64
def pempriv(n, e, d, p, q, dP, dQ, qInv):
template = '-----BEGIN RSA PRIVATE KEY-----\n{}-----END RSA PRIVATE KEY-----\n'
seq = pyasn1.type.univ.Sequence()
for i,x in enumerate((0, n, e, d, p, q, dP, dQ, qInv)):
seq.setComponentByPosition(i, pyasn1.type.univ.Integer(x))
der = pyasn1.codec.der.encoder.encode(seq)
return template.format(base64.encodebytes(der).decode('ascii'))
key = pempriv(n,e,d,p,q,dp,dq,qi)
f = open("recovered.key","w")
f.write(key)
f.close()