[免费专栏] Android安全之利用GDB动态调试Android APP应用程序


也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大

少走了弯路,也就错过了风景,无论如何,感谢经历


转移发布平台通知:将不再在CSDN博客发布新文章,敬请移步知识星球

感谢大家一直以来对我CSDN博客的关注和支持,但是我决定不再在这里发布新文章了。为了给大家提供更好的服务和更深入的交流,我开设了一个知识星球,内部将会提供更深入、更实用的技术文章,这些文章将更有价值,并且能够帮助你更好地解决实际问题。期待你加入我的知识星球,让我们一起成长和进步


Android安全付费专栏长期更新,本篇最新内容请前往:

0x01 前言

由于 Android 原生程序的软件保护技术日趋成熟,很多软件和病毒都开始用加密和混淆技术强化自己,对此,静态分析已很难奏效,要用到动态调试

用 C、C++ 开发的原生程序,其语言的先天特性决定其二进制代码的分析难度比 Java 开发的 DEX 高得多,加上高强度的代码加密和混淆技术,逆向分析更加困难。因此,使用调试器配合脚本自动化技术,实现原生程序的自动化分析、自动化去除混淆与动态脱壳,已成为逆向分析中必须掌握的技能

1.1 GDB远程调试原理图

在这里插入图片描述

如图上所示,我们需要使用gdbserver依附到要调试的进程上,gdb通过adbd和手机上的gdbserver 进行socket通信

1.2 什么是 GDB 调试器?

Android NDK 早期只支持用 gcc 开发原生程序(现在只支持 Clang),那时原生程序的调试器主要是 gdb(The GNU Project Debugger,GNU 工程调试器),即使是现在,用 gdb 调试原生程序也是一种选择

GDB是gnu组织开发的一个强大的unix程序调试工具,可以用它来调试Android上的C、C++代码

GDB主要做四件事情

  • 随心所欲地启动你的程序
  • 设置断点,程序执行到断点处会停住(断点可以是表达式)
  • 程序被停住后,可以查看此时程序中发生的事
  • 动态改变程序的执行环境

1.3 Android SDK GDB 安装路径

使用gdb进行嵌入式调试的必需品,是gdb和gdbserver二进制文件,对于Android平台而言,Google已经提供了预编译的版本,所以无需自行编译它(非要自己手动去编译的,麻烦的一批,反正我放弃了),你可以在Android SDK的目录下找到它们(高版本已经被舍弃了,版本不要超过Android NDK r23 LTS)

网上的一些教程是说通过Android Studio SDK Tools来安装,但默认会安装最新版本的,我们这里手动安装即可

在这里插入图片描述
在这里插入图片描述

  • windows 安装NDK-gdb(版本不要超过Android NDK r23 LTS(2021 年 8 月),在这个版本以上的默认去除了对GDB的支持)

https://developer.android.com/ndk/downloads/revision_history?hl=zh-cn

在这里插入图片描述
在这里插入图片描述

  • Android SDK 工具包的gdb和不同系统版本下的gdbserver路径
android_ndk_r23b/prebuilt/windows-x86_64/bin/gdb.exe

android_ndk_r23b/prebuilt/android-x86_64/gdbserver/gdbserver
android_ndk_r23b/prebuilt/android-x86/gdbserver/gdbserve
android_ndk_r23b/prebuilt/android-arm64/gdbserver/gdbserve
android_ndk_r23b/prebuilt/android-arm/gdbserver/gdbserve

:同时对Android手机需要做的准备工作,则是打开开发者选项以启用ADB调试,并且需要具有root权限。同时在某些机型上,SELinux可能会阻止gdbserver附加到目标进程,这种情况下建议暂时将SELinux改为宽容模式:

adb shell setenforce 0

1.4 NDK-GDB 脚本

在 Android NDK 目录下有个 ndk-gdb 脚本,它是由 Android NDK 提供且经过配置的 gdb 调试器的启动器,内容如下:

在这里插入图片描述

  • ndk-gdb 对应 ndk-gdb.py,ndk-gdb.py 主要执行了如下操作:

    • 解析 Android NDK 工程的工程信息。读取项目的 AndroidManifest.xml,解析原生程序的包名和由 APK 启动的 Activity
    • 解析 Android NDK 工程的 ABI 信息。读取原生程序支持的 ABI,将其与当前连接到计算机中的设备的 ABI 进行比对,找出适合当前设备的 ABI 版本的原生程序
    • 设置调试器的 stl_pretty_printer。当 APP_STL 为 stlport 或 gnustl 时,为 gdb 设置不同的 stl_pretty_printer
    • 获取已安装的原生程序在设备上的数据目录和 gdbserver 的位置
    • 以调试模式启动原生程序所在 APK 的主 Activity
    • 从设备上拉取(pull)一些调试中要用的系统动态库和原生程序
    • 启动 gdbserver
    • 启动 gdb,连接待调试的进程,开始调试
  • 从以上步骤可看出,用 ndk-gdb 动态调试原生程序时有如下限制:

    • 独立的原生程序无法调试。原生程序必须和 APK “绑定”,且 APK 必须包含主 Activity
    • 包含原生程序的 APK 必须是可调试的,且要先安装到设备上
    • 无法便携设置 gdb 调试器前端

0x02 GDB 调试器命令

  • gdb 尚在不断更新(变化),要想掌握其使用方法,最好的办法是去 gdb 官网查看其手册(gdb 用户手册)
  • gdb 调试器成功连接 gdbserver 后,会进入以 (gdb) 显示的 Shell 环境,在这里,既可执行 gdb 提供的所有命令,以驱动 gdb 调试 Android 原生程序,也可执行 help 命令,查看所有可用的命令。在调试场景中按 Tab 键,gdb 会显示当前所有可用命令

2.1 断点(BreakPoint)

在代码的指定位置中断,这个是我们用得最多的一种。设置断点的命令是break,它通常有如下方式:

  • break <function> 在进入指定函数时停住
  • break <linenum> 在指定行号停住。
  • break +/-offset 在当前行号的前面或后面的offset行停住。offiset为自然数。
  • break filename:linenum 在源文件filename的linenum行处停住。
  • break ... if <condition> ... 可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break if i=100,表示当i为100时停住程序

可以通过info breakpoints [n] 命令查看当前断点信息。此外,还有如下几个配套的常用命令:

  • delete 删除所有断点
  • delete breakpoint [n] 删除某个断点
  • disable breakpoint [n] 禁用某个断点
  • enable breakpoint [n] 使能某个断点

2.2 观察点(WatchPoint)

在变量读、写或变化时中断,这类方式常用来定位bug

  • watch <expr> 变量发生变化时中断
  • rwatch <expr> 变量被读时中断
  • awatch <expr> 变量值被读或被写时中断

可以通过info watchpoints [n] 命令查看当前观察点信息

2.3 捕捉点(CatchPoint)

捕捉点用来补捉程序运行时的一些事件。如:载入共享库(动态链接库)、C++的异常等。通常也是用来定位bug

捕捉点的命令格式是:catch <event>,event 可以是下面的内容

  • throw C++抛出的异常时中断
  • catch C++捕捉到的异常时中断
  • exec 调用系统调用exec时(只在某些操作系统下有用)
  • fork 调用系统调用fork时(只在某些操作系统下有用)
  • vfork 调用系统调用vfork时(只在某些操作系统下有用)
  • load 或 load <libname> 载入共享库时(只在某些操作系统下有用)
  • unload 或 unload <libname> 卸载共享库时(只在某些操作系统下有用)

另外,还有一个tcatch ,功能类似,不过它只设置一次捕捉点,当程序停住以后,应点被自动删除。捕捉点信息的查看方式和代码断点的命令是一样的

2.4 在特定线程中中断

你可以定义你的断点是否在所有的线程上,或是在某个特定的线程。GDB很容易帮你完成这一工作

  • break <linespec> thread <threadno>
  • break <linespec> thread <threadno> if ...

linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID,注意,这个ID是GDB分配的,你可以通过"info threads"命令来查看正在运行程序中的线程信息。如果你不指定thread <threadno> 则表示你的断点设在所有线程上面。还可以为某线程指定断点条件,如:

(gdb) break frik.c:13 thread 28 if bartab > lim

当你的程序被GDB停住时,所有的运行线程都会被停住。这方便你你查看运行程序的总体情况。而在你恢复程序运行时,所有的线程也会被恢复运行。那怕是主进程在被单步调试时。

2.5 恢复程序运行和单步调试

在gdb中,和调试步进相关的命令主要有如下几条:

  • continue :继续运行程序直到下一个断点(类似于VS里的F5)
  • next :逐过程步进,不会进入子函数(类似VS里的F10)
  • setp :逐语句步进,会进入子函数(类似VS里的F11)
  • until :运行至当前语句块结束
  • finish :运行至函数结束并跳出,并打印函数的返回值(类似VS的Shift+F11)

2.6 常用的GDB命令

  • 常用的 gdb 命令(按功能区分):
    • 与断点相关的命令
      • break:如break printf 表示在printf() 上设置断点
      • rbreak:用正则表达式的方式设置断点
      • tbreak:设置临时断点
      • delete:如delete n 表示删除第 n 个断点
      • disable:如disable n 表示禁用第 n 个断点
      • enable:如enable n 表示启用第 n 个断点
      • save:如save breakpoints 表示将当前断点信息保存为脚本文件
      • info:如info break 用于查看所有断点信息
      • watch:设置监视点(需要硬件支持)。在没有源码的情况下,可对内存地址进行监视,功能类似 OllyDbg(Windows 平台调试器)的硬件读写断点
      • catch:如catch syscall 用于捕获所有系统调用
    • 与调试相关的命令
      • stepi:单步步入。当遇到函数调用时,可进入此函数体
      • nexti:单步步过。当遇到函数调用时,不进入此函数体
      • until:快速退出循环
      • finish:执行程序,直到当前函数执行结束后返回
      • continue:继续运行被断点中断的程序
      • run:运行程序
      • kill:强行终止当前正在调试的程序
      • quit:退出 gdb
    • 与寄存器相关的命令
      • info:如info registers、info all-registers 可用于查看所有寄存器的信息;info registers x0 可用于查看 x0 寄存器的值
      • print:如print/x $pc 表示以十六进制形式显示 pc 寄存器的值
      • set:如set $sp += 4 用于将栈指针的值加4;set $x0 = 0 用于将x0 寄存器的值设为 0
    • 与堆栈相关的命令
      • info:如info frame 用于打印当前栈帧的信息
      • backtrace:打印所有栈帧的信息
      • down:选择并打印下一个栈帧的信息
      • up:选择并打印上一个栈帧的信息
      • frame:选择并打印指定栈帧的信息
      • return:将所选栈帧返回调用者
    • 与数据相关的命令
      • append:如append memory 用于将内存中的数据添加到指定文件的最后
      • call:调用一个函数,功能类似 IDA 的 AppCall
      • disassemble:反汇编当前栈帧的函数
      • display:每次程序停止运行时打印表达式的值
      • dump:读取内存中的数据和代码,并将其保存到文件中。如dump binary memory 用于将指定内存中的数据保存到文件中
      • explore:打印表达式的值(gdb 要能识别它的类型)
      • find:在内存中查找数据
      • print:打印表达式的值
      • printf:不仅和 print 一样可用于打印表达式的值,还可用于指定格式化字符串
      • set:修改表达式的值

0x03 GDB调试实例

1)在手机或模拟机启动gdbserver并attach想调试的进程,并指定监听调试命令的端口

# 注意这里的路径,第一个需在当前目录下,否则需带绝对路径,第二个可自己选择
adb push gdbserver /system/bin 

:push到/system/bin可能会报错,需确保手机或模拟机已经root,输入命令: adb remount,然后再push进去,同样,下面的chmod权限赋予如果报错,可以使用su命令

  • push到/system/bin可能会报错,赋予gdbserver权限
adb shell
su
chmod 777 /system/bin

或使用mount命令查看,然后更改权限(将/system 从挂载“ro”权限变为挂载“rw”权限)

mount
mount -o rw,remount /dev/block/sda6 /system

在这里插入图片描述

  • 有可能上传的时候又会出现如下错误,使用如下命令:
adb remount
adb push gdbserver /system/bin 

在这里插入图片描述

2)使用adb做端口映射,将pc机上的端口定向到手机上gdbserver监听的端口

adb forward tcp:54321 tcp:54321   #端口映射,将pc机的1234端口映射到手机的1234端口
$ adb shell
# ps   #查看要调试进程的PID

在这里插入图片描述

3)gdbserver附加进程方式

在手机上启动gdbserver并附加到目标进程

./gdbserver :54321 --attach $(pidof process_name)
  • 在PC上启动gdb
$ gdb
(gdb) target remote 127.0.0.1:54321

4)自行启动进程方式

  • 有时因为某些原因,需要自行启动一个进程进行调试,而不是直接附加到现有进程,参数就直接附加在后面,就像正常启动进程一样
./gdbserver :54321 elf_name[文件名]  params[参数]

5)在PC上启动gdb的方法是相同的

$ gdb
(gdb) target remote 127.0.0.1:54321

6)启动gdb向指定的pc机端口发信息开始调试,运行gdbserver 两种方法

方法1:adb shell; 命令: cd gdbserver路径; 命令: ./gdbserver

./gdbserver :54321 --attach 1419   #:54321是端口号,1419 是进程ID

方法2: 命令 adb shell gdbserver (适用于将gdbserver放在/system/bin下的情况)

adb shell gdbserver :23946 –attach [PID](PID可以在adb shell中使用 ps命令查询)

我们这里选择方法1:

adb shell
cd /system/bin
chmod 777 gdbserver
./gdbserver gdbserver :54321 –attach 1419

:要使用项目下的gdb客户端去连接gdbserver,gdb的类型要选择针对手机或模拟器平台的,版本要和gdbserver一致

  • gdb客户端执行远程链接,成功监听如下所示:
$ gdb
(gdb) target remote 127.0.0.1:54321

在这里插入图片描述

  • 列出当前进程的所有寄存器
info registers

在这里插入图片描述

3.1 设置断点

break 命令设置断点,简写b 
break main   在main()函数的入口处设置断点
break 5      在源代码的第5行设置断点
break hello.c:5  指定源码文件的代码第5行设置断点

单个断点:

b+设置断点所在行:设置断点
b+函数名:函数名处设定断点
b+文件名:行号/函数名:在别的函数设置断点

无效断点:对于函数中的,大括号和注释,设置的断点是无效的。看断点是否有效,看Enb这个项(如下图),y就是有效。其中Num为断点的标号

在这里插入图片描述

  • 在main()函数的入口处设置断点
b main

在这里插入图片描述

条件断点:b+ 行号+if 变量==var :当某行的变量等于某个值的时候停止

b 19 if i==5

3.2 查看断点

info breakpoints ,显示当前全部的断点,简写为i b

i b

在这里插入图片描述

3.3 清除断点

单个删除:每个断点号用空格隔开;delete + 断点的数值标识符,delete 1 ,删除第1个断点,简写为d 1

d 1

在这里插入图片描述

连续删除:断点号范围表示,例如4-7;d+ 断点号范围

d 4-7

clear + 函数名 、 +行号、+文件名:行号 ,清除断点main()函数处的断点:clear main 或者clear 5 (本质是main函数的第一条语句所在)

在这里插入图片描述

3.4 启用与禁用断点

  • disable + 断点的数值标识符,disable 1: 禁用第1个断点,简写为dis+ 断点号
  • enable + 断点的数值标识符,enable 1: 启用第1个断点,简写为enb+ 断点号
  • Enb字段,表明断点是禁用(n)还是启用(y)的

在这里插入图片描述

3.5 单步调试

  • next,n ,越过 函数调用(函数会在背地里自己悄悄运行完),单步执行
  • step,s ,进入 函数体内部,单步执行

3.6 恢复执行

  • continue ,c,恢复执行,直到遇到下一个断点
  • continue 命令执行期间,按下CTRL-C瞬间停止

3.7 查看变量

  • disp,使得每次有暂停,都会输出指定的变量的值;
  • print,p,只显示一次变量的值;
  • 要求变量名在当前的域是可见的,比如某个变量i是函数foo()的局部变量,那么只有是在进入到这个函数的里面时才可以使用print i 或者 disp i,不然gdb也不知道i是谁;

3.8 TUI模式,双开 汇编代码 窗口

  • Ctrl + X + A 进入TUI模式
  • (gdb) list :显示10行C源码
  • (gdb) layout split :同时显示C源码以及汇编源码
  • (gdb) info registers :显示使用到的寄存器信息
  • (gdb) set disassembly-flavor intel :改变显示的汇编语法
  • 再次输入(gdb) layout split :使语法改变生效
set disassembly-flavor intel
set disassembly-flavor att

3.9 查看文件信息

查看当前文件:

  • l :查看当前文件的调试信息,默认10行
  • l+ 行号:显示特定行号的上下文
  • l + 函数名:查看当前文件特定函数

查看当前文件:

  • l + 文件名:行号:查看文件信息
  • l + 文件名:函数名:查看特定函数的信息

设置显示行数:

  • show listsize :显示输出行
  • set listsize (number) :设置输出行号

参考链接

http://beej.us/guide/bggdb/

https://blog.csdn.net/zlmm741/article/details/105511833

https://blog.csdn.net/weixin_46185705/article/details/114498377

https://blog.csdn.net/weixin_46185705/article/details/114498377

https://www.cnblogs.com/TianFang/archive/2013/01/20/2868889.html

https://www.jianshu.com/p/8a5aade09ec0?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation


你以为你有很多路可以选择,其实你只有一条路可以走


  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橙留香Park

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值