破解Android程序的方法通常是:使用ApkTool反编译APK文件,生成smali格式的反汇编代码;通过阅读smali文件的代码来理解程序的运行机制,找到突破口,并对代码进行修改;使用ApkTool重新编译生成APK文件并对其进行签名;运行测试——如此循环,直至程序被破解。
在实际分析中,还可以使用IDA Pro直接分析APK文件,使用dex2jar与jd-gui配合进行Java源码级的分析等。这些分析方法会在本书后面的章节中详细介绍。
ApkTool是一款常用的跨平台APK文件反编译工具,可以在Windows、macOS和Ubuntu平台上使用。访问https://github.com/ibotpeaches/Apktool,即可下载新版本ApkTool的jar包。针对不同的系统,还需要下载相关的运行时包装脚本,具体如下。
将下载的jar包和脚本放到同一个目录下,然后将该路径添加到系统的PATH环境变量中,就完成了ApkTool的安装。对macOS用户,更简单的方式是执行brew install apktool命令来安装。
进入终端或命令行,执行如下命令,对APK文件进行反编译。
$ apktool d ./app-debug.apk -o outdir
I: Using Apktool 2.2.2 on app-debug.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/android/Library/apktool/framework/1.apkI: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
如无意外,会在当前的outdir目录下生成反编译文件。反编译文件包含一系列目录和文件,smali目录中存放了程序的所有反汇编代码,res目录中存放的则是程序中所有的资源文件,这些目录的子目录和文件的组织结构与开发时源码目录的组织结构是一致的。
如何寻找突破口是分析一个程序的关键。对大部分Android程序来说,错误提示信息是指引我们找到关键代码的明灯。错误提示代码附近通常就是程序的核心验证代码,我们需要通过阅读这些代码来理解软件的注册流程。
错误提示属于Android程序中的字符串资源。在开发Android程序时,这些字符串可能会被硬编码到源码中,也可能引用自res\values目录下的strings.xml文件。APK文件在打包时,strings.xml中的字符串被加密存储为resources.arsc文件并保存到APK程序包中;如果APK文件被成功反编译,这个文件就被解密了。
回顾2.1.2节介绍的以命令行方式生成APK文件的内容,如果软件注册失败,会以Toast的形式弹出提示信息,我们可以以此为线索来寻找关键代码。res/values/string.xml文件的内容如下,除了系统默认生成的一系列以“abc_”开头的字符串,都是Crackme0201程序使用的字符串。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="abc_action_bar_home_description">Navigate home</string>
<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>
<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>
<string name="abc_action_bar_up_description">Navigate up</string>
<string name="abc_action_menu_overflow_description">More options</string>
<string name="abc_action_mode_done">Done</string>
<string name="abc_activity_chooser_view_see_all">See all</string>
<string name="abc_activitychooserview_choose_application">Choose an app</string>
<string name="abc_capital_off">OFF</string>
<string name="abc_capital_on">ON</string>
<string name="abc_search_hint">Search...</string>
<string name="abc_searchview_description_clear">Clear query</string>
<string name="abc_searchview_description_query">Search query</string>
<string name="abc_searchview_description_search">Search</string>
<string name="abc_searchview_description_submit">Submit query</string>
<string name="abc_searchview_description_voice">Voice search</string>
<string name="abc_shareactionprovider_share_with">Share with</string>
<string name="abc_shareactionprovider_share_with_application">Share with %s</string>
<string name="abc_toolbar_collapse_description">Collapse</string>
<string name="search_menu_title">Search</string>
<string name="status_bar_notification_info_overflow">999+</string>
<string name="abc_font_family_body_1_material">sans-serif</string>
<string name="abc_font_family_body_2_material">sans-serif-medium</string>
<string name="abc_font_family_button_material">sans-serif-medium</string>
<string name="abc_font_family_caption_material">sans-serif</string>
<string name="abc_font_family_display_1_material">sans-serif</string>
<string name="abc_font_family_display_2_material">sans-serif</string>
<string name="abc_font_family_display_3_material">sans-serif</string>
<string name="abc_font_family_display_4_material">sans-serif-light</string>
<string name="abc_font_family_headline_material">sans-serif</string>
<string name="abc_font_family_menu_material">sans-serif</string>
<string name="abc_font_family_subhead_material">sans-serif</string>
<string name="abc_font_family_title_material">sans-serif-medium</string>
<string name="app_name">Crackme0201</string>
<string name="hint_sn">请输入16位的注册码</string>
<string name="hint_username">请输入用户名</string>
<string name="info">Android程序破解演示实例</string>
<string name="menu_settings">Settings</string>
<string name="register">注册</string>
<string name="registered">程序已注册</string>
<string name="sn">注册码:</string>
<string name="successed">恭喜您!注册成功</string>
<string name="title_activity_main">Crackme0201</string>
<string name="unregister">程序未注册</string>
<string name="unsuccessed">无效用户名或注册码</string>
<string name="username">用户名:</string>
</resources>
在开发Android程序时,string.xml文件中的所有字符串资源都在gen//R.java文件的String类中标识,每个字符串都有唯一的int类型的索引值。使用ApkTool反编译APK文件后,所有的索引值都保存在与string.xml文件处于同一目录的public.xml文件中。在以上代码中,“无效用户名或注册码”的字符串名称为“unsuccessed”。
打开public.xml文件,其内容如下。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public type="attr" name="drawerArrowStyle" id="0x7f010000" />
<public type="attr" name="height" id="0x7f010001" />
...
<public type="string" name="app_name" id="0x7f060021" />
<public type="string" name="hint_sn" id="0x7f060022" />
<public type="string" name="hint_username" id="0x7f060023" />
<public type="string" name="info" id="0x7f060024" />
<public type="string" name="menu_settings" id="0x7f060025" />
<public type="string" name="register" id="0x7f060026" />
<public type="string" name="registered" id="0x7f060027" />
<public type="string" name="sn" id="0x7f060028" />
<public type="string" name="successed" id="0x7f060029" />
<public type="string" name="title_activity_main" id="0x7f06002a" />
<public type="string" name="unregister" id="0x7f06002b" />
<public type="string" name="unsuccessed" id="0x7f06002c" />
<public type="string" name="username" id="0x7f06002d" />
unsuccessed字符串的id值为0x7f06002c。在outdir目录中搜索含有“0x7f05000c”字符串的文件,结果什么都没找到!这是怎么回事呢?仔细查看,会发现在outdir/unknown目录下有一个instant-run.zip文件。使用unzip命令查看其内容,具体如下。
$ unzip -l outdir/unknown/instant-run.zip
Archive: outdir/unknown/instant-run.zip
Length Date Time Name
-------- ---- ---- ----
185932 02-21-17 14:04 com.android.support-support-fragment-25.1.1_876a7f4c146abb7a4bfe709db4ae95f698cf7afc-classes.dex
127996 02-21-17 14:04 slice_8-classes.dex
255140 02-21-17 14:04 com.android.support-support-core-ui-25.1.1_4646db78ab051d00387c34b7487ff8cbda35e1d9-classes.dex
296 02-21-17 14:04 slice_9-classes.dex
296 02-21-17 14:04 slice_6-classes.dex
756 02-21-17 14:04 com.android.support-support-v4-25.1.1_1edf8a56efa25471ab24785fa5faf2d1bd2cc943-classes.dex
296 02-21-17 14:04 slice_7-classes.dex
604460 02-21-17 14:04 com.android.support-appcompat-v7-25.1.1_334fa07c2f0aef4b9ed22c27d83f86a39b94f7d9-classes.dex
49196 02-21-17 14:04 com.android.support-support-vector-drawable-25.1.1_03c7df1b6f19e0228ccd7bbc6283520b10ea32a1-classes.dex
228344 02-21-17 14:04 com.android.support-support-media-compat-25.1.1_60e3b20b322593837da8044d821bb0d26522d69d-classes.dex
621644 02-21-17 14:04 com.android.support-support-compat-25.1.1_0e52ea72ab593acbfcd0d0b4a9e8d5e0cb4b61ba-classes.dex
8984 02-21-17 14:04 support-annotations-25.1.1_db3de1499fed4ae6b95f8f78d56192e715c93897-classes.dex
15396 02-21-17 14:04 com.android.support-animated-vector-drawable-25.1.1_af2fb95bbc530e1b2e91c0e6d3c8058c970a69cf-classes.dex
85924 02-21-17 14:04 slice_4-classes.dex
296 02-21-17 14:04 slice_5-classes.dex
296 02-21-17 14:04 slice_2-classes.dex
296 02-21-17 14:04 slice_3-classes.dex
296 02-21-17 14:04 slice_0-classes.dex
102736 02-21-17 14:04 com.android.support-support-core-utils-25.1.1_924320d14eb503e843c1937440bbcf9f4cac7cea-classes.dex
296 02-21-17 14:04 slice_1-classes.dex
-------- -------
2288876 20 files
instant-run.zip中有20个DEX文件。可以猜测:ApkTool内部使用baksmali将APK中的DEX文件反编译为smali文件,但ApkTool在反编译APK时没有处理instant-run.zip中的DEX文件,因此,在反汇编输出信息中没有程序真正的反汇编代码。
instant-run.zip是Android Studio在开启Instant Run后生成的文件。Instant Run技术是在Android Studio 2.2中引入且默认开启的,其目的是让程序员在开发Android程序时能够快速进行编译,拥有顺畅的调试体验。Instant Run技术的本质是将程序的代码生成为一个个片段,以DEX文件的形式打包到instant-run.zip中。在APK启动时,会有固定的DEX加载启动程序来加载该文件,这缩短了代码编译过程所花费的打包时间。如果不想使用这个默认开启的功能,可以选择“Settings”→“Preferences”选项,在打开的对话框中定位“Build, Execution, Deployment”下的“Instant Run”选项,取消勾选“Enable Instant Run...”选项。
即使开启了Instant Run,instant-run.zip也只会在Debug版本的APK文件中出现(Release版本会禁用Instant Run技术,因此我们不会看到它)。
调整分析思路,接下来我们需要生成Release版本的APK文件。单击Android Studio菜单项“Build”→“Generate Signed APK”,在弹出的对话框中选择app模块,然后单击“Next”按钮,在选择Key Store的界面上单击“Create New...”按钮,新建一个用于程序签名的Key Store。将Key Store的密码设置为“androidbook”。设置Alias为“Android”,密码为“androidbook”。设置好其他选项,单击“OK”按钮,就会生成androidbook.jks文件。以上设置如图2-5所示。
图2-5 生成Release版本的APK文件
回到Android Studio主界面,打开项目的app模块下的build.gradle文件,会发现多出了signingConfigs这项配置。修正storeFile的路径为相对路径后,其内容如下。
signingConfigs {
config {
keyAlias 'Android'
keyPassword 'androidbook'
storeFile file('../../../ks/androidbook.jks')
storePassword 'androidbook'
}
}
在为Key Store添加配置信息后,还需要对具体的编译版本进行设置。可以在Android Studio中通过菜单项“File”→“Project Structure...”进行设置,更简单的方法是在buildTypes的release项下添加如下内容。
signingConfig signingConfigs.config
配置完成后,在终端提示符下执行 ./gradlew assembleRelease命令,或者单击Android Studio的菜单项“Build”→“Build APK”,都可以生成Release版本的APK文件。编译后,会得到app-release.apk文件。
使用ApkTool对app-release.apk进行反编译,操作方法与对app-debug.apk的一样,只不过要将输出目录设置为outdir_rel。
下面使用grep命令来查找错误提示信息。grep是macOS和Ubuntu的自带命令,在Windows中该命令可以通过Cygwin来安装。使用grep -r命令,可以在指定的目录中搜索包含特定字符串的文件。按照上面的分析思路,整个搜索流程及结果如下。
$ grep -r "无效用户名或注册码" outdir_rel/
outdir_rel//res/values/strings.xml: <string name="unsuccessed">无效用户名或注册码</string>
$ grep -r unsuccessed outdir_rel/
outdir_rel//res/values/public.xml: <public type="string" name="unsuccessed" id="0x7f06002c" />
outdir_rel//res/values/strings.xml: <string name="unsuccessed">无效用户名或注册码</string>
outdir_rel//smali/com/droider/crackme0201/R$string.smali:.field public static final unsuccessed:I = 0x7f06002c
$ grep -r 0x7f06002c outdir_rel/
outdir_rel//res/values/public.xml: <public type="string" name="unsuccessed" id="0x7f06002c" />
outdir_rel//smali/com/droider/crackme0201/MainActivity$1.smali: const v1, 0x7f06002c
outdir_rel//smali/com/droider/crackme0201/R$string.smali:.field public static final unsuccessed:I = 0x7f06002c
除了public.xml与R$string.smali资源文件,发现只有MainActivity$1.smali文件一处进行了调用,代码如下(smali代码中的注释以“#”开头)。
$ cat outdir_rel/smali/com/droider/crackme0201/MainActivity\$1.smali
.class Lcom/droider/crackme0201/MainActivity$1;
...
# virtual methods
.method public onClick(Landroid/view/View;)V
.locals 4
.param p1, "v" # Landroid/view/View;
.prologue
const/4 v3, 0x0
.line 32
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
iget-object v1, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
# getter for: Lcom/droider/crackme0201/MainActivity;->edit_userName:Landroid/widget/EditText;
invoke-static {v1}, Lcom/droider/crackme0201/MainActivity;->access$000(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/EditText;
move-result-object v1
invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v1
invoke-virtual {v1}, Ljava/lang/Object;->toString()Ljava/lang/String;
move-result-object v1
invoke-virtual {v1}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v1
iget-object v2, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
.line 33
# getter for: Lcom/droider/crackme0201/MainActivity;->edit_sn:Landroid/widget/EditText;
invoke-static {v2}, Lcom/droider/crackme0201/MainActivity;->access$100(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/EditText;
move-result-object v2
invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/Object;->toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v2
.line 32
# invokes: Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z
invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;->access$200(Lcom/droider/crackme0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
move-result v0
if-nez v0, :cond_0
.line 34
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f06002c
invoke-static {v0, v1, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
.line 35
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 42
:goto_0
return-void
.line 37
:cond_0
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f060029
invoke-static {v0, v1, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
.line 38
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 39
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
# getter for: Lcom/droider/crackme0201/MainActivity;->btn_register:Landroid/widget/Button;
invoke-static {v0}, Lcom/droider/crackme0201/MainActivity;->access$300(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/Button;
move-result-object v0
invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V
.line 40
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f060027
invoke-virtual {v0, v1}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
goto :goto_0
.end method
.line 32处调用了checkSN() 方法进行注册码合法性检查,接着有如下两行代码。
move-result v0
if-nez v0, :cond_0
checkSN() 方法返回Boolean类型的值。第1行代码将返回的结果保存到v0寄存器中。第2行代码对v0寄存器进行判断,如果其值不为0(即条件为真)就跳转到cond_0标号处,反之程序继续执行。
如果代码不跳转,会执行如下代码。
.line 34
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f06002c
invoke-static {v0, v1, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
.line 35
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 42
:goto_0
return-void
.line 34处使用iget-object指令获取MainActivity实例的引用。其中,->this$0是内部类MainActivity$1中的一个synthetic字段,存储的是父类MainActivity的引用。这是Java语言的一个特性,类似的特性还有 ->access$0(对这类代码,会在本书后面的章节中详细介绍)。const v1, 0x7f06002c指令将v1寄存器传入unsuccessed字符串的id值。接下来,调用Toast;->makeText()方法创建字符串,在 .line 35处将其显示出来。
如果代码跳转,则执行如下代码。
.line 37
:cond_0
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f060029
invoke-static {v0, v1, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
.line 38
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 39
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
# getter for: Lcom/droider/crackme0201/MainActivity;->btn_register:Landroid/widget/Button;
invoke-static {v0}, Lcom/droider/crackme0201/MainActivity;->access$300(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/Button;
move-result-object v0
invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V
.line 40
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
const v1, 0x7f060027
invoke-virtual {v0, v1}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
goto :goto_0
以上代码的功能是弹出注册成功的提示,也就是说,这里的跳转成功意味着程序注册成功。
通过2.2.3节的分析可以发现,.line 32处的代码if-nez v0, :cond_0是程序破解的关键点。if-nez是Dalvik指令集中的一个条件跳转指令,与之类似的指令有if-eqz、if-gez、if-lez等(这些指令会在本书的第3章中介绍)。在这里,读者只需要知道:与if-nez指令功能相反的指令为if-eqz,表示在比较结果为0或相等时跳转。
用任意一款文本编辑器打开MainActivity$1.smali文件,将 .line 32处的if-nez v0,:cond_0改为if-eqz v0, :cond_0,保存后退出,代码就修改完成了。
修改smali文件的代码后,需要将该文件重新编译,打包成APK文件。回编译命令是apktool b。在终端执行如下命令,即可将smali和资源编译成APK文件。
$ apktool b outdir_rel
I: Using Apktool 2.2.2
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
W: /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res/values-v23/styles.xml:8: error: Error retrieving parent for item: No resource found that matches the given name '@android:style/AlertDialog'.
W:
W: /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res/values-v23/styles.xml:9: error: Error retrieving parent for item: No resource found that matches the given name '@android:style/DialogWindowTitle'.
W:
W: /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res/values-v24/styles.xml:7: error: Error retrieving parent for item: No resource found that matches the given name '@android:style/Animation.InputMethodFancy'.
W:
W: /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res/values-v24/styles.xml:8: error: Error retrieving parent for item: No resource found that matches the given name '@android:style/Animation.VoiceInteractionSession'.
W:
Exception in thread "main" brut.androlib.AndrolibException: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/brut_util_Jar_6345550585086565161.tmp, p, --forced-package-id, 127, --min-sdk-version, 14, --target-sdk-version, 25, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/APKTOOL8773426023413751152.tmp, -0, arsc, -0, arsc, -I, /Users/android/Library/apktool/framework/1.apk, -S, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res, -M, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/AndroidManifest.xml]
at brut.androlib.Androlib.buildResourcesFull(Androlib.java:477)
at brut.androlib.Androlib.buildResources(Androlib.java:411)
at brut.androlib.Androlib.build(Androlib.java:310)
at brut.androlib.Androlib.build(Androlib.java:263)
at brut.apktool.Main.cmdBuild(Main.java:227)
at brut.apktool.Main.main(Main.java:84)
Caused by: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/brut_util_Jar_6345550585086565161.tmp, p, --forced-package-id, 127, --min-sdk-version, 14, --target-sdk-version, 25, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/APKTOOL8773426023413751152.tmp, -0, arsc, -0, arsc, -I, /Users/android/Library/apktool/framework/1.apk, -S, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res, -M, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/AndroidManifest.xml]
at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:440)
at brut.androlib.Androlib.buildResourcesFull(Androlib.java:463)
... 5 more
Caused by: brut.common.BrutException: could not exec (exit code = 1): [/var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/brut_util_Jar_6345550585086565161.tmp, p, --forced-package-id, 127, --min-sdk-version, 14, --target-sdk-version, 25, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /var/folders/rd/mts0362j0n92rq0z1cnmdb580000gn/T/APKTOOL8773426023413751152.tmp, -0, arsc, -0, arsc, -I, /Users/android/Library/apktool/framework/1.apk, -S, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res, -M, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/AndroidManifest.xml]
at brut.util.OS.exec(OS.java:95)
at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:434)
... 6 more
从以上代码中可以看出——回编译过程出错了!根据提示信息“at brut.androlib.Androlib.buildResourcesFull(Androlib.java:477)”判断,错误是在打包资源时发生的。目前使用的是ApkTool 2.2.2,而framework-res.apk的版本是基于Android 6.0的,其API为23,但Crackme0201的API为25,因此,出现了资源无法解析的问题。解决方法是:找一台API为25的Android 7.1设备,从中获取framework-res.apk,并将该APK文件安装到本地。
因为此处已经打开了Nexus 6 API 25的模拟器,所以只需要执行如下命令。
$ adb pull /system/framework/framework-res.apk
[100%] /system/framework/framework-res.apk
然后,执行如下命令,安装framework-res.apk。
$ apktool if ./framework-res.apk
I: Framework installed to:
/Users/android/Library/apktool/framework/1.apk
安装后,再次进行回编译,仍然出错,提示layout-v22/abcalertdialogbuttonbar_material.xml文件中有一个错误。这可能是资源处理上的一个Bug,最简单也最直接的处理方法是将layout-v22目录删除。再次进行回编译,提示程序执行成功,输出的内容如下。
$ apktool b outdir_rel/
I: Using Apktool 2.2.2
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...
回编译完成后,会在dist目录下生成app-release-unsigned.apk文件。
不过,通过编译生成的APK文件是没有签名的,因此不能进行安装和测试。接下来,需要使用signapk对APK文件进行签名。signapk是一个包装脚本,其内容如下。
$ cat /Users/android/Program/signapk
#!/bin/sh
java -jar ~/Program/signapk_jar/signapk.jar
~/Program/signapk_jar/testkey.x509.pem
~/Program/signapk_jar/testkey.pk8 $1 signed.apk
signapk.jar、testkey.x509.pem、testkey.pk8文件可以从Android系统源码中提取。
执行如下命令,完成APK的签名操作。
signapk outdir_rel/dist/app-release-unsigned.apk
签名操作完成后,会在当前目录下生成signed.apk文件。
启动一个Android模拟器,或者使用数据线将Android设备和计算机连接起来,在终端执行adb uninstall命令卸载原来安装的程序,然后执行adb install命令安装破解后的程序,输出的信息如下。
$ adb uninstall com.droider.crackme0201
Success
$ adb install signed.apk
Success
启动Crackme0201,在“用户名”和“注册码”输入框中输入任意字符,单击“注册”按钮,程序会弹出注册成功的提示信息,且标题栏的字符会变成“程序已注册”,如图2-6所示。
图2-6 注册成功
破解Android程序的操作流程为:反编译→分析→修改→回编译→签名。本节讲解的操作都是在命令行下完成的,如果读者觉得命令行操作比较烦琐,可以使用一些集成了这些操作步骤的工具,举例如下。
----------------------------------------------------------------
本文节选自《Android软件安全权威指南》一书,丰生强 著,电子工业出版社出版。
丰生强,网名"非虫”,独立软件安全研究员,资深安全专家,ISC2016安全训练营独立讲师,有丰富的软件安全实战经验。自2008年起,在知名安全杂志《黑客防线》上发表多篇技术文章,从此踏上软件安全研究道路,常年混迹于国内各大软件安全论坛,著有畅销安全图书《Android软件安全与逆向分析》与《macOS软件安全与逆向分析》。
《Android软件安全权威指南》从平台搭建和语言基础开始,循序渐进地讲解了Android平台上的软件安全技术,提供了对Windows、Linux、macOS三个平台的支持,涉及与Android软件安全相关的环境搭建、文件格式、静态分析、动态调试、Hook与注入、软件保护技术、软件壳等主题,涵盖OAT、ELF等新的文件格式。
将Java与Native层的软件安全技术分开讲解,加入了与软件壳相关的章节,内容安排细致、合理。
每一章都以实例讲解的方式来展开内容,实践性较强。