点击蓝字 关注我们
Android LocalSocket
安全风险探讨
简介
在Android系统中,进行进程间通信(IPC)时,我们经常使用AIDL。然而,由于Binder机制的限制,AIDL无法传输大量数据。为了解决这一问题,我们需要寻求其他方法来传输大数据。LocalSocket为我们提供了一个可行的解决方案。
Android LocalSocket基于UNIX-domain Socket(UDS),这是一种在Socket基础上衍生出来的IPC通信机制。因此,LocalSocket解决了同一台主机上不同进程间的通信问题。与网络通信使用的socket相比,LocalSocket不需要经过网络协议栈,不需要进行打包和拆包,也不需要进行校验计算,这使得LocalSocket的执行效率更高。
本文主要简单介绍Android LocalSocket技术原理以及其在实际应用可能存在的潜在风险,通过实践的案例展示其潜在的攻击场景。
基本概念
Socket通信模型
通过之前的简介我们知道UDS并非基于网络协议,所有通信过程均在内核中完成。使用LocalSocket进行数据传输的简单步骤如下:
1. 在服务端应用程序中创建一个ServerSocket对象,并指定要监听的端口号。
2. 在客户端应用程序中创建一个Socket对象,并连接到服务端应用程序的IP地址和端口号。
3. 通过输入流和输出流在客户端和服务端之间传输数据。
4. 关闭连接。
Android LocalSocket 的TCP通信基本流程参考下图:
数据流参考图:
Android LocalSocket实现类
LocalSocket
在Unix域名空间创建一个套接字(非服务端)。是对Linux中Socket进行了封装,采用JNI方式调用,实现进程间通信。具体就是Native层Server和Framework层Client之间进行通信,或在各层次中能使用Client/Server模式实现通信。
LocalServerSocket
创建服务器端Unix域套接字,与LocalSocket对应。
LocalSocketImpl
Framework层Socket的实现,通过JNI调用系统socket API。
LocalSocketAddress
Unix域socket的地址以及所处的空间。
JNI访问接口:
frameworksbasecorejniandroid_net_LocalSocketImpl.cpp
socket_create
socket_connect_local
socket_bind_local
socket_listen
……
通过关系类图发现,使用Android的LocalSocket建立socket通信,是基于网络socket过程一致的:
Socket类型和命名空间
UDS有3种Socket类型,分别为SOCK_STREAM、SOCK_DGRAM和SOCK_SEQPACKET。其中SOCK_STREAM是面向数据流的类型,类似于管道。SOCK_DGRAM是面向数据包的类型,类似于消息队列。SOCK_SEQPACKET是面向连接的类型,保留了消息边界。
每个UDS都拥有一个名称。既可以使用FILESYSTEM命名空间,将名称绑定到一个文件系统路径,也可以使用与文件系统的无关的ABSTRACT命名空间。
LocalSocket的安全性
使用说明
我们可以尝试编写一个简单的模拟程序,模拟基于Android LocalSocket的APP:
//客户端
private void demoClient() throws IOException {
LocalSocket client = new LocalSocket();
client.connect(new LocalSocketAddress("@secret"));
client.getOutputStream().write(520);
int read = client.getInputStream().read();
Log.d(TAG, "response from server : " + read);
client.close();
}
//服务端,不安全的代码,缺乏有效的鉴权机制
private void demoServer() throws IOException {
LocalServerSocket server = new LocalServerSocket("@secret");
LocalSocket client = server.accept();
int read = client.getInputStream().read();
Log.d(TAG, "request from client :" + read);
client.getOutputStream().write(read + 1314);
client.getOutputStream().flush();
client.close();
}
// 打印
// request from client :520
// response from server : 5201314
安全风险探讨
常用的Binder的跨进程安全性由系统实现的鉴权机制来保证,LocalSocket作为Unix domain socket的封装,我们必须考虑它的安全性问题。
从上述代码实例中我们明显看出由于LocalSocket本身缺乏鉴权机制,任意一个应用都可以进行连接,从而截取到数据或是向接收端传递非法数据引发异常。针对这个特点,常见防御方法有两种:
1. 随机化LocalSocket的命名,例如使用当前程序的AppId和用户uin等信息计算md5作为LocalSocket的名字,使得攻击者无法通过固定或穷举名字的方法尝试建立连接。
2. 引入鉴权机制,在连接成功后发送特定的随机信息来验证对方的真实性,然后才启动真正的数据传输。
为了防止攻击者通过固定或穷举名字的方法尝试建立连接,可以使用当前程序的AppId、用户uin等信息计算md5作为LocalSocket的名字:
private static String generateRandomSocketName(String appId, String uin) {
String randomString = appId + uin;
MessageDigest md = null;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
md.update(randomString.getBytes());
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
在连接成功后,可以发送一个随机的字符串(比如一个随机数或者一个随机的token)来验证对方的真实性。然后,只有当这个随机字符串被正确地返回时,才启动真正的数据传输:
在客户端:
private void demoClient() throws IOException {
LocalSocket client = new LocalSocket();
client.connect(new LocalSocketAddress(generateRandomSocketName(getPackageName(), uin), LocalSocketAddress.Namespace.RESERVED));
OutputStream outputStream = client.getOutputStream();
outputStream.write(520); //假设520是一个预定义的命令或标识符
outputStream.write(generateRandomString().getBytes()); // 发送随机字符串作为鉴权信息
InputStream inputStream = client.getInputStream();
int read = inputStream.read();
if (read == -1) {
Log.d(TAG, "Connection closed by server");
client.close();
return;
} else if (read != generateRandomString().hashCode()) {
Log.d(TAG, "Invalid authentication response from server");
client.close();
return;
} else {
Log.d(TAG, "Authentication successful"); // 如果鉴权成功,可以继续正常的数据传输。
// ... 数据传输代码 ...
client.close();
}
}
在服务端:
private void demoServer() throws IOException {
LocalServerSocket server = new LocalServerSocket(generateRandomSocketName(getPackageName(), uin), LocalSocketAddress.Namespace.RESERVED);
LocalSocket client = server.accept();
InputStream inputStream = client.getInputStream();
int command = inputStream.read();
if (command != 520) { // 假设520是一个预定义的命令或标识符,表示需要进行鉴权。
Log.d(TAG, "Invalid command from client");
client.close();
return;
} else {
String randomString = generateRandomString(); // 生成一个随机字符串作为鉴权信息。
OutputStream outputStream = client.getOutputStream();
outputStream.write(randomString.getBytes());
outputStream.flush();
int response = inputStream.read();
if (response == -1) {
Log.d(TAG, "Connection closed by client");
client.close();
return;
} else if (response != randomString.hashCode()) {
Log.d(TAG, "Invalid authentication response from client");
client.close();
return;
} else {
Log.d(TAG, "Authentication successful");
// 如果鉴权成功,可以继续正常的数据传输。
// ... 数据传输代码 ...
client.close();
}
}
}
参考资料
https://zhuanlan.zhihu.com/p/620136976?utm_id=0
https://www.cnblogs.com/bastard/archive/2012/10/09/2717052.html
https://blog.csdn.net/weixin_39173183/article/details/108377681
https://blog.51cto.com/u_13657808/5658169
https://www.jianshu.com/p/dc87aceabc4c
https://jiayunhan.github.io/material/misuse_ccs16.pdf
https://developer.baidu.com/article/detail.html?id=295123
END
往期精彩合集
长
按
关
注
联想GIC全球安全实验室(中国)
原文始发于微信公众号(联想全球安全实验室):Android LocalSocket 安全风险探讨