android逆向学习小结--CrackMe_1

时间:2023-03-08 22:37:02
android逆向学习小结--CrackMe_1

断断续续的总算的把android开发和逆向的这两本书看完了,虽然没有java,和android开发的基础,但总体感觉起来还是比较能接收的,毕竟都是触类旁通的。当然要深入的话还需要对这门语言的细节特性和奇技淫巧进行挖掘。

这里推荐2本书,个人觉得对android开发入门和android逆向入门比较好的教材:

《google android 开发入门与实战》

《android 软件安全与逆向分析》

1. 我对android逆向的认识

因为之前有一些windows逆向的基础,在看android逆向的时候感觉很多东西都是能共通的。但因为android程序本身的特性,还是有很多不同的地方。

1.1 反编译

android程序使用java语言编写,从java到android虚拟机(Dalvik)的dex代码(可以看成是android虚拟机的机器码)需要一个中间语言的转换过程。类似.NET的IL中间虚拟指令。而我们知道,.NET的IL中间代码之所以能很容易的"反编译"回C#源代码,是因为除了IL中间语言,还包含了大量的META元数据,这些元数据使我们可以很容易的一一对应的反编译回C#的源代码。java的中间语言.class文件也是类似的道理,我们可以使用工具直接从dex机器码反编译回java源代码。

1.2 逆向分析手段

windows的逆向分析中,我们可以使用OD或者C32ASM来分析汇编指令(当然OD还可以动态调试),或者使用IDA + F5(hex Ray反编译插件)来静态的分析源代码(C/C++)

在android逆向分析过程中:

1) 我们可以使用ApkTool(本质上是BakSmali反汇编引擎)对apk文件进行反汇编,得到各个类、方法、资源、布局文件...的smali代码,我们可以直接通过阅读smali代码来分析程序的代码流,进行关键点的修改或者代码注入。

2) 我们可以从apk中提取.dex文件,使用dex2jar工具对dex进行反汇编,得到jar包(java虚拟指令),然后使用jd-gui等工具再次反编译,得到java源代码,从源码级的高度来审计代码,更快的找到关键点函数或者判断,然后再回到smali层面,对代码进行修改。这种方法更倾向于辅助性的,最终的步骤我们都要回到smali层面来修改代码。

3) 使用IDA Pro直接分析APK包中的.dex文件,找到关键点代码的位置,记下文件偏移量,然后直接对.dex文件进行修改。修改完之后把.dex文件重新导入apk中。这个时候要注意修改dex文件头中DexHeader中的checksum字段。将这个值修复后,重新导入apk中,并删除apk中的META-INF文件夹,重新签名即可完成破解。

1.3 android与C的结合

在学习android逆向的时候感觉遇到的最难的问题就是分析原生代码,即JNI代码。开发者使用android NDK编写C/C++代码供android的java代码调用(通过java的代码转接层来完成接口的转换)。

使用android NDK编写的C/C++代码最终会生成基于ARM的ARM ELF可执行文件,我们想要分析软件的功能就必须掌握另一项技能,ARM汇编,ARM汇编个人感觉虽然和x86汇编类似,不过由于IDA Pro对ARM汇编没有反编译功能以及貌似没有工具能动态调试ARM代码(我网上没找到),导致我们只能直接硬看ARM代码,加上往往伴随着复杂的密码学算法等等,导致对Native Code的逆向相对来说比较困难,对基本功的要求比较高。

1.4 关于分析android程序

1) 了解程序的AndroidManifest.xml。在程序中使用的所有activity(交互组件)都需要在AndroidManifest.xml文件中手动声明。包括程序启动时默认启动的主activity,通过研究这个AndroidManifest.xml文件,我们可以知道该程序使用了多少的activity,主activity是谁,使用了哪些权限,使用了哪些服务,做到心中有数。

2) 重点关注Application类

这本来和1) AndroidManifest.xml是一起的,但是分出来说是因为这个思路和windows下的逆向思路有相通之处。

在windows exe的数据目录表中如果存在TLS项,那程序在加载后会首先执行这个TLS中的代码,执行完之后才进行main主程序入口。

在android 中Application类比程序中其他的类启动的都要早。

3) 定位关键代码

3.1) 信息反馈法(关键字查找法)

通过运行程序,查找程序UI中出现的提示消息或标题等关键字,到String.xmlzhong中查找指定字符串的di,然后到程序中查找指定的id即可。

3.2) 特征函数法

这种做法的原理和信息反馈法类似,因为不管你提示什么消息,就必然会调用相应的API函数来显示这个字符串,例如Toast.MakeText().show()

例如在程序中搜索Toast就有可能很快地定位到调用代码

3.3) 代码注入法

代码注入法属于动态调试的方法,我们可以手动修改smali反汇编代码,加入Log输入,配合LogCat来查看程序执行到特定点时的状态数据。

3.4) 栈跟踪法

栈跟踪法属于动态调试方法,从原理上和我们用OD调试时查看call stack的思想类似。我们可以在smali代码中注入输出运行时的栈跟踪信息,然后查看栈上的函数调用序列来理解方法的执行流程(因为每个函数的执行都会在栈上留下记录)

3.5) Method Profiling

Method Profiling,方法剖析(这是书上的叫法,我更愿意叫BenchMark测试法),它属于一种动态调试方法,它主要用于热点分析和性能优化。在DDMS中有提供这个功能,它除了可记录每个函数所占用的CPU时间外,还能够跟踪所有的函数调用关系。

1.5 关于android的代码混淆和加壳

java语言编写的代码本身就很容易被反编译,google为此在android 2.3的SDK中正式加入了ProGuard代码混淆工具,只要正确的配置好project.properties与proguard.cfg两个文件即可使用ProGuard混淆软件。

java语言由于语言自身的特殊性,没有外壳保护这个概念,只能通过混淆方式对其进行保护。对android NDK编写的Native Code倒是可以进行加壳,但目前貌似只能进行ups的压缩壳保护

2. CrackMe_1 分析学习

2.1 运行一下程序,收集一些基本信息

android逆向学习小结--CrackMe_1

只有一个输入框,那说明这个验证码的输入来自别的地方,因为我们知道,不管你的加密算法是啥,总是要有一个函数输入源的,我们在UI界面上输入的相当于是结果,而输入源应该来自于别的地方,计算完之后和我们在UI上输入的结果进行对比,大致是这个思路。

2.2 分析

使用apktool反编译apk文件。查看AndroidManifest.xml文件。了解到主activity为:Main。

接着我们从apk中提取.dex文件。用dex2jar->jd-gui来查看java源代码。

android逆向学习小结--CrackMe_1

看到里面很多的a,b,c方法,基本上可以判定是配ProGuard混淆了,不过问题也不大,虽然显示的是无意义的函数名但是不影响我们分析代码流程。

2.2.1 类b的分析

android逆向学习小结--CrackMe_1

从OnCreate()的代码来看,我们首先从类b开始分析:

类 b 提供了一个公共的构造函数 public b(Context paramContext),  一个私有的成员函数private String b(),  以及一个公有成员函数 public final void a()。

b(): 通过TelephonyManager获取设备相关的一些信息,然后通过PackageManager获取到自身的签名。然后把这些字符串拼接起来返回给调用者。

TelephonyManager localTelephonyManager = (TelephonyManager)this.a.getSystemService("phone");
    String str1 = localTelephonyManager.getDeviceId();
    String str2 = localTelephonyManager.getLine1Number();
    String str3 = localTelephonyManager.getDeviceSoftwareVersion();
    String str4 = localTelephonyManager.getSimSerialNumber();
    String str5 = localTelephonyManager.getSubscriberId();
    Object localObject = "";
    PackageManager localPackageManager = this.a.getPackageManager();
    try
    {
      String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString();
      localObject = str6;
      return str1 + str2 + str3 + str4 + str5 + (String)localObject;
    }

a():

SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.a);
    SharedPreferences.Editor localEditor;
    if (!localSharedPreferences.contains("machine_id"))
      localEditor = localSharedPreferences.edit();
    try
    {
      localEditor.putString("machine_id", b());
      localEditor.commit();
      return;
    }

a()调用方法b()获取字符串,然后通过SharedPreferences.Editor将这个字符串值存储到键machine_id,可以理解为机器码。也就是说,这个加密函数的输入是本机的机器码。

经过上面的分析,类b对外提供方法a,功能就是生成"机器码"并存储到系统中,对应的键为machine_id。

2.2.2 类c的分析

类c提供的方法较多,我们逐个分析。

1) 构造函数

public c(Context paramContext)
  {
    a = paramContext;
    b = "f0d412b5530e1f9841aab434d989cc77";
    c = "4ec407446b872351e613111339daae9";
  }

把参数环境上下文Context本地化,并声明了两个字符串。

2) public static boolean b()

MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
localMessageDigest.update(paramString.getBytes(), 0, paramString.length());
return new BigInteger(1, localMessageDigest.digest()).toString(16);

通过MessageDigest计算paramString 的MD5值。

3) public static boolean b()

PackageManager localPackageManager = a.getPackageManager();
    try
    {
      String str = b(new String(localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toChars()));
      if (!str.equals(b))
      {
        boolean bool = str.equals(c);
        if (!bool);
      }
      else
      {
        return false;
      }
    }

通过 getPackageManager 获取自身的签名,如果签名与构造函数中的两个字符串b(f0d412b5530e1f9841aab434d989cc77)或者c(4ec407446b872351e613111339daae9)任意一个相等,那么返回false,否则返回true。

4) public static int a(String paramString)

try
    {
      if (b())
        return 0;
      SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a);
      if (b(localSharedPreferences.getString("machine_id", "")).equals(paramString))
      {
        if (b())
          return 0;
        SharedPreferences.Editor localEditor = localSharedPreferences.edit();
        localEditor.putString("serial", paramString);
        localEditor.commit();
        return 1;
      }
    }

可以看出这段代码的功能为计算机器码的 MD5,如果与传入的参数paramString一致,那么通过SharedPreferences存入到serial(机器码的MD5值paramString)字段中。 当然还有调用b方法进行一些判断,自身的签名不能是已知的两个。

5) public static boolean a()

SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a);
    if (!localSharedPreferences.contains("serial"))
      return false;
    String str = localSharedPreferences.getString("serial", "");
    if (str.equals(""))
      return false;
    return a(str) >= 0;

这个其实就是上面的 int a(String paramString)的包装函数,通过SharedPreferences获取serial字段(机器码的MD5值),并传给这个方法,返回相应的返回值(判断结果)。

2.2.3 类a分析

可以看到,类a是一个CountDownTimer:

Schedule a countdown until a time in the future, with regular notifications on intervals along the way. Example of showing a 30 second countdown in a text field:(android Developer)

android逆向学习小结--CrackMe_1

从onFinish函数我们看出这个类的功能是倒计时6秒,然后调用c.a(),也就是判断我们输入的serial是否等于"机器码"的MD5值。如果不能通过,就设置TextView内容提示注册。

2.2.4 类Main分析

1) 在onCreate(),先初始化b和c的类。然后调用b.a()生成并存储"机器码",然后调用c.a(),也就是判断是否已经存储了serial,并判断是否能通过算法校验。如果不能通过,则什么都不做,这就是启动时检测注册状态的做法,即如果你之前已经注册了,那在之后的登录后就会自动识别出来,但是我们如果是第一次启动且没有注册,那这里就什么也不做。

如果能通过,则调用自身的方法a()。而自身的方法a()又调用了c.b()方法,即检查我们输入的serial和机器码的MD5值是否相同,如果相同则什么也不做,如果不同就把下面的按钮和TextView等UI控件给隐藏了。并启动倒计时类a.start()。即二次验证。

ps:

这里要注意的是,由于程序使用了ProGuard来混淆代码,所以用jd-gui翻译出来的代码全都是从a,b,c开始计数,而且经常是变量、类、方法的命名混合了起来。我们在看java代码的时候遇到难懂的地方要结合smali代码一起看,这样才能获取比较准确的对程序代码流的把握。

2) public void onClick(View paramView)

if (c.a(((EditText)findViewById(2131034114)).getText().toString()) == 0)
    {
      Toast.makeText(this, 2130968577, 0).show();
      return;
    }
    Toast.makeText(this, 2130968578, 0).show();

判断我们通过UI输入的serial是否和"机器码"的MD5值相同,如果不相同则弹出提示Invalid serial!(可以通过ID值反查出对应的字符串),如果相同则弹出Thanks for purchasing!

通过以上分析,我们来综合一下思路:

程序启动时会做一些初始化的工作,然后生成本地对应的机器码并保存在SharedPreferences中。

检查当前的SharedPreferences中是否已经保存了serial键值对,并检查正确性,即检查是否上一次已经注册了。如果没有这个键值对,说明还没注册,如果存在这个键值对且正确性也符合,代码接下来会继续检查APK自身的签名是否为代码中定义的那两个,如果相等则什么都不做(即依然不通过检查),如果不等则代码继续执行倒计时6秒的类a, 6秒后再次检查一次serial键值对。

对于那个按钮点击事件,onClick(),它获取用户通过UI输入的serial,并检测是否和"机器码"的MD5值相等,如果相等则存进SharedPreferences中的键值对中。

以上基本就是这个程序的代码思路了。我们可以看到,作者这里使用了双重保护的思路,即不仅要你输入的serial相同,而且对你的APK的签名也有限制。

3. 破解思路

3.1 单纯的破解,用代码注入的方法得到注册码。

经过分析,我们知道应该在b.smali的155行:

android逆向学习小结--CrackMe_1

move-result-object v2 这里代码注入,因为这个b()的作用就是获取当前"机器码"(注意,这里获取的是没有MD5之前的"机器码",因为程序中的MD5都是临时算出来的)

我们在这里加入:

const-string v3, "SN"

invoke-static {v3, v2}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I

重新回编译smalli代码。

在命令行中执行 adb logcat -s SN:v  ,然后再启动程序

会在命令行中看到一大串字符串,这些字符串就是我们要的机器码

android逆向学习小结--CrackMe_1

将这些字符串计算MD5值之后,就可以完成破解了。

3.2 读取程序对应的文件

我们知道,所谓的SharedPreferences本质上是保存在当前程序空间下的/data/data/<package name>/shared_prefs/<package name>_preferences.xml文件中的。

我们可以通过adb连接上去,直接读取这个文件的内容。

android逆向学习小结--CrackMe_1

可以看到,和我们通过代码注入的方式得到的机器码是相同的。

3.3 编写注册机

这种方法是最好的,编写注册机要求我们对目标程序的代码有全盘的认识,然后模拟原本的算法或者逆向原本的算法写出注册机

我们用Eclipse重新生成一个新的工程 com.lohan.crackme。注意,工程的报名必须和目标程序的包名一致,这样我们的注册机运行后得到的APK签名才会是一样的。

核心算法如下:

@Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setTitle("crackMe1_keyGen");
        final Context context = getApplicationContext();
        
        //获取UI控件
        txt_machineCode = (TextView) findViewById(R.id.machineCode);
        txt_apkSig = (TextView) findViewById(R.id.apkSig);
        txt_serial = (TextView) findViewById(R.id.serial);
        btn_Go = (Button) findViewById(R.id.ok);
        
        //设置监听事件
        btn_Go.setOnClickListener(new OnClickListener(){
            public void onClick(View v)
            {
                //计算机器码
                TelephonyManager localTelephonyManager = (TelephonyManager) context.getSystemService("phone");
                String str1 = localTelephonyManager.getDeviceId();
                String str2 = localTelephonyManager.getLine1Number();
                String str3 = localTelephonyManager.getDeviceSoftwareVersion();
                String str4 = localTelephonyManager.getSimSerialNumber();
                String str5 = localTelephonyManager.getSubscriberId();
                Object localObject = "";
                PackageManager localPackageManager = context.getPackageManager();
                try
                {
                  String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString();
                  localObject = str6;
                  String str_result =  str1 + str2 + str3 + str4 + str5 + (String)localObject;
                  //得出机器码
                  txt_machineCode.setText(str_result);
                  
                  //计算当前APK的签名
                  txt_apkSig.setText(str6);
                  
                  //计算注册码
                  MessageDigest localMessageDigest = null;
                    try {
                        localMessageDigest = MessageDigest.getInstance("MD5");
                    } catch (NoSuchAlgorithmException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                  localMessageDigest.update(str_result.getBytes(), 0, str_result.length());
                  String str_serial = new BigInteger(1, localMessageDigest.digest()).toString(16);
                  txt_serial.setText(str_serial);
                }
                catch (PackageManager.NameNotFoundException localNameNotFoundException)
                {
                  while (true)
                    localNameNotFoundException.printStackTrace();
                }
            }
 
        });

android逆向学习小结--CrackMe_1

破解结果

android逆向学习小结--CrackMe_1

APK:

http://pan.baidu.com/s/1qsygp

4. 总结

至此,这个android的CrackeMe_1就算破解完成了。这段时间的android学习也算暂时告一段落,移动无线安全是未来的新方向,在不远的将来,基于android平台的各种应用和软件不仅仅是手机甚至是各种的互联终端都将进入人们的视野,无线安全的研究应该也会慢慢成为热点。

我也希望下次再研究android安全的时候能有更深入的认识和体会。