MikroTik called RouterOS 7.24.2 an important security update, but the SSH entry in the release announcement just says “refactor SSH internal processes and improved system stability.” I wanted to know what that meant, so I compared the ssh binaries from the 7.24.1 and 7.24.2 arm64 packages.
The RSA key comparison in 7.24.1 checked only the modulus. In 7.24.2, it checks the exponent too, and the signature verifier uses the user’s stored key. There are also new checks on signature formatting and on usernames passed to the internal login program.
I worked from disassembly, and I haven’t tested an exploit on a running router. The pseudocode below is my reconstruction of the compiled code.
The binaries #
I compared rootfs/bndl/security/nova/bin/ssh from routeros-7.24.1-arm64.npk and routeros-7.24.2-arm64.npk. Both files are stripped, dynamically linked 32-bit ARM ELF executables. They’re 32-bit even though the packages are labeled arm64. In both versions, sshd is a relative symlink to ssh, so this file contains the server code as well as the client.
The file size barely changed: 220,972 bytes became 220,976 bytes. The executable .text section grew by 308 bytes, though. An alignment gap between load segments shrank, hiding most of the increase in the overall file size.
If you’re checking the same files, these are their SHA-256 hashes:
- 7.24.1:
3a95f59a2ca83b3fd9ae797ebcbd16bf882269156a525b6eeaf66d370e36cd8f - 7.24.2:
d9a79c0156b5b0cfe6fa007872a181661d6fe5611a97cec2fa5368200f0c474c
Two of the seven shared libraries SSH links to changed: libumsg.so and libucrypto.so. The other five, libubox.so, libufiber.so, libz.so, libuc++.so, and libc.so, are byte-identical. The separate nova/bin/login executable changed too; I didn’t analyze its changes for this post.
All addresses below are ELF virtual addresses.
| Area | RouterOS 7.24.1 | RouterOS 7.24.2 |
|---|---|---|
| RSA identity comparison | Key type and modulus n |
Key type, modulus n, and exponent e |
| Public-key signature check | Uses the peer-key object | Uses the matching stored-key object |
| RSA signature validation | Prefix, padding scan, and separator checks | Adds s < n and minimum eight-byte 0xff padding |
| Digest parsing | Parses the expected structure | Also checks sequence end and digest length |
| Login arguments | No matching validator call at the inspected SSH site | Rejects empty names, edge spaces, leading hyphens, and control bytes |
My routeros-extract tool can extract these firmwares if you want to look through them yourself. It’s a native Go CLI that extracts NPK files and the payloads inside them. Installation instructions are in the repository. Use a new output directory when extracting the packages:
routeros-extract extract \
routeros-7.24.1-arm64.npk \
routeros-7.24.2-arm64.npk \
-o extracted
You’ll find the newer SSH binary at extracted/routeros-7.24.2-arm64/rootfs/bndl/security/nova/bin/ssh, and the older one under its version’s directory. The tool extracts files without executing them. You’ll need a disassembler to follow the code changes below.
Matching the RSA key #
In 7.24.1, the RSA key comparison checks the key type and a big integer at object offset +4. Following that field into the verification code identifies it as the modulus, n. The 7.24.2 comparison adds the big integer at +0xc, which is the public exponent, e.
The comparison starts at ssh:0x1b218 in 7.24.1 and ssh:0x1b298 in 7.24.2. Written as pseudocode:
// 7.24.1
same_type && peer.n == stored.n
// 7.24.2
same_type && peer.n == stored.n && peer.e == stored.e
An RSA public key is the pair (n, e). The old comparison would accept the same modulus with a different exponent; the new one rejects it.
Verifying with the stored key #
The code that looks up the user’s key changed too. In 7.24.1, SSH parses the key supplied by the client and looks for a matching stored key. The lookup returns yes or no. If it finds a match, SSH verifies the signature using the client’s key.
In 7.24.2, the lookup returns the stored key it found, and SSH uses that key to verify the signature. It keeps the client’s key separately:
// 7.24.1
peer_key = parse(request.public_key)
require matching_stored_key_exists(user, peer_key)
require verify(peer_key, signed_data, signature)
// 7.24.2
peer_key = parse(request.public_key)
stored_key = find_matching_stored_key(user, peer_key)
require stored_key exists
require verify(stored_key, signed_data, signature)
The key lookup starts at ssh:0x29484 in 7.24.1 and ssh:0x295a4 in 7.24.2. The public-key authentication handler moves from 0x295f0 to 0x29724. You can follow the change through the stack locals: the old verifier gets the client’s key from +0xc; the new verifier gets the stored key from +0x10, with the client’s key at +0x14.
SSH has to check that the key is authorized and that the signature is valid. RFC 4252 section 7 requires both. With this change, the key used for verification comes directly from the user’s stored keys.
RSA signatures and padding #
The signature verification region grows from 2,064 to 2,132 bytes and moves from 0x25608 to 0x256e4. There are two new checks in its RSA code.
The first compares the signature integer s with the modulus n. It requires s < n before modular exponentiation, as specified in RFC 8017 section 5.2.2.
The padding parser also counts the 0xff bytes and rejects seven or fewer. That new comparison is at 0x25ea0–0x25ea4. The old code already checked the 00 01 prefix, scanned the padding, and required a zero separator. It just didn’t enforce the minimum padding length. RFC 8017 section 9.2 specifies 00 || 01 || PS || 00 || T, with at least eight bytes in PS.
Parsing the digest in libucrypto.so
#
The RSA DigestInfo parser in libucrypto.so takes a HashID argument in the new version, replacing an unsigned integer. The function grew from 348 bytes at 0x1bb68 to 480 bytes at 0x1bb94.
Both versions check the outer sequence, algorithm identifier, expected hash identifier, and octet-string tag. After reading the digest, the new code also checks:
- The parser must report the end of the parsed sequence. Otherwise it logs
trailing bytes after digest. - The extracted digest length must match the expected length for the selected hash. Otherwise it logs
bad digest length.
The size table contains 16-, 20-, 32-, 48-, and 64-byte entries. The end check catches trailing content inside the parsed sequence. I haven’t checked all the DER edge cases, including bytes outside that sequence.
Checking usernames before running login #
7.24.2 imports validLoginParamInput(string_view) from libumsg.so. The 108-byte validator rejects:
- an empty username;
- a leading ASCII space or hyphen;
- a trailing ASCII space; and
- any byte from
0x00through0x1f, or the delete byte0x7f.
Interior spaces, hyphens after the first byte, and high-bit bytes are still allowed. The function doesn’t require ASCII-only names or validate UTF-8.
SSH calls the validator at 0x2c010. If it fails, SSH reports invalid user input and stops setting up the login. The string being checked is the username later passed to /nova/bin/login.
This happens after the child process has been forked and its file descriptors prepared, before SSH builds the login arguments. The later execl calls are at 0x2c478 and 0x2c4fc.
Blocking a leading hyphen looks like a check against treating a username as a command-line option. It happens after SSH authentication, when the accepted username is passed to another program.
Connection state #
I found two more changes around connection state. Before handling server-side connection messages starting at 0x50, 7.24.2 checks for a nonzero value at connection offset +0x108. Elsewhere, that field is used as a policy bitmask: the code tests permission bits and passes the numeric value to the login program. This looks like an authorization check before session processing.
A transport-state routine adds checks too. State 2 keeps its setup path, and state 6 returns. Values up to 4, except 2, now cause a disconnect with reason value 2. The remaining path keeps the rekey behavior. I haven’t mapped these values to their original state names.
Nick Pratley published his analysis of RSA verification and login-argument handling on September 4. He compared the x86 builds of RouterOS 7.23.3 and 7.23.4 and reported exploitation tests from his lab.
These changes affect the code that decides who gets an SSH session. If you’re still running 7.24.1, follow MikroTik’s upgrade recommendation.