深入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 (valid_jdwp_agent(name, is_absolute_path)) {
jio_fprintf(defaultStream::error_stream(),
"Debugging agents are not supported in this VMn");
return JNI_ERR;
}
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;
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;
}
}
上述在解析参数的时候,会将-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 int
main(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 JNICALL
JLI_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是之后会一直跟到后面调用的。
jboolean
LoadJavaVM(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函数中都做了些什么
int
JVMInit(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
int
CallJavaMainInNewThread(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函数处理
int
JavaMain(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 jboolean
InitializeJVM(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函数
所以会去调用指定链接库的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 JNICALL
DEF_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
JPLISInitializationError
initializeJPLISAgent( 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
jboolean
processJavaStart( 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函数的内容
jboolean
createInstrumentationImpl( 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 */
/* Header for class org_jarEncoder_ByteCodeEncryptor */
extern "C" {
/*
* Class: org_jarEncoder_ByteCodeEncryptor
* Method: encrypt
* Signature: ([B)[B
*/
JNIEXPORT jbyteArray JNICALL Java_org_jarEncoder_ByteCodeEncryptor_encrypt
(JNIEnv *, jclass, jbyteArray);
}
const int compare_length = 8;
const char* package_prefix= "javaTest";
生成JNI对应的函数头,并设置之后要加密/解密的包名前缀和匹配的字长
//AES_IV
static unsigned char AES_IV[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };
//AES_KEY
static 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的文件
可以看到Jar包的javaTest/Bird.class类以及被加密完成
使用JVMTI事件的回调函数解密
现在已经加密完成了,可是由于类的不正确,导致无法加载进jvm中,这时候就需要通过之前介绍的JVMTI来动态修改类解密了。
之前介绍JVMTI的时候,说到agentpath这个参数最终会调用动态链接库中的Agent_OnLoad函数,在函数中,可以通过jvmtiEventCallbacks设置回调函数,先来看看这个jvmtiEventCallbacks的结构体
其中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的字节码文件,以此来达到运行时动态解密。
完整代码会上传到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函数的地址并调用。
具体步骤如下:
通过PEB方式获取Kernel32基址
这里我就用x64环境来做复现,x86的场景下具体方式也大同小异。
这里我先将rebeyond文章中给出的汇编代码用内联ASM的方式编译出来,拖到IDA中分析
在x64的环境下,可以通过程序的GS:[60h]来获取PEB指针,GS:[30h]获取TEB指针
这里我深入讲解一下获取kernel32基址的汇编代码
在Windows x64系统下,GS全局段寄存器是指向TEB。所以直接打开Windbg,查看TEB结构:
0:020> dt _TEB
ntdll!_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 _PEB
ntdll!_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_DATA
ntdll!_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_ENTRY
ntdll!_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> !peb
PEB at 0000009c2c029000
有了地址后,就可以用结构体来解析该地址
0:020> dt _PEB 0000009c2c029000
ntdll!_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_DATA
ntdll!_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的地址
发现出现乱码的情况,这里卡了我不少时间,后面发现其实是在结构体映射的时候,需要自己再手动对准结构体头。
这里我对照了下一结构体,从PEBLDRDATA中取出IninitializationOrderModuleList的Flink地址会映射到LDRDATATABLEENTRY的IninitializationOrderLinks成员上,但是由于该成语对于结构体已经偏移了0x20的位置,所以我们要在获取的地址上再减去0x20的位置,就能准确的获取到FullDllName和BaseDllName。
成功获取到模块的信息,我这里用内联汇编写了个循环来判断获取。
在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:
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
ret
GetKernel32 endp
END
在循环中,rax的值就是LDRDATATABLE_ENTRY结构体偏移0x20的位置上
然后在cpp文件中调用:
extern "C" PVOID _cdecl GetKernel32();
int main() {
std::cout << "Hello World!n";
std::cout << "GetKernel32 Function Return Address:" << GetKernel32() << std::endl;
return 0;
}
获取JNI_GetCreatedJavaVMs函数地址
先来看看如何获取导出表中GetProcAddress函数地址
上一小节通过PEB的方式拿到了DllBase,而这个DllBase就是我们要偏移的IMAGEDOSHEADER
0:022> dt _IMAGE_DOS_HEADER 0x00007ffb`36a10000
ntdll!_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+0xf8
ntdll!_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;
}
构造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代码实现,方便读者更好的理解
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的值
canredefineclasses参数是在JavaAgent的MANIFEST.MF指定的,当Can-Redefine-Classes的值为true表示能重定义此代理所需的类,默认值为 false。也就是说,当设置该值为true时,即可通过JavaAgent重新定义已经被JVM加载好的类。
但如果该值为false,还调用redefineclasses来修改,就会出现java.lang.reflect.InvocationTargetException异常
但其实在不同的JDK版本中,canredefineclasses的偏移量其实是不确定的
在分析该偏移量的时候,Windows环境下是真的很吐血,定位jdk1.8版本的jvm.dll值是360与0x200
但分析同样版本的libjvm.so的时候就能成功定位361与0x2
继续看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!”字样。
成功修改目标类!
Windows x64环境Shellcode编写
现在整体的调用思路有了,接下来就是编写ShellCode并通过WindowsVirtualMachine类来执行,好拿到构造的JPLISAgent指针地址。
这里我先拿构造MessageBoxA函数来讲述我编写ShellCode的过程,因为本章节更多的是讲解ShellCode的编写思路,而MessageBoxA函数可以方便测试,等到后续再替换成JNI_GetCreatedJavaVMs函数即可。
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
.DATA
LoadLibraryA BYTE "LoadLibraryA",0
User32 BYTE "User32",0
MessageBoxA 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:
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
rdx,LoadLibraryA
mov rdx, offset LoadLibraryA
mov rcx,rax
call PEGetFuncAddressByName
rcx,User32
mov rcx, offset User32
call rax
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
ret
RunMessageBox 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
dword ptr [rax+rax],eax
_7FF76D4F1060:
mov ecx,dword ptr [r11]
mov r9,rsi
add rcx,rbx
sub r9,rcx
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 _7FF76D4F1098
PEGetFuncAddressByName endp
END
PEGetFuncAddress函数就是我们自己写的获取导出表的函数地址
在函数的开头
sub rsp, 128h
and rsp, 0FFFFFFFFFFFFFFF0h ;将堆栈和16个字节倍数对齐
这个sub rsp的指令,我原先开辟的是100h的空间,但是100 % 16是有4个字节的空间多出来,导致堆栈不平衡。为此我排了很久,后面开辟到16的倍数128h就解决了。
如下图,就是在汇编中调用PEGetFuncAddress函数时返回LoadLibraryA函数地址的截图
最终成功调用MessageBoxA函数
但是这里我又遇到了一个问题,就是在汇编代码中,寻找LoadLibraryA和MessageBoxA字符串是用offset的方式去数据段寻找的,如果直接调用Shellcode,肯定会找不到对应的字符串,从而导致Shellcode运行失败。
而我这里的解决方案就是利用栈空间来存放字符串指针
mov rcx,41786fh ;oxA
push rcx
mov rcx,426567617373654dh ;MessageB
push rcx
mov rdx,rsp
mov rcx,rax
sub rsp,30h
call PEGetFuncAddressByName
add rsp,30h
add rsp,10h
先将MessageBoxA字符串压入堆栈中,rsp指针指向的位置正好就是字符串的地址,再sub rsp,30h开辟一个安全的堆栈空间调用PEGetFuncAddressByName函数。
编写完成后,选中相关汇编代码,右键->二进制->编辑,选择C样式Shellcode字符串
这里可以通过VirtualAlloc的方式验证ShellCode是否可以执行
// 使数据段可读可写可执行
// 生成的ShellCode
char arr[] = { "x48x81xECx28x01x00x00x48x83xE4xF0x65x48x8Bx04x25x60x00x00x00x48x8Bx40x18x48x8Bx40x30x48x8Bx70x10x48x8Bx58x40x48x8Bx00x81x3Bx4Bx00x45x00x75xEDx81x7Bx04x52x00x4Ex00x75xE4x81x7Bx08x45x00x4Cx00x75xDBx81x7Bx0Cx33x00x32x00x75xD2x48x8BxC6x48x85xC0x74x7Ex48xC7xC1x61x72x79x41x51x48xB9x4Cx6Fx61x64x4Cx69x62x72x51x48x8BxD4x48x8BxC8x48x83xECx30xE8x64x00x00x00x48x83xC4x30x48x83xC4x10x48xB9x75x73x65x72x33x32x00x00x51x48x8BxCCx48x83xECx30xFFxD0x48x83xC4x30x48x83xC4x08x48xC7xC1x6Fx78x41x00x51x48xB9x4Dx65x73x73x61x67x65x42x51x48x8BxD4x48x8BxC8x48x83xECx30xE8x1Ex00x00x00x48x83xC4x30x48x83xC4x10x45x33xC9x45x33xC0x33xD2x33xC9xFFxD0x33xC0x48x81xC4x28x01x00x00xC3x48x89x5Cx24x08x48x89x6Cx24x10x48x89x74x24x18x48x89x7Cx24x20x41x56x48x63x41x3Cx45x33xD2x48x8BxF2x48x8BxD9x44x8Bx84x08x88x00x00x00x4Cx03xC1x45x8Bx58x20x41x8Bx68x24x4Cx03xD9x45x8Bx70x1Cx48x03xE9x41x8Bx78x18x4Cx03xF1x85xFFx74x32x41x8Bx0Bx4Cx8BxCEx48x03xCBx4Cx2BxC9x44x0FxB6x01x42x0FxB6x14x09x44x2BxC2x75x07x48xFFxC1x85xD2x75xEBx45x85xC0x74x25x41xFFxC2x49x83xC3x04x44x3BxD7x72xCEx33xC0x48x8Bx5Cx24x10x48x8Bx6Cx24x18x48x8Bx74x24x20x48x8Bx7Cx24x28x41x5ExC3x42x0FxB7x4Cx55x00x41x8Bx04x8Ex48x03xC3xEBxDA" };
int main()
{
void* exec = VirtualAlloc(0, sizeof arr, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, arr, sizeof arr);
((void(*)())exec)();
}
熟悉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:
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
JNI_GetCreatedJavaVMs Function
test rax,rax
je _function_end
rdx,LoadLibraryA
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
JNI_GetCreatedJavaVMs Function
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
JNI_GetCreatedJavaVMs Ends
DetachCurrentThread Proc
mov rcx,r14
mov r15,qword ptr [r14] ;JavaVm variable
mov r15,qword ptr [r15+28h] ;vm->DetachCurrentThread
call r15 ;Call DetachCurrentThread()
DetachCurrentThread Ends
_function_end:
add rsp, 178h ;Restore Stack Frame 0x128 + 0x28 + 0x28 = 0x178
ret
GetJavaVmPoint 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
dword ptr [rax+rax],eax
_7FF76D4F1060:
mov ecx,dword ptr [r11]
mov r9,rsi
add rcx,rbx
sub r9,rcx
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 _7FF76D4F1098
PEGetFuncAddressByName 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;
}
}
不过好像在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找半天的情况- -。
也算是给后来的安全研究人员提个醒吧,一定得多多检查!!
根据相关法律法规,这里就不再放出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包加密