JEB、Android Studio、IDA Pro简单动态调试分析

1 背景

Android动态调试程序通常用来执行以下操作及其他任务:

  • 选择要在设备上调试应用;
  • 在Java、Kotlin和C/C++代码中设置断点;
  • 在运行时检查变量和对表达式求值。

2 JEB动态调试smali代码

2.1 JEB常用功能

JEB是一款强大的跨平台Android分析工具,功能丰富。此外,JEB的脚本化功能在自动化分析与对抗代码混淆中非常使用,其主要功能如下。

  • 反编译apk、dex
  • 包名、树状图
  • 查看指定类的smali代码
  • 转换成Java语言
  • Java代码中双击函数,进入函数方法的定义,查看方法的调用
  • 查看AndroidManifest.xml

JEB常用快捷键及其功能如。

  • JEB2按Q键将指定区域的smali代码转换成Java代码,JEB3是Tab键
  • X键查看方法的引用
  • Ctrl+B在指定的smali代码行中加断点
  • Ctrl+F搜索函数、字符串等,注意程序搜索是从上到下搜索,如果在中途的smali代码段开始搜索是搜索不到上面代码段中的信息,而环绕搜索是在全部的smali代码中搜索

Debug模式和普通模式的区别在于启动过程。

  • 普通模式:无法调试界面的创建,即onCreate函数
  • Debug模式:可以调试界面的onCreate函数,即刚开始创建界面是就开始调试

2.2 JEB普通调试

首先,在模拟器中打开注册机程序,然后,使用JEB打开zhuceji.apk文件,依次点击Debugger->Start,并找到相关进程双击进行附加调试,如图1所示。

图1 附加调试进程

接着,在MainAtivity的checkSN函数中的equalsIgnoreCase之前下断点,然后在模拟器中输入任意16位注册码进行调试,程序走到这里时查看相关寄存器即可找到用户名ihui对应的注册码9458797e3b684bde,如图2所示。

图2 找到真实注册码

2.3 JEB Debug调试

首先,查看AndroidManifest.xml中的application节点是否为可调试。如果是False,则不能调试,需要改成True然后重新打包APK,安装可调试的APK,然后adb shell am start -D -n 应用程序包名/应用程序入口界面,这里可以看到APK已经可调试。

图3 查看程序是否可调试

然后,在cmd中输入命令adb shell am start -D -n com.qianyu.zhuceji/.MainActivity以Debug调试模式启动APP,如图4所示。

图4 以Debug模式启动APP

这时候,一定不要点击弹窗,否则还得重新执行上述步骤。

使用JEB开始调试,和2.2中普通调试模式一样,最终也能到用户名ihui对应的注册码9458797e3b684bde。

3 Android Studio动态调试smali代码

首先,导入反编译出来的完整的smali代码,设置好调试所需要的条件,设置合理的断点。然后,连接模拟器,运行待调试APK的应用程序,执行程序流程,运行到断点处的smali代码会自动暂停,从而进一步分析和调试程序运行过程中的参数。

3.1 前期准备

3.1.1 前期工具准备

我们需要准备以下工具,以便进行后续操作。

  • Android Killer:反编译APK后获得smali代码
  • Android Studio:动态调试smali代码
  • smalidea-0.06.zip:Android Studio插件,用来给smali代码下断点,单步调试

【注】smali下载地址:https://github.com/JesusFreke/smalidea一定要下载smalidea-0.06.zip,0.05版本的不能在较新版本的AndroidStudio中下断点。

3.1.2 实现Android手机全局可调试

这一步很关键,就是让运行在设备中的程序支持Debug。方法有几种:

  • 把设备root掉
  • 修改测试机的/default.prop文件的ro.debuggable=1,目测这一步也可能需要root
  • 使用模拟器
  • 修改APK的Manifest application属性android:debuggable=”true”,可以用apktool解出Manifest然后修改,接着重新打包回去
  • 打开系统调试总开关,使用am命令,以调试模式启动应用
  • 终极办法,自己编译一个debug版的rom,此法稍微麻烦一点

3.2 将反编译后的smali代码导入到Android Studio

首先,使用Android Killer打开zhuceji.apk,然后点击工程管理器->注册机->右键打开方式->打开文件路径,就得到了反编译后的工程文件Project

3.2.1 导入smali代码

在AndroidStudio中打开反编译后的工程文件Project,然后,在项目目录依次选中smali->设置Mark Directory as->设置为Sources root,如图5所示。

图5 设置smali文件夹

3.2.2 设置Project的SDK

接着,依次点击File->Project Structure,设置Project SDK,如图6所示。

图6 设置Project的SDK

3.2.3 配置远程调试

首先,依次点击Run->Edit Configurations->Remote JVM Debug,添加一个remote调试。设置Name为任意值,我这里设置为APP的包名com.qianyu.zhuceji,端口为8700,未占用端口均可,Use module classpath设置为Project,如图7所示。

图7 添加JVM远程调试

3.2.4 Android Studio普通调试

在smali文件中的适当位置下断点,点击图8中Attach Debugger to Android Process

图8 普通模式调试

下一步,通过命令adb shell ps | findstr “com.qianyu.zhuceji”获得进程PID为4049,如图9所示。

图9 获得APP的PID

另外,通过命令adb forward tcp:8700 jdwp:4049进行端口转发。接着,在模拟器中输入用户名ihui,注册码1234567887654321,点击注册按钮后触发checkSN函数调用,单步步过后即可获得用户名ihui对应的注册码9458797e3b684bde,如图10所示。

图10 获得用户名ihui对应的正确注册码

3.2.5 Android Studio Debug调试

Debug模式动态调试与普通模式动态调试唯一的区别就是,Debug模式动态调试使用命令行启动调试模式adb shell am start -D -n com.qianyu.zhuceji/.MainActivity启动APP。后续步骤与3.2.4一致,这里不予赘述。

4 IDA Pro动态调试so库

4.1 基本步骤

IDA Pro动态调试so库的基本步骤如下所示:

  1. 要把android_server发送到手机目录的data/local/tmp
  2. (给手机最高权限su)来到手机的data/local/tmp目录下,运行android_server(mv android_server as、mv 原文件名 新文件名)
  3. 重新打开一个cmd窗口,开启端口转发:adb forward tcp:23946 tcp:23946(这里转发的端口号和你运行android_server的端口号一定一致)该端口的命令:./android_server -p 65535
  4. 挂起程序(需要程序的包名+类名),命令是adb shell am start -D -n com.example.javandk1/.MainActivity,执行完毕这条命令后,手机现象:出现个弹窗,Waiting For Debugger
  5. 打开ddms窗口,观察调试进程有红色虫子(debug调试)(这一步可以放在第4步之前)
  6. 打开IDA,填写主机号,端口号(与转发端口一致),选择进程,双击进来,勾选三项,F9运行
  7. 回到cmd窗口,记录调试程序的端口号,在命令的port处修改,执行jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8614,等待so库加载。在模块中找到该so库,在so库中找方法:找jni_onload(动态注册)或者java_开头的方法(静态注册)

4.2 具体操作步骤

首先,由于我这里没有root过的手机,就只能使用模拟器,所以是启动android_x86_server(root过的手机应该启动android_server),这里指定端口10001,如图11所示。

图11 启动android_x86_server

然后,开启端口转发,如图12所示。

图12 开启端口转发

接着,在打开Android Device Monitor之后,使用命令adb shell am start -D -n com.ihui.cmakedemo/.MainActivity挂起程序,如图13所示。

图13 挂起程序

下一步,打开IDA Pro,依次选择Debugger -> Attach -> Remote Linux debugger,填写主机号为127.0.0.1,端口号为10001,其中Debug options勾选三项,选择进程com.ihui.cmakedemo。

此外,执行jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8609,其中port的值在Android Device Monitor的第三列查看得到,而且程序前面的小红虫变成了小绿虫,如图14所示。

图14 jdb附加

最后,在Modules中搜索到jni_onload查看动态注册方式,没有发现java_开头的静态注册方式,如图15所示。

图15 查看JNI的动态注册方式

4.3 IDA动态调试so的常见面试题

这里先提出五个常见的问题,如下所示:

  1. 动态调试的作用以及与我们常说的脱壳区别之处?
  2. IDA的下断点调试的原理?
  3. 有无反调试的步骤区别?以及原理?
  4. 反调试与反附加的区别?
  5. IDA动态调试so时有哪三个层次?以及如何下断点?

【注意】so的动态调试与脱壳在步骤上有很多的相似之处,关于脱壳在后面会详细介绍加壳以及脱壳的发展历程。

4.3.1 动态调试的作用以及与我们常说的脱壳区别之处?

动态调试作用有二:

其一:dump内存,即:找准时机dump出解密后的正确文件;

其二:查看每一步状态,进一步分析出正确的逻辑;

脱壳只是我们在调试系统级别的.so文件后 ,找准时机dump出正确而真实的.so文件,而动态调试只不过是手动脱壳的一种表现方式。

4.3.2 IDA的下断点调试的原理?

下断点原理:

由于下断点有硬件断点和软件断点,我们在这里只说IDA中的软件断点原理:

X86系列处理器提供了一条专门用来支持调试的指令,即INT 3,这条指令的目的就是使CPU中断(break)到调试器,以供调试者对执行现场进行各种分析。

当我们在IDA中对代码的某一行设置断点时,即:F2,调试器会先把这里的本来指令的第一个字节保存起来,然后写入一条INT 3指令,因为INT 3指令的机器码为11001100b(0xCC)当运行到这的时候CPU会捕获一条异常,转去处理异常,CPU会保留上下文环境,然后中断到调试器,大多数调试器的做法是在被调试程序中断到调试器时,会先将所有断点位置替换为INT 3的指令恢复成原来的指令,然后再把控制权交给用户。这样我们就可以愉快的开始调试了,图16是调试器的原理图:

图16 调试器的原理图

4.3.3 有无反调试的步骤区别?以及原理?

先说无反调试:

1.adb push d:\android_server(IDA的dbgsrv目录下) /data/local/tmp/android_server(这个目录其实可以随便放,有的反调试会检测这)

2.adb shell

3.su(一定要有root权限)

4.cd /data/local/tmp

5.chmod 777 android_server(执行权限要给)

6.再开一个cmd
adb forward tcp:23946 tcp:23946(端口转发,调试手机上的某个进程要有协议支持通信)

7.打开待调试的应用程序,就可以愉快的调试了

再来说有反调试:

在很多情况下我们遇到的是有反调试并且用上面的步骤,附加进去以后直接就退出了,这样的例子数不胜数,那就是反调试惹的祸。

这时候我们就要改变调试战略了

在上文的基础上:

1.启动android_server;

2.端口转发adb forward tcp:23946 tcp:23946;

3.adb shell am start -D -n 包名/类名;

(说明:以启动模式启动,是停在加载so文件之前,包名在AndroidMainfest文件中可以找到)

4.打开IDA,附加上对应的进程之后,设置IDA中的load so的时机,在debug options中设置一下,后面会有实战部分;

5.adb forward tcp:8700 jdwp:进程号;(jdwp是后面jdb调试器的协议,转换到待调试的指定的应用程序);

6.jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700(jdb进行附加);

7.可以愉快的下断点,开始调试了;

4.3.4 反调试与反附加的区别?

反调试就是阻止你进行动态调试所采用的一种手段。

反附加,在这块重要的是说jdb的反附加,很多情况下jdb会附加不上,就是会出现“无法附加到目标的VM”这样的问题那是因为在每个应用程序下,有这个android:debuggable=”true”才能调试。

4.3.5 IDA动态调试so时有哪三个层次?以及如何下断点?

我们知道在so的加载时候有个这个过程:

.init -> .init array -> JNI_Onload -> java_com_XXX

还有我们在脱壳的过程中会在一些系统级的.so中下断点比如:fopen,fget,dvmdexfileopen,等等

而.init以及.init_array一般会作为壳的入口地方,那我们索性叫它外壳级的.so文件

这里归纳为三类:

  • 应用级别的:java_com_XXX;
  • 外壳级别的:JNI_Onload,.init,.init_array;
  • 系统级别的:fopen,fget,dvmdexfileopen;

对于在应用级别的和系统级别的就不说了比较简单容易理解,这里也是在实现篇中会重点说的,看到上面的.so的加载执行过程我们知道如果说反调试放在外壳级别的.so文件的话,我们就会遇到程序在应用级核心函数一下断点就退出的尴尬,事实上多数的反调试会放在这,那么过反调试就必须要在这些地方下断点,重点应说如何在.init_array和JNI_Onload处理下断点。

5 参考文献

[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.
[2]https://blog.csdn.net/freeking101/article/details/105910877
[3]https://blog.csdn.net/freeking101/article/details/105937026
[4]https://cloud.tencent.com/developer/article/1764588


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏