2018年3月

DirectBoot功能介绍

当手机已经通电开机但是用户并有解锁锁屏的时候,Android N运行于一个安全的模式,也就是Dierect Boot模式。

为了支持Dierect Boot模式,系统提供了两个存储数据的地方:
1.Credential encrypted storage,默认存储数据的地方,仅在用户解锁手机后可用。
2.Device encrypted storage,主要对应的就是Direct Boot使用的存储空间。在Direct Boot模式下和用户解锁手机后都可以使用的存储空间。

系统把部分系统数据和已经注册了相关权限的Apps的数据保存在device-encrypted store 。其他的数据默认保存到credential-encrypted store。
当手机开机,首先进入一个Dierect Boot的模式,在这个模式下只可以访问device-encrypted store下的数据,无法访问credential-encrypted store下的数据。当用户解锁后就都可以访问了。

一般情况下,应用是无法在Direct Boot模式下运行的
如果需要某个app能够在Direct Boot模式下运行,需要注册相关APP的组件。通常需要在这个模式下运行的app:
1.计划通知的应用,例如Clock
2.重要的用户通知的应用,例如sms
3.提供无障碍服务的应用,例如Talkback

应用组件申请在Direct Boot模式下运行:在AndroidManinfest.xml中设置 android:directBootAware="true"。

应用访问device encrypted storage:
创建Context.createDeviceEncryptedStorageContext().然后通过这个Context来使用device encrypted storage 的存储空间。

Context directBootContext = Context.createDeviceEncryptedStorageContext();
// Access appDataFilename that lives in device encrypted storage
FileInputStream inStream = directBootContext.openFileInput(appDataFilename);
// Use inStream to read content...

应用获取解锁的通知:
监听广播ACTION_USER_UNLOCKED 。
或者接收ACTION_BOOT_COMPLETED ,这个广播的意思是手机开机并且用户解锁。
也可调用UserManager.isUserUnlocked()方法来查询。
应用迁移已经存在的数据:
Context.migrateSharedPreferencesFrom()
Context.migrateDatabaseFrom()
两种方法在credential encrypted storage 和device encrypted storage存储空间之间去迁移preference 和database的数据.
启动FallbackHome流程

在分析7.0过程中发现在启动Launcher之前会先启动一个FallbackHome,之后才会启动Launcher,通过调查发现FallbackHome属于Settings中的一个activity,Settings的android:directBootAware为true,并且FallbackHome在category中配置了Home属性,而Launcher的android:directBootAware为false,所有只有FallbackHome可以在direct boot模式下启动。

<application android:label="@string/settings_label"

    android:icon="@mipmap/ic_launcher_settings"
    ............
    android:directBootAware="true">

<!-- Triggered when user-selected home app isn't encryption aware -->
<activity android:name=".FallbackHome"
          android:excludeFromRecents="true"
          android:theme="@style/FallbackHome">
    <intent-filter android:priority="-1000">
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.HOME" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

所以在ActivityManagerService启动Home界面时,从PackageManagerService中获取到的Home界面就是FallbackHome

   Intent getHomeIntent() {
        Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
        intent.setComponent(mTopComponent);
        intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
        if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
            intent.addCategory(Intent.CATEGORY_HOME);
        }
        return intent;
    }

    boolean startHomeActivityLocked(int userId, String reason) {
        if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
                && mTopAction == null) {
            // We are running in factory test mode, but unable to find
            // the factory test app, so just sit around displaying the
            // error message and don't try to start anything.
            return false;
        }
        Intent intent = getHomeIntent();
        ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);  //获取Home activity信息
        if (aInfo != null) {
            intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
            // Don't do this if the home app is currently being
            // instrumented.
            aInfo = new ActivityInfo(aInfo);
            aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);
            ProcessRecord app = getProcessRecordLocked(aInfo.processName,
                    aInfo.applicationInfo.uid, true);
            if (app == null || app.instrumentationClass == null) {
                intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
                mActivityStarter.startHomeActivityLocked(intent, aInfo, reason);    //启动FallbackHome
            }
        } else {
            Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());
        }

        return true;
    }

接着就会将FallbackHome启动起来,其实这个activity的代码非常简单不到100行,是个透明的activity,创建FallbackHome时注册ACTION_USER_UNLOCKED广播,然后进行判断用户是否都已经解锁,如果没有就结束执行。之后就会等待接收ACTION_USER_UNLOCKED广播,继续判断用户是否已经解锁,如果此时已经解锁,就找Home界面,如果没有找到就发延迟消息500ms再找一次,如果找到Launcher就会将FallbackHome finish掉。
下面就要看具体什么时候发送ACTION_USER_UNLOCKED广播了。

代码位置packages/apps/Settings/src/com/android/settings/FallbackHome.java

 * Copyright (C) 2015 The Android Open Source Project

package com.android.settings;

import android.app.Activity;

public class FallbackHome extends Activity {
    private static final String TAG = "FallbackHome";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Set ourselves totally black before the device is provisioned so that
        // we don't flash the wallpaper before SUW
        if (Settings.Global.getInt(getContentResolver(),
                Settings.Global.DEVICE_PROVISIONED, 0) == 0) {
            setTheme(android.R.style.Theme_Black_NoTitleBar_Fullscreen);
        }

        registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
        maybeFinish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            maybeFinish();
        }
    };

    private void maybeFinish() {
        if (getSystemService(UserManager.class).isUserUnlocked()) {
            final Intent homeIntent = new Intent(Intent.ACTION_MAIN)
                    .addCategory(Intent.CATEGORY_HOME);
            final ResolveInfo homeInfo = getPackageManager().resolveActivity(homeIntent, 0);
            if (Objects.equals(getPackageName(), homeInfo.activityInfo.packageName)) {
                Log.d(TAG, "User unlocked but no home; let's hope someone enables one soon?");
                mHandler.sendEmptyMessageDelayed(0, 500);
            } else {
                Log.d(TAG, "User unlocked and real home found; let's go!");
                finish();
            }
        }
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            maybeFinish();
        }
    };
}

发送ACTION_USER_UNLOCKED广播

在开机将近尾声时WindowManagerService会调用enableScreenIfNeededLocked函数来判断是否将Screen enable。通过Handler发送ENABLE_SCREEN消息到主线程

void enableScreenIfNeededLocked() {

if (mDisplayEnabled) {
    return;
}
if (!mSystemBooted && !mShowingBootMessages) {
    return;
}

mH.sendEmptyMessage(H.ENABLE_SCREEN);

}

在mH的handleMessage中处理消息ENABLE_SCREEN,调用函数performEnableScreen来处理。

    final class H extends Handler {
        ........
        public static final int ENABLE_SCREEN = 16;
        ........
        @Override
        public void handleMessage(Message msg) {

                case ENABLE_SCREEN: {
                    performEnableScreen();
                    break;
                }
       ........
}

在performEnableScreen函数中判断是否要enable Screen的两个主要因素有两个:
1.checkWaitingForWindowsLocked所有Windows是否绘制完成
2.checkBootAnimationCompleteLocked开机动画时候完成
如果都完成了会通知AMS开机动画完成了,并且要enable Screen了。

public void performEnableScreen() {

synchronized(mWindowMap) {
    if (mDisplayEnabled) {   //如果设备已经enabled,返回
        return;
    }
    if (!mSystemBooted && !mShowingBootMessages) {   //如果不是系统启动,并且没有启动信息,返回
        return;
    }

    // Don't enable the screen until all existing windows have been drawn.
    if (!mForceDisplayEnabled && checkWaitingForWindowsLocked()) {    //如果不是强制设备enable,并且Windows还没有绘制完成,返回
        return;
    }

    ...........

    if (!mForceDisplayEnabled && !checkBootAnimationCompleteLocked()) {    //如果不是强制设备enable,并且开机动画还没有结束,返回
        return;
    }

    EventLog.writeEvent(EventLogTags.WM_BOOT_ANIMATION_DONE, SystemClock.uptimeMillis());
    mDisplayEnabled = true;
    if (DEBUG_SCREEN_ON || DEBUG_BOOT) Slog.i(TAG_WM, "******************** ENABLING SCREEN!");

    // Enable input dispatch.
    mInputMonitor.setEventDispatchingLw(mEventDispatchingEnabled);
}

try {
    mActivityManager.bootAnimationComplete();   //通知ActivityManagerService开机动画完成
} catch (RemoteException e) {
}

mPolicy.enableScreenAfterBoot();    //通知ActivityManagerService Screen可以enable

// Make sure the last requested orientation has been applied.
updateRotationUnchecked(false, false);

}

检查Windows是否绘制完成主要是检查是否有启动message,是否有Wallpaper,Wallpaper是否可用,是否有Keyguard进行判断。

private boolean checkWaitingForWindowsLocked() {

        boolean haveBootMsg = false;    //是否有启动message
        boolean haveApp = false;       //是否有APP
        // if the wallpaper service is disabled on the device, we're never going to have
        // wallpaper, don't bother waiting for it
        boolean haveWallpaper = false;    //是否有Wallpaper
        boolean wallpaperEnabled = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_enableWallpaperService)
                && !mOnlyCore;   //Wallpaper是否可用
        boolean haveKeyguard = true;   //是否有Keyguard
        // TODO(multidisplay): Expand to all displays?
        final WindowList windows = getDefaultWindowListLocked();   //获取所有的Windows
        final int N = windows.size();
        for (int i=0; i<N; i++) {
            WindowState w = windows.get(i);
            if (w.isVisibleLw() && !w.mObscured && !w.isDrawnLw()) {
                return true;
            }
            if (w.isDrawnLw()) {   判断Window的属性
                if (w.mAttrs.type == TYPE_BOOT_PROGRESS) {
                    haveBootMsg = true;
                } else if (w.mAttrs.type == TYPE_APPLICATION) {
                    haveApp = true;
                } else if (w.mAttrs.type == TYPE_WALLPAPER) {
                    haveWallpaper = true;
                } else if (w.mAttrs.type == TYPE_STATUS_BAR) {
                    haveKeyguard = mPolicy.isKeyguardDrawnLw();
                }
            }
        }

        // If we are turning on the screen to show the boot message,
        // don't do it until the boot message is actually displayed.
        if (!mSystemBooted && !haveBootMsg) {
            return true;
        }

        // If we are turning on the screen after the boot is completed
        // normally, don't do so until we have the application and
        // wallpaper.
        if (mSystemBooted && ((!haveApp && !haveKeyguard) ||
                (wallpaperEnabled && !haveWallpaper))) {
            return true;
        }

        return false;
    }

检查开机动画是否完成,主要就是判断开机动画服务是否在运行,如果仍然在运行,就会发送一个200ms的延迟消息CHECK_IF_BOOT_ANIMATION_FINISHED,每200ms都再检查一次

private boolean checkBootAnimationCompleteLocked() {

if (SystemService.isRunning(BOOT_ANIMATION_SERVICE)) {
    mH.removeMessages(H.CHECK_IF_BOOT_ANIMATION_FINISHED);
    mH.sendEmptyMessageDelayed(H.CHECK_IF_BOOT_ANIMATION_FINISHED,
            BOOT_ANIMATION_POLL_INTERVAL);
    if (DEBUG_BOOT) Slog.i(TAG_WM, "checkBootAnimationComplete: Waiting for anim complete");
    return false;
}
if (DEBUG_BOOT) Slog.i(TAG_WM, "checkBootAnimationComplete: Animation complete!");
return true;

}

在处理CHECK_IF_BOOT_ANIMATION_FINISHED消息时,会再次判断开机动画是否完成,如果完成了就会调用performEnableScreen往下面执行,否则的还是每隔200ms发一次消息检查开机动画是否完成。

        case CHECK_IF_BOOT_ANIMATION_FINISHED: {
            final boolean bootAnimationComplete;
            synchronized (mWindowMap) {
                if (DEBUG_BOOT) Slog.i(TAG_WM, "CHECK_IF_BOOT_ANIMATION_FINISHED:");
                bootAnimationComplete = checkBootAnimationCompleteLocked();
            }
            if (bootAnimationComplete) {
                performEnableScreen();
            }
        }

当开机动画完成后就会调用AMS的bootAnimationComplete函数。

@Override
public void bootAnimationComplete() {

final boolean callFinishBooting;
synchronized (this) {
    callFinishBooting = mCallFinishBooting;
    mBootAnimationComplete = true;   //设置mBootAnimationComplete为true
}
if (callFinishBooting) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "FinishBooting");
    finishBooting();     //调用finishBooting
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}

}

在finishBooting中通过mUserController调用sendBootCompletedLocked函数

final void finishBooting() {

synchronized (this) {
    if (!mBootAnimationComplete) {
        mCallFinishBooting = true;
        return;
    }
    mCallFinishBooting = false;
}
................
// Let system services know.
mSystemServiceManager.startBootPhase(SystemService.PHASE_BOOT_COMPLETED);
...............
        mUserController.sendBootCompletedLocked(
                new IIntentReceiver.Stub() {
                    @Override
                    public void performReceive(Intent intent, int resultCode,
                            String data, Bundle extras, boolean ordered,
                            boolean sticky, int sendingUser) {
                        synchronized (ActivityManagerService.this) {
                            requestPssAllProcsLocked(SystemClock.uptimeMillis(),
                                    true, false);
                        }
                    }
                });

UserController.java代码位置frameworks/base/services/core/java/com/android/server/am/
具体流程图如下:
请输入图片描述
经过一系列的代码跳转,最终调用UserController的finishUserUnlocked函数来发送ACTION_USER_UNLOCKED广播。

    void finishUserUnlocked(final UserState uss) {
                .................
                final Intent unlockedIntent = new Intent(Intent.ACTION_USER_UNLOCKED);
                unlockedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
                unlockedIntent.addFlags(
                        Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND);
                mService.broadcastIntentLocked(null, null, unlockedIntent, null, null, 0, null,
                        null, null, AppOpsManager.OP_NONE, null, false, false, MY_PID, SYSTEM_UID,
                        userId);
               .................
}

当FallbackHome接收到ACTION_USER_UNLOCKED广播后,并且此时用户已经解锁,就会将将FallbackHome finish掉,启动launcher。
问题分析
就是因为现在启动Launcher时多了一个流程,导致启动launcher比原来6.0要慢。通过查看开机log可以看到从启动FallbackHome到启动google桌面花费了4s

18:10:50.653   769  1910 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10000100 cmp=com.android.settings/.FallbackHome} from uid 0 on display 0

18:10:54.586  2029  2029 D FallbackHome: User unlocked and real home found; let's go!

18:10:54.615   769  2207 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10000100 cmp=com.google.android.setupwizard/.SetupWizardActivity} from uid 0 on display 0

如果启动FallbackHome到启动launcher之间相隔的时间再长一点就可能发生开机过程中显示launcher时发生几秒的黑屏
小结
Android 7.0新增了DirectBoot功能,AOSP中为实现该功能修改了开机代码流程,并且这部分流程并未根据设备是否支持DirectBoot做区分,只是流程上做了兼容,确保不支持DirectBoot的设备在这套流程下也能正常开机。
在这套流程下,用户解锁后才可进入非directBootAware应用,包括Launcher。com.android.settings/.FallbackHome中判断用户解锁状态,已解锁才会Finish掉去启动Launcher,未解锁就等待ACTION_USER_UNLOCKED广播后再去启动Launcher。非DirectBoot模式下耗时4s就是在等待finishBooting后的系统广播ACTION_USER_UNLOCKED。
目前已从APP和PackageManagerService的角度尝试修改,在开机流程中绕过FallbackHome,但验证失败:
1)去除FallbackHome的android.intent.category.Home属性会导致停留在开机动画之后的界面。因为此时仍旧处于未解锁状态,且Launcher非directBootAware应用,PMS中的限制导致此时无法启动Launcher;
2)修改FallbackHome和Launcher的优先级仍旧先启动FallbackHome;
3)将Launcher标记为directBootAware应用会导致开机后Launcher crash。因为Launcher中的widget仍旧是非directBootAware的,此时仍旧无法启动,除非将widget相关的APP都标记为directBootAware;
4)PMS依赖手机当前的状态,需要user解锁才能正常查询。如果强制修改,不考虑DirectBoot和当前启动状态,即使当前user未解锁,依然可以查询符合条件的component,修改后会有无法开机的现象。因为Launcher不是directBootAware的,当前手机user尚未解锁,涉及存储相关的解锁也未进行。

开机绕过FallbackHome涉及的修改面很多,并非通过修改APP或PMS可以实现,还涉及存储区域解锁以及用户状态和ACTION_USER_UNLOCKED广播的修改,对AOSP开机流程改动较大,暂时尚未有较好的优化方案,欢迎大神指教

0x01    工具
①Android Studio最新版。
②apktool尽量使用最新版的。
③ideasmali插件。下载地址https://github.com/JesusFreke/smali/wiki/smalidea0x02     具体步骤
安装ideasmali插件,选择File->Settings->Plugins,安装之前下载的ideasmali插件。

282719-20160616220413854-617943589.png
以调试状态启动app
282719-20160616220534823-469140707.png

通过ddms查看端口:

282719-20160616220550854-328753120.png
如果不用 ddms 可以通过adb shell执行 ps | grep kugou命令查看转发端口:

然后进行端口转发:
adb forward tcp:8700 jdwp:24551
记住这时候需要将DDMS关掉,不然会出现错误
282719-20160616220623229-698993770.png

创建DebugSmali/src目录,使用apktool或者baksmali反编译要调试的apk到DebugSmali/src目录,使用Android Studio导入该目录,如图所示:
282719-20160616220644167-1801955265.png

然后选择Create project from existing sources,之后一直选择next。
282719-20160616220711745-239639306.png
成功导入工程后右键点击 src 目录,设定Mark Directory As->Sources Root
282719-20160616220744510-1233548569.png
配置远程调试的选项,选择Run-->Edit Configurations:
282719-20160616220921667-1473804168.png
增加一个Remote调试的调试选项,端口选择:8700
   282719-20160616220939667-429384959.png
设置端口号
  282719-20160616220957526-1101324191.png
之后选择File-->Project Structure 配置JDK
282719-20160616221015338-1600841699.png
282719-20160616221109120-1445199850.png
下好断点之后Run->Debug,
282719-20160616221208776-173578043.png
稍等几秒,断点触发后就可以单步调试  
282719-20160616221228995-47769053.png

一、前言
今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk,结果被人反编译了,那心情真心不舒服。虽然我们混淆,做到native层,但是这都是治标不治本。反编译的技术在更新,那么保护Apk的技术就不能停止。现在网上有很多Apk加固的第三方平台,最有名的应当属于:爱加密和梆梆加固了。其实加固有些人认为很高深的技术,其实不然,说的简单点就是对源Apk进行加密,然后在套上一层壳即可,当然这里还有一些细节需要处理,这就是本文需要介绍的内容了。

二、原理解析
下面就来看一下Android中加壳的原理:

请输入图片描述

我们在加固的过程中需要三个对象:

1、需要加密的Apk(源Apk)

2、壳程序Apk(负责解密Apk工作)

3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)

主要步骤:

我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。

在这个过程中我们可能需要了解的一个知识是:如何将源Apk和壳Apk进行合并成新的Dex

这里就需要了解Dex文件的格式了。下面就来简单介绍一下Dex文件的格式

具体Dex文件格式的详细介绍可以查看这个文件:http://download.csdn.net/detail/jiangwei0910410003/9102599

主要来看一下Dex文件的头部信息,其实Dex文件和Class文件的格式分析原理都是一样的,他们都是有固定的格式,我们知道现在反编译的一些工具:

1、jd-gui:可以查看jar中的类,其实他就是解析class文件,只要了解class文件的格式就可以

2、dex2jar:将dex文件转化成jar,原理也是一样的,只要知道Dex文件的格式,能够解析出dex文件中的类信息就可以了

当然我们在分析这个文件的时候,最重要的还是头部信息,应该他是一个文件的开始部分,也是索引部分,内部信息很重要。

请输入图片描述

我们今天只要关注上面红色标记的三个部分:

1) checksum

文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。

2) signature

使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。

3) file_size

Dex 文件的大小 。

为什么说我们只需要关注这三个字段呢?

因为我们需要将一个文件(加密之后的源Apk)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。

不过这里还需要一个操作,就是标注一下我们加密的Apk的大小,因为我们在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。

所以总结一下我们需要做:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了。

我们修改之后得到新的Dex文件样式如下:

请输入图片描述

那么我们知道原理了,下面就是代码实现了。所以这里有三个工程:

1、源程序项目(需要加密的Apk)

2、脱壳项目(解密源Apk和加载Apk)

3、对源Apk进行加密和脱壳项目的Dex的合并

三、项目案例
下面先来看一下源程序

1、需要加密的源程序Apk项目:ForceApkObj

请输入图片描述
需要一个Application类,这个到后面说为什么需要:

MyApplication.java

package com.example.forceapkobj;

import android.app.Application;
import android.util.Log;

public class MyApplication extends Application{


@Override
public void onCreate() {
    super.onCreate();
    Log.i("demo", "source apk onCreate:"+this);
}

}
就是打印一下onCreate方法。

MainActivity.java

package com.example.forceapkobj;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    TextView content = new TextView(this);
    content.setText("I am Source Apk");
    content.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View arg0) {
            Intent intent = new Intent(MainActivity.this, SubActivity.class);
            startActivity(intent);
        }});
    setContentView(content);
    
    Log.i("demo", "app:"+getApplicationContext());
    
}

}
也是打印一下内容。

2、加壳程序项目:DexShellTools

请输入图片描述
加壳程序其实就是一个Java工程,因为我们从上面的分析可以看到,他的工作就是加密源Apk,然后将其写入到脱壳Dex文件中,修改文件头,得到一个新的Dex文件即可。

看一下代码:

package com.example.reforceapk;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;

public class mymain {

/**
 * @param args
 */
public static void main(String[] args) {
    // TODO Auto-generated method stub
    try {
        File payloadSrcFile = new File("force/ForceApkObj.apk");   //需要加壳的程序
        System.out.println("apk size:"+payloadSrcFile.length());
        File unShellDexFile = new File("force/ForceApkObj.dex");    //解客dex
        byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
        byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex
        int payloadLen = payloadArray.length;
        int unShellDexLen = unShellDexArray.length;
        int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
        byte[] newdex = new byte[totalLen]; // 申请了新的长度
        //添加解壳代码
        System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
        //添加加密后的解壳数据
        System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
        //添加解壳数据长度
        System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
        //修改DEX file size文件头
        fixFileSizeHeader(newdex);
        //修改DEX SHA1 文件头
        fixSHA1Header(newdex);
        //修改DEX CheckSum文件头
        fixCheckSumHeader(newdex);

        String str = "force/classes.dex";
        File file = new File(str);
        if (!file.exists()) {
            file.createNewFile();
        }
        
        FileOutputStream localFileOutputStream = new FileOutputStream(str);
        localFileOutputStream.write(newdex);
        localFileOutputStream.flush();
        localFileOutputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

//直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){
    for(int i = 0;i<srcdata.length;i++){
        srcdata[i] = (byte)(0xFF ^ srcdata[i]);
    }
    return srcdata;
}

/**
 * 修改dex头,CheckSum 校验码
 * @param dexBytes
 */
private static void fixCheckSumHeader(byte[] dexBytes) {
    Adler32 adler = new Adler32();
    adler.update(dexBytes, 12, dexBytes.length - 12);//从12到文件末尾计算校验码
    long value = adler.getValue();
    int va = (int) value;
    byte[] newcs = intToByte(va);
    //高位在前,低位在前掉个个
    byte[] recs = new byte[4];
    for (int i = 0; i < 4; i++) {
        recs[i] = newcs[newcs.length - 1 - i];
        System.out.println(Integer.toHexString(newcs[i]));
    }
    System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)
    System.out.println(Long.toHexString(value));
    System.out.println();
}
/**
 * int 转byte[]
 * @param number
 * @return
 */
public static byte[] intToByte(int number) {
    byte[] b = new byte[4];
    for (int i = 3; i >= 0; i--) {
        b[i] = (byte) (number % 256);
        number >>= 8;
    }
    return b;
}

/**
 * 修改dex头 sha1值
 * @param dexBytes
 * @throws NoSuchAlgorithmException
 */
private static void fixSHA1Header(byte[] dexBytes)
        throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
    byte[] newdt = md.digest();
    System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
    //输出sha-1值,可有可无
    String hexstr = "";
    for (int i = 0; i < newdt.length; i++) {
        hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
                .substring(1);
    }
    System.out.println(hexstr);
}

/**
 * 修改dex头 file_size值
 * @param dexBytes
 */
private static void fixFileSizeHeader(byte[] dexBytes) {
    //新文件长度
    byte[] newfs = intToByte(dexBytes.length);
    System.out.println(Integer.toHexString(dexBytes.length));
    byte[] refs = new byte[4];
    //高位在前,低位在前掉个个
    for (int i = 0; i < 4; i++) {
        refs[i] = newfs[newfs.length - 1 - i];
        System.out.println(Integer.toHexString(newfs[i]));
    }
    System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
}
/**
 * 以二进制读出文件内容
 * @param file
 * @return
 * @throws IOException
 */
private static byte[] readFileBytes(File file) throws IOException {
    byte[] arrayOfByte = new byte[1024];
    ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
    FileInputStream fis = new FileInputStream(file);
    while (true) {
        int i = fis.read(arrayOfByte);
        if (i != -1) {
            localByteArrayOutputStream.write(arrayOfByte, 0, i);
        } else {
            return localByteArrayOutputStream.toByteArray();
        }
    }
}

}
下面来分析一下:
请输入图片描述

红色部分其实就是最核心的工作:

1>、加密源程序Apk文件

byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
加密算法很简单:

//直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){

for(int i = 0;i<srcdata.length;i++){
    srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;

}
对每个字节进行异或一下即可。

(说明:这里是为了简单,所以就用了很简单的加密算法了,其实为了增加破解难度,我们应该使用更高效的加密算法,同事最好将加密操作放到native层去做)

2>、合并文件:将加密之后的Apk和原脱壳Dex进行合并

int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
byte[] newdex = new byte[totalLen]; // 申请了新的长度
//添加解壳代码
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
//添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容

3>、在文件的末尾追加源程序Apk的长度

//添加解壳数据长度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
4>、修改新Dex文件的文件头信息:file_size; sha1; check_sum

//修改DEX file size文件头
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件头
fixSHA1Header(newdex);
//修改DEX CheckSum文件头
fixCheckSumHeader(newdex);
具体修改可以参照之前说的文件头格式,修改指定位置的字节值即可。

这里我们还需要两个输入文件:

1>、源Apk文件:ForceApkObj.apk

2>、脱壳程序的Dex文件:ForceApkObj.dex

那么第一个文件我们都知道,就是上面的源程序编译之后的Apk文件,那么第二个文件我们怎么得到呢?这个就是我们要讲到的第三个项目:脱壳程序项目,他是一个Android项目,我们在编译之后,能够得到他的classes.dex文件,然后修改一下名称就可。

3、脱壳项目:ReforceApk
请输入图片描述

在讲解这个项目之前,我们先来了解一下这个脱壳项目的工作:

1>、通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。

关于这部分内容,不了解的同学可以看一下ActivityThread.java的源码:
请输入图片描述

或者直接看一下这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104455

如何得到系统加载Apk的类加载器,然后我们怎么将加载进来的Apk运行起来等问题都在这篇文章中说到了。

2>、找到源程序的Application,通过反射建立并运行。

这里需要注意的是,我们现在是加载一个完整的Apk,让他运行起来,那么我们知道一个Apk运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类。所以我们必须找到解密之后的源Apk的Application类,运行的他的onCreate方法,这样源Apk才开始他的运行生命周期。这里我们如何得到源Apk的Application的类呢?这个我们后面会说道。使用meta标签进行设置。

下面来看一下整体的流程图:

请输入图片描述
所以我们看到这里还需要一个核心的技术就是动态加载。关于动态加载技术,不了解的同学可以看这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104581

下面来看一下代码:

package com.example.reforceapk;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;

public class ProxyApplication extends Application{

private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;

//这是context 赋值
@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    try {
        //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
        File odex = this.getDir("payload_odex", MODE_PRIVATE);
        File libs = this.getDir("payload_lib", MODE_PRIVATE);
        odexPath = odex.getAbsolutePath();
        libPath = libs.getAbsolutePath();
        apkFileName = odex.getAbsolutePath() + "/payload.apk";
        File dexFile = new File(apkFileName);
        Log.i("demo", "apk size:"+dexFile.length());
        if (!dexFile.exists())
        {
            dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
            // 读取程序classes.dex文件
            byte[] dexdata = this.readDexFileFromApk();
            
            // 分离出解壳后的apk文件已用于动态加载
            this.splitPayLoadFromDex(dexdata);
        }
        // 配置动态加载环境
        Object currentActivityThread = RefInvoke.invokeStaticMethod(
                "android.app.ActivityThread", "currentActivityThread",
                new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
        String packageName = this.getPackageName();//当前apk的包名
        //下面两句不是太理解
        ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                "android.app.ActivityThread", currentActivityThread,
                "mPackages");
        WeakReference wr = (WeakReference) mPackages.get(packageName);
        //创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
        DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
                libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                        "android.app.LoadedApk", wr.get(), "mClassLoader"));
        //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
        //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
        RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                wr.get(), dLoader);
        
        Log.i("demo","classloader:"+dLoader);
        
        try{
            Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
            Log.i("demo", "actObj:"+actObj);
        }catch(Exception e){
            Log.i("demo", "activity:"+Log.getStackTraceString(e));
        }
        

    } catch (Exception e) {
        Log.i("demo", "error:"+Log.getStackTraceString(e));
        e.printStackTrace();
    }
}

@Override
public void onCreate() {
    {
        //loadResources(apkFileName);
        
        Log.i("demo", "onCreate");
        // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
        String appClassName = null;
        try {
            ApplicationInfo ai = this.getPackageManager()
                    .getApplicationInfo(this.getPackageName(),
                            PackageManager.GET_META_DATA);
            Bundle bundle = ai.metaData;
            if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
                appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
            } else {
                Log.i("demo", "have no application class name");
                return;
            }
        } catch (NameNotFoundException e) {
            Log.i("demo", "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
        //有值的话调用该Applicaiton
        Object currentActivityThread = RefInvoke.invokeStaticMethod(
                "android.app.ActivityThread", "currentActivityThread",
                new Class[] {}, new Object[] {});
        Object mBoundApplication = RefInvoke.getFieldOjbect(
                "android.app.ActivityThread", currentActivityThread,
                "mBoundApplication");
        Object loadedApkInfo = RefInvoke.getFieldOjbect(
                "android.app.ActivityThread$AppBindData",
                mBoundApplication, "info");
        //把当前进程的mApplication 设置成了null
        RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
                loadedApkInfo, null);
        Object oldApplication = RefInvoke.getFieldOjbect(
                "android.app.ActivityThread", currentActivityThread,
                "mInitialApplication");
        //http://www.codeceo.com/article/android-context.html
        ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
                .getFieldOjbect("android.app.ActivityThread",
                        currentActivityThread, "mAllApplications");
        mAllApplications.remove(oldApplication);//删除oldApplication
        
        ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
                .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
                        "mApplicationInfo");
        ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
                .getFieldOjbect("android.app.ActivityThread$AppBindData",
                        mBoundApplication, "appInfo");
        appinfo_In_LoadedApk.className = appClassName;
        appinfo_In_AppBindData.className = appClassName;
        Application app = (Application) RefInvoke.invokeMethod(
                "android.app.LoadedApk", "makeApplication", loadedApkInfo,
                new Class[] { boolean.class, Instrumentation.class },
                new Object[] { false, null });//执行 makeApplication(false,null)
        RefInvoke.setFieldOjbect("android.app.ActivityThread",
                "mInitialApplication", currentActivityThread, app);
        ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
                "android.app.ActivityThread", currentActivityThread,
                "mProviderMap");
        Iterator it = mProviderMap.values().iterator();
        while (it.hasNext()) {
            Object providerClientRecord = it.next();
            Object localProvider = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread$ProviderClientRecord",
                    providerClientRecord, "mLocalProvider");
            RefInvoke.setFieldOjbect("android.content.ContentProvider",
                    "mContext", localProvider, app);
        }
        
        Log.i("demo", "app:"+app);
        
        app.onCreate();
    }
}

/**
 * 释放被加壳的apk文件,so文件
 * @param data
 * @throws IOException
 */
private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
    int ablen = apkdata.length;
    //取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化
    byte[] dexlen = new byte[4];
    System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
    ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
    DataInputStream in = new DataInputStream(bais);
    int readInt = in.readInt();
    System.out.println(Integer.toHexString(readInt));
    byte[] newdex = new byte[readInt];
    //把被加壳apk内容拷贝到newdex中
    System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
    //这里应该加上对于apk的解密操作,若加壳是加密处理的话
    //?
    
    //对源程序Apk进行解密
    newdex = decrypt(newdex);
    
    //写入apk文件   
    File file = new File(apkFileName);
    try {
        FileOutputStream localFileOutputStream = new FileOutputStream(file);
        localFileOutputStream.write(newdex);
        localFileOutputStream.close();
    } catch (IOException localIOException) {
        throw new RuntimeException(localIOException);
    }
    
    //分析被加壳的apk文件
    ZipInputStream localZipInputStream = new ZipInputStream(
            new BufferedInputStream(new FileInputStream(file)));
    while (true) {
        ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
        if (localZipEntry == null) {
            localZipInputStream.close();
            break;
        }
        //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
        String name = localZipEntry.getName();
        if (name.startsWith("lib/") && name.endsWith(".so")) {
            File storeFile = new File(libPath + "/"
                    + name.substring(name.lastIndexOf('/')));
            storeFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(storeFile);
            byte[] arrayOfByte = new byte[1024];
            while (true) {
                int i = localZipInputStream.read(arrayOfByte);
                if (i == -1)
                    break;
                fos.write(arrayOfByte, 0, i);
            }
            fos.flush();
            fos.close();
        }
        localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
}

/**
 * 从apk包里面获取dex文件内容(byte)
 * @return
 * @throws IOException
 */
private byte[] readDexFileFromApk() throws IOException {
    ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
    ZipInputStream localZipInputStream = new ZipInputStream(
            new BufferedInputStream(new FileInputStream(
                    this.getApplicationInfo().sourceDir)));
    while (true) {
        ZipEntry localZipEntry = localZipInputStream.getNextEntry();
        if (localZipEntry == null) {
            localZipInputStream.close();
            break;
        }
        if (localZipEntry.getName().equals("classes.dex")) {
            byte[] arrayOfByte = new byte[1024];
            while (true) {
                int i = localZipInputStream.read(arrayOfByte);
                if (i == -1)
                    break;
                dexByteArrayOutputStream.write(arrayOfByte, 0, i);
            }
        }
        localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
    return dexByteArrayOutputStream.toByteArray();
}
// //直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
    for(int i=0;i<srcdata.length;i++){
        srcdata[i] = (byte)(0xFF ^ srcdata[i]);
    }
    return srcdata;
}


//以下是加载资源
protected AssetManager mAssetManager;//资源管理器  
protected Resources mResources;//资源  
protected Theme mTheme;//主题  

protected void loadResources(String dexPath) {  
    try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, dexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    superRes.getDisplayMetrics();  
    superRes.getConfiguration();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());  
    mTheme = mResources.newTheme();  
    mTheme.setTo(super.getTheme());
}  

@Override  
public AssetManager getAssets() {  
    return mAssetManager == null ? super.getAssets() : mAssetManager;  
}  

@Override  
public Resources getResources() {  
    return mResources == null ? super.getResources() : mResources;  
}  

@Override  
public Theme getTheme() {  
    return mTheme == null ? super.getTheme() : mTheme;  
} 

}
首先我们来看一下具体步骤的代码实现:

1>、得到脱壳Apk中的dex文件,然后从这个文件中得到源程序Apk.进行解密,然后加载

//这是context 赋值
@Override
protected void attachBaseContext(Context base) {

super.attachBaseContext(base);
try {
    //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
    File odex = this.getDir("payload_odex", MODE_PRIVATE);
    File libs = this.getDir("payload_lib", MODE_PRIVATE);
    odexPath = odex.getAbsolutePath();
    libPath = libs.getAbsolutePath();
    apkFileName = odex.getAbsolutePath() + "/payload.apk";
    File dexFile = new File(apkFileName);
    Log.i("demo", "apk size:"+dexFile.length());
    if (!dexFile.exists())
    {
        dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
        // 读取程序classes.dex文件
        byte[] dexdata = this.readDexFileFromApk();

        // 分离出解壳后的apk文件已用于动态加载
        this.splitPayLoadFromDex(dexdata);
    }
    // 配置动态加载环境
    Object currentActivityThread = RefInvoke.invokeStaticMethod(
            "android.app.ActivityThread", "currentActivityThread",
            new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
    String packageName = this.getPackageName();//当前apk的包名
    //下面两句不是太理解
    ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mPackages");
    WeakReference wr = (WeakReference) mPackages.get(packageName);
    //创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
    DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
            libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));
    //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
    //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
            wr.get(), dLoader);

    Log.i("demo","classloader:"+dLoader);

    try{
        Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
        Log.i("demo", "actObj:"+actObj);
    }catch(Exception e){
        Log.i("demo", "activity:"+Log.getStackTraceString(e));
    }
} catch (Exception e) {
    Log.i("demo", "error:"+Log.getStackTraceString(e));
    e.printStackTrace();
}

}
这里需要注意的一个问题,就是我们需要找到一个时机,就是在脱壳程序还没有运行起来的时候,来加载源程序的Apk,执行他的onCreate方法,那么这个时机不能太晚,不然的话,就是运行脱壳程序,而不是源程序了。查看源码我们知道。Application中有一个方法:attachBaseContext这个方法,他在Application的onCreate方法执行前就会执行了,那么我们的工作就需要在这里进行

1)、从脱壳程序Apk中找到源程序Apk,并且进行解密操作

//创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:"+dexFile.length());
if (!dexFile.exists())
{

dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
// 读取程序classes.dex文件
byte[] dexdata = this.readDexFileFromApk();

// 分离出解壳后的apk文件已用于动态加载
this.splitPayLoadFromDex(dexdata);

}
这个脱壳解密操作一定要和我们之前的加壳以及加密操作对应,不然就会出现Dex加载错误问题

A) 从Apk中获取到Dex文件

/**

  • 从apk包里面获取dex文件内容(byte)
  • @return
  • @throws IOException
    */

private byte[] readDexFileFromApk() throws IOException {

ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
ZipInputStream localZipInputStream = new ZipInputStream(
        new BufferedInputStream(new FileInputStream(
                this.getApplicationInfo().sourceDir)));
while (true) {
    ZipEntry localZipEntry = localZipInputStream.getNextEntry();
    if (localZipEntry == null) {
        localZipInputStream.close();
        break;
    }
    if (localZipEntry.getName().equals("classes.dex")) {
        byte[] arrayOfByte = new byte[1024];
        while (true) {
            int i = localZipInputStream.read(arrayOfByte);
            if (i == -1)
                break;
            dexByteArrayOutputStream.write(arrayOfByte, 0, i);
        }
    }
    localZipInputStream.closeEntry();
}
localZipInputStream.close();
return dexByteArrayOutputStream.toByteArray();

}
其实就是解压Apk文件,直接得到dex文件即可

B) 从脱壳Dex中得到源Apk文件

/**

  • 释放被加壳的apk文件,so文件
  • @param data
  • @throws IOException
    */

private void splitPayLoadFromDex(byte[] apkdata) throws IOException {

int ablen = apkdata.length;
//取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化
byte[] dexlen = new byte[4];
System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
System.out.println(Integer.toHexString(readInt));
byte[] newdex = new byte[readInt];
//把被加壳apk内容拷贝到newdex中
System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
//这里应该加上对于apk的解密操作,若加壳是加密处理的话
//?

//对源程序Apk进行解密
newdex = decrypt(newdex);

//写入apk文件   
File file = new File(apkFileName);
try {
    FileOutputStream localFileOutputStream = new FileOutputStream(file);
    localFileOutputStream.write(newdex);
    localFileOutputStream.close();
} catch (IOException localIOException) {
    throw new RuntimeException(localIOException);
}

//分析被加壳的apk文件
ZipInputStream localZipInputStream = new ZipInputStream(
        new BufferedInputStream(new FileInputStream(file)));
while (true) {
    ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
    if (localZipEntry == null) {
        localZipInputStream.close();
        break;
    }
    //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
    String name = localZipEntry.getName();
    if (name.startsWith("lib/") && name.endsWith(".so")) {
        File storeFile = new File(libPath + "/"
                + name.substring(name.lastIndexOf('/')));
        storeFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(storeFile);
        byte[] arrayOfByte = new byte[1024];
        while (true) {
            int i = localZipInputStream.read(arrayOfByte);
            if (i == -1)
                break;
            fos.write(arrayOfByte, 0, i);
        }
        fos.flush();
        fos.close();
    }
    localZipInputStream.closeEntry();
}
localZipInputStream.close();

}
C) 解密源程序Apk

////直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {

for(int i=0;i<srcdata.length;i++){
    srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;

}
这个解密算法和加密算法是一致的

2>、加载解密之后的源程序Apk

//配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(

    "android.app.ActivityThread", "currentActivityThread",
    new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493

String packageName = this.getPackageName();//当前apk的包名
//下面两句不是太理解
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(

    "android.app.ActivityThread", currentActivityThread,
    "mPackages");

WeakReference wr = (WeakReference) mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,

    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
            "android.app.LoadedApk", wr.get(), "mClassLoader"));

//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",

    wr.get(), dLoader);

Log.i("demo","classloader:"+dLoader);

try{

Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
Log.i("demo", "actObj:"+actObj);

}catch(Exception e){

Log.i("demo", "activity:"+Log.getStackTraceString(e));

}
2)、找到源程序的Application程序,让其运行

@Override
public void onCreate() {

{
    //loadResources(apkFileName);
    
    Log.i("demo", "onCreate");
    // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
    String appClassName = null;
    try {
        ApplicationInfo ai = this.getPackageManager()
                .getApplicationInfo(this.getPackageName(),
                        PackageManager.GET_META_DATA);
        Bundle bundle = ai.metaData;
        if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
            appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
        } else {
            Log.i("demo", "have no application class name");
            return;
        }
    } catch (NameNotFoundException e) {
        Log.i("demo", "error:"+Log.getStackTraceString(e));
        e.printStackTrace();
    }
    //有值的话调用该Applicaiton
    Object currentActivityThread = RefInvoke.invokeStaticMethod(
            "android.app.ActivityThread", "currentActivityThread",
            new Class[] {}, new Object[] {});
    Object mBoundApplication = RefInvoke.getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mBoundApplication");
    Object loadedApkInfo = RefInvoke.getFieldOjbect(
            "android.app.ActivityThread$AppBindData",
            mBoundApplication, "info");
    //把当前进程的mApplication 设置成了null
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
            loadedApkInfo, null);
    Object oldApplication = RefInvoke.getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mInitialApplication");
    //http://www.codeceo.com/article/android-context.html
    ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
            .getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread, "mAllApplications");
    mAllApplications.remove(oldApplication);//删除oldApplication
    
    ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
            .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
                    "mApplicationInfo");
    ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
            .getFieldOjbect("android.app.ActivityThread$AppBindData",
                    mBoundApplication, "appInfo");
    appinfo_In_LoadedApk.className = appClassName;
    appinfo_In_AppBindData.className = appClassName;
    Application app = (Application) RefInvoke.invokeMethod(
            "android.app.LoadedApk", "makeApplication", loadedApkInfo,
            new Class[] { boolean.class, Instrumentation.class },
            new Object[] { false, null });//执行 makeApplication(false,null)
    RefInvoke.setFieldOjbect("android.app.ActivityThread",
            "mInitialApplication", currentActivityThread, app);
    ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mProviderMap");
    Iterator it = mProviderMap.values().iterator();
    while (it.hasNext()) {
        Object providerClientRecord = it.next();
        Object localProvider = RefInvoke.getFieldOjbect(
                "android.app.ActivityThread$ProviderClientRecord",
                providerClientRecord, "mLocalProvider");
        RefInvoke.setFieldOjbect("android.content.ContentProvider",
                "mContext", localProvider, app);
    }
    
    Log.i("demo", "app:"+app);
    
    app.onCreate();
}

}
直接在脱壳的Application中的onCreate方法中进行就可以了。这里我们还可以看到是通过AndroidManifest.xml中的meta标签获取源程序Apk中的Application对象的。

下面来看一下AndoridManifest.xml文件中的内容:
请输入图片描述

在这里我们定义了源程序Apk的Application类名。

项目下载:http://download.csdn.net/detail/jiangwei0910410003/9102741

四、运行程序
那么到这里我们就介绍完了,这三个项目的内容,下面就来看看如何运行吧:

运行步骤:

第一步:得到源程序Apk文件和脱壳程序的Dex文件

请输入图片描述

运行源程序和脱壳程序项目,之后得到这两个文件(记得将classes.dex文件改名ForceApkObj.dex),然后使用加壳程序进行加壳:

请输入图片描述

这里的ForceApkObj.apk文件和ForceApkObj.dex文件是输入文件,输出的是classes.dex文件。

第二步:替换脱壳程序中的classes.dex文件

我们在第一步中得到加壳之后的classes.dex文件之后,并且我们在第一步运行脱壳项目的时候得到一个ReforceApk.apk文件,这时候我们使用解压缩软件进行替换:
请输入图片描述

第三步:我们在第二步的时候得到替换之后的ReforceApk.apk文件,这个文件因为被修改了,所以我们需要从新对他签名,不然运行也是报错的。

请输入图片描述

工具下载:http://download.csdn.net/detail/jiangwei0910410003/9102767

下载之后的工具需要用ReforeceApk.apk文件替换ReforceApk_des.apk文件,然后运行run.bat就可以得到签名之后的文件了。

run.bat文件的命令如下:

cd C:UsersiDesktopforceapks
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei
del ReforceApk.apk

这里最主要的命令就是中间的一条签名的命令,关于命令的参数说明如下:

jarsigner -verbose -keystore 签名文件 -storepass 密码 -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 签名后的文件 签名前的apk alias名称

eg:
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei

签名文件的密码:123456
alais的密码:123456

所以这里我们在得到ReforceApk.apk文件的时候,需要签名,关于Eclipse中如何签名一个Apk的话,这里就不多说了,自己google一下吧:

请输入图片描述

那么通过上面的三个步骤之后我们得到一个签名之后的最终文件:ReforceApk_des.apk

我们安装这个Apk,然后运行,效果如下:

请输入图片描述

看到运行结果的那一瞬间,我们是多么的开心,多么的有成就感,但是这个过程中遇到的问题,是可想而知的。

我们这个时候再去反编译一下源程序Apk(这个文件是我们脱壳出来的payload.apk,看ReforeceApk中的代码,就知道他的位置了)

请输入图片描述

发现dex文件格式是不正确的。说明我们的加固是成功的。

五、遇到的问题
1、研究的过程中遇到签名不正确的地方,开始的时候,直接替换dex文件之后,就直接运行了Apk,但是总是提示签名不正确。

2、运行的过程中说找不到源程序中的Activity,这个问题其实我在动态加载的那篇文章中说道了,我们需要在脱壳程序中的AndroidManifest.xml中什么一下源程序中的Activiity:
请输入图片描述

六、技术要点
1、对Dex文件格式的了解

2、动态加载技术的深入掌握

3、Application的执行流程的了解

4、如何从Apk中得到Dex文件

5、如何从新签名一个Apk程序

七、综合概述
我们通过上面的过程可以看到,关于Apk加固的工作还是挺复杂的,涉及到的东西也挺多的,下面就在来总结一下吧:

1、加壳程序

任务:对源程序Apk进行加密,合并脱壳程序的Dex文件 ,然后输入一个加壳之后的Dex文件

语言:任何语言都可以,不限于Java语言

技术点:对Dex文件格式的解析

2、脱壳程序

任务:获取源程序Apk,进行解密,然后动态加载进来,运行程序

语言:Android项目(Java)

技术点:如何从Apk中获取Dex文件,动态加载Apk,使用反射运行Application

八、总结
Android中的Apk反编译可能是每个开发都会经历的事,但是在反编译的过程中,对于源程序的开发者来说那是不公平的,那么Apk加固也是应运而生,但是即使是这样,我们也还是做不到那么的安全,现在网上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那么加固还不是怎么安全。最后一句话:逆向和加固是一个永不停息的战争。

一、前言

在之前一篇文章已经介绍了一款网络访问软件的破解教程,当时采用的突破口是应用程序本身的一个漏洞,就是没有关闭日志信息,我们通过抓取日志获取到关键信息来找到突破口进行破解的。那篇文章也说到了,如果这个app应用没有日志信息,我们该怎么办呢?那么这时候就需要采用抓包来寻找突破口了。

二、安装Fiddler证书

我们通过Fiddler连接手机进行抓包查看,应用访问数据信息都是用的HTTPS协议,也就是加密的数据,我们可能无法查看,比较麻烦,本文就来讲解如何查看这些加密数据了。
20170830181046775.png

首先我们知道Fiddler抓包工具是可以抓HTTPS数据的,就是把Fiddler证书安装到设备中即可。安装过程也很简单:

第一步:导出Fidder的证书文件
20170830181216285.png

进入Fiddler选项中:
20170830181255597.png

然后直接选择导出证书到桌面即可。

第二步:把证书安装到设备中

先把上一步导出来的证书文件,放到设备的SD目录下,然后在设备中直接打开这个证书文件,会提示安装信息,当然这里在老版本系统中,可能还需要去设置页面进行操作:
20170830181434561.png

在设置中,选择安全,然后选择从SD卡安装证书,就可以正常安装了。安装成功之后,可以查看这个证书信息:
20170830181425858.png

三、抓取样本app的数据包

这样,我们的设备就有了Fiddler证书,这时候在用Fiddler抓包会看到正常的HTTPS协议数据了:
20170830181553675.png

我们可以查看返回的数据信息:
20170830182717082.png
可惜的是,这里的数据显示是不全的。所以这时候,我们得想办法获取返回的全部数据,可以借助Wireshark工具进行操作了,而Wireshark工具相对于Fiddler工具来说会分析的更加详细,但是Fiddler更加方便快捷。但是Wireshark工具只能在PC端抓包运行,所以如果想用这个工具抓取手机端数据的话,可以用两种方式:

第一种:Wireshark+Fiddler工具

Fiddler作为手机端的代理在PC上访问流量。然后Wireshark可以抓取PC上的请求,这样来做到用Wireshark抓取手机端的请求。

第二种:Wireshark+tcpdump工具

当然我们还可以利用tcpdump工具来直接抓取手机端的请求数据,一般保存为pcap文件格式,然后在PC端用Wireshark来分析pcap文件格式即可。关于tcpdump如何抓包生成pcap文件,可以自行搜索即可。这里不做解释了。

很多同学会考虑第二种,但是第二种会失败的,因为这里需要抓取HTTPS协议的数据,所以有证书相关的信息,如果直接用tcpdump工具抓包生成文件,然后用Wireshark工具分析是会失败的。因为Wireshark分析TLS协议数据是需要设置证书信息的,不然是解析失败的。所以这里采用第一种方式进行操作。

四、Wireshark分析TLS数据包

因为Wireshark分析TLS协议的数据是需要设置证书信息的,不然看到的也是加密数据信息:
20170830183728757.png
那么这里的问题其实就转化成,如何利用Wireshark来解密TLS数据信息了,这个就比较简单了,我们可以借助浏览器自动生成的证书信息,来作为解密的工具。这里需要借助一个知识点:就是浏览器在访问HTTPS协议信息的时候都会有这么个提示:
20170830183901364.png
让我们信任这个证书:
20170830183929754.png
然后,我们确认安全,这时候,如果设置了一个系统环境变量。浏览器会把证书信息保存到指定文件中的,这个系统环境变量就是:SSLKEYLOGFILE,然后自定义一个SSLKEY的保存文件名称即可。
20170830184037716.png
因为我们在上面用Fiddler可以抓到HTTPS请求链接的,这时候我们把这个链接放到浏览器中访问一次:
20170830184315578.png
然后就会有相对应的SSLKEY数据保存下来了,可以去看看这个信息:
20170830184245337.png

这样我们就弄到了这个url对应的证书信息了,这里可以看到其实是借助浏览器的功能获取到的,下面就来Wireshark中设置这个证书信息:
20170830184450955.png
在Wireshark的Edit->Preferences->Protocols->SSL,设置刚刚保存的SSLKEY信息文件位置,保存即可。

下面就来开始抓包分析吧,在启动Wireshark抓包的时候,可能有的同学无法看到抓包的网络接口信息:
20170830184644451.png

这个就需要启动系统的npf服务了,我们可以这么做:

01、在开始–>运行
02、输入:%windir%/system32
03、将会开启一个文件夹窗口,在里面找到CMD.EXE(cmd.exe)
04、点击右键,选择”以管理员身份运行”
05、输入命令:net start npf
06、系统提示NetGroup Packet Filter Driver 服务已经启动成功。
07、至此,Wireshark再点击Interfaces list就可以正常选择需要捕捉的接口了。
08、如果需要关闭此服务,是要在命令行输入命令:net stop npf 即可。
20170830184904432.png
操作成功之后,再去Wireshark选择接口列表信息就有了网络接口信息了:
20170830184935782.png

这里运行的时候,一定要记得把本地连接和无线网络连接都选择上,以免请求被遗漏:
20170830185026879.png
点击开始之后,就会发现很多请求在刷屏,为了想看到我们关心的数据,可以利用一些过滤规则来进行过滤,而这里其实有很多过滤规则命令的,感兴趣的同学可以自行搜索操作实验即可,这里我们只想看到SSL协议的请求信息:
20170830185233793.png
然后我们在利用浏览器去刷新刚刚那个HTTPS的请求,会发现多了很多TLS协议请求,如果这里没有设置上面的SSLKEY信息的话,看到的都是加密信息:
20170830185553768.png

这里简单说一下TLS协议的原理,直接盗了网上的一张图:
20170819125636956.gif

设置了SSLKEY信息之后,就可以看到Application Data会被解密:
20170830185615378.png
而且,这时候,底部会多出一个Decrypted SSL选项卡。这里解密之后会发现在SSL层后面多了一个超文本传输层,也就是HTTP等信息了。然后我们在选中这条信息,右键查看他的HTTP信息流:
20170830190052952.png

记得是有OK字样的那条HTTP协议数据信息条目,这里是因为已经解密了,所以看到的是HTTP协议:
20170830190134539.png

五、解密返回数据信息

这里就可以清晰的看到这次请求信息和返回信息了。然后我们把这部分的数据,在用AES进行解密:
20170830190225512.png

看到打印的结果就是一个返回json信息:
20170830190243959.png
关于这里的解密方法,之前一篇文章在用Jadx分析样本已经看到了:
20170830190333905.png

我们直接把这个方法拷贝出来,写一个简单的Java成功就可解密了。不了解的同学去回顾上一篇介绍的内容吧。

六、TLS知识点总结

好了,到这里我们就介绍完了,如何利用Wireshark和Fiddler这两个抓包神器,解密HTTPS请求信息。关于TLS协议可能有的同学还不太了解,而且他和SSL的区别是啥,其实TLS是SSL的升级版:
20170830190528713.png

现在很多协议都会用到TLS了,而且TLS1.3版本已经出来了。比如WX就采用了这个协议进行数据加密的。我们有了这个技能之后,后面就可以很轻松的分析TLS协议数据了,当然我们一定要熟练掌握Wireshark这个工具的用法,我们在学习过网络工程课的时候,都知道网络协议栈,比如物理层,网络层,传输层,应用层:
20170830190825390.png

我们在上面看到Wireshark的底部信息栏中也是有这么几层信息的:
20170830190950940.png
在SSL层,需要解密才能看到后面的超文本传输层协议的详细信息了,也就是上面的解密方法进行操作的结果。

七、问题解惑

其实看到这里有的同学会有好奇的地方:
第一个:开始利用Fiddler可以直接进行解密数据了,为何还要用Wireshark去抓包呢?因为我们可以看到Fiddler抓取的信息是不全的。当然有时候可能是全的,那么就不需要在用Wireshark进行抓包分析了。
第二个:在把Fiddler中抓取到的链接放到浏览器中访问,生成对应的SSLKEY信息文件。在浏览器中不是直接可以看到返回数据了吗?为什么还要用Wireshark进行抓包呢?这个的确是可以看到全部的返回信息在浏览器中访问的话,但是利用Wireshark抓包是为了更加信息的看到TLS协议的访问流程。这个也是为了以后熟练操作做准备。

在逆向研究中,有时候抓包是一个非常重要的一个突破口点。所以抓包分析包信息也是至关重要的技能。

一、前言

今天总算迎来了破解系列的最后一篇文章了,之前的两篇文章分别为:

第一篇:如何使用Eclipse动态调试smali源码

第二篇:如何使用IDA动态调试SO文件

现在要说的就是最后一篇了,如何应对Android中一些加固apk安全防护,在之前的两篇破解文章中,我们可以看到一个是针对于Java层的破解,一个是针对于native层的破解,还没有涉及到apk的加固,那么今天就要来介绍一下如何应对现在市场中一些加固的apk的破解之道,现在市场中加固apk的方式一般就是两种:一种是对源apk整体做一个加固,放到指定位置,运行的时候在解密动态加载,还有一种是对so进行加固,在so加载内存的时候进行解密释放。我们今天主要看第一种加固方式,就是对apk整体进行加固。

二、案例分析

按照国际惯例,咋们还是得用一个案例来分析讲解,这次依然采用的是阿里的CTF比赛的第三题:
20160607085728942.png
题目是:要求输入一个网页的url,然后会跳转到这个页面,但是必须要求弹出指定内容的Toast提示,这个内容是:祥龙!

了解到题目,我们就来简单分析一下,这里大致的逻辑应该是,输入的url会传递给一个WebView控件,进行展示网页,如果按照题目的逻辑的话,应该是网页中的Js会调用本地的一个Java方法,然后弹出相应的提示,那么这里我们就来开始操作了。

按照我们之前的破解步骤:

第一步:肯定是先用解压软件搞出来他的classes.dex文件,然后使用dex2jar+jd-gui进行查看java代码
20160607090240404.png
擦,这里我们看到这里只有一个Application类,从这里我们可以看到,这个apk可能被加固了,为什么这么说呢?因为我们知道一个apk加固,外面肯定得套一个壳,这个壳必须是自定义的Application类,因为他需要做一些初始化操作,那么一般现在加固的apk的壳的Application类都喜欢叫StubApplication。而且,这里我们可以看到,除了一个Application类,没有其他任何类了,包括我们的如可Activity类都没有了,那么这时候会发现,很蛋疼,无处下手了。

第二步:我们会使用apktool工具进行apk的反编译,得到apk的AndroidManifest.xml和资源内容
20160607090637910.png

反编译之后,看到程序会有一个入口的Activity就是MainActivity类,我们记住一点就是,不管最后的apk如何加固,即使我们看不到代码中的四大组件的定义,但是肯定会在AndroidManifest.xml中声明的,因为如果不声明的话,运行是会报错的。那么这里我们也分析完了该分析的内容,还是没发现我们的入口Activity类,而且我们知道他肯定是放在本地的一个地方,因为需要解密动态加载,所以不可能是放在网上的,肯定是本地,所以这里就有一些技巧了:

当我们发现apk中主要的类都没有了,肯定是apk被加固了,加固的源程序肯定是在本地,一般会有这么几个地方需要注意的:

1、应用程序的asset目录,我们知道这个目录是不参与apk的资源编译过程的,所以很多加固的应用喜欢把加密之后的源apk放到这里

2、把源apk加密放到壳的dex文件的尾部,这个肯定不是我们这里的案例,但是也有这样的加固方式,这种加固方式会发现使用dex2jar工具解析dex是失败的,我们这时候就知道了,肯定对dex做了手脚

3、把源apk加密放到so文件中,这个就比较难了,一般都是把源apk进行拆分,存到so文件中,分析难度会加大的。

一般都是这三个地方,其实我们知道记住一点:就是不管源apk被拆分,被加密了,被放到哪了,只要是在本地,我们都有办法得到他的。

好了,按照这上面的三个思路我们来分析一下,这个apk中加固的源apk放在哪了?

通过刚刚的dex文件分析,发现第二种方式肯定不可能了,那么会放在asset目录中吗?我们查看asset目录:
20160607091944975.png
看到asset目录中的确有两个jar文件,而且我们第一反应是使用jd-gui来查看jar,可惜的是打开失败,所以猜想这个jar是经过处理了,应该是加密,所以这里很有可能是存放源apk的地方。但是我们上面也说了还有第三种方式,我们去看看libs目录中的so文件:
20160607092113360.png

擦,这里有三个so文件,而我们上面的Application中加载的只有一个so文件:libmobisec.so,那么其他的两个so文件很有可能是拆分的apk文件的藏身之处。

通过上面的分析之后,我们大致知道了两个地方很有可能是源apk的藏身地方,一个是asset目录,一个是libs目录,那么分析完了之后,我们发现现在面临两个问题:

第一个问题:asset目录中的jar文件被处理了,打不开,也不知道处理逻辑

第二个问题:libs目录中的三个so文件,唯一加载了libmobisec.so文件了

那么这里现在的唯一入口就是这个libmobisec.so文件了,因为上层的代码没有,没法分析,下面来看一下so文件:
20160607093137739.png
擦,发现蛋疼的是,这里没有特殊的方法,比如Java_开头的什么,所以猜测这里应该是自己注册了native方法,混淆了native方法名称,那么到这里,我们会发现我们遇到的问题用现阶段的技术是没法解决了。
<h1>三、获取正确的dex内容</h1>
分析完上面的破解流程之后,发现现在首要的任务是先得到源apk程序,通过分析知道,处理的源apk程序很难找到和分析,所以这里就要引出今天说的内容了,使用动态调试,给libdvm.so中的函数:dvmDexFileOpenPartial 下断点,然后得到dex文件在内存中的起始地址和大小,然后dump处dex数据即可。

那么这里就有几个问题了:

第一个问题:为何要给dvmDexFileOpenPartial 这个函数下断点?

因为我们知道,不管之前的源程序如何加固,放到哪了,最终都是需要被加载到内存中,然后运行的,而且是没有加密的内容,那么我们只要找到这的dex的内存位置,把这部分数据搞出来就可以了,管他之前是如何加固的,我们并不关心。那么问题就变成了,如何获取加载到内存中的dex的地址和大小,这个就要用到这个函数了:dvmDexFileOpenPartial 因为这个函数是最终分析dex文件,加载到内存中的函数:

int dvmDexFileOpenPartial(const void addr, int len, DvmDex* ppDvmDex);

第一个参数就是dex内存起始地址,第二个参数就是dex大小。

第二个问题:如何使用IDA给这个函数下断点

我们在之前的一篇文章中说到了,在动态调试so,下断点的时候,必须知道一个函数在内存中的绝对地址,而函数的绝对地址是:这个函数在so文件中的相对地址+so文件映射到内存中的基地址,这里我们知道这个函数肯定是存在libdvm.so文件中的,因为一般涉及到dvm有关的函数功能都是存在这个so文件中的,那么我们可以从这个so文件中找到这个函数的相对地址,运行程序之后,在找到libdvm.so的基地址,相加即可,那么我们如何获取到这个libdvm.so文件呢?这个文件是存放在设备的/system/lib目录下的:
20160607094324823.png

那么我们只需要使用adb pull 把这个so文件搞出来就可以了。

好了,解决了这两个问题,下面就开始操作了:

第一步:运行设备中的android_server命令,使用adb forward进行端口转发
20160607095201420.png

这里的android_server工具可以去ida安装目录中dbgsrv文件夹中找到
20160607095502288.png
第二步:使用命令以debug模式启动apk

adb shell am start -D -n com.ali.tg.testapp/.MainActivity
20160607100210908.png
因为我们需要给libdvm.so下断点,这个库是系统库,所以加载时间很早,所以我们需要像之前给JNI_OnLoad函数下断点一样,采用debugger模式运行程序,这里我们通过上面的AndroidManifest.xml中,得到应用的包名和入口Activity:
20160607095857996.png
而且这里的android:debuggable=true,可以进行debug调试的。

第三步:双开IDA,一个用于静态分析libdvm.so,一个用于动态调试libdvm.so
20160607095641211.png

通过IDA的Debugger菜单,进行进程附加操作:
20160607095825954.png

第四步:使用jdb命令启动连接attach调试器

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

但是这里可能会出现这样的错误:
20160607100705629.png
这个是因为,我们的8700端口没有指定,这时候我们可以通过Eclipse的DDMS进行端口的查看:
20160607100742785.png

看到了,这里是8600端口,但是基本端口8700不在,所以这里我们有两种处理方式,一种是把上面的命令的端口改成8600,还有一种是选中这个应用,使其具有8700端口:
20160607100845051.png
点击这个条目即可,这时候我们在运行上面的jdb命令:
20160607100919301.png
处于等待状态。

第四步:给dvmDexFileOpenPartial函数下断点

使用一个IDA静态分析得到这个函数的相对地址:43308
20160607101506757.png

在动态调试的IDA解密,使用Ctrl+S键找到libdvm.so的在内存中的基地址:41579000
20160607101554179.png

然后将两者相加得到绝对地址:43308+41579000=415BC308,使用G键,跳转:
20160607101643146.png
跳转到dvmDexFileOpenPartial函数处,下断点:
20160607101707461.png
第五步:点击运行按钮或者F9运行程序

之前的jdb命令就连接上了:
20160607105542993.png
IDA出现如下界面,不要理会,一路点击取消按钮即可
20160607101806711.png

运行到了dvmDexFileOpenPartial函数处:
20160607101851118.png
使用F8进行单步调试,但是这里需要注意的是,只要运行过了PUSH命令就可以了,记得不要越过下面的BL命令,因为我们没必要走到那里,当执行了PUSH命令之后,我们就是使用脚本来dump处内存中的dex数据了,这里有一个知识点,就是R0~R4寄存器一般是用来存放一个函数的参数值的,那么我们知道dvmDexFileOpenPartial函数的第一个参数就是dex内存起始地址,第二个参数就是dex大小:
20160607102458682.png
那么这里就可以使用这样的脚本进行dump即可:

static main(void)
{
auto fp, dex_addr, end_addr;
fp = fopen(“F:\dump.dex”, “wb”);
end_addr = r0 + r1;
for ( dex_addr = r0; dex_addr < end_addr; dex_addr ++ )
fputc(Byte(dex_addr), fp);
}

脚本不解释了,非常简单,而且这个是固定的格式,以后dump内存中的dex都是这段代码,我们将dump出来的dex保存到F盘中。

然后这时候,我们使用:Shirt+F2 调出IDA的脚本运行界面:
20160607102713621.png
点击运行,这里可能需要等一会,运行成功之后,我们去F盘得到dump.dex文件,其实这里我们的IDA使命就完成了,因为我们得到了内存的dex文件了,下面开始就简单了,只要分析dex文件即可

四、分析正确的dex文件内容

我们拿到dump.dex之后,使用dex2jar工具进行反编译:
20160607103052700.png
可惜的是,报错了,反编译失败,主要是有一个类导致的,开始我以为是dump出来的dex文件有问题,最后我用baksmali工具得到smali文件是可以的,所以不是dump出来的问题,我最后用baksmali工具将dex转化成smali源码:

java -jar baksmali-2.0.3.jar -o C:classout/ dump.dex
20160607103452639.png
得到的smali源码目录classout在C盘中:
20160607103541016.png
我们得到了指定的smali源码了。

那么下面我们就可以使用静态方式分析smali即可了:

首先找到入口的MainActivity源码:
20160607103920645.png
这里不解释了,肯定是找按钮的点击事件代码处,这里是一个btn_listener变量,看这个变量的定义:
20160607103957005.png
是MainActivity$1内部类定义,查看这个类的smali源码,直接查看他的onClick方法:
20160607104019127.png
这里可以看到,把EditText中的内容,用Intent传递给WebViewActivity中,但是这里的intent数据的key是加密的。

下面继续看WebViewActivity这个类:
20160607104451856.png
我们直接查找onCreate方法即可,看到这里是初始化WebView,然后进行一些设置,这里我们看到一个@JavascriptInterface

这个注解,我们在使用WebView的时候都知道,他是用于Js中能够访问的设置了这个注解的方法,没有这个注解的方法Js是访问不了的

注意:

我们知道这个注解是在SDK17加上的,也就是Android4.2版本中,那么在之前的版本中没有这个注解,任何public的方法都可以在JS代码中访问,而Java对象继承关系会导致很多public的方法都可以在JS中访问,其中一个重要的方法就是  getClass()。然后JS可以通过反射来访问其他一些内容。那么这里就有这个问题了:比如下面的一段JS代码:

<script>
function findobj(){
for (var obj in window) {
if (“getClass” in window[obj]) {
return window[obj]
}
}
}
</script> 

看到了,这段js代码很危险的,使用getClass方法,得到这个对象(java中的每个对象都有这个方法的),用这个方法可以得到一个java对象,然后我们就可以调用这个对象中的方法了。这个也算是WebView的一个漏洞了。

所以通过引入 @JavascriptInterface注解,则在JS中只能访问 @JavascriptInterface注解的函数。这样就可以增强安全性。

回归到正题,我们上面分析了smali源码,看到了WebView的一些设置信息,我们可以继续往下面看:
20160607105639544.png

这里的我们看到了一些重要的方法,一个是addJavascriptInterface,一个是loadUrl方法。

我们知道addjavaascriptInterface方法一般的用法:

mWebView.addJavascriptInterface(new JavaScriptObject(this), “jiangwei”);

第一个参数是本地的Java对象,第二个参数是给Js中使用的对象的名称。然后js得到这个对象的名称就可以调用本地的Java对象中的方法了。

看了这里的addjavaascriptInterface方法代码,可以看到,这里用

ListViewAutoScrollHelpern;->decrypt_native(Ljava/lang/String;I)Ljava/lang/String;

将js中的名称进行混淆加密了,这个也是为了防止恶意的网站来拦截url,然后调用我们本地的Java中的方法。

注意:

这里又存在一个关于WebView的安全问题,就是这里的js访问的对象的名称问题,比如现在我的程序中有一个Js交互的类,类中有一个获取设备重要信息的方法,比如这里获取设备的imei方法,如果我们的程序没有做这样名称的混淆的话,破解者得到这个js名称和方法名,然后就伪造一个恶意url,来调用我们程序中的这个方法,比如这样一个例子:
20160607110522148.png
然后在设置js名称:
20160607110529101.png

我们就可以伪造一个恶意的url页面来访问这个方法,比如这个恶意的页面代码如下:
20160607110657747.png

运行程序:
20160607110735338.png

看到了,这里恶意的页面就成功的调用了程序中的一个重要方法。

所以,我们可以看到,对Js交互中的对象名称做混淆是必要的,特别是本地一些重要的方法。

回归到正题,我们分析完了WebView的一些初始化和设置代码,而且我们知道如果要被Js访问的方法,那么必须要有@JavascriptInterface注解 因为在Java中注解也是一个类,所以我们去注解类的源码看看那个被Js调用的方法:
20160607111937793.png

这里看到了有一个showToast方法,展示的内容:u7965u9f99uff01 ,我们在线转化一下:
20160607112357863.png

擦,这里就是题目要求展示的内容。

好了,到这里我们就分析完了apk的逻辑了,下面我们来整理一下:

1、在MainActivity中输入一个页面的url,跳转到WebViewActivity进行展示

2、WebViewActivity有Js交互,需要调用本地Java对象中的showToast方法展示消息

问题:

因为这里的js对象名称进行了加密,所以这里我们自己编写一个网页,但是不知道这个js对象名称,无法完成showToast方法的调用

五、破解的方法

下面我们就来分析一下如何解决上面的问题,其实解决这个问题,我们现有的方法太多了

第一种方法:修改smali源码,把上面的那个js对象名称改成我们自己想要的,比如:jiangwei,然后在自己编写的页面中直接调用:jiangwei.showToast方法即可,不过这里需要修改smali源码,在使用smali工具回编译成dex文件,在弄到apk中,在运行。方法是可行的,但是感觉太复杂,这里不采用

第二种方法:利用Android4.2中的WebView的漏洞,直接使用如下Js代码即可
20160607120036553.png
这里根本不需要任何js对象的名称,只需要方法名就可以完成调用,所以这里可以看到这个漏洞还是很危险的。

第三种方法:我们看到了那个加密方法,我们自己写一个程序,来调用这个方法,尽然得到正确的js对象名称,这里我们就采用这种方式,因为这个方式有一个新的技能,所以这里我就讲解一下了。

那么如果用第三种方法的话,就需要再去分析那个加密方法逻辑了:
20160607120604622.png
android.support.v4.widget.ListViewAutoScrollHelpern在这个类中,我们再去查找这个smali源码:
20160607120639569.png
这个类加载了libtranslate.so库,而且加密方法是native层的,那么我们用IDA查看libtranslate.so库:
20160607120955711.png

我们搜一下Java开头的函数,发现并没有和decrypt_native方法对应的native函数,说明这里做了native方法的注册混淆,我们直接看JNI_OnLoad函数:
20160607121050836.png
这里果然是自己注册了native函数,但是分析到这里,我就不往下分析了,为什么呢?因为我们其实没必要搞清楚native层的函数功能,我们知道了Java层的native方法定义,那么我们可以自己定义一个这么个native方法来调用libtranslate.so中的加密函数功能:
20160607121304320.png
我们新建一个Demo工程,仿造一个ListViewAutoScrollHelpern类,内部在定义一个native方法:
20160607121352525.png

然后我们在MainActivity中加载libtranslate.so:
20160607121449541.png
然后调用那个native方法,打印结果:
20160607121508275.png

这里的方法的参数可以查看smali源码中的那个方法参数:
20160607121530572.png

点击运行,发现有崩溃的,我们查看log信息:
20160607121608541.png
是libtranslate.so中有一个PagerTitleStripIcsn类找不到,这个类应该也有一个native方法,我们在构造这个类:
20160607121727122.png
再次运行,还是报错,原因差不多,还需要在构造一个类:TaskStackBuilderJellybeann
20160607121801135.png

好了,再次点击运行:
20160607121816807.png
OK了,成功了,从这个log信息可以看出来了,解密之后的js对象名称是:SmokeyBear,那么下面就简单了,我们在构造一个url页面,直接调用:SmokeyBear.showToast即可。

注意:

这里我们看到,如果知道了Java层的native方法的定义,那么我们就可以调用这个native方法来获取native层的函数功能了,这个还是很不安全的,但是我们如何防止自己的so被别人调用呢?之前的一篇文章:Android中的安全攻防之战 已经说过了,可以在so中的native函数做一个应用的签名校验,只有属于自己的签名应用才能调用,否则直接退出。

六,开始测试

上面已经知道了js的对象名称,下面我们就来构造这个页面了:
20160607124520787.png

那么这里又有一个问题了,这个页面构造好了?放哪呢?有的同学说我有服务器,放到服务器上,然后输入url地址就可以了,的确这个方法是可以的,但是有的同学没有服务器怎么办呢?这个也是有方法的,我们知道WebView的loadUrl方法是可以加载本地的页面的,所以我们可以把这个页面保存到本地,但是需要注意的是,这里不能存到SD卡中,因为这个应用没有读取SD的权限,我们可以查看他的AndroidManifest.xml文件:
20160607124855727.png
我们在不重新打包的情况下,是没办法做到的,那么放哪呢?其实很简单了,放在这个应用的/data/data/com.ali.tg.testapp/目录下即可,因为除了SD卡位置,这个位置是最好的了,那么我们知道WebView的loadUrl方法在加载本地的页面的格式是:

file:///data/data/com.ali.tg.testapp/crack.html

那么我们直接输入即可

注意:

这里在说一个小技巧:就是我们在一个文本框中输入这么多内容,是不是有点蛋疼,我们其实可以借助于命令来实现输入的,就是使用:adb shell input text ”我们需要输入的内容“。

具体用法很简单,打开我们需要输入内容的EditText,点击调出系统的输入法界面,然后执行上面的命令即可:
20160607125325290.png
不过这里有一个小问题,就是他不识别分号:
20160607125353946.png
不过我们直接修改成分号点击进入:
20160607125441446.png

运行成功,看到了toast的展示。

手痒的同学可以戳这里:http://download.csdn.net/detail/jiangwei0910410003/9543445

七、内容整理

到这里我们就破解成功了,下面来看看整理一下我们的破解步骤:

1、破解的常规套路

我们按照破解惯例,首先解压出classses.dex文件,使用dex2jar工具查看java代码,但是发现只有一个Application类,所以猜测apk被加壳了,然后用apktool来反编译apk,得到他的资源文件和AndroidManifest.xml内容,找到了包名和入口的Activity类。

2、加固apk的源程序一般存放的位置

知道是加固apk了,那么我们就分析,这个加固的apk肯定是存放在本地的一个地方,一般是三个地方:

1》应用的asset目录中

2》应用的libs中的so文件中

3》应用的dex文件的末尾

我们分析了一下之后,发现asset目录中的确有两个jar文件,但是打不开,猜测是被经过处理了,所以我们得分析处理逻辑,但是这时候我们也没有代码,怎么分析呢?所以这时候就需要借助于dump内存dex技术了:

不管最后的源apk放在哪里,最后都是需要经历解密动态加载到内存中的,所以分析底层加载dex源码,知道有一个函数:dvmDexFileOpenPartial 这个函数有两个重要参数,一个是dex的其实地址,一个是dex的大小,而且知道这个函数是在libdvm.so中的。所以我们可以使用IDA进行动态调试获取信息

3、双开IDA开始获取内存中的dex内容

双开IDA,走之前的动态破解so方式来给dvmDexFileOpenPartial函数下断点,获取两个参数的值,然后使用一段脚本,将内存中的dex数据保存到本地磁盘中。

4、分析获取到的dex内容

得到了内存中的dex之后,我们在使用dex2jar工具去查看源码,但是发现保存,以为是dump出来的dex格式有问题,但是最后使用baksmali工具进行处理,得到smali源码是可以的,然后我们就开始分析smali源码。

5、分析源码了解破解思路

通过分析源码得知在WebViewActivity页面中会加载一个页面,然后那个页面中的js会调用本地的Java对象中的一个方法来展示toast信息,但是这里我们遇到了个问题:Js的Java对象名称被混淆加密了,所以这时候我们需要去分析那个加密函数,但是这个加密函数是native的,然后我们就是用IDA去静态分析了这个native函数,但是没有分析完成,因为我们不需要,其实很简单,我们只需要结果,不需要过程,现在解密的内容我们知道了,native方法的定义也知道了,那么我们就去写一个简单的demo去调用这个so的native方法即可,结果成功了,我们得到了正确的Js对象名称。

6、了解WebView的安全性

WebView的早期版本的一个漏洞信息,在Android4.2之前的版本WebView有一个漏洞,就是可以执行Java对象中所有的public方法,那么在js中就可以这么处理了,先获取geClass方法获取这个对象,然后在调用这个对象中的一些特定方法即可,因为Java中所有的对象都有一个getClass方法,而这个方法是public的,同时能够返回当前对象。所以在Android4.2之后有了一个注解:

@JavascriptInterface ,只有这个注解标识的方法才能被Js中调用。

7、获取输入的新技能

验证结果的过程中我们发现了一个技巧,就是我们在输入很长的文本的时候,比较繁琐,可以借助adb shell input text命令来实现。

八、技术点概要

1、通过dump出内存中的dex数据,可以佛挡杀佛了,不管apk如何加固,最终都是需要加载到内存中的。

2、了解到了WebView的安全性的相关知识,比如我们在WebView中js对象名称做一次混淆还是有必要的,防止被恶意网站调用我们的本地隐私方法。

3、可以尝试调用so中的native方法,在知道了这个方法的定义之后

4、adb shell input text 命令来辅助我们的输入

九、总结

这里就介绍了Android中如何dump出那些加固的apk程序,其实核心就一个:不管上层怎么加固,最终加载到内存的dex肯定不是加固的,所以这个dex就是我们想要的,这里使用了IDA来动态调试libdvm.so中的dvmDexFileOpenPartial函数来获取内存中的dex内容,同时还可以使用gdb+gdbserver来获取,这个感兴趣的同学自行搜索吧。结合了之前的两篇文章,就算善始善终,介绍了Android中大体的破解方式,当然这三种方式不是万能的,因为加固和破解是相生相克的,没有哪个有绝对的优势,只是两者相互进步罢了,当然还有很多其他的破解方式,后面如果遇到的话,会在详细说明,我们的目的不是编写应用,而且让别人的应用变成炮灰!!