frida 初体验——记一次 Android APP 逆向 发表于 2025-09-02 | 更新于 2026-01-14
| 总字数: 104k | 阅读时长: 6:18分钟
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 { private final class MyCreateCallback implements IOutCreateCallback <IOutItem7z>, ICryptoGetTextPassword { public void setOperationResult (boolean operationResultOk) throws SevenZipException { } public void setTotal (long total) throws SevenZipException { } public void setCompleted (long complete) throws SevenZipException { } public IOutItem7z getItemInformation (int index, OutItemFactory<IOutItem7z> outItemFactory) { IOutItem7z item = outItemFactory.createOutItem(); if (items[index].getContent() == null ) { item.setPropertyIsDir(true ); } else { 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" ); outArchive = SevenZip.openOutArchive7z(); outArchive.setHeaderEncryption(true ); outArchive.createArchive(new RandomAccessFileOutStream (raf), items.length, new MyCreateCallback ()); success = true ; } catch (SevenZipException e) { System.err.println("7z-Error occurs:" ); 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这个方法,就能找到密码了。 理论可行,实践开始。
安装好 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 ); } }); });
在手机上做一遍备份工作:
我们真的得到了加密的密码!但是每个应用的密码都不一样,而不是共用相同的密码。我们还得找到密码的生成规则。
修改一下脚本,让它打印出函数的调用栈:
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" , "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 ; } }
顺着调用栈一路找下去,我们最终会来到这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final class C2458a { 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 { 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 (); } } public final class e { public static final C2420a f34985u = new C2420a (null ); static final class B extends kotlin .jvm.internal.p implements W3 .a { B() { super (0 ); } @Override 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.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 = 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 ; } }
看来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 public final class J { 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;public class a implements Checksum { private static F7.a f2584k = F7.a.a(); private final long f2585a = 0 ; private long f2586b; private long f2587c; private int f2588d; private int f2589e; private long f2590f; private long f2591g; private boolean f2592h; private long f2593i; 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 public long getValue () { b(); return this .f2593i; } @Override 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 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 : break ; } int i12 = i11 + 1 ; this .f2589e = i12; if (i12 == 16 ) { a(this .f2590f, this .f2591g); this .f2589e = 0 ; this .f2590f = 0L ; this .f2591g = 0L ; } this .f2588d++; } @Override 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 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;public abstract class a { private static volatile a f2342a; private static final a f2343b = new b (); private static class b extends a { private b () { } @Override 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 )); } } 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