前言

1
最近在分析一个文件 梆企,经过分析和研究,成功解密。并用 Python 写出了一个解密脚本,下面记录一下整个过程。

开始

1

1
改apk为zip然后复制so,ida打开。关键33f8,它像是在做运行环境/自身 ELF 文件检查,然后动态获取 sscanf 地址,最后调用 sub_33F8()

1

1
往上跟进,LABEL_15:它基本可以确定是在做读取 /proc/self/maps → 找到自身 ELF 映射 → 读取 ELF 内容 → 创建一块内存映射 → 准备修改/解析 ELF。
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
__int64 __fastcall sub_3184(__int64 a1, int a2)
{
int v2; // w3
int v3; // w2
int v4; // w10
__int64 result; // x0
unsigned __int64 v6; // x12
__int64 v7; // x1
char *v8; // x0
__int64 n256; // x1
unsigned __int64 v10; // x10
char v11; // w6
_BYTE *v12; // x5
int n16; // w1
int j; // w2
char v15; // t1
char v16; // w3
unsigned int v17; // w3
__int64 v18; // x2
__int64 v19; // x1
char v20; // w6
char *v21; // x5
char v22; // t1
char v23; // w3
int i; // [xsp-1Ch] [xbp-14Ch]
_BYTE v25[24]; // [xsp+28h] [xbp-108h] BYREF
__int64 v26; // [xsp+E8h] [xbp-48h]
_BYTE v27[9]; // [xsp+127h] [xbp-9h] BYREF

v26 = *(_QWORD *)off_14FF8[0];
for ( i = 273; i != 70; i = 70 )
;
v2 = dword_15040;
v3 = dword_1507C;
if ( dword_15040 <= a2 )
{
if ( dword_1507C <= a2 )
goto LABEL_6;
LODWORD(v6) = a2 - dword_15040;
v2 = a2;
if ( a2 + 64 <= dword_1507C )
v3 = a2 + 64;
v6 = (int)v6;
v7 = 0;
}
else
{
v4 = a2 + 64;
if ( dword_15040 >= a2 + 64 )
goto LABEL_6;
LODWORD(v7) = dword_15040 - a2;
if ( v4 <= dword_1507C )
v3 = v4;
v6 = 0;
v7 = (int)v7;
}
v8 = (char *)(a1 + v7);
n256 = 0;
v10 = v3 - v2;
do
{
v25[n256] = n256;
++n256;
}
while ( n256 != 256 );
v11 = v25[0];
v12 = v25;
n16 = 1;
for ( j = (unsigned __int8)(qword_15068 + v25[0]); ; j = (unsigned __int8)j )
{
*v12 = v25[j];
v25[j] = v11;
if ( v12 == v27 )
break;
v15 = *++v12;
v11 = v15;
v16 = *((_BYTE *)&qword_15068 + n16++);
LOBYTE(j) = v15 + j + v16;
if ( n16 >= 16 )
n16 = 0;
}
if ( v6 )
{
v17 = 0;
LOBYTE(v18) = 0;
LOBYTE(v19) = 0;
do
{
++v17;
LOBYTE(v19) = v19 + 1;
v20 = v25[(unsigned __int8)v19];
v18 = (unsigned __int8)(v18 + v20);
v25[(unsigned __int8)v19] = v25[v18];
v25[v18] = v20;
}
while ( v6 > v17 );
}
else
{
LOBYTE(v18) = 0;
LOBYTE(v19) = 0;
}
v21 = v8;
if ( v10 )
{
do
{
v22 = *v21++;
v19 = (unsigned __int8)(v19 + 1);
v23 = v25[v19];
v18 = (unsigned __int8)(v18 + v23);
v25[v19] = v25[v18];
v25[v18] = v23;
*(v21 - 1) = v25[(unsigned __int8)(v25[v19] + v23)] ^ v22;
}
while ( v10 > (unsigned int)((_DWORD)v21 - (_DWORD)v8) );
}
LABEL_6:
result = *(_QWORD *)off_14FF8[0];
if ( *(_QWORD *)&v27[1] != *(_QWORD *)off_14FF8[0] )
{
sub_2A00();
return sub_33F8();
}
return result;
}
1
((void (__fastcall *)(char *, _QWORD))sub_3184)(v145, (unsigned int)dword_15040);这段基本可以确定了:sub_3184() 就是在做 RC4-like 解密。v16 = *((_BYTE *)&qword_15068 + n16++);实际上是在不断读取 qword_15068 开始的 16 个字节作为 RC4 Key。

1

1

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
from pathlib import Path
import sys

PAYLOAD_OFFSET = 0x8000
TAIL_OFFSET = 0x1281ED
HEADER_SIZE = 0x40


def rc4_decrypt(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0

for i in range(256):
j = (j + s[i] + key[i % 16]) & 0xFF
s[i], s[j] = s[j], s[i]

out = bytearray(data)
i = j = 0

for n in range(len(out)):
i = (i + 1) & 0xFF
t = s[i]
j = (j + t) & 0xFF
s[i], s[j] = s[j], t
out[n] ^= s[(s[i] + t) & 0xFF]

return bytes(out)


def decrypt(path: Path):
data = path.read_bytes()

if len(data) < TAIL_OFFSET + 17:
raise ValueError("file is too small")

tail = data[TAIL_OFFSET:]
stream_key = tail[:16]
xor_key = tail[16]

payload = bytearray(data[PAYLOAD_OFFSET:TAIL_OFFSET])

if len(payload) < HEADER_SIZE:
raise ValueError("payload is too small")

for i in range(HEADER_SIZE, len(payload)):
payload[i] ^= xor_key

payload[:HEADER_SIZE] = rc4_decrypt(payload[:HEADER_SIZE], stream_key)

return bytes(payload), xor_key


def main():
if len(sys.argv) < 2:
print(f"用法: python {Path(sys.argv[0]).name} input.so [output.so]")
return 1

input_path = Path(sys.argv[1])
output_path = (
Path(sys.argv[2])
if len(sys.argv) > 2
else input_path.with_name(input_path.stem + ".decrypted" + input_path.suffix)
)

inner, xor_key = decrypt(input_path)

if inner[:4] != b"\x7fELF":
raise RuntimeError("解密结果不是 ELF")

output_path.write_bytes(inner)

print(f"input : {input_path}")
print(f"output: {output_path}")
print(f"xor key: {xor_key:#x}")
print(f"size: {len(inner):#x}")

return 0


if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as e:
print(f"error: {e}", file=sys.stderr)
raise SystemExit(1)

1
运行一下python decrypt.py libDexHelper.so  得到libDexHelper.decrypted.so

1

1

1
可以看到完整的展示出来了

dump

1
模拟器中dump下来有些差别.阅读没有这么方便(com.moutai.mall)

1

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
var soName = "libDexHelper-x86.so";

// =============================
// 获取包名(用于路径)
// =============================
function getPackageName() {
var pkg = "unknown";
try {
Java.perform(function () {
var ActivityThread = Java.use("android.app.ActivityThread");
var app = ActivityThread.currentApplication();
if (app) {
pkg = app.getApplicationContext().getPackageName();
}
});
} catch (e) {
console.log("[!] getPackageName failed:", e);
}
return pkg;
}

// =============================
// 等待模块加载
// =============================
// =============================
// 打印 JNI_OnLoad
// =============================
function printJNIOnLoad(module) {
try {
var jni = Module.findExportByName(
module.name,
"JNI_OnLoad"
);

if (!jni) {
var symbols = module.enumerateSymbols();

for (var i = 0; i < symbols.length; i++) {
if (symbols[i].name === "JNI_OnLoad") {
jni = symbols[i].address;
break;
}
}
}

if (jni) {
console.log("[+] JNI_OnLoad:", jni);

var offset = jni.sub(module.base);

console.log(
"[+] JNI_OnLoad offset:",
offset
);

console.log(
"[+] base + offset:",
module.base.add(offset)
);
} else {
console.log("[!] JNI_OnLoad not found");
}

} catch (e) {
console.log("[!] JNI_OnLoad lookup failed:", e);
}
}

function waitModule(cb) {
var t = setInterval(function () {
var m = Process.findModuleByName(soName);
if (m) {
clearInterval(t);
console.log("[+] loaded:", soName);
console.log("[+] base:", m.base);
console.log("[+] size:", m.size);
cb(m);
}
}, 100);
}

// =============================
// ELF 稳定 dump
// =============================
function dumpSoStable(module) {

try {
console.log("======================================");
console.log("[DUMP START]");

var pkg = getPackageName();

var dumpPath =
"/data/data/" +
pkg +
"/" +
module.name +
".stable.dump.so";

var dumpBuffer = Memory.alloc(module.size);

// 清空 buffer
Memory.writeByteArray(
dumpBuffer,
new Uint8Array(module.size)
);

// 收集 range(PT_LOAD 风格近似)
var ranges = Process.enumerateRangesSync({
protection: 'r--',
coalesce: true
}).concat(
Process.enumerateRangesSync({
protection: 'r-x',
coalesce: true
}),
Process.enumerateRangesSync({
protection: 'rw-',
coalesce: true
})
);

var copied = 0;

ranges.forEach(function (range) {
try {

if (
range.base.compare(module.base) < 0 ||
range.base.compare(module.base.add(module.size)) >= 0
) return;

var offset = Number(range.base.sub(module.base));
if (offset < 0 || offset >= module.size) return;

var data = Memory.readByteArray(range.base, range.size);
if (!data) return;

Memory.writeByteArray(dumpBuffer.add(offset), data);
copied++;

} catch (e) {
}
});

console.log("[+] ranges copied:", copied);

// ELF magic check
var magic = new Uint8Array(Memory.readByteArray(dumpBuffer, 4));

if (
magic[0] === 0x7f &&
magic[1] === 0x45 &&
magic[2] === 0x4c &&
magic[3] === 0x46
) {
console.log("[OK] ELF MAGIC");
} else {
console.log("[WARN] ELF BROKEN");
}

// 写文件
var file = new File(dumpPath, "wb");
file.write(Memory.readByteArray(dumpBuffer, module.size));
file.flush();
file.close();

console.log("======================================");
console.log("[DUMP SUCCESS]");
console.log("[PATH]:", dumpPath);
console.log("======================================");

} catch (e) {
console.log("[DUMP ERROR]", e);
}
}

// =============================
// ENTRY
// =============================
waitModule(function (module) {

printJNIOnLoad(module);

setTimeout(function () {
dumpSoStable(module);
}, 1000);

});
1
既然已经成功解密出完整的 SO 文件,接下来就可以在还原后的代码基础上进一步分析,定位程序中的检测点以及具体的检测逻辑。
1
参考大🐂:https://bbs.kanxue.com/thread-290874.htm,https://bbs.kanxue.com/homepage-968342.htm