前言
前两周看到M01N Team公众号发布的《内存马的攻防博弈之旅之gRPC内存马》,文中介绍了gRPC内存马是如何注入进去并执行命令的,但是由于原文当中只给出靶场的Demo,并未给出利用注入的poc,便借助这篇文章再深入研究一下。
gRPC介绍
了解gRPC之前,就需要引入RPC的设计理念,才能更好的理解gRPC的工作原理。
远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许一台计算上的程序调用另一台计算机上运行的程序,使得程序员无需再做额外的操作。如果是面向对象的场景,也可以称作为远程方法调用,比如熟知的Java RMI(Remote Method Invocation)调用。
而gRPC是由Google开发的一款高性能的开源RPC框架,经常用于微服务之间各种不同语言的程序调用函数和通信,大大的增加了微服务之间的通信效率和平台依赖性。同时gRPC是使用Protocol buffers作为接口定义语言(IDL),可以通过编写的proto文件来定义消息结构体和RPC远程调用函数。
协调的接口是通过proto文件来定义的消息结构,相关文档可以在Reference[1]中找到。再来看看gRPC的接口定义语言Protocol Buffers的工作流程图:
结合后续的案例说明,proto文件定义好之后需要通过生成器生成对应语言的代码,并在项目中使用才可以建立gRPC调用。
案例说明
这里直接用绿盟星云实验室开源的gRPC靶场来研究:https://github.com/snailll/gRPCDemo
首先直接看看他的user.proto是如何定义的
syntax = "proto3";
package protocol;
option go_package = "protocol";
option java_multiple_files = true;
option java_package = "com.demo.shell.protocol";
message User {
int32 userId = 1;
string username = 2;
sint32 age = 3;
string name = 4;
}
service UserService {
rpc getUser (User) returns (User) {}
rpc getUsers (User) returns (stream User) {}
rpc saveUsers (stream User) returns (User) {}
}
可以看到文件中定义了go_package和java_package两个变量,用处是明确指出包的命名空间,防止与其他语言的名称冲突。而java_multiple_files = true 选项则是允许为每个生成的类,生成一个单独的 .java 文件。
定义好了proto文件之后,就可以通过protoc或者maven的插件来生成grpc代码,这里我用的protoc二进制文件和插件protoc-gen-grpc来生成。
protoc下载地址:https://github.com/protocolbuffers/protobuf/releases
protoc-gen-grpc插件下载地址:https://repo.maven.apache.org/maven2/io/grpc/protoc-gen-grpc-java/
用下列两个命令生成对应的Java代码文件:
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
这里的grpc插件一定要重新命名为"protoc-gen-grpc-java",不然会显示找不到命令。
之后会在codes文件中生成对象关系的java文件,code文件夹中生成grpc相关的UserServiceGrpc.java文件。
把生成好的Java文件添加到开发的项目中,并新建一个UserServiceImpl类,用来实现grpc的方法。
package com.demo.shell.service;
import com.demo.shell.protocol.User;
import com.demo.shell.protocol.UserServiceGrpc;
import io.grpc.stub.StreamObserver;
/**
* @author demo
* @date 2022/11/27
*/
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(User request, StreamObserver<User> responseObserver) {
System.out.println(request);
User user = User.newBuilder()
.setName("response name")
.build();
responseObserver.onNext(user);
responseObserver.onCompleted();
}
@Override
public void getUsers(User request, StreamObserver<User> responseObserver) {
System.out.println("get users");
System.out.println(request);
User user = User.newBuilder()
.setName("user1")
.build();
User user2 = User.newBuilder()
.setName("user2")
.build();
responseObserver.onNext(user);
responseObserver.onNext(user2);
responseObserver.onCompleted();
}
@Override
public StreamObserver<User> saveUsers(StreamObserver<User> responseObserver) {
return new StreamObserver<User>() {
@Override
public void onNext(User user) {
System.out.println("get saveUsers list ---->");
System.out.println(user);
}
@Override
public void onError(Throwable throwable) {
System.out.println("saveUsers error " + throwable.getMessage());
}
@Override
public void onCompleted() {
User user = User.newBuilder()
.setName("saveUsers user1")
.build();
responseObserver.onNext(user);
responseObserver.onCompleted();
}
};
}
}
在创建一个Main方法启动Netty服务
public static void main(String[] args) throws Exception {
int port = 8082;
Server server = NettyServerBuilder
.forPort(port)
.addService(new UserServiceImpl())
.build()
.start();
System.out.println("server started, port : " + port);
server.awaitTermination();
}
再编写客户端调用服务器方法
package com.demo.shell.test;
import com.demo.shell.protocol.User;
import com.demo.shell.protocol.UserServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.Iterator;
/**
* @author demo
* @date 2022/11/27
*/
public class NsTest {
public static void main(String[] args) {
User user = User.newBuilder()
.setUserId(100)
.build();
String host = "127.0.0.1";
int port = 8082;
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
UserServiceGrpc.UserServiceBlockingStub userServiceBlockingStub = UserServiceGrpc.newBlockingStub(channel);
User responseUser = userServiceBlockingStub.getUser(user);
System.out.println(responseUser);
Iterator<User> users = userServiceBlockingStub.getUsers(user);
while (users.hasNext()) {
System.out.println(users.next());
}
channel.shutdown();
}
}
服务器输出对应的参数请求内容
gRPC内存马实现原理
先从服务端启动来看看UserServiceImpl是如何注册的
int port = 8082;
Server server = NettyServerBuilder
.forPort(port)
.addService(new UserServiceImpl())
.build()
.start();
forPort这里只是新建了一个NettyServerBuilder类,并设置了启动服务需要绑定的端口。
而到addService方法中,新建的UserServiceImpl类作为参数传递进了方法体中
public T addService(BindableService bindableService) {
this.delegate().addService(bindableService);
return this.thisT();
}
代码中的this.delegate()就是io.grpc.internal.ServerImplBuilder类
跟进查看
看到addService方法中添加的其实是bindService的返回值。
这里的正好是之前grpc插件生成的UserServiceGrpc类
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
.addMethod(
getGetUserMethod(),
io.grpc.stub.ServerCalls.asyncUnaryCall(
new MethodHandlers<
com.demo.shell.protocol.User,
com.demo.shell.protocol.User>(
this, METHODID_GET_USER)))
.addMethod(
getGetUsersMethod(),
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
new MethodHandlers<
com.demo.shell.protocol.User,
com.demo.shell.protocol.User>(
this, METHODID_GET_USERS)))
.addMethod(
getSaveUsersMethod(),
io.grpc.stub.ServerCalls.asyncClientStreamingCall(
new MethodHandlers<
com.demo.shell.protocol.User,
com.demo.shell.protocol.User>(
this, METHODID_SAVE_USERS)))
.build();
}
里面的代码正好对应proto文件中定义的三个方法名
addService添加了需要注册的方法,之后就是通过Build方法编译好且设置不可修改。
public Server build() {
return new ServerImpl(this, this.clientTransportServersBuilder.buildClientTransportServers(this.getTracerFactories()), Context.ROOT);
}
Build方法中创建了ServerImpl对象,再来看看ServerImpl对象的构造方法
ServerImpl(ServerImplBuilder builder, InternalServer transportServer, Context rootContext) {
this.executorPool = (ObjectPool)Preconditions.checkNotNull(builder.executorPool, "executorPool");
this.registry = (HandlerRegistry)Preconditions.checkNotNull(builder.registryBuilder.build(), "registryBuilder");
...
}
主要是关注builder.registryBuilder.build()方法,进入的正好是io.grpc.internal.InternalHandlerRegistry$Builder类的build方法。
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
0
最后返回的Collections.unmodifiableList和Collections.unmodifiableMap,就是将list列表和map转换成无法修改的对象,因此注册的UserServiceImpl对象中的方法从一开始就确定了。
至此,内存马的实现步骤就可以得知,需要通过反射重新定义ServerImpl对象中的this.registry值,添加进我们内存马的ServerServiceDefinition和ServerMethodDefinition。
内存马注入
由于M01N Team公众号中并未直接给出poc利用,这里我也只能凭借自己的想法慢慢复现。
由于需用反射替换掉原先被设置unmodifiable的ServerServiceDefinition和ServerMethodDefinition,因此就需要ServerImpl对象的句柄。
由于ServerImpl并不是静态的类,需要获取的字段也不是静态的,因此要获取到JVM中ServerImpl的类,可目前为止我没有想到有什么很好的方式获取。如果读者们有更好的思路可以留言给我,欢迎相互讨论学习。
注入的思路,就是先获取ServerImpl中已经有的ServerServiceDefinition和ServerMethodDefinition,读取到新的List和Map中,并在新的List和Map中添加WebShell内存马的信息,最后再设置unmodifiable属性并更改registry对象的值。
Poc如下所示,需要提供ServerImpl对象的实例。
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
1
上面的代码片段只是一个demo版本,具体的实现需要把WebShellServiceGrpc类转成字节码,再Definition到JVM中。
注入完成后,在客户端执行如下代码调用即可:
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
2
而原本公众号中给出的防御方式是通过RASP技术对动态修改Service对象的行为做出拦截。其实我个人觉得这里不太好埋点,比如我可以对Service的上层对象registry直接做修改,或者我对Services对象的某个ServerServiceDefinition做修改,不做添加而只是修改原来已经存在的Method,操作的对象就不需要再更改Services的值。
gRPC内存马查杀
我在原先编写的内存马查杀工具MemoryShellHunter添加检测模块:https://github.com/sf197/MemoryShellHunter
首先在Agent中的transform方法中用ASM消费所有的类
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
3
这里的GrpcClassVisitor就是当前类的父类的接口是否继承自io.grpc.BindableService,如果是,则说明这是一个gRPC实现类,因此当中定义的方法都可以是危险函数,需要进一步使用可达性分析判断是否有危险Sink函数。
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
4
判断函数逻辑:
protoc -I=. --java_out=./codes/ user.proto
protoc.exe --plugin=protoc-gen-grpc-java.exe --grpc-java_out=./code --proto_path=. user.proto
5
这样的好处可以查找出系统中gRPC的内存马。
缺点是在查找gRPC实现类的时候,需要用到当前线程的ClassLoader判断父类是否继承自io.grpc.BindableService,因此攻击的时候只需要更改加载的ClassLoader即可绕过。
这里也是抛砖引玉,如果有更好的检测思路和查杀思路欢迎提交pr:https://github.com/sf197/MemoryShellHunter
Reference
[1].https://developers.google.com/protocol-buffers
[2].https://grpc.io
[3].https://mp.weixin.qq.com/s/osuoinwCpOwNM4WoI6SOnQ
[4].https://www.cnblogs.com/easyidea/p/15767542.html
还没有评论,来说两句吧...