MikroTik RouterOS 授权签名验证分析
MikroTik 软路由
[百科

官方在线百科全书[米克罗特官网]

https://mikrotik.com/](https://mikrotik.com/ "官网")

授权文件分析
-----BEGIN MIKROTIK SOFTWARE KEY------------
mr3jH5qhn9irtF53ZICFTN7Tk7wIx7ZkxdAxJ19ydASY
ShhFteHMntBTyaS8wuNdIJJPidJxbuNPLTvCsv7zLA==
-----END MIKROTIK SOFTWARE KEY--------------
采用自定义的Base64编码,自定义Base64的转换如下:
MIKRO_BASE64_CHARACTER_TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
def mikro_base64_encode(data:bytes, pad = False)->str:
encoded = ''
left = 0
for i in range(0, len(data)):
if left == 0:
encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i] & 0x3F])
left = 2
else:
if left == 6:
encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i - 1] >> 2])
encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i] & 0x3F])
left = 2
else:
index1 = data[i - 1] >> (8 - left)
index2 = data[i] << (left)
encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[(index1 | index2) & 0x3F])
left += 2
if left != 0:
encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[len(data) - 1] >> (8 - left)])
if pad:
for i in range(0, (4 - len(encoded) % 4) % 4):
encoded += '='
return encoded
def mikro_base64_decode(data:str)->bytes:
ret = b""
data = data.replace("=", "").encode()
left = 0
for i in range(0, len(data)):
if left == 0:
left = 6
else:
value1 = MIKRO_BASE64_CHARACTER_TABLE.index(data[i - 1]) >> (6 - left)
value2 = MIKRO_BASE64_CHARACTER_TABLE.index(data[i]) & (2 ** (8 - left) - 1)
value = value1 | (value2 << left)
ret += bytes([value])
left -= 2
return ret
经Base64解码得到恒定长度的64位数据块中包含了以下三个部分:包含16位授权信息块、16位随机数字段和32位签名字段;其中还包括指定的授权字段数据、随机的数值字段和用于验证的签名部分。
16字节授权内容通过如下方式加密解密
def mikro_encode(s:bytes)->bytes:
s = list(struct.unpack('>' + 'I' * (len(s) // 4), s))
for i in reversed(range(16)):
s[(i+0) % 4] = to32bits(rotl(s[(i+3) % 4], MIKRO_SHA256_K[i*4+3] & 0x0F) ^ (s[(i+0) % 4] - s[(i+3) % 4]))
s[(i+3) % 4] = to32bits(s[(i+3) % 4] + s[(i+1) % 4] + MIKRO_SHA256_K[i*4+3])
s[(i+1) % 4] = to32bits(rotl(s[(i+2) % 4], MIKRO_SHA256_K[i*4+2] & 0x0F) ^ (s[(i+1) % 4] - s[(i+2) % 4]))
s[(i+0) % 4] = to32bits(s[(i+0) % 4] + s[(i+2) % 4] + MIKRO_SHA256_K[i*4+2])
s[(i+2) % 4] = to32bits(rotl(s[(i+1) % 4], MIKRO_SHA256_K[i*4+1] & 0x0F) ^ (s[(i+2) % 4] - s[(i+1) % 4]))
s[(i+1) % 4] = to32bits(s[(i+1) % 4] + s[(i+3) % 4] + MIKRO_SHA256_K[i*4+1])
s[(i+3) % 4] = to32bits(rotl(s[(i+0) % 4], MIKRO_SHA256_K[i*4+0] & 0x0F) ^ (s[(i+3) % 4] - s[(i+0) % 4]))
s[(i+2) % 4] = to32bits(s[(i+2) % 4] + s[(i+0) % 4] + MIKRO_SHA256_K[i*4+0])
encodedLicensePayload = b''
for x in s:
encodedLicensePayload += x.to_bytes(4, 'big')
return encodedLicensePayload
def mikro_decode(s:bytes)->bytes:
s = list(struct.unpack('>'+'I'*(len(s) // 4), s))
for i in range(16):
s[(i+2) % 4] = to32bits(s[(i+2) % 4] - s[(i+0) % 4] - MIKRO_SHA256_K[i*4+0])
s[(i+3) % 4] = to32bits((rotl(s[(i+0) % 4], MIKRO_SHA256_K[i*4+0] & 0x0F) ^ s[(i+3) % 4]) + s[(i+0) % 4])
s[(i+1) % 4] = to32bits(s[(i+1) % 4] - s[(i+3) % 4] - MIKRO_SHA256_K[i*4+1])
s[(i+2) % 4] = to32bits((rotl(s[(i+1) % 4], MIKRO_SHA256_K[i*4+1] & 0x0F) ^ s[(i+2) % 4]) + s[(i+1) % 4])
s[(i+0) % 4] = to32bits(s[(i+0) % 4] - s[(i+2) % 4] - MIKRO_SHA256_K[i*4+2])
s[(i+1) % 4] = to32bits((rotl(s[(i+2) % 4], MIKRO_SHA256_K[i*4+2] & 0x0F) ^ s[(i+1) % 4]) + s[(i+2) % 4])
s[(i+3) % 4] = to32bits(s[(i+3) % 4] - s[(i+1) % 4] - MIKRO_SHA256_K[i*4+3])
s[(i+0) % 4] = to32bits((rotl(s[(i+3) % 4], MIKRO_SHA256_K[i*4+3] & 0x0F) ^ s[(i+0) % 4]) + s[(i+3) % 4])
ret = b''
for x in s:
ret += x.to_bytes(4, 'big')
return ret
经过解密过程的前六个字节是授权软件ID
6字节转换为软件ID的方式如下:
SOFTWARE_ID_CHARACTER_TABLE = b'TN0BYX18S5HZ4IA67DGF3LPCJQRUK9MW2VE'
def mikro_softwareid_decode(software_id:str)->int:
assert(isinstance(software_id, str))
software_id = software_id.replace('-', '')
ret = 0
for i in reversed(range(len(software_id))):
ret *= len(SOFTWARE_ID_CHARACTER_TABLE)
ret += SOFTWARE_ID_CHARACTER_TABLE.index(ord(software_id[i]))
return ret
def mikro_softwareid_encode(id:int)->str:
assert(isinstance(id, int))
ret = ''
for i in range(8):
ret += chr(SOFTWARE_ID_CHARACTER_TABLE[id % 0x23])
id //= 0x23
if i == 3:
ret += '-'
return ret
16字节随机数与32字节校验码经EC-KCDSA算法进行签名生成,并采用了toyecc库支持,其中哈希函数基于自定义参数K及State表结构
自定义的sha256实现算法
MIKRO_SHA256_K = (
0x0548D563, 0x98308EAB, 0x37AF7CCC, 0xDFBC4E3C,
0xF125AAC9, 0xEC98ACB8, 0x8B540795, 0xD3E0EF0E,
0x4904D6E5, 0x0DA84981, 0x9A1F8452, 0x00EB7EAA,
0x96F8E3B3, 0xA6CDB655, 0xE7410F9E, 0x8EECB03D,
0x9C6A7C25, 0xD77B072F, 0x6E8F650A, 0x124E3640,
0x7E53785A, 0xE0150772, 0xC61EF4E0, 0xBC57E5E0,
0xC0F9A285, 0xDB342856, 0x190834C7, 0xFBEB7D8E,
0x251BED34, 0x0E9F2AAD, 0x256AB901, 0x0A5B7890,
0x9F124F09, 0xD84A9151, 0x427AF67A, 0x8059C9AA,
0x13EAB029, 0x3153CDF1, 0x262D405D, 0xA2105D87,
0x9C745F15, 0xD1613847, 0x294CE135, 0x20FB0F3C,
0x8424D8ED, 0x8F4201B6, 0x12CA1EA7, 0x2054B091,
0x463D8288, 0xC83253C3, 0x33EA314A, 0x9696DC92,
0xD041CE9A, 0xE5477160, 0xC7656BE8, 0x5179FE33,
0x1F4726F1, 0x5F393AF0, 0x26E2D004, 0x6D020245,
0x85FDF6D7, 0xB0237C56, 0xFF5FBD94, 0xA8B3F534
)
class MikroSHA256(SHA256):
K = MIKRO_SHA256_K
INITIAL_STATE = SHA256.State(
0x5B653932, 0x7B145F8F, 0x71FFB291, 0x38EF925F,
0x03E1AAF9, 0x4A2057CC, 0x4CAF4DD9, 0x643CC9EA
)
def mikro_sha256(data:bytes)->bytes:
return MikroSHA256(data).digest()
EC-KCDSA签名验证算法
from toyecc import AffineCurvePoint, getcurvebyname, FieldElement,ECPrivateKey,ECPublicKey,Tools
from toyecc.Random import secure_rand_int_between
def mikro_kcdsa_sign(data:bytes,private_key:bytes)->bytes:
assert(isinstance(data, bytes))
assert(isinstance(private_key, bytes))
curve = getcurvebyname('Curve25519')
private_key:ECPrivateKey = ECPrivateKey(Tools.bytestoint_le(private_key), curve)
public_key:ECPublicKey = private_key.pubkey
while True:
nonce_secret = secure_rand_int_between(1, curve.n - 1)
nonce_point = nonce_secret * curve.G
nonce = int(nonce_point.x) % curve.n
nonce_hash = mikro_sha256(Tools.inttobytes_le(nonce,32))
data_hash = bytearray(mikro_sha256(data))
for i in range(16):
data_hash[8+i] ^= nonce_hash[i]
data_hash[0] &= 0xF8
data_hash[31] &= 0x7F
data_hash[31] |= 0x40
data_hash = Tools.bytestoint_le(data_hash)
signature = pow(private_key.scalar, -1, curve.n) * (nonce_secret - data_hash)
signature %= curve.n
if int((public_key.point * signature + curve.G * data_hash).x) == nonce:
return bytes(nonce_hash[:16]+Tools.inttobytes_le(signature,32))
def mikro_kcdsa_verify(data:bytes, signature:bytes, public_key:bytes)->bool:
assert(isinstance(data, bytes))
assert(isinstance(signature, bytes))
assert(isinstance(public_key, bytes))
curve = getcurvebyname('Curve25519')
#y^2 = x^3 + ax^2 + x
x = Tools.bytestoint_le(public_key)
X_field = FieldElement(x, curve.p)
YY = ((X_field**3) + (curve.a * X_field**2) + X_field).sqrt()
public_keys = []
for y in YY:
public_keys += [AffineCurvePoint(x, int(y), curve)]
data_hash = bytearray(mikro_sha256(data))
nonce_hash = signature[:16]
signature = signature[16:]
for i in range(16):
data_hash[8+i] ^= nonce_hash[i]
data_hash[0] &= 0xF8
data_hash[31] &= 0x7F
data_hash[31] |= 0x40
data_hash = Tools.bytestoint_le(data_hash)
signature = Tools.bytestoint_le(signature)
for public_key in public_keys:
nonce = int((public_key * signature + curve.G * data_hash).x)
if mikro_sha256(Tools.inttobytes_le(nonce,32))[:len(nonce_hash)] == nonce_hash:
return True
return False
授权文件解析
通过分析/nova/bin/keyman可以获取授权文件验证签名的公钥为
MIKRO_LICENSE_PUBLIC_KEY = bytes.fromhex('8E1067E4305FCDC0CFBF95C10F96E5DFE8C49AEF486BD1A4E2E96C27F01E3E32')
因此可以解析验证授权文件是否有效
def prase_license(lic:str,public_key:bytes):
assert(isinstance(public_key, bytes))
slic = lic.replace(MIKRO_LICENSE_HEADER, '').replace(MIKRO_LICENSE_FOOTER, '').replace('\n', '').replace(' ','')
lic:bytes = mikro_base64_decode(slic)
licVal = mikro_decode(lic[:16])
software_id = int.from_bytes(licVal[:6], 'little')
print(f"Software ID: {mikro_softwareid_encode(software_id)}({hex(software_id)})")
print(f"RouterOS Version: {licVal[6]}")
print(f"License Level: {licVal[7]}")
nonce_hash = lic[16:32]
print(f"Nonce Hash: {nonce_hash.hex()}")
signature = lic[32:64]
print(f"Signature: {signature.hex()}")
print(f'License valid:{mikro_kcdsa_verify(licVal, nonce_hash+signature,public_key)}')
解析后的输出
Software ID: TI09-7WK3(0x137f8e8673d)
RouterOS Version: 6
License Level: 6
Nonce Hash: b34fe40e23f19e917107c449ddcb1d20
Signature: 61521816ad7730671b4cb226f1b0db7448923c6297c49bdb3ccbf40aecbbcf0b
License valid:True
授权文件生成
生成秘钥对,通过私钥对授权内容进行签名即可。
MIKRO_LICENSE_HEADER = '-----BEGIN MIKROTIK SOFTWARE KEY------------'
MIKRO_LICENSE_FOOTER = '-----END MIKROTIK SOFTWARE KEY--------------'
def generate_license(software_id,private_key:bytes,version:int=7, level:int=6):
assert(isinstance(private_key, bytes))
if isinstance(software_id, str):
software_id = mikro_softwareid_decode(software_id)
lic = software_id.to_bytes(6, 'little')
lic += version.to_bytes(1, 'little')
lic += level.to_bytes(1, 'little')
lic += b'\0'*8
sig = mikro_kcdsa_sign(lic, private_key)
lic = mikro_base64_encode(mikro_encode(lic)+sig,True)
return MIKRO_LICENSE_HEADER + '\n' + lic[:len(lic)//2] + '\n' + lic[len(lic)//2:] + '\n' + MIKRO_LICENSE_FOOTER
替换公钥
用生成的秘钥对的公钥替换原先官方的公钥
The squashfs-root directory under Nova bin has a patched public key for the following files: squashfs-root/nova/bin/loader, squashfs-root/nova/bin/keyman, and squashfs-root/nova/bin/mode. Each of these files contains the same patch identifier: 8E1067E4305FCDCCOFFCFBF95C10F96$E5DF. Additionally, both initramfs:init and initramfs/setup directories also feature this specific patched public key.
然而,在routeros系统中,npk文件同样具备校验签名的功能.若直接替换公钥,在系统启动过程中,对npk文件进行签名验证时会失败,因此必须为该npk文件重新生成其签名.
NPK文件签名分析
该npk文件包含一个总计132个字节的签名字段,其中一部分为20个字节用于SHA1校验码,另一部分为48个字节用于EC-KCDSA子类型签名,剩余64个字节用于EDDSA子类型签名
通过自定义秘钥生成自定义秘钥对替代原有的官方公钥,并使用自定义私钥对npk进行重新签名即可完成
The squashfs-root/nova/bin/installer has been updated with the new public keys from the specified hash value.
Similarly, squashfs-root/nova/bin/sys2 has also undergone an update using the same cryptographic parameters.
During the initramfs initialization process, the system applied the latest patch to ensure proper functionality.
Finally, during the setup phase of initramfs, all necessary configurations were completed to align with the provided cryptographic keys.
源代码
[MikroTikPatch

https://github.com/elseif/MikroTikPatch](https://github.com/elseif/MikroTikPatch "MikroTikPatch")
https://github.com/elseif/MikroTikPatch](https://github.com/elseif/MikroTikPatch "MikroTikPatch")
** npk.py,创建、修改、签名、校验npk文件**
** patch.py,替换公钥,重新签名npk文件**
修改 netinstall.exe 和 netinstall.py。
通过 重新 配置 netinstall 可以 在 网络上 安装并 重新 签名 的 NPK 文件。
version.upgrade ,通过配置 routeros 添加静态域名解析表 ,能够自动完成 ISO 内部对 npk 文件进行重新签名的过程
