本文主要介绍 Android 逆向学习,这里整理逆向学习的思路及学习要点,并附示例代码,帮助大家学习理解,有需要的小伙伴可以参考下
Android 是一种基于 Linux 的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由 Google 公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用 "安卓" 或 "安致"。
断断续续的总算的把 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:
4. 总结
至此,这个 android 的 CrackeMe_1 就算破解完成了。这段时间的 android 学习也算暂时告一段落,移动无线安全是未来的新方向,在不远的将来,基于 android 平台的各种应用和软件不仅仅是手机甚至是各种的互联终端都将进入人们的视野,无线安全的研究应该也会慢慢成为热点。
我也希望下次再研究 android 安全的时候能有更深入的认识和体会。
来源: http://www.phperz.com/article/17/0319/295238.html