首页
社区
课程
招聘
[原创]某月抛软件的全流程分析
发表于: 2025-10-21 21:28 2773

[原创]某月抛软件的全流程分析

2025-10-21 21:28
2773

APP概述

这是一个月抛软件,为什么要来分析这个软件呢,一个师傅告诉我,他只能发现这个APP的流量从127.0.0.1走,并且找不到dns解析的过程,我以前也没有接触过这样的软件,刚好没事来练练手。

这个APP一进去就会有一个客服给你对话,让你冲VIP就能提供某种不正当服务,这种套路在网上也能查到挺多再说约炮软件诈骗套路(深入解析,3787字,阅读时长5分钟) - 知乎,也不知道能不能真约。

这个APP图标长这样,包名更是一堆随机字符串dsaindas.*********.sdancsuhsfj。一看就不是什么好东西。

主要有两个界面能够进行诈骗,一是聊天页面:

二是关联网站:

接下来针对这两个点进行详细分析。

逆向思路

dns解析

因为我们已知这个app会从127.0.0.1走,那么,很容易就能想到几种实现方式

  1. 内部vpn

  2. 自定义的dns解析

那么我最开始就从这里两个点开始入手的

固定证据

既然说,从APP本身不好抓包,那直接走安卓的全局tcpdump不就行了,这样排除无关的ip,剩下的一定有关啦。

但是这种思路存在一个问题,无法证明这个ip和这个app的犯罪行为的关联性。所以我必须去分析其内部逻辑实现。

脱壳手法

frida-dexdump

一想到壳,不是vmp,那frida-dexdump肯定是最简单的脱壳,脱下来之后,发现,确实脱出来了东西,这时候一看,脱出来了东西,那很好呀,接着分析呗。

而且一看,这里有个类的名字叫qiniu.android.dns,于是再一想,和上面的假设对上了,那就出现了第一个想当然的地方:以为dns是这里实现的。

上网一搜,这是个开源项目,甚至连版本号都一模一样,那可太高兴了,读源码,上hook,一看,怎么hook不到???

好吧,枚举所有类,压根没加载这玩意儿

接着分析,对于在动态环境中枚举出来的类,基本都在jadx中找不到,此时就发现问题不太对了。

fart

我们可爱的showmaker师傅告诉我可能是抽取壳。

但是很奇怪的是,我的安卓13的手机跑不起来fart环境,网上找了各种方式都没解决掉,借了他的手机跑了一下fart,此时果然脱出来的dex文件中有更多的类,但是每个方法jadx都无法解析,只能看到函数签名,看不到反汇编实现。而且查看一个类的具体实现,jadx会把我的内存占满,经常卡死。这可能是dex文件恢复得不对的原因。

此时,我接着通过动态方法做。发现还是有很多枚举出来的类不在fart脱壳后的dex文件中。

手动脱壳

枚举classloader

我通过fart脱壳之后发现类仍然不全,我开始怀疑是不是这个app自己实现了一个classloader,而且是完全自定义的。

function get_all_method_from_classname(class_name){  
    Java.perform(function () {  
        try {  
            // 尝试查找并设置正确的类加载器  
            var foundLoader = null;  
            Java.enumerateClassLoadersSync().forEach(function (loader) {  
                console.log("loader: " + loader);  
                try {  
                    ;  
                    // if (loader.findClass(class_name)) {  
                    //     // 优先选择包含"CronetDynamite"的加载器,否则使用第一个找到的  
                    //     if (loader.toString().indexOf("CronetDynamite") != -1 || !foundLoader) {  
                    //         foundLoader = loader;                    //     }                    // }                } catch (error) {  
                    // 忽略单个加载器的错误  
                }  
            });  
  
            // 如果找到了合适的类加载器,则设置它  
            // if (foundLoader) {  
            //     Java.classFactory.loader = foundLoader;            //     console.log("使用类加载器: " + foundLoader);  
            // }            //            // var DymClass = Java.use(class_name);            // var methods = DymClass.class.getDeclaredMethods();            // var method_names = [];            // for(var i=0; i<methods.length; i++){            //     method_names[i] = methods[i].getName();            // }            // resolve(method_names);        } catch (error) {  
            console.log('error', "获取类方法时出错: " + class_name + ", 错误: " + error);  
            resolve([]); // 即使出错也resolve空数组,避免Promise被拒绝  
        }  
    });  
}  
get_all_method_from_classname("java.lang.String")

此时枚举了classloader发现,自定义的classloader没找到(因为确实没有),但我找到了一个名叫i11111i111.zip的文件,并且作为dex加载了。

这当然很不对劲,直接adb pull 拷出来看。

拷出来的文件是几个dex


我每个都加载了一遍(一起拖到jadx会直接让jadx崩溃),一是dex都比较大,二是这实际是抽取壳,现在处于没有回填的状态,jadx只能分析出函数签名,但是仍然会尝试反汇编代码,导致卡死。

这时候我能看到更多的类了,我以为壳就到这里就算脱完了。随后又进行了很多动态方法的分析,但一直没什么效果。

libdpt.so分析

这时候我看到了native方法,于是我又将注意力转移到so文件上。

此时看到这些so,libbaidusec.so这名字就像一个壳,所以,我尝试hook了加载so的调用

Java.perform(function () {
    console.log("[*] 开始 Hook .so 加载行为");

    // Hook Java 层的 System.load()
    var System = Java.use("java.lang.System");
    System.load.overload("java.lang.String").implementation = function (path) {
        console.log("[+] System.load() 被调用,路径: " + path);
        return this.load(path);
    };

    // Hook Java 层的 System.loadLibrary()
    System.loadLibrary.overload("java.lang.String").implementation = function (libname) {
        console.log("[+] System.loadLibrary() 被调用,库名: " + libname);
        return this.loadLibrary(libname);
    };
});

// Hook native 层 dlopen(实际加载 .so 的函数)
if (Process.platform === 'linux') {
    var dlopen = Module.findExportByName("libc.so", "dlopen");
    if (dlopen) {
        Interceptor.attach(dlopen, {
            onEnter: function (args) {
                var path = args[0].readUtf8String();
                console.log("[+] dlopen() 被调用,路径: " + path);
            },
            onLeave: function (retval) {
                // 可选:打印返回值(句柄)
                // console.log("    dlopen 返回: " + retval);
            }
        });
    } else {
        console.warn("[-] 未找到 dlopen 函数");
    }
}

此时我发现,在tmessage.so前有个名叫libdpt.so的东西被调用了,经过一番搜索,发现,这才是真的壳[原创]记录一次加固逆向分析以及加固步骤详解-Android安全-看雪论坛-安全社区|非营利性质技术交流社区

这里借鉴了这篇文章的很多分析结果,从androidmanifest.xml开始


其实就能发现这个壳的痕迹,但是我才疏学浅,没见过这个壳,没认出来,而未脱壳的java层是有调用代码的

于是开始分析libdpt.so

这个so首先从init_array段进行加载,目的是自解密,采用rc4加密了bitcode段,导致无法直接查看函数

这是两个关键函数,通过自解密函数,写出还原so的脚本,当然是GPT生成的

from Crypto.Cipher import ARC4  
from elftools.elf.elffile import ELFFile  
from elftools.common.exceptions import ELFError  
import sys  
  
# 密钥(十六进制列表)  
key = [0xEE, 0x45, 0x33, 0x20, 0xB4, 0xA0, 0xD3, 0x5A,  
       0x22, 0x74, 0x70, 0x61, 0xC2, 0x40, 0xB7, 0x2A]  
key_bytes = bytes(key)  
  
def rc4_decrypt(data: bytes, key: bytes) -> bytes:  
    """使用 RC4 解密数据"""  
    cipher = ARC4.new(key)  
    return cipher.decrypt(data)  
  
def patch_elf_bitcode(input_path: str, output_path: str):  
    # 1. 读取整个 ELF 文件到内存  
    with open(input_path, "rb") as f:  
        elf_data = bytearray(f.read())  # 使用 bytearray 以便修改  
  
    # 2. 用 elftools 解析,找到 .bitcode 节信息  
    with open(input_path, "rb") as f:  
        try:  
            elf = ELFFile(f)  
            bitcode_section = None  
            for section in elf.iter_sections():  
                if section.name == ".bitcode":  
                    bitcode_section = section  
                    break  
            if bitcode_section is None:  
                print("错误:未找到 .bitcode 节!", file=sys.stderr)  
                return False  
  
            offset = bitcode_section['sh_offset']  
            size = bitcode_section['sh_size']  
            encrypted_data = elf_data[offset:offset + size]  
  
            print(f"找到 .bitcode 节:offset=0x{offset:08x}, size={size} 字节")  
  
        except (ELFError, Exception) as e:  
            print(f"解析 ELF 失败: {e}", file=sys.stderr)  
            return False  
  
    # 3. 解密数据  
    try:  
        decrypted_data = rc4_decrypt(encrypted_data, key_bytes)  
    except Exception as e:  
        print(f"解密失败: {e}", file=sys.stderr)  
        return False  
  
    if len(decrypted_data) != size:  
        print(f"警告:解密后长度 ({len(decrypted_data)}) 与原节大小 ({size}) 不一致!",  
              file=sys.stderr)  
        # 通常 RC4 是流加密,长度应一致;若不一致,可能是密钥错误或数据损坏  
  
    # 4. 覆盖原加密部分  
    elf_data[offset:offset + size] = decrypted_data[:size]  # 确保不越界  
  
    # 5. 写入新 ELF 文件  
    with open(output_path, "wb") as out_f:  
        out_f.write(elf_data)  
  
    print(f"成功:已将解密后的 .bitcode 节写入 {output_path}")  
    return True  
  
if __name__ == "__main__":  
    input_file = "libdpt.so"  
    output_file = "libdpt.so.patched"  
  
    if patch_elf_bitcode(input_file, output_file):  
        print("✅ ELF 修复完成!")  
    else:  
        print("❌ 处理失败。", file=sys.stderr)

恢复出jni_onload,发现注册了这些方法,一个一个挨着分析,找到注册hook的地方,这里ida中间崩溃了一次,注释和函数重命名全没了

整体使用dobbyhook框架,这里我直接使用了上面大佬的脚本进行dump

const dexMap = new Map();
var dex_count=0;
function analysisDex(Base) {
    var originalDefineClass = Base.add("0x54F6C");
    console.log("originalDefineClassAddr->", originalDefineClass)
    Interceptor.attach(originalDefineClass, {
        onEnter: function (args) {
            this.dex_file = this.context.x5;
            var base = ptr(this.dex_file).add(Process.pointerSize).readPointer();
            var size = ptr(this.dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();
            console.log("[DexFile]-> Base = ", base);
            console.log("[DexFile]-> size = ", size);
            var magic = ptr(base).readCString();
            console.log("[DexFile]-> magic = ", magic);
            // 检查 base 和 size 是否已存在
            let isDuplicate = false;
            for (let [existingBase, existingSize] of dexMap.entries()) {
                if (existingBase.equals(base) && existingSize === size) {
                    isDuplicate = true;
                    break;
                }
            }

            if (isDuplicate) {
                console.log(`[WARN] DexFile with base ${base} and size ${size} already exists, skipping...`);
            } else {
                dexMap.set(base, size);
                console.log(`[INFO] New DexFile found: base=${base}, size=${size}`);
            }
        },
        onLeave: function (args) {
        }
    })

}

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 dumpDex() {

    dexMap.forEach((size, base) => {
        console.log(`Base: ${base}, Size: ${size}`);
        var magic = ptr(base).readCString();
        console.log("DesFileMagic->", magic);
        if (magic.indexOf("dex") == 0) {
            var process_name = get_self_process_name();
            if (process_name != "-1") {
                var dex_dir_path = "/sdcard/Download/" + 'beebee' + "/files"
                Mkdir(dex_dir_path)
                dex_dir_path += "/dump_dex_" + 'beebee';
                Mkdir(dex_dir_path)
                var dex_path = dex_dir_path + "/class" + (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)
                }
            }
        }
    });
}
function printDexMap() {
    console.log("Current DexFile Map:");
    for (let [base, size] of dexMap.entries()) {
        console.log(`Base: ${base}, Size: ${size}`);
    }
}
Java.perform(function (){
    var module = Process.findModuleByName("libdpt.so").base;
    if (module) {
        console.log("[+] 找到模块: " + module.name + " 基址: " + module.base);
    } else {
        console.log("[-] 未找到 libdpt.so");
    }
    analysisDex(module);
})

dump出来是这样的,这里基本就拿到所有类的,而且只要是调用过的函数,都能被jadx正常解析 ,此时脱壳工作基本就完成了

逆向TG协议

这个app不仅使用了tmessage.so,而且聊天框和telegram一模一样,因为tmessage是开源的,所以直接去github下载,一边看源码,一遍逆so。

最终我发现Connection结构体可以作为入手的点

这里面又有ip和端口,又有协议密钥,hook这个能拿到的信息更多。在这个过程中,我尝试了很多hook点位,但是都没有这个效果好,但是我好像没截图!那不展示了。这段hook代码太长了,放不上去,刚好tg协议的分析不是重点,跳过了。

真正的流量发送类

为什么上一节我去hook了tg协议相关的,浪费很多时间,这里有几点反思

  1. 经验不足,在我明知是tg报文走127.0.0.1的情况下,去分析tg没有意义,应该去找流量转发的地方

  2. 这个类的名字太可疑了,如下图,很难不去关注

  3. 脱壳之前的xml中主入口的下方紧跟这个类,自然想到去分析这个类

  4. 我应该脱完壳就开始找堆栈调用,不然不容易追到核心逻辑

打印堆栈找到流量转发的方法

直接打印了堆栈,发现这个app新开一个进程,监听端口,是通过com.app.protect来实现的,而在此之前,我一直都没点开过com包,主观性认为com下的东西一般都是好的。

其中a.a.a下实现了和外部服务器通信的加密方法,包括xxtea和AES等,我甚至都不用解密,直接hook,让他帮我解密

这里贴出最后找到的关键信息的所有hook代码

var debug=0;

function hookaaa5109(){
    Java.perform(function (){
        var aaapro5109=Java.use("a.a.a.pro5109");
        aaapro5109.pro5109.overload('java.lang.String', 'java.lang.String').implementation=function (){
            console.log('==========hookaaa5109.pro5109==========');
            console.log('arg1:\t'+arguments[0]);
            console.log('arg2:\t'+arguments[1]);
            if (debug){
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            }
            var ret=this.pro5109.apply(this, arguments);
            console.log('ret:\t'+ret);
            return ret;
        }
        aaapro5109.pro5130.overload('java.lang.String', 'java.lang.String').implementation=function (){
            console.log('==========hookaaa5109.pro5130==========');
            console.log('arg1:\t'+arguments[0]);
            console.log('arg2:\t'+arguments[1]);
            var ret=this.pro5130.apply(this, arguments);
            console.log('ret:\t'+ret);
            return ret;
        }
        aaapro5109.pro5130.overload('[B', 'java.lang.String').implementation=function (){
            console.log('==========hookaaa5109.pro5130[B==========');
            console.log('arg1:\t'+arguments[0]);
            console.log('arg2:\t'+arguments[1]);
            var ret=this.pro5130.apply(this, arguments);
            console.log('ret:\t'+ret);
            return ret;
        }
    })
}
function hookaaa5136(){
    Java.perform(function (){
        var aaapro5136=Java.use("a.a.a.pro5136");
        console.log('==========hookaaa5136==========')
        aaapro5136.pro5130.overload('java.nio.channels.SelectionKey', 'java.nio.channels.SocketChannel').implementation=function (){
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            console.log('==========hookaaa5136==========')
            return this.pro5130.apply(this, arguments);
        }
    })
}

function hookappprotect(){
    Java.perform(function (){
        var appprotect=Java.use("com.app.protect.AppProtectManager");
        appprotect.pro5109.overload('java.lang.String', 'java.lang.String', 'int', 'a.a.a.pro5137').implementation=function (){
            console.log('==========hookappprotect.pro5109==========')
            console.log('arg1:\t'+arguments[0]);
            console.log('arg2:\t'+arguments[1]);
            console.log('arg3:\t'+arguments[2]);
            console.log('arg4:\t'+arguments[3]);
            var ret=this.pro5109.apply(this, arguments);
            console.log('ret:\t'+ret);
            return ret;
        }
    })
}
function hookNetWorkManager(){
    Java.perform(function (){
        var NetWorkManager=Java.use("im.hqcgkirgub.network.NetWorkManager");
        NetWorkManager.pro5130.implementation=function (){
            console.log('==========hookaaa5137.pro5130==========')
            console.log('arg1:\t'+arguments[0]);
            console.log('arg2:\t'+arguments[1]);
        }
    })
}

function hookdohnet() {
    Java.perform(function () {
        var DohNet = Java.use("im.hqcgkirgub.network.DohNet");
        // Hook dataDecryption 方法
        DohNet.dataDecryption.implementation = function (arg1, arg2) {
            console.log('==========hookdohnet.dataDecryption==========');
            console.log('arg1:\t' + arg1);
            console.log('arg2:\t' + arg2);
            var ret = this.dataDecryption(arg1, arg2); // 或者用 apply: this.dataDecryption.apply(this, arguments);
            console.log('ret:\t' + ret);
            return ret;
        };

        // 主动创建实例并调用 initUrlDOH
        // try {
        //     // 尝试使用无参构造函数创建实例
        //     var instance = DohNet.$new();
        //     console.log('[+] Successfully created DohNet instance');
        //
        //     // 调用 initUrlDOH 方法(假设它是 public 且无参)
        //     instance.initUrlDOH();
        //     console.log('[+] Called initUrlDOH successfully');
        // } catch (e) {
        //     console.error('[-] Failed to create instance or call initUrlDOH:', e.message);
        // }
    });
}
function hookossnet() {
    Java.perform(function () {
        var OSSNet = Java.use("im.hqcgkirgub.network.OSSNet");
        // Hook dataDecryption 方法
        OSSNet.initOssUrl.implementation = function () {
            console.log('==========OSSNet.initOssUrl==========');
            var ret = this.initOssUrl(); // 或者用 apply: this.dataDecryption.apply(this, arguments);
            console.log('ret:\t' + ret);
            return ret;
        };

        try {
            // 尝试使用无参构造函数创建实例
            var instance = OSSNet.$new();
            console.log('[+] Successfully created DohNet instance');

            // 调用 initUrlDOH 方法(假设它是 public 且无参)
            var res =instance.initOssUrl();
            console.log('res:\t'+res);
            console.log('[+] Called initUrlDOH successfully');
        } catch (e) {
            console.error('[-] Failed to create instance or call initUrlDOH:', e.message);
        }
    });
}
function main(){
    console.log('==========main==========')
    //hookaaa5136();
    hookaaa5109();
    hookappprotect();
    hookdohnet();
    hookossnet();
}
main();
setImmediate(main);
setTimeout(main, 800);


这里就能拿到作为证据的关键信息了,包括域名的解析方法,后台服务器的地址等一系列详细信息,稍微展示一点


com.app.protect.AppProtectManager类

这个类实现了从端口监听、流量转发,到域名解析等一系列操作,基本所有和普通app不一样的地方都在这个类中实现。


域名解析

为什么之前找不到域名解析的方法,到这里就有答案了,这里走的不是dns协议,而是doh协议(dns over https)

各种相关的配置信息都拿到了,只是做一个doh解析,这也回答了上面的走127.0.0.1,并且找不到dns痕迹的问题了。

从这个函数来写脚本,发包,就能拿到域名的真正解析,上上图展示的域名正常用dns服务器是解析不到的。


游戏盾

这个点是一个师傅提出来的盾的问题,没想到游戏盾用来做犯罪app了。哈哈。


到这一步就很明确了,拿到整个app的所有关键信息了,但是很多敏感信息不便放出来。总的来说,分析这个app的加载流程还是学到很多新东西,也反应我经验确实不足。多打印点堆栈!!!!!


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

收藏
免费 10
支持
分享
最新回复 (8)
雪    币: 157
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
博主你好,我想问一下这个app可以通过hook socket打印堆栈来追踪吗
2025-10-22 17:00
0
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
求助,被此类软件骗一万余元,希望能帮忙找回,找回愿拿2000做报酬
2025-10-23 06:02
0
雪    币: 104
活跃值: (7325)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
mb_elfvhhdv 求助,被此类软件骗一万余元,希望能帮忙找回,找回愿拿2000做报酬
对楼主来说 不够开机费的 别找了
2025-10-23 10:16
2
雪    币: 687
活跃值: (1001)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
mb_cfjwplfo 博主你好,我想问一下这个app可以通过hook socket打印堆栈来追踪吗
你说得对,最开始思路陷在tmessage里面了,hook socket确实是一种定位的方法
2025-10-23 10:37
0
雪    币: 687
活跃值: (1001)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mb_elfvhhdv 求助,被此类软件骗一万余元,希望能帮忙找回,找回愿拿2000做报酬
建议尽快找当地警方处理哦
2025-10-23 10:38
0
雪    币: 110
活跃值: (1720)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
这类软件,走了好几种通联,DOH、DOT、游戏盾,还有隐藏的几种类型。
2025-10-23 10:48
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
这个是哪个公司的游戏盾啊?
2025-10-29 17:50
0
雪    币: 1915
活跃值: (1717)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
9
lsxyq 这个是哪个公司的游戏盾啊?
阿里的,但这个app没用到
2025-11-11 15:34
0
游客
登录 | 注册 方可回帖
返回