Android Backup 文件解析
本帖最后由 nikoxves 于 2015-12-19 01:36 编辑#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的实现代码,那里主要以解析为主,方便理解。有不对的地方请大牛多多指教
#0x1
我从github 和 abe源码对比看了下。
////////////////////////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
//
// When line 4 is not "none", then additional header data follows:
//
/ line 5: user password salt
// line 6: master key checksum salt
// line 7: number of PBKDF2 rounds to use (same for user & master)
// line 8: IV of the user key
// line 9: master key blob
// 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 是开启的。
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); 主要是记录一些秘钥信息和校验值
// line 4: name of encryption algorithm
headerbuf.append(ENCRYPTION_ALGORITHM_NAME);
headerbuf.append('\n');
// line 5: user password salt
headerbuf.append(byteArrayToHex(newUserSalt));
headerbuf.append('\n');
// line 6: master key checksum salt
headerbuf.append(byteArrayToHex(checksumSalt));
headerbuf.append('\n');
// line 7: number of PBKDF2 rounds used
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
下面是后续分析,
// 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 不是很清楚是做什么的,可能是因为我没有从头开始分析,导致个别参数不明确。
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有关。然后在另一个目录找到相应的接口。
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;
if (p != null) {
pool = 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)进行备份.
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,
token, sendApk, !isSharedStorage, widgetBlob);
pipes.close(); // the runner has dup'd it
pipes = 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, 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 != null) pipes.close();
if (pipes != null) pipes.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来决定写入文件。
// 这个函数还是挺难找到的
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,暂时不做讨论。
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);
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,上面的注释已经很详细了。
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的备份数据了。
private void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out)
throws IOException {
FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
DataInputStream in = new DataInputStream(raw);
byte[] buffer = new byte;
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的结构,结果不知不觉就分析了那么多的代码,收货还是挺大的。当然,灰常欢迎大大们提问。
看着看着看困了.....论格式的重要性啊 gaojun 发表于 2015-12-30 20:28
这是C ++吗?
代码是java的,不过深入到binder那应该是有c++的东西 学习一下,感谢分享 感谢分享。。。。。。。。。。。。 前排膜拜~~
学习一下,感谢分享 看不懂,还的学习了。 我竟然啥都看不懂。。 学习一下,感谢分享 学习一下,感谢分享