移动端安全 安卓java层socket hook点分析
北海 发表于 广东 移动安全 695浏览 · 2024-10-15 03:35

前言

Socket提供了一种通用的网络通信接口。对于安卓app自定义协议来说,Socket可以作为数据传输的基础通道。我们可以通过分析Socket源码,来寻找hook点,从而可以定位通信过程中一些关键的加密api和潜在的漏洞点,发现更多攻击面

编写socket的demo

首先,我们先写简单的socket通信demo以供后面的hook点分析。socket作为传输层及以下网络通信的一种抽象接口和编程工具,可以使用传输层的TCP和UDP协议。
socket基本实现过程如下:

  1. 创建socket
  2. 绑定地址和端口
  3. 进行连接(对于TCP)或直接发送数据(对于UDP)
  4. 数据传输
  5. 关闭socket

下面我们采用寒冰大佬的socket demo
(我们的app作为客户端,然后在一台可以与app所在设备互通的设备运行python脚本开启socket服务端。我这里使用雷电模拟器和云主机作为载体)

tcp client

import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Date;

public class TcpClient {

    public static String ip = "x.x.x.x";
    public static int port = xxxx;
    public static boolean connected = false;
    public static Socket socket = null;
    public static OutputStream outputstream = null;
    public static InputStream inputStream = null;
    public static long lastheartresponse = 0;

    public static void start() {
        servicethread();
    }

    public static Long getTimestamp() {
        Date date = new Date();
        if (null == date) {
            return (long) 0;
        }
        String timestamp = String.valueOf(date.getTime());
        return Long.valueOf(timestamp);
    }

    public static void close() {
        try {
            if (socket != null) {
                socket.close();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        inputStream = null;
        outputstream = null;
        connected = false;
    }

    public static void sendmsg(final String msg) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (connected == false) {
                } else {
                    try {
                        if (outputstream != null) {
                            String crypt = msg;
                            outputstream.write(crypt.getBytes("utf-8"));
                            outputstream.flush();
                        }

                    } catch (IOException e) {
                        close();
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }

    public static void heartthread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    long currenttime = getTimestamp();
                    if (lastheartresponse != 0) {
                        long offset = currenttime - lastheartresponse;
                        int seconds = (int) (offset / 1000);
                        if (seconds > 10) {
                            close();
                        }
                    }
                    try {
                        Thread.currentThread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }

            }
        }).start();
    }

    public static void receivethread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int arraysize = 1024;
                byte[] content = new byte[arraysize];
                while (true) {
                    if (inputStream != null) {
                        try {
                            int count = inputStream.read(content);
                            if (count > 0 && count < arraysize) {
                                byte[] tmparray = new byte[count];
                                System.arraycopy(content, 0, tmparray, 0, count);
                                String str = new String(tmparray, "utf-8");

                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                            close();
                            break;
                        }
                    } else {
                        close();
                        break;
                    }
                }
            }
        }).start();
    }

    public static void servicethread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                heartthread();
                while (true) {
                    if (connected == false) {
                        try {
                            socket = new Socket(ip, port);
                            socket.setSoTimeout(10*1000);
                            connected = true;
                            outputstream = socket.getOutputStream();
                            inputStream = socket.getInputStream();
                            receivethread();
                        } catch (IOException e) {
                            e.printStackTrace();
                            connected = false;
                            socket = null;
                            outputstream = null;
                            inputStream = null;
                        }
                    }
                    if (outputstream != null) {
                        try {
                            JSONObject object = new JSONObject();
                            object.put("msgtype", "heart");
                            sendmsg(object.toString());
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        Thread.currentThread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        }).start();
    }
}
ip:静态字符串变量,存储要连接的服务器 IP 地址。
port:静态整数变量,存储要连接的服务器端口号。
connected:静态布尔变量,用于表示当前是否与服务器连接成功。
socket:静态Socket对象,代表与服务器的连接套接字。
outputstream:静态OutputStream对象,用于向服务器发送数据。
inputStream:静态InputStream对象,用于从服务器接收数据。
lastheartresponse:静态长整型变量,可能用于记录心跳响应的时间戳。
start():调用servicethread()方法启动与服务器的连接和通信相关的操作

getTimestamp():获取当前时间的时间戳

close():尝试关闭socket连接,并将相关的输入输出流和连接状态变量重置为初始状态

sendmsg(String msg):简单来说是将给定的消息字符串按照 UTF-8 编码转换为字节数组,并写入输出流,然后刷新输出流

heartthread():在一个新线程中不断运行,检查上一次心跳响应的时间与当前时间的差值

receivethread():简单来说就是在一个新线程中不断尝试从输入流读取数据。如果读取成功,将读取到的数据转换为字符串

servicethread():简单来说就是先处理心跳机制,并不断检查连接状态,如果未连接,则尝试创建与服务器的连接,如果连接成功且输出流不为空,构造一个包含 “msgtype” 为 “heart” 的 JSON 对象,并发送该消息

tcp server

import socket

# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址和端口
server_address = ('0.0.0.0', 9999)
server_socket.bind(server_address)

# 开始监听连接
server_socket.listen(1)
print("等待连接...")

while True:
    # 接受连接
    connection, client_address = server_socket.accept()
    print(f"连接来自:{client_address}")
    try:
        while True:
            data = connection.recv(1024)
            if data:
                print(f"接收到数据:{data.decode()}")
                connection.sendall(b"ok")
            else:
                break
    finally:
        # 关闭连接
        connection.close()

udp client

package com.demo.myapplication;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;


public class UdpClient {
    public static final String DEST_IP = "x.x.x.x";
    public static final int DEST_PORT = xxxx;
    public static final int DATA_LEN = 4096;
    public static byte[] inBuff = new byte[DATA_LEN];
    public static DatagramSocket socket;

    static {
        try {
            socket = new DatagramSocket();
            receivethread();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    public static DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
    public static DatagramPacket outPacket = null;

    public static void udpsend(String content) {
        try {
            outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName(DEST_IP), DEST_PORT);
            byte[] buff = content.getBytes();
            outPacket.setData(buff);
            socket.send(outPacket);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void receivethread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        socket.receive(inPacket);
                        Log.i("udpreceive", new String(inBuff, 0, inPacket.getLength()));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public static void start() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.currentThread().sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    udpsend("i am from udpclient!");
                }
            }
        }).start();

    }
}
DEST_IP:静态字符串常量,代表目标服务器的 IP 地址。
DEST_PORT:静态整数常量,代表目标服务器的端口号。
DATA_LEN:静态整数常量,定义接收和发送数据的缓冲区大小。
inBuff:静态字节数组,用于存储接收到的数据。
socket:静态DatagramSocket对象,用于发送和接收 UDP 数据包。
inPacket:静态DatagramPacket对象,用于接收数据的数据包。
outPacket:静态DatagramPacket对象,用于发送数据的数据包,初始化为指向一个空数据和长度为 0 的数据包。
udpsend():发送 UDP 数据包

receivethread():主要实现在一个新线程中不断循环接收 UDP 数据包的目的

start():在一个新线程中不断循环调用udpsend函数,每隔 1 秒发送一条内容为 “i am from udpclient!” 的 UDP 数据包

udp server

import socket

# 创建 UDP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定地址和端口
server_address = ('0.0.0.0', 7777)
server_socket.bind(server_address)

print("等待数据...")

while True:
    data, client_address = server_socket.recvfrom(1024)
    print(f"接收到数据来自:{client_address},数据为:{data.decode()}")
    # 发送响应
    response = b"ok"
    server_socket.sendto(response, client_address)

运行效果

编辑activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/tcpButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动 TCP 客户端"
        android:layout_centerHorizontal="true" />

    <Button
        android:id="@+id/udpButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动 UDP 客户端"
        android:layout_below="@id/tcpButton"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

编写MainActivity

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button tcpButton = findViewById(R.id.tcpButton);
        tcpButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TcpClient.start();
            }
        });

        Button udpButton = findViewById(R.id.udpButton);
        udpButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                UdpClient.start();
            }
        });
    }
}


运行app

运行tcp客户端,服务端

效果

运行udp客户端,服务端

效果

分析hook点

demo已经有了,我们接下来分析hook点
本文的分析角度重点放在发送数据和接收数据,所以先要在代码中寻找发送数据和接收数据的api

tcp

发送

通过调试可以发现,tcpclient运行几个关键步骤为
从start()进入,然后在servicethread()创建socket对象

接着通过sendmsg()发送消息


在sendmsg()中又会调用write()函数


可以发现write函数是在源码java.net.SocketOutputStream中的

接着可以在安卓源码中搜索,由于我这个模拟器是安卓9的,于是下面也都选择android9的源码

键入socketWrite()函数


发现里面还调用了socketWrite0()函数


显然这是一个jni函数,本文仅分析java层socket的hook点,因此socketWrite0()可以作为终点

于是整体的调用链为

java.net.SocketOutputStream.write(byte b[]) ——>
java.net.SocketOutputStream.socketWrite(byte b[], int off, int len) ——>
java.net.SocketOutputStream.socketWrite0(FileDescriptor fd, byte[] b, int off,int len)

接收

接着我们分析一下接收部分的调用链


可以看到read()函数是在java.net.SocketInputStream中

源码中搜索


接着寻找调用链的方法就跟上面一样了(以下图的顺序就是调用的顺序)


最后这个socketRead0显示又是jni函数,于是到了终点

总结一下调用链

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

udp

发送

我们从send()函数开始

step into send()函数,发现调用栈:java.net.DatagramSocket->java.net.PlainDatagramSocketImpl->libcore.io.IoBridge.sendto

在源码中搜索PlainDatagramSocketImpl


搜索send函数


进入IoBridge.sendto函数,发现里面存在libcore.os.sendto()的调用


在这里我直接键入是会出现下图的情况,搜索出多处sendto,但是我们无法确定具体是哪一个


回到libcore.os.sendto()的调用处,我们要确认是调用哪个类对象的sendto函数,显然是libcore.os


搜索Libcore,进入查看


查看BlockGuardOs类


在里面搜索sendto,发现里面继续调用了os属性的sento方法

向上拉至构造函数处,发现BlockGuardOs继承自ForwardingOs,我们进入ForwardingOs观察os属性的初始化,发现os来自传入的参数


于是我们回到libcore.io.Libcore,发现传入的参数为rawOs,我们继续追溯rawOs,

进入Linux,搜索sendto函数,发现里面都调用了sendtoBytes


我们键入sendtoBytes,发现它是jni函数,说明java层调用链的终点就是这儿了

下面来总结一下调用链

java.net.DatagramSocket.send(DatagramPacket p)——>

java.net.PlainDatagramSocketImpl.send(DatagramPacket p)——>

libcore.io.IoBridge.sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)——>

libcore.io.Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, inetAddress, port)——>

libcore.io.BlockGuardOs.os.sendto(fd, bytes, byteOffset, byteCount, flags, inetAddress, port)——>

libcore.io.ForwardingOs.os.sendto.(fd, bytes, byteOffset, byteCount, flags, inetAddress, port)——>

libcore.io.Linux.sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)——>

libcore.io.Linux.sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)  jni函数

这条调用链的每个节点都可能是可以用于发现关键api的hook点

接收

udp接收调用链的追踪思路与udp发送调用链追踪思路相似,这里就不进行赘述了
最后定位到的jni函数为libcore.io.Linux.recvfromBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException

hook代码

直接hook最后的这两个JNI函数,并且把其中传递的buf以及IP等信息打印出来

function hookudp() {
    Java.perform(function () {
        var LinuxClass = Java.use('libcore.io.Linux')
        // 数据接收
        // private native int recvfromBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException;
        LinuxClass.recvfromBytes.implementation = function (arg0, arg1, arg2, arg3, arg4,arg5) {
            var size = this.recvfromBytes(arg0, arg1, arg2, arg3, arg4, arg5)

            var byteArray = Java.array('byte', arg1)
            var content = ""
            for(var i = 0; i < size; i++){
                content = content + String.fromCharCode(byteArray[i])
            }
            console.log("address" + arg5 + "[" + Process.getCurrentThreadId() + "]socketRead0 > size: " + size + "--content: " + content)

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

        // 发送数据这里使用的是七个参数的重载
        // private native int sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port) throws ErrnoException, SocketException;
        LinuxClass.sendtoBytes.overload('java.io.FileDescriptor', 'java.lang.Object', 'int', 'int', 'int', 'java.net.InetAddress', 'int').implementation = function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
            var size = this.sendtoBytes(arg0, arg1, arg2, arg3, arg4, arg5, arg6)

            var byteArray = Java.array('byte', arg1)
            var content = "";
            for(var i=0; i<size; i++){
                content = content + String.fromCharCode(byteArray[i])
            }
            console.log("address" + arg5 + ":" + arg6 + "[" + Process.getCurrentThreadId() + "]sendtoBytes > len: " + size + "--content: " + content)

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

运行效果

tcp

udp

参考文章

https://xiaoeeyu.github.io/2024/06/01/Java%E5%B1%82socket%E6%8A%93%E5%8C%85%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8A%EF%BC%89/
https://xiaoeeyu.github.io/2024/06/02/Java%E5%B1%82socket%E6%8A%93%E5%8C%85%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8B%EF%BC%89/

0 条评论
某人
表情
可输入 255