无agent文件的条件下使用Java Instrumentation API
序:Java Instrumentation API
从Java SE 5开始,可以使用Java的Instrumentation接口来编写Agent。如果需要在目标JVM启动的同时加载Agent,可以选择实现下面的方法:
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
我们这里只讨论运行时加载的情况。Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”:
Premain-Class: class
Agent-Class: class
生成agent.jar之后,可以通过com.sun.tools.attach.VirtualMachine的loadAgent方法加载:
private void attachAgentToTargetJVM() throws Exception {
List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
VirtualMachineDescriptor targetVM = null;
for (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) {
if (descriptor.id().equals(configure.getPid())) {
targetVM = descriptor;
break;
}
}
if (targetVM == null) {
throw new IllegalArgumentException("could not find the target jvm by process id:" + configure.getPid());
}
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(targetVM);
virtualMachine.loadAgent("{agent}", "{params}");
} catch (Exception e) {
if (virtualMachine != null) {
virtualMachine.detach();
}
}
}
以上代码可以用反射实现,使用Java agent这种方式可以修改已有方法,java.lang.instrument.Instrumentation提供了如下方法:
public interface Instrumentation {
/**
* 加入一个转换器Transformer,之后的所有的类加载都会被Transformer拦截。
* ClassFileTransformer类是一个接口,使用时需要实现它,该类只有一个方法,该方法传递类的信息,返回值是转换后的类的字节码文件。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
*此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
*在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
*该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}
可以使用redefineClasses方法完成对类方法的修改,结合javassist可以说是非常方便:
public static void agentmain(String args, Instrumentation inst) throws Exception {
Class[] loadedClasses = inst.getAllLoadedClasses();
for (int i = 0; i < loadedClasses.length; ++i) {
Class clazz = loadedClasses[i];
if (clazz.getName().equals("com.huawei.xxxx")) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(clazz.getName());
ctClass.stopPruning(true);
// javaassist freezes methods if their bytecode is saved
// defrost so we can still make changes.
if (ctClass.isFrozen()) {
ctClass.defrost();
}
CtMethod method; // populate this from ctClass however you wish
method.insertBefore("{ System.out.println("Wheeeeee!"); }");
byte[] bytecode = ctClass.toBytecode();
ClassDefinition definition = new ClassDefinition(Class.forName(clazz.getName()), bytecode);
inst.redefineClasses(definition);
} catch (Exception var9) {
var9.printStackTrace();
}
}
}
}
能否直接构造Instrumentation对象?
使用Java Instrumentation API的一个前提条件就是必须提供agent.jar,这是一个必须要放在硬盘上的文件。要解决这个问题,需要先识别问题的关键点:前面所有的编译生成agent.jar、loadagent加载最后都是为了产生Instrumentation对象,通过这个对象提供的redefineClasses方法,只需要提供字节码就可以完成类修改。java.lang.instrument.Instrumentation只是一个接口,它的实现类是:
/**
* The Java side of the JPLIS implementation. Works in concert with a native JVMTI agent
* to implement the JPLIS API set. Provides both the Java API implementation of
* the Instrumentation interface and utility Java routines to support the native code.
* Keeps a pointer to the native data structure in a scalar field to allow native
* processing behind native methods.
*/
public class InstrumentationImpl implements Instrumentation {
private final TransformerManager mTransformerManager;
private TransformerManager mRetransfomableTransformerManager;
// needs to store a native pointer, so use 64 bits
private final long mNativeAgent;
private final boolean mEnvironmentSupportsRedefineClasses;
private volatile boolean mEnvironmentSupportsRetransformClassesKnown;
private volatile boolean mEnvironmentSupportsRetransformClasses;
private final boolean mEnvironmentSupportsNativeMethodPrefix;
private
InstrumentationImpl(long nativeAgent,
boolean environmentSupportsRedefineClasses,
boolean environmentSupportsNativeMethodPrefix) {
mTransformerManager = new TransformerManager(false);
mRetransfomableTransformerManager = null;
mNativeAgent = nativeAgent;
mEnvironmentSupportsRedefineClasses = environmentSupportsRedefineClasses;
mEnvironmentSupportsRetransformClassesKnown = false; // false = need to ask
mEnvironmentSupportsRetransformClasses = false; // don't know yet
mEnvironmentSupportsNativeMethodPrefix = environmentSupportsNativeMethodPrefix;
}
...
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 native void
redefineClasses0(long nativeAgent, ClassDefinition[] definitions)
throws ClassNotFoundException;
...
}
该类java.sun.instrument.InstrumentationImpl的构造函数私有,但使用反射仍然可以调用。重点关注这个参数nativeAgent,这是一个native的指针,那么如果我们能提供这个指针,就可以不通过加载agent文件的方式实现修改类代码。
如何获得nativeAgent指针?
继续翻看Hotspot代码
public class InstrumentationImpl implements Instrumentation {
...
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 native void
redefineClasses0(long nativeAgent, ClassDefinition[] definitions)
throws ClassNotFoundException;
...
}
可以看到mNativeAgent变量经由redefineClasses0函数,经JNI方法传递到了native层代码。
/*
* Class: sun_instrument_InstrumentationImpl
* Method: redefineClasses0
* Signature: ([Ljava/lang/instrument/ClassDefinition;)V
*/
JNIEXPORT void JNICALL Java_sun_instrument_InstrumentationImpl_redefineClasses0
(JNIEnv * jnienv, jobject implThis, jlong agent, jobjectArray classDefinitions) {
redefineClasses(jnienv, (JPLISAgent*)(intptr_t)agent, classDefinitions);
}
/*
* Java code must not call this with a null list or a zero-length list.
*/
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;
jplis_assert(classDefinitions != NULL);
numDefs = (*jnienv)->GetArrayLength(jnienv, classDefinitions);
errorOccurred = checkForThrowable(jnienv);
jplis_assert(!errorOccurred);
if (!errorOccurred) {
jplis_assert(numDefs > 0);
/* get method IDs for methods to call on class definitions */
classDefClass = (*jnienv)->FindClass(jnienv, "java/lang/instrument/ClassDefinition");
errorOccurred = checkForThrowable(jnienv);
jplis_assert(!errorOccurred);
}
...
}
可以看到这个agent指针的结构类型为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 */
};
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
redefineClasses的第一行代码是jvmtiEnv* jvmtienv = jvmti(agent), 这个jvmti是个宏:
#define jvmti(a) a->mNormalEnvironment.mJVMTIEnv
在Java SE 5以前,就支持通过C/C++语言实现JVMTI agent,Java Instrumentation API的底层就是通过这种方式实现的。开发agent时,需要包含位于JDK include目录下的jvmti.h,这里面定义了使用JVMTI所用到的函数、事件、数据类型和常量,最后agent会被编译成一个动态库。JVMTI的函数调用与JNI相似,可以通过一个接口指针来访问JVMTI的函数。JVMTI的接口指针称为环境指针(environment pointer),环境指针是指向执行环境的指针,其类型为jvmtiEnv*。
jvmtiEnv *jvmti;
...
(*jvmti)->GetLoadedClasses(jvmti, &class_count, &classes); =
jvmtiEnv也同样提供了RedefineClasses函数,Java Instrumentation API同样功能就是封装于此之上。
jvmtiError RedefineClasses(jint class_count,
const jvmtiClassDefinition* class_definitions) {
return functions->RedefineClasses(this, class_count, class_definitions);
}
那么问题进一步的变为:怎样得到jvmtiEnv指针。
JPLISAgent实例是如何创建的?
继续查看Hotspot代码
/*
* Creates a new JPLISAgent.
* Returns error if the agent cannot be created and initialized.
* The JPLISAgent* pointed to by agent_ptr is set to the new broker,
* or NULL if an error has occurred.
*/
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;
}
JPLISInitializationError
initializeJPLISAgent( JPLISAgent * agent,
JavaVM * vm,
jvmtiEnv * jvmtienv) {
jvmtiError jvmtierror = JVMTI_ERROR_NONE;
jvmtiPhase phase;
agent->mJVM = vm;
agent->mNormalEnvironment.mJVMTIEnv = jvmtienv;
agent->mNormalEnvironment.mAgent = agent;
agent->mNormalEnvironment.mIsRetransformer = JNI_FALSE;
agent->mRetransformEnvironment.mJVMTIEnv = NULL; /* NULL until needed */
agent->mRetransformEnvironment.mAgent = agent;
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_FALSE; /* 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实例是通过native函数createNewJPLISAgent创建的,该函数是内部函数,没有从动态库中导出,Java层也没办法直接调用。那么思路还得回到jvmtiEnv指针上去。
*agent_ptr = NULL;
jnierror = (*vm)->GetEnv( vm,
(void **) &jvmtienv,
JVMTI_VERSION_1_1);
从以上代码我们可知,jvmtiEnv可以通过JavaVM对象获得。而关于JavaVM对象,在JDK的jni.h中,有定义导出方法:
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *);
该方法由libjvm.so中导出,即使so经过strip,符号也一定是存在的。因此我们可以通过此API获得JavaVM对象,通过JavaVM对象就能获得jvmtiEnv指针。
伪造JPLISAgent实例
JPLISAgent结构中虽然有很多成员,但分析Instrumentation对象中我们需要使用的redefineClasses等方法的native实现
public class InstrumentationImpl implements Instrumentation {
private native void
redefineClasses0(long nativeAgent, ClassDefinition[] definitions)
throws ClassNotFoundException;
"rawtypes") (
private native Class[]
getAllLoadedClasses0(long nativeAgent);
...
}
它们都只是从agent中获取jvmtiEnv指针,之后都没有再使用agent的其他成员
jobjectArray
JNIEnv * jnienv,
JPLISAgent * agent,
jobject classLoader,
ClassListFetcher fetcher) {
jvmtiEnv * jvmtienv = jvmti(agent);
...
void
* jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
jvmtienv = jvmti(agent);
...
那么我们只需要使用unsafe方法,申请一段内存,并在对应的偏移上放置jvmtiEnv指针值,就完成了JPLISAgent实例的构造。关键问题还是要解决获取jvmtiEnv指针。
如何在Java层调用native接口?
获取jvmtienv指针,可以采用暴力搜索内存的方式,但是这种方法很难做到通用。jvmtienv实例中有固定不变的4字节魔术字0x71EE,this指针就是jvmtiEnv指针。
//JVMTI_MAGIC = 0x71EE,
bool __fastcall JvmtiEnvBase::is_valid(JvmtiEnvBase *this)
{
return *((_DWORD *)this + 2) == 0x71EE;
}
稳定的办法就是上文分析的,通过JavaVM对象来获取。
struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;
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);
}
};
JavaVM对象其实也只是一个函数指针数组,不存在固定不变的魔术字。如果要通过JNI_GetCreatedJavaVMs方法获得,在Java层怎么调用它呢?
Java层想要调用native方法,常规做法是通过JNI,这种办法仍然需要提供一个so文件,然后通过dlopen的方式加载,这显然与本文初衷不符。不通过JNI能不能做到?至少在Linux是能做到的。
参考如下代码
/* Write @len bytes at @ptr to @addr in this address space using
* /proc/self/mem.
*/
void memwrite(void *addr, char *ptr, size_t len) {
std::ofstream ff("/proc/self/mem");
ff.seekp(reinterpret_cast<size_t>(addr));
ff.write(ptr, len);
ff.flush();
}
int main(int argc, char **argv) {
// Map an unwritable page. (read-only)
auto mymap =
(int *)mmap(NULL, 0x9000,
PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mymap == MAP_FAILED) {
std::cout << "FAILEDn";
return 1;
}
std::cout << "Allocated PROT_READ only memory: " << mymap << "n";
getchar();
// Try to write to the unwritable page.
memwrite(mymap, "x40x41x41x41", 4);
std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
getchar();
std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "n";
getchar();
// Try to writ to the text segment (executable code) of libc.
auto getchar_ptr = (char *)getchar;
memwrite(getchar_ptr, "xcc", 1);
// Run the libc function whose code we modified. If the write worked,
// we will get a SIGTRAP when the 0xcc executes.
getchar();
}
编译执行后,得到结果
root@ecs-16:~# ./proc_mem_poc
Allocated PROT_READ only memory: 0x7f4390429000
did mymap[0] = 0x41414140 via proc self mem..
mymap[0] = 0x41414140
Trace/breakpoint trap (core dumped)
以上代码示例说明,Linux下进程可以通过/proc/self/mem修改自身内存,即使是只读内存也可以修改。示例代码修改了getchar函数的开头为int3,结果真的执行了。
使用Java代码读写/proc/self/mem是完全没问题的,而Java原生就有很多JNI的native方法,比如libjava.so中的
Java_java_lang_ClassLoader_registerNatives等等很多。
如果先修改Java_java_lang_ClassLoader_registerNatives的代码为我想要的,然后再主动调用ClassLoader.registerNatives,就实现了native层的任意代码执行。然后再还原代码,一切好像从未发生过!
那么关键问题就变为:如何获取
Java_java_lang_ClassLoader_registerNatives地址
Java查找ELF导出符号
再次得益于LINUX下的/proc文件系统,我们可以从/proc/self/maps轻易的获取所有已加载ELF对象的基址及文件路径
7fcbb8c0d000-7fcbb9a95000 r-xp 00000000 fc:01 1179725 /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
7fcbb9a95000-7fcbb9c95000 ---p 00e88000 fc:01 1179725 /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
7fcbb9c95000-7fcbb9d33000 r--p 00e88000 fc:01 1179725 /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
7fcbb9d33000-7fcbb9d5c000 rw-p 00f26000 fc:01 1179725 /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
那么获取导出符号就变得非常简单,直接打开ELF文件解析得到对应符号地址,然后再加上库基址即可。对于x64 ELF的实例代码如下:
static long find_symbol(String elfpath, String sym, long libbase) throws IOException{
long func_ptr = 0;
RandomAccessFile fin = new RandomAccessFile(elfpath, "r");
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());
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;
i = 0; i < e_shnum; ++i) {
+ 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());
= SHT_DYNSYM) { =
break;
}
}
int symtab_shdr_sh_link = sh_link;
long symtab_shdr_sh_size = sh_size;
long symtab_shdr_sh_entsize = sh_entsize;
long symtab_shdr_sh_offset = sh_offset;
+ symtab_shdr_sh_link * e_shentsize);
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());
long symstr_shdr_sh_offset = sh_offset;
long cnt = symtab_shdr_sh_entsize > 0 ? symtab_shdr_sh_size/symtab_shdr_sh_entsize : 0;
i = 0; i < cnt; ++i) {
+ symtab_shdr_sh_entsize*i);
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());
= 0 =
st_name == 0
(ELF_ST_TYPE(st_info) != STT_FUNC && ELF_ST_TYPE(st_info) != STT_GNU_IFUNC))
{
continue;
}
+ st_name);
String name = "";
byte ch = 0;
fin.readByte()) != 0) =
{
name += (char)ch;
}
if(sym.equals(name))
{
func_ptr = libbase + st_value;
break;
}
}
fin.close();
return func_ptr;
}
最后的步骤
为了能从native层得到返回值到java层,我们需要找一个返回值为long的native方法,把shellcode植入到它的开头。
void * shellcode()
{
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
movabs rax, _JNI_GetCreatedJavaVMs
sub rsp, 20h
xor rsi, rsi
inc rsi
lea rdx, [rsp+4]
lea rdi, [rsp+8]
call rax
mov rdi, [rsp+8]
lea rsi, [rsp+10h]
mov edx, 30010200h
mov rax, [rdi]
call qword ptr [rax+30h]
mov rax, [rsp+10h]
add rsp, 20h
ret
后来我选择了libjava.so中的Java_java_io_RandomAccessFile_length。使用unsafe申请一段内存,并在偏移8(x64下指针长度为8)的位置上放置jvmtienv指针
long JPLISAgent = unsafe.allocateMemory(0x1000);
unsafe.putLong(JPLISAgent + 8, native_jvmtienv);
再通过反射最终得到InstrumentationImpl对象
try {
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);
Method getAllLoadedClasses = instrument_clazz.getMethod("getAllLoadedClasses");
Class<?>[] clazzes = (Class<?>[]) getAllLoadedClasses.invoke(insn);
for(Class<?> cls : clazzes) {
System.out.println(cls.getName());
}
}catch(Exception e) {
System.out.println("Exception: " + e.getMessage());
}
需要注意的是,在Java11中sun.instrument包已不再可引用。这里已经可以获取所有加载的类。
意外
在正确查找得到jvmtienv指针之后,执行redefineClasses会报异常
Java_java_io_RandomAccessFile_length 0x7fb29c485e40
JNI_GetCreatedJavaVMs 0x7fb29d52b650
native_jvmtienv 7fb2980ef070
Exception: null
使用调试工具跟踪,在函数redefineClasses中会调用
void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
jvmtiEnv* jvmtienv = jvmti(agent);
jboolean errorOccurred = JNI_FALSE;
jclass classDefClass = NULL;
jmethodID getDefinitionClassMethodID = NULL;
...
if (!errorOccurred) {
jvmtiError errorCode = JVMTI_ERROR_NONE;
errorCode = (*jvmtienv)->RedefineClasses(jvmtienv, numDefs, classDefs);
if (errorCode == JVMTI_ERROR_WRONG_PHASE) {
/* insulate caller from the wrong phase error */
errorCode = JVMTI_ERROR_NONE;
} else {
errorOccurred = (errorCode != JVMTI_ERROR_NONE);
if ( errorOccurred ) {
createAndThrowThrowableFromJVMTIErrorCode(jnienv, errorCode);
}
}
}
...
}
这个(*jvmtienv)->RedefineClasses调用,暂时没找到源码,在IDA中逆向的结果如下
__int64 __fastcall jvmti_RedefineClasses(JvmtiEnvBase *this, JavaThread *a2, __int64 a3)
{
int v4; // er15
unsigned int v5; // er12
void *v7; // rax
unsigned __int64 v8; // r14
unsigned __int64 v9; // rsi
__int64 v10; // rbx
_QWORD *v11; // rax
_QWORD *v12; // r13
signed __int32 v13; // [rsp+0h] [rbp-60h] BYREF
CautiouslyPreserveExceptionMark *v14; // [rsp+8h] [rbp-58h]
char v15[40]; // [rsp+10h] [rbp-50h] BYREF
v4 = (int)a2;
v5 = 112;
if ( JvmtiEnvBase::_phase == 4 )
{
v7 = pthread_getspecific(ThreadLocalStorage::_thread_index);
v8 = (unsigned __int64)v7;
if ( v7 && (*(unsigned __int8 (__fastcall **)(void *))(*(_QWORD *)v7 + 40LL))(v7) )
{
*(_DWORD *)(v8 + 624) = 5;
if ( os::_processor_count != 1 || AssumeMP )
{
if ( UseMembar )
{
if ( os::_processor_count != 1 || AssumeMP )
_InterlockedAdd(&v13, 0);
}
else
{
*(_DWORD *)((char *)os::_mem_serialize_page
+ ((unsigned int)(v8 >> 4) & (unsigned int)os::_serialize_page_mask)) = 1;
}
}
if ( SafepointSynchronize::_state || (*(_DWORD *)(v8 + 48) & 0x30000000) != 0 )
JavaThread::check_safepoint_and_suspend_for_native_trans((JavaThread *)v8, a2);
*(_DWORD *)(v8 + 624) = 6;
v9 = v8;
v5 = 116;
v14 = (CautiouslyPreserveExceptionMark *)v15;
CautiouslyPreserveExceptionMark::CautiouslyPreserveExceptionMark(
(CautiouslyPreserveExceptionMark *)v15,
(Thread *)v8);
if ( (unsigned __int8)JvmtiEnvBase::is_valid(this) )
{
v5 = 99;
if ( (*((_BYTE *)this + 361) & 2) != 0 ) <--- 这个位置校验不过,需要令它为2
{
v5 = 103;
if ( v4 >= 0 )
{
v5 = 100;
if ( a3 )
{
v9 = (unsigned int)v4;
v5 = JvmtiEnv::RedefineClasses(this, (unsigned int)v4, a3);
}
}
}
...
}
因此需要用unsafe设置一下
unsafe.putByte(native_jvmtienv + 361, (byte) 2);
测试
修改java.io.RandomAccessFile的getFD方法,插入打印语句
public static void main(String[] args) {
ClassPool pool = ClassPool.getDefault();
CtClass string_clazz = null;
try {
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");");
string_clazz.writeFile("D:\1.txt");
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (CannotCompileException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
从1.txt文件夹里读取类的字节码
try {
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);
//修改过的java.io.RandomAccessFile
byte hexData[] = {
... //太长省略
};
ClassDefinition definition = new ClassDefinition(Class.forName("java.io.RandomAccessFile"), hexData);
Method redefineClazz = instrument_clazz.getMethod("redefineClasses", ClassDefinition[].class);
redefineClazz.invoke(inst, new Object[] {
new ClassDefinition[] {
definition
}
});
}catch(Exception e) {
System.out.println("Exception: " + e.getMessage());
}
fout.getFD();
正确输出结果
Java_java_io_RandomAccessFile_length 0x7fd720689e40
JNI_GetCreatedJavaVMs 0x7fd72172f650
native_jvmtienv 7fd71c0e71d0
from java instrucment api
完整代码请参考:https://github.com/bigBestWay/ice
结语
要在不提供agent文件的条件下完成Java Instrument,有如下步骤:
-
解析ELF,得到Java_java_io_RandomAccessFile_length和JNI_GetCreatedJavaVMs
-
生成利用JNI_GetCreatedJavaVMs获取jvmtienv指针的shellcode
-
在Java_java_io_RandomAccessFile_length放置shellcode并调用
-
恢复Java_java_io_RandomAccessFile_length代码
-
利用unsafe伪造agent实例
-
利用反射实例化sun.instrument.InstrumentationImpl
-
使用此对象修改类
参考
rebeyond 《Java内存攻击技术漫谈》
https://mp.weixin.qq.com/s/JIjBjULjFnKDjEhzVAtxhw
本文始发于微信公众号(SilverNeedleLab):Linux下内存马进阶植入技术
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论