欢迎来到Mirai解密
活动详细
持续时间 | 7天 |
开始时间 | 四月一日 |
游戏形式类似信息安全中的ctf比赛,但是题目的设置并不完全是ctf的类型,这是为了让不会信息安全的玩家也能通过搜索引擎和动脑来参与到游戏中。
前三位完成解密的玩家可以获得奖品一份,游戏为个人参加,请勿泄漏各种信息给其他人
持续时间 | 7天 |
开始时间 | 四月一日 |
游戏形式类似信息安全中的ctf比赛,但是题目的设置并不完全是ctf的类型,这是为了让不会信息安全的玩家也能通过搜索引擎和动脑来参与到游戏中。
前三位完成解密的玩家可以获得奖品一份,游戏为个人参加,请勿泄漏各种信息给其他人
由于 gitee 停止 raw 的直接访问, 我们已经将 mirai-console-loader 的数据文件从 gitee 搬出
要继续使用使用 mirai-console-loader, 您需要进行以下的配置
1. 打开 $MCL/config.json
2. 找到
"mirai_repo": "....."
将其地址修改为以下地址中的一个 (注意不要删除双引号 ""
(英文半角))
前言: 仅研究 JDK 9+, JDK 8- 无研究意义
从 Java 9
开始, Java 引入了一个新的概念, 模块(Module)
. 模块的存在, 限制了反射技术, 在 JDK 16
中, 直接反射越权修改 java.base
甚至会得到错误 java.lang.reflect.InaccessibleObjectException
, 对于某些需要的 devops 而言意味着无法完成预期操作
在阅读 java.lang.reflect.AccessibleObject
源码后, 有如下代码片段
/**
* If the given AccessibleObject is a {@code Constructor}, {@code Method}
* or {@code Field} then checks that its declaring class is in a package
* that can be accessed by the given caller of setAccessible.
*/
void checkCanSetAccessible(Class<?> caller) {
// do nothing, needs to be overridden by Constructor, Method, Field
}
final void checkCanSetAccessible(Class<?> caller, Class<?> declaringClass) {
checkCanSetAccessible(caller, declaringClass, true);
}
private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
}
Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();
if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;
String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {
modifiers = ((Field) this).getModifiers();
}
// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// member is public
if (Modifier.isPublic(modifiers)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
}
// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
logIfOpenedForIllegalAccess(caller, declaringClass);
return true;
}
if (throwExceptionIfDenied) {
// not accessible
String msg = "Unable to make ";
if (this instanceof Field)
msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;
InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
}
throw e;
}
return false;
}
有两个关键判断逻辑: declaringModule.isExported(pn, callerModule)
, declaringModule.isOpen(pn, callerModule)
阅读 Module.java
后发现有 implAddExports
方法, 通过 IDEA
查找调用引用发现了 java.lang.System
有访问此方法的 JDK Internal API
private static void setJavaLangAccess() {
// Allow privileged classes outside of java.lang
SharedSecrets.setJavaLangAccess(new JavaLangAccess() {
public Module defineModule(ClassLoader loader,
ModuleDescriptor descriptor,
URI uri) {
return new Module(null, loader, descriptor, uri);
}
public Module defineUnnamedModule(ClassLoader loader) {
return new Module(loader);
}
public void addReads(Module m1, Module m2) {
m1.implAddReads(m2);
}
public void addReadsAllUnnamed(Module m) {
m.implAddReadsAllUnnamed();
}
public void addExports(Module m, String pn, Module other) {
m.implAddExports(pn, other);
}
public void addExportsToAllUnnamed(Module m, String pn) {
m.implAddExportsToAllUnnamed(pn);
}
public void addOpens(Module m, String pn, Module other) {
m.implAddOpens(pn, other);
}
public void addOpensToAllUnnamed(Module m, String pn) {
m.implAddOpensToAllUnnamed(pn);
}
public void addOpensToAllUnnamed(Module m, Set<String> concealedPackages, Set<String> exportedPackages) {
m.implAddOpensToAllUnnamed(concealedPackages, exportedPackages);
}
public void addUses(Module m, Class<?> service) {
m.implAddUses(service);
}
});
}
找到了 JDK 提供的后门之后, 我们只需要调用 SharedSecrets.getJavaLangAccess().addExports
就能开后门了....
不对,目前还无法调用 SharedSecrets
, 还需要一些手段....
在 java.lang.reflect
中翻到了一个特别的东西, java.lang.reflect.Proxy
, 她是破局的关键中心
抱着好奇的心里, 我尝试了使用 Proxy
实现 jdk.internal.access
中的一个接口玩玩
public static void main(String[] args) throws Exception {
var obj = Proxy.newProxyInstance(
Usffsa.class.getClassLoader(),
new Class[]{Class.forName("jdk.internal.access.JavaLangAccess")},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
);
System.out.println(obj);
}
没想到, 运行成功了(eg: 没有对应权限(Exported
)是不能实现对应接口的), 迎接着激动的心情, 输出了更多的详细信息
System.out.println(obj);
System.out.println(obj.getClass());
System.out.println(obj.getClass().getModule());
System.out.println(Object.class.getModule().isExported("jdk.internal.access", obj.getClass().getModule()));
null
class com.sun.proxy.jdk.proxy1.$Proxy0
module jdk.proxy1
true
破局点找到了, java.lang.reflect.Proxy
拥有打开模块访问的权利, 然后尝试对该模块进行注入
public static void main(String[] args) throws Exception {
var ccl = new ClassLoader(Usffsa.class.getClassLoader()) {
Class<?> defineClass(byte[] code) {
return defineClass(null, code, 0, code.length);
}
};
var obj = Proxy.newProxyInstance(
ccl,
new Class[]{Class.forName("jdk.internal.access.JavaLangAccess")},
(proxy, method, args1) -> null
);
var writer = new ClassWriter(0); // org.objectweb.asm.ClassWriter
writer.visit(Opcodes.V1_8, 0,
obj.getClass().getPackageName().replace('.', '/') + "/Test0",
null,
"java/lang/Object",
null
);
var injectedClass = ccl.defineClass(writer.toByteArray());
System.out.println("Proxy Module : " + obj.getClass().getModule());
System.out.println("Injected Module : " + injectedClass.getModule());
System.out.println("Is Same Module : " + (injectedClass.getModule() == obj.getClass().getModule()));
}
Proxy Module : module jdk.proxy1
Injected Module : module jdk.proxy1
Is Same Module : true
至此已经破开了 JVM 的模块限制的死局, 实际应用可参考 [Karlatemp/UnsafeAccessor]
本帖子将展示 MiraiForum 的额外 markdown 语法
这里是MiraiForum
+=[要隐藏的内容]=+
Colored Text Here
%(#66ccff)[Colored Text Here]
要折叠的内容
注: 咱不支持嵌套引用
> ^fold
>
> 要折叠的内容
>
在使用高版本的时候, 总会不可避免的接触到模块系统, 比如反射操作 java.base
已经十分困难. 既然 JDK 内部可以享受到模块的保护, 那么我们自己的代码是否也可以享受到模块系统的保护呢
当然可以,而且也不是非常麻烦。
使用模块,你将面对以下问题
--add-opens=....=ALL-UNNAMED
的归属判断使用模块的适用情况
注: 此处的定义指的是, 通过运行时代码在运行时定义一个模块.
而不是大多数资料说的直接写一个module-info.java
要定义一个模块, 首先需要一个模块的描述符文件 (ModuleDescriptor
), 可以从以编码文件读取 (ModuleDescriptor.read(InputStream) <- module-info.class
), 也可以在运行时动态生成一个(ModuleDescriptor.newModule("name_of_module").build()
)
jvm 通过包来区分模块, 而一个模块的全部包都需要提前指定, jvm 才会为这些包分配到一个模块内
var moduleDescriptor = ModuleDescriptor.newModule("my.custom_module")
.packages(Set.of("io.github.karlatemp.jmse.main"))
.exports("io.github.karlatemp.jmse.main")
.build();
这里我们已经拥有了一个模块的描述符, 现在我们还需要一个模块描述的引用, 以及一个模块查找器以让 jvm 可以找到我们的模块
var myModuleReference = new ModuleReference(
moduleDescriptor, null
) {
@Override public ModuleReader open() throws IOException {
throw new UnsupportedOperationException();
}
}
var myModuleFinder = new ModuleFinder() {
@Override
public Optional<ModuleReference> find(String name) {
if (name.equals(moduleDescriptor.name())) {
return Optional.of(myModuleReference);
}
return Optional.empty();
}
@Override
public Set<ModuleReference> findAll() {
return Set.of(myModuleReference);
}
};
最后,定义一个模块
var bootLayer = ModuleLayer.boot();
var myConfiguration = bootLayer.configuration().resolve(
myModuleFinder, ModuleFinder.of(), Set.of(moduleDescriptor.name())
);
var classLoader = ClassLoader.getSystemClassLoader();
var controller = ModuleLayer.defineModules(
myConfiguration, List.of(bootLayer), $ -> classLoader
);
Class.forName("io.github.karlatemp.jmse.main.ModuleMain", false, classLoader)
.getMethod("launch")
.invoke(null);
还记得 需要专门的 ClassLoader / 需要自行实现 ClassLoader
吗, 虽然在上文已经成功定义了一个模块,但是只要使用 ServiceLoader
/ Class.forName(Module, String)
, 那么将无法找到对应的类, 因为一般的 ClassLoader 并没有专门处理动态加载的模块
通过进行调用分析, 最终可以发现以上两个东西最终都进入到了下面的方法
public class ClassLoader {
final Class<?> loadClass(Module module, String name) {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(module.getName(), name);
}
if (c != null && c.getModule() == module) {
return c;
} else {
return null;
}
}
}
protected Class<?> findClass(String moduleName, String name) {
if (moduleName == null) {
try {
return findClass(name);
} catch (ClassNotFoundException ignore) { }
}
return null;
}
}
不难发现, 由于默认没有处理模块, 导致指定搜索模块的时候将搜索不到动态定义的模块
而 jdk.internal.loader.ClassLoader$AppClassLoader
并没有处理通过 ModuleLayer.defineModule
定义的模块, 于是也不能直接将模块定义到系统类加载器
自行实现类加载器十分简单,只需要
public class MyCustomClassLoader extends URLClassLoader {
String moduleName;
@Override
protected Class<?> findClass(String moduleName, String name) {
// System.out.println("Find class: " + moduleName + "/" + name);
if (this.moduleName.equals(moduleName)) {
try {
return findClass(name);
} catch (ClassNotFoundException ignored) {
}
}
return super.findClass(moduleName, name);
}
}
只需要实现 ModuleReference.open(): ModuleReader
, 然后使用
var controller = ModuleLayer.defineModulesWithOneLoader(
myConfiguration,
List.of(bootLayer),
ClassLoader.getSystemClassLoader().getParent()
);
var classLoader = controller.layer().findLoader(moduleDescriptor.name());
即可使用 JDK 内置的内加载器
分区 | title | 链接 |
---|---|---|
- | 常见问题 QA | https://mirai.mamoe.net/topic/71/ |
DEV | 忽略某个用户的全部消息 | https://mirai.mamoe.net/topic/327/ |
DEV | 在 bot 发言前进行检查 | https://mirai.mamoe.net/topic/599/ |
DEV | 在 console 命令系统以外的地方使用权限系统 | https://mirai.mamoe.net/topic/535/ |
DEV | 发送网络图片 | https://mirai.mamoe.net/topic/337/ |
DEV | 在 MemberJoinEvent 发出的 @ 无法识别 |
https://github.com/mamoe/mirai/issues/1559 |
DEV | 群名片修改事件没有触发 | https://github.com/mamoe/mirai/issues/1570 |
DEV | 自定义 mirai-console 日志系统 | https://github.com/mamoe/mirai-console/issues/412 |
USE | Mirai Console Loader | https://mirai.mamoe.net/topic/177/ |
USE | chat-command | https://github.com/project-mirai/chat-command |
USE | LuckPerms Mirai - 高级权限服务插件 | https://mirai.mamoe.net/topic/68 |
USE | 命令无法执行 | https://mirai.mamoe.net/topic/184/ |
USE | Mirai api http | https://github.com/project-mirai/mirai-api-http |
DOC | Mirai UserManual | https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md |
DOC | SDK List | https://github.com/mamoe/mirai/tree/dev/docs#社区-sdk |
Notes:
- 此处列出的插件为最核心最常用的插件, 需要更多插件请查看 https://mirai.mamoe.net/category/6/
- 如果有较为常见的问题此处没有列出的可以在本贴内回复
@Cuki 你需要让她至少执行一次命令(比如/help, 只需要让她发出去就行),为了减少数据占用单纯的聊天是不会进行数据初始化的
优化 java gui 主题(显示)
Maven: com.kasukusakura:java-flatlaf-style-setup
GitHub: KasukuSakura/java-flatlaf-style-setup
Snapshot:
>= mirai-core 2.0-RC
>= mirai-console 2.0-RC
一款高级易使用的 mirai-console 权限服务插件
Way 1. By MCL: mcl --update-package io.github.karlatemp:luckperms --channel nightly --type plugin
Way 2. Download release from Releases. Then put it into plugins
LuckPerms-Mirai 基于 LuckPerms 开发, 详细用法请百度/谷歌/阅读 LuckPerms wiki
LuckPerms-Mirai 的身份上下文使用 context 实现, 可以在聊天中使用
/lp user <****> info
查看上下文
下面是一些示例命令
// 授予群聊管理员(包含群主)一项权限
/lp group default permission set AdminPermission admin=true
// 授予群主一条权限
/lp group default permission set OwnerPermission level=owner
// 授予管理员(不含群主)一条权限
/lp group default permission set OwnerPermission level=admin
// 授予在某个群的所有人一条权限
/lp group default permission set PermissionInGroup group=1234567890
// 授予某个群的群聊管理员一条权限
/lp group default permission set PermissionInGroup group=1234567890 admin=true
// 创建系统管理组
/lp creategroup root
/lp group root permission set *
/lp user 1234567890 parent set root
// 开启权限调试模式 (debug(verbose) mode)
// 查看具体权限名
// WARNING: Dont run this command in chatting
/lp verbose on
// 开启权限调试模式 (debug(verbose) mode), 并在 Web 查看
/lp verbose record
//WAIT.....
/lp verbose upload