欢迎新同学的光临
… …
人若无名,便可专心练剑
我不是一条咸鱼,而是一条死鱼啊!
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,解除调试,使其恢复;
Java调试器(JDB)是Java类在命令行中调试程序的工具。 它实现了Java平台调试器体系结构。 它有助于使用Java调试接口(JDI)检测和修复Java程序中的错误
JDB是一个简单的Java命令行调试器,包含在JDK中,使用JDB命令可以分析简单的Java程序。通俗的来说就是Java中用来调试Java类在命令行中调试程序用的工具
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 完成其调试功能的
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程序,以便我们在生成的类中获得一些额外的调试信息
要调试Java程序,需要一条JDB到JVM的通信信道,因为Java 程序实际上是运行在JVM(java虚拟机)中的,如下所示,有两种连接JDB和JVM的方法
直接使用JDB来加载类文件,JDB会自动创建一个JAVA 虚拟机,并建立连接

注:Debug是源代码编译后生成的类文件
使用java -Xdebug -Xrunjdwp:transport=dt_socket, server=y,address=54321 Debug 命令启动一个Java虚拟机,Java虚拟机会监听54321端口

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

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

stop in 命令在方法开始的地方设置断点,如下图:stop in Debug.main(java.lang.String[])


System.out.println("This is a test program");
list 命令来查看当前的上下文代码,如下所示:
clear 命令查看设置的所有断点,如下所示。正如我们设置的那样,它显示了我们设置断点的位置:
threadgroups 命令查看所有的线程组,正如我们所见,有两个线程组可用:system 和main ,如下所示:
threads 查看所有线程,如下所示:
使用classes 命令查看当前Java虚拟机所加载的类的信息,如下所示:

class 命令,下图显示了Debug类的详细信息:class <classname>


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

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


paswdCheck("Orangey_0x0") ,想进入passCheck方法进行调试,就使用step 命令。在test方法处输入step命令,如下所示:
next 命令继续运行下一条代码,如下所示:
step up 命令,如下所示:
where 命令会打印显示当前的调用堆栈,如下所示:
step 命令进入方法,并检查调用堆栈,如下所示:

Debug.paswdCheck 中运行,而Debug.paswdCheck 又是被Debug.main 调用的。假如想查看paswdCheck方法的局部变量信息,可以使用locals 命令查看所有的局部变量(如果代码不是使用“-g”选项编译的,这不起作用)
password ,可以使用print 命令打印出指定变量的内容,如下所示:print secret

如上介绍了 jdb 的基础知识、各种 jdb 命令以及如何将它们应用于调试 Java 应用程序,接下来我们将介绍如何利用jdb调试APK… …
android:debuggable="true" 】反编译APK,然后查看AndroidManifest.xml中是否有android:debuggable="true" ,没有的话则在相应位置添加
启动模拟器或连接真机,然后安装apk –> adb install AliCrackme_1.apk
方法1: 可以使用DDMS,直接打开DDMS,如下图箭头所指:

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


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


方法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 进行查看
$ 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 # 代码行断点


where

up 上移线程的堆栈
down 下移线程的堆栈

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


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),基本流程是利用寄存器指令中断:
备注:APK动态注入动态链接库 后续文章再更新… …
参考链接:
http://www.tasfa.cn/index.php/2016/06/01/android-re-gdb
https://blog.csdn.net/weixin_30485799/article/details/96760631
我自横刀向天笑,去留肝胆两昆仑