戳上面的蓝字关注我吧!
看到jrasp的架构之后,我的思路就是先从agent为入口进行分析,并带着如下几个疑问:
Agent是如何织入插桩代码的
Agent是通过何种方式或者规则进行研判并拦截的
其参数和规则是如何动态的更改传递给Agent的
查看Agent的启动脚本,查看agent的启动方式是通过
function attach_jvm() {
# attach target jvm
"${RASP_JAVA_HOME}/bin/java" \
${RASP_JVM_OPS} \
-jar "${RASP_LIB_DIR}/jrasp-core.jar" \
"${TARGET_JVM_PID}" \
"${RASP_LIB_DIR}/jrasp-launcher.jar" \
"raspHome=${RASP_HOME_DIR};serverIp=${TARGET_SERVER_IP};serverPort=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE};enableAuth=${ENABLE_AUTHTH};username=${DEFAULT_USERNAME};password=${DEFAULT_PASSWORD}" ||
exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."
# get network from attach result
RASP_SERVER_NETWORK=$(grep "${TARGET_NAMESPACE}" "${RASP_TOKEN_FILE}" | awk -F ";" '{print $4";"$5}')
[[ -z ${RASP_SERVER_NETWORK} ]] &&
exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."
}
程序通过lib/jrasp-core.jar包来启动到目标JVM的PID中,跟进core的源码查看
从pom.xml文件可以得知
<mainClass>com.jrasp.core.CoreLauncher</mainClass>
启动该core jar包的类是CoreLauncher
跟入代码之后发现是动态加载的Agent,关于动态加载Agent的方式我之前在文章《瞒天过海计之Tomcat隐藏内存马》中也有说到过,原文链接:https://tttang.com/archive/1368/
这里加载的AgentJarPath就是jrasp-launcher的jar包路径
找到其中的关键启动代码:
// 启动加载
public static void premain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_AGENT; //agent方式
install(toFeatureMap(featureString), inst);
}
// 动态加载
public static void agentmain(String featureString, Instrumentation inst) {
LAUNCH_MODE = LAUNCH_MODE_ATTACH; //attach模式
install(toFeatureMap(featureString), inst);
}
toFeatureMap就是解析了shell启动脚本中的参数
跟进install看看agent是如何安装到jvm环境中的
// 在当前JVM安装rasp
private static synchronized InetSocketAddress install(final Map<String, String> featureMap,
final Instrumentation inst) {
final String namespace = getNamespace(featureMap);
Map<String, String> coreConfigMap = toCoreConfigMap(featureMap);
try {
final String home = getRaspHome(featureMap);
// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getRaspSpyJarPath(home)
)));
// 构造自定义的类加载器,尽量减少Rasp对现有工程的侵蚀
final ClassLoader raspClassLoader = loadOrDefineClassLoader(
namespace,
getRaspCoreJarPath(home)
);
// CoreConfigure类定义
final Class<?> classOfConfigure = raspClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);
// 反序列化成CoreConfigure类实例
final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", Map.class)
.invoke(null, coreConfigMap);
// CoreServer类定义
final Class<?> classOfProxyServer = raspClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);
// 获取CoreServer单例
final Object objectOfProxyServer = classOfProxyServer
.getMethod("getInstance")
.invoke(null);
// CoreServer.isBind()
final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);
// 如果未绑定,则需要绑定一个地址
if (!isBind) {
try {
classOfProxyServer
.getMethod("bind", classOfConfigure, Instrumentation.class)
.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
} catch (Throwable t) {
classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
throw t;
}
}
// 返回服务器绑定的地址
return (InetSocketAddress) classOfProxyServer
.getMethod("getLocal")
.invoke(objectOfProxyServer);
} catch (Throwable cause) {
throw new RuntimeException("rasp attach failed.", cause);
}
}
代码利用反射,实际调用了jrasp-core中的JettyCoreServer类的bind方法。该方法中初始化了Http服务器和加载了相关模块。
跟进查看模块是如何加载的,可以看到DefaultCoreModuleManager类的reset方法
@Override
public synchronized CoreModuleManager reset() throws ModuleException {
logger.info(AGENT_COMMON_LOG_ID, "resetting all loaded modules:{}", loadedModuleBOMap.keySet());
// 1. 强制卸载所有模块
unloadAll();
// 2.加载系统模块、必装模块、非必须模块
loadModule(systemModuleLibDir, systemModuleLibCopyDir, cfg.getLaunchMode());
loadModule(requiredModuleLibDir, requiredModuleLibCopyDir, cfg.getLaunchMode());
loadModule(optionalModuleLibDir, optionalModuleLibCopyDir, cfg.getLaunchMode());
return this;
}
跟进loadModule方法,发现其中使用了两个模块回调
ModuleLibLoader moduleLibLoader = new ModuleLibLoader(from, to, mode);
moduleLibLoader.load(new InnerModuleJarLoadCallback(), new InnerModuleLoadCallback());
可以看到这个InnerModuleLoadCallback的回调函数被ModuleJarLoader类的load方法调用了。
进入load方法后,创建了一个ModuleJarClassLoader对象,这是一个继承自URLClassLoader的加载器,后续跟进发现,该加载器对象当做参数被loadingModules方法调用了
// 加载模块类并判断模块中module上的注解信息
private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
final ModuleLoadCallback mCb) {
final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>(); // 仅用于记录modules个数在打印日志时
// todo 怎么加载的??有点类似于服务发现
final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
......//Many Code
}
看loadingModules方法的前一部分,调用了SPI机制
ServiceLoader.load(Module.class, moduleClassLoader);
而这里的moduleClassLoader就是之前自定义的加载器,加载之前设置好模块目录下的jar文件
因此模块是通过SPI机制注入进去的,加载完之后,就获取了Module的实现类,以及是否实现了相关注解等
这里我拿官方编写的RCE-Hook模块来辅助说明
正好满足继承了Module类,且该类实现了Information注解
之后就调用了刚才创建的回调函数的onLoad方法
if (null != mCb) {
mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
}
继续跟进后可以到达DefaultCoreModuleManager的load方法中
/**
* 加载并注册模块
* <p>1. 如果模块已经存在则返回已经加载过的模块</p>
* <p>2. 如果模块不存在,则进行常规加载</p>
* <p>3. 如果模块初始化失败,则抛出异常</p>
*
* @param uniqueId 模块ID
* @param module 模块对象
* @param moduleJarFile 模块所在JAR文件
* @param moduleClassLoader 负责加载模块的ClassLoader
* @throws ModuleException 加载模块失败
*/
private synchronized void load(final String uniqueId,
final Module module,
final File moduleJarFile,
final ModuleJarClassLoader moduleClassLoader) throws ModuleException {
//判断是否在注册模块列表中
if (loadedModuleBOMap.containsKey(uniqueId)) {
return;
}
logger.info(LOADING_MODULE_LOG_ID, "loading module, module={};class={};module-jar={};",
uniqueId,
module.getClass().getName(),
moduleJarFile
);
// 初始化模块信息
final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);
// 注入@Resource资源
injectResourceOnLoadIfNecessary(coreModule);
callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);
// 设置为已经加载
coreModule.markLoaded(true);
// 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
markActiveOnLoadIfNecessary(coreModule);
// 注册到模块列表中
loadedModuleBOMap.put(uniqueId, coreModule);
// 通知生命周期,模块加载完成
callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);
}
仔细分析一下这个load方法中的加载过程,最重要的部分就是injectResourceOnLoadIfNecessary方法
private void injectResourceOnLoadIfNecessary(final CoreModule coreModule) throws ModuleException {
try {
final Module module = coreModule.getModule();
for (final Field resourceField : FieldUtils.getFieldsWithAnnotation(module.getClass(), Resource.class)) { //获取Module中带有Resouce注解的字段
final Class<?> fieldType = resourceField.getType(); //获取字段类型
// LoadedClassDataSource对象注入
if (LoadedClassDataSource.class.isAssignableFrom(fieldType)) {
writeField(
resourceField,
module,
classDataSource,
true
);
}
// JsonImpl注入
else if (JSONObject.class.isAssignableFrom(fieldType)) {
writeField(resourceField, module, new JsonImpl(), true);
}
// AlgorithmManager 注入
else if (AlgorithmManager.class.isAssignableFrom(fieldType)) {
writeField(resourceField, module, DefaultAlgorithmManager.instance, true);
}
...
这个方法主要动作是注入模块中带@Resource字段的变量
例如之前展示的RCE-Hook图中的几个带@Resource注解,有一个AlgorithmManager的类字段,注入的是DefaultAlgorithmManager.instance实例。
跟进查看DefaultAlgorithmManager类的实现
正好对应rce-hook模块中,算法调用的doCheck方法来验证
至此,Rce-Hook模块就正常通过SPI的方式装载进JVM虚拟机中了
前面讲到模块加载过程,模块加载后,会自行调用重写的loadCompleted方法
这里还是拿Rce-hook模块来举例
@Override
public void loadCompleted() {
String clazzName;
if (isGreaterThanJava8()) {
clazzName = "java.lang.ProcessImpl";
} else {
clazzName = "java.lang.UNIXProcess";
}
nativeProcessRceHook(clazzName);
}
首先简单的判断了一下版本,就调用需要织入的类,到nativeProcessRceHook方法中
public void nativeProcessRceHook(final String clazz) {
new EventWatchBuilder(moduleEventWatcher)
.onClass(clazz)
.includeBootstrap()
.onBehavior("forkAndExec")
.onWatch(new AdviceListener() {
@Override
protected void before(Advice advice) throws Throwable {
byte[] prog = (byte[]) advice.getParameterArray()[2]; // 命令
byte[] argBlock = (byte[]) advice.getParameterArray()[3]; // 参数
String cmdString = getCommandAndArgs(prog, argBlock);
HashMap<String, Object> requestInfo = new HashMap<String, Object>(requestInfoThreadLocal.get());
algorithmManager.doCheck(ATTACK_TYPE, requestInfo, cmdString);
}
@Override
protected void afterReturning(Advice advice) throws Throwable {
requestInfoThreadLocal.remove();
}
});
}
方法中新建了一个EventWatch监听器,用于织入到java.lang.ProcessImpl的forkAndExec方法中,至于为什么是forkAndExec方法可以看官方的文档:https://www.jrasp.com/algorithm/rce/rce-basic-principles.html
@Override
public EventWatcher onWatch(AdviceListener adviceListener) {
return build(new AdviceAdapterListener(adviceListener), null, BEFORE, RETURN, THROWS, IMMEDIATELY_RETURN, IMMEDIATELY_THROWS);
}
看到onWatch方法的内容如上,参数是一个AdviceListener对象,跟进build方法查看是如何解析该对象并织入到目标类方法中的。
private EventWatcher build(final EventListener listener,
final Progress progress,
final Event.Type... eventTypes) {
final int watchId = moduleEventWatcher.watch(
toEventWatchCondition(),
listener,
progress,
eventTypes
);
....
}
调用了moduleEventWatcher.watch方法
查看RaspClassFileTransformer类
final byte[] toByteCodeArray = new EventEnhancer(this).toByteCodeArray(
loader,
srcByteCodeArray,
behaviorSignCodes,
namespace,
listenerId,
eventTypeArray
);
而EventWeaver类继承了ClassVisitor,而ASM织入方法的关键函数肯定就是重写的visitMethod方法
这里方法会先判断是否为织入的方法体,之后会有一个替换Native原生方法的过程
@Override
public void makrNativeMethodEnhance() {
if(setNativeMethodPrefix.compareAndSet(false,true)){
if(inst.isNativeMethodPrefixSupported()){
inst.setNativeMethodPrefix(this,getNativeMethodPrefix());
}else{
throw new UnsupportedOperationException("Native Method Prefix Unspported");
}
}
}
setNativeMethodPrefix方法可以设置native原生方法的前缀,假如有一个名为foo的native方法,则对应的C底层函数名为
native boolean foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);
执行完inst.setNativeMethodPrefix(transformer,"wrapped_");
native boolean wrapped_foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);
这里想仔细了解一下是如何工作的,可以移步官方文档:https://www.jrasp.com/guide/technology/native_method.html
再继续回到EventWeaver类,看看是织入了什么在方法中
之前替换了native的前缀为“JRASP”,因此需要在织入的forkAndExec方法中再调用JRASPforkAndExec方法,类似一种方法替换的方式。
上图是我将织入后的ProcessImpl类dump下来反编译之后的截图,至此就是整个织入的原理和替换的过程
继续看到ProcessImpl类的forkAndExec第40行,调用了Spy.spyMethodOnBefore方法
public static Ret spyMethodOnBefore(final Object[] argumentArray,
final String namespace,
final int listenerId,
final int targetClassLoaderObjectID,
final String javaClassName,
final String javaMethodName,
final String javaMethodDesc,
final Object target) throws Throwable {
final Thread thread = Thread.currentThread();
if (selfCallBarrier.isEnter(thread)) {
return Ret.RET_NONE;
}
final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
try {
final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
if (null == spyHandler) {
return Ret.RET_NONE;
}
return spyHandler.handleOnBefore(
listenerId, targetClassLoaderObjectID, argumentArray,
javaClassName,
javaMethodName,
javaMethodDesc,
target
);
} catch (Throwable cause) {
handleException(cause);
return Ret.RET_NONE;
} finally {
selfCallBarrier.exit(thread, node);
}
}
跟进handleOnBefore方法
方法中首先获取了对应的事件管理器
// 获取事件处理器
final EventProcessor processor = mappingOfEventProcessor.get(listenerId);
并针对Before事件创建了BeforeEvent对象
final BeforeEvent event = process.getEventFactory().makeBeforeEvent(
processId,
invokeId,
javaClassLoader,
javaClassName,
javaMethodName,
javaMethodDesc,
target,
argumentArray
);
并传递在handleEvent方法后,调用了对应Listener的onEvent方法
跟进onEvent方法中,可以到下图处理listener的位置
此时的堆栈关系图如下所示
而处理的正好是RceHook模块的内部类
@Override
protected void before(Advice advice) throws Throwable {
byte[] prog = (byte[]) advice.getParameterArray()[2]; // 命令
byte[] argBlock = (byte[]) advice.getParameterArray()[3]; // 参数
String cmdString = getCommandAndArgs(prog, argBlock);
HashMap<String, Object> requestInfo = new HashMap<String, Object>(requestInfoThreadLocal.get());
algorithmManager.doCheck(ATTACK_TYPE, requestInfo, cmdString);
}
解析advice对象中传入的命令参数,并添加到algorithmManager类的doCheck方法中检测。
而这个algorithmManager对象,之前在将@Resource注解的时候,会在SPI注入的时候自动装载了,但由于DefaultAlgorithmManager类的字段都有static修饰词,因此是个单例模式。只需要找到在哪里注册过algorithmManager的算法即可。
注册的过程在com.jrasp.module.rcenative.algorithm.RceAlgorithm类中
@Override
public String getName() {
return "rce-check";
}
@Override
public String getType() {
return "rce"; //定义模块的类型
}
@Override
public String getDescribe() {
return "命令执行检测算法";
}
@Override
public void loadCompleted() {
// 默认初始化
RceReflectCheck rceReflectCheck = new RceReflectCheck();// 算法1:基于栈的检测
RceCommonCheck rceCommonCheck = new RceCommonCheck(null);// 算法2:常用渗透命令
RceDnsCheck rceDnsCheck = new RceDnsCheck();// 算法3: DNSlog检测
RceOtherCheck rceOtherCheck = new RceOtherCheck();// 算法4: 记录所有的命令执行
list.add(rceReflectCheck);
list.add(rceCommonCheck);
list.add(rceDnsCheck);
list.add(rceOtherCheck);
algorithmManager.register(this);
}
@Override
public void onUnload() throws Throwable {
// 算法卸载
algorithmManager.destroy(this);
for (int i = 0; i < list.size(); i++) {
list.get(i).close();
}
// 算法模块清空
list = null;
}
所以前面调用doCheck方法,会直接进入该算法的check方法中处理
查看com.jrasp.module.rcenative.algorithm.RceAlgorithm类的check方法,其中返回的状态可以通过jrasp/cfg/config.json中的rce_reflct_check_action等参数的参数值。
其检测思路就是调用RCE检测算法模块中的runCheckUnit方法
@Override
public CheckResult runCheckUnit(String[] stack, Object... parameters) {
if (action != -1) {
boolean userCode = false, reachedInvoke = false;
String message = "";
int i = 0;
if (stack.length > 3 && stack[0].startsWith("sun.reflect.GeneratedMethodAccessor")
&& "sun.reflect.GeneratedMethodAccessorImpl.invoke".equals(stack[1])
&& "java.lang.reflect.Method.invoke".equals(stack[2])
) {
i = 3;
}
for (; i < stack.length; i++) {
String method = stack[i];
// 命令执行----->用户代码----->反射调用
if (!reachedInvoke) {
if ("java.lang.reflect.Method.invoke".equals(method)) {
reachedInvoke = true;
}
// 用户代码,即非 JDK、com.jrasp 相关的函数
if (!method.startsWith("java.")
&& !method.startsWith("sun.")
&& !method.startsWith("com.sun.")
&& !method.startsWith("com.jrasp.")) {
userCode = true;
}
}
if (method.startsWith("ysoserial.Pwner")) {
message = "Using YsoSerial tool";
break;
}
if (method.startsWith("net.rebeyond.behinder")) {
message = "Using BeHinder defineClass webshell";
break;
}
if (method.startsWith("com.fasterxml.jackson.databind.")) {
message = "Using Jackson deserialze method";
break;
}
// 对于如下类型的反射调用:
// 1. 仅当命令直接来自反射调用才拦截
// 2. 如果某个类是反射生成,这个类再主动执行命令,则忽略
if (!userCode) {
if ("ognl.OgnlRuntime.invokeMethod".equals(method)) {
message = "Using OGNL library";
break;
} else if ("java.lang.reflect.Method.invoke".equals(method)) {
message = "Unknown vulnerability detected";
}
}
// 本算法的核心检测逻辑
if (attackStackSet.contains(method)) {
message = method;
}
}
if (!"".equals(message)) {
CheckResult result = new CheckResult(action);
// 确定是攻击
result.setConfidence(100);
result.setMessage(message);
result.setAlgorithm(checkName);
// 阻断
result.setCanBlock(true);
return result;
}
}
return null;
}
前面做一些常规检测,之后在判断堆栈的方法是否在attackStackSet存在
而attackStackSet就是添加的黑名单Set列表
以上就是整个agent加载模块到检测的全过程
事件是通过filebeat监控日志文件输出到Kafka,可以看官方给出的安装脚本:http://www.jrasp.com/guide/install/filebeat.html
filebeat.yaml文件的内容如下
filebeat.inputs:
- type: log
fields:
kafka_topic: "jrasp-daemon"
paths:
- /usr/local/jrasp/logs/jrasp-daemon.log
- type: log
fields:
kafka_topic: "jrasp-agent"
paths:
- /usr/local/jrasp/logs/jrasp-agent.log
- type: log
fields:
kafka_topic: "jrasp-module"
paths:
- /usr/local/jrasp/logs/jrasp-module.log
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
setup.template.settings:
index.number_of_shards: 1
output.kafka:
enabled: true
hosts: ["kafka1:9092","kafka2:9092","kafka3:9092"]
topic: '%{[fields.kafka_topic]}'
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
- add_docker_metadata: ~
- add_kubernetes_metadata: ~
processors:
- decode_json_fields:
fields: ['message']
target: ''
overwrite_keys: true
- drop_fields:
fields: ["host","agent","log","input","ecs","@timestamp"]
logging.level: info
可以从yaml配置文件中看出三个不同的jrasp日志文件会通过filebeat上报输出给Kafak,此时再从后台订阅消息并处理事件即可查看到漏洞信息。
最后,非常感谢jrasp的作者Patton师傅的答疑,在遇到环境问题和代码问题的时候所给予的帮助!
[1].https://www.jrasp.com
[2].https://blog.csdn.net/zxlyx/article/details/124120795
[3].https://www.wangan.com/docs/90
[4].https://www.elastic.co/guide/en/beats/filebeat/current/command-line-options.html
[5].https://www.cnblogs.com/lsdb/p/7762871.html