Java层SSL通信抓包与溯源

xiaoeryu Lv5

http/https都是建立在tcp/udp之上应用层的协议,SSL/TLS又是HTTPS工作的一个基础,所以对socket进行字段溯源的时候绕不开的一个就是HTTPS协议。但是HTTPS是加密的,另外它中间还有证书验证,所以通过抓包工具无法直接抓到数据包。

在这篇文章中我们先尝试通过源码分析HTTPS的加密流程,获取hook点。通过FridaHook的方式抓到数据包。

这里同样写一个使用okhttp去访问https://www.baidu.com Demo App

环境:

环境跟前两篇文章用的不同,因为pixel XL的手机电池鼓包game over了,可能电脑连接的USB接口上电流不稳定+时间长没拔发热严重的原因。换了一台pixel 5来测试,虚拟机换了kali,因为安装的在Ubuntu上安装了新版Charles界面显示有点问题,还是新版kali更好看点

Android版本:11.0

虚拟机:kali 2023.4

那环境配置不再啰嗦,直接开始调试吧

因为这里都是多线程,所以没法一步一步的单步调试,只能下断点F9执行然后往断点上撞了:因为HTTP/HTTPS无论如何都是要经过socket执行的所以我们这里在connectSocket都下了断点

分析这段代码可知

 try {
   Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
 } catch (ConnectException e) {
   throw new ConnectException("Failed to connect to " + route.socketAddress());
 }
  • 使用前面创建的套接字来尝试连接目标地址

创建缓冲的输入和输出流,方便后续的读写操作

source = Okio.buffer(Okio.source(rawSocket));	// 创建一个带缓冲的输入流
sink = Okio.buffer(Okio.sink(rawSocket));		// 创建一个带缓冲的输出流
  • 使用Okio库为套接字创建输入和输出流缓冲区,我们抓包的目的就是通过hook拿到输入(接收数据)和输出(发送数据),跟进Okio.java分析

分析Okio.java中对输出流缓冲区的操作

  • 这里进行了一些检查判断数据是否为空,不为空就调用write方法循环将数据从buffer写到OutputStream
  • 之后调用OutputStreamflush方法,确保所有数据都已写入底层流
  • 所以我们等下直接去Android 11的源码中去找java.net.SocketinputStream->write()看怎么hook能获取到输出流

分析Okio.java中对输入流缓冲区的操作

  • 这里同样的使用read方法从InputStream读取数据
  • 同样去Android源码中找java.net.SocketinputStream->read()

源码分析

不确定在哪个模块中,直接在Android11的源码中全局搜索

java.net.SocketinputStream->write()
  • 进来之后直接找到了它最终调用的JNI函数socketWrite0()等下直接hook这个函数
java.net.SocketinputStream->read()

基本跟刚刚是一样的流程(代码太长了不好截图,直接贴了下来)

97      private native int socketRead0(FileDescriptor fd,
98                                     byte b[], int off, int len,
99                                     int timeout)
100          throws IOException;
101  
    
。。。。。。
    
115      private int socketRead(FileDescriptor fd,
116                             byte b[], int off, int len,
117                             int timeout)
118          throws IOException {
119          return socketRead0(fd, b, off, len, timeout);
120      }

。。。。。。
    
147      int read(byte b[], int off, int length, int timeout) throws IOException {
148          int n;
149  
150          // EOF already encountered
151          if (eof) {
152              return -1;
153          }
154  
155          // connection reset
156          if (impl.isConnectionReset()) {
157              throw new SocketException("Connection reset");
158          }
159  
160          // bounds check
161          if (length <= 0 || off < 0 || length > b.length - off) {
162              if (length == 0) {
163                  return 0;
164              }
165              throw new ArrayIndexOutOfBoundsException("length == " + length
166                      + " off == " + off + " buffer length == " + b.length);
167          }
168  
169          boolean gotReset = false;
170  
171          // acquire file descriptor and do the read
172          FileDescriptor fd = impl.acquireFD();
173          try {
174              // Android-added: Check BlockGuard policy in read().
175              BlockGuard.getThreadPolicy().onNetwork();
176              n = socketRead(fd, b, off, length, timeout);	// 标记
177              if (n > 0) {
178                  return n;
179              }
180          } catch (ConnectionResetException rstExc) {
181              gotReset = true;
182          } finally {
183              impl.releaseFD();
184          }
185  
186          /*
187           * We receive a "connection reset" but there may be bytes still
188           * buffered on the socket
189           */
190          if (gotReset) {
191              impl.setConnectionResetPending();
192              impl.acquireFD();
193              try {
194                  n = socketRead(fd, b, off, length, timeout);	// 标记
195                  if (n > 0) {
196                      return n;
197                  }
198              } catch (ConnectionResetException rstExc) {
199              } finally {
200                  impl.releaseFD();
201              }
202          }
203  
204          /*
205           * If we get here we are at EOF, the socket has been closed,
206           * or the connection has been reset.
207           */
208          if (impl.isClosedOrPending()) {
209              throw new SocketException("Socket closed");
210          }
211          if (impl.isConnectionResetPending()) {
212              impl.setConnectionReset();
213          }
214          if (impl.isConnectionReset()) {
215              throw new SocketException("Connection reset");
216          }
217          eof = true;
218          return -1;
219      }

FridaHook

hook点

// 发送数据
//  java.net.SocketinputStream->socketRead0()
private native int socketRead0(FileDescriptor fd,
                               byte b[], int off, int len,
                               int timeout)
// 接收数据
// java.net.SocketinputStream->socketRead0()
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
                                 int len) throws IOException;
  • 最后发现这两个hook点跟我们之前Java层socket的TCP hook点是一样的

编写脚本

直接使用原来的脚本稍微修改一下就ok了

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);

}
function printJavaStack(name) {
    Java.perform(function () {
        var Exception = Java.use("java.lang.Exception");
        var ins = Exception.$new("Exception");
        var straces = ins.getStackTrace();
        if (straces != undefined && straces != null) {
            var strace = straces.toString();
            var replaceStr = strace.replace(/,/g, " \n ");
            LogPrint("=============================" + name + " Stack strat=======================");
            LogPrint(replaceStr);
            LogPrint("=============================" + name + " Stack end======================= \n ");
            Exception.$dispose();
        }
    });
}

// 对可打印字符区间进行一个判断只打印可打印字符
function isprintable(value) {
    if (value >= 32 && value <= 126) {
        return true
    }
    return false
}

function hookssl() {
    Java.perform(function () {

        var SocketInputStreamClass = Java.use('java.net.SocketInputStream')
        // hook socketRead0()
        SocketInputStreamClass.socketRead0.implementation = function (arg0, arg1, arg2, arg3, arg4) {
            var size = this.socketRead0(arg0, arg1, arg2, arg3, arg4)
            console.log("[" + Process.getCurrentThreadId() + "]socketRead0 > size: " + size + ",content: " + JSON.stringify(arg1))
            var byteArray = Java.array("byte", arg1)
            var content = '';
            for (var i = 0; i < size; i++) {
                if (isprintable(byteArray[i])) {
                    content = content + String.fromCharCode(byteArray[i])
                }
            }
            var socketimpl = this.impl.value
            var address = socketimpl.address.value
            var port = socketimpl.port.value

            console.log("\naddress:" + address + ",port: " + port + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]receive:" + content);

            printJavaStack('socketRead0()...')
            return size;
        }


        var SocketOutputStreamClass = Java.use('java.net.SocketOutputStream')
        // hook socketWrite0()
        SocketOutputStreamClass.socketWrite0.implementation = function (arg0, arg1, arg2, arg3) {
            var result = this.socketWrite0(arg0, arg1, arg2, arg3)
            console.log("[" + Process.getCurrentThreadId() + "]socketWrite0 > result: " + arg3 + "--content: " + JSON.stringify(arg1))
            var byteArray = Java.array("byte", arg1)
            var content = '';
            for (var i = 0; i < arg3; i++) {
                if (isprintable(byteArray[i])) {
                    content = content + String.fromCharCode(byteArray[i])
                }
            }

            var socketimpl = this.impl.value
            var address = socketimpl.address.value
            var port = socketimpl.port.value

            // console.log("[" + Process.getCurrentThreadId() + "]send: " + content)
            console.log("send address:" + address + ",port: " + port + "[" + Process.getCurrentThreadId() + "]send:" + content);
            console.log("\n" + JSON.stringify(this.socket.value));
            printJavaStack('socketWrite0()...')
            return result;
        }
    })
}

function main() {
    hookssl()
}

setImmediate(main)

执行hook

frida -UF -l hookSSL.js -o OkhttpDemo.log

  • 这里成功的抓到了数据包,也把调用的堆栈打印了出来

对其他App进行抓包

滴答清单

Frida也支持对多进程App进行抓包:UC浏览器

frida -U -p 进程编号 -l hookSSL.js

后记:

分析后对照源码发现8.0的SSL流程和8.0以后的SSL流程完全不一样,8.0最后的JNI函数分别是

com.android.org.conscrypt.NativeCrypto.SSL_read
com.android.org.conscrypt.NativeCrypto.SSL_write

附件:

8.0的hook代码

11.0的hook代码

  • 标题: Java层SSL通信抓包与溯源
  • 作者: xiaoeryu
  • 创建于 : 2024-06-06 22:06:18
  • 更新于 : 2024-06-09 17:29:18
  • 链接: https://github.com/xiaoeryu/2024/06/06/Java层SSL通信抓包与溯源/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论