《Android软件安全与逆向分析》之如何分析Android程序
进军安全界,找到了关于一本关于Android安全方面的书——丰生强老师的《Android软件安全与逆向分析》,是看雪的版主哦
这段时间就好好研读吧~在这里写一写过程和体会~(其实基本上都是书上的内容啦)
这一篇就是简单的入门,把注册时的跳转条件给改一下
第一步,自己写一个小的验证注册码的Android工程
(然后自己破解,是不是很有自己拱了自己种的白菜的赶脚~)
Ok,大概界面就是下面这样子
关键的代码是验证注册码的正确性这一部分,这个方法的主要功能是计算用户名与注册码是否匹配。
计算的步骤为:使用MD5算法计算用户名字符串的Hash,将计算所得的结果转换成长度为32位的十六进制字符串,然后取字符串的所有奇数位重新组合生成新的字符串,这个字符串就是最终的注册码,最后将它与传入的注册码进行比较,如果相同表示注册码是正确的,反之注册码是错误的。
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算法进行Hash
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.reset();
digest.update(username.getBytes());
byte[] bytes = digest.digest();
String str = bytesToHexString(bytes);
System.out.println(str);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < str.length(); i += 2){
sb.append(str.charAt(i));
}
String userSN = sb.toString();
if(!userSN.equals(sn))
return false;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
return true;
}
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
下面的是onCreate()方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle(R.string.unregister);
final EditText et_username = (EditText) findViewById(R.id.edit_username);
final EditText et_sn = (EditText) findViewById(R.id.edit_sn);
final Button bt_register = (Button) findViewById(R.id.button_register);
bt_register.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(!checkSN(et_username.getText().toString().trim(),
et_sn.getText().toString().trim())){
Toast.makeText(MainActivity.this, R.string.unregister, Toast.LENGTH_SHORT).show();
setTitle(R.string.unregister);
}else{
Toast.makeText(MainActivity.this, R.string.success, Toast.LENGTH_SHORT).show();
setTitle(R.string.success);
bt_register.setEnabled(false);
}
}
});
}
完成后再模拟器上运行的结果如下
第二步,现在假装这颗白菜是别人家的,我们要开始拱了.....
使用apktool反编译刚才生成的apk文件,命令为apktool d Crackme02.apk outdir
反编译的结果保存在outdir文件夹中,其中smali目录下存放了程序所有的反汇编代码,res文件夹下存放的是资源文件
注册码不正确时的错误提示“未注册”是Android程序中的字符串资源,
开发Android程序时,这些字符串可能硬编码到源码中,也可能引用自“res\values”目录下的strings.xml文件,
apk文件在打包时,strings.xml中的字符串被加密存储为resources.arsc 文件保存到apk程序包中,apk被成功反编译后这个文件也被解密出来了。
现在找到res目录下values文件夹中的strings.xml文件,用记事本打开如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Crackme0201</string>
<string name="username">用户名:</string>
<string name="zhucema">注册码:</string>
<string name="action_settings">setting</string>
<string name="title">Android程序破解演示实例</string>
<string name="register">注 册</string>
<string name="hint_username">请输入用户名</string>
<string name="hint_sn">请输入16位注册码</string>
<string name="success">注册成功</string>
<span style="color:#ff0000;"><string name="unregister">未注册</string></span>
</resources>
String.xml文件中的所有字符串资源都在“gen// R.java”文件的String类中被标识,每个字符串都有唯一的int类型索引值,
使用Apktool反编译apk文件后,所有的索引值保存在string.xml文件同目录下的public.xml文件中。
可以看到未注册的字符名称是“unregister”
打开public.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public type="drawable" name="ic_launcher" id="0x7f020000" />
<public type="layout" name="activity_main" id="0x7f030000" />
<public type="dimen" name="activity_horizontal_margin" id="0x7f040000" />
<public type="dimen" name="activity_vertical_margin" id="0x7f040001" />
<public type="string" name="app_name" id="0x7f050000" />
<public type="string" name="username" id="0x7f050001" />
<public type="string" name="zhucema" id="0x7f050002" />
<public type="string" name="action_settings" id="0x7f050003" />
<public type="string" name="title" id="0x7f050004" />
<public type="string" name="register" id="0x7f050005" />
<public type="string" name="hint_username" id="0x7f050006" />
<public type="string" name="hint_sn" id="0x7f050007" />
<public type="string" name="success" id="0x7f050008" />
<span style="color:#ff0000;"><public type="string" name="unregister" id="0x7f050009" /></span>
<public type="style" name="AppBaseTheme" id="0x7f060000" />
<public type="style" name="AppTheme" id="0x7f060001" />
<public type="menu" name="main" id="0x7f070000" />
<public type="id" name="edit_username" id="0x7f080000" />
<public type="id" name="edit_sn" id="0x7f080001" />
<public type="id" name="button_register" id="0x7f080002" />
<public type="id" name="action_settings" id="0x7f080003" />
</resources>
unregister的ID值为0x7f050009
在smali文件夹中搜索内容中含有0x7f050009的文件(在windows下我用的是fileseek工具),发现只有MainActivity$1.smali文件有一处调用
代码如下
.class Lcom/droider/crackme0201/MainActivity$1;
.super Ljava/lang/Object;
.source "MainActivity.java"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/droider/crackme0201/MainActivity;->onCreate(Landroid/os/Bundle;)V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x0
name = null
.end annotation
# instance fields
.field final synthetic this$0:Lcom/droider/crackme0201/MainActivity;
.field private final synthetic val$bt_register:Landroid/widget/Button;
.field private final synthetic val$et_sn:Landroid/widget/EditText;
.field private final synthetic val$et_username:Landroid/widget/EditText;
# direct methods
.method constructor <init>(Lcom/droider/crackme0201/MainActivity;Landroid/widget/EditText;Landroid/widget/EditText;Landroid/widget/Button;)V
.locals 0
.parameter
.parameter
.parameter
.parameter
.prologue
.line 1
iput-object p1, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
iput-object p2, p0, Lcom/droider/crackme0201/MainActivity$1;->val$et_username:Landroid/widget/EditText;
iput-object p3, p0, Lcom/droider/crackme0201/MainActivity$1;->val$et_sn:Landroid/widget/EditText;
iput-object p4, p0, Lcom/droider/crackme0201/MainActivity$1;->val$bt_register:Landroid/widget/Button;
.line 26
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public onClick(Landroid/view/View;)V
.locals 6
.parameter "v"
.prologue
const v5, 0x7f050009
const v4, 0x7f050008
const/4 v3, 0x0
.line 30
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
iget-object v1, p0, Lcom/droider/crackme0201/MainActivity$1;->val$et_username:Landroid/widget/EditText;
invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v1
invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v1
invoke-virtual {v1}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v1
.line 31
iget-object v2, p0, Lcom/droider/crackme0201/MainActivity$1;->val$et_sn:Landroid/widget/EditText;
invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v2
invoke-interface {v2}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v2
.line 30
#calls: Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z
invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;->access$0(Lcom/droider/crackme0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
move-result v0
.line 31
if-nez v0, :cond_0
.line 32
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-static {v0, v5, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 33
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-virtual {v0, v5}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
.line 40
:goto_0
return-void
.line 35
:cond_0
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-static {v0, v4, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 36
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-virtual {v0, v4}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
.line 37
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->val$bt_register:Landroid/widget/Button;
invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V
goto :goto_0
.end method
Smali代码中添加的注释使用井号“#”开头,“.line 31”行调用了checkSN()函数进行注册码的合法检查,代码:
.line 30
#calls: Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z
invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;->access$0(Lcom/droider/crackme0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
move-result v0
.line 31
if-nez v0, :cond_0
在“.line 30”下面的注释可以看到这一段调用了checkSN方法,接着“.line 31”前后的两行代码的作用就是检查checkSN()函数返回Boolean类型的值
第一行代码将返回的结果保存到v0寄存器中,
第二行代码对v0进行判断,如果v0的值不为零,即条件为真的情况下,跳转到cond_0标号处,反之,程序顺利向下执行。
如果代码不跳转,即注册码不正确的时候,会执行如下几行代码:
.line 32
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-static {v0, v5, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 33
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-virtual {v0, v5}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
.line 40
:goto_0
return-void
“.line 32”行使用iget-object指令获取MainActivity实例的引用。代码中的->this$0是内部类MainActivity$1中的一个synthetic字段,存储的是父类MainActivity的引用,这是Java语言的一个特性
“.line 35”行向v0寄存器传入unsuccessed字符串(v5)的id值,接着调用Toast;->makeText()创建字符串,然后调用Toast;->show()V方法弹出提示,最后.line 40行调用return-void函数返回。
如果代码跳转,即注册码正确时,会执行如下代码:
:cond_0
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-static {v0, v4, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
.line 36
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->this$0:Lcom/droider/crackme0201/MainActivity;
invoke-virtual {v0, v4}, Lcom/droider/crackme0201/MainActivity;->setTitle(I)V
.line 37
iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;->val$bt_register:Landroid/widget/Button;
invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V
这段代码的功能是弹出注册成功提示,也就是说,上面的跳转如果成功意味着程序会成功注册。
经过上一小节的分析可以发现,“.line 31”行的代码“if-nez v0, :cond_0”是程序的破解点。
if-nez是Dalvik指令集中的一个条件跳转指令,类似的还有if-eqz、if-gez、if-lez等。
这里只需要知道,与if-nez指令功能相反的指令为if-eqz,表示比较结果为0或相等时进行跳转。
用任意一款文本编辑器打开MainActivity$1.smali文件,将“.line 31”行的代码“if-nez v0, :cond_0”修改为“if-eqz v0, :cond_0”,保存后退出,代码就算修改完成了。
第三步,好不容易费劲拱完了白菜,我们还要把白菜恢复原样....
现在需要将修改后的文件重新进行编译打包成apk文件。
编译apk文件的命令格式为“apktool b[uild] [OPTS] [<app_path>] [<out_file>]”,
打开CMD命令提示符窗口,进入到outdir同目录,执行以下命令。
编译成功后会在outdir目录下生成dist目录,里面存放着编译成功的apk文件。
编译生成的crackme02.apk没有签名,还不能安装测试,接下来需要使用signapk.jar工具对apk文件进行签名。
signapk.jar是Android源码包中的一个签名工具。
代码位于Android源码目录下的/build/tools/signapk/SignApk.java文件中,源码编译后可以在/out/host/linux- x86/framework目录中找到它。
使用signapk.jar签名时需要提供签名文件,我们在此可以使用Android源码中提供的签名文件testkey.pk8与testkey.x509.pem,
它们位于Android源码的build/target/product/security目录。
新建signapk.bat文件,内容为:
<span style="font-size:14px;">java -jar "%~dp0signapk.jar" "%~dp0testkey.x509.pem" "%~dp0testkey.pk8" %1 signed.apk</span>
将signapk.jar、signapk.bat、testkey.x509.pem、testkey.pk8等4个文件放到同一目录并添加到系统PATH环境变量中,
然后在命令提示符下输入如下命令对APK文件进行签名。
signapk crackme02.apk
签名成功后会在同目录下生成signed.apk文件。
如果不行的话,将Crackme02.apk和上面的四个文件放在同一个文件夹中,在控制台进入该层目录后再执行签名命令即可。
第四步,咱们要把白菜再种回地里,看看长出来是不是咱们想要的样子....
把签名好的apk安装在虚拟机或者真机上
任意输入注册码即可验证
总结:
这个实例只是介绍了Android程序的一般分析与破解流程。
在实际的分析过程中,接触到的代码远比这些要复杂得多,有些代码甚至经过混淆处理过,很难阅读,这样就需要使用其它手段如动态调试结合一些其它的技巧来辅助分析。
路漫漫其修远兮啊,Fighting,Kiya!