2020.06.03更新
关于java 层动态调试,也就是smali代码的调试。网上有一些帖子会介绍一些方法,需要反编译再回编译,其实那不是必须的。不过作为学习,方法我们都需要理解。
之前介绍过一些做逆向的工具,其中不乏一些反编译和回编译的工具。这篇文章的主题,进行smali 的动态调试,其中关键的一点,就是获得apk的smali代码。当然,这里先排除加壳等情况。默认是可以通过反编译的,获取smali代码确实是必要的,但是回编译就不是必须了。
如果不知道反编译和回编译,可以自行补课。这里不展开了
apk 的反编译和回编译.note
回到smali的动态调试,主流的有2种调试工具:
1、使用JEB
2、使用android studio + smalidea 插件进行
关于这2种工具的调试其实都大同小异。但是其实个人来说更偏向使用android studio。具体的差异后面会提到。
在开始调试之前,其实是有工作需要做的。
一、准备调试机
我们可以使用真机也可以使用模拟器来进行smali代码的调试。不过模拟器相对会比较不稳定,出现一些奇怪的问题。为了避免影响到后续的调试工作,最好是使用真机。
使用真机进行调试工作,最好需要root 的。有一台root后的真机,几乎是逆向分析者必备的。
另外,root 之后的真机,在开始工作的时候,别忘了开启usb调试。
二、获取到apk 的调试权限
获取权限的方法有很多,这里介绍主流的几个:
1、在 AndroidManifest.xml 的 application 节点 加上 android:debuggable="true" 如:
给apk的清单文件加上 android:debuggable="true" 就意味着这个apk是可以被调试的。但是修改清单文件的前提是,需要成功反编译apk的资源文件,然后再回编译,最后还需要重新签名。签名之后apk是否能正常运行,运行之后是否有签名校验需要绕过等。这都是需要我们解决的问题。
毫无疑问,这个反编译和回编译的工作量是比较大的。早期的apk没什么防护问题,回编译也方便,但是现在这个方法越来越不好用了。而且这个方法获得的权限只是当前的apk 可调式权限,对于不同apk来说这一套签名验证的方法依旧无法绕过,无法做到通用。
2、有一个通用的获得调试权限的方法,那就是修改default.prop中的ro.debuggable的值为1。
这里有两种做法:
(a)改文件
(b)改内存
像这种系统文件,一般改文件都是只读的。首先是没有权限,另外即使改了这个文件的值,内存中的值也没改,需要重启。但是一旦重启,可能这个被修改的文件又恢复了,还是不行。(这个不同的机子可能会有所不同,简单测试下就知道了),当然,如果有人说我刷机改这个文件。那当我没说(不过刷机是有风险的,能接受变砖那就无所谓了)。。
然后另一个方法就是改内存了。
当我们拿到一个手机的时候,一般的 ro.debuggalbe 的值都是0。 这代表不使用调试.
init进程会解析这个default.prop文件,然后把这些属性信息解析到内存中,给所有app进行访问使用,所以在init进程的内存块中是存在这些属性值的,那么这时候我们可以利用进程注入技术,我们可以使用ptrace注入到init进程,然后修改内存中的这些属性值,只要init进程不重启的话,那么这些属性值就会起效。
解决方法:
我们可以通过 mprop 这个网上大神给出的工具对着值进行修改。
第一步:拷贝mprop 到/data/目录下(注意mprop 跟你的手机是否适配,比如armeabi-v7a,X86)
第二步:./mprop ro.debuggable 1 (当然前提赋予mprop 可执行权限)
或者使用 setprop ro.debuggable 1 也可以修改
第三步:getprop ro.debuggable(查看此时ro.debuggable在内存中的值是否修改成功)
第四步:stop start(重启adbd进程,adb 其实包括3部分,adb server ,adb client 和 adbd 进程。注意如果这里手机重启的话这边改动的值是会变回来的)
成功修改 ro.debuggable =1 之后,这里我们使用 getprop可以查看ro.debuggable 值得状态, 现在手机中的所有应用都支持debug了。
到这里,我们就有了能获取apk 调试权限的通用解决方案。
接下来我们先来了解下调试过程中会使用到的工具以及几个常用端口。这对于理解调试过程是很必要的。
1、5037
这是adb 工具(server端)的默认端口。
当您启动某个 adb 客户端时,客户端会先检查是否有 adb 服务器进程正在运行。如果没有,它将启动服务器进程。服务器在启动后会与本地 TCP 端口 5037 绑定,并监听 adb 客户端发出的命令 - 所有 adb 客户端均通过端口 5037 与 adb 服务器通信。
然后,服务器会与所有正在运行的设备建立连接。它通过扫描 5555 到 5585 之间(该范围供前 16 个模拟器使用)的奇数号端口查找模拟器。服务器一旦发现 adb 守护进程 (adbd),便会与相应的端口建立连接。请注意,每个模拟器都使用一对按顺序排列的端口 - 用于控制台连接的偶数号端口和用于 adb 连接的奇数号端口。例如:
模拟器 1,控制台:5554
模拟器 1,adb:5555
模拟器 2,控制台:5556
模拟器 2,adb:5557
依此类推
如上所示,在端口 5555 处与 adb 连接的模拟器与控制台监听端口为 5554 的模拟器是同一个。
服务器与所有设备均建立连接后,您便可以使用 adb 命令访问这些设备。由于服务器管理与设备的连接,并处理来自多个 adb 客户端的命令,因此您可以从任意客户端(或从某个脚本)控制任意设备。
2、8600 / 8700
这是工具ddms (就是上面的那个monitor工具)的端口。
对于Android或者模拟器中每一个运行的Android应用,都是在每一个单独的dalvik虚拟机实例中的。每一个虚拟机实例都是独立的进程空间,对于每一个虚拟机中的debugger,ddms都会监听一个独立的监听端口,端口从8600开始,8601,8602...等各种调试器就是通过这些端口来进行调试的。
比如,我们这里一台测试手机设置了 ro.debuggable =1,然后 打开了monitor发现 发现有许多应用都是可以调试的,但是如果是使用 android:debuggable="true" 这种方式获得调试权限,那样就只有回编译成功的少数app才能在这个列表中展示。
最后一个8615就是我们当前要调试的应用,对应的pid是702
说到这里,其实已经离调试的原理很接近了。
如果我们需要调试pid 为702 的程序,那么只需要让我们的调试器跟这个702程序通讯就行了。
怎么做,有个命令
比如:
adb forward tcp:58099 jdwp:702
这个命令的意思是将pc端的58099 端口接收到的数据转发到手机中pid 702的进程中。JDWP(java debug wire protocol)是dalvik VM的一个线程,可以建立在adb或者tcp基础上,与DDMS或debugger进行通信。
用人话说,就是调试手机中pid 为702 的应用,要通过本地的58099端口来进行调试。
比如我这个图,就是说手机中运行的pid 为5037 程序,需要通过本地58099端口调试。
记住这点,后面很多东西都能理解了。
至于8700端口,我们后面再说
接下来我们本别说一下使用Android studio 和jeb 来调试smali代码。
#############################################################################
#############################################################################
(AS)android studio安装 smalidea 插件,并且导入smali 反编译代码。
先来看看使用android studio 是怎么调试smali的,不过android studio本身并不支持smali 代码的识别。不信的人可以试试现在就导入smali代码,会发现smali代码中的关键字不会被高亮显示,而且也无法设置对应的断点。所以我们需要安装smailIdea插件才行。插件在这下载
https://bitbucket.org/JesusFreke/smali/downloads/
目前最新的是smalidea-0.05.zip。我们先下载下来。下载后需要在android studio中安装
点击File->Settings->plugins->instal plugins from disk,选择下载的zip包就行,不要解压。安装完成后,AS提示重启生效,重启即可
等待插件安装完成并且重启即可。
接下来,有2种做法:
1、这个是通用方法,就是先使用第三方的工具,先将目标apk反编译成smali代码,再重新导入到这边的AS
具体步骤如下:
点击import project
然后出现
不用管这个一路next,最后finished
最后工程被导入进来了
用project 视图查看,不然看不清。
导入之后我们发现smali 代码已经高亮显示了,而且还可以加上断点
如果不能彩色显示,也无法添加断点,那就是smalidea 插件没有成功安装。需要返回去检查下步骤。
这边打上断点之后就可以开始我们的调试工作了。
首先打开目标app,然后通过
ps |grep xxx(app包名)
来查询目标app 的pid,比如:
这样就查到了进程名含有 console 的进程pid为25674.
接下来做一个转发操作 ,
这样就是说明pid 为25674的程序现在要通过本地58099这个端口来远程调试。这个58099端口大家可以随意,如果被占用了就换别的无所谓的。
还有就是 如果没有 打印出 started successfully 的话是有问题的。
需要使用 adb kill-server ,然后重试一次
做了转发之后,我们需要设置一下这个58099的调试端口号
点开调试设置选项,添加一个remote 类型的调试
类型是remote,然后再端口处填58099就行
完成之后按照正常的调试程序来就行。
点击 debug(这个是比较旧的 AS版本)
我后面更新AS 到 3.6.1 的话界面有点变了,点击这个小虫子才是调试。
顺利进入调试,前面的断点也被触发了。
我们可以单击 android studio 上的代码行数旁边空白处,添加断点。然后运行app 触发断点。如下:
我们还可以在变量查看器中添加我们感兴趣的寄存器,查看器会显示出当前寄存器的值
2、方法2是适用于新版的Android studio 的。后面我更新Android stuido 版本到3.6.1之后发现,android studio 的功能真的越来越强大了。
我们完全可以通过这个profile or debug apk 选项来完成我们的调试工作。也不需要先找个第三方工具来反编译什么的,直接apk打开就行。后面的工作完全自动。甚至你连在android 机子中都不需要提前安装待调试的apk
跟方法1一样的是smalidea 插件也还是需要安装的,这里就不重复说了。
接下来还是一样打断点。
在project 视图找到需要打断点的类。
这里需要注意一下,就是smali 代码是按照dex 分开的。一个apk可能有多个dex。所以要多找找。完了之后就可以开始调试。
调试直接点开debug 那个小虫子就行。
然后看下面的输出
这里有一个
adb shell am start -D -n <包名>/<入口activity全类名>
这是一个以调试模式启动应用的启动方式,先留意以下,后面还会提到。
然后这边就能看到,我们的调试器已经附加到18202这个进程。然后这个进程就是我们的目标
我们的断点也能生效了
再来看看我们的debug窗口
这里明显告诉我们 ,能调试成功,是因为AS 主动帮我们做了这个进程的端口转发工作
也就是帮我们运行了
adb forward tcp:8631 jdwp:18202
这个命令。
除了点击那个debug的小虫子,点击旁边的attach,也可以成功将进程附加到我们的调试器中。如图
知道了这个原理,我们其实对于工具的使用就很灵活了。大致的步骤就是下面几步:
1、反编译得到smali
2、得到目标的进程pid
3、做一次进程端口的转发
4、将调试器附加到目标进程转发出来的端口
现在,我们大致知道了AS 怎么动态调试smali。现在来看看JEB.的samli 调试过程
#############################################################################
#############################################################################
(JEB)、JEB中调试SMALI
和新版的AS一样,JEB的调试也是很简单的。因为它不需要安装什么插件,也不需要先将apk反编译回smali代码导入,非常简单粗暴的将开启debug 的apk拖入到JEB中,就能自动进行反编译
然后就会得到这样的一个反编译的信息,双击一下bytecode,会跳出apk的代码结构,如下
找到我们关心的类,点击进去就能转到相应的smali代码
我们可以右击选择decompile 然后跳转到对应的java层代码
操作都挺简单的。接下来我们下断点。
鼠标放在某一行smali代码上,然后按下ctrl+B 就可以添加一个断点。再重复一次可以取消断点。
设置完成断点之后我们点击debug 这个按钮开始进入调试。和前面一样的,要先启动我们的app,使之进入一个空白的状态,这就是在等待进入调试了。
然后出现选择如图所示,点击选择设备和进程,带D的就是该apk的进程,如下所示,然后这里注意下包名,要与当前的一致
点击attach,就可以进入调试(这里要注意一点,如果你开启了ddms,在这一步之前需要关闭,否则会attach失败的)
attach 成功后会看到程序已经卡在断点处了。
单步执行什么的可以点击上面的工具栏,也可以将鼠标停留在图标上,可以看到对应的快捷键,用快捷键调试也是可以的。
接下来,我们查看VM/Breakpoints这个窗口,就能看到变量的值了,但是这里有个小问题,可以看到v3这个地方应该是个字符串,但是显示的却是数字:
所以这个时候我们需要自己动手改一下啦,将int改为string,就能够正常显示啦!
这里跟android studio 相比起来有个不好的地方。就是寄存器的值类型我需要设置一下,不然就默认是int,这有点不直观。
还有的就是android studio可以自己添加需要观察的变量,这里好像不能添加。有时候我想要观察某些变量,但是这里没有的话就挺抓狂了。有时候会遇到,估计是我这边破解版本的bug。
#############################################################################
上面就是基本的smali 动态调试流程了。
这里来聊聊调试模式启动的问题。
前面我们还留了一个命令,还记得不?
就是
adb shell am start -D -n <包名>/<入口activity全类名>
这个命令其实是以调试的模式打开apk。有啥用?
也许我们会遇到这样一个场景。需要跟踪或者分析一个apk初始化的过程。前面我们都是怎么做的?手动点击app的图标来获得程序的pid,这是不可行的。因为你点击图标之后找到pid然后再转发端口,附加调试器到进程的过程中,apk的初始化流程早就执行过去了。那怎么办?
这时候就需要使用到这个命令。命令执行
debug模式启动之后,进程会被阻塞。在我们看来就是app打开一个全白的页面,然后卡住不动了。这其实就是android 进程在一直在等待调试器附加上来。这样就不会错过初始化流程了。
网上一直有人提及另一种以调试模式打开app的方法。
反编译dex 文件,然后找到程序启动的入口,入口activity也好,application 也行,都无所谓。
找到之后在smali 中修改onCreate 方法。
一般入口activity 在AndroidManifest.xml 中找,有 intent-filter节点,并且有android.intent.action.MAIN 和 android.intent.category.LAUNCHER 声明的就是。
比如我们找到入口是MainActivity。然后找到这个activity对应的smali代码 ,在onCreate处加上
invoke-static {}, Landroid/os/Debug;->waitForDebugger()V
加上这句话之后,app启动的时候就会以debug模式启动。debug模式启动之后,进程会被阻塞。在我们看来就是app打开一个全白的页面,然后卡住不动了。这就是android 进程一直在等待调试器附加上来,后面的故事我们也都知道了。
别的不说,就这种对于apk需要修改的方法就已经很讨人厌了。首先这个方法通用性不强。每开始一个app的smali 动态调试 都需要先进行 反编译和回编译,重签名,签名绕过这些的研究,是不是有点太折腾了一些?所以对于我来说一般都是果断pass 这种方法的。但是对于方法来说,还是需要知道。
以调试模式启动apk,并不是必须的。除非你要研究apk初始化过程。然后启动方式最好也是用
adb shell am start -D -n <包名>/<入口activity全类名>
这个命令启动,在smali中插入 waitForDebugger() 方法启动的方法,用起来还是比较不方便的。
#############################################################################
上面说到过 ddms工具,ddms 全程 是dalvik Debug monitor Service。它的功能很多,提供查进程,线程,堆栈,logcat,广播,提供截屏,模拟来电呼叫,发送短信,虚拟地理坐标等等功能。但是我们这边其实用到的功能更不多。
另外还有一个8700端口我们没有介绍。
网上很多帖子有用到ddms 工具。其实这个工具也不是必要的。
在新版的AS中,这个工具已经被代替了。(https://developer.android.com/studio/profile/monitor)
抛开ddms 其他很多功能我们先不说,就看这里用到的devices窗口
对于这个窗口大家都熟悉,
前面是进程的包名,后面紧跟pid (5620) 。
关于这点我们可以通过 top | grep 5620 来验证这个pid 是否当前调试的程序。
这个pid号我们是有办法查到的,在AS中,也能轻易找到这个pid,比如通过debug窗口
比如:logcat 后面跟着的数字:
后面的8600是ddms为每一个进程单独开启的监听端口。如果有多个可调式的进程,这里就会有很多个端口,从8600开始,8601,8602...以此类推。
那8700呢?就是一个转发的端口。这个端口是ddms自己的端口。它的功能是将进程的端口做一个转发。
比如我们要调试8600,8601,8602对应的程序,就需要每一次都在调试的时候找到这个端口号,然后在我们的IDE中修改这个端口的配置。
但如果通过端口转发,比如动态将8600,8601这些端口转到8700上面。那我们调试的时候这个端口号就是8700.,就不会变了。这就是8700的作用,接受86**这些端口的数据,然后让IDE通过8700与这些进程通讯。
所以有的文章会写道,直接使用这个8700端口进行调试,也是没问题的。
但是使用8700这个端口的前提是,你用的AS需要时比较低的版本,在我现在的3.61版本中。ddms的部分功能已经集成进AS,对应的8700端口也已经被AS占用了。所以在新版中使用9700端口进行调试时行不通的。
使用比较低版本的AS调试smali 代码的文章中,经常会看到一会打开ddms,一会又要关掉的情况,这其实就是8700端口的占用问题。
开启了DDMS,调试器不论连接到8700,还是86** ,都是可以成功调试的。
需要注意的时需要先使用
adb shell am start -D -n <包名>/<启动Activity名称>
这个命令启动需要调试的程序,
然后使用ddms 可以看到,这时候目标程序 5620已经被转到8600端口了,而且这时候8600被转到8700,正等待我们对8700这个端口进行挂接。
我们可以在AS中remote 调试中使用8700 端口进行调试。
如果出现了端口被占用了,就找到占用端口的进程,kill掉就行。
一般的我们可以关掉ddms 再试一下,如果不行,那就使用 adb kill-server 关掉adb 再试一次。
如果不开启ddms,自己找到需要调试的进程,然后设置一次端口转发也是可以的。
#############################################################################