1

I am trying to find a solution for getting the SessionID and more important the SessionKey. I already found a solution which is based on Java:

http://jsslkeylog.sourceforge.net

it is using the following class to log the RSA-SessionKey:

/**
 * Transformer to transform <tt>RSAClientKeyExchange</tt> and
 * <tt>PreMasterSecret</tt> classes to log <tt>RSA</tt> values.
 */
public class RSAClientKeyExchangeTransformer extends AbstractTransformer {

public RSAClientKeyExchangeTransformer(String className) {
    super(className, "<init>");
}

@Override
protected void visitEndOfMethod(MethodVisitor mv, String desc) {
    String preMasterType = "Ljavax/crypto/SecretKey;";
    if (className.endsWith("/PreMasterSecret")) {
        preMasterType = "[B";
    }
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETFIELD, className, "encrypted", "[B");
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETFIELD, className, "preMaster", preMasterType);
    mv.visitMethodInsn(INVOKESTATIC, className, "$LogWriter$logRSA", "([B" + preMasterType + ")V");
}
}

In my android application I am using the DefaultHttpClient (org.apache.http.impl.client) to establish a HTTPS connection. For this connection I am trying to find the SessionKey. Does someone has an idea if it´s possible to read out the key by using an android / java method? If not, does someone know where the key- generation is implemented?

davidOhara
  • 1,008
  • 5
  • 17
  • 39

2 Answers2

3

I don't believe this can be done via the public API. You can get the session ID but there is no public interface to obtain the key.

However, I was able to use a combination of reflection and native code to get access to the underlying OpenSSL struct, which does contain both the session ID and the master key. So it is possible but it is not at all safe as the hidden member and libraries are not guaranteed to remain the same. In fact, it looks like the struct layout has changed on the OpenSSL master branch so the parsing code below will need updating if/when that is pulled into Android.

I used URL.openConnection() instead of DefaultHttpClient for making an HTTPS connection as the latter is now deprecated. Here's the class that calls URL.openConnection() and replaces the default SSLSocketFactory (nothing that interesting here):

public class MyConnection implements Runnable {
   @Override
   public void run() {
      try {
         // Create the connection.
         URL url = new URL("https://www.google.com");
         HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();

         // Replace the default SSLSocketFactory with our own.
         MySSLSocketFactory sslSocketFactory = new MySSLSocketFactory();
         urlConnection.setSSLSocketFactory(sslSocketFactory);

         // Establish the TLS connection.
         int statusCode = urlConnection.getResponseCode();
         Log.i("MyConnection", String.format("status %d", statusCode));

         // Get SSL details from the captured socket.
         sslSocketFactory.getSessionInfo();

      } catch (IllegalAccessException e) {
         e.printStackTrace();
      } catch (NoSuchFieldException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();

      }
   }
}

Here is the custom SSLSocketFactory, where most of the magic is. All it does is forward overridden methods to the real SSLSocketFactory, caching the SSLSocket instance that gets created. There are also two new (non-overridden) methods - a native method shown further below and getSessionInfo() which uses reflection on the SSLSocket to get the native OpenSSL ssl_session_st pointer and parses (and logs) fields of interest. Note that you could get the session ID using the supported SSLSession.getId(); it's getting the key that requires being sneaky.

// Use Decorator pattern to capture the SSL socket from the default SSLSocketFactory.
class MySSLSocketFactory extends SSLSocketFactory {
   // Load NDK shared library.
   static {
      System.loadLibrary("my_native_helper");
   }

   // All overridden methods will be forwarded to the real SSLSocketFactory.
   // The only addition is that the SSLSocket returned by createSocket() is
   // cached.
   SSLSocketFactory realFactory_ = HttpsURLConnection.getDefaultSSLSocketFactory();
   SSLSocket s_;

   // This native method copies data from a native pointer into a ByteBuffer.
   native void readNative(long pointer, ByteBuffer dst);

   // Use the cached SSLSocket to access native OpenSSL session data.
   void getSessionInfo() throws NoSuchFieldException, IllegalAccessException {
      // Get the protected OpenSSL ssl_session_st pointer. Note that this
      // is not part of the API and could change across Android versions.
      // See https://android.googlesource.com/platform/external/conscrypt/+/lollipop-mr1-dev/src/main/java/org/conscrypt/OpenSSLSessionImpl.java
      SSLSession session = s_.getSession();
      Field field = session.getClass().getDeclaredField("sslSessionNativePointer");
      field.setAccessible(true);
      long sessionPointer = field.getLong(session);

      // Read as many bytes as we need from the native pointer.
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(104);
      byteBuffer.order(ByteOrder.nativeOrder());
      readNative(sessionPointer, byteBuffer);

      // Parse the OpenSSL ssl_session_st. Note that the layout of this structure
      // may change with OpenSSL versions and different compilers/platforms (e.g.
      // 32-bit vs. 64-bit).
      // See https://github.com/openssl/openssl/blob/OpenSSL_1_0_0-stable/ssl/ssl.h#L451
      IntBuffer intBuffer = byteBuffer.asIntBuffer();
      Log.i("MyConnection", String.format("SSL version %04x", intBuffer.get(0)));

      int master_key_length = intBuffer.get(4);
      String master_key = "";
      for (int i = 0; i < master_key_length; ++i)
         master_key += String.format("%02x", byteBuffer.get(20 + i));
      Log.i("MyConnection", String.format("Master key %s", master_key));

      int session_id_length = intBuffer.get(17);
      String session_id = "";
      for (int i = 0; i < session_id_length; ++i)
         session_id += String.format("%02x", byteBuffer.get(72 + i));
      Log.i("MyConnection", String.format("Session ID %s", session_id));
   }

   @Override
   public String[] getDefaultCipherSuites() {
      return realFactory_.getDefaultCipherSuites();
   }

   @Override
   public String[] getSupportedCipherSuites() {
      return realFactory_.getSupportedCipherSuites();
   }

   @Override
   public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
      s_ = (SSLSocket)realFactory_.createSocket(s, host, port, autoClose);
      return s_;
   }

   @Override
   public Socket createSocket(String host, int port) throws IOException {
      s_ =  (SSLSocket)realFactory_.createSocket(host, port);
      return s_;
   }

   @Override
   public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
      s_ = (SSLSocket)realFactory_.createSocket(host, port, localHost, localPort);
      return s_;
   }

   @Override
   public Socket createSocket(InetAddress host, int port) throws IOException {
      s_ =  (SSLSocket)realFactory_.createSocket(host, port);
      return s_;
   }

   @Override
   public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
      s_ = (SSLSocket)realFactory_.createSocket(address, port, localAddress, localPort);
      return s_;
   }
}

Finally, here is the native C code that enables reading memory into a ByteBuffer from a native pointer. This needs to be built using the Android NDK and loaded as shown at the top of MySSLSocketFactory.

#include <jni.h>
#include <string.h>

JNIEXPORT
void JNICALL Java_com_example_mysocketfactory_MySSLSocketFactory_readNative(
   JNIEnv *env, jobject o,
   jlong pointer, jobject buffer) {
   const char *p = (const char *)pointer;
   memcpy(
      (*env)->GetDirectBufferAddress(env, buffer),
      p,
      (*env)->GetDirectBufferCapacity(env, buffer));
}

That's it. When MyConnection.run() is invoked on my KitKat device, the log shows:

I/MyConnection﹕ status 200
I/MyConnection﹕ SSL version 0301
I/MyConnection﹕ Master key 81ef39c5f8f7f796a34b307ff453511378fd081d14c37eb2e912fa829edf280e0fa7a499c370fdc156b8499758373d67
I/MyConnection﹕ Session ID b9ee4ae0c7738909430d47e9b0d6d60420d34a17d08181f21996e55a463aa5cf

I did make a brief attempt with DefaultHttpClient but abandoned it when I couldn't figure out how to access the default SchemeRegistry. I think it can be done by specifying a ClientConnectionManager when constructing the DefaultHttpClient but I didn't feel like pursuing a deprecated path any further. If you want to try then you would likely use a similar approach to intercept the SSLSessionImpl instance handling the connection. This class has a master_secret member so native code would not be required, only reflection (this code path does not use OpenSSL).

Community
  • 1
  • 1
rhashimoto
  • 15,650
  • 2
  • 52
  • 80
1

To add to rhashimoto's answer, here's what I came up with. This approach does not require messing around with JNI (well to be fair, it messes around with an existing JNI interface). And, it only works for OpenSSLSessionImpl.

It also begins with getting the native pointer, but then calls the i2d_SSL_SESSION() method to get the session data in ASN.1 encoding. Finally, it extracts the master secret from the ASN.1 data. This should hopefully be more robust against future OpenSSL versions.

// Returns master secret as byte array, or null if nothing was found.
private static byte[] getMasterSecret(SSLSession sslSession) {
    try {
        // First get sslSessionNativePointer from sslSession (assume it is a com.android.org.conscrypt.OpenSSLSessionImpl)
        Class sslSessionClass = sslSession.getClass();
        Field sslSessionNativePointerField = sslSessionClass.getDeclaredField("sslSessionNativePointer");
        sslSessionNativePointerField.setAccessible(true);
        long sslSessionNativePointer = sslSessionNativePointerField.getLong(sslSession);

        // Then get SSL session object, encoded as ASN.1
        Class<?> nativeCryptoClass = Class.forName("com.android.org.conscrypt.NativeCrypto");
        Method i2d_SSL_SESSION_method = nativeCryptoClass.getMethod("i2d_SSL_SESSION", long.class);
        byte[] sslASN1SessionData = (byte[]) i2d_SSL_SESSION_method.invoke(nativeCryptoClass, sslSessionNativePointer);

        // Parse the ASN.1 data
        ASN1Primitive asn1Primitive = new ASN1InputStream(new ByteArrayInputStream(sslASN1SessionData)).readObject();

        // Get the master secret; blindly assume that the first octet string of 48 bytes is the master secret
        if (asn1Primitive instanceof ASN1Sequence) {
            for (ASN1Encodable item : (ASN1Sequence) asn1Primitive) {
                if (item instanceof ASN1OctetString) {
                    byte[] octets = ((ASN1OctetString) item).getOctets();
                    if (octets.length == 48) {
                        return octets;
                    }
                }
            }
        }

        // Hmm, it failed. Dump all data then.
        Log.w("TAG", "Did not find master secret in ASN.1 data.");
        Log.w("TAG", ASN1Dump.dumpAsString(asn1Primitive, true));
    } catch (IllegalAccessException | ClassNotFoundException | InvocationTargetException | NoSuchMethodException | NoSuchFieldException | IOException e) {
        Log.w("TAG", "Failed to get master secret", e);
    }
    return null;
}
Jesse
  • 349
  • 2
  • 8