MikroTik RouterOS 授权签名验证分析

2024-06-18 22:04

本文主要是介绍MikroTik RouterOS 授权签名验证分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

MikroTik 软路由

百科icon-default.png?t=N7T8https://baike.baidu.com/item/mikrotik/9776775官网icon-default.png?t=N7T8https://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 = 0for i in range(0, len(data)):if left == 0:encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i] & 0x3F])left = 2else:if left == 6:encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i - 1] >> 2])encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[data[i] & 0x3F])left = 2else:index1 = data[i - 1] >> (8 - left)index2 = data[i] << (left)encoded += chr(MIKRO_BASE64_CHARACTER_TABLE[(index1 | index2) & 0x3F])left += 2if 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 = 0for i in range(0, len(data)):if left == 0:left = 6else: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 -= 2return 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 encodedLicensePayloaddef 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

16字节解密后前6个字节为授权软件ID,第7个字节为授权软件版本号,第8个字节为授权等级

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 = 0for i in reversed(range(len(software_id))):ret *= len(SOFTWARE_ID_CHARACTER_TABLE)ret += SOFTWARE_ID_CHARACTER_TABLE.index(ord(software_id[i]))return retdef 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 //= 0x23if i == 3:ret += '-'return ret

16字节随机数和32字节校验码通过EC-KCDSA算法签名生成,采用toyecc库,其中sha256采用自定义的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_KINITIAL_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.pubkeywhile True:nonce_secret = secure_rand_int_between(1, curve.n - 1)nonce_point = nonce_secret * curve.Gnonce = int(nonce_point.x) % curve.nnonce_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] &= 0xF8data_hash[31] &= 0x7Fdata_hash[31] |= 0x40data_hash = Tools.bytestoint_le(data_hash)signature = pow(private_key.scalar, -1, curve.n) * (nonce_secret - data_hash)signature %= curve.nif 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 + xx = 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] &= 0xF8data_hash[31] &= 0x7Fdata_hash[31] |= 0x40data_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 Truereturn 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'*8sig = 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

替换公钥

用生成的秘钥对的公钥替换原先官方的公钥

squashfs-root/nova/bin/loader public key patched 8E1067E4305FCDC0CFBF95C10F96E5DF...

squashfs-root/nova/bin/keyman public key patched 8E1067E4305FCDC0CFBF95C10F96E5DF...

squashfs-root/nova/bin/mode public key patched 8E1067E4305FCDC0CFBF95C10F96E5DF...

initramfs/init public key patched 8E1067E4305FCDC0CFBF95C10F96E5DF...

initramfs/setup public key patched 8E1067E4305FCDC0CFBF95C10F96E5DF...

但是routeros的npk文件也是验证签名的,因此直接替换公钥,系统启动的时候验证npk的文件的签名是不通过的,所以还得对npk文件重新签名

NPK文件签名分析

npk文件的签名由132个字节组成,其中20字节sha1,48字节EC-KCDSA签名,64字节EDDSA签名

同理生成自定义秘钥对替换原来的官方公钥,用自定义的私钥对npk重新签名即可。

squashfs-root/nova/bin/installer public key patched C293CED638A2A33C681FC8DE98EE26C5...

squashfs-root/nova/bin/sys2 public key patched C293CED638A2A33C681FC8DE98EE26C5...

initramfs/init public key patched C293CED638A2A33C681FC8DE98EE26C5...

initramfs/setup public key patched C293CED638A2A33C681FC8DE98EE26C5...

 源代码

MikroTikPatchicon-default.png?t=N7T8https://github.com/elseif/MikroTikPatch

       npk.py,创建、修改、签名、校验npk文件

       patch.py,替换公钥,重新签名npk文件

        netinstall.py,修改netinstall.exe,通过修改过的netinstall可以网络安装ISO里重新签名的npk文件

        upgrade.py,通过在routeros添加静态域名解析,实现upgrade的时候安装ISO里重新签名的npk文件。

这篇关于MikroTik RouterOS 授权签名验证分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1073180

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

Java JDK Validation 注解解析与使用方法验证

《JavaJDKValidation注解解析与使用方法验证》JakartaValidation提供了一种声明式、标准化的方式来验证Java对象,与框架无关,可以方便地集成到各种Java应用中,... 目录核心概念1. 主要注解基本约束注解其他常用注解2. 核心接口使用方法1. 基本使用添加依赖 (Maven

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe

python库pydantic数据验证和设置管理库的用途

《python库pydantic数据验证和设置管理库的用途》pydantic是一个用于数据验证和设置管理的Python库,它主要利用Python类型注解来定义数据模型的结构和验证规则,本文给大家介绍p... 目录主要特点和用途:Field数值验证参数总结pydantic 是一个让你能够 confidentl

Java中最全最基础的IO流概述和简介案例分析

《Java中最全最基础的IO流概述和简介案例分析》JavaIO流用于程序与外部设备的数据交互,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),处理... 目录IO流简介IO是什么应用场景IO流的分类流的超类类型字节文件流应用简介核心API文件输出流应用文

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按