从零开始分析第一个简单的Android程序

<<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文件,使用dex2jarjd-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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章