当前位置:网站首页>[免费专栏] Android安全之动态代码注入技术(利用JDB调试APK)

[免费专栏] Android安全之动态代码注入技术(利用JDB调试APK)

2022-08-09 17:59:00 菠萝_橙留香


欢迎新同学的光临
… …
人若无名,便可专心练剑


我不是一条咸鱼,而是一条死鱼啊!


0x01 前言

Android动态代码注入即是不修改源程序只修改进程的寄存器、内存值等就能控制程序实现既定目标的一种方法。动态代码注入技术本质上就是一种调度技术,动态代码注入相比于普通的调试,最大的区别就是动态代码注入是一个“自动化调试并达到加载自定义动态链接库”的过程。所谓自动化,其实就是通过代码实现,在Linux上通过Ptrace就可以完成查看变量值、修改变量值、跟踪进程跳转、查看进程调试堆栈等待所有功能,当然,Ptrace功能是比较原始的,平时调试中的功能还需要很多高层逻辑封装才可以实现。一般而言,我们要对一个进程进行动态注入,主要有以下几个方便目的:增强目标进程的功能、修复目标进程缺陷、劫持目标进程函数、窃取目标进程数据、篡改目标进程数据

在这里插入图片描述

如上图所示,进程A注入到进程B后,通过更改寄存器和内存,让进程B加载自定义的动态链接库a,当a被加载后,a会尝试加载其他模块,比如加载dex文件等等,具体的注入过程如下:

?ATTATCH,指定目标进程,开始调试;

?GETREGS,获取目标进程的寄存器,保存现场;

?SETREGS,修改PC等相关寄存器,使其指向mmap;

?POPETEXT,把so path写入mmap申请的地址空间;

?SETRESG,修改PC等相关寄存器,使其指向dlopen;

?SETREGS,恢复现场;

?DETACH,解除调试,使其恢复;

1.1 JDB是什么?

Java调试器(JDB)是Java类在命令行中调试程序的工具。 它实现了Java平台调试器体系结构。 它有助于使用Java调试接口(JDI)检测和修复Java程序中的错误

JDB是一个简单的Java命令行调试器,包含在JDK中,使用JDB命令可以分析简单的Java程序。通俗的来说就是Java中用来调试Java类在命令行中调试程序用的工具

1.2 JDWP 协议

JDWP的全写是:Java Debug Wire Protocol:即JAVA调试器无线协议,它定义了调试器(Debugger)和被调试的JAVA虚拟机(target vm)之间的通信协议。

在这里,我更要说明下:Debugger与Target vm,Target vm 中运行着我们希望要调试的程序,它与一般运行的Java虚拟机没有什么区别,只是在启动时加载了Agent JDWP 从而具备了调试功能。而debugger 就是我们熟知的调试器,它向运行中的target vm 发送命令来获取target vm 运行时的状态和控制Java 程序的执行。

Debugger 和target vm 分别在各自的进程中运行,他们之间的通信协议就是JDWP。JDWP 与其他许多协议不同,它仅仅定义了数据传输的格式,但并没有指定具体的传输方式。这就意味着一个JDWP 的实现可以不需要做任何修改就正常工作在不同的传输方式上(在JDWP 传输接口中会做详细介绍)。JDWP 是语言无关的。

理论上我们可以选用任意语言实现JDWP。然而我们注意到,在JDWP 的两端分别是target vm 和debugger。Target vm 端,JDWP 模块必须以Agent library 的形式在Java 虚拟机启动时加载,并且它必须通过Java 虚拟机提供的JVMTI 接口实现各种debug 的功能,所以必须使用C/C++ 语言编写。而debugger 端就没有这样的限制,可以使用任意语言编写,只要遵守JDWP 规范即可。JDI(Java Debug Interface)就包含了一个Java 的JDWP debugger 端的实现,JDK 中调试工具jdb 也是使用JDI 完成其调试功能的

0x02 jdb动态分析java应用程序

  • 创建Debug.java 文件
public class Debug {
    
    public static void paswdCheck(String secret){
    
        String pasword = secret;
        System.out.println("Enter Password:"+pasword);
    }

    public  static  void test(){
    
        System.out.println("This is a test program");
        System.out.println("----------------------");
    }

    public static  void  main(String args[]){
    
        System.out.println("This is a test program");
        test();
        paswdCheck("Orangey_0x0");
    }

}

打开cmd 执行javac Debug.java 生成对应的class文件,然后运行java Debug 看看是否是我们想要的预期结果(paswdCheck、test方法从Debug类的main方法调用,输入如下内容)

在这里插入图片描述

但为了方便调试,此处我们要额外选择"-g" 选项编译Java程序,以便我们在生成的类中获得一些额外的调试信息

2.1 运行 JDB调试java程序步骤

要调试Java程序,需要一条JDB到JVM的通信信道,因为Java 程序实际上是运行在JVM(java虚拟机)中的,如下所示,有两种连接JDB和JVM的方法

  • 方法1

直接使用JDB来加载类文件,JDB会自动创建一个JAVA 虚拟机,并建立连接

在这里插入图片描述

:Debug是源代码编译后生成的类文件

  • 方法2

使用java -Xdebug -Xrunjdwp:transport=dt_socket, server=y,address=54321 Debug 命令启动一个Java虚拟机,Java虚拟机会监听54321端口

在这里插入图片描述

然后使用以下命令启动 JDB 以连接到端口 54321 上的 JVM

jdb -attach 54321

在这里插入图片描述

第二种方法也可以用来进行远程调试

  • 现在开始用第一种方法来调试示例程序,执行一条run命令来让JDB启动Java虚拟机,如下图:
jdb  Debug

在这里插入图片描述

  • 上图显示JVM已经启动,程序立刻执行完成并退出了。为了中断程序执行以便手工单步调试,需要在程序运行之前设置断点。用stop in 命令在方法开始的地方设置断点,如下图:
stop in Debug.main(java.lang.String[])

在这里插入图片描述

  • 可以看到我们在Debug类的main方法中设置了一个断点,还指定了参数的类型,现在我们来启动 JVM 并运行程序以达到断点,使用run命令运行程序来触发断点

在这里插入图片描述

  • 触发断点后,JDB会自动显示将要执行的下一行代码:
System.out.println("This is a test program");
  • 可以使用list 命令来查看当前的上下文代码,如下所示:

在这里插入图片描述

  • 使用clear 命令查看设置的所有断点,如下所示。正如我们设置的那样,它显示了我们设置断点的位置:

在这里插入图片描述

  • 使用threadgroups 命令查看所有的线程组,正如我们所见,有两个线程组可用:system main ,如下所示:

在这里插入图片描述

  • 使用threads 查看所有线程,如下所示:

在这里插入图片描述

  • 如上图,当前的system线程组中有三个线程,而main线程组中有一线程,这就是将要调试的线程

使用classes 命令查看当前Java虚拟机所加载的类的信息,如下所示:

在这里插入图片描述

  • 上图中显示了当前Java虚拟机所加载的全部类中的部分类,要查看特定类的更加详细的信息,可以使用class 命令,下图显示了Debug类的详细信息:
class <classname>

在这里插入图片描述

  • 同样,也能查看其它类的详细信息,例如下图就显示了java.io.File类的详细信息:

在这里插入图片描述

使用methods <classname> 命令查看所加载的方法,如下所示:

在这里插入图片描述

  • 以上介绍了JDB调试的常用命令,现在将深入程序的执行流程,体验怎样用JDB来帮助调试程序。可以使用“next”命令执行下一行代码:

执行下一行,即System.out.println("This is a test program"); 可以输入命令“next”,如下所示

在这里插入图片描述

  • 执行完当前代码后,JDB会自动显示下一行代码:调用test方法。这里,再执行“next”命令后,会执行完test方法并中断到下一行代码:paswdCheck方法,如下所示:

在这里插入图片描述

  • 现在等待执行下一行paswdCheck("Orangey_0x0") ,想进入passCheck方法进行调试,就使用step 命令。在test方法处输入step命令,如下所示:

在这里插入图片描述

  • 输入next 命令继续运行下一条代码,如下所示:

在这里插入图片描述

  • 如果想直接离开该方法,而不是运行余下的代码,可以使用step up 命令,如下所示:

在这里插入图片描述

  • 当前代码已经离开test方法,回到main方法,等待执行下一条。where 命令会打印显示当前的调用堆栈,如下所示:

在这里插入图片描述

  • 如上所见,目前在方法Debug.main中,当前程序正在main方法中,现在使用step 命令进入方法,并检查调用堆栈,如下所示:

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

  • 如上图,程序当前正在Debug.paswdCheck 中运行,而Debug.paswdCheck 又是被Debug.main 调用的。假如想查看paswdCheck方法的局部变量信息,可以使用locals 命令查看所有的局部变量(如果代码不是使用“-g”选项编译的,这不起作用)

在这里插入图片描述

  • 如上图所示,该方法接收了一个main方法传入的密码变量,因为参数还没有被赋值给局部变量,所只显示了参数而没有局部变量,先执行下一行代码再查看局部变量password ,可以使用print 命令打印出指定变量的内容,如下所示:
print secret

在这里插入图片描述

如上介绍了 jdb 的基础知识、各种 jdb 命令以及如何将它们应用于调试 Java 应用程序,接下来我们将介绍如何利用jdb调试APK… …


0x03 JDB动态分析(调试)Android实例应用程序

  • 反编译【本次使用的案例APK,已是android:debuggable="true"

反编译APK,然后查看AndroidManifest.xml中是否有android:debuggable="true" ,没有的话则在相应位置添加

  • 启动模拟器

启动模拟器或连接真机,然后安装apk –> adb install AliCrackme_1.apk

  • 打开刚安装的测试应用,然后打开终端,输入如下命令,查看Dalvik虚拟机监听,会显示所有可以连接并调试的端口,如下所示:

方法1: 可以使用DDMS,直接打开DDMS,如下图箭头所指:

在这里插入图片描述

方法2: adb jdwp ,然后再打开应用(如果之前打开过apk,需要杀死进程),再执行一次命令 adb jdwp,比较两者多出来的数字即是PID

adb jdwp

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

切记,此处一定要打开DDMS(此步不是必须的,这步的工作其实相当于手动敲:adb -d forward tcp:8700 jdwp:$PID ,其中的$PID 为要调程序的进程号)

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

  • 需要用JDB去连接该端口,在连接之前,需要使用adb来转发端口,且用JDB连接调试android应用。重定向步端口

方法1:

adb forward tcp:8700 jdwp:pid
jdb -attach localhost:8700

:如果打开了DDMS,转发adb forward tcp:54321 jdwp:1234 这步就不用了

1.使用adb转发端口: adb forward tcp:54321 jdwp:1234(注解:这里的54321可任意,但尽量避免端口冲突,这里的1234为第三步获取的PID)
2.jdb连接: jdb -attach localhost:54321(注解:这里的端口注意与上面对应)

方法2:

jdb –connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

:如果自己的机器是使用共享内存(shared memory),那么请使用方法2,否则会报错,如果不是,可以使用方法1或者方法2

在这里插入图片描述

jdb 连接上后,可以看到小虫子变绿色了;如果小虫子是红的,那代表映射的端口被占用

在这里插入图片描述

:还有网上说的一些报错啥的,其实只要按照正常的步骤去做,都是没有问题,很大可能都是你端口被占用,不一定是Android Studio打开的原因,可能是你自己做了多个adb forward 的映射影响有问题,重启adb 服务即可,如下所示:

# 停止adb服务
adb kill-server
# 开启adb服务
adb start-server
  • 下面就实现在应用运行时通过远程代码注入的方式改变变量值达到测试远程代码注入方法的目的。为此,需要设置断点来控制程序的执行流程。但之前并不知道该应用所使用的类和方法。可以使用classes 命令来查看类和方法

在这里插入图片描述

:使用classes命令查看所有的类,这里会打印出相当多的类名,可以结合DDMS找到需要类名
当然直接反编译后可查看相当多的类名,具体可使用class 类名或ID 进行查看

  • 查看一下jdp的命令有哪些:help 一下
$ jdb –connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
正在初始化jdb...
> help
** 命令列表 **
connectors                -- 列出此 VM 中可用的连接器和传输

run [class [args]]        -- 开始执行应用程序的主类

threads [threadgroup]     -- 列出线程
thread <thread id>        -- 设置默认线程
suspend [thread id(s)]    -- 挂起线程 (默认值: all)
resume [thread id(s)]     -- 恢复线程 (默认值: all)
where [<thread id> | all] -- 转储线程的堆栈
wherei [<thread id> | all]-- 转储线程的堆栈, 以及 pc 信息
up [n frames]             -- 上移线程的堆栈
down [n frames]           -- 下移线程的堆栈
kill <thread id> <expr>   -- 终止具有给定的异常错误对象的线程
interrupt <thread id>     -- 中断线程

print <expr>              -- 输出表达式的值
dump <expr>               -- 输出所有对象信息
eval <expr>               -- 对表达式求值 (与 print 相同)
set <lvalue> = <expr>     -- 向字段/变量/数组元素分配新值
locals                    -- 输出当前堆栈帧中的所有本地变量

classes                   -- 列出当前已知的类
class <class id>          -- 显示已命名类的详细资料
methods <class id>        -- 列出类的方法
fields <class id>         -- 列出类的字段

threadgroups              -- 列出线程组
threadgroup <name>        -- 设置当前线程组

stop in <class id>.<method>[(argument_type,...)]
                          -- 在方法中设置断点
stop at <class id>:<line> -- 在行中设置断点
clear <class id>.<method>[(argument_type,...)]
                          -- 清除方法中的断点
clear <class id>:<line>   -- 清除行中的断点
clear                     -- 列出断点
catch [uncaught|caught|all] <class id>|<class pattern>
                          -- 出现指定的异常错误时中断
ignore [uncaught|caught|all] <class id>|<class pattern>
                          -- 对于指定的异常错误, 取消 'catch'
watch [access|all] <class id>.<field name>
                          -- 监视对字段的访问/修改
unwatch [access|all] <class id>.<field name>
                          -- 停止监视对字段的访问/修改
trace [go] methods [thread]
                          -- 跟踪方法进入和退出。
                          -- 除非指定 'go', 否则挂起所有线程
trace [go] method exit | exits [thread]
                          -- 跟踪当前方法的退出, 或者所有方法的退出
                          -- 除非指定 'go', 否则挂起所有线程
untrace [methods]         -- 停止跟踪方法进入和/或退出
step                      -- 执行当前行
step up                   -- 一直执行, 直到当前方法返回到其调用方
stepi                     -- 执行当前指令
下一步                      -- 步进一行 (步过调用)
cont                      -- 从断点处继续执行

list [line number|method] -- 输出源代码
use (或 sourcepath) [source file path]
                          -- 显示或更改源路径
exclude [<class pattern>, ... | "none"]
                          -- 对于指定的类, 不报告步骤或方法事件
classpath                 -- 从目标 VM 输出类路径信息

monitor <command>         -- 每次程序停止时执行命令
monitor                   -- 列出监视器
unmonitor <monitor#>      -- 删除监视器
read <filename>           -- 读取并执行命令文件

lock <expr>               -- 输出对象的锁信息
threadlocks [thread id]   -- 输出线程的锁信息

pop                       -- 通过当前帧出栈, 且包含当前帧
reenter                   -- 与 pop 相同, 但重新进入当前帧
redefine <class id> <class file name>
                          -- 重新定义类的代码

disablegc <expr>          -- 禁止对象的垃圾收集
enablegc <expr>           -- 允许对象的垃圾收集

!!                        -- 重复执行最后一个命令
<n> <command>             -- 将命令重复执行 n 次
# <command>               -- 放弃 (无操作)
help (?)               -- 列出命令
version                   -- 输出版本信息
exit (或 quit)            -- 退出调试器

<class id>: 带有程序包限定符的完整类名
<class pattern>: 带有前导或尾随通配符 ('*') 的类名
<thread id>: 'threads' 命令中报告的线程编号
<expr>: Java(TM) 编程语言表达式。
支持大多数常见语法。

可以将启动命令置于 "jdb.ini"".jdbrc" 中
位于 user.home 或 user.dir 中
  • 查看方法
methods com.example.simpleencryption.MainActivity$1

在这里插入图片描述

  • 代码行方式设置断点(选择我们自己感兴趣的地方,即关键的地方进行断点设置)
stop at com.example.simpleencryption.MainActivity:100
# 设置断点
stop in android.app.ContextImpl.getSystemService   # 方法名断点
stop in com.example.debug.MainActivity$1.onClick(android.view.View)
stop at com.xxx.app.MainActivity:42          # 代码行断点

  • 接下来就是在app中手动输入文字后点击按钮,运行到断点处,然后使用next执行下一句

在这里插入图片描述

  • 运行到断点处,然后使用next执行下一句

在这里插入图片描述

  • 查看调用栈
where

在这里插入图片描述

  • 在栈间移动, up移到上一级调用或使用down移动下一级调用
up    上移线程的堆栈
down  下移线程的堆栈

在这里插入图片描述

  • locals 输出当前堆栈帧中的所有本地变量
locals

在这里插入图片描述

  • 我们之前破解的案例里面已经知道了这个密码,接下来我们之间更改这个变量的值,set把这个变量给修改了

在这里插入图片描述

  • 打印变量
print `基本类型`
dump `对象`

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

  • 单步进入
step

在这里插入图片描述

:触发断点后,如果执行一次next命令没断下来或者使用locals没查看到变量,可以尝试多执行几下next命令或者step命令

  • 继续运行
cont

在这里插入图片描述

上面介绍了使用JDB做简单的动态代码注入方法,其实使用JDB注入代码在Android中不算是真正意义上的动态代码注入,也是没有多大实际意义的方法,因为这种方法只是一种简单的手动动态代码注入方式,不说效率问题,但是实际操作意义就不大。在实际Android 应用中运用最多的其实是一种来源于Linux系统中的调试方法——HOOK,就是通过一定的方法改变API的执行结果的技术,在Android 系统中就是在Native层面对软件的运行库so进行HOOK,从而达到既定目的。Hook的前提是进程注入,而在Android系统中来源于Linux 的最便捷的进程注入手段就是ptrace,这是常用调试工具GDB的关键技术。ptrace函数是调试程序所用,功能强大,不仅可以附加某一进程(PTRACE_ATTACH),而且可以任意修改目标进程的内存空间(PTRACE_PEEKDATA,读内存。PTRACE_POKEDATA,写内存),甚至是寄存器(PTRACE_SETREGS,PTRACE_GETREGS),基本流程是利用寄存器指令中断:

  • PTRACE_ATTACH,绑定目标进程
  • PTRACE_GETREGS,获取目标进程寄存器状态,并保存
  • PTRACE_PEEKDATA与PTRACE_POKEDATA配合,保存原代码,写入要注入的代码到当前运行位置
  • PTRACE_SETREGS,恢复寄存器状态,并继续执行,这是注入的代码开始在目标进程内执行,注入代码完成HOOK,过程与Windows下相似
  • 在HOOK完成后,注入的代码执行int3被ptrace捕获,目标进程再次暂停执行
  • PTRACE_GETREGS,再次保存寄存器
  • PTRACE_PEEKDATA与PTRACE_POKEDATA配合还原代码
  • PTRACE_SETREGS,恢复寄存器,目标进程继续执行
  • PTRACE_DETACH,撤销绑定目标进程

备注APK动态注入动态链接库 后续文章再更新… …

参考链接

http://www.tasfa.cn/index.php/2016/06/01/android-re-gdb

https://blog.csdn.net/weixin_30485799/article/details/96760631


我自横刀向天笑,去留肝胆两昆仑


原网站

版权声明
本文为[菠萝_橙留香]所创,转载请带上原文链接,感谢
https://orangey.blog.csdn.net/article/details/126219947