Android 逆向学习详解及实例

断断续续的总算的把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 运行一下程序,收集一些基本信息

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

  2.2 分析

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

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

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

  2.2.1 类b的分析

  从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) 构造函数

Java代码

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

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

  2) public static boolean b()

Java代码

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()

Java代码

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)

Java代码

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()

Java代码

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)

  从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)

Java代码

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行:

  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  ,然后再启动程序

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

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

  3.2 读取程序对应的文件

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

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

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

  3.3 编写注册机

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

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

  核心算法如下:

Java代码

@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();
      }
    } 

  });

  破解结果

  APK:

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

  4. 总结

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

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

有兴趣的同学可以看下本文,谢谢大家对本站的支持!

(0)

相关推荐

  • Android APK优化工具Zipalign详解

    Android SDK中包含了一个用于优化APK的新工具zipalign.它提高了优化后的Applications与Android系统的交互效率(俗话:"要致富先修路",Android小组重新为Applications与Android系统之间搭建了一条高速公路),从而可以使整个系统的运行速度有了较大的提升.Android小组强烈建议开发者在发布新Apps之前使用zipalign优化工具,而且对于已经发布但不受限于系统版本的Apps,建议用优化后的APK替换现有的版本. 在下面的内容中将

  • Android自定义圆形倒计时进度条

    效果预览 源代码传送门:https://github.com/yanzhenjie/CircleTextProgressbar 实现与原理 这个文字圆形的进度条我们在很多APP中看到过,比如APP欢迎页倒计时,下载文件倒计时等. 分析下原理,可能有的同学一看到这个自定义View就慌了,这个是不是要继承View啊,是不是要绘制啊之类的,答案是:是的.但是我们也不要担心,实现这个效果实在是so easy.下面就跟我一起来看看核心分析和代码吧. 原理分析 首先我们观察上图,需要几个部分组成: 1. 外

  • Android中使用ViewStub实现布局优化

    在Android开发中,View是我们必须要接触的用来展示的技术.通常情况下随着View视图的越来越复杂,整体布局的性能也会随之下降.这里介绍一个在某些场景下提升布局性能的View,它就是ViewStub. ViewStub是什么 ViewStub是View的子类 它不可见,大小为0 用来延迟加载布局资源 注,关于Stub的解释 A stub is a small program routine that substitutes for a longer program, possibly to

  • 利用Android中BitmapShader制作自带边框的圆形头像

    效果如下: BitmapShader 的简单介绍 关于 Shader是什么,Shader的种类有哪几种以及如何使用不属于本文范畴,对这方面不是很了解的同学,建议先去学习一下 Shader 的基本使用. BitmapShader主要的作用就是 通过Paint对象,对 画布进行指定的Bitmap填充,实现一系列效果,可以有以下三种模式进行选择 1.CLAMP - 拉伸,这里拉伸的是图片的最后一个元素,不断地重复,这个效果,在图片比较小,而所要画的面积比较大的时候会比较明显. 2.REPEAT - 重

  • Android样式的开发:layer-list实例详解

    上图Tab的背景效果,和带阴影的圆角矩形,是怎么实现的呢?大部分的人会让美工切图,用点九图做背景.但是,如果只提供一张图,会怎么样呢?比如,中间的Tab背景红色底线的像素高度为4px,那么,在mdpi设备上显示会符合预期,在hdpi设备上显示时会细了一点点,在xhdpi设备上显示时会再细一点,在xxhdpi上显示时又细了,在xxxhdpi上显示时则更细了.因为在xxxhdpi上,1dp=4px,所以,4px的图,在xxxhdpi设备上显示时,就只剩下1dp了.所以,为了适配好各种分辨率,必须提供

  • Android ViewPager实现Banner循环播放

    问题的起源 在项目里,有时候需要实现一个图片轮播的效果,用来展示Banner.同时,图片能循环播放,下面还有一排小圆点来指示当前轮播到哪一页了. 如下图: 分析 · 图片的个数是会变化的,同时小圆点的个数也会跟着图片个数变化 · 每一个page的布局是一样的.变化的就是小圆点的个数,所以需要用代码来动态生成小圆点 编码 布局 首先完成 MainActivity 的布局 activity_main.xml <RelativeLayout xmlns:android="http://schem

  • Android Zipalign工具优化Android APK应用

    生成的Android应用APK文件最好进行优化,因为APK包的本质是一个zip压缩文档,经过优化能使包内未压缩的数据有序的排列,从而减少应用程序运行时的内存消耗.我们可以使用Zipalign工具进行APK优化. 据Android官方网站的说明,Zipalign是一款重要的优化APK应用程序的工具. 多数软件开发商在正式推出其Android应用程序,都使用Zipalign工具优化APK包.但是,仍然有一些应用程序需要我们自己动手进行Zipalign优化,例如一些个人开发的软件.一些破解版的软件.

  • AndroidSDK Support自带夜间、日间模式切换详解

    写这篇博客的目的就是教大家利用AndroidSDK自带的support lib来实现APP日间/夜间模式的切换,最近看到好多帖子在做关于这个日夜间模式切换的开源项目,其实AndroidSDK Support中已经有了非常好的支持了. 本文demo下载地址在文章的末尾,看完文档如果还不能实现可以下载玩玩. -------------------------------------------------------------------------------- 效果演示 左是Android 4

  • Android 动态高斯模糊效果教程

    写在前面 最近一直在做毕设项目的准备工作,考虑到可能要用到一个模糊的效果,所以就学习了一些高斯模糊效果的实现.比较有名的就是 FastBlur 以及它衍生的一些优化方案,还有就是今天要说的RenderScript . 因为这东西是现在需要才去学习的,所以关于一些图像处理和渲染问题就不提了.不过在使用的过程中确实能感受到,虽然不同的方案都能实现相同的模糊效果,但是效率差别真的很大. 本篇文章实现的高斯模糊是根据下面这篇文章学习的,先推荐一下.本文内容与其内容差不多,只是稍微讲的详细一点,并修改了代

  • Android优质索尼滚动相册

    虽然索尼手机卖的不怎么样,但是有些东西还是做的挺好的,工业设计就不用说了,索尼的相册的双指任意缩放功能也是尤其炫酷.其桌面小部件滚动相册我觉得也挺好的,比谷歌原生的相册墙功能好多了,网上搜了一下也没发现有人写这个,于是,下面就介绍下我的高A货. 首先是效果图: 主要手势操作有: 1.上/下满速移动,可以上滑/下滑一张图片  2.上/下快读移动,则根据滑动速度,上滑/下滑多张图片  3.单击则请求系统图库展示该图片 该小部件的主要优点:在屏幕内的小范围内提供一个很好的图片选择/浏览部件,尤其是切换

随机推荐