<<Android软件安全权威指南>>笔记
第二章 如何分析Android程序
编写一个Android程序
新建一个Android工程,名称为Crackme0201
吧
主界面添加用户名和注册码输入框
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity"
android:id="@+id/linearLayout">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/top_string"
android:textAlignment="center"
android:gravity="center_horizontal" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/username"/>
<EditText
android:id="@+id/edit_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:hint="@string/hint_username"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/regCode"/>
<EditText
android:id="@+id/edit_regCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/hint_regCode"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1" />
</LinearLayout>
<Button
android:id="@+id/button_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_register"
android:layout_marginRight="10dp"
android:layout_gravity="right"/>
</LinearLayout>
然后,编写MainActivity
类的代码,添加一个checkSN()
方法
private boolean checkSN(String userName, String sn) {
try {
if ((userName == null) || (userName.length() == 0)) {
return false;
}
if ((sn == null) || (sn.length() != 16)) {
return false;
}
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.reset();
digest.update(userName.getBytes());
byte[] bytes = digest.digest();
String hexstr = toHexString(bytes, "");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < hexstr.length(); i+=2) {
stringBuilder.append(hexstr.charAt(i));
}
String userSN = stringBuilder.toString();
//Log.d("crackme", hexstr);
//Log.d("crackme", userSN);
if (!userSN.equalsIgnoreCase(sn)) {
return false;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
return true;
}
该方法计算用户名与注册码是否匹配,用MD5计算用户名的散列值,转换为hex串,128bit转换为32位十六进制串,再取32位中的奇数位,拼成16位,即为注册码。
最后判断传入的注册码与计算得出的注册码是否匹配。
在MainActivity
中的onCreate()
方法中添加注册按钮点击事件
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle(R.string.unregister);
edit_userName = (EditText) findViewById(R.id.edit_username);
edit_sn = (EditText) findViewById(R.id.edit_regCode);
btn_register = (Button) findViewById(R.id.button_register);
btn_register.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!checkSN(edit_userName.getText().toString().trim(),
edit_sn.getText().toString().trim())) {
Toast.makeText(MainActivity.this, R.string.unsuccessed,
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, R.string.successed,
Toast.LENGTH_SHORT).show();
btn_register.setEnabled(false);
setTitle(R.string.register);
}
}
});
}
如果匹配,则弹出注册成功,如果不匹配,则弹出注册无效的提示
编译APK文件
有两种编译方式,使用命令行和AS的Build菜单
命令行
gradlew是基于task来编译项目的,在命令行下执行gradlew tasks
会列出所有支持的task,我们主要关注编译使用的Build tasks和安装APK时用的Install tasks
生成Debug调试版本的APK:./gradlew assembleDebug
,会在Crackme0201/app/build/outputs/apk
目录下生成app-debug.apk文件
安装Debug版本APK:./gradlew installDebug
,会安装到与系统相连的设备/模拟器
adb devices
,列出当前连接的设备,包括模拟器
AS编译
直接点击run app
会自动编译安装
只编译不安装点make project
破解第一个Android程序
以Crackme0201为例
通常方法是,使用ApkTool
反编译APK文件,生成smali格式的反汇编代码,通过阅读smali代码理解程序运行机制,找到突破口进行修改,再使用ApkTool
工具重新编译为APK并签名,进行测试
上述过程循环,直到完全破解
实际中还可以用IDA Pro
直接分析APK文件,使用dex2jar
和jd-gui
配合进行Java源码级分析
反编译APK文件
下载ApkTool工具,添加路径到PATH环境变量,命令行直接使用
apktool d ./apk-debug.apk -o outdir
会在outdir
文件夹下生成反编译文件,smali
目录下是程序的所有反汇编代码,res
目录下是程序所有资源文件,子目录的文件结构与开发源码一致。
分析APK文件
找到突破口,一般是错误提示信息,属于字符串资源
APK打包时,string.xml
文件被加密存储为resources.arsc
文件,反编译会解密
string.xml
中的所有字符串资源都在gen/R.java
文件中的String类中标识,每个字符串都有int类型的索引值,对应关系在同目录下的public.xml
文件中
找到unsuccessed
字符串和对应索引值
但是搜索文件夹下没有与索引值相关的信息
接下来生成Release版本APK文件
用AS进行签名生成app-release.apk,再用apktool进行反编译
找到unsuccessed
对应的索引值,全局搜索,在MainActivity$1.smali
中找到了索引值
在.line 33
处调用checkSN()
(但是这里是access$000
,没有#备注,不知道为啥)
.line 33
invoke-static {p1, v0, v1}, Lorg/nuaa/crackme0201/MainActivity;->access$000(Lorg/nuaa/crackme0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
move-result p1
const/4 v0, 0x0
if-nez p1, :cond_0
意思是,返回Boolean
类型值(哪里看出来的?)
返回结果保存在p1
move-result p1
忽略掉第二行,对寄存器p1进行判断,其值不为0,即条件为真则跳转cond_0处
const/4 v0, 0x0
if-nez p1, :cond_0
继续往下看,不跳转会执行
.line 35
iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;
const v1, 0x7f0b0032
invoke-static {p1, v1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object p1
.line 36
invoke-virtual {p1}, Landroid/widget/Toast;->show()V
goto :goto_0
在.line 35
处事要iget-object
指令获取MainActivity
的实例的引用,-this$0
是内部类MainActivity$1
中的一个synthetic
字段,存储父类MainActivity的引用
const v1, 0x7f0b0032
该指令是将v1寄存器传入0x7f0b0032,也就是unsuccessed的id值,接下来调用Toast;->makeToast()
如果跳转cond_0,则执行如下指令
.line 38
:cond_0
iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;
const v1, 0x7f0b002f
invoke-static {p1, v1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object p1
.line 39
invoke-virtual {p1}, Landroid/widget/Toast;->show()V
.line 40
iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;
iget-object p1, p1, Lorg/nuaa/crackme0201/MainActivity;->btn_register:Landroid/widget/Button;
invoke-virtual {p1, v0}, Landroid/widget/Button;->setEnabled(Z)V
.line 41
iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;
const v0, 0x7f0b002c
invoke-virtual {p1, v0}, Lorg/nuaa/crackme0201/MainActivity;->setTitle(I)V
:goto_0
return-void
这里的代码是注册成功时候的逻辑
修改smali文件代码
通过上述分析,,line 32
处的代码,if-nez p1, :cond_0
是跳转关键点
这是Dalvik指令集的一个条件跳转指令(和汇编差不多),应该是not equal zero,不等于0的意思,相反的指令是if-eqz
,表示等于0时跳转
就用if-eqz
代替if-nez
,保存修改
if-eqz p1, :cond_0
重新编译APK并签名
修改好以后,使用apktool重新编译打包
apktool b outdir_rel
在outdir_rel/dist
目录下生成apk文件
要安装还需要签名,之前签名的时候生成了证书,我们可以用命令行来对apk进行签名
apksigner sign --ks [xxx.jks] --out [xxx.apk] [signed.apk]
apksigner
是Android的build-tool中自带的,--ks
后接数字证书存储路径,--out
后接签名后apk输出路径,最后是原apk路径
在Terminal下,模拟器开启的情况下,推出原应用,输入命令卸载
adb uninstall org.nuaa.crackme0201
再安装签名后的apk
adb install signed.apk
安装成功,测试随意的用户名+注册码都可以验证成功。
总结
一般流程
反编译 -> 分析 -> 修改 -> 回编译 -> 签名
集成工具:MAC下Android-Crack-Tool
,Windows下Android Killer