前言
Socket提供了一种通用的网络通信接口。对于安卓app自定义协议来说,Socket可以作为数据传输的基础通道。我们可以通过分析Socket源码,来寻找hook点,从而可以定位通信过程中一些关键的加密api和潜在的漏洞点,发现更多攻击面
编写socket的demo
首先,我们先写简单的socket通信demo以供后面的hook点分析。socket作为传输层及以下网络通信的一种抽象接口和编程工具,可以使用传输层的TCP和UDP协议。
socket基本实现过程如下:
- 创建socket
- 绑定地址和端口
- 进行连接(对于TCP)或直接发送数据(对于UDP)
- 数据传输
- 关闭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/