某梆梆加固企业版APP协议分析

两只羊 Lv3

流量截取

没有证书校验,直接开wifi代理就好了

1
2
3
4
5
6
7
8
9
10
POST /v3/api.php/Run2/beforeRunV260 HTTP/2
Host: api2.lptiyu.com
Cookie: acw_tc="2f62d7f817716442398976314e8a27f4650fc7520c0d36199ec0b4a2952ae9";$Path="/";$Domain="api2.lptiyu.com"; PHPSESSID=gcr1h7n5j9g35eg3rgctn7caic
Connection: close
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 600
User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; M2007J22C Build/TQ3A.230901.001)
Accept-Encoding: gzip, deflate, br

key=zLCkGpVvhyUwflk8PPOg5CW2NX2J846YpMO5FtCw7EZpRD%2BbHWUX6MtZZ1lKCQV2OKFr9UGvsfHD%0A4oC%2BahCeF0kYu31d9IePTYSrSPOmR2a%2FULvB%2BP%2BdKMtBxVcunlI%2FGu4GXxwaZdfRoZ3ATSSuB3cu%0Ao3%2F6KaZEjSSleC0aJA37jvNVuk34oQHAAF8cx9WS5UVResPS36iF4e8ZEiCZloxwUNf31np4i4xP%0AllguBsRepIU1gyuJV4T5zi5YrC8ylg6OeaOQ%2F7v6YBg%2FRBKLJlfjahmYttR%2FmiAPLjmSYwJyhAne%0AV2KQMBL5AreJKKQhfAksXGx98Q2VYs6SRKYyMaSAdG4UoGtVhFSBWuaSuSiPN9Mj7cL513AKvO7E%0AIDOHuiWTdY15iVhY5%2F7fjE3MJkT2wr5HvcSWaOTkFHM8bm0K1omcqFsZyRERMLQaXh7ZgmsdyHbX%0ANBFletO%2Bgs%2BW18hHBQiT%2BM%2FvBwiHc0DrKkpDbNyGdikG%2BwBXFuZ6CTlCi6QSJk9nXFqdU5r5ro0V%0ATw%3D%3D%0A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/2 200 OK
Date: Sat, 21 Feb 2026 03:32:19 GMT
Content-Type: text/json; charset=utf-8
Content-Length: 2769
X-Powered-By: PHP/7.3.18
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

{
"data": "XZGEdk97PasvnP\/Bm2GwZagC+DBTXYSV0UI+8fxHOGjj2kBligkBqI3TzMdvhy\/VYV7dpOae22o4KDKKDNoJzizUfkA1ROQGG0Ibm2i7b9tFwSS06qF2xMThRMZybwRwemlAEEq0RXJQwdFvVUV0XsC940IdDSuGXlajI1Su83rs2CGtDm+vsVOETlPgz8zFWkUJaKKMV7wlzWO8\/CBfy+5clGZq2bvSM0+UX0X+rwBRWVbSk\/8PmnVy37NYFVtzBYgvWOjZgklxA8Mv3xmwvuazz789RwCOkwS0A8L5LgkO4Wd\/1D3yNlxOC3MljEH1WnTS+W6jGWZZGUyghorUpDMJiClp4PpdREoQe32rbbQmx9TWjAlBWWbF9r8B9TKDGnxltYavN+vRBzyLr7s6udVVauO252CSTYR3yEG3fVa10yAbHVUjL4urkhOHu3hSK02t3UUcmgYJukem40KfKewepCU\/9V9N2RoyeJv3w2gNR\/xNL7erocRfBppcLUq0k2jDCg5YUukXcLRE9LWPy2fMhFvcCdBcQiMnzaXvUPJ7nXWjeofSQ0l1DBzlhg2w49ejdHXiFRIcU\/JtxylrZb1trDmGQQV0J+e8tIkKDyWSlx5SN+XY53xKhsR7GIDo17j7z4irIoz22ZP9kQJrJJtSx\/M\/\/PGqFoomx+ieycbdF\/bzXnIwiwecmYOSnpBhyS5m9l3jkNbeHtb3bJv4T\/TKRo1kxRPizsm69J5sGk8XpDaZe6HpdoibcW0rVSbdpp7KisjQcp8B4W2wl9ooIn8iXrNZ9UM5RJgqmoWmUlQ\/58GKuQSSG5Ju1+n8Dk8dtWX20eNw5oZ90kpoq99k7\/B+0a6tbZyI8oHJr9GCLg+NQgxUnpGEt1of66ZTXN9XDGDfSbfiecAyvq4PnbAkYRrCuQYDSagrUuXk3BAYHTidRL7CwMomgksFxC4WAIyqwrhkDXjGoDJMMECIRd2ny\/lWO3wnCk+ZC8sb\/QaT6cwg1SFsYoGXpSOT2Pxh5tgVroDsrtUskCK8z74oRaMX92oI1mTH6yOzEsaIeuBqi9Rm4Dsg7ZYVJfMwKdat4Rff7SsLdQ7czHy2+i8UhFInOor2rLXCKaysbeOV8JuUKxsnwUC7iY9m++Yvh4uhJr5a3ZXJQ2myn1KSZdA2EjCo\/S4V0K+uUKw5o+R6CjI0aR2rNppKdWsQQ3IcnOQlf5DD+A4M4nVi9BGUoOu2wviwce\/PlieTmRUD8tFQRSoEJRjfvEovW\/fiGAkl7nt\/x7wcbiR92EHagStfv6QXfIja18uI4RTUdRMaMte1M3xokS4FPp7qZN6j1nZfTBF5CnR62ByOjmYQ\/\/7jGe7w2Rt9yu\/FKG9QfqgYECPLpEaCewah+13zLp2FUR1ApMn5qYnGlVhPM6lN1Y9Zc+BMQdWHuJTSBT6D69N8g+2ggOhrOZ7aYdKMk10FUF+NFhXBr5DxaswYLuVNDPDP1+tpH+KU4\/CpbcyVVJkGip2nSOn94Us0dfpgmdR9AzUj5XRZ5PMJUhgqZFcVuZPFKpYFo77mR5U4ppF9B7fwdT3TOUsp7nENWOHxwAwGX5aKehASMk\/A4LLoCGajLZwmP7fkSAEUvMll2FkrtGtn2VIfJG7UFk2NYCOB5V\/e9W8RSYfS\/SR3hWn6uoejVzPJhMW8kJp20PBIDbUll+LZv6+X9txt+SVi+g2ZC\/qRtNR\/U528BlAr+JJvnKSMeG5EuSGK0v7hXSPOtm0ufFDCE+Xm0iLWmgyszBHvLcr7tk29egMaIVSlTQxOCRXSCrf2lvX0KkAx0bw5hIRYAupoVol3jRbHEvxxoQkYuVNcq\/cER4vzUrBEIFFAN+hm37m3l5ADbnojtlzLxI\/xyNb7UuE\/EoIB6ih2mZnwAp8zC\/fOFkuDSEn7YxQACM0mForAbNF+12qZoND+dgREOJR\/S217V4Q0A6pWyMlQ42xpjiSDyNG5jvv3KToXwNU+1yqKYB4i4uYBNw2GBhEjsQp9n7SkFOMkuKjXen5THBTuFDCRb18yu7xA+5D\/TmBDs3bTEFy3qjomXJp3aKfHJ2HFDmlFSqOvmYVOrpa0FkAyVWsIE6\/8WySn\/41kQDVIMPzmSSWAEAW4K3Sc8fxtoTngJNFU9xNNN5RvIuWiO5e0aZcZQKWl0jNZkpZCnrkJL7WXd0VcVVaNC4xOiPczUJrhLFV\/cBu\/PzqQbbLwqObVPiBUD97hkpoawfd8liLg4S0kVmSBTDwNHhjbfiLHhpVVaGvWpWt485bqmnKZ05Ojm6jK2bDPwH0Kmkvsqf++ZuspDGhSrM78vK85iEdL\/c0PuBwqwktuv\/3i0ANr+HY82AiMBHyYD27Z5RXiV7JuM9io\/6MQQN+AnPI3qNiGqD\/WKVxsjDXLWGsy1Kn3YHQvNJ\/5YpdssULwyrdMAGoRy+RHLdkEL5Yh7mgUWgN5XXtK2p\/hCt5h72WS4Hq10rJUwDXX4xnsx3xGMZBlZSBL98KEJMRqSUl3WlxdB1M2g0XXF4xq72yPp59rQPRV7Gve77WD0TO+CGmpVkoxp+X7tQ+tvvwGW2\/L9Pm2cDy7++BlbqHulIFU9LJeSPRDhmgLoe87al7Fh3Y\/LATa\/y4wcRdifTF2Z+u5Sw==",
"status": 1,
"info": "",
"is_encrypt": 1
}

过frida检测

使用rusda + hook clone nop掉多线程检测即可

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
function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}

function hook_clone(soname)
{
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// args[3] 子线程的栈地址。如果这个值为 0,可能意味着没有指定栈地址
if(args[3] != 0){
var addr = args[3].add(96).readPointer()
var so_name = Process.findModuleByAddress(addr).name;
var so_base = Module.getBaseAddress(so_name);
var offset = (addr - so_base);
//console.log("===============>", so_name, addr,offset, offset.toString(16));
if(so_name.indexOf(soname) >= 0) {
console.log("===============>", so_name, addr,offset, offset.toString(16));
nop_64(addr)
}
}
},
onLeave: function(retval) {

}
});

}
setImmediate(hook_clone, "libDexHelper.so")

脱壳

直接frida-dexdumpdex出来的会少很多逻辑,是抽取壳。之前用frida fart折腾了好久,脱出来的也并不完整,dex层的脱壳直接用https://56.al/就好了,虽然也少了类,但好多了

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//typed by hanbingle,just for fun!!
//email:edunwu@gmail.com
//只是对Android 8 版本进行了测试,其他版本请自行移植
/*使用说明
首先拷贝fart.so和fart64.so到/data/app目录下,并使用chmod 777 设置好权限,然后就可以使用了。
该frida版fart是利用反射的方式实现的函数粒度的脱壳,与使用hook方式实现的方法不同,可以使用spawn和attach两种方式使用。
使用方式1、以spawn方式启动app,等待app进入Activity界面后,执行fart()函数即可
使用方式2、app启动后,使用frida -U直接attach上进程,执行fart()函数即可
高级用法:可以调用dump(classname),传入要处理的类名,只完成对某一个类下的所有函数的CodeItem完成dump,效率更高,dump下来的类函数的所有CodeItem在含有类名的bin文件中。
* */
var addrGetDexFile = null;
var funcGetDexFile = null;
var addrGetObsoleteDexCache = null;
var addrGetCodeItemLength = null;
var funcGetCodeItemLength = null;
var addrBase64_encode = null;
var funcBase64_encode = null;
var addrFreeptr = null;
var funcFreeptr = null;
//需要保存的路径,默认直接保存到sdcard
var savepath = "/sdcard";

function DexFile(start, size) {
this.start = start;
this.size = size;
}

function ArtMethod(dexfile, artmethodptr) {
this.dexfile = dexfile;
this.artmethodptr = artmethodptr;
}

function dumpcodeitem(methodname, artmethodobj, fileflag) {
if (artmethodobj != null) {
var dexfileobj = artmethodobj.dexfile;
var dexfilebegin = dexfileobj.start;
var dexfilesize = dexfileobj.size;
var dexfile_path = savepath + "/" + dexfilesize + "_" + Process.getCurrentThreadId() + ".dex";
var dexfile_handle = null;
try {
dexfile_handle = new File(dexfile_path, "r");
if (dexfile_handle && dexfile_handle != null) {
dexfile_handle.close()
}

} catch (e) {
dexfile_handle = new File(dexfile_path, "a+");
if (dexfile_handle && dexfile_handle != null) {
var dex_buffer = ptr(dexfilebegin).readByteArray(dexfilesize);
dexfile_handle.write(dex_buffer);
dexfile_handle.flush();
dexfile_handle.close();
console.log("[dumpdex]:", dexfile_path);
}
}
var artmethodptr = artmethodobj.artmethodptr;
var dex_code_item_offset_ = Memory.readU32(ptr(artmethodptr).add(8));
var dex_method_index_ = Memory.readU32(ptr(artmethodptr).add(12));
if (dex_code_item_offset_ != null && dex_code_item_offset_ > 0) {
var dir = savepath;
var file_path = dir + "/" + dexfilesize + "_" + Process.getCurrentThreadId() + "_" + fileflag + ".bin";
var file_handle = new File(file_path, "a+");
if (file_handle && file_handle != null) {
var codeitemstartaddr = ptr(dexfilebegin).add(dex_code_item_offset_);
var codeitemlength = funcGetCodeItemLength(ptr(codeitemstartaddr));
if (codeitemlength != null & codeitemlength > 0) {
Memory.protect(ptr(codeitemstartaddr), codeitemlength, 'rwx');
var base64lengthptr = Memory.alloc(8);
var arr = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
Memory.writeByteArray(base64lengthptr, arr);
var base64ptr = funcBase64_encode(ptr(codeitemstartaddr), codeitemlength, ptr(base64lengthptr));
var b64content = ptr(base64ptr).readCString(base64lengthptr.readInt());
funcFreeptr(ptr(base64ptr));
var content = "{name:" + methodname + ",method_idx:" + dex_method_index_ + ",offset:" + dex_code_item_offset_ + ",code_item_len:" + codeitemlength + ",ins:" + b64content + "};";
file_handle.write(content);
file_handle.flush();
file_handle.close();
}

} else {
console.log("openfile failed,filepath:", file_path);
}
}


}

}

function init() {
console.log("go into init," + "Process.arch:" + Process.arch);
var module_libext = null;
if (Process.arch === "arm64") {
module_libext = Module.load("/data/app/fart64.so");
} else if (Process.arch === "arm") {
module_libext = Module.load("/data/app/fart.so");
}
if (module_libext != null) {
addrGetDexFile = module_libext.findExportByName("GetDexFile");
funcGetDexFile = new NativeFunction(addrGetDexFile, "pointer", ["pointer", "pointer"]);
addrGetCodeItemLength = module_libext.findExportByName("GetCodeItemLength");
funcGetCodeItemLength = new NativeFunction(addrGetCodeItemLength, "int", ["pointer"]);
addrBase64_encode = module_libext.findExportByName("Base64_encode");
funcBase64_encode = new NativeFunction(addrBase64_encode, "pointer", ["pointer", "int", "pointer"]);
addrFreeptr = module_libext.findExportByName("Freeptr");
funcFreeptr = new NativeFunction(addrFreeptr, "void", ["pointer"]);
}
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("ArtMethod") >= 0 && symbol.name.indexOf("GetObsoleteDexCache") >= 0) {
addrGetObsoleteDexCache = symbol.address;
break;
}
}
}

function dealwithmethod(classname, method) {
var jnienv = Java.vm.getEnv();
var ArtMethodptr = jnienv.fromReflectedMethod(ptr(method.$handle));
var DexFileptr = funcGetDexFile(ptr(ArtMethodptr), ptr(addrGetObsoleteDexCache));
if (DexFileptr != null) {
var dexfilebegin = Memory.readPointer(ptr(DexFileptr).add(Process.pointerSize * 1));
var dexfilesize = Memory.readU32(ptr(DexFileptr).add(Process.pointerSize * 2));
var dexfileobj = new DexFile(dexfilebegin, dexfilesize);
if (ArtMethodptr != null) {
var artmethodobj = new ArtMethod(dexfileobj, ArtMethodptr);
dumpcodeitem(classname + "->" + method.toString(), artmethodobj, 'all');
}
}
}

function dumpmethod(classname, method) {
console.log("start dump method:" + classname + "---" + method.toString());
var jnienv = Java.vm.getEnv();
var ArtMethodptr = jnienv.fromReflectedMethod(ptr(method.$handle));
var DexFileptr = funcGetDexFile(ptr(ArtMethodptr), ptr(addrGetObsoleteDexCache));
if (DexFileptr != null) {
var dexfilebegin = Memory.readPointer(ptr(DexFileptr).add(Process.pointerSize * 1));
var dexfilesize = Memory.readU32(ptr(DexFileptr).add(Process.pointerSize * 2));
var dexfileobj = new DexFile(dexfilebegin, dexfilesize);
if (ArtMethodptr != null) {
var artmethodobj = new ArtMethod(dexfileobj, ArtMethodptr);
dumpcodeitem(classname + "->" + method.toString(), artmethodobj, classname);
}
}
}

function dumpclass(classname) {
if (Java.available) {
Java.perform(function () {
console.log("go into enumerateClassLoaders!");
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
var loadclass = loader.loadClass(classname);
console.log(loader + "-->loadclass " + classname + " success!");
var methods = loadclass.getDeclaredConstructors();
for (var i in methods) {
dumpmethod(classname, methods[i]);
}
methods = loadclass.getDeclaredMethods();
for (var i in methods) {
dumpmethod(classname, methods[i]);
}
} catch (e) {
//console.log("error", e);
}

},
onComplete: function () {
//console.log("find Classloader instance over");
}
});
});
}
}

function dealwithClassLoader(classloaderobj) {
if (Java.available) {
Java.perform(function () {
try {
var dexfileclass = Java.use("dalvik.system.DexFile");
var BaseDexClassLoaderclass = Java.use("dalvik.system.BaseDexClassLoader");
var DexPathListclass = Java.use("dalvik.system.DexPathList");
var Elementclass = Java.use("dalvik.system.DexPathList$Element");
var basedexclassloaderobj = Java.cast(classloaderobj, BaseDexClassLoaderclass);
var tmpobj = basedexclassloaderobj.pathList.value;
var pathlistobj = Java.cast(tmpobj, DexPathListclass);
console.log("pathlistobj->" + pathlistobj);
var dexElementsobj = pathlistobj.dexElements.value;
console.log("dexElementsobj->" + dexElementsobj);
for (var i in dexElementsobj) {
var obj = dexElementsobj[i];
var elementobj = Java.cast(obj, Elementclass);
console.log("elementobj->" + elementobj);
tmpobj = elementobj.dexFile.value;
var dexfileobj = Java.cast(tmpobj, dexfileclass);
var enumeratorClassNames = dexfileobj.entries();
while (enumeratorClassNames.hasMoreElements()) {
var classname = enumeratorClassNames.nextElement().toString();
console.log("start loadclass->" + classname);
var loadclass = classloaderobj.loadClass(classname);
console.log("after loadclass->" + classname);
var methods = loadclass.getDeclaredConstructors();
for (var i in methods) {
dealwithmethod(classname, methods[i]);
}
methods = loadclass.getDeclaredMethods();
for (var i in methods) {
dealwithmethod(classname, methods[i]);
}
}

}
} catch (e) {
console.log(e);
}

});
}


}

function fart() {
if (Java.available) {
Java.perform(function () {
console.log("go into enumerateClassLoaders!");
Java.enumerateClassLoaders({
onMatch: function (loader) {
if (loader.toString().indexOf("BootClassLoader") >= 0) {
console.log("this is a BootClassLoader!")
} else {
try {
console.log("startdealwithclassloader:", loader, '\n');
dealwithClassLoader(loader);
} catch (e) {
console.log("error", e);
}
}
},
onComplete: function () {
console.log("find Classloader instance over");
}
});
});
}
}

function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}

function hook_clone(soname)
{
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// args[3] 子线程的栈地址。如果这个值为 0,可能意味着没有指定栈地址
if(args[3] != 0){
var addr = args[3].add(96).readPointer()
var so_name = Process.findModuleByAddress(addr).name;
var so_base = Module.getBaseAddress(so_name);
var offset = (addr - so_base);
//console.log("===============>", so_name, addr,offset, offset.toString(16));
if(so_name.indexOf(soname) >= 0) {
console.log("===============>", so_name, addr,offset, offset.toString(16));
nop_64(addr)
}
}
},
onLeave: function(retval) {

}
});

}

setImmediate(hook_clone, "libDexHelper.so")

算法定位

dump so

在主类中,可以看到基本上所有函数都被保护了起来,并走了JniLib的native层调用

image-20260221114020463

接下来尝试分析libdexjni.so

image-20260221114057481

libdexjni.so也进行了一波加固

image-20260224022202109

这里选择在加载完毕后再进行dump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function dump_so(so_name) {
so_name = "libdexjni.so";
var libso = Process.getModuleByName(so_name);
console.log("[name]:", libso.name);
console.log("[base]:", libso.base);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);
var file_path = "/sdcard/Download/步道乐跑/419/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
Memory.protect(ptr(libso.base), libso.size, 'rwx');
var libso_buffer = ptr(libso.base).readByteArray(libso.size);
file_handle.write(libso_buffer);
file_handle.flush();
file_handle.close();
console.log("[dump]:", file_path);
}
}

在dump出来后要使用dexRepair进行修复

image-20260221121146481

接下来就可以定位到动态注册JNI函数的位置,但也是经过了大量混淆,甚至没办法反编译

image-20260224022255322

JNI hook

由于是第一次碰上,对于这个dexvmp保护,我的猜想是,它们不过是通过解析java层smali的语句,并通过jni反射调用来实现保护。那么只需要hook所有JNI的反射调用,通过jmethod获取原函数签名,然后再进行参数解析,就可以获取执行流进行分析

image-20260221121738981

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
function parseValue(ptr, typeName) {
if (ptr.isNull() && !["int", "boolean", "long", "float", "double", "short", "byte", "char"].includes(typeName)) {
return "null";
} else if (["int", "short", "byte", "char"].includes(typeName)) {
return ptr.toInt32();
} else if (typeName === "boolean") {
return (ptr.toInt32() === 1) ? "true" : "false";
} else if (typeName === "long") {
return ptr.toString();
} else if (typeName === "float" || typeName === "double") {
return "<Float/Double>";
} else if (typeName === "java.lang.String") {
let strObj = Java.cast(ptr, Java.use('java.lang.String'));
return `"${strObj.toString()}"`;
} else {
// 普通 Object 或 Array
let obj = Java.cast(ptr, Java.use('java.lang.Object'));
return `[${obj.getClass().getName()}] ${obj.toString()}`;
}
}


function hook_libdexjni()
{
Java.perform(function() {
var so_name = "libdexjni.so";
var libso = Process.getModuleByName(so_name);
var base_addr = libso.base
console.log("[name]:", libso.name);
console.log("[base]:", base_addr);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);


Interceptor.attach(base_addr.add(0x00737CC), {

onEnter: function(args) {
console.log("JNIEnv::CallStaticObjectMethod")

},
onLeave: function(retval){
console.log("JNIEnv::CallStaticObjectMethod res")
}

});

Interceptor.attach(base_addr.add(0x00737CC), {

onEnter: function(args) {
console.log("JNIEnv::CallStaticObjectMethod")

},
onLeave: function(retval){
console.log("JNIEnv::CallStaticObjectMethod res")
}

});

Interceptor.attach(base_addr.add(0x02AC50), {

onEnter: function(args) {
this.shouldParseReturn = false; // 初始化标志位
let envPtr = args[0];
let objPtr = args[1];
let methodIdPtr = args[2];

try {
let jniNativeInterface = envPtr.readPointer();

// 1. 获取对象的 jclass (索引 31)
let getObjectClassFunc = new NativeFunction(
jniNativeInterface.add(31 * Process.pointerSize).readPointer(),
'pointer', ['pointer', 'pointer']
);
let classPtr = getObjectClassFunc(envPtr, objPtr);

// 2. 将 jmethodID 转换为 Method (索引 9)
let toReflectedMethodFunc = new NativeFunction(
jniNativeInterface.add(9 * Process.pointerSize).readPointer(),
'pointer', ['pointer', 'pointer', 'pointer', 'uint8']
);
let reflectedMethodPtr = toReflectedMethodFunc(envPtr, classPtr, methodIdPtr, 0);

if (!reflectedMethodPtr.isNull()) {
// 3. 使用 Java API 解析 Method 签名
let javaMethod = Java.cast(reflectedMethodPtr, Java.use('java.lang.reflect.Method'));
let className = javaMethod.getDeclaringClass().getName();
let methodName = javaMethod.getName();
let paramTypes = javaMethod.getParameterTypes(); // 获取所有参数类型
this.returnTypeName = javaMethod.getReturnType().getName(); // 获取返回值类型
this.shouldParseReturn = true;
// 打印函数签名
let paramNames = paramTypes.map(function(p) { return p.getName(); });
console.log(`\n[+] CallObjectMethod: ${className}.${methodName}(${paramNames.join(', ')}).${this.returnTypeName}`);

// 4. 自动匹配并解析传入的参数 (从 args[3] 开始)
for (let i = 0; i < paramTypes.length; i++) {
let typeName = paramTypes[i].getName();
let argPtr = args[3 + i];
let argValueStr = parseValue(argPtr, typeName);
console.log(` |-- arg[${i}] (${typeName}): ${argValueStr}`);
}


}
} catch (e) {
console.error("[-] General Error: " + e);
}

},
onLeave: function(retval){
console.log("JNIEnv::CallObjectMethod res")

//if (!this.shouldParseReturn) return;

// 如果是 CallVoidMethod,直接跳过解析
if (this.returnTypeName === "void") {
console.log(` |<-- [Return] void`);
return;
}

console.log(this.returnTypeName)

let retValueStr = "unknown";
try {
retValueStr = parseValue(retval, this.returnTypeName);
} catch (e) {
retValueStr = `<Parse Error>`;
}

console.log(` |<-- [Return] : ${retValueStr}`);
}

});

});

}

虽然理想很丰满,效果也有,但毕竟这样还是太过于混乱和庞大,分析效率低下,于是选择换一种思路

image-20260221131631472

unidbg

在主类中可以找到JNIUtils这个类,看起来与加解密有关,直接尝试unidbg模拟一波

image-20260221144009778

这里需要补环境的地方不多,主要是对stringBuilder的处理

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package budaolepao;


import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;

import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import unicorn.Arm64Const;
import unicorn.Unicorn;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.HashMap;


public class MainActivity extends AbstractJni {

private final AndroidEmulator emulator;
private final VM vm;
private final Module module;


private final boolean logging;

void addHooks() {
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
System.out.println("hook..asddddddddddddddddddddddddddddddddddddddd.");
RegisterContext context = emulator.getContext();

byte[] soBytes = emulator.getBackend().mem_read(module.base, 0x012c000);
System.out.printf("%x ", soBytes[1] & 0xff);
//System.out.println("hook 0x2418c");
//emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, module.base + 0x24190);
//long x0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();
//System.out.println(x0);

}

@Override
public void onAttach(Unicorn.UnHook unHook) {}
@Override
public void onAttach(UnHook unHook) {}
@Override
public void detach() {}
},module.base + 0x333ac ,module.base +0x333ac + 3,"a123456");

}


MainActivity(boolean logging) throws FileNotFoundException {
this.logging = logging;

emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.qidian.dldl.official")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析


vm = emulator.createDalvikVM(new File("D:\\Reverse\\realworld\\budaolepao\\bdlp.apk")); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
vm.setJni(this);

DalvikModule dm = vm.loadLibrary(new File("D:\\Reverse\\realworld\\budaolepao\\419\\libs\\libdexjni.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数

module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块

Debugger attach = emulator.attach();
//addHooks();
//attach.addBreakPoint(module.base + 0x333ac);
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
//attach.addBreakPoint(module.base + 0x24188);



String tracefile = "minil2025_trace";
PrintStream traceStream = null;
try {
traceStream = new PrintStream(new FileOutputStream(tracefile), true);
}catch (FileNotFoundException e) {
e.printStackTrace();
}
// emulator.traceCode(module.base, module.base + 0x0000000000063290).setRedirect(traceStream);
}


void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}

public static void main(String[] args) throws Exception {
MainActivity test = new MainActivity(true);

test.callFunc();

test.destroy();
}

boolean callFunc() {

DvmClass jniLibClass = vm.resolveClass("com/fort/andjni/JniLib");
DvmClass jniUtilsClass = vm.resolveClass("com/lptiyu/tanke/jni/JNIUtils");

// 准备字符串参数 (替换为你实际要加密的数据)
StringObject keyObj = new StringObject(vm, "your_key");
StringObject ivObj = new StringObject(vm, "your_iv");
StringObject saltObj = new StringObject(vm, "your_salt");
StringObject transObj = new StringObject(vm, "AES/CBC/PKCS5Padding");
StringObject encryptObj = new StringObject(vm, "data_to_encrypt");

// 准备 Integer 参数 (Java中 12930 会被自动装箱为 Integer,所以在 unidbg 中必须是 DvmInteger)
DvmInteger intObj = DvmInteger.valueOf(vm, 12930);

// ★ 核心步骤:构造 Object... 对应的数组 (ArrayObject) ★
// 第一个参数就是 jniUtilsClass
ArrayObject objArray = new ArrayObject(
jniUtilsClass, // 第 1 个元素:JNIUtils.class
keyObj, // 第 2 个元素:key
ivObj, // 第 3 个元素:iv
saltObj, // 第 4 个元素:salt
transObj, // 第 5 个元素:transformation
encryptObj, // 第 6 个元素:encrypt
intObj // 第 7 个元素:12930
);


DvmObject<?> result = jniLibClass.callStaticJniMethodObject(
emulator,
"cL([Ljava/lang/Object;)Ljava/lang/Object;",
objArray // 这里把整个数组作为一个参数传进去
);

// 获取返回值
if (result != null) {
System.out.println("Result: " + result.getValue());
}
return true;
}


@Override
public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "android/os/Build$VERSION->SDK_INT:I":
{
return 114;
}
}
return super.getStaticIntField(vm, dvmClass, signature);
}

@Override
public DvmObject<?> allocObject(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "java/lang/StringBuilder->allocObject":
{
return dvmClass.newObject(new StringBuilder());
}
}

return super.allocObject(vm, dvmClass, signature);
}

@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/lang/StringBuilder-><init>()V":
// allocObject 已经 new 过 StringBuilder 了,这里直接 return 即可
StringBuilder stringBuilder = new StringBuilder();
return vm.resolveClass("java/lang/StringBuilder").newObject(stringBuilder);

}
return super.newObjectV(vm, dvmClass, signature, vaList);
}

@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/StringBuilder->append(Ljava/lang/String;)Ljava/lang/StringBuilder;":
{
StringBuilder stringBuilder = (StringBuilder) dvmObject.getValue();
String newString = (String)vaList.getObjectArg(0).getValue();
System.out.println(newString);
stringBuilder.append(newString);
return vm.resolveClass("java/lang/StringBuilder").newObject(stringBuilder);
}
case "java/lang/StringBuilder->toString()Ljava/lang/String;":
{
StringBuilder stringBuilder = (StringBuilder) dvmObject.getValue();
String newString = stringBuilder.toString();
return vm.resolveClass("java/lang/String").newObject(newString);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}


}

在一顿操作后,最终停在了g/z/c/e0/j->b这个方法

image-20260221143848474

回到jadx,我并没有找到g/z/c/e0/j这个类,但在其周围,我发现大量与流量协议相关的类,且都是有用的

也就是说这个壳也许就是个皮套?只不过是把真实的类名混淆隐藏起来了,那这样就很草蛋了啊

image-20260221144154252

算法解密

后面就是很常规的java层代码分析了,走官方的加密库,AES_CBC,key和iv都是写明的,我甚至跑个算法自吐脚本可能都直接出来了,那就有点无语了

image-20260224024356064

总结

可见该壳在面对实际的应用场景时,还是有点敷衍了,甚至连算法的核心也没有进行保护,真的就是简单套了个壳

  • 标题: 某梆梆加固企业版APP协议分析
  • 作者: 两只羊
  • 创建于 : 2026-02-24 02:06:51
  • 更新于 : 2026-02-24 02:54:52
  • 链接: https://twogoat.github.io/2026/02/24/某梆梆加固企业版APP协议分析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论