开源dpt-shell加固分析

Intro

本来笔记是五月份就做好的,出于原因搁置了,想了想还是放出来算了,充当留给学弟学妹的彩蛋了

项目地址:https://github.com/luoyesiqiu/dpt-shell](https://github.com/luoyesiqiu/dpt-shell)

分析版本:V1.12.2

正文

java -jar dpt.jar -f /path/to/apk

java&bitcode

入口点


到最后还是跟进classloader

定位到dpt加固so

自定义加密.bitcode段

Segments shift + f7

dlopen, load library后是initArray段执行构造函数,即加固so加载后的执行解密阶段,追踪initArray段

跟进,这里改了a1的变量类型和变量名

函数内部在寻找bitcode段地址

返回上级函数,寻址完后修改内存页权限

那么后面就是解密了

rc4,上图DPT为密钥

idapy解密

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
import idc
import ida_segment
import idautils

def rc4_decrypt(key, data):
"""RC4解密实现"""
S = list(range(256))
j = 0
out = []

# KSA初始化
for i in range(256):
j = (j + S[i] + key[i % 16]) % 256
S[i], S[j] = S[j], S[i]

# PRGA生成密钥流并解密
i = j = 0
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
out.append(byte ^ k)
return bytes(out)

def decrypt_bitcode():
# 配置目标段名(根据步骤1结果修改)
target_segment = ".bitcode" # 修改为你的段名

# 获取段对象
seg = ida_segment.get_segm_by_name(target_segment)
if not seg:
print(f"[!] 错误:未找到段 '{target_segment}'")
return

start_ea = seg.start_ea
end_ea = seg.end_ea
print(f" 找到段 {target_segment}: 0x{start_ea:X}-0x{end_ea:X}")

# 读取加密数据
encrypted_data = idc.get_bytes(start_ea, end_ea - start_ea)
if not encrypted_data:
print("[!] 错误:无法读取段数据")
return

# 定义RC4密钥
rc4_key = bytes([
0x16, 0xF8, 0x85, 0x20, 0x3, 0x38, 0xF8, 0x6B,
0x90, 0x74, 0x7, 0x90, 0xFC, 0xE3, 0xC2, 0xA2,
0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00
])

# 执行解密
decrypted_data = rc4_decrypt(rc4_key, encrypted_data)
print("[+] 解密完成,正在写回IDA数据库...")

# 临时修改段权限为可写
original_perms = idc.get_segm_attr(start_ea, idc.SEGATTR_PERM)
idc.set_segm_attr(start_ea, idc.SEGATTR_PERM, 0x7) # RWX

# 逐字节修补数据
for offset, byte in enumerate(decrypted_data):
idc.patch_byte(start_ea + offset, byte)

# 恢复段权限
idc.set_segm_attr(start_ea, idc.SEGATTR_PERM, original_perms)

print("[+] 解密数据已成功写入,建议重新分析代码区域!")
print(" 操作完成!")

# 执行解密函数
decrypt_bitcode()

执行完后保存再重载文件会看到bitcode段代码被成功识别

保存patch不保存ida数据库,overwrite

接下来分析frida检测点

frida检测

字符串定位回溯可以发现是解密后fork进程进行校验

sub_4E380->sub_100E0中采用自定义strstr逐个字符校验,避免系统strstr被hook,但是特征检测是明文的

检测到后返回sub_4E864空函数导致崩溃

1
2
3
4
5
6
7
8
9
10
void sub_4E864()
{
;
}

.bitcode:000000000004E864 sub_4E864 ; CODE XREF: sub_4D608+46C↑p
.bitcode:000000000004E864 ; sub_4E86C+24↓j ...
.bitcode:000000000004E864 ; __unwind {
.bitcode:000000000004E864 MOV X30, #0
.bitcode:000000000004E868 RET

X30寄存器在ARM64中相当于rsp,在ret之前储存的是返回地址,这里函数将X30赋值为0之后就会产生一个 Process crashed: Bad access due to invalid address 的报错,导致程序崩溃。
直接用空函数将其替换

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
function antiDetectFrida(Base) {
var crashAddr = Base.add("0x4E864");

var originalFunc = new NativeFunction(crashAddr, 'void', []);
Interceptor.replace(originalFunc, new NativeCallback(function () {
// console.log("[Replaced] - Empty function executed");
console.log('sub_4E894 called from:\n' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n');

}, 'void', []));
}

function NativeFunc() {
console.info("[Hook Beging]");
var Base = Module.getBaseAddress("libdpt.so");
console.warn("[Base]->", Base);
antiDetectFrida(Base);
}

function hook_android_dlopen_ext() {
var isHook = false;
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
this.name = args[0].readCString();
if (this.name.indexOf("libdpt.so") > 0) {
console.log(this.name);
var symbols = Process.getModuleByName("linker64").enumerateSymbols();
var callConstructorAdd = null;
for (var index = 0; index < symbols.length; index++) {
const symbol = symbols[index];
if (symbol.name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
callConstructorAdd = symbol.address;
}
}
console.log("callConstructorAdd -> " + callConstructorAdd);
Interceptor.attach(callConstructorAdd, {
onEnter: function (args) {
if (!isHook) {
NativeFunc();
isHook = true;
}
},
onLeave: function () { }
});

}
}, onLeave: function () { }
});
}

setImmediate(hook_android_dlopen_ext);

Dex填充

接下来考虑检测绕过,解密完后的原dex填充

思路Get:首先我们需要知道抽取壳,肯定是要对dex处理并且回填CodeItem的,那么程序肯定是要对DefineClass或者loadMEthod来在执行方法之前回填正确的字节码,那么让我们看一下在执行一个Java方法时的调用链—luoyesiqiu

1
ClassLoader.java::loadClass -> DexPathList.java::findClass -> DexFile.java::defineClass -> class_linker.cc::LoadClass -> class_linker.cc::LoadClassMembers -> class_linker.cc::LoadMethod

可以看出hook回填dex的痕迹


插曲

然后该hook格式跟shadowwhook相似:https://github.com/bytedance/android-inline-hook

真得吐槽句,swdd师傅的博客知识点太密了,难搞哦,但是之前那段时间研究壳老是能搜到swdd师傅的笔记Orz

模板类似如下

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
#include "shadowhook.h"

void *shadowhook_hook_func_addr(
void *func_addr,
void *new_addr,
void **orig_addr);

void *shadowhook_hook_sym_addr(
void *sym_addr,
void *new_addr,
void **orig_addr);

void *shadowhook_hook_sym_name(
const char *lib_name,
const char *sym_name,
void *new_addr,
void **orig_addr);

typedef void (*shadowhook_hooked_t)(
int error_number,
const char *lib_name,
const char *sym_name,
void *sym_addr,
void *new_addr,
void *orig_addr,
void *arg);

void *shadowhook_hook_sym_name_callback(
const char *lib_name,
const char *sym_name,
void *new_addr,
void **orig_addr,
shadowhook_hooked_t hooked,
void *hooked_arg);

int shadowhook_unhook(void *stub);

hook填充完后会返回原来的状态,关注此时相应函数的DexFile参数

n28是sdk版本,我们研究第二个

跟进sub_4D608,内部是具体内容,那么参数在a3,a6,a7之间

文件读取有指针判断是否为空,对应过去确定为a6

这里在读静态资源,后续对assets文件夹下的加密文件进行处理

DexFile对象结构如下

1
2
3
4
5
6
7
8
9
10
11
DexFile::DexFile(
const uint8_t* base, //dex文件基址
size_t size, // dex文件长度
const uint8_t* data_begin,
size_t data_size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
std::unique_ptr<DexFileContainer> container,
bool is_compact_dex
)

第一个是基地址,第二个是长度,那么只需要这两个我们就可以dump下来完整的dexfile了,那么这个时候我们,使用如下(frida代码spwn启动,注意dlopen时机)

tab切过去对应地址hook,确保稳定hook选0x4DB44

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function analysisDex(Base) {

var originalDefineClass = Base.add("0x4DAF4");
console.log("originalDefineClassAddr->", originalDefineClass)
Interceptor.attach(originalDefineClass, {
onEnter: function (args) {
this.dex_file = this.context.x5;
console.log(hexdump(this.context.x5))
},
onLeave: function (args) {
var dex_file = this.dex_file;
}
})

}

C++的调用约定里面第一个参数实际上是this指针,解析时需要跳过这个指针的,据此修改代码和之前Dexfile对应的参数呼应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function analysisDex(Base) {
var originalDefineClass = Base.add("0x4DAF4");
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);
console.log("[DexFile]-> magic = ", magic);
},
onLeave: function (args) {
}
})

}

在满足 DexFile 格式规范的前提下,针对同一 Dex 文件基址(Base)存在多次调用的现象。这主要是因为抽取壳并非在加载阶段一次性完成填充,而是采用运行时动态回填指令(insns)的机制,因此会对同一个 Dex 文件进行多次操作。
此外,从 Hook 逻辑中可以看出,dpt-shell 并未在方法装载完成后将其卸载。事实上,出于避免引发严重性能损耗的考量,绝大多数厂商均不会采用此类设计。
基于上述机制,我们可以构建一个映射表(Maps),用于记录所有不同的 Dex 文件基址(Base)及其大小(Size)。待程序完全加载后,只需遍历该映射表,即可高效完成 Dex 文件的 Dump 操作。

完整脚本

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
// frida -U -f com.ma5k.polis -l dump.js 

'use strict';

const dexMap=new Map();
var dex_count=1;

function get_self_process_name(){
var open=new NativeFunction(Module.getExportByName('libc.so','open'),'int',['pointer','int']);
var read=new NativeFunction(Module.getExportByName('libc.so','read'),'int',['int','pointer','int']);
var close=new NativeFunction(Module.getExportByName('libc.so','close'),'int',['int']);
var path=Memory.allocUtf8String("/proc/self/cmdline");
var fd=open(path,0);
if(fd!=-1){
var buf=Memory.alloc(0x1000);
read(fd,buf,0x1000);
close(fd);
return ptr(buf).readCString();
}
return "-1";
}

function chmod(path){
var func=new NativeFunction(Module.getExportByName('libc.so','chmod'),'int',['pointer','int']);
func(Memory.allocUtf8String(path),755);
}

function Mkdir(path){
if(path.indexOf("com")==-1)return;
var mkdir=new NativeFunction(Module.getExportByName('libc.so','mkdir'),'int',['pointer','int']);
var opendir=new NativeFunction(Module.getExportByName('libc.so','opendir'),'pointer',['pointer']);
var closedir=new NativeFunction(Module.getExportByName('libc.so','closedir'),'int',['pointer']);
var cpath=Memory.allocUtf8String(path);
var dir=opendir(cpath);
if(dir!=0){
closedir(dir);
return;
}
mkdir(cpath,0o755);
chmod(path);
console.log("[Mkdir]->",path);
}

function antiDetectFrida(Base){
var crashAddr=Base.add(0x4E864);
console.log("[crashAddr]->",crashAddr);

var range=Process.findRangeByAddress(crashAddr);
if(range){
Memory.protect(range.base,range.size,'rwx');
console.log("[protect]->",range.base,range.size);
}

Interceptor.replace(crashAddr,new NativeCallback(function(){},'void',[]));
console.log("[antiDetectFrida] success");
}

function analysisDex(Base){
var addr=Base.add(0x4DB44);
console.log("[DefineClass]->",addr);

var range=Process.findRangeByAddress(addr);
if(range){
Memory.protect(range.base,range.size,'rwx');
}

Interceptor.attach(addr,{
onEnter:function(args){
try{
this.dex_file=this.context.x5;
if(this.dex_file.isNull())return;

var base=ptr(this.dex_file).add(Process.pointerSize).readPointer();
if(base.isNull())return;

var size=ptr(this.dex_file).add(Process.pointerSize*2).readUInt();
if(size<0x1000||size>0x20000000)return;

var magic=ptr(base).readCString();
if(magic.indexOf("dex")!=0)return;

let dup=false;
for(let [b,s] of dexMap.entries()){
if(b.equals(base)&&s===size){
dup=true;
break;
}
}

if(!dup){
dexMap.set(base,size);
console.log("[DexFile]->",base,size,magic);
}

}catch(e){
console.log("[DefineClass error]->",e);
}
}
});
}

function dumpDex(){
console.log("\n[*] start dump dex\n");

dexMap.forEach((size,base)=>{
try{
var magic=ptr(base).readCString();
if(magic.indexOf("dex")!=0)return;

var process_name=get_self_process_name();
if(process_name=="-1")return;

var dir="/data/data/"+process_name+"/files";
Mkdir(dir);

dir+="/dump_dex_"+process_name;
Mkdir(dir);

var path=dir+"/class"+(dex_count==1?"":dex_count)+".dex";

try{
Memory.protect(ptr(base),size,'rwx');
}catch(e){}

var buf=ptr(base).readByteArray(size);
var fd=new File(path,"wb");

if(fd){
dex_count++;
fd.write(buf);
fd.flush();
fd.close();
console.log("[dump dex]->",path);
}

}catch(e){
console.log("[dumpDex error]->",e);
}
});

console.log("\n[*] dump finished\n");
}

function NativeFunc(){
console.log("[Hook Begin]");

var Base=Module.getBaseAddress("libdpt.so");
console.log("[Base]->",Base);

antiDetectFrida(Base);
analysisDex(Base);
}

function hook_android_dlopen_ext(){
var isHook=false;

Interceptor.attach(Module.findExportByName(null,"android_dlopen_ext"),{
onEnter:function(args){
this.name="";
if(args[0]){
try{
this.name=args[0].readCString();
}catch(e){}
}

if(this.name.indexOf("libdpt.so")>=0){

console.log("[dlopen]->",this.name);

var symbols=Process.getModuleByName("linker64").enumerateSymbols();
var callConstructorAdd=null;

for(var i=0;i<symbols.length;i++){
var s=symbols[i];
if(s.name.indexOf("__dl__ZN6soinfo17call_constructorsEv")!=-1){
callConstructorAdd=s.address;
break;
}
}

console.log("[call_constructors]->",callConstructorAdd);

if(callConstructorAdd&&!isHook){

isHook=true;

Interceptor.attach(callConstructorAdd,{
onEnter:function(args){

console.log("[call_constructors enter]");

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

setTimeout(function(){
dumpDex();
},10000);
}
});
}
}
}
});
}

setImmediate(function(){
console.log("[*] dpt dump start");
hook_android_dlopen_ext();
});

frida -U -f com.ma5k.polis -l dump_dex.js

Refer

https://www.52pojie.cn/thread-2008061-1-1.html


开源dpt-shell加固分析
https://alenirving.github.io/2026/07/11/开源dpt-shell加固分析/
作者
Ma5k
许可协议
CC-BY-NC-SA