Here a toy SSH client is to be described.
This is a little known fact, but SSH protocol allows to turn off encryption, setting "none" to cipher. It's no more secure than telnet, of course, but can be used for debugging purposes.
This feature is disabled by default, so both OpenSSH server and client are to be patched to turn off encryption.
I found it invaluable for learning, to dissect unencrypted SSH packets.
Many knows about such modes as ECB (that Linux penguin), CBC, CFB, OFB. Except ECB, CBC/CFB/OFB are 'feedback' modes, meaning that each block encryption process depends on previous. If there is no previous block (we encrypt a first block), it depends on IV (Initialization Vector). Also, you can't modify a random block without error propagation.
Unlike 'feedback' modes, CTR (counter mode) encrypts each block with incrementing counter plus key. This offers several advantages. Several blocks can be encrypted/decrypted in parallel, given multicore CPU. Even more, several SSH packets can be encrypted/decrypted in parallel. This is impossible for 'feedback' modes.
Also, particular/random block can be picked as a single and decrypted/modified/encrypted, without traversing all the rest. There is no error propagation. To protect from single block modifications, MAC is to be used.
Same property has GCM (Galois/Counter Mode), but it can also compute MAC during encryption/decryption, as a bonus.
It's widely believed that CTR and GCM offers the same level of security as 'feedback' modes.
In fact, there are 3 SHA algorithms.
When you see the 'SHA-256' name, this is usually means SHA2-256, not SHA3-256.
MAC is like checksum. Earlier SSH versions used CRC32 as checksum, which is insecure -- attacker can just recalculate it after modification. This could be true for any SHA hash used instead of CRC32.
But MAC is different - it's "keyed-hash message authentication code", meaning, that to calculate a hash, one must also posses a key. During SSH communication, only client and server have these keys.
In simplest term, you can think that MAC is just HASH(key || message), where '||' is concatenation and HASH is any hash algo you like. Real MACs are slightly more complex (to protect against numerous attacks), but the essence is just this.
A very important algorithm in cryptography. Used in SSH. It's advised to read my blog post about DH before.
To turn off encryption, OpenSSH-9.0 is to be patched and recompiled. Also, I added numerous printf's ("debug crumbs") which helped me a great deal in understanding SSH protocol.
I used OpenBSD version of OpenSSH (not 'portable'): openssh-9.0-openbsd.patch
(OpenBSD is not a prerequisite. I just got used to it. All this will work on any Unix or Linux.)
Here is 'portable' OpenSSH patched to support 'none' cipher: openssh-9.0p1.patch.
Also, libssh2 example patched to turn off encryption: libssh2-1.10.0.patch.
libssh2 to be compiled as:
./configure --enable-crypt-none
Two OpenSSH daemons can coexist on the same (virtual) machine. Run the second on different port. Here is how:
/root/openssh/ssh/sshd/sshd -f sshd_config_tmp -D -E log
My minimal config:
Port 669 SyslogFacility DAEMON LogLevel DEBUG3 PermitRootLogin yes AuthorizedKeysFile .ssh/authorized_keys PidFile /var/run/sshd_669.pid Subsystem sftp /usr/libexec/sftp-server
You can run patched libssh2:
./ssh2_exec 192.168.1.104 dennis megapass "uname -a"
Or patched OpenSSH client:
ssh -v -oCiphers=none -p 669 dennis@192.168.1.104
Now run my toyssh_v1.py:
./toyssh_v1.py -vv -h 192.168.1.104 -u dennis -pass megapass -port 669 -c uptime -no-encryption
ToySSH will produce a lot of debug info and dumps. And so is patched OpenSSH 9.0.
Let's see all the packets between client and server.
When you first connect to SSH server (even with netcat), you see a banner, like:
got from server: 00000000: 53 53 48 2D 32 2E 30 2D 4F 70 65 6E 53 53 48 5F SSH-2.0-OpenSSH_ 00000010: 39 2E 30 0D 0A 9.0..
ToySSH sends his banner:
my_send() sending: 00000000: 53 53 48 2D 32 2E 30 2D 54 6F 79 53 53 48 5F 30 SSH-2.0-ToySSH_0 00000010: 2E 31 0D 0A .1..
The server sends list of supported algorithms:
my_recv_plain() got: 00000000: 00 00 04 3C 05 14 20 F9 EA 42 49 03 41 1B D9 72 ...... ..BI.A..r 00000010: B8 38 28 14 3C 5F 00 00 01 09 73 6E 74 72 75 70 .8(.._....sntrup 00000020: 37 36 31 78 32 35 35 31 39 2D 73 68 61 35 31 32 761x25519-sha512 00000030: 40 6F 70 65 6E 73 73 68 2E 63 6F 6D 2C 63 75 72 @openssh.com,cur 00000040: 76 65 32 35 35 31 39 2D 73 68 61 32 35 36 2C 63 ve25519-sha256,c 00000050: 75 72 76 65 32 35 35 31 39 2D 73 68 61 32 35 36 urve25519-sha256 [...] 000003F0: 31 32 2C 68 6D 61 63 2D 73 68 61 31 00 00 00 15 12,hmac-sha1.... 00000400: 6E 6F 6E 65 2C 7A 6C 69 62 40 6F 70 65 6E 73 73 none,zlib@openss 00000410: 68 2E 63 6F 6D 00 00 00 15 6E 6F 6E 65 2C 7A 6C h.com....none,zl 00000420: 69 62 40 6F 70 65 6E 73 73 68 2E 63 6F 6D 00 00 ib@openssh.com.. 00000430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Which are (as of OpenSSH 9.0):
kex_algorithms: sntrup761x25519-sha512@openssh.com kex_algorithms: curve25519-sha256 kex_algorithms: curve25519-sha256@libssh.org kex_algorithms: ecdh-sha2-nistp256 kex_algorithms: ecdh-sha2-nistp384 kex_algorithms: ecdh-sha2-nistp521 kex_algorithms: diffie-hellman-group-exchange-sha256 kex_algorithms: diffie-hellman-group16-sha512 kex_algorithms: diffie-hellman-group18-sha512 kex_algorithms: diffie-hellman-group14-sha256 server_host_algorithms: rsa-sha2-512 server_host_algorithms: rsa-sha2-256 server_host_algorithms: ecdsa-sha2-nistp256 server_host_algorithms: ssh-ed25519 encryption_algorithms: chacha20-poly1305@openssh.com encryption_algorithms: aes128-ctr encryption_algorithms: aes192-ctr encryption_algorithms: aes256-ctr encryption_algorithms: aes128-gcm@openssh.com encryption_algorithms: aes256-gcm@openssh.com encryption_algorithms: none mac_algorithms: umac-64-etm@openssh.com mac_algorithms: umac-128-etm@openssh.com mac_algorithms: hmac-sha2-256-etm@openssh.com mac_algorithms: hmac-sha2-512-etm@openssh.com mac_algorithms: hmac-sha1-etm@openssh.com mac_algorithms: umac-64@openssh.com mac_algorithms: umac-128@openssh.com mac_algorithms: hmac-sha2-256 mac_algorithms: hmac-sha2-512 mac_algorithms: hmac-sha1 compression_algorithms: none compression_algorithms: zlib@openssh.com
Please note "none" in "encryption_algorithms" -- it doesn't exist when a 'real' unpatched SSH server running.
Also, header + padding are added to 'payload'.
The basic SSH packet format is:
[See RFC 4253 6]
ToySSH sends its own list of algorithms. But there is only one algorithm for each function. KEX=diffie-hellman-group-exchange-sha256. Cipher=none. MAC=hmac-sha2-256. Only this is important so far. All the rest is to be ignored yet.
my_send() sending: 00000000: 00 00 00 D4 04 14 11 11 11 11 11 11 11 11 11 11 ................ 00000010: 11 11 11 11 11 11 00 00 00 24 64 69 66 66 69 65 .........$diffie 00000020: 2D 68 65 6C 6C 6D 61 6E 2D 67 72 6F 75 70 2D 65 -hellman-group-e 00000030: 78 63 68 61 6E 67 65 2D 73 68 61 32 35 36 00 00 xchange-sha256.. 00000040: 00 21 73 73 68 2D 72 73 61 2C 72 73 61 2D 73 68 .!ssh-rsa,rsa-sh 00000050: 61 32 2D 32 35 36 2C 72 73 61 2D 73 68 61 32 2D a2-256,rsa-sha2- 00000060: 35 31 32 00 00 00 04 6E 6F 6E 65 00 00 00 04 6E 512....none....n 00000070: 6F 6E 65 00 00 00 0D 68 6D 61 63 2D 73 68 61 32 one....hmac-sha2 00000080: 2D 32 35 36 00 00 00 0D 68 6D 61 63 2D 73 68 61 -256....hmac-sha 00000090: 32 2D 32 35 36 00 00 00 15 6E 6F 6E 65 2C 7A 6C 2-256....none,zl 000000A0: 69 62 40 6F 70 65 6E 73 73 68 2E 63 6F 6D 00 00 ib@openssh.com.. 000000B0: 00 15 6E 6F 6E 65 2C 7A 6C 69 62 40 6F 70 65 6E ..none,zlib@open 000000C0: 73 73 68 2E 63 6F 6D 00 00 00 00 00 00 00 00 00 ssh.com......... 000000D0: 00 00 00 00 AA AA AA AA ........
Please note four 0xAA bytes at the end. This is padding bytes added by ToySSH.
It's recommended by RFC to use random bytes in padding.
libssh2 uses random padding. So when you see noise between payload and MAC, this may be padding.
OpenSSH 9.0 use zero bytes for padding.
But I use 0xAA bytes so that the padding would be clearly visible in this demonstration.
Also, KEX packet has 'random cookie' field (16 bytes). It's random and used to randomize KEX packet, so that it will be different each time. Again, for demonstration, I use 16 0x11 bytes.
Here we send SSH_MSG_KEX_DH_GEX_REQUEST message. We send 3 integers:
DH_GEX_min, DH_GEX_bits, DH_GEX_max = 512, 512, 8192
We say that we require minumum DH bits 512, maximum 8192 bits. (In fact, real OpenSSH 9.0 client today sends 2048/8192, requiring at least 2048 bits.)
When I set 8k DH, ToySSH can login, but pow() operation is very slow. Login happens after 1-2 seconds on my venerable Intel(R) Xeon(R) CPU E31220 @ 3.10GHz. Yes, this is Python, after all, and all SSH implementations are usually written in optimized pure C. But you got the idea, more secure is more slow. Sysadmins seeks balance between security and performance, and 2k DH is a good choice in 2022.
my_send() sending: 00000000: 00 00 00 14 06 22 00 00 02 00 00 00 02 00 00 00 .....".......... 00000010: 20 00 AA AA AA AA AA AA .......
Servers sends DH parameters. Which are also called "DH group". These are two numbers: P and G. See my blog post about DH about it.
my_recv_plain() got: 00000000: 00 00 01 14 08 1F 00 00 01 01 00 C1 17 F4 B6 31 ...............1 00000010: CA 03 2F D2 F0 0A C0 D9 A5 47 3D 8E 56 DA 24 6F ../......G=.V.$o 00000020: C4 4F D5 94 BF 56 57 D3 99 E4 53 72 83 41 CC 92 .O...VW...Sr.A.. 00000030: 0E E9 12 77 29 63 76 83 D2 68 CA 3B 62 F5 CB 61 ...w)cv..h.;b..a [...] 000000F0: 8A 23 2F AD 4C DC 6E 58 0F EC 73 0D 07 0D 49 E8 .#/.L.nX..s...I. 00000100: 8A 23 C5 28 A4 98 5F 87 DB EA 23 00 00 00 01 02 .#.(.._...#..... 00000110: 00 00 00 00 00 00 00 00 ........ ... DH GEX modulus (P): 0xc117f4b6[...]87dbea23 binlog(P): 2048 DH GEX base (G): 2
Basically, this is a random number from /etc/ssh/moduli file. (Or /etc/moduli in OpenBSD.) As of OpenSSH 9.0, there are ~400 parameters. Ranging from 2048 bits from 8192.
As we know, attacker can get prepared to a G/P parameters. So SSH increased number of them and picks randomly. Also, you can add your own parameters or remove existing, if they are suspected to insecuriness. Also, they are very slow to generate.
Multi-precision numbers (bignums) are sent as is, prepended by length (encoded as u32).
ToySSH generates DH public key and send it to server:
client_e 0x27606b0[...]a83f1b6 ... my_send() sending: 00000000: 00 00 01 0C 06 20 00 00 01 00 27 60 6B 0F 73 F6 ..... ....'`k.s. 00000010: 51 59 05 2B 5F 76 90 15 EA 7D E2 AE 61 A0 72 C6 QY.+_v...}..a.r. 00000020: A5 CB 7C E7 C8 3B 89 B9 57 A6 BF 68 89 95 45 68 ..|..;..W..h..Eh 00000030: D7 4C C6 61 4A A0 66 44 69 F5 48 82 DF 46 AC 54 .L.aJ.fDi.H..F.T [...] 000000F0: 45 C4 34 B3 43 96 DF 44 55 94 79 C5 4B 83 7E 10 E.4.C..DU.y.K.~. 00000100: 1C 09 D2 2C 42 DA 1A 83 F1 B6 AA AA AA AA AA AA ...,B...........
Servers sends a big pack of info, which includes his DH public key, RSA public key and something else, which are to be ignored so far:
my_recv_plain() got: 00000000: 00 00 04 44 0B 21 00 00 01 97 00 00 00 07 73 73 ...D.!........ss 00000010: 68 2D 72 73 61 00 00 00 03 01 00 01 00 00 01 81 h-rsa........... 00000020: 00 DD 85 12 E3 82 7B 61 34 3F E7 ED 2C 21 48 3F ......{a4?..,!H? 00000030: 27 C7 11 DC D6 62 C2 74 A7 2C 78 42 66 1D B2 CD '....b.t.,xBf... 00000040: 96 09 E5 32 CB ED 15 E8 1B 9B 60 5F 6E EE C1 C1 ...2......`_n... [...] 00000400: 63 B8 6B 5A 7A 69 20 4F A4 40 69 36 30 F9 FF 3B c.kZzi O.@i60..; 00000410: 33 62 98 FB DB 59 96 79 37 D2 28 39 85 4B 4C 52 3b...Y.y7.(9.KLR 00000420: BB AF 14 D3 FE 59 80 92 17 89 A4 11 69 E3 62 D3 .....Y......i.b. 00000430: 28 D0 8E 06 1F 08 60 99 FE EB CA 19 FC 00 00 00 (.....`......... 00000440: 00 00 00 00 00 00 00 00 ........ KEX_host_key: will be hashed as blob: 00000000: 00 00 00 07 73 73 68 2D 72 73 61 00 00 00 03 01 ....ssh-rsa..... 00000010: 00 01 00 00 01 81 00 DD 85 12 E3 82 7B 61 34 3F ............{a4? 00000020: E7 ED 2C 21 48 3F 27 C7 11 DC D6 62 C2 74 A7 2C ..,!H?'....b.t., 00000030: 78 42 66 1D B2 CD 96 09 E5 32 CB ED 15 E8 1B 9B xBf......2...... [...] 00000170: 72 92 32 48 E4 1E E7 80 7A CC B5 D3 A6 B1 F4 2A r.2H....z......* 00000180: B9 8D E5 39 A2 84 18 D1 6F 10 D4 9E F2 88 E5 3E ...9....o....... 00000190: 68 C2 1B 8B B0 18 45 h.....E host_key_type: ssh-rsa RSA_e: 65537 RSA_modulus_n: 5027120590[...]44816056389 binlog(RSA_modulus_n): 3072 server_f: 0x4816d8757d8[...]8a763f7a08a KEX_H_sig: 00000000: 00 00 00 0C 72 73 61 2D 73 68 61 32 2D 32 35 36 ....rsa-sha2-256 00000010: 00 00 01 80 37 7F 98 69 B4 F1 1E E9 83 0C D1 76 ....7..i.......v 00000020: F5 B5 74 11 BC 6E EF DA EA 38 8B 63 CE 37 64 E5 ..t..n...8.c.7d. 00000030: D2 8B C4 5B 33 02 90 F4 C5 F1 2D 4E F7 E4 DD 41 ...[3.....-N...A [...] 00000160: 40 69 36 30 F9 FF 3B 33 62 98 FB DB 59 96 79 37 @i60..;3b...Y.y7 00000170: D2 28 39 85 4B 4C 52 BB AF 14 D3 FE 59 80 92 17 .(9.KLR.....Y... 00000180: 89 A4 11 69 E3 62 D3 28 D0 8E 06 1F 08 60 99 FE ...i.b.(.....`.. 00000190: EB CA 19 FC ....
BTW, RSA public key sent by server is the very same that are located at /etc/ssh/ssh_host_rsa_key.pub, encoded in base64.
Let's see, this is real data from my experimental virtual machine:
~/1# cat /etc/ssh/ssh_host_rsa_key.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDdhRLjgnthND/n7SwhSD8nxxHc1mLCdKcseEJmHbLNlgnlMsvtFegbm2Bfbu7BwQUflOKTd4aqN6A2rMJD23XO+dOLR1tkS48Pd7WDG7cUeRAjtCAajB6hHI38zcdGkdlP1kQjjQ7puo1mpe9ezRVdwppfS0KutZ9fvZw2BfSxeXAD/1hewS9hMaq8IxOg2v5oF0VN4CFY/7vnFUqXCxKwgjgczt4/q7kLXNb62MkZ0bzZS9AdxpSWxlsZletP9rsjQT7ASDy4ATqXk8NYZDxK6a6t+fKENhG1f+aGAs6tboS0+ImS53mDGOAu+tMD6K4cftEv4GP+SxQE0RnYSoKz/xxZ10TzjGk0OFwtXWDCfUBW9putXqH6BiaBaBSqcD+ijldrLyRVOqryU5ijTFOnRIsdU/ArUU8iEScPo2+Py+EDJs9R/e0W/7qL5w1TyYYWp3peWtitmV5ykjJI5B7ngHrMtdOmsfQquY3lOaKEGNFvENSe8ojlPmjCG4uwGEU= root@bersarin.my.domain ~/1# echo "AAAAB3NzaC1yc2EAAAADAQABAAABgQDdhRLjgnthND/n7SwhSD8nxxHc1mLCdKcseEJmHbLNlgnlMsvtFegbm2Bfbu7BwQUflOKTd4aqN6A2rMJD23XO+dOLR1tkS48Pd7WDG7cUeRAjtCAajB6hHI38zcdGkdlP1kQjjQ7puo1mpe9ezRVdwppfS0KutZ9fvZw2BfSxeXAD/1hewS9hMaq8IxOg2v5oF0VN4CFY/7vnFUqXCxKwgjgczt4/q7kLXNb62MkZ0bzZS9AdxpSWxlsZletP9rsjQT7ASDy4ATqXk8NYZDxK6a6t+fKENhG1f+aGAs6tboS0+ImS53mDGOAu+tMD6K4cftEv4GP+SxQE0RnYSoKz/xxZ10TzjGk0OFwtXWDCfUBW9putXqH6BiaBaBSqcD+ijldrLyRVOqryU5ijTFOnRIsdU/ArUU8iEScPo2+Py+EDJs9R/e0W/7qL5w1TyYYWp3peWtitmV5ykjJI5B7ngHrMtdOmsfQquY3lOaKEGNFvENSe8ojlPmjCG4uwGEU=" | base64 -d | xxd -g 1 00000000: 00 00 00 07 73 73 68 2d 72 73 61 00 00 00 03 01 ....ssh-rsa..... 00000010: 00 01 00 00 01 81 00 dd 85 12 e3 82 7b 61 34 3f ............{a4? 00000020: e7 ed 2c 21 48 3f 27 c7 11 dc d6 62 c2 74 a7 2c ..,!H?'....b.t., [...] 00000170: 72 92 32 48 e4 1e e7 80 7a cc b5 d3 a6 b1 f4 2a r.2H....z......* 00000180: b9 8d e5 39 a2 84 18 d1 6f 10 d4 9e f2 88 e5 3e ...9....o....... 00000190: 68 c2 1b 8b b0 18 45 h.....E
Back to our log. ToySSH computes DH shared secret:
shared_secret: 0x61c646[...]e75ea3d
The same secret known by server, but no one else, thanks to DH coolness.
Now we compute hash of 'hodgepodge'. It contains almost all data exchanged so far (easily visible to attacker), but the most critical is shared_secret (which is not visible).
[See calc_kexgex_hash() in ToySSH or kexgex_hash() in OpenSSH 9.0.]
The resulting hodgepodge is hashed by SHA256 (because ToySSH picked 'rsa-sha2-256' algorithm.
SHA256(hodgepodge):
kexgex_hash: 00000000: 80 E9 F4 B5 F3 86 92 C6 1E 66 7E 39 0F 60 6C D3 .........f~9.`l. 00000010: 30 8C 3E 2B BD 1D A2 CF 09 D0 23 F4 05 42 B1 77 0..+......#..B.w
In SSH lingo, this value is often called just 'H'.
Now derive 6 keys from 'H'. Basically, key_A is just SHA256(H, "A"), where "A" is just ASCII string. All other keys are calculated in the same manner. [See kex.c, derive_key() in OpenSSH 9.0, also RFC 4253 7.2]
derive_keys() begin key_A: 00000000: 35 23 E8 EA 71 61 27 FB 19 30 17 25 72 24 07 1A 5#..qa'..0.%r$.. 00000010: 77 BE D5 E9 4E 36 95 96 A1 9C B6 FB 1B BF 40 A1 w...N6........@. key_B: 00000000: 6A ED FD 37 FB D9 E1 32 4A 82 03 F0 8E 30 6D E8 j..7...2J....0m. 00000010: F4 27 96 6B 39 A0 73 F5 63 9C A9 C6 85 89 DE D2 .'.k9.s.c....... key_C: 00000000: BF 23 D4 8E 8A 9B 15 02 9A AA 26 9A 2B 58 E7 F9 .#........&.+X.. 00000010: C8 68 9E 71 04 D5 E5 D7 AA 28 2F AD CA E0 0A 07 .h.q.....(/..... key_D: 00000000: D4 D9 04 2C EB 3D C1 CC 32 3B B7 63 91 8A BA CC ...,.=..2;.c.... 00000010: 77 23 84 9B 4E 98 2F 7B 6F E0 5A B1 3A D5 9E B8 w#..N./{o.Z.:... key_E: 00000000: A2 41 81 4D A2 22 D4 24 60 F3 B6 0D C7 30 49 E1 .A.M.".$`....0I. 00000010: B9 B1 1C B1 FA 81 0E 4A 96 3B 19 ED 46 73 57 19 .......J.;..FsW. key_F: 00000000: 20 A2 69 45 88 FC 19 E7 E7 18 A2 8C 82 4A E1 07 .iE.........J.. 00000010: 62 B6 46 64 E1 99 A5 1C 01 89 57 5D E6 FD 5A F5 b.Fd......W]..Z.
Six keys are used during SSH session. Three -- for client-to-server and three other -- in backward direction.
(Yes, different keys used for each direction. Even more -- SSH allows to use different algorithsm for each direction, but this is not used.)
One key is used as IV (or initial counter for CTR mode) for encryption. Second key -- as a key for encryption. Third key -- as a key for MAC. /p>
Now service message, nothing special:
expecting SSH_MSG_NEWKEYS my_recv_plain() got: 00000000: 00 00 00 0C 0A 15 00 00 00 00 00 00 00 00 00 00 ................ ... send_new_keys(): 00000000: 00 00 00 0C 0A 15 AA AA AA AA AA AA AA AA AA AA ................ my_send() sending: 00000000: 00 00 00 0C 0A 15 AA AA AA AA AA AA AA AA AA AA ................
From that moment, all the data encrypted and MAC-ed, since both parties know all the keys.
But since we turned off encryption, only MAC is added to each packet. These are 32 bytes at the end of each packet. Calculated as HMAC-SHA2-256 in our case.
Sending SSH_MSG_SERVICE_REQUEST:
sending ssh-userauth my_send() sending: 00000000: 00 00 00 1C 0A 05 00 00 00 0C 73 73 68 2D 75 73 ..........ssh-us 00000010: 65 72 61 75 74 68 AA AA AA AA AA AA AA AA AA AA erauth.......... 00000020: 3A 06 88 36 01 E2 82 21 4D 2F 12 BB BE 47 C7 41 :..6...!M/...G.A 00000030: CC 84 C4 F0 18 9B 2F AA C1 AB 48 9F F3 3C D2 F4 ....../...H..<..
Receiving SSH_MSG_SERVICE_ACCEPT:
my_recv_MAC() got: 00000000: 00 00 00 1C 0A 06 00 00 00 0C 73 73 68 2D 75 73 ..........ssh-us 00000010: 65 72 61 75 74 68 00 00 00 00 00 00 00 00 00 00 erauth.......... 00000020: 0D 80 40 3C 52 E1 DD F7 53 71 E5 5A 87 98 C7 D9 ..@.R...Sq.Z.... 00000030: D8 3E 6A 47 59 56 59 CE 8A 34 A6 0D E3 4D 54 0C ..jGYVY..4...MT.
So far so good. Now we enumerate all possible auth modes. Several strings are sent - username, "ssh-connection", "none":
sending ssh-connection my_send() sending: 00000000: 00 00 00 2C 06 32 00 00 00 06 64 65 6E 6E 69 73 ...,.2....dennis 00000010: 00 00 00 0E 73 73 68 2D 63 6F 6E 6E 65 63 74 69 ....ssh-connecti 00000020: 6F 6E 00 00 00 04 6E 6F 6E 65 AA AA AA AA AA AA on....none...... 00000030: 8C FF 87 A2 DC 60 20 30 6C DB 27 B8 C2 6E 91 58 .....` 0l.'..n.X 00000040: 63 B5 5C E7 BE A0 9C E8 B0 F1 F5 36 8B 99 68 48 c.\........6..hH
Server replies:
expecting msg_code=0x33 or: SSH_MSG_USERAUTH_FAILURE my_recv_MAC() got: 00000000: 00 00 00 34 06 33 00 00 00 27 70 75 62 6C 69 63 ...4.3...'public 00000010: 6B 65 79 2C 70 61 73 73 77 6F 72 64 2C 6B 65 79 key,password,key 00000020: 62 6F 61 72 64 2D 69 6E 74 65 72 61 63 74 69 76 board-interactiv 00000030: 65 00 00 00 00 00 00 00 E1 21 1A 27 44 DC 09 BB e........!.'D... 00000040: B7 7C 01 4F 6E 11 64 D4 F4 05 22 20 97 37 C1 C7 .|.On.d..." .7.. 00000050: F7 70 51 B1 37 C7 50 B7 .pQ.7.P. allowed auth modes: publickey,password,keyboard-interactive
This is it. Several sysadmins may turn off "keyboard-interactive" and/or "password", leaving only "publickey", which is assumed more secure than password-based.
But so far, we use password-based auth.
Sending several strings, including password. Yes, in plain text, not encrypted. But in real, encrypted SSH, all packets are encrypted anyway.
sending ssh-connection my_send() sending: 00000000: 00 00 00 3C 05 32 00 00 00 06 64 65 6E 6E 69 73 .....2....dennis 00000010: 00 00 00 0E 73 73 68 2D 63 6F 6E 6E 65 63 74 69 ....ssh-connecti 00000020: 6F 6E 00 00 00 08 70 61 73 73 77 6F 72 64 00 00 on....password.. 00000030: 00 00 08 6D 65 67 61 70 61 73 73 AA AA AA AA AA ...megapass..... 00000040: 4C 71 68 47 CC 8B 73 5D 76 EA 3F EB CA 9F BB 35 LqhG..s]v.?....5 00000050: 54 58 7E 4B D3 AA 5D 90 7D B4 FC E3 2A CE B8 25 TX~K..].}...*..%
Getting the SSH_MSG_USERAUTH_SUCCESS message:
my_recv_MAC() got: 00000000: 00 00 00 0C 0A 34 00 00 00 00 00 00 00 00 00 00 .....4.......... 00000010: 82 0D 49 95 8E 51 85 3F E0 DB 6A 6D A2 DD CE 0E ..I..Q.?..jm.... 00000020: 34 0E 0C BC 9F 98 06 0A 11 7B 44 DF 60 12 B2 81 4........{D.`... Password correct.
Meaning, password correct.
Service messages SSH_MSG_CHANNEL_OPEN:
my_send() sending: 00000000: 00 00 00 24 0B 5A 00 00 00 07 73 65 73 73 69 6F ...$.Z....sessio 00000010: 6E 00 00 00 00 00 00 20 00 00 80 00 00 AA AA AA n...... ........ 00000020: AA AA AA AA AA AA AA AA 91 4B 21 9B 4E 37 10 87 .........K!.N7.. 00000030: A6 1F F4 78 29 33 1D A7 F0 80 5E 79 D8 2E D6 32 ...x)3....^y...2 00000040: F9 3F 1B 80 D8 70 1B 2C .?...p.,
Another service message from server, to be ignored yet:
expecting msg_code=0x50 or: SSH_MSG_GLOBAL_REQUEST my_recv_MAC() got: 00000000: 00 00 02 64 08 50 00 00 00 17 68 6F 73 74 6B 65 ...d.P....hostke 00000010: 79 73 2D 30 30 40 6F 70 65 6E 73 73 68 2E 63 6F ys-00@openssh.co 00000020: 6D 00 00 00 01 97 00 00 00 07 73 73 68 2D 72 73 m.........ssh-rs 00000030: 61 00 00 00 03 01 00 01 00 00 01 81 00 DD 85 12 a............... 00000040: E3 82 7B 61 34 3F E7 ED 2C 21 48 3F 27 C7 11 DC ..{a4?..,!H?'... 00000050: D6 62 C2 74 A7 2C 78 42 66 1D B2 CD 96 09 E5 32 .b.t.,xBf......2 [...] 00000250: DB B0 B2 9E 71 36 AD 21 D7 F0 66 2E 96 A2 2F C6 ....q6.!..f.../. 00000260: 00 00 00 00 00 00 00 00 24 21 4D AD DD F7 52 74 ........$!M...Rt 00000270: 92 82 40 E7 66 EC 91 FE 74 EB D5 A0 B4 6F C0 40 ..@.f...t....o.@ 00000280: 7A 1B FE 04 78 7A AE 6A z...xz.j
Service message, to be ignored:
expecting msg_code=0x5b or: SSH_MSG_CHANNEL_OPEN_CONFIRMATION my_recv_MAC() got: 00000000: 00 00 00 1C 0A 5B 00 00 00 00 00 00 00 00 00 00 .....[.......... 00000010: 00 00 00 00 80 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 74 ED 5D 75 5E 38 8B 00 7B A7 EB 7A 2C 0A A1 79 t.]u^8..{..z,..y 00000030: D7 6D CD 2C 3B 9D E1 80 D7 04 46 8E 93 8D 76 AE .m.,;.....F...v.
Now something real. Sending Unix command, "uptime". Message SSH_MSG_CHANNEL_REQUEST:
send_exec uptime my_send() sending: 00000000: 00 00 00 24 0B 62 00 00 00 00 00 00 00 04 65 78 ...$.b........ex 00000010: 65 63 01 00 00 00 06 75 70 74 69 6D 65 AA AA AA ec.....uptime... 00000020: AA AA AA AA AA AA AA AA E3 2B 01 4C 71 E6 67 20 .........+.Lq.g 00000030: 6A 19 1A 3E 27 21 AB CB C3 C6 5E 0C 4B 90 89 B4 j..>'!....^.K... 00000040: 10 E2 D5 2B 55 DF 2A 0D ...+U.*.
Several messages received. Two most important "exit-status" which passes exit code of command. And the string from stdout:
my_recv_MAC() got: 00000000: 00 00 00 14 0A 5D 00 00 00 00 00 20 00 00 00 00 .....]..... .... 00000010: 00 00 00 00 00 00 00 00 E6 53 76 3D 63 3B C3 70 .........Sv=c;.p 00000020: F9 09 1F FB AC EC D4 11 82 4C 28 38 DB 7B C6 22 .........L(8.{." 00000030: 1D 1B EF 66 AD FD 00 93 ...f.... got message 0x5d or SSH_MSG_CHANNEL_WINDOW_ADJUST my_recv_MAC() got: 00000000: 00 00 00 0C 06 63 00 00 00 00 00 00 00 00 00 00 .....c.......... 00000010: 21 AD 4C 36 E2 92 1E CE 4B CF 42 40 73 25 E7 59 !.L6....K.B@s%.Y 00000020: 98 BE 97 59 31 D8 47 CF 22 37 C6 DA 76 53 63 CC ...Y1.G."7..vSc. got message 0x63 or SSH_MSG_CHANNEL_SUCCESS my_recv_MAC() got: 00000000: 00 00 00 24 0A 62 00 00 00 00 00 00 00 0B 65 78 ...$.b........ex 00000010: 69 74 2D 73 74 61 74 75 73 00 00 00 00 00 00 00 it-status....... 00000020: 00 00 00 00 00 00 00 00 62 4A 30 F3 55 30 97 F9 ........bJ0.U0.. 00000030: 13 FA 8E FF E7 B7 A0 3F 18 F5 6A F0 5F 74 B6 D1 .......?..j._t.. 00000040: 2D 70 10 83 B1 1A 4E 08 -p....N. got message 0x62 or SSH_MSG_CHANNEL_REQUEST exit-status 0 my_recv_MAC() got: 00000000: 00 00 00 54 07 5E 00 00 00 00 00 00 00 43 20 31 ...T.^.......C 1 00000010: 3A 35 36 50 4D 20 20 75 70 20 31 20 64 61 79 2C :56PM up 1 day, 00000020: 20 31 38 3A 31 32 2C 20 32 20 75 73 65 72 73 2C 18:12, 2 users, 00000030: 20 6C 6F 61 64 20 61 76 65 72 61 67 65 73 3A 20 load averages: 00000040: 30 2E 30 34 2C 20 30 2E 30 39 2C 20 30 2E 30 34 0.04, 0.09, 0.04 00000050: 0A 00 00 00 00 00 00 00 F2 32 45 FF 9F 63 A4 1E .........2E..c.. 00000060: 2C 69 66 62 34 5C DC 53 48 14 F9 C9 D3 9F 89 8B ,ifb4\.SH....... 00000070: 18 40 1B 1A 68 20 E4 54 .@..h .T got message 0x5e or SSH_MSG_CHANNEL_DATA response: == 1:56PM up 1 day, 18:12, 2 users, load averages: 0.04, 0.09, 0.04 == my_recv_MAC() got: 00000000: 00 00 00 0C 06 60 00 00 00 00 00 00 00 00 00 00 .....`.......... 00000010: FB D1 6B 8D E2 AB 73 CB C4 F5 5B A5 03 72 FA E8 ..k...s...[..r.. 00000020: 42 8C D5 FD D0 4B AB EB E2 39 CF E2 52 53 58 84 B....K...9..RSX. got message 0x60 or SSH_MSG_CHANNEL_EOF my_recv_MAC() got: 00000000: 00 00 00 0C 06 61 00 00 00 00 00 00 00 00 00 00 .....a.......... 00000010: B9 44 0D 22 42 1D 5E 97 13 0B 41 D9 8F 6A CD 0C .D."B.^...A..j.. 00000020: 99 CB 6D 32 F3 19 D7 41 E1 8A B7 46 07 63 7D 58 ..m2...A...F.c}X got message 0x61 or SSH_MSG_CHANNEL_CLOSE
This is it. We got uptime's output.
Polite client would send EOF before disconnecting:
send_eof() my_send() sending: 00000000: 00 00 00 0C 06 60 00 00 00 00 AA AA AA AA AA AA .....`.......... 00000010: CA FB 7A 09 41 0B FC 3C 21 99 11 15 46 5D 99 DD ..z.A...!...F].. 00000020: F5 76 57 63 2C 59 BB 8B 5F 5C 33 0C 3C 86 E8 03 .vWc,Y.._\3.....
It was easy step to add encryption to all this.
When we send a packet, we just encrypt it with AES-CTR using IV (as counter) and key we already know.
But couple important notes.
1) Packet is encrypted with packet length. MAC is added to encrypted packet, but unencrypted. This is OK.
2) Here we use SDCTR mode (stateful-decryption CTR mode): 1, 2. This means that the AES-CTR counter for each direction is 'global' and initialized during key derivation step. This is considered more secure, than resetting counter at the start of each packet, as CTR mode would normally be implemented.
3) How do you decrypt a packet if you don't know its length? Decrypt first 16 bytes (AES block size) and read packet length. Then read the rest of packet.
This would work with your SSH server, unless it's ancient:
./toyssh_v1.py -vv -h 192.168.1.104 -u dennis -pass megapass -c uptime
So my toy SSH client only able to execute a command. This corresponds to ssh command like:
ssh user@host command
To extend it to support console/terminal, a 'pty-req' message is to be sent. Then each keypress is sent via each packet.
But I wanted to learn cryptography around SSH, not to recreate SSH client, so my client only can execute a command.
Even when using my toy SSH client, thanks to DH, an attacker literally can't decrypt all the traffic, even if it knows the username/password. This is important for backward/forward security. Attacker wouldn't have such opportunity even if he/she recorded all the traffic and stash it somewhere, for the moment when another SSH vulnerability will surface or when password will be leaked. Even if he will have physical access to all the files on client/server after the communication is finished.
All he/she can do is to try getting shared secret from RAM of client or server. Or predict PRNGs for both parties (this is why PRNG must be as good as possible).
This is the coolness of DH algo.
But DH is vulnerable to MITM. As well, as my toy SSH client. An attacker can just pretend he is a real server you connect to.
But 'real' SSH is protected from this as well. This is to be covered in my next blog post(s). Stay tuned.
These commands I used to run patched OpenSSH client:
ssh -v -oKexAlgorithms=diffie-hellman-group-exchange-sha256 -oCiphers=none -oHostkeyAlgorithms=rsa-sha2-256 -oMACs=hmac-sha2-256 -p 669 USER@HOST uptime ssh -v -oKexAlgorithms=diffie-hellman-group-exchange-sha256 -oCiphers=aes256-ctr -oHostkeyAlgorithms=ssh-rsa -oMACs=hmac-sha2-256 USER@HOST uptime
Then I replicated all these algorithms in my ToySSH client.
When hacking, each algo to be fixed. Also, thanks to Wireshark -- it can dissect many SSH packets.
Now a joke in spirit of the "Gödel, Escher, Bach" book. I connected via SSH to my OpenBSD VM that running patched OpenSSH 9.0, that dumps all the data exchanged, unencrypted. After my login, OpenSSH began dumping what it already dumped, i.e., hex dumps:
mac_compute() data: 0000: 00 00 0c ec 0a 5e 00 00 00 00 00 00 0c d8 1b 5b .....^.........[ 0010: 3f 32 35 6c 1b 5b 48 30 31 37 30 3a 20 32 30 20 ?25l.[H0170: 20 0020: 32 30 20 32 30 20 32 30 20 32 30 20 32 30 20 32 20 20 20 20 20 2 0030: 65 20 32 65 20 32 65 20 32 65 20 32 65 20 35 65 e 2e 2e 2e 2e 5e 0040: 20 32 65 20 35 62 20 34 62 20 32 65 20 20 20 20 2e 5b 4b 2e 0050: 20 20 20 20 2e 2e 2e 2e 2e 5e 2e 5b 4b 2e 1b 5b .....^.[K..[ 0060: 4b 0d 0a 30 31 38 30 3a 20 31 62 20 35 62 20 34 K..0180: 1b 5b 4 0070: 62 20 30 64 20 30 61 20 33 30 20 33 33 20 36 32 b 0d 0a 30 33 62 0080: 20 33 30 20 33 61 20 32 30 20 33 30 20 36 31 20 30 3a 20 30 61 0090: 32 30 20 33 37 20 33 33 20 20 2e 5b 4b 2e 2e 30 20 37 33 .[K..0 00a0: 33 62 30 3a 20 30 61 20 37 33 1b 5b 4b 0d 0a 30 3b0: 0a 73.[K..0 00b0: 31 39 30 3a 20 32 30 20 33 37 20 33 33 20 32 30 190: 20 37 33 20 00c0: 20 33 36 20 33 38 20 32 30 20 33 36 20 33 32 20 36 38 20 36 32 00d0: 32 30 20 33 37 20 33 35 20 32 30 20 33 36 20 33 20 37 35 20 36 3 00e0: 36 20 32 30 20 20 20 37 33 20 36 38 20 36 32 20 6 20 73 68 62 00f0: 37 35 20 36 36 20 1b 5b 4b 0d 0a 30 31 61 30 3a 75 66 .[K..01a0: 0100: 20 33 35 20 36 36 20 32 30 20 33 37 20 33 30 20 35 66 20 37 30 ...
The following effect may be achieved if you connect via telnet (if enabled) to a host and run:
tcpdump -X port 23
Packet number is also used during MAC calculation.
So, MAC is calculated as MAC(key=derived key, body=packet/sequence number (u32) || payload) ToySSH client keeps tracking on packet numbers.
[See RFC 4253 6.4]
A word about AES-CTR. As I wrote before, SSH use it in SDCTR mode (stateful-decryption CTR mode). In other words, counter is global. Why? Otherwise two similar packets would produce the same ciphertexts. This may be undesirable. Attacker will know that clients sends the same commands or server responds with similar replies. Some cryptographers compare this with the well-known joke about ECB and Linux penguin.
Both CTR and GCM modes protects from replay attack: each packet encrypted different, even if its contents is the same. The attacker wouldn't be able to send pre-recorded packet. Also, the attacker sitting in the middle (MITM) couldn't skip packets, which is also may be important.
The same story about incorporating packet number into HMAC when calculating MAC for each packet. MAC must be different for each packet, even if packets are the same.
Yes, I know about these lousy Disqus ads. Please use adblocker. I would consider to subscribe to 'pro' version of Disqus if the signal/noise ratio in comments would be good enough.