package de.tudarmstadt.informatik.hostage.protocol; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; import java.util.List; import java.util.Random; import javax.crypto.KeyAgreement; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.DHPublicKeySpec; import de.tudarmstadt.informatik.hostage.commons.HelperUtils; import de.tudarmstadt.informatik.hostage.wrapper.ByteArray; /** * SSH protocol. * @author Wulf Pfeiffer */ public final class SSH implements Protocol { /** * Represents the states of the protocol. */ private enum STATE { NONE, SERVER_VERSION, CLIENT_VERSION, KEX_INIT, CLOSED } /** * Denotes in which state the protocol is right now. */ private STATE connectionState = STATE.NONE; private String serverVersion = "SSH-2.0-"; private String serverType = "OpenSSH_6.0p1 Debian-4"; //Diffie-Hellman-Group-1 p and g private final byte[] p = { (byte)0x00, (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, (byte)0xC9,(byte)0x0F,(byte)0xDA,(byte)0xA2,(byte)0x21,(byte)0x68,(byte)0xC2,(byte)0x34, (byte)0xC4,(byte)0xC6,(byte)0x62,(byte)0x8B,(byte)0x80,(byte)0xDC,(byte)0x1C,(byte)0xD1, (byte)0x29,(byte)0x02,(byte)0x4E,(byte)0x08,(byte)0x8A,(byte)0x67,(byte)0xCC,(byte)0x74, (byte)0x02,(byte)0x0B,(byte)0xBE,(byte)0xA6,(byte)0x3B,(byte)0x13,(byte)0x9B,(byte)0x22, (byte)0x51,(byte)0x4A,(byte)0x08,(byte)0x79,(byte)0x8E,(byte)0x34,(byte)0x04,(byte)0xDD, (byte)0xEF,(byte)0x95,(byte)0x19,(byte)0xB3,(byte)0xCD,(byte)0x3A,(byte)0x43,(byte)0x1B, (byte)0x30,(byte)0x2B,(byte)0x0A,(byte)0x6D,(byte)0xF2,(byte)0x5F,(byte)0x14,(byte)0x37, (byte)0x4F,(byte)0xE1,(byte)0x35,(byte)0x6D,(byte)0x6D,(byte)0x51,(byte)0xC2,(byte)0x45, (byte)0xE4,(byte)0x85,(byte)0xB5,(byte)0x76,(byte)0x62,(byte)0x5E,(byte)0x7E,(byte)0xC6, (byte)0xF4,(byte)0x4C,(byte)0x42,(byte)0xE9,(byte)0xA6,(byte)0x37,(byte)0xED,(byte)0x6B, (byte)0x0B,(byte)0xFF,(byte)0x5C,(byte)0xB6,(byte)0xF4,(byte)0x06,(byte)0xB7,(byte)0xED, (byte)0xEE,(byte)0x38,(byte)0x6B,(byte)0xFB,(byte)0x5A,(byte)0x89,(byte)0x9F,(byte)0xA5, (byte)0xAE,(byte)0x9F,(byte)0x24,(byte)0x11,(byte)0x7C,(byte)0x4B,(byte)0x1F,(byte)0xE6, (byte)0x49,(byte)0x28,(byte)0x66,(byte)0x51,(byte)0xEC,(byte)0xE6,(byte)0x53,(byte)0x81, (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF }; private final byte[] g = {0x02}; //SSH Parameters for Kex etc. private byte[] V_S = serverType.getBytes(); private byte[] V_C; private byte[] I_S; private byte[] I_C; private byte[] e; private byte[] f; private byte[] k; private byte[] h; private byte[] K_S; private byte[] sig; //Keys for signature private KeyPair dsa; //allowed algorithms for kexinit private String kex_alg = "diffie-hellman-group1-sha1"; private String server_alg = "ssh-dss"; private String encrypt_alg_c = "aes128-ctr"; private String encrypt_alg_s = "aes128-ctr"; private String mac_alg_c = "hmac-sha1"; private String mac_alg_s = "hmac-sha1"; private String comp_alg_c = "none"; private String comp_alg_s = "none"; private int cipherBlockSize = 16; /** Denotes in which state the protocol is right now */ private STATE state = STATE.NONE; private byte[] lastMessage; @Override public int getPort() { return 22; } @Override public TALK_FIRST whoTalksFirst() { return TALK_FIRST.SERVER; } @Override public List processMessage(ByteArray message) { if(message != null) lastMessage = message.get(); List response = new ArrayList(); byte[] request = null; if(message != null) request = message.get(); switch(connectionState) { case NONE: response.add(new ByteArray(serverVersion + serverType + "\r\n")); connectionState = STATE.SERVER_VERSION; break; case SERVER_VERSION: extractType(request); extractCookie(request); response.add(new ByteArray(kexInit())); connectionState = STATE.CLIENT_VERSION; break; case CLIENT_VERSION: extractPubKey(request); response.add(new ByteArray(dhKexReply())); //FIXME signature in dhKexReply is wrong, don't know why response.add(new ByteArray(newKeys())); connectionState = STATE.KEX_INIT; break; case KEX_INIT: connectionState = STATE.CLOSED; break; case CLOSED: break; default: connectionState = STATE.CLOSED; break; } return response; } @Override public boolean isClosed() { return (state == STATE.CLOSED); } @Override public boolean isSecure() { return false; } @Override public Class getType() { return ByteArray.class; } @Override public String toString() { return "SSH"; } /** * Wraps the packets with packet length and padding. * @param packet content that is wrapped. * @return wrapped packet. */ private byte[] wrapPacket(byte[] packet) { int packetLength = 5 + packet.length; //4 byte packet length, 1 byte padding length, payload length int paddingLengthCBS = cipherBlockSize - (packetLength % cipherBlockSize); int paddingLength8 = 8 - (packetLength % 8); int paddingLength = paddingLengthCBS > paddingLength8 ? paddingLengthCBS : paddingLength8; if(paddingLength < 4) paddingLength += cipherBlockSize; packetLength = packetLength + paddingLength - 4; //add padding string length to packet length byte[] packetLen = ByteBuffer.allocate(4).putInt(packetLength).array(); byte[] paddingLen = {(byte) paddingLength}; byte[] paddingString = new byte[paddingLength]; for(int i = 0; i < paddingLength; i++) { paddingString[i] = 0x00; } return HelperUtils.concat(packetLen, paddingLen, packet, paddingString); } /** * Builds the Kex Init packet that contains all the allowed algorithms by the server. * @return Kex Init packet. */ private byte[] kexInit() { byte[] msgCode = {0x14}; I_S = randomBytes(16); byte[] kexLength = ByteBuffer.allocate(4).putInt(kex_alg.getBytes().length).array(); byte[] serverLength = ByteBuffer.allocate(4).putInt(server_alg.getBytes().length).array(); byte[] encrypt_c_Length = ByteBuffer.allocate(4).putInt(encrypt_alg_c.getBytes().length).array(); byte[] encrypt_s_Length = ByteBuffer.allocate(4).putInt(encrypt_alg_s.getBytes().length).array(); byte[] mac_c_Length = ByteBuffer.allocate(4).putInt(mac_alg_c.getBytes().length).array(); byte[] mac_s_Length = ByteBuffer.allocate(4).putInt(mac_alg_s.getBytes().length).array(); byte[] comp_c_Length = ByteBuffer.allocate(4).putInt(comp_alg_c.getBytes().length).array(); byte[] comp_s_Length = ByteBuffer.allocate(4).putInt(comp_alg_s.getBytes().length).array(); byte[] language_c_s = {0x00, 0x00, 0x00, 0x00}; byte[] language_s_c = {0x00, 0x00, 0x00, 0x00}; byte[] kexFirsPckt = {0x00}; byte[] reserved = {0x00, 0x00, 0x00, 0x00}; byte[] response = HelperUtils.concat(msgCode, I_S, kexLength, kex_alg.getBytes(), serverLength, server_alg.getBytes(), encrypt_c_Length, encrypt_alg_c.getBytes(), encrypt_s_Length, encrypt_alg_s.getBytes(), mac_c_Length, mac_alg_c.getBytes(), mac_s_Length, mac_alg_s.getBytes(), comp_c_Length, comp_alg_c.getBytes(), comp_s_Length, comp_alg_s.getBytes(), language_c_s, language_s_c, kexFirsPckt, reserved); return wrapPacket(response); } /** * Builds the Diffie-Hellman Kex Reply, containing the host key,f and the signature. * @return Diffie-Hellman Kex Reply packet. */ private byte[] dhKexReply() { generateDHKeys(); generateHostKey(); generateSha1Hash(); generateSignature(); byte[] msgCode = {0x1f}; byte[] hostKeyLength = ByteBuffer.allocate(4).putInt(K_S.length).array(); byte[] fDHLength = ByteBuffer.allocate(4).putInt(f.length).array(); byte[] signatureLength = ByteBuffer.allocate(4).putInt(sig.length).array(); byte[] server_algLength = ByteBuffer.allocate(4).putInt(server_alg.getBytes().length).array(); byte[] payloadLength = ByteBuffer.allocate(4).putInt(server_algLength.length + signatureLength.length + sig.length + server_alg.getBytes().length).array(); byte[] response = HelperUtils.concat(msgCode, hostKeyLength, K_S, fDHLength, f, payloadLength, server_algLength, server_alg.getBytes(), signatureLength, sig); return wrapPacket(response); } /** * New Keys response. * @return New Keys response. */ private byte[] newKeys() { byte[] msgCode = {0x15}; return wrapPacket(msgCode); } /** * Generates the required Diffie-Hellman keys with p and g from Oakley Group 1. */ private void generateDHKeys() { try { KeyPairGenerator myKpairGen = KeyPairGenerator.getInstance("DH"); KeyAgreement myKeyAgree = KeyAgreement.getInstance("DH"); BigInteger p = new BigInteger(this.p); BigInteger g = new BigInteger(this.g); BigInteger e = new BigInteger(this.e); DHParameterSpec dhParamSpec = new DHParameterSpec(p, g); myKpairGen.initialize(dhParamSpec); KeyPair myKpair = myKpairGen.generateKeyPair(); myKeyAgree.init(myKpair.getPrivate()); BigInteger f = ((DHPublicKey) (myKpair.getPublic())).getY(); this.f = f.toByteArray(); KeyFactory myKeyFac = KeyFactory.getInstance("DH"); DHPublicKeySpec keySpec = new DHPublicKeySpec(e, p, g); PublicKey yourPubKey = myKeyFac.generatePublic(keySpec); myKeyAgree.doPhase(yourPubKey, true); byte[] mySharedSecret = myKeyAgree.generateSecret(); k = mySharedSecret; } catch (Exception e) { e.printStackTrace(); } } /** * Generates the Host Key based on the DSA algorithm */ private void generateHostKey() { try { KeyPairGenerator generator = KeyPairGenerator.getInstance("DSA"); dsa = generator.generateKeyPair(); byte[] string = "ssh-dss".getBytes(); byte[] stringLength = ByteBuffer.allocate(4).putInt(string.length).array(); byte[] p = ((DSAPublicKey) dsa.getPublic()).getParams().getP().toByteArray(); if(p[0] != 0x00) p = HelperUtils.concat(new byte[]{0x00}, p); byte[] pLength = ByteBuffer.allocate(4).putInt(p.length).array(); byte[] q = ((DSAPublicKey) dsa.getPublic()).getParams().getQ().toByteArray(); if(q[0] != 0x00) q = HelperUtils.concat(new byte[]{0x00}, q); byte[] qLength = ByteBuffer.allocate(4).putInt(q.length).array(); byte[] g = ((DSAPublicKey) dsa.getPublic()).getParams().getG().toByteArray(); if(g[0] != 0x00) g = HelperUtils.concat(new byte[]{0x00}, g); byte[] gLength = ByteBuffer.allocate(4).putInt(g.length).array(); byte[] y = ((DSAPublicKey) dsa.getPublic()).getY().toByteArray(); if(y[0] != 0x00) y = HelperUtils.concat(new byte[]{0x00}, y); byte[] yLength = ByteBuffer.allocate(4).putInt(y.length).array(); K_S = HelperUtils.concat(stringLength, string, pLength, p, qLength, q, gLength, g, yLength, y); } catch (Exception e) { e.printStackTrace(); } } /** * Generates the SHA-1 Hash from several values */ private void generateSha1Hash() { try { MessageDigest sha = MessageDigest.getInstance("SHA-1"); sha.update(V_C); sha.update(V_S); sha.update(I_C); sha.update(I_S); sha.update(K_S); sha.update(e); sha.update(f); sha.update(k); h = sha.digest(); } catch (Exception e) { e.printStackTrace(); } } /** * Generates the signature of the hash using DSA algorithm with SHA-1 */ private void generateSignature() { //FIXME something is wrong with this signature.. maybe one of the used components is generated wrong?! try { Signature sig = Signature.getInstance("SHA1withDSA"); sig.initVerify(dsa.getPublic()); sig.initSign(dsa.getPrivate()); sig.update(h); this.sig = extractSignature(sig.sign()); } catch (Exception e) { e.printStackTrace(); } } /** * Extracts the type of the client * @param request containing the clients type */ private void extractType(byte[] request) { int length = 0; for(int i = 8; i < request.length; i++, length++) { //start at 8 because "SSH-2.0-" is not part of type if(request[i] == 0x0d) break; //find the end of the type: '\r' } V_C = new byte[length]; System.arraycopy(request, 8, V_C, 0, length); } /** * Extracts the cookie from the Kex Init client request * @param request containing the clients cookie */ private void extractCookie(byte[] request) { int pos = 0; if(request[5] != 0x14) { //if type packet is in front of kex init pos = 1; //start behind the end of type message for(int i = 0; i < request.length; i++, pos++) { if(request[i] == 0x0a) break; //find end of type message: '\n' } } I_C = new byte[16]; System.arraycopy(request, 6+pos, I_C, 0, 16); //srcLen: headersize+position after type packet } /** * Extracts the public key from the DH Kex Request * @param request containing the clients public key */ private void extractPubKey(byte[] request) { e = new byte[byteToInt(new byte[] {request[6], request[7], request[8], request[9]})]; for(int i = 0; i < e.length; i++) { e[i] = request[i+10]; } } /** * Converts a byte[] to int * @param bytes that are converted * @return converted byte[] as int */ private static int byteToInt(byte[] bytes) { int ret = 0; for (int i=0; i < bytes.length; i++) { ret <<= 8; ret |= bytes[i] & 0xFF; } return ret; } /** * Generates a random byte[] of a specified size * @param size of the byte[] * @return random byte[] */ private byte[] randomBytes(int size) { byte[] bytes = new byte[size]; Random rdm = new Random(); rdm.nextBytes(bytes); return bytes; } /** * Extracts r and s from a DSA-signature * @param signature * @return r and s as byte[] */ private byte[] extractSignature(byte[] signature) { //{ r INTEGER, s INTEGER } int length = 0; int index = 3; length = signature[index++] & 0xff; byte[] r = new byte[length]; System.arraycopy(signature, index, r, 0, r.length); index = index + length + 1; length = signature[index++] & 0xff; byte[] s = new byte[length]; System.arraycopy(signature, index, s, 0, s.length); byte[] result = new byte[40]; // result must be 40 bytes, but length of r and s may not be 20 bytes System.arraycopy(r, (r.length > 20) ? 1 : 0, result, (r.length > 20) ? 0 : 20 - r.length, (r.length > 20) ? 20 : r.length); System.arraycopy(s, (s.length > 20) ? 1 : 0, result, (s.length > 20) ? 20 : 40 - s.length, (s.length > 20) ? 20 : s.length); return result; } }