首页
社区
课程
招聘
某钓鱼木马APP分析
发表于: 2026-1-27 14:13 1106

某钓鱼木马APP分析

2026-1-27 14:13
1106

前言

自动拾取忘记关了,捡到了一个7.37M的短剧APP。WDF!什么短剧APP居然有如此惊为天人的压缩技术,于是...

现象分析

木马伪装成了一个短剧APP并设置弹窗伪装需要开启“加速器”来诱导用户对其进行授权,点开后实际请求的是无障碍权限,获取无障碍权限之后就能监控屏幕了。

file
file

代码分析

壳APP

这个APP我们拿到的包实际上是一个壳APP,没有真正的业务逻辑。

首先注意到这个温馨提示的弹窗功能如(org.tools.pro.AccessibilityGuide.showBrandSpecificDialog)

file

这里看到设置的onclick回调函数a,点进去看,就可以发现他在安装一个新的APP。

file

file

解密资源逻辑,其实就是异或了一个90,异或的文件为.dat的,其实这里都不需要看FileName,Assets就一个.dat结尾的,直接异或90再次JADX。

file

file

木马APP

脱壳分析

直接jadx反编译发现实际上是加固的,什么类都看不到并且有很明显的加固字样。

file

没看出来是什么加固,没有时间管那么多了,直接看看他加载的so文件,这里我选择的是Arm64版本的。

file

分析的话首先还是看init_array段,发现没有逻辑,那么只能在JNI_onLoad了。

file

清晰明了

file

file

file
发现了加固逻辑,释放了一个vm.dex 然后在 sub_1E680 进行AES解密填充

最开始以为是抽取方案,后面看看sub_1E680 里面的代码,反射调用的InMemoryDexClassLoader,那么实则是采用动态加载dex的方案了。

file

那么既然如此,我们可以通过Hook InMemoryDexClassLoader 的方案来脱壳,但是InMemoryDexClassLoader 最终还是要走loadMethod的,所以通用Hook DefineClass的脱壳方案也是可以用的。

frida代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
function get_self_process_name() {
    var openPtr = Module.getExportByName('libc.so', 'open');
    var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var readPtr = Module.getExportByName("libc.so", "read");
    var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);
    var closePtr = Module.getExportByName('libc.so', 'close');
    var close = new NativeFunction(closePtr, 'int', ['int']);
    var path = Memory.allocUtf8String("/proc/self/cmdline");
    var fd = open(path, 0);
 
    if (fd != -1) {
        var buffer = Memory.alloc(0x1000);
        var result = read(fd, buffer, 0x1000);
        close(fd);
        result = ptr(buffer).readCString();
        return result
    }
 
    return "-1"
}
 
function Mkdir(path) {
    if (path.indexOf("com") == -1) {
        console.log("[Mkdir]-> Pass:", path);
        return 0;
    }
    var mkdirPtr = Module.getExportByName('libc.so', 'mkdir');
    var mkdir = new NativeFunction(mkdirPtr, 'int', ['pointer', 'int']);
    var opendirPtr = Module.getExportByName('libc.so', 'opendir');
    var opendir = new NativeFunction(opendirPtr, 'pointer', ['pointer']);
    var closedirPtr = Module.getExportByName('libc.so', 'closedir');
    var closedir = new NativeFunction(closedirPtr, 'int', ['pointer']);
    var cPath = Memory.allocUtf8String(path);
    var dir = opendir(cPath);
 
    if (dir != 0) {
        closedir(dir);
        return 0
    }
 
    mkdir(cPath, 0o755);
    chmod(path)
    console.log("[Mkdir]->", path);
}
 
function chmod(path) {
    var chmodPtr = Module.getExportByName('libc.so', 'chmod');
    var chmod = new NativeFunction(chmodPtr, 'int', ['pointer', 'int']);
    var cPath = Memory.allocUtf8String(path);
    chmod(cPath, 755)
}
 
function dump_dex() {
    var libart = Process.findModuleByName("libart.so");
    var addr_DefineClass = null;
    var symbols = libart.enumerateSymbols();
 
    for (var index = 0; index < symbols.length; index++) {
        var symbol = symbols[index];
        var symbol_name = symbol.name;
 
        if (symbol_name.indexOf("ClassLinker") >= 0 && symbol_name.indexOf("DefineClass") >= 0 && symbol_name.indexOf("Thread") >= 0 && symbol_name.indexOf("DexFile") >= 0) {
            console.log(symbol_name, symbol.address);
            addr_DefineClass = symbol.address
        }
    }
 
    var dex_maps = {}
 
        ;
    var dex_count = 1;
    console.log("[DefineClass:]", addr_DefineClass);
 
    if (addr_DefineClass) {
        Interceptor.attach(addr_DefineClass, {
            onEnter: function (args) {
                var dex_file = args[5]; var base = ptr(dex_file).add(Process.pointerSize).readPointer(); var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt(); if (dex_maps[base] == undefined) {
                    dex_maps[base] = size; var magic = ptr(base).readCString(); if (magic.indexOf("dex") == 0) {
                        var process_name = get_self_process_name(); if (process_name != "-1") {
                           /* var dex_dir_path = "/data/data/" + process_name + "/files/dump_dex_" + process_name;
                            Mkdir(dex_dir_path
                             
                            */
                            var dex_dir_path = "/data/data/" + process_name + "/files"
                            Mkdir(dex_dir_path)
                            dex_dir_path += "/dump_dex"
                            Mkdir(dex_dir_path)
                            var dex_path = dex_dir_path + "/classes" + (dex_count == 1 ? "" : dex_count) + ".dex"; console.log("[find dex]:", dex_path); var fd = new File(dex_path, "wb");
                            if (fd && fd != null) {
                                dex_count++; var dex_buffer = ptr(base).readByteArray(size);
                                fd.write(dex_buffer); fd.flush();
                                fd.close(); console.log("[dump dex]:", dex_path)
                            }
                        }
                    }
                }
            }
 
            , onLeave: function (retval) { }
        })
    }
}
 
var is_hook_libart = false;
 
function hook_dlopen() {
    Interceptor.attach(Module.findExportByName(null, "dlopen"), {
        onEnter: function (args) {
            var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString(); if (path.indexOf("libart.so") >= 0) {
                    this.can_hook_libart = true; console.log("[dlopen:]", path)
                }
            }
        }
 
        , onLeave: function (retval) {
            if (this.can_hook_libart && !is_hook_libart) {
                dump_dex(); is_hook_libart = true
            }
        }
    });
 
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString(); if (path.indexOf("libart.so") >= 0) {
                    this.can_hook_libart = true; console.log("[android_dlopen_ext:]", path)
                }
            }
        }
 
        , onLeave: function (retval) {
            if (this.can_hook_libart && !is_hook_libart) {
                dump_dex(); is_hook_libart = true
            }
        }
    })
}
 
setImmediate(dump_dex);

这里直接使用frida -U -f 启动本木马APK你会会发现直接报错了,因为这个APP是没有正常的Luncher的,

他不接受 android.intent.action.MAIN file

但是他接受BOOT_COMPLETED ,ndroid.provider.Telephony.SMS_RECEIVED 来进行拉起自己,典型的木马行为。那这样要如何才能Hook到他是个问题,这里其实最好的思路是hook 壳app的接入点,去Hook木马APP。

但,没有更方便的办法了吗?

没有更方便的办法了吗?

真的没有了吗?

其实突然我想到了算法助手PRO,如有神助,为何呢,因为算法助手基于LSP可以很方便的就监控到APP的启动,正好算法助手又有加载Frida的功能,这简直太爽了。

file

file

随便点点触发到木马APP的启动逻辑就行,算法助手就自动Hook了

file

同时加载的dex也被脱下来了,爽爽爽

file

业务逻辑分析

该 APK 在申请屏幕捕获权限后会启动 MediaProjection 前台服务并持续抓取屏幕帧,压缩后通过 WebSocket 外传到 C2;同时具备摄像头/麦克风、短信、通讯录/通话记录、位置、文件、支付密码与锁屏密码窃取以及反卸载等能力。

屏幕捕获链 / 屏幕截取行为链

liliao.mine.king.liliao.SimplifiedConnectionManager.handleMessage ,此处接受C2下发的 order ,发送给屏幕捕获的入口
file
file
liliao.mine.king.liliao.accessibility.ScreenCaptureManager 负责创建 VirtualDisplay/ImageReader 进行抓屏,帧被压缩为 JPEG 并标记 type=video_frame 后上送。

file

传输通道是 WebSocket,地址端口都在资源文件里

file

file

恶意能力 / 恶意行为能力
摄像头取流/拍照并 Base64 回传:

liliao.mine.king.liliao.b
file

file

麦克风实时音频流与定时录音文件外传

liliao.mine.king.liliao.h

file

通讯录与通话记录批量读取并上送

liliao.mine.king.liliao.d

file

凭据窃取:伪装锁屏 PIN、支付密码覆盖层、安装密码覆盖层并回传

liliao.mine.king.liliao.phishing.d(负责保存骗取的锁屏密码)

file

liliao.mine.king.liliao.accessibility.smartmonitor.w0.j(匹配支付类型)

file

模拟点击操作用户设备

liliao.mine.king.liliao.accessibility.e.e 自动解锁

file

实际负责执行的是: liliao.mine.king.liliao.accessibility.RemoteAccessibilityService

file

总结

最后看了这个APP所有的操作,我们尝试还原一下他是如何转账走钱财的:

  1. 远控进入受害者手机

    通过 WebSocket 远程下发命令(C2):liliao.mine.king.liliao.SimplifiedIOSocket

    无障碍执行点击/滑动/系统键:

    ​ 指令处理:liliao.mine.king.liliao.accessibility.e

    ​ 实际手势注入:liliao.mine.king.liliao.accessibility.b0

    ​ 服务入口:liliao.mine.king.liliao.accessibility.RemoteAccessibilityService

  2. 获取屏幕画面与操作反馈

    申请 MediaProjection 权限并自动点击“允许”,随后持续抓屏上传帧:liliao.mine.king.liliao.ScreenCapturePermissionActivity,liliao.mine.king.liliao.accessibility.s0,iliao.mine.king.liliao.accessibility.ScreenCaptureManager

  3. 获取支付/解锁密码

    支付密码覆盖层输入并保存(含支付宝/微信等):
    liliao.mine.king.liliao.accessibility.smartmonitor.w0

    伪锁屏 PIN/解锁密码捕获并上报:liliao.mine.king.liliao.phishing.b,liliao.mine.king.liliao.phishing.d

    保存后会在 reportPaymentPasswords() 里打包上送:liliao.mine.king.liliao.SimplifiedConnectionManager

  4. 最终转账动作

​ 攻击者利用远控打开银行/支付宝/微信等 App,按真实步骤输入收款账号与金额,并使用已获取的支付密码完成转账。


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 4
支持
分享
最新回复 (3)
雪    币: 88
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
太牛了sw
2026-1-27 14:34
0
雪    币: 7718
活跃值: (8063)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
3
swdd,yes!
6天前
0
雪    币: 4162
活跃值: (5154)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
4
东方玻璃 swdd,yes!
6天前
0
游客
登录 | 注册 方可回帖
返回