有沒(méi)有想過(guò),XMind是如何被破解的?那么今天我們就來(lái)看看javaassit這項(xiàng)技術(shù),其實(shí)在你接觸的很多其他工具中這個(gè)工具早就被廣泛使用了
javaassit
我們知道,java是一門(mén)面向?qū)ο蟮?a target="_blank">編程語(yǔ)言,更是一門(mén)面向切面的編程語(yǔ)言,正是這個(gè)特性,讓Java更加地靈活。
可能你寫(xiě)過(guò)基于Spring AOP的代碼,其原理都是基于JDK動(dòng)態(tài)代理或者CGLIB來(lái)實(shí)現(xiàn),其局限性在于我們只能以方法作為連接點(diǎn),來(lái)實(shí)現(xiàn)基于方法執(zhí)行過(guò)程的代理。
你可還知道更厲害的代理工具:AspectJ、javaassit,這些都是基于字節(jié)碼,屬于更底層,但是功能更強(qiáng)大的代理。
知識(shí)點(diǎn)
- ASM
通過(guò)指令修改class字節(jié)碼,主要基于ClassReader結(jié)合JVM指令集直接操作字節(jié)碼,Cglib即是通過(guò)該技術(shù)實(shí)現(xiàn)。
- JavaAssit
基于org.javassist:javassist類(lèi)庫(kù)提供的CtPool工具類(lèi)對(duì)字節(jié)碼進(jìn)行修改
- Instrumentation
JVM提供的一個(gè)可以修改已加載類(lèi)的類(lèi)庫(kù),通過(guò)編寫(xiě)java代碼即可完成對(duì)字節(jié)碼的修改
- JavaAgent
JVM加載類(lèi)之前與JVM運(yùn)行時(shí),基于JavaAssit、Instrumentation實(shí)現(xiàn)字節(jié)碼修改并加載到JVM
應(yīng)用場(chǎng)景
- IDE的調(diào)試功能,例如 Eclipse、IntelliJ IDEA
- 熱部署功能,例如 JRebel、XRebel、spring-loaded
- 線(xiàn)上診斷工具,例如 Btrace、Greys,還有阿里的 Arthas
- 性能分析工具,例如 Visual VM、JConsole、TProfiler等
- 全鏈路性能檢測(cè)工具,例如 Skywalking、Pinpoint等
示例
下面我們基于javaagent以及運(yùn)行時(shí)Attach的模式看下javaassit如何實(shí)現(xiàn)目標(biāo)類(lèi)的代理的:
基于javaagent
- 編寫(xiě)代理類(lèi)
方法簽名固定,方法名為 premain ,參數(shù)分別對(duì)應(yīng)args(不是數(shù)組)以及Instrumentation
public class JavaAgent {
private static final String TARGET_CLASS_NAME = "com.sucl.blog.javaassit.Target";
public static void premain(String args, Instrumentation instrumentation){
AgentHelper.create(TARGET_CLASS_NAME).proxy(args, instrumentation);
}
}
- 打包代理類(lèi)
這里我們借助maven插件 maven-shade-plugin ,主要是為了打包時(shí)修改/META-INF/MANIFEST.MF文件,需要加上Premain-Class這項(xiàng)
< plugin >
< groupId >org.apache.maven.plugins< /groupId >
< artifactId >maven-shade-plugin< /artifactId >
< version >2.3< /version >
< executions >
< execution >
< phase >package< /phase >
< goals >
< goal >shade< /goal >
< /goals >
< configuration >
< transformers >
< transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer" >
< manifestEntries >
< Premain-Class >com.sucl.blog.agent.JavaAgent< /Premain-Class >
< Agent-Class >com.sucl.blog.agent.AttachAgent< /Agent-Class >
< Can-Redefine-Classes >true< /Can-Redefine-Classes >
< Can-Retransform-Classes >true< /Can-Retransform-Classes >
< /manifestEntries >
< /transformer >
< /transformers >
< /configuration >
< /execution >
< /executions >
< /plugin >
- 編寫(xiě)測(cè)試類(lèi)
目的很簡(jiǎn)單,每隔3秒打印當(dāng)前時(shí)間
public class JavaAgentMain {
public static void main(String[] args) throws InterruptedException {
Target target = new Target();
while (true) {
target.print(new Date());
TimeUnit.SECONDS.sleep(3);
}
}
}
@Slf4j
class Target {
public void print(Object obj) {
log.info("打印內(nèi)容:{}", obj);
}
}
- 配置代理
如何讓我們編寫(xiě)的代理生效,這里提供兩種方法:
- 當(dāng)你使用IDEA啟動(dòng)時(shí),可以在Config Configurations中通過(guò)配置VM OPTION,添加如下內(nèi)容:
-javaagent:/your_jar_path/agent.jar=param=value
- 當(dāng)你使用java命令啟動(dòng)時(shí):
java -javaagent:/path/agent.jar=param=value -jar xxx.jar
- 測(cè)試
執(zhí)行測(cè)試類(lèi)main方法,你可以看到,在打印時(shí)間前后,分別會(huì)打印“開(kāi)始執(zhí)行方法:print”,“結(jié)束執(zhí)行方法:print”,這也是我們代理類(lèi)實(shí)現(xiàn)的功能。
>> > 開(kāi)始執(zhí)行方法:print
14:46:09.457 [main] INFO com.sucl.blog.javaassit.Target - 打印內(nèi)容:Fri Mar 10 14:46:09 CST 2023
>> > 結(jié)束執(zhí)行方法:print
基于Attach
- 編寫(xiě)代理類(lèi)
方法簽名固定,方法名為 attachmain ,參數(shù)分別對(duì)應(yīng)args(不是數(shù)組)以及Instrumentation; 和上面的相比唯一的不同是方法名稱(chēng)。
public class AttachAgent {
private static final String TARGET_CLASS_NAME = "com.sucl.blog.javaassit.Target";
public static void agentmain(String args, Instrumentation instrumentation){
System.out.println(String.format(" >> > agentmain starting, args: %s",args));
AgentHelper.create(TARGET_CLASS_NAME).proxy(args, instrumentation);
System.out.println(String.format(" >> > agentmain finished"));
}
}
- 打包代理類(lèi)
同樣借助插件 maven-shade-plugin ,主要是為了打包時(shí)修改/META-INF/MANIFEST.MF文件,需要加上Agent-Class這項(xiàng)
< !-- 省略 ...-- >
< Agent-Class >com.sucl.blog.agent.AttachAgent< /Agent-Class >
< !-- 省略 ...-- >
注意,這里我們使用了ClassPool、CtClass、CtMethod相關(guān)的類(lèi),記得在pom.xml中引入對(duì)應(yīng)的依賴(lài)
< dependency >
< groupId >org.javassist< /groupId >
< artifactId >javassist< /artifactId >
< /dependency >
- 編寫(xiě)測(cè)試類(lèi)
測(cè)試類(lèi)完全一樣,由于啟動(dòng)代理織入的方式不一樣,因此分為兩個(gè)類(lèi)
public class AttachAgentMain {
public static void main(String[] args) throws InterruptedException {
Target target = new Target();
while (true) {
target.print(new Date());
TimeUnit.SECONDS.sleep(3);
}
}
}
- 執(zhí)行代理
如何將編寫(xiě)的代碼(AttachAgent)織入到目標(biāo)類(lèi)完成對(duì)目標(biāo)類(lèi)(Target)方法的代理?
這里我們需要用到j(luò)dk中的tool.jar,你可以在測(cè)試模塊中添加下面的依賴(lài):
< dependency >
< groupId >com.sun< /groupId >
< artifactId >tools< /artifactId >
< version >1.8< /version >
< scope >system< /scope >
< systemPath >${java.home}/../lib/tools.jar< /systemPath >
< /dependency >
如何在運(yùn)行時(shí)進(jìn)行代理織入:
public class AttachAgentTests {
private static String JAR_PATH = AttachAgentTests.class.getClassLoader().getResource("").getPath().replace("test-classes/","")+"agent.jar";
@Test
public void attachAgent() throws Exception {
String pid = findPid(KEY); // 通過(guò)jps命令找到AttachAgentMain執(zhí)行的pid
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
virtualMachine.loadAgent(JAR_PATH.substring(1));
virtualMachine.detach();
}
}
- 測(cè)試
- a. 先執(zhí)行測(cè)試代碼(AttachAgentMain.java),此時(shí)每間隔3秒會(huì)打印當(dāng)前時(shí)間。
- b. 執(zhí)行代理織入方法(AttachAgentTests#attachAgent)
- c. 觀察測(cè)試代碼輸出結(jié)果,你會(huì)會(huì)發(fā)現(xiàn)此時(shí)每次打印時(shí)間前后都會(huì)有“開(kāi)始執(zhí)行方法:print”,“結(jié)束執(zhí)行方法:print”
AgentHelper
public class AgentHelper {
private String targetClassName;
private AgentHelper(String targetClassName) {
this.targetClassName = targetClassName;
}
public static AgentHelper create(String targetClassName){
AgentHelper agentHelper = new AgentHelper(targetClassName);
return agentHelper;
}
public void proxy(String args, Instrumentation instrumentation){
Class targetClass = obtainTargetClass(instrumentation);
try {
instrumentation.addTransformer(new SimpleTransformer(targetClassName), true);
instrumentation.retransformClasses(targetClass); //
} catch (Exception e) {
System.out.println(String.format(" >> > agentmain failure, error: %s: %s", e.getClass().getName(),e.getLocalizedMessage()));
e.printStackTrace();
}
}
private Class obtainTargetClass(Instrumentation instrumentation) {
Class targetClass = null;
for (Class loadedClass : instrumentation.getAllLoadedClasses()) {
if(targetClassName.equals(loadedClass.getName())){
targetClass = loadedClass;
}
}
if(targetClass == null){
try {
// 無(wú)法加載
targetClass = Class.forName(targetClassName);
} catch (ClassNotFoundException e) {
System.out.println(String.format(" >> > Class [%s] not found", targetClassName));
}
}
return targetClass;
}
public static class SimpleTransformer implements ClassFileTransformer {
private String targetClassName;
public SimpleTransformer(String targetClassName) {
this.targetClassName = targetClassName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class< ? > classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(!className.equals(targetClassName.replaceAll(".","/"))){
return null;
}
ClassPool classPool = ClassPool.getDefault();
System.out.println(String.format("+++++ 代理類(lèi)名:%s", className));
try {
CtClass ctClass = classPool.get(className.replace("/","."));
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
for (CtMethod ctMethod : ctMethods) { // 所有類(lèi)方法
ctMethod.insertBefore(String.format("{System.out.println(" >> > 開(kāi)始執(zhí)行方法:%s");}",ctMethod.getName()));
ctMethod.insertAfter(String.format("{System.out.println(" >> > 結(jié)束執(zhí)行方法:%s");}",ctMethod.getName()));
}
return ctClass.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
System.out.println(String.format("+++++ 代理出錯(cuò):%s",e.getMessage()));
e.printStackTrace();
}
return classfileBuffer;
}
}
}
通過(guò)上面的例子可以看到,兩種方式的比對(duì)如下:
對(duì)比 | JavaAgent | AttachAgent |
---|---|---|
/META-INF/MANIFEST.MF | Premain-Class | Agent-Class |
代理類(lèi)方法名稱(chēng) | premain | attachmain |
代理入口 | VM配置:-javaagent | JVM attach進(jìn)程ID |
代理時(shí)機(jī) | JVM加載字節(jié)碼時(shí) | 程序運(yùn)行時(shí) |
作用 | Java桌面程序 | Web應(yīng)用 |
原理
代理可以發(fā)送在編譯時(shí),類(lèi)加載時(shí)或者是運(yùn)行時(shí)。
這里你要清楚, java程序的入口是main方法 ,不管是普通程序(比如桌面應(yīng)用、可執(zhí)行jar)或是Web應(yīng)用(在Web容器中運(yùn)行的基于Servlet的應(yīng)用)
以javaagent為例,是在執(zhí)行main方法前對(duì)已經(jīng)加載到JVM的類(lèi)進(jìn)行修改,從而實(shí)現(xiàn)對(duì)目標(biāo)類(lèi)的代理,這里的修改是在字節(jié)碼層面的,當(dāng)然我們可以基于ASM工具庫(kù)來(lái)實(shí)現(xiàn),但是門(mén)檻太高。
基于Instrumentation可以與編寫(xiě)java代碼一樣,實(shí)現(xiàn)修改字節(jié)碼來(lái)
ClassPool:保存CtClass的池子,通過(guò)classPool.get(類(lèi)全路徑名)來(lái)獲取CtClass CtClass:編譯時(shí)類(lèi)信息,它是一個(gè)class文件在代碼中的抽象表現(xiàn)形式 CtMethod:對(duì)應(yīng)類(lèi)中的方法 CtField:對(duì)應(yīng)類(lèi)中的屬性、變量
XMind
還記得XMind8的破解之法嗎?
是不是需要在XMind.ini文件中插入這樣一段:-javaagent:.../XMindCrack.jar 要是你打開(kāi)這個(gè)jar,你會(huì)看到這樣的內(nèi)容:
首先你需要知道其原理,是通過(guò)/plugins/net.xmind.verify.jar中提供的方法LicenseVerifier#doCheckLicenseKeyBlacklisted來(lái)進(jìn)行身份校驗(yàn)
我們是不是只用修改License的校驗(yàn)方法 doCheckLicenseKeyBlacklisted ,忽略其校驗(yàn)過(guò)程并直接返回true就完事了?當(dāng)然截圖中就是這樣做的,如果你想看懂那幾行代碼,可能你先要去學(xué)習(xí)ASM相關(guān)的知識(shí)。
InsnList insnList = methodNode.instructions;
insnList.clear();
insnList.add((AbstractInsnNode)new InsnNode(4));
insnList.add((AbstractInsnNode)new InsnNode(172));
methodNode.exceptions.clear();
methodNode.visitEnd();
以上代碼其實(shí)就是講方法體清除,并寫(xiě)入“return true”
結(jié)束語(yǔ)
通過(guò)示例了解javaassit如何實(shí)現(xiàn)代對(duì)目標(biāo)類(lèi)的代理。是不是覺(jué)得java應(yīng)用程序都能被修改,那不是太不安全了?所以,你覺(jué)得呢...
-
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104701 -
編程語(yǔ)言
+關(guān)注
關(guān)注
10文章
1942瀏覽量
34707 -
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68521 -
代理
+關(guān)注
關(guān)注
1文章
44瀏覽量
11203
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論