cdxy.me
Footprints on Cyber Security and Python

#1 机制

破解 Android 程序通常的方法是将 apk 文件利用 ApkTool 反编译,生成 Smali 格式的反汇编代码,然后阅读Smali 文件的代码来理解程序的运行机制,找到程序的突破口进行修改,最后使用 ApkTool 重新编译生成 apk 文件并签名,最后运行测试,如此循环,直至程序被成功破解。

#2 准备工作

APKtool的安装使用请参考笔者上篇博客

编写软件Crackme_01用做测试

value文件夹下的String.xml内容如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Crackme0201</string>
<string name="hello_world">Hello world!</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">crackme02</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>


onCreate方法修改如下

public 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_sn);
		btn_register = (Button) findViewById(R.id.button_register);
		btn_register.setOnClickListener(new OnClickListener() {
			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); // 模拟程序已注册
				}
			}
		});
	}


MainActivity中添加类

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(); // 采用MD5对用户名进行Hash
			String hexstr = bytes2HexString(bytes); // 将计算结果转化成字符串
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < hexstr.length(); i += 2) {
				sb.append(hexstr.charAt(i));
			}
			String userSN = sb.toString(); // 计算出的SN
			if (!userSN.equalsIgnoreCase(sn)) // 比较注册码是否正确
				return false;
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}


在上面的checkSN中用到的bytes2string方法如下

public String bytes2HexString(byte[] b) {

		byte[] hex ="0123456789ABCDEF".getBytes();
		byte[] buff = new byte[2 * b.length];


		for (int i = 0; i < b.length; i++) {
		buff[2 * i] = hex[(b[i] >> 4) & 0x0f];
		buff[2 * i + 1] = hex[b[i] & 0x0f];
		}
		return new String(buff);
	}


布局文件如下

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" 
    android:id="@+id/root">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="android程序破解演示实例1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="用户名:" />

        <EditText
            android:id="@+id/edit_username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10" >

        </EditText>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="注册码:" />

        <EditText
            android:id="@+id/edit_sn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10" />

    </LinearLayout>

    <Button
        android:id="@+id/button_register"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="注册" />

</LinearLayout>

 

#3 破解过程

反编译 apk 文件成功后,会在当前的 outdir 目录下生成一系列目录与文件。其中 smali目录下存放了程序所有的反汇编代码, res 目录则是程序中所有的资源文件,这些目录的子目录和文件与开发时的源码目录组织结构是一致的。如何寻找突破口是分析一个程序的关键。对于一般的Android 来说,错误提示信息通常是指引关键代码的风向标,在错误提示附近一般是程序的核心验证代码,分析人员需要阅读
这些代码来理解软件的注册流程。错误提示是 Android 程序中的字符串资源,开发 Android 程序时,这些字符串可能硬编码到源码中,也可能引用自“res\values”目录下的strings.xml 文件, apk 文件在打包时,strings.xml 中的字符串被加密存储resources.arsc 文件保存到 apk 程序包中, apk 被成功反编译后这个文件也被解密出来了。
 

在软件注册失败时会弹出“无效用户名或注册码”,我们以此为线索来寻找关键代码。打开“res\values\string.xml”文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Crackme0201</string>
<string name="hello_world">Hello world!</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">crackme02</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>
开发 Android 程序时, String.xml 文件中的所有字符串资源都在“ gen/<packagename>/
R.java”文件的String 类中被标识,每个字符串都有唯一的 int 类型索引值,使用 Apktool 反
编译 apk 文件后,所有的索引值保存在 string.xml 文件同目录下的 public.xml 文件中。

 

unsuccessed的 id 值为 0x7f05000c,在 smali 目录中搜索含有内容为 0x7f05000c 的文件,
最后发现只有 MainActivity$1.smali 文件一处调用,代码如下:

# virtual methods
.method public onClick(Landroid/view/View;)V
.locals 4
.parameter "v"
.prologue

const/4 v3, 0x0
……
.line 32
#calls:
Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;
Ljava/lang/String;)Z
invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;-> #检查注册码是否合法
access$2(Lcom/droider/crackme0201/MainActivity;Ljava/lang/String;Ljava/
lang/String;)Z
move-result v0
if-nez v0, :cond_0#如果结果不为0,就跳转到cond_0标号处
.line 34
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->
this$0:Lcom/droider/crackme0201/MainActivity;
.line 35
const v1, 0x7f05000c# unsuccessed字符串
.line 34
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;
.line 38
const v1, 0x7f05000d# successed字符串
.line 37
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$3(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, 0x7f05000b# registered字符串,模拟注册成功
invoke-virtual {v0, v1}, Lcom/droider/crackme0201/MainActivity;->
setTitle(I)V
goto :goto_0
.end method

Smali代码中添加的注释使用井号“ #”开头,“ .line 32”行调用了 checkSN()函数进行注册码的合法检查,接着下面有如下两行代码:
move-result v0
if-nez v0, :cond_0
checkSN()函数返回Boolean 类型的值。这里的第一行代码将返回的结果保存到 v0寄存器中,第二行代码对v0 进行判断,如果 v0 的值不为零,即条件为真的情况下,跳转到 cond_0标号处,反之,程序顺利向下执行。

分析可以发现,“.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-nezv0, :cond_0”修改为“if-eqz v0, :cond_0”,保存后退出,代码就算修改完成了。修改完Smali 文件代码后,需要将修改后的文件重新进行编译打包成 apk 文件,签名后安装,随意输入,可以发现注册成功。