某钓鱼木马APP分析

前言

自动拾取忘记关了,捡到了一个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代码:

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,按真实步骤输入收款账号与金额,并使用已获取的支付密码完成转账。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇