Swift Backup 是一个非常好用的备份软件——它可以备份应用、数据,甚至还能备份部分资源文件。

但是它有一个缺陷——它的备份文件默认加密,并且密码无从得知。

根据我的测试,在 A 手机上生成的备份,在 B 手机上可以正常还原。这说明所有密钥是相同的(加密加了个寂寞),官网上的文档也证实了这一点。

简单搜索了一下,有很多人讨论了加密规则,但是没有给出密码。看来我们还是得自己动手了。

首先我们使用 jadx 反编译 dex,得到 Java 源代码。
通过观察,我们可以发现它使用了7-Zip-JBinding作为第三方库,密码肯定通过库的 API 传入。我们不妨去看看文档:

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
import java.io.IOException;
import java.io.RandomAccessFile;

import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
import net.sf.sevenzipjbinding.IOutCreateArchive7z;
import net.sf.sevenzipjbinding.IOutCreateCallback;
import net.sf.sevenzipjbinding.IOutItem7z;
import net.sf.sevenzipjbinding.ISequentialInStream;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.OutItemFactory;
import net.sf.sevenzipjbinding.impl.RandomAccessFileOutStream;
import net.sf.sevenzipjbinding.junit.snippets.CompressArchiveStructure.Item;
import net.sf.sevenzipjbinding.util.ByteArrayStream;

public class CompressWithPassword {
/**
* The callback provides information about archive items.
*
* It also implements
* <ul>
* <li>{@link ICryptoGetTextPassword}
* </ul>
* to provide a password for encryption.
*/
private final class MyCreateCallback
implements IOutCreateCallback<IOutItem7z>, ICryptoGetTextPassword {
public void setOperationResult(boolean operationResultOk)
throws SevenZipException {
// Track each operation result here
}

public void setTotal(long total) throws SevenZipException {
// Track operation progress here
}

public void setCompleted(long complete) throws SevenZipException {
// Track operation progress here
}

public IOutItem7z getItemInformation(int index,
OutItemFactory<IOutItem7z> outItemFactory) {
IOutItem7z item = outItemFactory.createOutItem();

if (items[index].getContent() == null) {
// Directory
item.setPropertyIsDir(true);
} else {
// File
item.setDataSize((long) items[index].getContent().length);
}

item.setPropertyPath(items[index].getPath());

return item;
}

public ISequentialInStream getStream(int i) throws SevenZipException {
if (items[i].getContent() == null) {
return null;
}
return new ByteArrayStream(items[i].getContent(), true);
}

public String cryptoGetTextPassword() throws SevenZipException {
return password;
}
}

private Item[] items;
private String password;

public static void main(String[] args) {
if (args.length == 2) {
new CompressWithPassword().compress(args[0], args[1]);
return;
}
System.out.println("Usage: java CompressWithPassword <archive> <pass>");
}

private void compress(String filename, String pass) {
items = CompressArchiveStructure.create();
password = pass;

boolean success = false;
RandomAccessFile raf = null;
IOutCreateArchive7z outArchive = null;
try {
raf = new RandomAccessFile(filename, "rw");

// Open out-archive object
outArchive = SevenZip.openOutArchive7z();

// Header encryption is only available for 7z
outArchive.setHeaderEncryption(true);

// Create archive
outArchive.createArchive(new RandomAccessFileOutStream(raf),
items.length, new MyCreateCallback());

success = true;
} catch (SevenZipException e) {
System.err.println("7z-Error occurs:");
// Get more information using extended method
e.printStackTraceExtended();
} catch (Exception e) {
System.err.println("Error occurs: " + e);
} finally {
if (outArchive != null) {
try {
outArchive.close();
} catch (IOException e) {
System.err.println("Error closing archive: " + e);
success = false;
}
}
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
System.err.println("Error closing file: " + e);
success = false;
}
}
}
if (success) {
System.out.println("Compression operation succeeded");
}
}
}

可以看到,通过传入实现了cryptoGetTextPassword方法的类MyCreateCallback,库内部就会自动完成加密操作。

那么,我们只需要找到cryptoGetTextPassword这个方法,就能找到密码了。
1756789485945
理论可行,实践开始。

安装好 frida 后,写个脚本注入cryptoGetTextPassword:

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
Java.perform(function () {
var targets = [
"H8.c$a",
"H8.d$a",
"net.sf.sevenzipjbinding.SevenZip$ArchiveOpenCryptoCallback",
"net.sf.sevenzipjbinding.SevenZip$DummyOpenArchiveCallback",
"net.sf.sevenzipjbinding.impl.InArchiveImpl$ExtractSlowCryptoCallback"
];

function tryHook(className) {
try {
var Target = Java.use(className);
Target.cryptoGetTextPassword.implementation = function () {
var pw = this.cryptoGetTextPassword();
console.log("[+] " + className + ".cryptoGetTextPassword -> " + pw);
return pw;
};
console.log("[*] Successfully hooked " + className + ".cryptoGetTextPassword");
return true;
} catch (e) {
console.log(e);
return false;
}
}

targets.forEach(function (cls) {
if (!tryHook(cls)) {
console.log("[-] " + cls + " not loaded yet, waiting...");
var timer = setInterval(function () {
if (tryHook(cls)) {
clearInterval(timer);
}
}, 1000);
}
});
});

在手机上做一遍备份工作:
1756789706088

我们真的得到了加密的密码!但是每个应用的密码都不一样,而不是共用相同的密码。我们还得找到密码的生成规则。

修改一下脚本,让它打印出函数的调用栈:

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
function tryHook() {
try {
var C = Java.use("H8.c");
C.$init.overload(
"java.util.List",
"java.util.List",
"org.swiftapps.filesystem.File",
"[C", // char[] -> "[C"
"G8.d",
"boolean",
"W3.q"
).implementation = function (list1, list2, file, charArray, dVar, z10, qVar) {
console.log("[*] H8.c.<init> called");
console.log(" Call stack: " + Java.use("java.lang.Exception").$new().getStackTrace().toString());

console.log(" list1 = " + list1);
console.log(" list2 = " + list2);
console.log(" file = " + file);
console.log(" charArray = " + Java.use("java.lang.String").$new(charArray));
console.log(" dVar = " + dVar);
console.log(" z10 = " + z10);
console.log(" qVar = " + qVar);
return this.$init(list1, list2, file, charArray, dVar, z10, qVar);
};
console.log("[*] Successfully hooked H8.c.<init>");

var A = Java.use("H8.c$a");
A.$init.overload(
"java.util.List",
"java.util.List",
"java.util.List",
"[C",
"boolean",
"W3.q"
).implementation = function (list1, list2, list3, charArray, z10, qVar) {
console.log("[*] H8.c$a.<init> called");
console.log(" Call stack: " + Java.use("java.lang.Exception").$new().getStackTrace().toString());

console.log(" list1 = " + list1);
console.log(" list2 = " + list2);
console.log(" list3 = " + list3);
console.log(" charArray = " + Java.use("java.lang.String").$new(charArray));
console.log(" z10 = " + z10);
console.log(" qVar = " + qVar);
return this.$init(list1, list2, list3, charArray, z10, qVar);
};
console.log("[*] Successfully hooked H8.c$a.<init>");
return true;
} catch (e) {
console.log(e);
return false;
}
}

1756789974675

顺着调用栈一路找下去,我们最终会来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class C2458a {

/* renamed from: a */
public static final C2458a f36305a = new C2458a();

public final char[] a(String str, String str2, Integer num) {
if (num == null || num.intValue() != 1) {
char[] charArray = str.toCharArray();
AbstractC2084n.assert_not_null(charArray, "toCharArray(...)");
return charArray;
}
return J.f36236a.a(l5.x.i1(str).toString() + l5.x.i1(l5.u.A(str2, ".", "", false, 4, null)).toString());
}
}

在匿名用户的设备上,num似乎永远为 1,而str似乎也是固定值 d58b0944415a4889d7f11aa95fbeca50:

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
public final class C1731b {

/* renamed from: a */
public static final C1731b f29245a = new C1731b();

private final char[] s(String str, int i10) {
return C2458a.f36305a.a(C2459a0.f36306a.a().getUid(), str, Integer.valueOf(i10));
}

public final char[] m(EnumC1730a enumC1730a, String str, int i10) {
char[] cArrS = s(str, i10);
int i11 = a.f29246a[enumC1730a.ordinal()];
if (i11 == 1) {
return cArrS;
}
if (i11 == 2) {
return j(cArrS, r());
}
throw new NoWhenBranchMatchedException();
}
}


// org.swiftapps.swiftbackup.apptasks;
public final class e {

/* renamed from: u */
public static final C2420a f34985u = new C2420a(null);

static final class B extends kotlin.jvm.internal.p implements W3.a {
B() {
super(0);
}

@Override // W3.a
/* renamed from: a, reason: merged with bridge method [inline-methods] */
public final char[] mo510invoke() {
C1731b c1731b = C1731b.f29245a;
return c1731b.m(c1731b.q(), e.this.f34998m.getPackageName(), 1);
}
}
}

str2则是当前备份的软件包名,如org.swiftapps.swiftbackup。

重写此函数,跟踪一下内部状态:

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 tryHook2() {
try {
// var Target = Java.use("org.swiftapps.swiftbackup.common.C2458a");
var Target = Java.use("org.swiftapps.swiftbackup.common.a");

var L5X = Java.use("l5.x");
var L5U = Java.use("l5.u");
var J = Java.use("org.swiftapps.swiftbackup.common.J");

Target.a.overload('java.lang.String', 'java.lang.String', 'java.lang.Integer').implementation = function(str, str2, num) {
console.log("org.swiftapps.swiftbackup.common.C2458a called:");
console.log(" str: " + str);
console.log(" str2: " + str2);
console.log(" num: " + num);

var i1_res = L5X.i1.overload('java.lang.CharSequence').call(L5X, str).toString();
console.log(" l5.x.i1(str) -> " + i1_res);

var uA_res = L5U.A.overload('java.lang.String','java.lang.String','java.lang.String','boolean','int','java.lang.Object')
.call(L5U, str2, ".", "", false, 4, null);
console.log(" l5.u.A(str2) -> " + uA_res);

var i1_res2 = L5X.i1.overload('java.lang.CharSequence').call(L5X, uA_res).toString();
console.log(" l5.x.i1(uA_res) -> " + i1_res2);

var combined = i1_res + i1_res2;
console.log(" combined = " + combined);

// var res = J.a.overload('java.lang.String').call(J, combined);
// console.log(" final result (char[]) length=" + res.length);

// var res_str = Java.use("java.lang.String").$new(res);
// console.log(" J.a(combined) as String -> " + res_str);

var res = this.a(str, str2, num);
console.log(" final result (char[]) length=" + res.length);

var res_str = Java.use("java.lang.String").$new(res);
console.log(" J.a(combined) as String -> " + res_str);

return res;
};

console.log("[*] Successfully hooked org.swiftapps.swiftbackup.common.C2458a");
return true;
} catch (e) {
console.log(e);
return false;
}
}

1756815091745

看来l5.x.i1(str).toString()永远返回 05acebf59aa11f7d9884a5144490b85d,而l5.x.i1(l5.u.A(str2, ".", "", false, 4, null)).toString()则是将包名去除符号并逆序。

J.f36236a.a(str)则是真正的密钥生成逻辑,它的内部实现非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// org.swiftapps.swiftbackup.common;
public final class J {

/* renamed from: a */
public static final J f36236a = new J();

private J() {
}

public final char[] a(String str) {
G7.a aVar = new G7.a();
byte[] bytes = str.getBytes(C2109d.f32211b);
AbstractC2084n.assert_not_null(bytes, "getBytes(...)");
aVar.update(bytes);
char[] charArray = aVar.e().toCharArray();
AbstractC2084n.assert_not_null(charArray, "toCharArray(...)");
return charArray;
}
}

反编译G7.a,看起来是个复杂的 Hash 算法(Hash 算法没写在 native 层挺意外的):

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
package G7;

import java.util.zip.Checksum;

/* loaded from: classes.dex */
public class a implements Checksum {

/* renamed from: k, reason: collision with root package name */
private static F7.a f2584k = F7.a.a();

/* renamed from: a, reason: collision with root package name */
private final long f2585a = 0;

/* renamed from: b, reason: collision with root package name */
private long f2586b;

/* renamed from: c, reason: collision with root package name */
private long f2587c;

/* renamed from: d, reason: collision with root package name */
private int f2588d;

/* renamed from: e, reason: collision with root package name */
private int f2589e;

/* renamed from: f, reason: collision with root package name */
private long f2590f;

/* renamed from: g, reason: collision with root package name */
private long f2591g;

/* renamed from: h, reason: collision with root package name */
private boolean f2592h;

/* renamed from: i, reason: collision with root package name */
private long f2593i;

/* renamed from: j, reason: collision with root package name */
private long f2594j;

private void a(long j10, long j11) {
long jRotateLeft = (Long.rotateLeft(j10 * (-8663945395140668459L), 31) * 5545529020109919103L) ^ this.f2586b;
this.f2586b = jRotateLeft;
this.f2586b = ((Long.rotateLeft(jRotateLeft, 27) + this.f2587c) * 5) + 1390208809;
long jRotateLeft2 = (Long.rotateLeft(j11 * 5545529020109919103L, 33) * (-8663945395140668459L)) ^ this.f2587c;
this.f2587c = jRotateLeft2;
this.f2587c = ((Long.rotateLeft(jRotateLeft2, 31) + this.f2586b) * 5) + 944331445;
}

private void b() {
if (this.f2592h) {
return;
}
this.f2592h = true;
this.f2593i = this.f2586b;
this.f2594j = this.f2587c;
int i10 = this.f2589e;
if (i10 > 0) {
if (i10 > 8) {
this.f2594j = (Long.rotateLeft(this.f2591g * 5545529020109919103L, 33) * (-8663945395140668459L)) ^ this.f2594j;
}
this.f2593i = (Long.rotateLeft(this.f2590f * (-8663945395140668459L), 31) * 5545529020109919103L) ^ this.f2593i;
}
long j10 = this.f2593i;
int i11 = this.f2588d;
long j11 = j10 ^ i11;
long j12 = this.f2594j ^ i11;
long j13 = j11 + j12;
this.f2593i = j13;
this.f2594j = j12 + j13;
this.f2593i = c(j13);
long jC = c(this.f2594j);
long j14 = this.f2593i + jC;
this.f2593i = j14;
this.f2594j = jC + j14;
}

private long c(long j10) {
long j11 = (j10 ^ (j10 >>> 33)) * (-49064778989728563L);
long j12 = (j11 ^ (j11 >>> 33)) * (-4265267296055464877L);
return j12 ^ (j12 >>> 33);
}

private String d(long j10) {
String hexString = Long.toHexString(j10);
while (hexString.length() < 16) {
hexString = '0' + hexString;
}
return hexString;
}

public String e() {
b();
return d(this.f2594j) + d(this.f2593i);
}

@Override // java.util.zip.Checksum
public long getValue() {
b();
return this.f2593i;
}

@Override // java.util.zip.Checksum
public void reset() {
long j10 = this.f2585a;
this.f2587c = j10;
this.f2586b = j10;
this.f2588d = 0;
this.f2589e = 0;
this.f2592h = false;
this.f2591g = 0L;
this.f2590f = 0L;
this.f2594j = 0L;
this.f2593i = 0L;
}

/*
@Override // java.util.zip.Checksum
public void update(int i10) {
this.f2592h = false;
int i11 = this.f2589e;
switch (i11) {
case 0:
this.f2590f = i10 & 255;
break;
case 1:
this.f2590f |= (i10 & 255) << 8;
break;
case 2:
this.f2590f |= (i10 & 255) << 16;
break;
case 3:
this.f2590f |= (i10 & 255) << 24;
break;
case 4:
this.f2590f = ((255 & i10) << 32) | this.f2590f;
break;
case 5:
this.f2590f |= (i10 & 255) << 40;
break;
case 6:
this.f2590f |= (255 & i10) << 48;
break;
case 7:
this.f2590f |= (i10 & 255) << 56;
break;
case 8:
this.f2591g = i10 & 255;
break;
case 9:
this.f2591g |= (i10 & 255) << 8;
break;
case 10:
this.f2591g |= (i10 & 255) << 16;
break;
case 11:
this.f2591g |= (i10 & 255) << 24;
break;
case 12:
this.f2591g = ((255 & i10) << 32) | this.f2591g;
break;
case 13:
this.f2591g |= (i10 & 255) << 40;
break;
case 14:
this.f2591g |= (255 & i10) << 48;
break;
case 15:
this.f2591g |= (i10 & 255) << 56;
break;
}
int i12 = i11 + 1;
this.f2589e = i12;
if (i12 == 16) {
a(this.f2590f, this.f2591g);
this.f2589e = 0;
}
this.f2588d++;
}
*/
@Override
public void update(int i10) {
this.f2592h = false;
int i11 = this.f2589e;
long val = (long) (i10 & 0xFF);
switch (i11) {
case 0:
this.f2590f = val;
break;
case 1:
this.f2590f |= val << 8;
break;
case 2:
this.f2590f |= val << 16;
break;
case 3:
this.f2590f |= val << 24;
break;
case 4:
this.f2590f |= val << 32;
break;
case 5:
this.f2590f |= val << 40;
break;
case 6:
this.f2590f |= val << 48;
break;
case 7:
this.f2590f |= val << 56;
break;
case 8:
this.f2591g = val;
break;
case 9:
this.f2591g |= val << 8;
break;
case 10:
this.f2591g |= val << 16;
break;
case 11:
this.f2591g |= val << 24;
break;
case 12:
this.f2591g |= val << 32;
break;
case 13:
this.f2591g |= val << 40;
break;
case 14:
this.f2591g |= val << 48;
break;
case 15:
this.f2591g |= val << 56;
break;
default:
// shouldn't happen
break;
}
int i12 = i11 + 1;
this.f2589e = i12;
if (i12 == 16) {
a(this.f2590f, this.f2591g);
this.f2589e = 0;
// clear buffers after consuming them
this.f2590f = 0L;
this.f2591g = 0L;
}
this.f2588d++;
}

@Override // java.util.zip.Checksum
public void update(byte[] bArr, int i10, int i11) {
this.f2592h = false;
while (this.f2589e != 0 && i11 > 0) {
update(bArr[i10]);
i10++;
i11--;
}
int i12 = i11 & 15;
int i13 = (i11 + i10) - i12;
for (int i14 = i10; i14 < i13; i14 += 16) {
a(f2584k.b(bArr, i14), f2584k.b(bArr, i14 + 8));
}
this.f2588d += i13 - i10;
for (int i15 = 0; i15 < i12; i15++) {
update(bArr[i13 + i15]);
}
}

@Override // java.util.zip.Checksum
public void update(byte[] bArr) {
update(bArr, 0, bArr.length);
}
}

F7.a.b有一些大小端的问题,需要稍微修改一下:

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
package F7;

/* loaded from: classes.dex */
public abstract class a {

/* renamed from: a, reason: collision with root package name */
private static volatile a f2342a;

/* renamed from: b, reason: collision with root package name */
private static final a f2343b = new b();

private static class b extends a {
private b() {
}

@Override // F7.a
public long b(byte[] bArr, int i10) {
return ((long)(bArr[i10 + 7] & 0xFF) << 56)
| ((long)(bArr[i10 + 6] & 0xFF) << 48)
| ((long)(bArr[i10 + 5] & 0xFF) << 40)
| ((long)(bArr[i10 + 4] & 0xFF) << 32)
| ((long)(bArr[i10 + 3] & 0xFF) << 24)
| ((long)(bArr[i10 + 2] & 0xFF) << 16)
| ((long)(bArr[i10 + 1] & 0xFF) << 8)
| ((long)(bArr[i10] & 0xFF));

// return (bArr[i10 + 7] << 56) | (bArr[i10] & 255) | ((bArr[i10 + 1] & 255) << 8) | ((bArr[i10 + 2] & 255) << 16) | ((bArr[i10 + 3] & 255) << 24) | ((bArr[i10 + 4] & 255) << 32) | ((bArr[i10 + 5] & 255) << 40) | ((bArr[i10 + 6] & 255) << 48);
}
}

public static a a() {
a aVar = f2342a;
return aVar == null ? f2343b : aVar;
}

public abstract long b(byte[] bArr, int i10);
}

仿照J.f36236a.a(str),写个 demo 验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.nio.charset.StandardCharsets;

public class HelloWorld {
public static void main(String[] args) {
G7.a aVar = new G7.a();
String str = "05acebf59aa11f7d9884a5144490b85dsulptmnib";
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
aVar.update(bytes);
char[] charArray = aVar.e().toCharArray();

System.out.println(str);
System.out.println(java.util.Arrays.toString(bytes));
System.out.println(charArray);
System.out.println(charArray.length);
}
}
1
2
3
4
5
> java demo.java
05acebf59aa11f7d9884a5144490b85dsulptmnib
[48, 53, 97, 99, 101, 98, 102, 53, 57, 97, 97, 49, 49, 102, 55, 100, 57, 56, 56, 52, 97, 53, 49, 52, 52, 52, 57, 48, 98, 56, 53, 100, 115, 117, 108, 112, 116, 109, 110, 105, 98]
ca6d086c948ac076bd6849a5e1b6b2d9
32

符合预期,收工!

相关代码放在这里了,有需要可以自取:
swift_backup_password_generator.7z