【问题标题】:Searching for SessionID, SessionKey in a HTTPS Session在 HTTPS 会话中搜索 SessionID、SessionKey
【发布时间】:2015-05-29 12:49:32
【问题描述】:

我正在尝试找到获取 SessionID 以及更重要的是 SessionKey 的解决方案。我已经找到了一个基于 Java 的解决方案:

http://jsslkeylog.sourceforge.net

它使用以下类来记录 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");
}
}

在我的 android 应用程序中,我使用 DefaultHttpClient (org.apache.http.impl.client) 来建立 HTTPS 连接。对于这个连接,我试图找到 SessionKey。有人知道是否可以使用 android / java 方法读出密钥?如果没有,有人知道密钥生成在哪里实现吗?

【问题讨论】:

    标签: java


    【解决方案1】:

    我不相信这可以通过公共 API 完成。您可以获取会话ID,但没有公共接口来获取密钥。

    但是,我能够结合使用反射和本机代码来访问底层 OpenSSL struct,它确实包含会话 ID 和主密钥。所以这是可能的,但它根本不安全,因为隐藏的成员和库不能保证保持不变。事实上,看起来 OpenSSL 主分支上的结构布局已经改变,所以如果/当它被拉入 Android 时,下面的解析代码将需要更新。

    我使用URL.openConnection() 而不是DefaultHttpClient 来建立HTTPS 连接,因为后者现在已被弃用。这是调用URL.openConnection() 并替换默认SSLSocketFactory 的类(这里没什么有趣的):

    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();
    
          }
       }
    }
    

    这里是自定义的SSLSocketFactory,大部分魔法都在这里。它所做的只是将重写的方法转发给真正的SSLSocketFactory,缓存创建的SSLSocket 实例。还有两个新的(非覆盖)方法 - 下面进一步显示的本机方法和 getSessionInfo(),它使用 SSLSocket 上的反射来获取本机 OpenSSL ssl_session_st 指针并解析(和日志)感兴趣的字段。请注意,您可以使用支持的SSLSession.getId() 获取会话 ID;它得到了需要偷偷摸摸的关键。

    // 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_;
       }
    }
    

    最后,这里是本机 C 代码,它可以将内存从本机指针读入 ByteBuffer。这需要使用 Android NDK 构建并加载,如 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));
    }
    

    就是这样。在我的 KitKat 设备上调用 MyConnection.run() 时,日志显示:

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

    我确实对DefaultHttpClient 进行了一次简短的尝试,但是当我无法弄清楚如何访问默认的SchemeRegistry 时放弃了它。我认为可以通过在构造DefaultHttpClient 时指定ClientConnectionManager 来完成,但我不想再追求弃用的路径。如果您想尝试,那么您可能会使用类似的方法来拦截处理连接的SSLSessionImpl 实例。这个类有一个master_secret 成员,所以不需要本地代码,只需要反射(这个代码路径不使用 OpenSSL)。

    【讨论】:

      【解决方案2】:

      为了补充 rhashimoto 的答案,这就是我想出的。这种方法不需要弄乱 JNI(公平地说,它弄乱了现有的 JNI 接口)。而且,它只适用于OpenSSLSessionImpl

      它也是从获取本机指针开始,然后调用i2d_SSL_SESSION() 方法来获取ASN.1 编码的会话数据。最后,它从 ASN.1 数据中提取主密钥。这应该有望对未来的 OpenSSL 版本更加稳健。

      // 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;
      }
      

      【讨论】:

        猜你喜欢
        • 2017-02-05
        • 2011-11-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多