首页
社区
课程
招聘
[原创]深入理解Android逆向调试原理
2021-3-9 21:24 20442

[原创]深入理解Android逆向调试原理

2021-3-9 21:24
20442

(转载请声明出处,谢谢!)

一.为什么要写这篇文章

使用IDA远程调试Android SO时,有一些场景需要在init_array、JNI_OnLoad等函数入口处下断点,对应的调试手法是以调试模式启动应用,处理完毕之后通过jdb命令通知应用继续运行,这种方法的优势是能够在程序开始运行之前获取程序的控制权。

 

在上面的步骤中,jdb命令有一个选项,是调试端口(port),这个端口号一般通过DDMS获取,常见的教程也是教大家通过DDMS来获取。但是DDMS已经被官方的开发套件弃用,而我既不想修改已经搭建好的环境,也不想额外下载旧版本的开发套件,所以打算研究一下有没有更加便捷的方式。

 

我最初的思路是这样:DDMS在调试过程中最主要的作用就是展示端口号,这个端口号由jdb在host上直接使用,说明是host的本地端口,jdb使用本地端口是无法直接与远程终端(这里就是Android)通信的,说明经过了端口转发。所以我的初步判断是,DDMS通过端口转发将本地端口转发到了目标机器的端口,如果能够找到目标机器的端口,那就可以通过命令行来执行转发。

 

经过初步的探索,我找到了DDMS的替代方案。但在通过搜索引擎寻找答案的过程之中,我发现两个非常普遍的问题:一个是很多做Android逆向的同学,虽然对逆向调试的操作过程都很熟练,但是对这些操作的原理并不了解;第二个问题,就是网络上甚至实体教材上的教程,只注重操作,不注重背后的原理,这从源头上导致后来的学习者只知其然,不知其所以然。

因此我有了写一篇科普这方面基础知识文章的想法,希望用这篇文章帮助初入Android逆向领域的同学更好地入门,也希望所有对Android逆向调试的步骤有疑问的同学,能够从这一篇文章中找到答案。

二.文本的目标读者是谁

本文的目标读者包括:
1、刚入门Android逆向的同学
2、想要深入了解Android逆向调试原理的同学
3、从事Android开发相关工作的同学

三.阅读本文的潜在收益是什么

阅读本文的潜在收获包括但不限于如下几点:
1、对Android调试原理较为深入的理解;
2、对Android逆向调试原理的深入理解;
3、掌握Android逆向调试的操作步骤;
4、掌握在没有DDMS的情况下逆向调试Android的操作技巧。
本文不包括的内容:
1、对JDWP协议细节的介绍;
2、对调试器内部原理的介绍;
3、相关原理源代码的详细分析。

四.Android逆向调试的几个关键步骤

本文介绍的主要场景是使用IDA远程调试Android SO,因为这个场景是最常用、知识面覆盖最广的Android逆向调试场景。使用IDA远程调试Android SO的操作方式有两种,一种是在进程起来之后直接附加,另外一种是以调试模式启动应用。本文只讨论第二种,因为第二种操作方式能够完全覆盖第一种。

 

使用IDA远程调试Android SO的一般步骤如下:
1、以调试模式启动应用
Adb shell下执行命令:

1
am start -D -n com.outdoor.debugtest/.MainActivity

操作结果,应用进入等待调试连接的界面:

2、启动android_server

1
2
3
4
ringtag:/data/local/tmp # ./android_server64
 
IDA Android 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...

3、执行端口转发

1
adb forward tcp:23946 tcp:23946

4、IDA附加到调试进程
选择远程ARMLinux/Android debugger:

填写hostname为127.0.0.1或localhost:

附加到调试进程:

成功附加到调试进程:

配置调试选项,勾选“Suspend on library load/unload”,以在目标SO加载时进程能够停下来:

5、Jdb连接到调试进程的VM
DDMS查看调试进程端口号,假设为55555,在host机器上执行

1
2
3
4
5
C:\Users\reverser>jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=55555
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
正在初始化jdb...
>

IDA中按F9进程继续运行,直到目标SO被加载。
IDA output窗口显示目标SO文件被加载:

1
PDBSRC: loading symbols for '/data/app/com.outdoor.debugtest-iQxBx29FyKFuLtnU6wwQag==/lib/arm64/libnative-lib.so'...

6、在目标SO上下断点
在IDA Modules模块中搜索目标SO库并双击选中,找到目标函数下断,即可开启调试流程。

 

以上便是IDA远程调试Android SO的关键步骤,针对如上步骤,我现在提出如下几个疑问供大家思考:
a) DDMS展示的端口号,是怎么得来的?有没有更加便捷的方式获取这个端口号?
b) Jdb connect命令是Java VM调试相关的命令,在SO调试中为什么要用它?它在和哪个模块进行通信?IDA附加上进程之后为什么不能直接开始调试?
c) Adb、android_server、IDA之间的关系是什么?IDA与应用进程之间的调试连接到底是如何建立的?IDA调试Android SO的技术原理是什么?

 

以上问题由浅入深,从对操作技巧层面的思考,到对背后调试原理的思考,这也是我自己探索这个问题的一个过程。在回答这些问题之前,我们需要做一些关于调试原理的铺排工作,以帮助大家全面、深入地理解问题。

五.Android调试模型分析

本小节对Android调试模型的介绍,主要参考自源码文件/system/core/adb/jdwp_service.cpp中的相关注释。因为这一部分注释详细阐述了Android调试模型是如何建立起来的,所以我会首先翻译这个模型的建立过程,然后进行一个抽象的总结,以帮助大家更好地理解Android的调试模型。
1、Android源码对调试模型建立的描述:
1)当adbd启动之后,它会创建一个unix类型的server socket套接字,名字叫@jdwp-control;
2)当一个新的JDWP守护线程在一个新的VM进程中启动时,这个新启动的JDWP线程会创建一个到@jdwp-control的连接,以宣告它自己的可用性;

1
2
3
4
5
6
50       JDWP thread                             @jdwp-control
51           |                                         |
52           |------------------------------->         |
53           | hello I'm in process <pid>              |
54           |                                         |
55           |                                         |

上面这个连接一直保持着,直到JDWP线程所在的进程结束;
3)因此,adbd维护着一个JDWP列表,记录着每个活跃的进程。Adbd可以通过“device:debug-ports”服务同每一个客户进程进行通信;
4)当有调试器想要连接时,调试器执行“adb forward tcp:<hostport> jdwp:<pid>”命令,“jdwp:<pid>”用来指定设备上的目标JDWP进程。
5)Adbd收到命令后,执行如下操作:
— 调用socketpair()创建一对“equivalent sockets”;
— 将第一个socket附加到本地socket,本地socket本身又连接到远程socket
— 使用sendmsg()将第二个socket的文件描述符直接发送给JDWP进程

1
2
3
4
5
6
JDWP thread                             @jdwp-control
    |                                         |
    |                  <----------------------|
    |           OK, try this file descriptor  |
    |                                         |
    |                                         |

6)接着,JDWP线程使用这个新的socket描述符作为它与调试器的通信通道(接收JDWP-Handshake消息,应答调试器的消息等等)。
整个过程建立起来后,可以用下面这个图表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
            ____________________________________
           |                                    |
           |          ADB Server (host)         |
           |                                    |
Debugger <---> LocalSocket <----> RemoteSocket  |
           |                           ^^       |
           |___________________________||_______|
                                       ||
                             Transport ||
   (TCP for emulator - USB for device) ||
                                       ||
            ___________________________||_______
           |                           ||       |
           |          ADBD  (device)   ||       |
           |                           VV       |
 JDWP <======> LocalSocket <----> RemoteSocket  |
           |                                    |
           |____________________________________|

2、对Android调试模型的总结
对于如上内容,我要明确阐述的第一个点是,以上调试模型是针对Android VM的调试模型,这个模型的顶层形式如下图,其中,调试器通过JDWP协议与Android虚拟机进行通信。

而IDA对android的支持,是在整个进程的层面,并不局限于Android虚拟机。这其中的区别可以用如下方法简单区分,IDA能够支持对原生程序进程的调试,如linker、init进程等等,而基于JDWP协议的这一套原生调试框架并不支持。

归纳而言之,原生基于JDWP的调试框架与IDA的android调试框架,是两个层面上的调试实现,前者是虚拟机层面的实现,后者是整个进程层面的实现。理解这一点对理解Android逆向调试的原理至关重要。

六.逆向调试关键步骤解密

有了以上知识的铺垫,我们就可以开始研究第四章中提出的几个问题了。

 

1、DDMS展示的端口号,是怎么得来的?有没有更加便捷的方式获取这个端口号?
根据前一章的阐述,调试器通过执行“adb forward tcp:<hostport> jdwp:<pid>”命令将host机器上的hostport端口转发到Android上的调试进程,以便调试器通过这个端口连接到目标进程。所以这个hostport是调试器指定的,据此推测,DDMS便是通过这种转发方式获取到这个端口并展示到界面上。

因此,我们可以通过“adb forward tcp:<hostport> jdwp:<pid>”命令,替代使用DDMS的方式。

 

与问题1相关的进一步思考
当我们以调试模式启动应用时,应用会输出如下形式的日志信息

1
2
--------- beginning of system
03-08 10:01:05.709 6761 6761 W ActivityThread: Application com.outdoor.appcomvulbypassactivityperm is waiting for the debugger on port 8100...

日志信息表明应用在端口8100处等待调试连接,那是不是表明我们可以直接将hostport转发至Android 8100端口呢,尝试后你会发现不会成功。原因如下:

Android应用的调试模式有“dt_socket”和“dt_android_adb”两种,即通过socket或者adb连接到JDWP线程。所以理论上,通过将hostport转发到8100也是可以实现的。但进一步分析源码之后发现,Android虚拟机默认以“dt_android_adb”的模式启动,并且这个启动参数硬编码在源文件之中,不可手动配置的。

 

frameworks/base/core/jni/AndroidRuntime.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
601  int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
602  {
……
772 
773      /*
774       * Enable debugging only for apps forked from zygote.
775       * Set suspend=y to pause during VM init and use android ADB transport.
776       */
777      if (zygote) {
778        addOption("-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y");
779      }
 
……
995      if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
996          ALOGE("JNI_CreateJavaVM failed\n");
997          return -1;
998      }
999 
1000      return 0;
1001  }

所以直接转发8100端口会失败,因为Android VM的启动模式是“dt_android_adb”,JDWP线程也没有监听8100端口,这条日志给很多人带来了困扰。

 

最后,如果想要实现通过socket直接连接到JDWP线程,有没有办法呢?答案是有的。有两种可行的方式,一种是安装hook框架直接修改参数,但这种做法意义不大;另外一种是通过操作动态库的方式(dlopen、dlsym),修改调试启动参数,然后重启JDWP线程,这种适合实现生产环境下的远程调试。

 

2、Jdb connect命令是Java VM调试相关的命令,在SO调试中为什么要用它?它在和哪个模块进行通信?IDA附加上进程之后为什么不能直接开始调试?
在前面一章中,我们明确了通过原生JDWP的方式调试与使用IDA调试是两个层面的调试手段。使用Jdb的目的,是为了通知VM继续运行,因为应用以调试模式启动(以调试模式启动的原因是为了在程序早期下断点),启动之后一直在等待调试连接,Jdb通过“dt_android_adb”的方式,连接到了目标进程的VM,VM继续运行,这样IDA才可以在运行着的进程上进行调试。

归纳而言之,对JDWP调试手段的运用,是一种辅助手段,这种辅助手段使得我们能够在进程早期下断点。

 

3、Adb、android_server、IDA之间的关系是什么?IDA与应用进程之间的调试连接到底是如何建立的?IDA调试Android SO的技术原理是什么?
根据前两个小节的内容,我们知道了adb套件实际上是在辅助建立调试连接:
a)在JDWP调试连接中,提供“dt_android_adb”模式所需的通信功能;
b)在IDA调试连接中,提供端口转发、远程启动进程的手段。
而android_server、IDA之间的关系,基本上与原生调试模型保持着一致,它是IDA对android逆向调试的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
             ____________________________________
            |                                    |
            |     IDA (host)                     |
            |                                    |
            |                      RemoteSocket  |
            |                           ^^       |
            |___________________________||_______|
                                        ||
                              Transport ||
    (TCP for emulator - USB for device) ||
                                        ||
             ___________________________||_______
            |                           ||       |
            |   android_server (device) ||       |
            |                           VV       |
PROCESS <======>  ptrace <---->    RemoteSocket  |
            |                                    |
            |____________________________________|

android_server的作用类似于adbd,区别在于,adbd通过本地socket与JDWP进行通信,以转发调试信号;而android_server此处实际上是基于ptrace实现的一个调试器,它一端通过socket与IDA相连接,传输调试指令和数据,另一端通过ptrace直接操控调试进程。

七.总结

经过以上几个章节的阐述,我希望已经讲清楚我提出的那些问题,也希望这篇文章能够达到它的目的。如果您在阅读过程中有任何问题,既可以在评论区留言,也可以发送邮件至1054624587[@]qq[.]com与我进一步交流。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2021-3-9 21:50 被kxliping编辑 ,原因: 脱敏处理
收藏
点赞22
打赏
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  憨豆爆发   +2.00 2021/12/09 多谢大佬解决这么痛的痛点,DDMS每次近出些日脓包问题,一直没找到合适方案
最新回复 (21)
雪    币: 19
活跃值: (78)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
糟木头 2021-3-9 22:59
2
1
DDMD那个问题,解决了我很长时间以来的一个疑惑
雪    币: 226
活跃值: (1279)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hpphpp 2021-3-10 10:23
3
0
我理解adb forward tcp:<hostport> jdwp:<pid>” 这个端口是可以自己指定的
雪    币: 1337
活跃值: (2422)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
kxliping 2021-3-10 10:25
4
0
hpphpp 我理解adb forward tcp: jdwp:” 这个端口是可以自己指定的
是的,只要不和host上的其他端口号冲突,可以任意指定
雪    币: 1337
活跃值: (2422)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
kxliping 2021-3-10 10:25
5
0
糟木头 DDMD那个问题,解决了我很长时间以来的一个疑惑[em_63]
DDMS
雪    币: 331
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
思源欲涩 2021-3-12 22:46
6
0
感谢分享
雪    币: 3350
活跃值: (3372)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2021-3-12 22:50
7
0
感谢分享!
雪    币: 1636
活跃值: (653)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
琅環玉碎 2021-3-15 07:56
8
0
感谢分享
雪    币: 3125
活跃值: (578)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小hanger 2021-3-15 12:14
9
0
感谢分享
雪    币: 6
活跃值: (990)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
菜鸟也想飞 2021-3-15 15:43
10
0
感谢分享!
雪    币: 1438
活跃值: (1521)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
奋斗der小鸟 2021-3-17 09:51
11
0
糟木头 DDMD那个问题,解决了我很长时间以来的一个疑惑[em_63]
ddms 的源码工程是java工程,可以直接下载下来运行,而且可以单步调试。
雪    币: 5486
活跃值: (2336)
能力值: ( LV7,RANK:108 )
在线值:
发帖
回帖
粉丝
Bxb0 1 2021-3-17 20:53
12
0
感谢分享。
雪    币: 2576
活跃值: (4738)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
Thehepta 3 2021-3-20 11:16
13
0
老哥,你真能折腾,android studio ,完美的替代了ddms, jdwp 好像是java调试的协议,要不然为什么用jdk 调试啊
雪    币: 16
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mJqalJqN 2021-3-22 21:36
14
0
           ____________________________________
            |                                    |
            |     IDA (host)                     |
            |                                    |
            |                      RemoteSocket  |
            |                           ^^       |
            |___________________________||_______|
                                        ||
                              Transport ||
    (TCP for emulator - USB for device) ||
                                        ||
             ___________________________||_______
            |                           ||       |
            |   android_server (device) ||       |
            |                           VV       |
PROCESS <======>  ptrace <---->    RemoteSocket  |
            |                                    |
            |____________________________________| 
师傅,这个图怎么画的
雪    币: 1337
活跃值: (2422)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
kxliping 2021-3-23 14:28
15
0
mJqalJqN ____________________________________ | | ...
套用前面的模板画的
雪    币: 61
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_goxyneyf 2021-3-26 16:01
16
0
谢谢分享
雪    币: 1024
活跃值: (431)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yxylwt 2021-11-5 22:54
17
0
写的太好了
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
SnoopyT 2022-9-14 20:03
18
0
雪    币: 440
活跃值: (848)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
猫子 2023-9-4 16:20
19
0
mJqalJqN ____________________________________ | | ...
http://asciiflow.cn/ 可以试试这个网站。
雪    币: 52
活跃值: (506)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-9-5 21:54
20
0
如沐春风
雪    币: 246
活跃值: (398)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Jnj3369 2023-9-5 22:03
21
0
谢谢楼主!
雪    币: 19215
活跃值: (28834)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-9-6 09:29
22
1
感谢分享
游客
登录 | 注册 方可回帖
返回