首页
社区
课程
招聘
[原创]Android Backup 文件格式解析
发表于: 2015-12-17 11:54 9691

[原创]Android Backup 文件格式解析

2015-12-17 11:54
9691

#Android Backup 文件头解析
#0x0
>今天有空做了一下RCTF的mobile题。第一题首先是android backup 的问题,这个可以手动提取backup的文件,也可以使用abe.jar提取(需要改一下文件格式),
使用前者的推荐看一下* [这篇博文](http://bobao.360.cn/learning/detail/169.html)。第二种方法则需要了解一下文件格式(其实也没多少- -)
.我去github上找了一下,backup 在 * [这里](https://github.com/android/platform_frameworks_base/blob/master/services/backup/java/com/android/server/backup/BackupManagerService.java),另外还有一个backup的app在* [这里](https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/backup/BackupManager.java)
另外我竟然发现了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的结构,结果不知不觉就分析了那么多的代码,收货还是挺大的。当然,灰常欢迎大大们提问。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (2)
雪    币: 139
活跃值: (1175)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
然而我早就写好了 安卓备份密码破解的工具了
2015-12-17 11:55
0
雪    币: 48
活跃值: (61)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
3
还请大牛多多指教啊
2015-12-17 12:15
0
游客
登录 | 注册 方可回帖
返回
//