Android反编译学习-使用APKtool破解注册类程序
[Android]
#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 文件,签名后安装,随意输入,可以发现注册成功。