APP概述
这是一个月抛软件,为什么要来分析这个软件呢,一个师傅告诉我,他只能发现这个APP的流量从127.0.0.1走,并且找不到dns解析的过程,我以前也没有接触过这样的软件,刚好没事来练练手。
这个APP一进去就会有一个客服给你对话,让你冲VIP就能提供某种不正当服务,这种套路在网上也能查到挺多再说约炮软件诈骗套路(深入解析,3787字,阅读时长5分钟) - 知乎,也不知道能不能真约。

这个APP图标长这样,包名更是一堆随机字符串dsaindas.*********.sdancsuhsfj。一看就不是什么好东西。
主要有两个界面能够进行诈骗,一是聊天页面:

二是关联网站:

接下来针对这两个点进行详细分析。
逆向思路
dns解析
因为我们已知这个app会从127.0.0.1走,那么,很容易就能想到几种实现方式
内部vpn
自定义的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协议相关的,浪费很多时间,这里有几点反思
经验不足,在我明知是tg报文走127.0.0.1的情况下,去分析tg没有意义,应该去找流量转发的地方
这个类的名字太可疑了,如下图,很难不去关注
脱壳之前的xml中主入口的下方紧跟这个类,自然想到去分析这个类
我应该脱完壳就开始找堆栈调用,不然不容易追到核心逻辑
打印堆栈找到流量转发的方法
直接打印了堆栈,发现这个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实战!