-
-
[原创]Android Backup 文件格式解析
-
发表于: 2015-12-17 11:54 9892
-
#Android Backup 文件头解析
#0x0
>今天有空做了一下RCTF的mobile题。第一题首先是android backup 的问题,这个可以手动提取backup的文件,也可以使用abe.jar提取(需要改一下文件格式),
使用前者的推荐看一下* [这篇博文](bcdK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3u0G2j5X3q4G2i4K6u0W2x3K6j5H3i4K6u0W2j5$3&6Q4x3V1k6D9k6h3q4J5L8X3W2F1k6#2)9J5c8X3c8W2N6r3q4A6L8q4)9J5c8U0p5$3z5g2)9J5k6h3S2@1L8h3I4Q4x3U0W2Q4c8e0y4Q4z5o6m8Q4z5o6u0Q4c8e0N6Q4b7f1y4Q4b7f1y4Q4c8e0c8Q4b7V1q4Q4z5p5y4Q4c8e0N6Q4b7e0N6Q4z5p5c8Q4c8e0k6Q4z5e0k6Q4b7U0W2Q4c8e0k6Q4b7U0y4Q4z5e0g2Q4c8e0g2Q4z5o6S2Q4z5e0W2Q4c8e0W2Q4z5f1y4Q4z5o6m8Q4c8e0S2Q4b7e0k6Q4z5o6q4Q4c8e0c8Q4b7V1q4Q4z5o6k6Q4c8e0S2Q4b7e0N6Q4b7e0y4Q4c8e0c8Q4b7U0S2Q4z5o6m8Q4c8e0c8Q4b7U0S2Q4z5p5u0Q4c8e0k6Q4z5e0k6Q4z5o6N6Q4c8e0c8Q4b7V1u0Q4b7U0k6Q4c8e0k6Q4b7e0m8Q4b7V1y4Q4c8e0g2Q4b7V1y4Q4z5p5k6Q4x3U0S2Q4c8e0g2Q4z5o6g2Q4b7U0k6Q4c8e0g2Q4b7f1g2Q4z5f1g2Q4c8e0c8Q4b7U0W2Q4z5f1k6Q4c8e0k6Q4b7U0u0Q4b7e0q4Q4c8e0g2Q4b7e0c8Q4z5f1q4Q4c8e0g2Q4b7U0m8Q4z5e0q4Q4x3X3b7`. -)
.我去github上找了一下,backup 在 * [这里](d40K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4y4h3k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6g2X3j5X3q4K6k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6E0j5i4y4@1k6i4u0Q4x3V1k6K6k6i4u0$3K9h3y4W2M7#2)9J5c8X3u0S2j5$3E0#2M7q4)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0r3j5X3q4U0K9%4g2H3i4K6u0r3b7X3q4U0K9%4g2H3e0h3q4F1j5h3N6W2M7W2y4W2M7Y4k6A6j5$3g2Q4x3X3g2B7j5i4k6S2i4K6t1&6i4K6u0o6i4@1f1#2i4K6S2r3i4@1p5$3i4@1f1#2i4@1p5@1i4K6V1$3i4@1f1^5i4@1u0r3i4K6V1^5i4@1f1$3i4K6W2o6i4K6R3&6i4@1f1@1i4@1t1^5i4K6R3H3i4@1f1@1i4@1t1^5i4@1q4m8j5X3q4U0K9%4g2H3i4@1f1%4i4K6W2m8i4K6R3@1j5i4m8H3i4@1f1#2i4K6W2o6i4@1p5^5i4K6u0m8 [这里](a4aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4y4h3k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6g2X3j5X3q4K6k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6E0j5i4y4@1k6i4u0Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6T1j5h3y4C8N6i4m8Q4x3V1k6n7j5h3y4C8N6i4m8y4j5h3&6S2k6$3g2J5i4K6u0W2K9X3q4$3j5g2)9J5z5b7`.`.
另外我竟然发现了smali版的BMS(6666666666)。如果这里的文件不想看的话(毕竟有近1W行代码),还可以看一下abe.jar的实现代码,那里主要以解析为主,方便理解。有不对的地方请大牛多多指教
#0x2
>我从github 和 abe源码对比看了下。
```java
////////////////////////github///////////////////////////////////////
// Write the global file header. All strings are UTF-8 encoded; lines end
// with a '\n' byte. Actual backup data begins immediately following the
// final '\n'.
//
// line 1: "ANDROID BACKUP"
// line 2: backup file format version, currently "2"
// line 3: compressed? "0" if not compressed, "1" if compressed.
// line 4: name of encryption algorithm [currently only "none" or "AES-256"]
//
// When line 4 is not "none", then additional header data follows:
//
/ line 5: user password salt [hex]
// line 6: master key checksum salt [hex]
// line 7: number of PBKDF2 rounds to use (same for user & master) [decimal]
// line 8: IV of the user key [hex]
// line 9: master key blob [hex]
// IV of the master key, master key itself, master key checksum hash
//
// The master key checksum is the master key plus its checksum salt, run through
// 10k rounds of PBKDF2. This is used to verify that the user has supplied the
// correct password for decrypting the archive: the master key decrypted from
// the archive using the user-supplied password is also run through PBKDF2 in
// this way, and if the result does not match the checksum as stored in the
// archive, then we know that the user-supplied password does not match the
// archive's.
static final int BACKUP_FILE_VERSION = 3;
static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
static final int BACKUP_PW_FILE_VERSION = 2;
static final String BACKUP_METADATA_FILENAME = "_meta";
StringBuilder headerbuf = new StringBuilder(1024);
headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n
headerbuf.append(mCompress ? "\n1\n" : "\n0\n");
////////////////////////////abe//////////////////////////////////
StringBuilder headerbuf = new StringBuilder(1024);
headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
// integer, no trailing \n
headerbuf.append(isKitKat ? BACKUP_FILE_V2 : BACKUP_FILE_V1);
headerbuf.append(compressing ? "\n1\n" : "\n0\n");
```
>这里一开始还是MAGIC ,然后是一个version和是否压缩的选项,version现在是3,但是代码的注释里还没有改,还可以看到version 写的是2, 另外compressed 是开启的。
```java
try {
// Set up the encryption stage if appropriate, and emit the correct header
if (encrypting) {
finalOutput = emitAesBackupHeader(headerbuf, finalOutput);
} else {
headerbuf.append("none\n");
}
```
>接下来就是判断用户是否输入了密码,有的话就加密, 没有就在header加一个"none"。这里abe对emitAesBackupHeader函数做了一些修改,感兴趣的可以去看一下,大体还是和源码一样的。然后如果有输入密码的话,会执行emitAesBackupHeader(headerbuf, finalOutput); 主要是记录一些秘钥信息和校验值
```java
// line 4: name of encryption algorithm
headerbuf.append(ENCRYPTION_ALGORITHM_NAME);
headerbuf.append('\n');
// line 5: user password salt [hex]
headerbuf.append(byteArrayToHex(newUserSalt));
headerbuf.append('\n');
// line 6: master key checksum salt [hex]
headerbuf.append(byteArrayToHex(checksumSalt));
headerbuf.append('\n');
// line 7: number of PBKDF2 rounds used [decimal]
headerbuf.append(PBKDF2_HASH_ROUNDS);
headerbuf.append('\n');
IV = c.getIV();
byte[] mk = masterKeySpec.getEncoded();
byte[] checksum = makeKeyChecksum(PBKDF_CURRENT, masterKeySpec.getEncoded(),
checksumSalt, PBKDF2_HASH_ROUNDS);
ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
+ checksum.length + 3);
DataOutputStream mkOut = new DataOutputStream(blob);
mkOut.writeByte(IV.length);
mkOut.write(IV);
mkOut.writeByte(mk.length);
mkOut.write(mk);
mkOut.writeByte(checksum.length);
mkOut.write(checksum);
mkOut.flush();
byte[] encryptedMk = mkC.doFinal(blob.toByteArray());
headerbuf.append(byteArrayToHex(encryptedMk));
headerbuf.append('\n');
```
>分析到这一步,第一题就可以做了,只要把version和compressed的恢复一下就可以用abe提取了。 到这里整个头也分析完毕,实际就24个字节。然后abe剩下的部分也和源码不一样了,毕竟两个代码的用处都不一样。
#0x2
下面是后续分析,
```java
// Shared storage if requested
if (mIncludeShared) {
try {
pkg = mPackageManager.getPackageInfo(SHARED_BACKUP_AGENT_PACKAGE, 0);
backupQueue.add(pkg);
} catch (NameNotFoundException e) {
Slog.e(TAG, "Unable to find shared-storage backup handler");
}
}
// Now actually run the constructed backup sequence
int N = backupQueue.size();
for (int i = 0; i < N; i++) {
pkg = backupQueue.get(i);
final boolean isSharedStorage =
pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
mBackupEngine = new FullBackupEngine(out, pkg.packageName, null, mIncludeApks);
sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
mBackupEngine.backupOnePackage(pkg);
// after the app's agent runs to handle its private filesystem
// contents, back up any OBB content it has on its behalf.
if (mIncludeObbs) {
boolean obbOkay = obbConnection.backupObbs(pkg, out);
if (!obbOkay) {
throw new RuntimeException("Failure writing OBB stack for " + pkg);
}
}
}
// Done!
finalizeBackup(out);
.....
try {
if (out != null) out.close();
mOutputFile.close();
} catch (IOException e) {
/* nothing we can do about this */
}
synchronized (mCurrentOpLock) {
mCurrentOperations.clear();
}
synchronized (mLatch) {
mLatch.set(true);
mLatch.notifyAll();
}
sendEndBackup();
obbConnection.tearDown();
if (DEBUG) Slog.d(TAG, "Full backup pass complete.");
mWakelock.release();
```
> 后续的操作主要是3个,把Sharedstorage.apk加入备份队列(不知道这里有没有理解错),然后就是对所选的应用进行备份,接着调用finalizeBackup(out)代表tar文件结束,最后就是一些扫尾工作。
由于右半部分比较简单不做分析。主要看循环里的代码
>首先从队列里取一个pkg,然后创建一个FullBackupEngine mBackupEngine对象.代码如下,可以看到,另外还创建了两个文件_meta,_manifest 但是这里的alsoApks 不是很清楚是做什么的,可能是因为我没有从头开始分析,导致个别参数不明确。
```java
FullBackupEngine(OutputStream output, String packageName, FullBackupPreflight preflightHook,
boolean alsoApks) {
mOutput = output;
mPreflightHook = preflightHook;
mIncludeApks = alsoApks;
mFilesDir = new File("/data/system");
mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
mMetadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
}
```
>初始化完成后,调用sendOnBackupPackage(),参数是报名,如果是Sharedstorage.apk就用"Shared storage";
sendOnBackupPackage又调用mObserver.onBackupPackage(packageName)进行处理,这里的mObserver是一个
IFullBackupRestoreObserver对象,听名字大致可以猜测是用来监视整个备份过程的,然后
sendEndBackup()又有mObserver有关。然后在另一个目录找到相应的接口。
```java
static final int TRANSACTION_onStartBackup = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
@Override
public void onBackupPackage(java.lang.String name) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(name);
mRemote.transact(Stub.TRANSACTION_onBackupPackage, _data, null, android.os.IBinder.FLAG_ONEWAY);
}
finally {
_data.recycle();
}
}
public static Parcel obtain() {
final Parcel[] pool = sOwnedPool;
synchronized (pool) {
Parcel p;
for (int i=0; i<POOL_SIZE; i++) {
p = pool[i];
if (p != null) {
pool[i] = null;
if (DEBUG_RECYCLE) {
p.mStack = new RuntimeException();
}
return p;
}
}
}
return new Parcel(0);
}
public final boolean transact(int code, Parcel data, Parcel reply,
int flags) throws RemoteException {
......
boolean r = onTransact(code, data, reply, flags);
....
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
.........
case TRANSACTION_onStartBackup:
{
data.enforceInterface(DESCRIPTOR);
this.onStartBackup();
return true;
}
..........
```
> 上面的代码主要用于和Binder通信,Parcel主要用于通信,不能包含复杂的数据,Binder比较复杂,这里也不过多讨论。
最后调用mBackupEngine.backupOnePackage(pkg)进行备份.
```java
public int backupOnePackage(PackageInfo pkg) throws RemoteException {
.......
IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo,
IApplicationThread.BACKUP_MODE_FULL);
if (agent != null) {
ParcelFileDescriptor[] pipes = null;
try {
// Call the preflight hook, if any
if (mPreflightHook != null) {
result = mPreflightHook.preflightFullBackup(pkg, agent);
if (MORE_DEBUG) {
Slog.v(TAG, "preflight returned " + result);
}
}
// If we're still good to go after preflighting, start moving data
if (result == BackupTransport.TRANSPORT_OK) {
pipes = ParcelFileDescriptor.createPipe();
ApplicationInfo app = pkg.applicationInfo;
final boolean isSharedStorage = pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
final boolean sendApk = mIncludeApks
&& !isSharedStorage
&& ((app.privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) == 0)
&& ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ||
(app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
byte[] widgetBlob = AppWidgetBackupBridge.getWidgetState(pkg.packageName,
UserHandle.USER_OWNER);
final int token = generateToken();
FullBackupRunner runner = new FullBackupRunner(pkg, agent, pipes[1],
token, sendApk, !isSharedStorage, widgetBlob);
pipes[1].close(); // the runner has dup'd it
pipes[1] = null;
Thread t = new Thread(runner, "app-data-runner");
t.start();
// Now pull data from the app and stuff it into the output
try {
routeSocketDataToOutput(pipes[0], mOutput);
} catch (IOException e) {
Slog.i(TAG, "Caught exception reading from agent", e);
result = BackupTransport.AGENT_ERROR;
}
if (!waitUntilOperationComplete(token)) {
Slog.e(TAG, "Full backup failed on package " + pkg.packageName);
result = BackupTransport.AGENT_ERROR;
} else {
if (MORE_DEBUG) {
Slog.d(TAG, "Full package backup success: " + pkg.packageName);
}
}
}
} catch (IOException e) {
Slog.e(TAG, "Error backing up " + pkg.packageName, e);
result = BackupTransport.AGENT_ERROR;
} finally {
try {
// flush after every package
mOutput.flush();
if (pipes != null) {
if (pipes[0] != null) pipes[0].close();
if (pipes[1] != null) pipes[1].close();
}
} catch (IOException e) {
Slog.w(TAG, "Error bringing down backup stack");
result = BackupTransport.TRANSPORT_ERROR;
}
}
} else {
Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName);
result = BackupTransport.AGENT_ERROR;
}
tearDown(pkg);
return result;
}
```
>同样又是分为几个要点,首先是启动一个agent,然后创建一个管道,开启一个线程转移数据,然后从管道中获取数据,最后输出到文件。
在创建runner的时候会根据是否是Sharedstorage.apk来决定写入文件。
```java
// 这个函数还是挺难找到的
IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) {
IBackupAgent agent = null;
synchronized(mAgentConnectLock) {
mConnecting = true;
mConnectedAgent = null;
try {
if (mActivityManager.bindBackupAgent(app, mode)) {
Log.d(TAG, "awaiting agent for " + app);
// success; wait for the agent to arrive
// only wait 10 seconds for the clear data to happen
long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL;
while (mConnecting && mConnectedAgent == null
&& (System.currentTimeMillis() < timeoutMark)) {
try {
mAgentConnectLock.wait(5000);
} catch (InterruptedException e) {
// just bail
return null;
}
}
// if we timed out with no connect, abort and move on
if (mConnecting == true) {
Log.w(TAG, "Timeout waiting for agent " + app);
return null;
}
agent = mConnectedAgent;
}
} catch (RemoteException e) {
// can't happen
}
}
return agent;
}
```
>这里的关系还是比较简单的,调用mActivityManager.bindBackupAgent(app, mode)后等待Agent到来,然后赋值给agent,返回。这里同样涉及到binder,暂时不做讨论。
```java
public void run() {
try {
FullBackupDataOutput output = new FullBackupDataOutput(mPipe);
if (mWriteManifest) {
final boolean writeWidgetData = mWidgetData != null;
if (MORE_DEBUG) Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
writeAppManifest(mPackage, mManifestFile, mSendApk, writeWidgetData);
FullBackup.backupToTar(mPackage.packageName, null, null,
mFilesDir.getAbsolutePath(),
mManifestFile.getAbsolutePath(),
output);
mManifestFile.delete();
// We only need to write a metadata file if we have widget data to stash
if (writeWidgetData) {
writeMetadata(mPackage, mMetadataFile, mWidgetData);
FullBackup.backupToTar(mPackage.packageName, null, null,
mFilesDir.getAbsolutePath(),
mMetadataFile.getAbsolutePath(),
output);
mMetadataFile.delete();
}
}
if (mSendApk) {
writeApkToBackup(mPackage, output);
}
if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName);
prepareOperationTimeout(mToken, TIMEOUT_FULL_BACKUP_INTERVAL, null);
mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder);
} catch (IOException e) {
Slog.e(TAG, "Error running full backup for " + mPackage.packageName);
} catch (RemoteException e) {
Slog.e(TAG, "Remote agent vanished during full backup of "
+ mPackage.packageName);
} finally {
try {
mPipe.close();
} catch (IOException e) {}
}
}
private void writeApkToBackup(PackageInfo pkg, FullBackupDataOutput output) {
// Forward-locked apps, system-bundled .apks, etc are filtered out before we get here
// TODO: handle backing up split APKs
final String appSourceDir = pkg.applicationInfo.getBaseCodePath();
final String apkDir = new File(appSourceDir).getParent();
FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null,
apkDir, appSourceDir, output);
// TODO: migrate this to SharedStorageBackup, since AID_SYSTEM
// doesn't have access to external storage.
// Save associated .obb content if it exists and we did save the apk
// check for .obb and save those too
final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_OWNER);
final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0];
if (obbDir != null) {
if (MORE_DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
File[] obbFiles = obbDir.listFiles();
if (obbFiles != null) {
final String obbDirName = obbDir.getAbsolutePath();
for (File obb : obbFiles) {
FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null,
obbDirName, obb.getAbsolutePath(), output);
}
}
}
}
```
>创建线程后,普通app需要写入manifest,代码如下 ,写入到文件后,就调用FullBackup.backupToTar()将文件压缩到tar中,
下面的wigetData也差不多,就不再分析。对于mSendApk应该是和系统有关的apk,上面的注释已经很详细了。
```java
private void writeAppManifest(PackageInfo pkg, File manifestFile,
boolean withApk, boolean withWidgets) throws IOException {
// Manifest format. All data are strings ending in LF:
// BACKUP_MANIFEST_VERSION, currently 1
//
// Version 1:
// package name
// package's versionCode
// platform versionCode
// getInstallerPackageName() for this package (maybe empty)
// boolean: "1" if archive includes .apk; any other string means not
// number of signatures == N
// N*: signature byte array in ascii format per Signature.toCharsString()
StringBuilder builder = new StringBuilder(4096);
StringBuilderPrinter printer = new StringBuilderPrinter(builder);
printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
printer.println(pkg.packageName);
printer.println(Integer.toString(pkg.versionCode));
printer.println(Integer.toString(Build.VERSION.SDK_INT));
String installerName = mPackageManager.getInstallerPackageName(pkg.packageName);
printer.println((installerName != null) ? installerName : "");
printer.println(withApk ? "1" : "0");
if (pkg.signatures == null) {
printer.println("0");
} else {
printer.println(Integer.toString(pkg.signatures.length));
for (Signature sig : pkg.signatures) {
printer.println(sig.toCharsString());
}
}
FileOutputStream outstream = new FileOutputStream(manifestFile);
outstream.write(builder.toString().getBytes());
outstream.close();
// We want the manifest block in the archive stream to be idempotent:
// each time we generate a backup stream for the app, we want the manifest
// block to be identical. The underlying tar mechanism sees it as a file,
// though, and will propagate its mtime, causing the tar header to vary.
// Avoid this problem by pinning the mtime to zero.
manifestFile.setLastModified(0);
}
```
>上面的备份完成后就是主体的备份了,同样涉及到了Binder(可见Binder在android中的重要性),这里同样不再讨论。
等到这里结束后,管道里就是一个apk的备份数据了。
```java
private void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out)
throws IOException {
FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
DataInputStream in = new DataInputStream(raw);
byte[] buffer = new byte[32 * 1024];
int chunkTotal;
while ((chunkTotal = in.readInt()) > 0) {
while (chunkTotal > 0) {
int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal;
int nRead = in.read(buffer, 0, toRead);
out.write(buffer, 0, nRead);
chunkTotal -= nRead;
}
}
}
```
>从管道获取数据就不分析了,代码还是比较简单的。
#0xed
本来只是想分析一下backup的结构,结果不知不觉就分析了那么多的代码,收货还是挺大的。当然,灰常欢迎大大们提问。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创]一个简单的jeb字符串解密脚本 10081
- [求助]求一个vmp 加固过的app 4570
- [讨论] 有米有老司机搞过某易的易盾 4836
- [应届求职][已完成 求管理员删除] 4362
- [分享]Soot 教程 7993