编写并分析第一个Android应用程序

1 背景

要想掌握Android逆向分析技术,就要从开发学起。这个学习路线是线性的、循序渐进的。分析Android程序是开发Android程序的逆过程。要想分析一个Android程序,首先应该了解其开发流程、程序结构、语句分支、解密原理等。
今天,我开始跟着《Android软件开发权威指南》这本书的第二章开始正式编写并分析Android应用程序。

2 编写Android程序

2.1 MainActivity.java

这个方法主要用于计算用户名与注册码是否匹配。首先,使用MD5算法计算用户名字符串的散列值,将计算结果转换成长度为32位的十六进制字符串。然后,将字符串中的所有奇数位重新组合,生成新的字符串,这个字符串就是最终的注册码。最后,将这个字符串与传入的注册码进行比较。如果二者相同,表示注册码是正确的;如果二者不同,表示注册码是错误的。

在MainActivity的OnCreate()方法中添加注册按钮点击事件的监听器。如果用户名与注册码匹配,就弹出注册成功的提示;如果用户名与注册码不匹配,则弹出用户名或注册码无效的提示,具体代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.iReverse.crackme0201;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import android.view.View.OnClickListener;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MainActivity extends AppCompatActivity {

private EditText edit_userName;
private EditText edit_sn;
private Button btn_register;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

setTitle(R.string.unregister);
edit_userName = findViewById(R.id.edit_username); // 获取EditText的输入获取用户名的id
edit_sn = findViewById(R.id.edit_sn); // 获取EditText的输入获取用注册码的id
btn_register = findViewById(R.id.button_register); // 获取注册按钮的id

// 设置注册按钮的时间监听器
btn_register.setOnClickListener(new OnClickListener() {

// 重写onClick方法
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.registered);
}
}
});
}

/* 计算用户名与注册码是否匹配 */
private boolean checkSN(String userName, String sn) {
try {
if ((userName == null) || (userName.length() == 0))
return false;
if ((sn == null) || (sn.length() != 16))
return false;
// 返回实现 MD5消息摘要算法 的 MessageDigest 对象
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.reset(); // 重置摘要以供再次使用
// 将字符串编码为 byte 序列后,再使用该字节数组更新摘要
digest.update(userName.getBytes());
// 使用指定的字节数组对摘要执行最终更新,然后完成摘要计算
byte[] bytes = digest.digest();
String hexstr = toHexString(bytes, "");
// 创建字符串的 StringBuilder 类
StringBuilder sb = new StringBuilder();

/* 将字符串的奇数位重新组合生成注册码sn
charAt() 方法用于返回指定索引处的字符 */
for (int i = 0; i < hexstr.length(); i += 2) {
sb.append(hexstr.charAt(i));
}
String userSN = sb.toString();

Log.d("用户名:", hexstr);
Log.d("序列号:", userSN);

if (!userSN.equalsIgnoreCase(sn))
return false;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
return true;
}

private static String toHexString(byte[] bytes, String separator) {
// 创建字符串的 StringBuilder 类
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b); // 与运算
if(hex.length() == 1){
hexString.append('0');
}
hexString.append(hex).append(separator);
}
return hexString.toString();
}
}

2.2 activity_main.xml

工程布局文件的主要信息如下所示,主要是用户名和注册码的文本输入框和注册按钮,在我的真机上的运行效果如图1所示。

图1 真机运行效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/info"
android:textSize="20dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
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:hint="@string/hint_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:ems="10" >
</EditText>
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sn" />
<EditText
android:id="@+id/edit_sn"
android:hint="@string/hint_sn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:ems="10" >
</EditText>
</LinearLayout>

<Button
android:id="@+id/button_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_gravity="right"
android:text="@string/register" />

</LinearLayout>

2.3 strings.xml

本文件主要存储module中使用的字符串资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<resources>
<string name="app_name">Crackme0201</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">Crackme0201</string>
<string name="info">Android程序破解演示实例</string>
<string name="username">用户名:</string>
<string name="sn">注册码:</string>
<string name="register">注 册</string>
<string name="hint_username">请输入用户名</string>
<string name="hint_sn">请输入16位的注册码</string>
<string name="unregister">程序未注册</string>
<string name="registered">程序已注册</string>
<string name="unsuccessed">无效用户名或注册码</string>
<string name="successed">恭喜您!注册成功</string>
</resources>

2.4 打包apk文件

打包过程中需要指定相应的密钥库和输入密钥库的密码,如图2所示。

图2 编译生成APK文件

3 破解第一个Android程序

3.1 使用ApkTool反编译APK文件

ApkTool是一款常用的跨平台APK文件反编译工具,使用命令apktool d app-release.apk -o outdir可以在当前的outdir目录下生成反编译文件。反编译文件包含一系列目录和文件,smali目录中存放了程序的所有反汇编代码,res目录中存放的则是程序中所有的资源文件,这些目录的子目录和文件的组织结构与开发时源码目录的组织结构是一致的,反编译APK文件的过程如图3所示。

图3 反编译APK文件

3.2 分析APK文件

如何寻找突破口是分析一个程序的关键。对大部分Android程序来说,错误提示信息是指路明灯。错误提示代码附近通常就是程序的核心验证代码,我们可以通过阅读这些代码理解软件的注册流程。

错误提示属于Android程序中的字符串资源。在开发Android程序时,这些字符串可能会被硬编码到源码中,也可能引用自res/values/strings.xml文件。

首先,使用grep命令可以找到错误提示代码所在的字符串资源文件strings.xml中的string类标识,在public.xml找到字符串对应的唯一的int类型的索引值,在outdir/smali/com/iReverse/crackme0201/MainActivity$1.smali文件中找到错误提示信息所在的代码,如图4所示。

图4 grep命令查找错误提示代码文件

其次,用记事本打开outdir/smali/com/iReverse/crackme0201/MainActivity$1.smali文件,经过对代码分析可知if-nez p1, :cond_0是决定程序接下来往哪里执行,cond_0是注册成功的标识部分的代码,而goto_0是失败部分的代码,如图5所示。

图5 smali代码分析逻辑

接着,这里我们需要修改if-nezif-eqz,然后保存后退出,代码就修改完成了。

修改smali文件的代码后,需要将该文件重新编译,打包成APK文件。回编译命令是apktool b outdir。在终端执行图6中的命令,即可将smali和资源编译成APK文件。

图6 编译未签名的APK文件

然后,但可能会出现反编译失败的情况,这时可以重新使用命令apktool -r d app-release.apk -o outdir进行编译APK文件,然后进行修改,最后再进行反编译。

编译成功之后,此时我们的APK文件是没有签名的,这时我们可以利用singapk对APK文件进行签名,iReverse.x509.pem和iReverse.pk8文件可以利用openssl进行生成,APK的签名过程如图7所示。

图7 利用signapk签名APK文件

最后,在真机上安装APK文件,在“用户名”和“注册码”输入框中输入任意字符,单击“注册”按钮,程序会弹出注册成功的提示信息,而且标题栏的字符会编程“程序已注册”,如图8 所示。

图8 注册成功

3 总结

破解Android程序的操作流程为:反编译->分析->修改->回编译->签名,但此次过程非常简单,属于入门知识。

在以后的实际分析过程中,我们接触的代码远比这些代码复制,有些代码甚至经过了混淆,很难阅读。对这样的代码,需要使用一些辅助分析手段(例如,将动态调试与其他技巧结合使用)进行分析。

【注】可能有的人感觉命令行比较繁琐,可以使用一些集成了这些操作步骤的工具,比如Android Killer

4 参考文献

[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏