Java层socket抓包与源码分析(上)

xiaoeryu Lv5

tcp/udp协议以及一些字段的溯源,快速定位一些字段(例如用户名和密码)是怎么加密的,分析出来之后怎么去进行枚举、重放。

典型的应用就是怎么去编写爬虫去爬去例如商城的商品等


问题

接下来几篇文章我们需要解决的问题

如何对自定义协议进行逆向分析?

发送参数被加密,如何快速完成参数处理流程的定位?

加密算法复杂,如何主动调用完成对数据包的处理和重放?

目标

通过分析需要达到的目的

掌握阅读和分析Android系统框架层网络数据包接收发送的源码逻辑

掌握快速进行字段追踪溯源的技巧

掌握基于frida、xposed、AndroidNativeEmu、Unidbg等的主动调用技巧,从而完成对协议的枚举和爆破、甚至是数据的爬取

逆向分析思想

堆栈回溯思想

逐层向上追溯、对参数的处理,关键函数和参数处理放在so中。所以分别在Java和JNI中怎么处理

控制流分析与数据流分析相结合思想

参数处理流程、关键API

关键字符串、关键API定位思想

对这些关键点要格外注意跟Windows分析一样


  • socket协议就在传输层
  • 再上层的就是tcp、udp等协议在应用层

有些App不会自己去调用socket来完成网络请求,因为麻烦。

通过hook** java.net.Socket**背后所封装的关键API,获取信息


创建demo来作为分析案例

这里写一个简单的客户端和服务端的收发数据的demo

服务端:python接收器

客户端:App Demo

要使用的工具安装

安装tcpdump:等下用来抓包测试通信是否畅通

  • 下载tcpdump包 ,将其拷贝到手机adb push tcpdump /data/local/tmp/
  • 设置执行权限chmod 755 tcpdump
  • 添加到环境变量export PATH=$PATH:/data/local/tmp

安装wire shark:Ubuntu自身没有带,如果是kali的话自带的就有

# 更新软件包列表
sudo apt update

# 安装Wireshark
sudo apt install wireshark-qt

# 在安装过程中选择允许非超级用户捕获数据包(如果提示)

# 将当前用户添加到wireshark组
sudo usermod -aG wireshark $(whoami)

使用tcpdump对手机上的所有通信进行抓包然后用wireshark打开分析

  • tcpdump抓包

    adb shell
    su
    cd /data/local/tmp/
    tcpdump -i any -s 0 -w /sdcard/01.pcap
    

然后运行我们demo的客户端和服务端

抓完后用wireshark打开查看,确认数据收发通信正常

把我们抓到的数据包pull下来用wireshark查看wireshark 01.pcap

  • 这里我们的demo没有对数据进行加密,所以抓到的包都是明文的。现在的App一般都会对数据进行加密,不会使用这种http明文传输
  • 抓包看到通信没有问题,接下来调试分析我们的代码,分析合适的hook点

代码分析

调试分析客户端的的源码来获取抓包的hook时机

调试的时候注意下载好对应系统的sdk,在设置>androidSDK里面可以直接下载,具体的配置过程都可以搜索到不在这里啰嗦

  • 下面这段代码分析比较简单也比较啰嗦可以直接看分析结果
构造socket

从socket构造开始调试我们的客户端代码

  • 按F7步入

创建套接字,建立连接

  • socket有很多重载,这里使用的是这四个参数的重载

  • 在for循环中使用工厂模式创建套接字实例setImpl()

    • impl -> java.net.SocksSocketImpl
  • 使用connect建立连接使用的方法

    connect(SocketAddress endpoint)

接收数据

清除其它断点直接断在接收数据处,看看接收数据使用的方法

  • java.net.SocketInputStream.read(byte[])
发送数据

同样直接断下载看发送数据使用的方法

  • java.net.SocketOutputStream.write(byte[])
分析结果

经过分析我们拿到了下面这些hook点

java.net.Socket类构造函数:new Socket(ip, port);

​ private Socket(InetAddress[] addresses, int port, SocketAddress localAddr,boolean stream)

​ 创建套接字:impl -> java.net.SocksSocketImpl

建立连接:connect(SocketAddress endpoint)

接收数据:java.net.SocketInputStream.read(byte[])

发送数据:java.net.SocketOutputStream.write(byte[])

Android源码 分析

接下来分析Android源码,获取我们方才分析拿到的方法,在框架背后的实现

暂时我们的源码分析先到java层结束为止,不进入native层分析。后面如果碰到不使用Java层的API,直接调用libc中的API进行通信的再继续往下分析,当然还有更进一步的跳过libc中的API,直接使用系统调用的情况,如果遇到再进行单独分析

接收数据:java.net.SocketInputStream.read(byte[])
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
->这里的回调指向了下面有三个参数的read函数
    
     public int read(byte b[], int off, int length) throws IOException {
        return read(b, off, length, impl.getTimeout());
    }   
->这里的回调指向了下面有四个参数的read函数
    
        int read(byte b[], int off, int length, int timeout) throws IOException {
        int n;

        // EOF already encountered
        if (eof) {
            return -1;
        }

        // connection reset
        if (impl.isConnectionReset()) {
            throw new SocketException("Connection reset");
        }

        // bounds check
        if (length <= 0 || off < 0 || length > b.length - off) {
            if (length == 0) {
                return 0;
            }
            throw new ArrayIndexOutOfBoundsException("length == " + length
                    + " off == " + off + " buffer length == " + b.length);
        }

        // acquire file descriptor and do the read
        FileDescriptor fd = impl.acquireFD();
        try {
            // Android-added: Check BlockGuard policy in read().
            BlockGuard.getThreadPolicy().onNetwork();
            n = socketRead(fd, b, off, length, timeout);	// 这个函数中时机调用了socketRead()来进行读,跟进去这个函数
            if (n > 0) {
                return n;
            }
        } catch (ConnectionResetException rstExc) {
            impl.setConnectionReset();
        } finally {
            impl.releaseFD();
        }

        /*
         * If we get here we are at EOF, the socket has been closed,
         * or the connection has been reset.
         */
        if (impl.isClosedOrPending()) {
            throw new SocketException("Socket closed");
        }
        if (impl.isConnectionReset()) {
            throw new SocketException("Connection reset");
        }
        eof = true;
        return -1;
    }

>>>跟进上面的socketRead()
        private int socketRead(FileDescriptor fd,
                           byte b[], int off, int len,
                           int timeout)
        throws IOException {
        return socketRead0(fd, b, off, len, timeout);
    }
->这里的回调是socketRead0()
        private native int socketRead0(FileDescriptor fd,
                                   byte b[], int off, int len,
                                   int timeout)	// 可以看到这个socketRead0是一个JNI函数,TCP协议接收处理的框架Java层到这里就结束了
  • 接收数据的调用链
发送数据:java.net.SocketOutputStream.write(byte[])
    public void write(byte b[]) throws IOException {
        socketWrite(b, 0, b.length);	// 跟进回调
    }
->
        private void socketWrite(byte b[], int off, int len) throws IOException {


        if (len <= 0 || off < 0 || len > b.length - off) {
            if (len == 0) {
                return;
            }
            throw new ArrayIndexOutOfBoundsException("len == " + len
                    + " off == " + off + " buffer length == " + b.length);
        }

        FileDescriptor fd = impl.acquireFD();
        try {
            // Android-added: Check BlockGuard policy in socketWrite.
            BlockGuard.getThreadPolicy().onNetwork();
            socketWrite0(fd, b, off, len);	// 继续往下跟进
        } catch (SocketException se) {
            if (impl.isClosedOrPending()) {
                throw new SocketException("Socket closed");
            } else {
                throw se;
            }
        } finally {
            impl.releaseFD();
        }
    }
->
        private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
                                     int len) throws IOException;	// 到这里跟接收数据的代码一样,Java层的代码调用就结束了
  • 发送数据的调用链

根据对Android源码的分析,我们hook刚才框架层调用链中的任何一个函数都可以得到它发送和接收的原始数据包,当然这里通过hook拿到的数据可能是已经加密处理过的数据

接下来就根据我们之前调试代码的时候分析到这些点来编写一个简单的hook代码

function hooktcp() {
    Java.perform(function () {
        var SocketClass = Java.use('java.net.Socket')
        SocketClass.$init.overload('java.lang.String', 'int').implementation = function (arg0, arg1) {
            console.log("[" + Process.getCurrentThreadId() + "]new Socket connection: " + arg0 + "port: " + arg1)
            return this.$init(arg0, arg1)
        }

        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)
            return size;
        }


        var SocketOutputStreamClass = Java.use('java.net.SocketOutputStream')
        // hook socketWrite0()
        SocketOutputStreamClass.socketWrite0.implementation = function (arg0, arg1, arg2, arg3) {
            var size = this.socketWrite0(arg0, arg1, arg2, arg3)
            console.log("[" + Process.getCurrentThreadId() + "]socketWrite0 > size: " + arg3 + "--content: " + JSON.stringify(arg1))
            return size;
        }
    })
}

function main() {
    hooktcp()
}

setImmediate(main)
  • hook发送和接收数据的点选择了Java层调用链最后的JNI函数

执行结果:打印出了调用过程中传递的参数

通过hook成功的获取了通信的数据包,同样的在这个过程中如果把堆栈打印出来也就能成功的定位到是哪个地方发起了通信请求

添加堆栈打印代码:

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

执行后的打印结果:

  • 打印出来了堆栈信息,代码中还添加了打印出接收到的信息并处理了一下转为字符串
打印IP地址

通过调试断在发送和接收数据的地方,看他的IP地址在那个字段中放着

  • 从这个地方拿到通信对端的IP和端口

添加frida脚本代码

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

发送的时候也是一样

脚本跟刚才的一摸一样都不用变

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

添加完之后再将这些信息再打印出来

执行脚本
  • 可以看到ip和端口都打印出来了
拿其它的App试一下

例如测试了嘿嘿连载和咸鱼的App

  • 都可以成功拿到ip和端口
附件:

本章写的frida-socket抓包脚本

  • 标题: Java层socket抓包与源码分析(上)
  • 作者: xiaoeryu
  • 创建于 : 2024-06-01 10:26:37
  • 更新于 : 2024-06-06 06:05:16
  • 链接: https://github.com/xiaoeryu/2024/06/01/Java层socket抓包与源码分析(上)/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论