From 1477add1bd8b9bb48e28229b2f3469ca5d65ccd0 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Thu, 19 Sep 2024 18:19:42 +0800 Subject: [PATCH 01/13] revamped all icons --- core/build.gradle.kts | 2 +- .../java/me/nov/threadtear/Threadtear.java | 4 +- .../swing/panel/ConfigurationPanel.java | 14 ---- .../main/resources/me/nov/threadtear/add.svg | 14 +--- .../me/nov/threadtear/add_disabled.svg | 14 +--- .../resources/me/nov/threadtear/analysis.svg | 15 ++--- .../me/nov/threadtear/analysis_disabled.svg | 15 ++--- .../resources/me/nov/threadtear/bit_qr.png | Bin 4511 -> 0 bytes .../resources/me/nov/threadtear/bytecode.svg | 16 ++--- .../me/nov/threadtear/bytecode_disabled.svg | 28 +++++--- .../resources/me/nov/threadtear/class.svg | 9 --- .../resources/me/nov/threadtear/decompile.svg | 15 +---- .../me/nov/threadtear/decompile_disabled.svg | 15 +---- .../main/resources/me/nov/threadtear/enum.svg | 11 ++- .../resources/me/nov/threadtear/failure.svg | 24 +++---- .../resources/me/nov/threadtear/field.svg | 12 ++-- .../main/resources/me/nov/threadtear/file.svg | 17 ++--- .../me/nov/threadtear/file_disabled.svg | 27 +++++--- .../resources/me/nov/threadtear/folder.svg | 12 +--- .../resources/me/nov/threadtear/graph.svg | 21 ++---- .../me/nov/threadtear/graph_disabled.svg | 21 ++---- .../resources/me/nov/threadtear/ignore.svg | 12 +--- .../me/nov/threadtear/ignore_disabled.svg | 24 ++++--- .../me/nov/threadtear/innerClass.svg | 11 ++- .../resources/me/nov/threadtear/interface.svg | 11 ++- .../me/nov/threadtear/load_config.svg | 18 ++--- .../resources/me/nov/threadtear/mainClass.svg | 18 ++--- .../resources/me/nov/threadtear/method.svg | 12 ++-- .../resources/me/nov/threadtear/move_down.svg | 11 +-- .../me/nov/threadtear/move_down_disabled.svg | 11 +-- .../resources/me/nov/threadtear/move_up.svg | 11 +-- .../me/nov/threadtear/move_up_disabled.svg | 11 +-- .../resources/me/nov/threadtear/package.svg | 13 ++-- .../resources/me/nov/threadtear/refresh.svg | 15 ++--- .../me/nov/threadtear/refresh_disabled.svg | 15 ++--- .../resources/me/nov/threadtear/remove.svg | 11 +-- .../me/nov/threadtear/remove_disabled.svg | 23 ++++--- .../main/resources/me/nov/threadtear/run.svg | 8 +-- .../main/resources/me/nov/threadtear/save.svg | 17 ++--- .../me/nov/threadtear/save_config.svg | 15 ++--- .../nov/threadtear/save_config_disabled.svg | 24 ++++--- .../resources/me/nov/threadtear/skidsuite.svg | 6 ++ .../me/nov/threadtear/threadtear.svg | 63 ------------------ .../resources/me/nov/threadtear/zoom_in.svg | 17 ++--- .../resources/me/nov/threadtear/zoom_out.svg | 15 ++--- .../me/nov/threadtear/zoom_reset.svg | 16 +---- 46 files changed, 246 insertions(+), 468 deletions(-) delete mode 100644 gui/src/main/resources/me/nov/threadtear/bit_qr.png delete mode 100644 gui/src/main/resources/me/nov/threadtear/class.svg create mode 100644 gui/src/main/resources/me/nov/threadtear/skidsuite.svg delete mode 100644 gui/src/main/resources/me/nov/threadtear/threadtear.svg diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f589207..3e164bb 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,7 +18,7 @@ dependencies { implementation("org.ow2.asm:asm-util") implementation("org.ow2.asm:asm-commons") - implementation("com.github.leibnitz27:cfr") { isChanging = true } + implementation("com.github.leibnitz27:cfr:0.151") implementation("ch.qos.logback:logback-classic") externalLib("fernflower-15-05-20") diff --git a/gui/src/main/java/me/nov/threadtear/Threadtear.java b/gui/src/main/java/me/nov/threadtear/Threadtear.java index d6b7d76..ea7f192 100644 --- a/gui/src/main/java/me/nov/threadtear/Threadtear.java +++ b/gui/src/main/java/me/nov/threadtear/Threadtear.java @@ -32,8 +32,8 @@ public class Threadtear extends JFrame { public Threadtear() { logFrame = new LogFrame(); this.initBounds(); - this.setTitle("Threadtear " + CoreUtils.getVersion()); - this.setIconImage(SwingUtils.iconToFrameImage(SwingUtils.getIcon("threadtear.svg", true), this)); + this.setTitle("SkidSuite " + CoreUtils.getVersion()); + this.setIconImage(SwingUtils.iconToFrameImage(SwingUtils.getIcon("skidsuite.svg", true), this)); this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); this.addWindowListener(new ExitListener(this)); this.initializeFrame(); diff --git a/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java b/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java index 7c4d221..cab91b4 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java @@ -109,20 +109,6 @@ private JPanel createBottomButtons() { JOptionPane.showMessageDialog(this, "You have to load a jar file first."); return; } - if (main.listPanel.executionList.getExecutions().size() > 0) { - JTextArea ta = new JTextArea(); - ta.setText("This project is entirely open-source and many hours have went into developing it.\n" + - "Please consider donating a small amount, if you are happy with your deobfuscation results.\n" + - "Every paid coffee will result in motivation to develop this tool, as it lives of it.\n" + - "You can also contact me on twitter (@graxcoding) for more options.\n" + - "Thank you.\n\n" + - "Bitcoin adress: 3LfBXghKn8KAj74tyetaUdJLic4NpGY3Vr"); - ta.setCaretPosition(0); - ta.setEditable(false); - JOptionPane.showMessageDialog(this, - ta, "Consider donating", - JOptionPane.INFORMATION_MESSAGE, SwingUtils.getIcon("bit_qr.png", 150, 150)); - } JFileChooser jfc = new JFileChooser(inputFile.getParentFile()); jfc.setAcceptAllFileFilterUsed(false); jfc.setSelectedFile(new File(FilenameUtils.removeExtension(inputFile.getAbsolutePath()) + ".jar")); diff --git a/gui/src/main/resources/me/nov/threadtear/add.svg b/gui/src/main/resources/me/nov/threadtear/add.svg index fed1ab5..397e932 100644 --- a/gui/src/main/resources/me/nov/threadtear/add.svg +++ b/gui/src/main/resources/me/nov/threadtear/add.svg @@ -1,12 +1,4 @@ - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/add_disabled.svg b/gui/src/main/resources/me/nov/threadtear/add_disabled.svg index 9ea445c..57eaaf5 100644 --- a/gui/src/main/resources/me/nov/threadtear/add_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/add_disabled.svg @@ -1,12 +1,4 @@ - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/analysis.svg b/gui/src/main/resources/me/nov/threadtear/analysis.svg index c3ee1f8..d5a82d9 100644 --- a/gui/src/main/resources/me/nov/threadtear/analysis.svg +++ b/gui/src/main/resources/me/nov/threadtear/analysis.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg b/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg index 61b0a16..021b0c3 100644 --- a/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/bit_qr.png b/gui/src/main/resources/me/nov/threadtear/bit_qr.png deleted file mode 100644 index edad7ac0f83f912150e4198dcd6cd6accc10fbf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4511 zcmb7IX;_oT76z0>B_g}55v>(vvnsf-qymKkB329_Ad8kFpdecV1QHYw!D6%)Wl?B~ zz%>LCN+>2kLPSN@AQ<98h>8#vNst7>mgIg3E^vKrZ|{%qd1lU>Ip@rK=FIobWF0x| z3|gzZRz^k!Qc<@-liwWil{0aI&#mRBSUJIp7 zjsst$MBC$46T?s0L+Sl5ulP-MU%MR2Aqb;T=D8WN%#h&}mXrUFxj%*}NI( z9CyNd;R2W=jB2vob+YKXD0y2-3Mzh%01lE}=hGy2W8HewrIr@{?_^$@x^M589HN_? zVd^PVbpxz3O&h`c6r5>1_2@QRlE#tjDhSKs+gZ%K73iLiUzLa%FZGckj zddaDq@c!pM1@%dP84k;XmN(VAYwmpPUQ!ex{Cv}>G30ZoU%CL`Tp5bHwNE^fZeY{m zApm2`OjDIigZ3LrP#-iB6~E+oK5*YYfah9bG4+p ze;JG`CYPb_dywq&_JzyZf03dUiYs#zl)ukckcEm@M2{8I2n<|Uao#!r6M5;O z-V`L~^R2AZ%zwsIYeShNSaf^CHf#eq_2L4V+ae`7S-z`pj4OVC`EwvKLP;Dsg#p&e zGu-u!Vv*|zCl54HZ>~IHjW8ilCdu98iM{slaYcLhLI%1%)?qQY5D362i8DNi0b&_H#7}K zaW2ZGr7U&??LN#abQG@E!ry4TuTsQPCkaF$#*xVaIeVSb3NO@*E5sV*cfeUNaGAGX zS{UPW6C_2Cq!M-I@ht=RA&4e53kQnQH^4Xqjy{XKGF_hjhDXsL)kkJ=pGa}SmY9fN zWu%~a0;C$;x%>g6O{ojBB{RG=0-D&h@65Y1;=$Pf59uE0yOxWbe5a1SCtTe3i{Cc^ zsBj1Svz-aHRkMbpjXiKV37UNgJ_ZT9H0p{xPg)+v?RZ@?1473?k7(25tf>N3IE|mJ zwEjk;kk-ltNs%rIo_<5_9*E)Ccs2iiJa)2J^D43w5MYi3t~Nj-w!cFt$U^EY@XLT>OLnT=aTFk~}^Y>W(iEo$m`NY3EcG zx}{$P4ZaauRLwo_Ce8Dep<@R-PwY}B!Ng&Se-MS}c%emlUs;_w|Hc7=d_h0ha*XVcG*f;-%QzTXWg`|d!P)K* zuy5FG!jMaeJN#0JONj2!H-?g;^<3?hM8?HGY%QVHwZ)bn?%4adx9&o7OQ9-$l~|pm zO58ltwadS`+m)-@{#%=ZG}S!cTKmwK;EO~wOWAM=z%|)pM`wEAkG-Onsoj|X7Mht^ z;(Ef~x=?51$i<-;QuEcmy2pFQE`6@ct)Xz5- z*yET^IfT1n^sk!adLCw~kW$^WV_UYCUl$0?dvh=|px@gZa@~;T(%ks$va4}z-Quv3 zfH04aWoO>+FGuOU9$sjz{-vh2s`$Wapj;}Bi3mGR6V;mYD$E?ukOJ#ZS39s+nwjUK zTE!MjR>8v4=wQwi<02wnN-f?1nkoR~oX#^7EH)s0#{?%x6zTra5=~BgVUP_JSmP+}k@BbwUZ>tY1 zC%V_x-8`qUBCe)EFmm>?GhF7n2KdXZ41?lc}! zTF80HjPn)))tKJW_55|OZqKP*HPsBl(BhF~&WS|cCH}WOYW2V-Tsz`i05ti8a5!dk zkpDYBmUY}_&mo#tnHkk-G-N0Y@+Ncx*Ih# zU8w+BjC2Z@{aw4Lt>%RvxOh7}c(l4O{Z;91oqL4*hnw*;HXE|jU(M%kIjrBv%j-jt z(srA@0t$(T8g7`-@HKtAQX^ZdI09-|2H26|N|_;N4u$i>q(JU=t!+LjOq-GGE~4p4bwLs2&^}bBfn=Sc4B8Lxn^iz$o;{6IAE4!Tj?rz z5!XDl&Wy6ZX4p>TbS&lsyB5Uk6ui&xt`|?K;b&QwbIRG4)ji?L7L8LoO7sTyJ^EUM ztCYqTEd8k3%NHiin(O9V^Za9-Mnt{E86x#S&_pFy5E1ov4m7k3wQuE*LYVSk4KuDn$SxxMf~UteEFHy~Nl}|8W#`+F?Jn#^3y> z>B)s4+&!p}NCf^nMiPdCG4|1^Bup@d1I=!x>L?Z~KMO0deVRPtKEvNgt7N|wRK2(= zVVqtW)V9;M3kk-N5X!r-9FmZ6tzl|1<~szATH|qS%J3@*?%8QjMC|Obxg)V;@7k%y znMs-B(9+nhqF>8x_2(Omq$h1oF=89?)J?C`k6D|_qGV8IExA|R+27Z5`V)R0v1>?d zo!Y%r*l~@OY(YAwQTt~7n>)E1d-i4xV&#hwRtD-Qv&baaz!}TiHESg-gT525b--V- zsDmb`txMSn5S-f7Bm2+XqKp;=gf&+1S;NsLRn{ip>sQ=Sa?tHf1-tNHgO1Wo9O-)f zs-=weX~KG*FO+#(Z%P3(Q3-xLj;jNRR|7WMx06tR$_hu!Ma*=w*PUSfv`CHYcfvG< zZ}RcfkkZSTlus9i&!lcS>C?i8gS_b5il7D$FdqJA=h-j$G~9ON&i^@W89yyEz`BU< zyGI>~FHW$YV8cH8wA&dJc4Tac2@}au(Bw_Q8}WOLKhSgkz#v(-G$5QCgR3eQ9?(t+ z7fe`jhi+lV*#NuUE-4k$O_CD-7_r-&)UE7i$vU4lfVDygFVGtg(i^EyQ%d^60%fYV zPpn$5^U1(6TPvEKcF5jsWUG{up}m3e;6R1tgv!&DfNS&j}rhC z_9T1`5!)K|_Bd=q#d;??^|n^`GOd5oRQDTy`D&4R*Xf@qe{u@tsvOI}3c#l7ZvW37 zO{K5Kmo?fS*6qM}=tC(Op<=|?{|VNHr3VM#^#ORJ0A3%K-YEXbgT>u%Mc^$bAKd*4 TCMW - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg b/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg index 78ed8f2..6783e2e 100644 --- a/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg @@ -1,13 +1,19 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/class.svg b/gui/src/main/resources/me/nov/threadtear/class.svg deleted file mode 100644 index 7e0b553..0000000 --- a/gui/src/main/resources/me/nov/threadtear/class.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/gui/src/main/resources/me/nov/threadtear/decompile.svg b/gui/src/main/resources/me/nov/threadtear/decompile.svg index 90273fe..139178b 100644 --- a/gui/src/main/resources/me/nov/threadtear/decompile.svg +++ b/gui/src/main/resources/me/nov/threadtear/decompile.svg @@ -1,13 +1,4 @@ - - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg b/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg index 7da12ea..cf4caeb 100644 --- a/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg @@ -1,13 +1,4 @@ - - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/enum.svg b/gui/src/main/resources/me/nov/threadtear/enum.svg index 2d98928..44d6e5c 100644 --- a/gui/src/main/resources/me/nov/threadtear/enum.svg +++ b/gui/src/main/resources/me/nov/threadtear/enum.svg @@ -1,8 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/failure.svg b/gui/src/main/resources/me/nov/threadtear/failure.svg index b42d812..6162ef0 100644 --- a/gui/src/main/resources/me/nov/threadtear/failure.svg +++ b/gui/src/main/resources/me/nov/threadtear/failure.svg @@ -1,13 +1,13 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/field.svg b/gui/src/main/resources/me/nov/threadtear/field.svg index 4c06576..02a9b20 100644 --- a/gui/src/main/resources/me/nov/threadtear/field.svg +++ b/gui/src/main/resources/me/nov/threadtear/field.svg @@ -1,9 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/file.svg b/gui/src/main/resources/me/nov/threadtear/file.svg index 0133b67..3ebb0ee 100644 --- a/gui/src/main/resources/me/nov/threadtear/file.svg +++ b/gui/src/main/resources/me/nov/threadtear/file.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/file_disabled.svg b/gui/src/main/resources/me/nov/threadtear/file_disabled.svg index 0fe1114..cf07a4c 100644 --- a/gui/src/main/resources/me/nov/threadtear/file_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/file_disabled.svg @@ -1,13 +1,18 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/folder.svg b/gui/src/main/resources/me/nov/threadtear/folder.svg index b55af3d..56a65ef 100644 --- a/gui/src/main/resources/me/nov/threadtear/folder.svg +++ b/gui/src/main/resources/me/nov/threadtear/folder.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/graph.svg b/gui/src/main/resources/me/nov/threadtear/graph.svg index d510edc..c6f16e7 100644 --- a/gui/src/main/resources/me/nov/threadtear/graph.svg +++ b/gui/src/main/resources/me/nov/threadtear/graph.svg @@ -1,15 +1,8 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg b/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg index 3d4f1b0..ade5425 100644 --- a/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg @@ -1,15 +1,8 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/ignore.svg b/gui/src/main/resources/me/nov/threadtear/ignore.svg index 3eaa68e..8a12b67 100644 --- a/gui/src/main/resources/me/nov/threadtear/ignore.svg +++ b/gui/src/main/resources/me/nov/threadtear/ignore.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg b/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg index 02a281a..19a4782 100644 --- a/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg @@ -1,10 +1,16 @@ - - - - - - - - + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/innerClass.svg b/gui/src/main/resources/me/nov/threadtear/innerClass.svg index 583beb2..d4cc1cb 100644 --- a/gui/src/main/resources/me/nov/threadtear/innerClass.svg +++ b/gui/src/main/resources/me/nov/threadtear/innerClass.svg @@ -1,7 +1,4 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/interface.svg b/gui/src/main/resources/me/nov/threadtear/interface.svg index 94e68c3..53905c5 100644 --- a/gui/src/main/resources/me/nov/threadtear/interface.svg +++ b/gui/src/main/resources/me/nov/threadtear/interface.svg @@ -1,8 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/load_config.svg b/gui/src/main/resources/me/nov/threadtear/load_config.svg index c6bce31..2fd83e7 100644 --- a/gui/src/main/resources/me/nov/threadtear/load_config.svg +++ b/gui/src/main/resources/me/nov/threadtear/load_config.svg @@ -1,13 +1,9 @@ - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/mainClass.svg b/gui/src/main/resources/me/nov/threadtear/mainClass.svg index b1aa104..79b8d51 100644 --- a/gui/src/main/resources/me/nov/threadtear/mainClass.svg +++ b/gui/src/main/resources/me/nov/threadtear/mainClass.svg @@ -1,9 +1,11 @@ - - - - - - + + + + + + + + + - \ No newline at end of file + diff --git a/gui/src/main/resources/me/nov/threadtear/method.svg b/gui/src/main/resources/me/nov/threadtear/method.svg index 0723ffc..bca827f 100644 --- a/gui/src/main/resources/me/nov/threadtear/method.svg +++ b/gui/src/main/resources/me/nov/threadtear/method.svg @@ -1,9 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_down.svg b/gui/src/main/resources/me/nov/threadtear/move_down.svg index 46e7af0..70a4356 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_down.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_down.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg b/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg index d344154..f8a8475 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_up.svg b/gui/src/main/resources/me/nov/threadtear/move_up.svg index e790d2a..bb98e4a 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_up.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_up.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg b/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg index df3ab3e..c681a5d 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/package.svg b/gui/src/main/resources/me/nov/threadtear/package.svg index 224f87e..4da77ad 100644 --- a/gui/src/main/resources/me/nov/threadtear/package.svg +++ b/gui/src/main/resources/me/nov/threadtear/package.svg @@ -1,10 +1,5 @@ - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/refresh.svg b/gui/src/main/resources/me/nov/threadtear/refresh.svg index 67b4684..24af79e 100644 --- a/gui/src/main/resources/me/nov/threadtear/refresh.svg +++ b/gui/src/main/resources/me/nov/threadtear/refresh.svg @@ -1,10 +1,7 @@ - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg b/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg index 24258c9..35b9763 100644 --- a/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg @@ -1,10 +1,7 @@ - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/remove.svg b/gui/src/main/resources/me/nov/threadtear/remove.svg index 54c6074..89429c2 100644 --- a/gui/src/main/resources/me/nov/threadtear/remove.svg +++ b/gui/src/main/resources/me/nov/threadtear/remove.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg b/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg index 327ae50..dd8e294 100644 --- a/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg @@ -1,9 +1,16 @@ - - - - - - - - + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/run.svg b/gui/src/main/resources/me/nov/threadtear/run.svg index a1aba0e..bfaf56a 100644 --- a/gui/src/main/resources/me/nov/threadtear/run.svg +++ b/gui/src/main/resources/me/nov/threadtear/run.svg @@ -1,4 +1,4 @@ - - - - \ No newline at end of file + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save.svg b/gui/src/main/resources/me/nov/threadtear/save.svg index fafa508..06841c5 100644 --- a/gui/src/main/resources/me/nov/threadtear/save.svg +++ b/gui/src/main/resources/me/nov/threadtear/save.svg @@ -1,12 +1,7 @@ - - - - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save_config.svg b/gui/src/main/resources/me/nov/threadtear/save_config.svg index 9aa8965..a5c20a8 100644 --- a/gui/src/main/resources/me/nov/threadtear/save_config.svg +++ b/gui/src/main/resources/me/nov/threadtear/save_config.svg @@ -1,9 +1,8 @@ - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg b/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg index a54150e..0f03177 100644 --- a/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg @@ -1,9 +1,17 @@ - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/skidsuite.svg b/gui/src/main/resources/me/nov/threadtear/skidsuite.svg new file mode 100644 index 0000000..155295f --- /dev/null +++ b/gui/src/main/resources/me/nov/threadtear/skidsuite.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/threadtear.svg b/gui/src/main/resources/me/nov/threadtear/threadtear.svg deleted file mode 100644 index 92c34fd..0000000 --- a/gui/src/main/resources/me/nov/threadtear/threadtear.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_in.svg b/gui/src/main/resources/me/nov/threadtear/zoom_in.svg index 68f9cef..39cb0f6 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_in.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_in.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_out.svg b/gui/src/main/resources/me/nov/threadtear/zoom_out.svg index 9afaff0..41c92b4 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_out.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_out.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg b/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg index f735a22..e497a8f 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg @@ -1,14 +1,4 @@ - - - - - - - - - - - - - + + + From 5bbbf63257a2aea18823530d7aa27760f6d27d5d Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Thu, 19 Sep 2024 20:33:44 +0800 Subject: [PATCH 02/13] add class icon --- gui/src/main/resources/me/nov/threadtear/class.svg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 gui/src/main/resources/me/nov/threadtear/class.svg diff --git a/gui/src/main/resources/me/nov/threadtear/class.svg b/gui/src/main/resources/me/nov/threadtear/class.svg new file mode 100644 index 0000000..a802794 --- /dev/null +++ b/gui/src/main/resources/me/nov/threadtear/class.svg @@ -0,0 +1,5 @@ + + + + + From fbff07d2f00d98873a4473240bfa28b01b4cbf3b Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Fri, 20 Sep 2024 18:50:23 +0800 Subject: [PATCH 03/13] Upgraded to java 22 and implemented vineflower decompiler --- .gitignore | 1 + build.gradle.kts | 10 +++++----- core/build.gradle.kts | 5 +++-- .../main/java/me/nov/threadtear/ThreadtearCore.java | 5 ++++- gradle.properties | 6 +++--- gui/src/main/java/me/nov/threadtear/Threadtear.java | 2 +- .../main/java/me/nov/threadtear/swing/SwingUtils.java | 3 ++- .../java/me/nov/threadtear/swing/laf/LookAndFeel.java | 7 ++++--- 8 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index dacd169..325c59a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Ignore Gradle build output directory build +.gui/gradle/wrapper/ # Ignore logs *.log diff --git a/build.gradle.kts b/build.gradle.kts index d5cc2df..031cbb3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,8 +62,8 @@ allprojects { plugins.withType { configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.toVersion(22) + targetCompatibility = JavaVersion.toVersion(22) } if (!skipAutostyle) { @@ -103,7 +103,7 @@ allprojects { windowTitle = "Threadtear ${project.name} API" header = "Threadtear" addBooleanOption("Xdoclint:none", true) - addStringOption("source", "8") + addStringOption("source", "22") if (JavaVersion.current().isJava9Compatible) { addBooleanOption("html5", true) links("https://docs.oracle.com/javase/9/docs/api/") @@ -132,10 +132,10 @@ allprojects { // This includes either project-specific license or a default one if (file("$projectDir/LICENSE").exists()) { textFrom("$projectDir/LICENSE") - rename { s -> "${project.name.toUpperCase()}_LICENSE" } + rename { "${project.name.toUpperCase()}_LICENSE" } } else { textFrom("$rootDir/LICENSE") - rename { s -> "${rootProject.name.toUpperCase()}_LICENSE" } + rename { "${rootProject.name.toUpperCase()}_LICENSE" } } } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 3e164bb..3ffd466 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,8 +18,9 @@ dependencies { implementation("org.ow2.asm:asm-util") implementation("org.ow2.asm:asm-commons") - implementation("com.github.leibnitz27:cfr:0.151") + implementation("com.github.leibnitz27:cfr") { isChanging = true } + implementation("org.vineflower:vineflower:1.10.1") implementation("ch.qos.logback:logback-classic") - externalLib("fernflower-15-05-20") +// externalLib("fernflower-15-05-20") } diff --git a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java index 6f81675..21a1ccc 100644 --- a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java +++ b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java @@ -15,13 +15,16 @@ import java.util.stream.Collectors; public class ThreadtearCore { + + // removed due to being unused + /* public static void configureEnvironment() throws Exception { System.setProperty("file.encoding", "UTF-8"); Field charset = Charset.class.getDeclaredField("defaultCharset"); charset.setAccessible(true); charset.set(null, null); } - +*/ public static void configureLoggers() { LogWrapper.logger.addLogger(LoggerFactory.getLogger("logfile")); LogWrapper.logger.addLogger(LoggerFactory.getLogger("form")); diff --git a/gradle.properties b/gradle.properties index 04c0ca1..2745889 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,10 +16,10 @@ com.github.autostyle.version = 3.1 commons-io.version = 2.6 commons-configuration2.version = 2.7 commons-beanutils.version = 1.9.4 -darklaf.version = 2.6.1 -darklaf.extensions.version = 0.3.4 +darklaf.version = 3.0.2 +darklaf.extensions.version = 0.4.1 asm.version = 9.1 -cfr.version = -SNAPSHOT +cfr.version = 0.151 rsyntaxtextarea.version = 3.1.1 jgraphx.version = v4.0.0 logback-classic.version = 1.2.3 diff --git a/gui/src/main/java/me/nov/threadtear/Threadtear.java b/gui/src/main/java/me/nov/threadtear/Threadtear.java index ea7f192..c8740e3 100644 --- a/gui/src/main/java/me/nov/threadtear/Threadtear.java +++ b/gui/src/main/java/me/nov/threadtear/Threadtear.java @@ -48,7 +48,7 @@ public static Threadtear getInstance() { public static void main(String[] args) throws Exception { LookAndFeel.init(); LookAndFeel.setLookAndFeel(); - ThreadtearCore.configureEnvironment(); +// ThreadtearCore.configureEnvironment(); ThreadtearCore.configureLoggers(); configureGUILoggers(); getInstance().setVisible(true); diff --git a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java index 8611666..39325d3 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java +++ b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java @@ -2,7 +2,8 @@ import com.github.weisj.darklaf.components.OverlayScrollPane; import com.github.weisj.darklaf.components.border.DarkBorders; -import com.github.weisj.darklaf.icons.IconLoader; + +import com.github.weisj.darklaf.properties.icons.IconLoader; import com.github.weisj.darklaf.ui.button.DarkButtonUI; import me.nov.threadtear.Threadtear; import me.nov.threadtear.swing.textarea.DecompilerTextArea; diff --git a/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java b/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java index d81697b..52d8116 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java @@ -6,10 +6,11 @@ import com.github.weisj.darklaf.LafManager; import com.github.weisj.darklaf.theme.*; -import com.github.weisj.darklaf.theme.info.ColorToneRule; -import com.github.weisj.darklaf.theme.info.ContrastRule; import com.github.weisj.darklaf.theme.info.DefaultThemeProvider; -import com.github.weisj.darklaf.theme.info.PreferredThemeStyle; +import com.github.weisj.darklaf.theme.spec.ColorToneRule; +import com.github.weisj.darklaf.theme.spec.ContrastRule; +import com.github.weisj.darklaf.theme.spec.PreferredThemeStyle; + public class LookAndFeel { From 676cca13f459dacf9fe01a5a2d444c894810968e Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Sun, 22 Sep 2024 20:53:27 +0800 Subject: [PATCH 04/13] fixed gradle issues and used a newer rendreing framework for svg icons, full revamping of theme system in place --- build.gradle.kts | 14 +++++----- .../threadtear/decompiler/DecompilerInfo.java | 2 +- ...lowerBridge.java => VineFlowerBridge.java} | 14 +++++----- gui/build.gradle.kts | 1 + gui/gradle/wrapper/gradle-wrapper.properties | 6 ++++ .../me/nov/threadtear/swing/SwingUtils.java | 28 +++++++++++++++---- 6 files changed, 45 insertions(+), 20 deletions(-) rename core/src/main/java/me/nov/threadtear/decompiler/{FernflowerBridge.java => VineFlowerBridge.java} (91%) create mode 100644 gui/gradle/wrapper/gradle-wrapper.properties diff --git a/build.gradle.kts b/build.gradle.kts index 031cbb3..4fcf9ec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -88,7 +88,7 @@ allprojects { include("**/*.properties") filteringCharset = "UTF-8" // apply native2ascii conversion since Java 8 expects properties to have ascii symbols only - filter(org.apache.tools.ant.filters.EscapeUnicode::class) +// filter(org.apache.tools.ant.filters.EscapeUnicode::class) } } @@ -104,12 +104,12 @@ allprojects { header = "Threadtear" addBooleanOption("Xdoclint:none", true) addStringOption("source", "22") - if (JavaVersion.current().isJava9Compatible) { - addBooleanOption("html5", true) - links("https://docs.oracle.com/javase/9/docs/api/") - } else { - links("https://docs.oracle.com/javase/8/docs/api/") - } +// if (JavaVersion.current().isJava9Compatible) { +// addBooleanOption("html5", true) +// links("https://docs.oracle.com/javase/9/docs/api/") +// } else { +// links("https://docs.oracle.com/javase/8/docs/api/") +// } } } diff --git a/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java b/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java index 8a7bd03..591c486 100644 --- a/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java +++ b/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java @@ -19,7 +19,7 @@ public String toString() { public static List> getDecompilerInfos() { List> list = new ArrayList<>(3); list.add(new CFRBridge.CFRDecompilerInfo()); - list.add(new FernflowerBridge.FernflowerDecompilerInfo()); + list.add(new VineFlowerBridge.FernflowerDecompilerInfo()); list.add(new KrakatauBridge.KrakatauDecompilerInfo()); return list; } diff --git a/core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java b/core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java similarity index 91% rename from core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java rename to core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java index 1b82dac..990030e 100644 --- a/core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java +++ b/core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java @@ -10,7 +10,7 @@ import me.nov.threadtear.io.JarIO; -public class FernflowerBridge implements IDecompilerBridge, IBytecodeProvider, IResultSaver { +public class VineFlowerBridge implements IDecompilerBridge, IBytecodeProvider, IResultSaver { protected static final Map options = new HashMap<>(); @@ -68,7 +68,7 @@ public String decompile(File archive, String name, byte[] bytez) { return sw.toString(); } if (result == null || result.trim().isEmpty()) { - result = "No Fernflower output received\n\nOutput log:\n" + new String(log.toByteArray()); + result = "No VineFlower output received\n\nOutput log:\n" + new String(log.toByteArray()); } return result; } @@ -112,21 +112,21 @@ public void saveClassEntry(String path, String archiveName, String qualifiedName public void closeArchive(String path, String archiveName) { } - public static class FernflowerDecompilerInfo extends DecompilerInfo { + public static class FernflowerDecompilerInfo extends DecompilerInfo { @Override public String getName() { - return "Fernflower"; + return "VineFlower"; } @Override public String getVersionInfo() { - return "15-08-20"; + return "1.10.1"; } @Override - public FernflowerBridge createDecompilerBridge() { - return new FernflowerBridge(); + public VineFlowerBridge createDecompilerBridge() { + return new VineFlowerBridge(); } } } diff --git a/gui/build.gradle.kts b/gui/build.gradle.kts index b213a94..0cc4b29 100644 --- a/gui/build.gradle.kts +++ b/gui/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation("com.github.weisj:darklaf-theme") { isChanging = true } implementation("com.github.weisj:darklaf-property-loader") { isChanging = true } implementation("com.github.weisj:darklaf-extensions-rsyntaxarea") + implementation("com.github.weisj:jsvg:1.6.0") implementation("com.fifesoft:rsyntaxtextarea") implementation("com.github.jgraph:jgraphx") diff --git a/gui/gradle/wrapper/gradle-wrapper.properties b/gui/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..110609d --- /dev/null +++ b/gui/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Sep 20 18:35:57 CST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java index 39325d3..541773b 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java +++ b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java @@ -5,6 +5,9 @@ import com.github.weisj.darklaf.properties.icons.IconLoader; import com.github.weisj.darklaf.ui.button.DarkButtonUI; +import com.github.weisj.jsvg.SVGDocument; +import com.github.weisj.jsvg.geometry.size.FloatSize; +import com.github.weisj.jsvg.parser.SVGLoader; import me.nov.threadtear.Threadtear; import me.nov.threadtear.swing.textarea.DecompilerTextArea; import org.fife.ui.rtextarea.RTextScrollPane; @@ -17,10 +20,13 @@ import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.function.Consumer; public class SwingUtils { - private static final IconLoader ICON_LOADER = IconLoader.get(Threadtear.class); + public static SVGLoader loader = new SVGLoader(); public static TitledPanel withTitleAndBorder(String title, JComponent c) { Border border = DarkBorders.createLineBorder(1, 1, 1, 1); @@ -173,21 +179,33 @@ public static Image iconToFrameImage(Icon icon, Window window) { } public static Icon getIcon(String path) { - return getIcon(path, false); + return getIcon(path, 16, 16, false); // Default size and themed flag } public static Icon getIcon(String path, boolean themed) { - return ICON_LOADER.getIcon(path, themed); + return getIcon(path, 16, 16, themed); // Default size with themed flag } public static Icon getIcon(String path, int width, int height) { - return ICON_LOADER.getIcon(path, width, height, false); + return getIcon(path, width, height, false); // Default themed flag } public static Icon getIcon(String path, int width, int height, boolean themed) { - return ICON_LOADER.getIcon(path, width, height, themed); + URL svgUrl = Threadtear.class.getResource(path); + assert svgUrl != null; + SVGDocument svgDocument = loader.load(svgUrl); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + assert svgDocument != null; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + svgDocument.render(null, g); + g.dispose(); + + return new ImageIcon(image); } + public static JButton createSlimButton(Icon icon, ActionListener l) { JButton jButton = new JButton(icon); jButton.putClientProperty(DarkButtonUI.KEY_NO_BORDERLESS_OVERWRITE, true); From c508e70e0491a43a2ee0e55e656bf923a895ae41 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Mon, 23 Sep 2024 17:56:28 +0800 Subject: [PATCH 05/13] Uses ReflectionUtil to automate the loading of executions --- .../threadtear/execution/ExecutionLink.java | 71 +++++++------------ .../util/reflection/ReflectionUtil.java | 66 +++++++++++++++++ 2 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java diff --git a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java index fc823a5..2e15205 100644 --- a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java +++ b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java @@ -23,55 +23,34 @@ import me.nov.threadtear.execution.stringer.StringObfuscationStringer; import me.nov.threadtear.execution.tools.*; import me.nov.threadtear.execution.zkm.*; +import me.nov.threadtear.logging.LogWrapper; +import me.nov.threadtear.util.reflection.ReflectionUtil; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; public class ExecutionLink { - public static final List> executions = new ArrayList>() {{ - add(InlineMethods.class); - add(InlineUnchangedFields.class); - add(RemoveUnnecessary.class); - add(RemoveUnusedVariables.class); - add(RemoveAttributes.class); - - add(ArgumentInliner.class); - add(JSRInliner.class); - add(ObfuscatedAccess.class); - add(KnownConditionalJumps.class); - add(ConvertCompareInstructions.class); - - add(RestoreSourceFiles.class); - add(ReobfuscateClassNames.class); - add(ReobfuscateMembers.class); - add(ReobfuscateVariableNames.class); - add(RemoveMonitors.class); - add(RemoveTCBs.class); - - add(StringObfuscationStringer.class); - add(AccessObfuscationStringer.class); - - add(TryCatchObfuscationRemover.class); - add(StringObfuscationZKM.class); - add(AccessObfuscationZKM.class); - add(FlowObfuscationZKM.class); - add(DESObfuscationZKM.class); - - add(StringObfuscationAllatori.class); - add(ExpirationDateRemoverAllatori.class); - add(JunkRemoverAllatori.class); - - add(StringObfuscationDashO.class); - - add(BadAttributeRemover.class); - add(StringObfuscationParamorphism.class); - add(AccessObfuscationParamorphism.class); - - add(Java7Compatibility.class); - add(Java8Compatibility.class); - add(IsolatePossiblyMalicious.class); - add(AddLineNumbers.class); - add(LogAllExceptions.class); - add(RemoveMaxs.class); - }}; + public static final List> executions = new ArrayList>() {}; + static { + // Use reflection to get every class in the execution package and add it if it extends Execution + Class[] classes = ReflectionUtil.getClassInPackage("me.nov.threadtear.execution"); + LogWrapper.logger.info("Found " + classes.length + " classes in execution package"); + for(Class clazz : classes){ + // Skip the Execution class itself, abstract classes, and interfaces + if(Execution.class.isAssignableFrom(clazz) && clazz != Execution.class && !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isInterface()){ + try { + // Check if the class has a public no-argument constructor + clazz.getConstructor(); + // Add the class directly to the list without instantiating + executions.add((Class) clazz); + } catch (NoSuchMethodException e) { + // The class doesn't have a public no-argument constructor; skip it + continue; + } + } + } + + } } diff --git a/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java b/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java new file mode 100644 index 0000000..01daba6 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java @@ -0,0 +1,66 @@ +package me.nov.threadtear.util.reflection; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class ReflectionUtil { + public static Class[] getClassInPackage(String packageName) { + List> classes = new ArrayList<>(); + String path = packageName.replace('.', '/'); + try { + Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(path); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if (resource.getProtocol().equals("file")) { + classes.addAll(findClasses(new File(resource.getFile()), packageName)); + } else if (resource.getProtocol().equals("jar")) { + String jarPath = resource.getPath().substring(5, resource.getPath().indexOf("!")); + classes.addAll(findClassesInJar(jarPath, path)); + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + return classes.toArray(new Class[0]); + } + + private static List> findClasses(File directory, String packageName) throws ClassNotFoundException { + List> classes = new ArrayList<>(); + if (!directory.exists()) { + return classes; + } + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + classes.addAll(findClasses(file, packageName + "." + file.getName())); + } else if (file.getName().endsWith(".class")) { + classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6))); + } + } + } + return classes; + } + + private static List> findClassesInJar(String jarPath, String packagePath) throws IOException, ClassNotFoundException { + List> classes = new ArrayList<>(); + JarFile jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith(packagePath) && entryName.endsWith(".class") && !entry.isDirectory()) { + String className = entryName.replace('/', '.').substring(0, entryName.length() - 6); + classes.add(Class.forName(className)); + } + } + jarFile.close(); + return classes; + } +} From d0b26ca4cf40a604afabd07579b7620addfb1c57 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Tue, 24 Sep 2024 20:48:03 +0800 Subject: [PATCH 06/13] Added Cafedood for stripping classes and removing illigal immigrants --- core/build.gradle.kts | 1 + .../me/nov/threadtear/ThreadtearCore.java | 20 +++++------ gui/build.gradle.kts | 1 + .../threadtear/swing/tree/ClassTreePanel.java | 34 +++++++++++++++++++ 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 3ffd466..999b808 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation("com.github.leibnitz27:cfr") { isChanging = true } implementation("org.vineflower:vineflower:1.10.1") implementation("ch.qos.logback:logback-classic") + implementation("software.coley:cafedude-core:2.1.1") // externalLib("fernflower-15-05-20") } diff --git a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java index 21a1ccc..e7a3310 100644 --- a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java +++ b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java @@ -4,8 +4,15 @@ import me.nov.threadtear.execution.Execution; import me.nov.threadtear.logging.LogWrapper; import me.nov.threadtear.security.VMSecurityManager; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; import org.slf4j.LoggerFactory; +import software.coley.cafedude.InvalidClassException; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; +import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.lang.reflect.Field; @@ -16,15 +23,6 @@ public class ThreadtearCore { - // removed due to being unused - /* - public static void configureEnvironment() throws Exception { - System.setProperty("file.encoding", "UTF-8"); - Field charset = Charset.class.getDeclaredField("defaultCharset"); - charset.setAccessible(true); - charset.set(null, null); - } -*/ public static void configureLoggers() { LogWrapper.logger.addLogger(LoggerFactory.getLogger("logfile")); LogWrapper.logger.addLogger(LoggerFactory.getLogger("form")); @@ -35,13 +33,14 @@ public static void run(List classes, List executions, boolean LogWrapper.logger.info("Executing {} tasks on {} classes!", executions.size(), classes.size()); if (!disableSecurity) { LogWrapper.logger.info("Initializing security manager if something goes horribly wrong"); - System.setSecurityManager(new VMSecurityManager()); } else { LogWrapper.logger.warning("Starting without security manager!"); } List ignoredClasses = classes.stream().filter(c -> !c.transform).collect(Collectors.toList()); LogWrapper.logger.warning("{} classes will be ignored", ignoredClasses.size()); classes.removeIf(c -> !c.transform); + + Map map = classes.stream().collect(Collectors.toMap(c -> c.node.name, c -> c, (c1, c2) -> { LogWrapper.logger.warning("Warning: Duplicate class definition of {}, one class may not get decrypted", c1.node.name); return c1; @@ -73,7 +72,6 @@ public static void run(List classes, List executions, boolean } catch (InterruptedException e1) { } LogWrapper.logger.info("Successful completion!"); - System.setSecurityManager(null); } // TODO: make a CLI diff --git a/gui/build.gradle.kts b/gui/build.gradle.kts index 0cc4b29..e31c16b 100644 --- a/gui/build.gradle.kts +++ b/gui/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation("commons-io:commons-io") implementation("org.apache.commons:commons-configuration2") + implementation("software.coley:cafedude-core:2.1.1") } tasks.shadowJar { diff --git a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java index 9050b1c..cb1625a 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java @@ -17,7 +17,11 @@ import me.nov.threadtear.swing.tree.renderer.ClassTreeCellRenderer; import me.nov.threadtear.util.format.Strings; import org.apache.commons.io.FilenameUtils; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; import javax.swing.*; import javax.swing.tree.*; @@ -26,6 +30,7 @@ import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; +import java.io.InvalidClassException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; @@ -211,6 +216,15 @@ private void loadFile(String type) { switch (type) { case "jar": this.classes = JarIO.loadClasses(inputFile); + // Transform each class before execution + for (int i = 0; i < classes.size(); i++) { + try { + LogWrapper.logger.debug("CAFEDOOD stripping class: {}", classes.get(i).node.name); + classes.set(i, transformClazz(classes.get(i))); + } catch (IOException | software.coley.cafedude.InvalidClassException e) { + LogWrapper.logger.error("Failed to transform class: {}", classes.get(i).node.name, e); + } + } if (classes.stream().anyMatch(c -> c.oldEntry.getCertificates() != null)) { JOptionPane.showMessageDialog(this, "Warning: File is signed and may not load correctly if already " + @@ -292,4 +306,24 @@ public void addToTree(ClassTreeNode current, Clazz c, String[] packages, int pck current.add(newChild); addToTree(newChild, c, packages, ++pckg); } + + public static Clazz transformClazz(Clazz originalClazz) throws IOException, InvalidClassException, software.coley.cafedude.InvalidClassException { + // Read the original bytecode + byte[] originalBytecode = originalClazz.streamOriginal().readAllBytes(); + + // Use Cafedude to strip attributes + ClassFileReader reader = new ClassFileReader(); + ClassFile classFile = reader.read(originalBytecode); + byte[] strippedBytecode = new ClassFileWriter().write(classFile); + + // Create a new ClassNode from the stripped bytecode + ClassReader classReader = new ClassReader(strippedBytecode); + ClassNode newNode = new ClassNode(); + classReader.accept(newNode, 0); + + // Return a new Clazz object + return new Clazz(newNode, originalClazz.oldEntry, originalClazz.inputFile); + } + + } From 6913c385b4bff5bcd0679939507f6c9eccae8452 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Tue, 24 Sep 2024 22:26:05 +0800 Subject: [PATCH 07/13] Refined Reobfuscate Variable Names --- .../execution/analysis/ReobfuscateVariableNames.java | 5 +++-- .../java/me/nov/threadtear/swing/tree/ClassTreePanel.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java b/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java index 49ba89c..b735004 100644 --- a/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java +++ b/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java @@ -18,13 +18,14 @@ public ReobfuscateVariableNames() { super(ExecutionCategory.ANALYSIS, "Reobfuscate variable names", "Reobfuscate method local variable names for easier analysis." + "
" + - "Gets rid of default names like a, a2, ... and obfuscated names like 恼人的名字.", + "Gets rid of default names like a, a2, ... and obfuscated names like 恼人的名字."+ + "
" + "refactored by neilhuang007", ExecutionTag.BETTER_DECOMPILE, ExecutionTag.POSSIBLE_DAMAGE); } @Override public String getAuthor() { - return "ViRb3"; + return "neilhuang007"; } private int getVariableCount(MethodNode method) { diff --git a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java index cb1625a..5fb6e9b 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java @@ -219,7 +219,7 @@ private void loadFile(String type) { // Transform each class before execution for (int i = 0; i < classes.size(); i++) { try { - LogWrapper.logger.debug("CAFEDOOD stripping class: {}", classes.get(i).node.name); + LogWrapper.logger.info("CAFEDOOD stripping class: {}", classes.get(i).node.name); classes.set(i, transformClazz(classes.get(i))); } catch (IOException | software.coley.cafedude.InvalidClassException e) { LogWrapper.logger.error("Failed to transform class: {}", classes.get(i).node.name, e); From 0d9bc15529a407151b182611767c2decb81be0d5 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Wed, 23 Oct 2024 16:17:33 +0800 Subject: [PATCH 08/13] updated executions --- .../threadtear/execution/ExecutionLink.java | 24 +- .../ExpirationDateRemoverAllatori.java | 55 ---- .../allatori/JunkRemoverAllatori.java | 63 ---- .../allatori/StringObfuscationAllatori.java | 186 ------------ .../execution/cleanup/InlineMethods.java | 285 +++++++++++++----- .../cleanup/InlineUnchangedFields.java | 1 + .../execution/zkm/FlowObfuscationZKM.java | 67 ++-- .../me/nov/threadtear/util/ByteCodeUtil.java | 105 +++++++ 8 files changed, 358 insertions(+), 428 deletions(-) delete mode 100644 core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java delete mode 100644 core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java delete mode 100644 core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java create mode 100644 core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java diff --git a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java index 2e15205..46355a9 100644 --- a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java +++ b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java @@ -1,28 +1,6 @@ package me.nov.threadtear.execution; -import me.nov.threadtear.execution.allatori.ExpirationDateRemoverAllatori; -import me.nov.threadtear.execution.allatori.JunkRemoverAllatori; -import me.nov.threadtear.execution.allatori.StringObfuscationAllatori; -import me.nov.threadtear.execution.analysis.*; -import me.nov.threadtear.execution.cleanup.InlineMethods; -import me.nov.threadtear.execution.cleanup.InlineUnchangedFields; -import me.nov.threadtear.execution.cleanup.remove.RemoveAttributes; -import me.nov.threadtear.execution.cleanup.remove.RemoveUnnecessary; -import me.nov.threadtear.execution.cleanup.remove.RemoveUnusedVariables; -import me.nov.threadtear.execution.dasho.StringObfuscationDashO; -import me.nov.threadtear.execution.generic.ConvertCompareInstructions; -import me.nov.threadtear.execution.generic.KnownConditionalJumps; -import me.nov.threadtear.execution.generic.ObfuscatedAccess; -import me.nov.threadtear.execution.generic.TryCatchObfuscationRemover; -import me.nov.threadtear.execution.generic.inliner.ArgumentInliner; -import me.nov.threadtear.execution.generic.inliner.JSRInliner; -import me.nov.threadtear.execution.paramorphism.AccessObfuscationParamorphism; -import me.nov.threadtear.execution.paramorphism.BadAttributeRemover; -import me.nov.threadtear.execution.paramorphism.StringObfuscationParamorphism; -import me.nov.threadtear.execution.stringer.AccessObfuscationStringer; -import me.nov.threadtear.execution.stringer.StringObfuscationStringer; -import me.nov.threadtear.execution.tools.*; -import me.nov.threadtear.execution.zkm.*; + import me.nov.threadtear.logging.LogWrapper; import me.nov.threadtear.util.reflection.ReflectionUtil; diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java deleted file mode 100644 index cd4c38d..0000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java +++ /dev/null @@ -1,55 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.*; - -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.analysis.BasicValue; - -import me.nov.threadtear.analysis.stack.*; -import me.nov.threadtear.execution.*; - -public class ExpirationDateRemoverAllatori extends Execution implements IConstantReferenceHandler { - - public ExpirationDateRemoverAllatori() { - super(ExecutionCategory.ALLATORI, "Remove expiry date", - "Allatori adds expiration dates to the code
that stop the obfuscated jar " + - "file from running after being passed.
They can be removed easily.", - ExecutionTag.POSSIBLE_DAMAGE); - } - - @Override - public boolean execute(Map classes, boolean verbose) { - try { - logger.info("Finding most common long ldc cst"); - long mostCommon = classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .map(m -> m.instructions.spliterator()).flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == LDC && ((LdcInsnNode) ain).cst instanceof Long) - .map(ain -> (LdcInsnNode) ain) - .filter(ldc -> Math.abs((long) ldc.cst - System.currentTimeMillis()) < 157784760000L) - .collect(Collectors.groupingBy(ldc -> (long) ldc.cst, Collectors.counting())).entrySet().stream() - .max(Entry.comparingByValue()).map(Entry::getKey).orElseThrow(RuntimeException::new); - logger.info("Expiration date is " + new Date(mostCommon).toString() + ", replacing"); - classes.values().stream().map(c -> c.node.methods).flatMap(List::stream).map(m -> m.instructions.spliterator()) - .flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == LDC && ((LdcInsnNode) ain).cst.equals(mostCommon)) - .map(ain -> (LdcInsnNode) ain).forEach(ldc -> ldc.cst = 1337133713371337L); - return true; - } catch (Exception e) { - logger.error("Failure", e); - return false; - } - } - - @Override - public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { - return null; - } - - @Override - public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, - List values) { - return null; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java deleted file mode 100644 index 4261608..0000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java +++ /dev/null @@ -1,63 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import me.nov.threadtear.execution.Clazz; -import me.nov.threadtear.execution.Execution; -import me.nov.threadtear.execution.ExecutionCategory; -import me.nov.threadtear.execution.ExecutionTag; -import me.nov.threadtear.util.asm.InstructionModifier; -import org.objectweb.asm.tree.*; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class JunkRemoverAllatori extends Execution { - public JunkRemoverAllatori() { - super(ExecutionCategory.ALLATORI, "Junk instruction remover", - "Removes junk instructions that create a lot of boolean variables when " + - "decompiled with Fernflower.", ExecutionTag.BETTER_DECOMPILE); - } - - @Override - public boolean execute(Map map, boolean verbose) { - int methodTotal = 0; - int methodModified = 0; - int removedTotal = 0; - final List classNodes = map.values().stream().map(c -> c.node).collect(Collectors.toList()); - for (ClassNode clazz : classNodes) { - for (MethodNode method : clazz.methods) { - methodTotal++; - int removed = processMethod(method); - removedTotal += removed; - if (removed > 0) { - methodModified++; - } - } - } - logger.info("Removed {} junk instructions from {}/{} methods.", removedTotal, methodModified, methodTotal); - return true; - } - - private int processMethod(MethodNode method) { - AtomicInteger removed = new AtomicInteger(); - InstructionModifier modifier = new InstructionModifier(); - StreamSupport.stream(method.instructions.spliterator(), false) - .filter(i -> i.getOpcode() == ICONST_1 && i.getNext() != null && i.getNext().getOpcode() == DUP && - i.getNext().getNext() != null && i.getNext().getNext().getOpcode() == POP2).map(i -> (InsnNode) i) - .forEach(i -> { - removed.getAndIncrement(); - modifier.remove(i); - modifier.remove(i.getNext()); - modifier.remove(i.getNext().getNext()); - }); - modifier.apply(method); - return removed.get(); - } - - @Override - public String getAuthor() { - return "ViRb3"; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java deleted file mode 100644 index afb78de..0000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java +++ /dev/null @@ -1,186 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import java.lang.reflect.Method; -import java.util.*; - -import org.objectweb.asm.tree.*; -import org.objectweb.asm.tree.analysis.*; - -import me.nov.threadtear.analysis.stack.*; -import me.nov.threadtear.execution.*; -import me.nov.threadtear.util.asm.Instructions; -import me.nov.threadtear.util.format.Strings; -import me.nov.threadtear.vm.*; - -public class StringObfuscationAllatori extends Execution implements IVMReferenceHandler, IConstantReferenceHandler { - - private static final String ALLATORI_DECRPYTION_METHOD_DESC = "(Ljava/lang/String;)Ljava/lang/String;"; - private Map classes; - private int encrypted; - private int decrypted; - private boolean verbose; - - public StringObfuscationAllatori() { - super(ExecutionCategory.ALLATORI, "String obfuscation removal", - "Tested on version 7.3, should work for older versions too.", ExecutionTag.RUNNABLE, - ExecutionTag.POSSIBLY_MALICIOUS); - } - - @Override - public boolean execute(Map classes, boolean verbose) { - this.verbose = verbose; - this.classes = classes; - this.encrypted = 0; - this.decrypted = 0; - - classes.values().forEach(this::decrypt); - if (encrypted == 0) { - logger.error("No strings matching Allatori 7.3 string obfuscation have been found!"); - return false; - } - float decryptionRatio = Math.round((decrypted / (float) encrypted) * 100); - logger.info("Of a total " + encrypted + " encrypted strings, " + (decryptionRatio) + "% were " + - "successfully decrypted"); - return decryptionRatio > 0.25; - } - - private void decrypt(Clazz c) { - ClassNode cn = c.node; - logger.collectErrors(c); - cn.methods.forEach(m -> { - InsnList rewrittenCode = new InsnList(); - Map labels = Instructions.cloneLabels(m.instructions); - - // as we can't add instructions because frame index - // and instruction index - // wouldn't fit together anymore we have to do it - // this way - loopConstantFrames(cn, m, this, (ain, frame) -> { - for (AbstractInsnNode newInstr : tryReplaceMethods(cn, m, ain, frame)) { - rewrittenCode.add(newInstr.clone(labels)); - } - }); - if (rewrittenCode.size() > 0) { - Instructions.updateInstructions(m, labels, rewrittenCode); - } - }); - } - - private AbstractInsnNode[] tryReplaceMethods(ClassNode cn, MethodNode m, AbstractInsnNode ain, - Frame frame) { - if (ain.getOpcode() == INVOKESTATIC) { - MethodInsnNode min = (MethodInsnNode) ain; - if (min.desc.equals(ALLATORI_DECRPYTION_METHOD_DESC)) { - try { - encrypted++; - ConstantValue top = frame.getStack(frame.getStackSize() - 1); - if (top.isKnown() && top.isString()) { - String encryptedString = (String) top.getValue(); - // strings are not high utf and no high sdev, - // don't check - String realString = invokeProxy(cn, m, min, encryptedString); - if (realString != null) { - if (Strings.isHighUTF(realString)) { - logger.warning("String may have not decrypted correctly in " + cn.name + "." + m.name + m.desc); - } - this.decrypted++; - return new AbstractInsnNode[]{new InsnNode(POP), new LdcInsnNode(realString)}; - } else { - logger.error("Failed to decrypt string in " + cn.name + "." + m.name + m.desc); - } - } else if (verbose) { - logger.warning("Unknown top stack value in " + cn.name + "." + m.name + m.desc + ", skipping"); - } - } catch (Throwable e) { - if (verbose) { - logger.error("Throwable", e); - } - logger.error( - "Failed to decrypt string in " + cn.name + "." + m.name + m.desc + ": " + e.getClass().getName() + - ", " + e.getMessage()); - } - } - } - return new AbstractInsnNode[]{ain}; - } - - private String invokeProxy(ClassNode cn, MethodNode m, MethodInsnNode min, String encrypted) throws Exception { - VM vm = VM.constructNonInitializingVM(this); - createFakeClone(cn, m, min, encrypted); // create a - // duplicate of the current class, - // we need this because stringer checks for - // stacktrace method name and class - - final Clazz owner = classes.get(min.owner); - if (owner == null) { - logger.error("Could not find owner class in class list"); - return null; - } - ClassNode decryptionMethodOwner = owner.node; - if (decryptionMethodOwner == null) - return null; - vm.explicitlyPreload(fakeInvocationClone); // proxy - // class can't contain code in clinit other than the - // one we want to run - if (!vm.isLoaded(decryptionMethodOwner.name.replace('/', '.'))) // decryption class - // could be the same class - vm.explicitlyPreload(decryptionMethodOwner, true, (name, desc) -> !name.matches("java/lang/.*")); - Class loadedClone = vm.loadClass(fakeInvocationClone.name.replace('/', '.'), true); // load - // dupe class - - if (m.name.equals("")) { - loadedClone.newInstance(); // special case: - // constructors have to be invoked by newInstance. - // Sandbox.createMethodProxy automatically handles - // access and super call - } else { - for (Method reflectionMethod : loadedClone.getMethods()) { - if (reflectionMethod.getName().equals(m.name)) { - reflectionMethod.invoke(null); - break; - } - } - } - return (String) loadedClone.getDeclaredField("proxyReturn").get(null); - } - - private void createFakeClone(ClassNode cn, MethodNode m, MethodInsnNode min, String encrypted) { - ClassNode node = Sandbox.createClassProxy(cn.name); - InsnList instructions = new InsnList(); - instructions.add(new LdcInsnNode(encrypted)); - instructions.add(min.clone(null)); // we can clone - // original method here - instructions.add(new FieldInsnNode(PUTSTATIC, node.name, "proxyReturn", "Ljava/lang/String;")); - instructions.add(new InsnNode(RETURN)); - - node.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, "proxyReturn", "Ljava/lang/String;", null, null)); - node.methods.add(Sandbox.createMethodProxy(instructions, m.name, "()V")); // method should return real - // string - if (min.owner.equals(cn.name)) { - // decryption method is in own class - node.methods.add(Sandbox.copyMethod(getMethod(classes.get(min.owner).node, min.name, min.desc))); - } - fakeInvocationClone = node; - } - - private ClassNode fakeInvocationClone; - - @Override - public ClassNode tryClassLoad(String name) { - if (name.equals(fakeInvocationClone.name)) { - return fakeInvocationClone; - } - return classes.containsKey(name) ? classes.get(name).node : null; - } - - @Override - public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { - return null; - } - - @Override - public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, - List values) { - return null; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java index f057968..38bfd64 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java @@ -12,87 +12,170 @@ public class InlineMethods extends Execution { public InlineMethods() { super(ExecutionCategory.CLEANING, "Inline static methods without invocation", - "Inline static methods that only return or throw.
Can be" + - " useful for deobfuscating try catch block obfuscation.", ExecutionTag.SHRINK, - ExecutionTag.RUNNABLE); + "Inline static methods that only return or throw.
Can be" + + " useful for deobfuscating try catch block obfuscation.", ExecutionTag.SHRINK, + ExecutionTag.RUNNABLE); } public int inlines; @Override public boolean execute(Map classes, boolean verbose) { + // Map to hold methods that can potentially be inlined HashMap map = new HashMap<>(); - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.stream().filter(this::isUnnecessary) - .forEach(m -> map.put(c.name + "." + m.name + m.desc, m))); + + // Step 1: Collect all unnecessary methods that can be inlined + classes.values().stream() + .map(c -> c.node) + .forEach(c -> c.methods.stream() + .filter(this::isUnnecessary) + .forEach(m -> { + String methodKey = c.name + "." + m.name + m.desc; + map.put(methodKey, m); + if (verbose) { + logger.debug("Collected method for inlining: {}", methodKey); + } + })); + logger.info("{} unnecessary methods found that could be inlined", map.size()); inlines = 0; - classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .forEach(m -> m.instructions.forEach(ain -> { - if (ain.getOpcode() == INVOKESTATIC) { // - // can't inline invokevirtual / special - // as object could only be superclass and - // real overrides - MethodInsnNode min = (MethodInsnNode) ain; - String key = min.owner + "." + min.name + min.desc; - if (map.containsKey(key)) { - inlineMethod(m, min, map.get(key)); - m.maxStack = Math.max(map.get(key).maxStack, m.maxStack); - m.maxLocals = Math.max(map.get(key).maxLocals, m.maxLocals); - inlines++; - } - } - })); - - // map.forEach((key, method) -> classes.get(key - // .substring(0, key.lastIndexOf('.'))).node.methods - // .removeIf(m -> m.equals(method) && !Access - // .isPublic(method.access))); - map.forEach((key, method) -> classes.get(key.substring(0, key.lastIndexOf('.'))).node.methods.remove(method)); + + // Step 2: Scan for method invocations and attempt inlining + classes.values().stream() + .map(c -> c.node.methods) + .flatMap(List::stream) + .forEach(m -> m.instructions.forEach(ain -> { + if (ain.getOpcode() == INVOKESTATIC) { + MethodInsnNode min = (MethodInsnNode) ain; + String invocationKey = min.owner + "." + min.name + min.desc; + if (verbose) { + logger.debug("Found method invocation: {}", invocationKey); + } + if (map.containsKey(invocationKey)) { + inlineMethod(m, min, map.get(invocationKey)); + m.maxStack = Math.max(map.get(invocationKey).maxStack, m.maxStack); + m.maxLocals = Math.max(map.get(invocationKey).maxLocals, m.maxLocals); + inlines++; + if (verbose) { + logger.debug("Inlined method {} in method {} of class {}", invocationKey, m.name, m.desc); + } + } + } + })); + logger.info("Inlined {} method references!", inlines); + + // Step 3: Remove all methods identified as unnecessary, regardless of inlining + // This ensures that any methods not inlined (i.e., not invoked) are deleted + int deletedUnnecessaryMethods = 0; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + MethodNode method = entry.getValue(); + String className = key.substring(0, key.lastIndexOf('.')); + Clazz clazz = classes.get(className); + if (clazz != null) { + boolean removed = clazz.node.methods.remove(method); + if (removed) { + deletedUnnecessaryMethods++; + if (verbose) { + logger.debug("Deleted unnecessary method {} from class {}", key, className); + } + } + } + } + logger.info("Deleted {} unnecessary methods!", deletedUnnecessaryMethods); + + // Step 4: Delete junk methods (static methods without RETURN or ATHROW) + // These methods are already considered unnecessary, but are handled separately for clarity + int deletedJunkMethods = 0; + for (Clazz clazz : classes.values()) { + Iterator methodIterator = clazz.node.methods.iterator(); + while (methodIterator.hasNext()) { + MethodNode method = methodIterator.next(); + if (isJunkMethod(method)) { + methodIterator.remove(); + deletedJunkMethods++; + if (verbose) { + logger.debug("Deleted junk method {}.{}{}", clazz.node.name, method.name, method.desc); + } + } + } + } + logger.info("Deleted {} junk methods without return or throw instructions!", deletedJunkMethods); + return true; } - private void inlineMethod(MethodNode m, MethodInsnNode min, MethodNode method) { - InsnList copy = Instructions.copy(method.instructions); + /** + * Inlines a static method invocation with the method's instructions. + * + * @param callerMethod The method containing the invocation. + * @param min The method invocation instruction node. + * @param calleeMethod The method being inlined. + */ + private void inlineMethod(MethodNode callerMethod, MethodInsnNode min, MethodNode calleeMethod) { + InsnList copy = Instructions.copy(calleeMethod.instructions); + + // Remove line and frame instructions StreamSupport.stream(copy.spliterator(), false) - .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) - .forEach(copy::remove); - removeReturn(copy); + .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) + .forEach(copy::remove); - InsnList fakeVarList = createFakeVarList(method); + boolean hasReturn = removeReturn(copy); + if (!hasReturn) { + // Cannot inline methods without RETURN or ATHROW + logger.error("Cannot inline method {}.{}{} because it has no return or throw instruction.", + calleeMethod.name, calleeMethod.name, calleeMethod.desc); + return; + } + + InsnList fakeVarList = createFakeVarList(calleeMethod); copy.insert(fakeVarList); - StreamSupport.stream(copy.spliterator(), false).filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) - .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); // - // offset local - // variables to not - // collide with existing ones - m.instructions.insert(min, copy); - m.instructions.remove(min); + // Offset local variables to avoid collisions + int localVarOffset = callerMethod.maxLocals + 4; + StreamSupport.stream(copy.spliterator(), false) + .filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) + .map(ain -> (VarInsnNode) ain) + .forEach(v -> v.var += localVarOffset); + + callerMethod.instructions.insert(min, copy); + callerMethod.instructions.remove(min); } - private InsnList createFakeVarList(MethodNode m) { + /** + * Creates an instruction list to load method parameters into local variables. + * + * @param method The method whose parameters are being inlined. + * @return An instruction list for loading parameters. + */ + private InsnList createFakeVarList(MethodNode method) { InsnList fakeVarList = new InsnList(); - LinkedHashMap varTypes = getVarsAndTypesForDesc(m.desc.substring(1, m.desc.indexOf(')'))); - for (int var : varTypes.keySet()) { - fakeVarList.insert(new VarInsnNode(varTypes.get(var), var)); // make sure its reversed + LinkedHashMap varTypes = getVarsAndTypesForDesc(method.desc.substring(1, method.desc.indexOf(')'))); + for (Map.Entry entry : varTypes.entrySet()) { + fakeVarList.insert(new VarInsnNode(entry.getValue(), entry.getKey())); } - // pop object here for non static invoke: fakeVarList - // .add(new InsnNode(POP)); return fakeVarList; } + /** + * Parses method descriptors to determine local variable types and their corresponding store opcodes. + * + * @param rawType The raw method descriptor parameters (between '(' and ')'). + * @return A map of local variable indices to their store opcodes. + */ public static LinkedHashMap getVarsAndTypesForDesc(String rawType) { LinkedHashMap map = new LinkedHashMap<>(); - int var = 0; // would be 1 on non-static methods + int var = 0; // Starting index for local variables boolean object = false; boolean array = false; - for (char c : rawType.toCharArray()) { + for (int i = 0; i < rawType.length(); i++) { + char c = rawType.charAt(i); if (!object) { if (array && c != 'L') { - map.put(var, ASTORE); // array type is astore + map.put(var, ASTORE); // array type is ASTORE var++; array = false; continue; @@ -123,6 +206,9 @@ public static LinkedHashMap getVarsAndTypesForDesc(String rawT case '[': array = true; break; + default: + // Handle other types if necessary + break; } } else if (c == ';') { object = false; @@ -131,44 +217,96 @@ public static LinkedHashMap getVarsAndTypesForDesc(String rawT return map; } - private void removeReturn(InsnList copy) { - int i = copy.size() - 1; + /** + * Attempts to remove the return instruction from the method's instructions. + * + * @param instructions The instructions to modify. + * @return true if a RETURN or ATHROW instruction was found and removed, false otherwise. + */ + private boolean removeReturn(InsnList instructions) { + int i = instructions.size() - 1; while (i >= 0) { - AbstractInsnNode ain = copy.get(i); + AbstractInsnNode ain = instructions.get(i); + int opcode = ain.getOpcode(); - if (ain.getOpcode() == ATHROW) { - // keep athrow, as it would still be in code - return; + if (opcode == ATHROW) { + // Keep ATHROW; it's part of the method's behavior + return true; } - copy.remove(ain); - switch (ain.getOpcode()) { - case RETURN: - case ARETURN: - case DRETURN: - case FRETURN: - case IRETURN: - case LRETURN: - return; - default: + + if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || + opcode == FRETURN || opcode == IRETURN || opcode == LRETURN) { + instructions.remove(ain); + return true; } + + instructions.remove(ain); i--; } - throw new RuntimeException("no return found to remove, invalid method?"); + // No return or throw instruction found + return false; } + /** + * Determines if a method is unnecessary for inlining. + * A method is unnecessary if it is static, contains at least one RETURN or ATHROW instruction, + * and does not contain any method invocations or jump instructions. + * + * @param m The method node to check. + * @return true if the method is unnecessary, false otherwise. + */ public boolean isUnnecessary(MethodNode m) { if (!Access.isStatic(m.access)) { return false; - } else if (m.instructions.size() > 32) { - // do not inline huge methods - return false; - } else if (m.instructions.size() < 2) { - // abstract methods or similar + } + + boolean hasReturnOrThrow = false; + for (AbstractInsnNode ain : m.instructions) { + int opcode = ain.getOpcode(); + if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || + opcode == FRETURN || opcode == IRETURN || opcode == LRETURN || + opcode == ATHROW) { + hasReturnOrThrow = true; + } + if (isInvocationOrJump(ain)) { + return false; + } + } + return hasReturnOrThrow; + } + + /** + * Determines if a method is a junk method. + * A junk method is static and contains no RETURN or ATHROW instructions. + * + * @param m The method node to check. + * @return true if the method is junk, false otherwise. + */ + public boolean isJunkMethod(MethodNode m) { + // A junk method is static and has no return or throw instruction + if (!Access.isStatic(m.access)) { return false; } - return StreamSupport.stream(m.instructions.spliterator(), false).noneMatch(this::isInvocationOrJump); + + boolean hasReturnOrThrow = false; + for (AbstractInsnNode ain : m.instructions) { + int opcode = ain.getOpcode(); + if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || + opcode == FRETURN || opcode == IRETURN || opcode == LRETURN || + opcode == ATHROW) { + hasReturnOrThrow = true; + break; + } + } + return !hasReturnOrThrow; } + /** + * Determines if an instruction node represents a method invocation or jump. + * + * @param ain The instruction node to check. + * @return true if the instruction is a method invocation or jump, false otherwise. + */ public boolean isInvocationOrJump(AbstractInsnNode ain) { switch (ain.getType()) { case AbstractInsnNode.METHOD_INSN: @@ -177,7 +315,8 @@ public boolean isInvocationOrJump(AbstractInsnNode ain) { case AbstractInsnNode.TYPE_INSN: case AbstractInsnNode.JUMP_INSN: return true; + default: + return false; } - return false; } } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java index 70c99f3..46db592 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java @@ -52,6 +52,7 @@ public void inline(ClassNode cn, FieldNode fn) { if (isGetReferenceTo(cn, fin, fn)) { m.instructions.set(ain, Instructions.makeNullPush(Type.getType(fn.desc))); inlines++; + logger.debug("Inlined field {} in method {} in class {}", fn.name, m.name, c.name); } } } diff --git a/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java b/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java index 1a0ce80..12627e8 100644 --- a/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java +++ b/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java @@ -4,55 +4,66 @@ import me.nov.threadtear.execution.Execution; import me.nov.threadtear.execution.ExecutionCategory; import me.nov.threadtear.execution.ExecutionTag; +import me.nov.threadtear.util.asm.Access; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodNode; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; public class FlowObfuscationZKM extends Execution { private static final Predicate singleJump = op -> (op >= IFEQ && op <= IFLE) || op == IFNULL || op == IFNONNULL; - private int replaced; public FlowObfuscationZKM() { super(ExecutionCategory.ZKM, "Flow obfuscation removal", - "Tested on ZKM 14, could work on newer versions too.", ExecutionTag.POSSIBLE_DAMAGE, + "Rewritten and needs to be teseted", ExecutionTag.POSSIBLE_DAMAGE, ExecutionTag.BETTER_DECOMPILE); } @Override public boolean execute(Map classes, boolean verbose) { - replaced = 0; - logger.info("Removing all garbage jumps"); - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.forEach(this::removeZKMJumps)); - logger.info("Removed {} jumps matching ZKM pattern in total", replaced); - return replaced > 0; - } + AtomicInteger counter = new AtomicInteger(); + + classes.values().forEach(clazz -> { + ClassNode classNode = clazz.node; + + classNode.methods.stream() + .filter(methodNode -> !Access.isAbstract(methodNode.access) && !Access.isNative(methodNode.access)) + .forEach(methodNode -> { + int originalTryCatchCount = methodNode.tryCatchBlocks.size(); + + methodNode.tryCatchBlocks.removeIf(tc -> { + AbstractInsnNode handlerNext = tc.handler.getNext(); + if (handlerNext == null) return false; + + int opcode = handlerNext.getOpcode(); + + // Check for INVOKESTATIC followed by ATHROW + if (opcode == Opcodes.INVOKESTATIC) { + AbstractInsnNode nextNext = handlerNext.getNext(); + return nextNext != null && nextNext.getOpcode() == Opcodes.ATHROW; + } + // Check for ATHROW directly + else if (opcode == Opcodes.ATHROW) { + return true; + } + return false; + }); + + int removedTryCatchCount = originalTryCatchCount - methodNode.tryCatchBlocks.size(); + counter.addAndGet(removedTryCatchCount); + }); + }); - public void removeZKMJumps(MethodNode mn) { - for (AbstractInsnNode ain : mn.instructions.toArray()) { - if (ain.getPrevious() != null && singleJump.test(ain.getOpcode())) { - AbstractInsnNode previous = ain.getPrevious(); - boolean shouldPop = false; - if (ain.getOpcode() == IFNULL || ain.getOpcode() == IFNONNULL) { //first case flow obfuscation scenario - if (previous.getOpcode() == ALOAD) { - shouldPop = true; - } - } else if (ain.getOpcode() >= IFEQ && ain.getOpcode() <= IFLE) { //second case - if (previous.getOpcode() == ILOAD) { - shouldPop = true; - } - } - if (shouldPop) { - mn.instructions.set(ain, new InsnNode(POP)); - replaced++; - } - } - } + logger.info("[ZKM] Removed {} fake try-catch blocks.", counter.get()); + return counter.get() > 0; } } diff --git a/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java b/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java new file mode 100644 index 0000000..2b875b4 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java @@ -0,0 +1,105 @@ +package me.nov.threadtear.util; + + +import me.nov.threadtear.execution.Clazz; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ByteCodeUtil { + /** + * Finds all classes that import the specified class. + * + * @param classes The map of class names to Clazz objects. + * @param specificClass The name of the specific class to find imports for. + * @return A map of class names to Clazz objects that import the specific class. + */ + public static Map findallimports(Map classes, String specificClass) { + Map result = new HashMap<>(); + + for (Map.Entry entry : classes.entrySet()) { + Clazz clazz = entry.getValue(); + if (importsClass(clazz, specificClass)) { + result.put(entry.getKey(), clazz); + } + } + + return result; + } + + /** + * Checks if the given class imports the specified class. + * + * @param clazz The Clazz object to check. + * @param specificClass The name of the specific class to check for. + * @return True if the clazz imports the specific class, false otherwise. + */ + private static boolean importsClass(Clazz clazz, String specificClass) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + for (AbstractInsnNode instruction : method.instructions) { + if (instruction instanceof MethodInsnNode) { + MethodInsnNode methodInsn = (MethodInsnNode) instruction; + if (methodInsn.owner.equals(specificClass.replace('.', '/'))) { + return true; + } + } + } + } + return false; + } + + public static List findVariableModifications(Map classes, Clazz Class, int varIndex) { + List modifications = new ArrayList<>(); + String targetClassName = Class.node.name; + + // Retrieve the list of fields from the target class + List fields = Class.node.fields; + if (varIndex < 0 || varIndex >= fields.size()) { + // Invalid varIndex + return modifications; + } + FieldNode targetField = fields.get(varIndex); + String targetFieldName = targetField.name; + String targetFieldDesc = targetField.desc; + + for (Clazz clazz : classes.values()) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + InsnList instructions = method.instructions; + for (AbstractInsnNode instruction : instructions) { + if (instruction instanceof FieldInsnNode) { + FieldInsnNode fieldInsn = (FieldInsnNode) instruction; + if ((fieldInsn.getOpcode() == Opcodes.PUTFIELD || fieldInsn.getOpcode() == Opcodes.PUTSTATIC) + && fieldInsn.owner.equals(targetClassName) + && fieldInsn.name.equals(targetFieldName) + && fieldInsn.desc.equals(targetFieldDesc)) { + // Record the modification + String modificationDetail = "Field " + fieldInsn.owner.replace('/', '.') + "." + fieldInsn.name + + " modified in method " + method.name + " of class " + classNode.name.replace('/', '.'); + modifications.add(modificationDetail); + } + } + } + } + } + return modifications; + } + + + /** + * Checks if the given opcode corresponds to a variable modification. + * + * @param opcode The opcode to check. + * @return True if the opcode is a modification, false otherwise. + */ + private static boolean isModificationOpcode(int opcode) { + // Opcodes for variable modification (store instructions) + return opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE || opcode == Opcodes.FSTORE || + opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE; + } +} From 7d2f94e3e70f68b9f954fc9a59631f5da6115788 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Wed, 23 Oct 2024 17:17:46 +0800 Subject: [PATCH 09/13] updated executions --- .../execution/cleanup/InlineArithmetics.java | 24 ++ .../execution/cleanup/InlineMethods.java | 261 ++++-------------- .../cleanup/remove/RemovedUnusedClasses.java | 143 ++++++++++ 3 files changed, 228 insertions(+), 200 deletions(-) create mode 100644 core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java create mode 100644 core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java new file mode 100644 index 0000000..9d7430d --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java @@ -0,0 +1,24 @@ +package me.nov.threadtear.execution.cleanup; + +import me.nov.threadtear.execution.Clazz; +import me.nov.threadtear.execution.Execution; +import me.nov.threadtear.execution.ExecutionCategory; +import me.nov.threadtear.execution.ExecutionTag; + +import java.util.Map; + +public class InlineArithmetics extends Execution { + + public InlineArithmetics() { + super(ExecutionCategory.CLEANING, "Inline arithmetics", + "Inline simple arithmetics that only return or throw.
Can be useful for deobfuscating arithmetic obfuscation.", + ExecutionTag.SHRINK, ExecutionTag.RUNNABLE); + } + + @Override + public boolean execute(Map classes, boolean verbose) { + + + return false; + } +} diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java index 38bfd64..4f78a5c 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java @@ -21,161 +21,78 @@ public InlineMethods() { @Override public boolean execute(Map classes, boolean verbose) { - // Map to hold methods that can potentially be inlined HashMap map = new HashMap<>(); - - // Step 1: Collect all unnecessary methods that can be inlined - classes.values().stream() - .map(c -> c.node) - .forEach(c -> c.methods.stream() - .filter(this::isUnnecessary) - .forEach(m -> { - String methodKey = c.name + "." + m.name + m.desc; - map.put(methodKey, m); - if (verbose) { - logger.debug("Collected method for inlining: {}", methodKey); - } - })); - + classes.values().stream().map(c -> c.node).forEach(c -> c.methods.stream().filter(this::isUnnecessary) + .forEach(m -> map.put(c.name + "." + m.name + m.desc, m))); logger.info("{} unnecessary methods found that could be inlined", map.size()); inlines = 0; - - // Step 2: Scan for method invocations and attempt inlining - classes.values().stream() - .map(c -> c.node.methods) - .flatMap(List::stream) + classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) .forEach(m -> m.instructions.forEach(ain -> { - if (ain.getOpcode() == INVOKESTATIC) { + if (ain.getOpcode() == INVOKESTATIC) { // + // can't inline invokevirtual / special + // as object could only be superclass and + // real overrides MethodInsnNode min = (MethodInsnNode) ain; - String invocationKey = min.owner + "." + min.name + min.desc; - if (verbose) { - logger.debug("Found method invocation: {}", invocationKey); - } - if (map.containsKey(invocationKey)) { - inlineMethod(m, min, map.get(invocationKey)); - m.maxStack = Math.max(map.get(invocationKey).maxStack, m.maxStack); - m.maxLocals = Math.max(map.get(invocationKey).maxLocals, m.maxLocals); + String key = min.owner + "." + min.name + min.desc; + if (map.containsKey(key)) { + inlineMethod(m, min, map.get(key)); + m.maxStack = Math.max(map.get(key).maxStack, m.maxStack); + m.maxLocals = Math.max(map.get(key).maxLocals, m.maxLocals); inlines++; - if (verbose) { - logger.debug("Inlined method {} in method {} of class {}", invocationKey, m.name, m.desc); - } } } })); + // map.forEach((key, method) -> classes.get(key + // .substring(0, key.lastIndexOf('.'))).node.methods + // .removeIf(m -> m.equals(method) && !Access + // .isPublic(method.access))); + map.forEach((key, method) -> classes.get(key.substring(0, key.lastIndexOf('.'))).node.methods.remove(method)); logger.info("Inlined {} method references!", inlines); - - // Step 3: Remove all methods identified as unnecessary, regardless of inlining - // This ensures that any methods not inlined (i.e., not invoked) are deleted - int deletedUnnecessaryMethods = 0; - for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - MethodNode method = entry.getValue(); - String className = key.substring(0, key.lastIndexOf('.')); - Clazz clazz = classes.get(className); - if (clazz != null) { - boolean removed = clazz.node.methods.remove(method); - if (removed) { - deletedUnnecessaryMethods++; - if (verbose) { - logger.debug("Deleted unnecessary method {} from class {}", key, className); - } - } - } - } - logger.info("Deleted {} unnecessary methods!", deletedUnnecessaryMethods); - - // Step 4: Delete junk methods (static methods without RETURN or ATHROW) - // These methods are already considered unnecessary, but are handled separately for clarity - int deletedJunkMethods = 0; - for (Clazz clazz : classes.values()) { - Iterator methodIterator = clazz.node.methods.iterator(); - while (methodIterator.hasNext()) { - MethodNode method = methodIterator.next(); - if (isJunkMethod(method)) { - methodIterator.remove(); - deletedJunkMethods++; - if (verbose) { - logger.debug("Deleted junk method {}.{}{}", clazz.node.name, method.name, method.desc); - } - } - } - } - logger.info("Deleted {} junk methods without return or throw instructions!", deletedJunkMethods); - return true; } - /** - * Inlines a static method invocation with the method's instructions. - * - * @param callerMethod The method containing the invocation. - * @param min The method invocation instruction node. - * @param calleeMethod The method being inlined. - */ - private void inlineMethod(MethodNode callerMethod, MethodInsnNode min, MethodNode calleeMethod) { - InsnList copy = Instructions.copy(calleeMethod.instructions); - - // Remove line and frame instructions + private void inlineMethod(MethodNode m, MethodInsnNode min, MethodNode method) { + InsnList copy = Instructions.copy(method.instructions); StreamSupport.stream(copy.spliterator(), false) .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) .forEach(copy::remove); + removeReturn(copy); - boolean hasReturn = removeReturn(copy); - if (!hasReturn) { - // Cannot inline methods without RETURN or ATHROW - logger.error("Cannot inline method {}.{}{} because it has no return or throw instruction.", - calleeMethod.name, calleeMethod.name, calleeMethod.desc); - return; - } - - InsnList fakeVarList = createFakeVarList(calleeMethod); + InsnList fakeVarList = createFakeVarList(method); copy.insert(fakeVarList); - // Offset local variables to avoid collisions - int localVarOffset = callerMethod.maxLocals + 4; - StreamSupport.stream(copy.spliterator(), false) - .filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) - .map(ain -> (VarInsnNode) ain) - .forEach(v -> v.var += localVarOffset); - - callerMethod.instructions.insert(min, copy); - callerMethod.instructions.remove(min); + StreamSupport.stream(copy.spliterator(), false).filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) + .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); // + // offset local + // variables to not + // collide with existing ones + m.instructions.insert(min, copy); + m.instructions.remove(min); } - /** - * Creates an instruction list to load method parameters into local variables. - * - * @param method The method whose parameters are being inlined. - * @return An instruction list for loading parameters. - */ - private InsnList createFakeVarList(MethodNode method) { + private InsnList createFakeVarList(MethodNode m) { InsnList fakeVarList = new InsnList(); - LinkedHashMap varTypes = getVarsAndTypesForDesc(method.desc.substring(1, method.desc.indexOf(')'))); - for (Map.Entry entry : varTypes.entrySet()) { - fakeVarList.insert(new VarInsnNode(entry.getValue(), entry.getKey())); + LinkedHashMap varTypes = getVarsAndTypesForDesc(m.desc.substring(1, m.desc.indexOf(')'))); + for (int var : varTypes.keySet()) { + fakeVarList.insert(new VarInsnNode(varTypes.get(var), var)); // make sure its reversed } + // pop object here for non static invoke: fakeVarList + // .add(new InsnNode(POP)); return fakeVarList; } - /** - * Parses method descriptors to determine local variable types and their corresponding store opcodes. - * - * @param rawType The raw method descriptor parameters (between '(' and ')'). - * @return A map of local variable indices to their store opcodes. - */ public static LinkedHashMap getVarsAndTypesForDesc(String rawType) { LinkedHashMap map = new LinkedHashMap<>(); - int var = 0; // Starting index for local variables + int var = 0; // would be 1 on non-static methods boolean object = false; boolean array = false; - for (int i = 0; i < rawType.length(); i++) { - char c = rawType.charAt(i); + for (char c : rawType.toCharArray()) { if (!object) { if (array && c != 'L') { - map.put(var, ASTORE); // array type is ASTORE + map.put(var, ASTORE); // array type is astore var++; array = false; continue; @@ -206,9 +123,6 @@ public static LinkedHashMap getVarsAndTypesForDesc(String rawT case '[': array = true; break; - default: - // Handle other types if necessary - break; } } else if (c == ';') { object = false; @@ -217,96 +131,44 @@ public static LinkedHashMap getVarsAndTypesForDesc(String rawT return map; } - /** - * Attempts to remove the return instruction from the method's instructions. - * - * @param instructions The instructions to modify. - * @return true if a RETURN or ATHROW instruction was found and removed, false otherwise. - */ - private boolean removeReturn(InsnList instructions) { - int i = instructions.size() - 1; + private void removeReturn(InsnList copy) { + int i = copy.size() - 1; while (i >= 0) { - AbstractInsnNode ain = instructions.get(i); - int opcode = ain.getOpcode(); + AbstractInsnNode ain = copy.get(i); - if (opcode == ATHROW) { - // Keep ATHROW; it's part of the method's behavior - return true; + if (ain.getOpcode() == ATHROW) { + // keep athrow, as it would still be in code + return; } - - if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || - opcode == FRETURN || opcode == IRETURN || opcode == LRETURN) { - instructions.remove(ain); - return true; + copy.remove(ain); + switch (ain.getOpcode()) { + case RETURN: + case ARETURN: + case DRETURN: + case FRETURN: + case IRETURN: + case LRETURN: + return; + default: } - - instructions.remove(ain); i--; } - // No return or throw instruction found - return false; + throw new RuntimeException("no return found to remove, invalid method?"); } - /** - * Determines if a method is unnecessary for inlining. - * A method is unnecessary if it is static, contains at least one RETURN or ATHROW instruction, - * and does not contain any method invocations or jump instructions. - * - * @param m The method node to check. - * @return true if the method is unnecessary, false otherwise. - */ public boolean isUnnecessary(MethodNode m) { if (!Access.isStatic(m.access)) { return false; - } - - boolean hasReturnOrThrow = false; - for (AbstractInsnNode ain : m.instructions) { - int opcode = ain.getOpcode(); - if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || - opcode == FRETURN || opcode == IRETURN || opcode == LRETURN || - opcode == ATHROW) { - hasReturnOrThrow = true; - } - if (isInvocationOrJump(ain)) { - return false; - } - } - return hasReturnOrThrow; - } - - /** - * Determines if a method is a junk method. - * A junk method is static and contains no RETURN or ATHROW instructions. - * - * @param m The method node to check. - * @return true if the method is junk, false otherwise. - */ - public boolean isJunkMethod(MethodNode m) { - // A junk method is static and has no return or throw instruction - if (!Access.isStatic(m.access)) { + } else if (m.instructions.size() > 32) { + // do not inline huge methods + return false; + } else if (m.instructions.size() < 2) { + // abstract methods or similar return false; } - - boolean hasReturnOrThrow = false; - for (AbstractInsnNode ain : m.instructions) { - int opcode = ain.getOpcode(); - if (opcode == RETURN || opcode == ARETURN || opcode == DRETURN || - opcode == FRETURN || opcode == IRETURN || opcode == LRETURN || - opcode == ATHROW) { - hasReturnOrThrow = true; - break; - } - } - return !hasReturnOrThrow; + return StreamSupport.stream(m.instructions.spliterator(), false).noneMatch(this::isInvocationOrJump); } - /** - * Determines if an instruction node represents a method invocation or jump. - * - * @param ain The instruction node to check. - * @return true if the instruction is a method invocation or jump, false otherwise. - */ public boolean isInvocationOrJump(AbstractInsnNode ain) { switch (ain.getType()) { case AbstractInsnNode.METHOD_INSN: @@ -315,8 +177,7 @@ public boolean isInvocationOrJump(AbstractInsnNode ain) { case AbstractInsnNode.TYPE_INSN: case AbstractInsnNode.JUMP_INSN: return true; - default: - return false; } + return false; } } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java new file mode 100644 index 0000000..b121e45 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java @@ -0,0 +1,143 @@ +package me.nov.threadtear.execution.cleanup.remove; + +import me.nov.threadtear.execution.Clazz; +import me.nov.threadtear.execution.Execution; +import me.nov.threadtear.execution.ExecutionCategory; +import me.nov.threadtear.execution.ExecutionTag; +import me.nov.threadtear.util.ByteCodeUtil; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.*; + +/** + * Execution to remove unused classes from the bytecode. + * A class is considered unused if no other class imports it and it's not a root class (e.g., has a main method). + */ +public class RemovedUnusedClasses extends Execution { + + public RemovedUnusedClasses() { + super(ExecutionCategory.CLEANING, "Remove Unused Classes","removes junk classes", + ExecutionTag.RUNNABLE, ExecutionTag.BETTER_DECOMPILE, ExecutionTag.BETTER_DEOBFUSCATE); + } + + @Override + public boolean execute(Map classes, boolean verbose) { + // Step 1: Identify Root Classes (Classes with main methods) + Set rootClasses = identifyRootClasses(classes, verbose); + + // Step 2: Identify Unused Classes + List unusedClasses = identifyUnusedClasses(classes, rootClasses, verbose); + + // Step 3: Remove Unused Classes + removeUnusedClasses(classes, unusedClasses, verbose); + + // Step 4: Log Summary + if (verbose) { + logger.info("Removal of unused classes completed."); + } + + return !unusedClasses.isEmpty(); + } + + /** + * Identifies root classes (classes containing a main method). + * + * @param classes Map of class names to Clazz objects. + * @param verbose Flag to enable verbose logging. + * @return Set of class names that are root classes. + */ + private Set identifyRootClasses(Map classes, boolean verbose) { + Set rootClasses = new HashSet<>(); + + for (Clazz clazz : classes.values()) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + if (isMainMethod(method)) { + rootClasses.add(classNode.name); + if (verbose) { + logger.debug("Identified root class (main method): {}", classNode.name.replace('/', '.')); + } + break; // No need to check other methods + } + } + } + + if (verbose) { + logger.debug("Total root classes identified: {}", rootClasses.size()); + } + + return rootClasses; + } + + /** + * Checks if a method is a main method. + * + * @param method The MethodNode to check. + * @return True if the method is a main method, false otherwise. + */ + private boolean isMainMethod(MethodNode method) { + return method.name.equals("main") && + method.desc.equals("([Ljava/lang/String;)V") && + (method.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)) == (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); + } + + /** + * Identifies unused classes that are not root classes and are not imported by any other class. + * + * @param classes Map of class names to Clazz objects. + * @param rootClasses Set of root class names. + * @param verbose Flag to enable verbose logging. + * @return List of class names that are unused. + */ + private List identifyUnusedClasses(Map classes, Set rootClasses, boolean verbose) { + List unusedClasses = new ArrayList<>(); + + for (Clazz clazz : classes.values()) { + String className = clazz.node.name; + + // Skip root classes + if (rootClasses.contains(className)) { + if (verbose) { + logger.debug("Skipping root class: {}", className.replace('/', '.')); + } + continue; + } + + // Check if any other class imports this class + Map importingClasses = ByteCodeUtil.findallimports(classes, className.replace('/', '.')); + + // Remove self-imports (if any) + importingClasses.remove(className); + + if (importingClasses.isEmpty()) { + unusedClasses.add(className); + if (verbose) { + logger.debug("Identified unused class: {}", className.replace('/', '.')); + } + } + } + + if (verbose) { + logger.info("Total unused classes identified: {}", unusedClasses.size()); + } + + return unusedClasses; + } + + /** + * Removes unused classes from the class map. + * + * @param classes Map of class names to Clazz objects. + * @param unusedClasses List of class names to remove. + * @param verbose Flag to enable verbose logging. + */ + private void removeUnusedClasses(Map classes, List unusedClasses, boolean verbose) { + for (String className : unusedClasses) { + classes.remove(className); + logger.debug("Removed unused class: {}", className.replace('/', '.')); + } + logger.info("Removed {} unused classes.", unusedClasses.size()); + } +} From 3ad7d0b862cf650b1fe7b18185df8b69958ccc28 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Sun, 27 Oct 2024 10:30:20 +0800 Subject: [PATCH 10/13] updated executions --- .../simplify/SimplifyBitOperations.java | 367 ++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java new file mode 100644 index 0000000..1d4a4c1 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java @@ -0,0 +1,367 @@ +package me.nov.threadtear.execution.cleanup.simplify; + +import java.util.*; + +import me.nov.threadtear.analysis.stack.ConstantTracker; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.*; +import me.nov.threadtear.execution.*; +import me.nov.threadtear.util.asm.Access; +import me.nov.threadtear.analysis.stack.ConstantValue; +import me.nov.threadtear.analysis.stack.IConstantReferenceHandler; + +/** + * Execution to simplify bitwise operations in Java bytecode. + * It performs constant folding and attempts to simplify common obfuscation patterns. + */ +public class SimplifyBitOperations extends Execution implements IConstantReferenceHandler { + + public SimplifyBitOperations() { + super( + ExecutionCategory.CLEANING, + "Simplify Bitwise Operations", + "Simplifies bitwise operations by performing constant folding and simplifying common obfuscation patterns." + ); + } + + private int simplifications = 0; + + @Override + public boolean execute(Map classes, boolean verbose) { + simplifications = 0; + + for (Clazz clazz : classes.values()) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + if (Access.isAbstract(method.access) || Access.isNative(method.access)) { + continue; // Skip abstract and native methods + } + + try { + simplifyMethod(classNode, method, verbose); + } catch (Exception e) { + clazz.addFail(e); + if (verbose) { + logger.error("Failed to simplify method {}.{}: {}", classNode.name, method.name, e.getMessage()); + } + } + } + } + + logger.info("Simplified {} bitwise operations.", simplifications); + return true; + } + + /** + * Simplifies bitwise operations within a method. + * + * @param classNode The class containing the method. + * @param method The method to simplify. + * @param verbose Flag to enable verbose logging. + * @throws AnalyzerException If bytecode analysis fails. + */ + private void simplifyMethod(ClassNode classNode, MethodNode method, boolean verbose) throws AnalyzerException { + Analyzer analyzer = new Analyzer<>(new ConstantTracker(this, Access.isStatic(method.access), method.maxLocals, method.desc, new Object[0])); + analyzer.analyze(classNode.name, method); + Frame[] frames = analyzer.getFrames(); + AbstractInsnNode[] insns = method.instructions.toArray(); + + for (int i = 0; i < insns.length; i++) { + AbstractInsnNode insn = insns[i]; + int opcode = insn.getOpcode(); + + if (isBitwiseOperation(opcode)) { + Frame frame = frames[i]; + if (frame == null) continue; // Dead code + + // Depending on the operation, retrieve operands + switch (opcode) { + case IAND: + case IOR: + case IXOR: + // Binary integer operations + simplifyBinaryIntOperation(method, insn, frame, opcode, Type.INT_TYPE, verbose); + break; + case LAND: + case LOR: + case LXOR: + // Binary long operations + simplifyBinaryIntOperation(method, insn, frame, opcode, Type.LONG_TYPE, verbose); + break; + case ISHL: + case ISHR: + case IUSHR: + // Shift operations + simplifyShiftOperation(method, insn, frame, opcode, Type.INT_TYPE, verbose); + break; + case LSHL: + case LSHR: + case LUSHR: + // Shift operations for long + simplifyShiftOperation(method, insn, frame, opcode, Type.LONG_TYPE, verbose); + break; + default: + break; + } + + // Attempt to simplify common bitmask patterns + simplifyBitMaskPattern(method, insn, frame, i, verbose); + } + } + } + + /** + * Checks if the opcode corresponds to a bitwise operation. + * + * @param opcode The opcode to check. + * @return True if it's a bitwise operation, else false. + */ + private boolean isBitwiseOperation(int opcode) { + return opcode == IAND || opcode == IOR || opcode == IXOR || + opcode == LAND || opcode == LOR || opcode == LXOR || + opcode == ISHL || opcode == ISHR || opcode == IUSHR || + opcode == LSHL || opcode == LSHR || opcode == LUSHR; + } + + /** + * Simplifies binary integer or long bitwise operations. + * + * @param method The method containing the instruction. + * @param insn The instruction to potentially replace. + * @param frame The current stack frame. + * @param opcode The opcode of the instruction. + * @param type The type of the operation (INT or LONG). + * @param verbose Verbose logging flag. + */ + private void simplifyBinaryIntOperation(MethodNode method, AbstractInsnNode insn, Frame frame, int opcode, Type type, boolean verbose) { + // For binary operations, the stack should have two operands + ConstantValue value2 = frame.getStack(frame.getStackSize() - 1); + ConstantValue value1 = frame.getStack(frame.getStackSize() - 2); + + if (value1.isKnown() && (value1.isInteger() || value1.isLong()) && + value2.isKnown() && (value2.isInteger() || value2.isLong())) { + // Perform the bitwise operation + long operand1 = ((Number) value1.getValue()).longValue(); + long operand2 = ((Number) value2.getValue()).longValue(); + long result = 0; + + switch (opcode) { + case IAND: + case LAND: + result = operand1 & operand2; + break; + case IOR: + case LOR: + result = operand1 | operand2; + break; + case IXOR: + case LXOR: + result = operand1 ^ operand2; + break; + default: + return; // Not a binary bitwise operation + } + + // Replace the bitwise operation with the constant result + AbstractInsnNode replacement = getConstantInsn(result, type); + method.instructions.set(insn, replacement); + simplifications++; + + if (verbose && logger != null) { // Ensure logger is not null + logger.debug("Simplified bitwise operation in method {}: {} {} {} -> {}", method.name, operand1, getOpcodeName(opcode), operand2, result); + } + } + } + + /** + * Simplifies shift operations. + * + * @param method The method containing the instruction. + * @param insn The instruction to potentially replace. + * @param frame The current stack frame. + * @param opcode The opcode of the instruction. + * @param type The type of the operation (INT or LONG). + * @param verbose Verbose logging flag. + */ + private void simplifyShiftOperation(MethodNode method, AbstractInsnNode insn, Frame frame, int opcode, Type type, boolean verbose) { + // For shift operations, the stack should have two operands: value and shift + ConstantValue shiftValue = frame.getStack(frame.getStackSize() - 1); + ConstantValue value = frame.getStack(frame.getStackSize() - 2); + + if (value.isKnown() && (value.isInteger() || value.isLong()) && + shiftValue.isKnown() && (shiftValue.isInteger() || shiftValue.isLong())) { + long operand = ((Number) value.getValue()).longValue(); + long shift = ((Number) shiftValue.getValue()).longValue(); + long result = 0; + + switch (opcode) { + case ISHL: + case LSHL: + result = operand << shift; + break; + case ISHR: + case LSHR: + result = operand >> shift; + break; + case IUSHR: + case LUSHR: + result = operand >>> shift; + break; + default: + return; // Not a shift operation + } + + // Replace the shift operation with the constant result + AbstractInsnNode replacement = getConstantInsn(result, type); + method.instructions.set(insn, replacement); + simplifications++; + + if (verbose && logger != null) { // Ensure logger is not null + logger.debug("Simplified shift operation in method {}: {} {} {}", method.name, operand, getOpcodeName(opcode), shift, result); + } + } + } + + /** + * Attempts to simplify common bitmask patterns. + * + * @param method The method containing the instruction. + * @param insn The current instruction. + * @param frame The current stack frame. + * @param index The index of the instruction. + * @param verbose Verbose logging flag. + */ + private void simplifyBitMaskPattern(MethodNode method, AbstractInsnNode insn, Frame frame, int index, boolean verbose) { + // Example pattern: (var0 >>> shift) & mask + // Attempt to simplify if possible + if (!(insn instanceof InsnNode)) return; + + int opcode = insn.getOpcode(); + if (opcode != IAND && opcode != LAND) return; + + // Get the previous instruction (should be the shift operation) + AbstractInsnNode prevInsn = insn.getPrevious(); + if (prevInsn == null) return; + + int prevOpcode = prevInsn.getOpcode(); + if (!(prevOpcode == ISHL || prevOpcode == ISHR || prevOpcode == IUSHR || + prevOpcode == LSHL || prevOpcode == LSHR || prevOpcode == LUSHR)) { + return; + } + + // Check if the shift operation has a known shift value + Frame shiftFrame = frame; + ConstantValue maskValue = frame.getStack(frame.getStackSize() - 1); + ConstantValue shiftResult = frame.getStack(frame.getStackSize() - 2); + + if (maskValue.isKnown() && (maskValue.isInteger() || maskValue.isLong())) { + long mask = ((Number) maskValue.getValue()).longValue(); + + // Attempt to determine if the mask is a power of two minus one (e.g., 1, 3, 7, 15, 31, 63, ...) + if (isPowerOfTwoMinusOne(mask)) { + int bitCount = Long.bitCount(mask); + // For example, mask = 63 (0x3F) -> bitCount = 6 + + // This pattern is often used to extract specific bits + // Without knowing var0's value, we cannot simplify further + // However, we can annotate or mark this pattern for manual review + + if (verbose) { + logger.debug("Detected bitmask pattern in method {}: mask = {}", method.name, mask); + } + + // Optionally, you can insert comments or metadata for manual analysis + // Since ASM does not support comments, this step is limited + } + } + } + + /** + * Checks if a number is a power of two minus one (e.g., 1, 3, 7, 15, 31, ...). + * + * @param number The number to check. + * @return True if the number is a power of two minus one, else false. + */ + private boolean isPowerOfTwoMinusOne(long number) { + return (number & (number + 1)) == 0 && number != 0; + } + + /** + * Creates a constant instruction node based on the type and value. + * + * @param value The constant value. + * @param type The type of the constant (INT or LONG). + * @return The corresponding instruction node. + */ + private AbstractInsnNode getConstantInsn(long value, Type type) { + if (type.equals(Type.INT_TYPE)) { + if (value >= -1 && value <= 5) { + switch ((int) value) { + case -1: return new InsnNode(ICONST_M1); + case 0: return new InsnNode(ICONST_0); + case 1: return new InsnNode(ICONST_1); + case 2: return new InsnNode(ICONST_2); + case 3: return new InsnNode(ICONST_3); + case 4: return new InsnNode(ICONST_4); + case 5: return new InsnNode(ICONST_5); + } + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + return new IntInsnNode(BIPUSH, (int) value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + return new IntInsnNode(SIPUSH, (int) value); + } else { + return new LdcInsnNode((int) value); + } + } else if (type.equals(Type.LONG_TYPE)) { + if (value == 0L || value == 1L) { + return new InsnNode(value == 0L ? LCONST_0 : LCONST_1); + } else { + return new LdcInsnNode(value); + } + } + return new LdcInsnNode(value); + } + + /** + * Retrieves the opcode name for logging purposes. + * + * @param opcode The opcode to get the name for. + * @return The name of the opcode. + */ + private String getOpcodeName(int opcode) { + switch (opcode) { + case IAND: return "IAND"; + case IOR: return "IOR"; + case IXOR: return "IXOR"; + case LAND: return "LAND"; + case LOR: return "LOR"; + case LXOR: return "LXOR"; + case ISHL: return "ISHL"; + case ISHR: return "ISHR"; + case IUSHR: return "IUSHR"; + case LSHL: return "LSHL"; + case LSHR: return "LSHR"; + case LUSHR: return "LUSHR"; + default: return "UNKNOWN"; + } + } + + /** + * Implementation of IConstantReferenceHandler interface methods. + * These methods are required for constant tracking but are not utilized in this execution. + */ + + @Override + public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { + // Not required for this execution + return null; + } + + @Override + public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, List values) { + return null; + } + + +} From e3f9dd3ba345f8e7216157e82daca796d9536436 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Sun, 27 Oct 2024 17:13:29 +0800 Subject: [PATCH 11/13] modified inline methods to handle special invokes, also modified jar IO and file stripping to take in illegal bytecode and try to retain its class content --- .../execution/cleanup/InlineMethods.java | 46 +++--- .../cleanup/remove/RemovedUnusedClasses.java | 143 ------------------ .../main/java/me/nov/threadtear/io/JarIO.java | 32 +++- .../threadtear/swing/tree/ClassTreePanel.java | 25 --- 4 files changed, 51 insertions(+), 195 deletions(-) delete mode 100644 core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java index 4f78a5c..eb0dd90 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java @@ -28,10 +28,7 @@ public boolean execute(Map classes, boolean verbose) { inlines = 0; classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) .forEach(m -> m.instructions.forEach(ain -> { - if (ain.getOpcode() == INVOKESTATIC) { // - // can't inline invokevirtual / special - // as object could only be superclass and - // real overrides + if (ain.getOpcode() == INVOKESTATIC || ain.getOpcode() == INVOKEVIRTUAL || ain.getOpcode() == INVOKESPECIAL) { MethodInsnNode min = (MethodInsnNode) ain; String key = min.owner + "." + min.name + min.desc; if (map.containsKey(key)) { @@ -43,10 +40,6 @@ public boolean execute(Map classes, boolean verbose) { } })); - // map.forEach((key, method) -> classes.get(key - // .substring(0, key.lastIndexOf('.'))).node.methods - // .removeIf(m -> m.equals(method) && !Access - // .isPublic(method.access))); map.forEach((key, method) -> classes.get(key.substring(0, key.lastIndexOf('.'))).node.methods.remove(method)); logger.info("Inlined {} method references!", inlines); return true; @@ -57,16 +50,19 @@ private void inlineMethod(MethodNode m, MethodInsnNode min, MethodNode method) { StreamSupport.stream(copy.spliterator(), false) .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) .forEach(copy::remove); - removeReturn(copy); + removeReturn(copy, method); InsnList fakeVarList = createFakeVarList(method); copy.insert(fakeVarList); + if (min.getOpcode() != INVOKESTATIC) { + // Handle 'this' reference for non-static methods + copy.insert(new VarInsnNode(ALOAD, 0)); // Load 'this' + } + StreamSupport.stream(copy.spliterator(), false).filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) - .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); // - // offset local - // variables to not - // collide with existing ones + .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); + m.instructions.insert(min, copy); m.instructions.remove(min); } @@ -131,7 +127,7 @@ public static LinkedHashMap getVarsAndTypesForDesc(String rawT return map; } - private void removeReturn(InsnList copy) { + private void removeReturn(InsnList copy, MethodNode m) { int i = copy.size() - 1; while (i >= 0) { AbstractInsnNode ain = copy.get(i); @@ -150,22 +146,24 @@ private void removeReturn(InsnList copy) { case LRETURN: return; default: + // Continue removing until a proper return statement is found + break; } i--; } - throw new RuntimeException("no return found to remove, invalid method?"); + // then skip this method and log the exception instead of throwing it + logger.error("No return found to remove, invalid method?, method name: {}", m); + } public boolean isUnnecessary(MethodNode m) { - if (!Access.isStatic(m.access)) { - return false; - } else if (m.instructions.size() > 32) { - // do not inline huge methods - return false; - } else if (m.instructions.size() < 2) { - // abstract methods or similar - return false; - } +// if (m.instructions.size() > 32) { +// // do not inline huge methods +// return false; +// } else if (m.instructions.size() < 2) { +// // abstract methods or similar +// return false; +// } return StreamSupport.stream(m.instructions.spliterator(), false).noneMatch(this::isInvocationOrJump); } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java deleted file mode 100644 index b121e45..0000000 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/remove/RemovedUnusedClasses.java +++ /dev/null @@ -1,143 +0,0 @@ -package me.nov.threadtear.execution.cleanup.remove; - -import me.nov.threadtear.execution.Clazz; -import me.nov.threadtear.execution.Execution; -import me.nov.threadtear.execution.ExecutionCategory; -import me.nov.threadtear.execution.ExecutionTag; -import me.nov.threadtear.util.ByteCodeUtil; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.*; - -/** - * Execution to remove unused classes from the bytecode. - * A class is considered unused if no other class imports it and it's not a root class (e.g., has a main method). - */ -public class RemovedUnusedClasses extends Execution { - - public RemovedUnusedClasses() { - super(ExecutionCategory.CLEANING, "Remove Unused Classes","removes junk classes", - ExecutionTag.RUNNABLE, ExecutionTag.BETTER_DECOMPILE, ExecutionTag.BETTER_DEOBFUSCATE); - } - - @Override - public boolean execute(Map classes, boolean verbose) { - // Step 1: Identify Root Classes (Classes with main methods) - Set rootClasses = identifyRootClasses(classes, verbose); - - // Step 2: Identify Unused Classes - List unusedClasses = identifyUnusedClasses(classes, rootClasses, verbose); - - // Step 3: Remove Unused Classes - removeUnusedClasses(classes, unusedClasses, verbose); - - // Step 4: Log Summary - if (verbose) { - logger.info("Removal of unused classes completed."); - } - - return !unusedClasses.isEmpty(); - } - - /** - * Identifies root classes (classes containing a main method). - * - * @param classes Map of class names to Clazz objects. - * @param verbose Flag to enable verbose logging. - * @return Set of class names that are root classes. - */ - private Set identifyRootClasses(Map classes, boolean verbose) { - Set rootClasses = new HashSet<>(); - - for (Clazz clazz : classes.values()) { - ClassNode classNode = clazz.node; - for (MethodNode method : classNode.methods) { - if (isMainMethod(method)) { - rootClasses.add(classNode.name); - if (verbose) { - logger.debug("Identified root class (main method): {}", classNode.name.replace('/', '.')); - } - break; // No need to check other methods - } - } - } - - if (verbose) { - logger.debug("Total root classes identified: {}", rootClasses.size()); - } - - return rootClasses; - } - - /** - * Checks if a method is a main method. - * - * @param method The MethodNode to check. - * @return True if the method is a main method, false otherwise. - */ - private boolean isMainMethod(MethodNode method) { - return method.name.equals("main") && - method.desc.equals("([Ljava/lang/String;)V") && - (method.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)) == (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC); - } - - /** - * Identifies unused classes that are not root classes and are not imported by any other class. - * - * @param classes Map of class names to Clazz objects. - * @param rootClasses Set of root class names. - * @param verbose Flag to enable verbose logging. - * @return List of class names that are unused. - */ - private List identifyUnusedClasses(Map classes, Set rootClasses, boolean verbose) { - List unusedClasses = new ArrayList<>(); - - for (Clazz clazz : classes.values()) { - String className = clazz.node.name; - - // Skip root classes - if (rootClasses.contains(className)) { - if (verbose) { - logger.debug("Skipping root class: {}", className.replace('/', '.')); - } - continue; - } - - // Check if any other class imports this class - Map importingClasses = ByteCodeUtil.findallimports(classes, className.replace('/', '.')); - - // Remove self-imports (if any) - importingClasses.remove(className); - - if (importingClasses.isEmpty()) { - unusedClasses.add(className); - if (verbose) { - logger.debug("Identified unused class: {}", className.replace('/', '.')); - } - } - } - - if (verbose) { - logger.info("Total unused classes identified: {}", unusedClasses.size()); - } - - return unusedClasses; - } - - /** - * Removes unused classes from the class map. - * - * @param classes Map of class names to Clazz objects. - * @param unusedClasses List of class names to remove. - * @param verbose Flag to enable verbose logging. - */ - private void removeUnusedClasses(Map classes, List unusedClasses, boolean verbose) { - for (String className : unusedClasses) { - classes.remove(className); - logger.debug("Removed unused class: {}", className.replace('/', '.')); - } - logger.info("Removed {} unused classes.", unusedClasses.size()); - } -} diff --git a/core/src/main/java/me/nov/threadtear/io/JarIO.java b/core/src/main/java/me/nov/threadtear/io/JarIO.java index 45a14f9..6955588 100644 --- a/core/src/main/java/me/nov/threadtear/io/JarIO.java +++ b/core/src/main/java/me/nov/threadtear/io/JarIO.java @@ -8,9 +8,15 @@ import me.nov.threadtear.logging.LogWrapper; import org.apache.commons.io.IOUtils; +import org.benf.cfr.reader.bytecode.analysis.opgraph.op4rewriters.IllegalGenericRewriter; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; import me.nov.threadtear.execution.Clazz; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; +import software.coley.cafedude.transform.IllegalStrippingTransformer; public final class JarIO { private JarIO() { @@ -25,15 +31,35 @@ public static ArrayList loadClasses(File jarFile) throws IOException { return classes; } + public static byte[] transformClazz(byte[] bytes) throws software.coley.cafedude.InvalidClassException { + + // Use Cafedude to strip attributes + ClassFileReader reader = new ClassFileReader(); + ClassFile classFile = reader.read(bytes); + // Modifies the 'cf' instance + new IllegalStrippingTransformer(classFile).transform(); + byte[] strippedBytecode = new ClassFileWriter().write(classFile); + + // Return a new Clazz object + return strippedBytecode; + } + private static ArrayList readEntry(JarFile jar, JarEntry en, ArrayList classes) { String name = en.getName(); try (InputStream jis = jar.getInputStream(en)) { - byte[] bytes = IOUtils.toByteArray(jis); + byte[] bytes = (IOUtils.toByteArray(jis)); if (isClassFile(bytes)) { try { - final ClassNode cn = Conversion.toNode(bytes); + final ClassNode cn = Conversion.toNode(transformClazz(bytes)); + if (cn != null && (cn.superName != null || (cn.name != null && cn.name.equals("java/lang/Object")))) { - classes.add(new Clazz(cn, en, jar)); + // transform using cafedood + try{ + classes.add(new Clazz(cn, en, jar)); + }catch (Exception e){ + LogWrapper.logger.error("Failed to transform class {}", e, name); + } + } } catch (Exception e) { LogWrapper.logger.error("Failed to load file {}", e, name); diff --git a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java index 5fb6e9b..bc1af1f 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java @@ -216,15 +216,6 @@ private void loadFile(String type) { switch (type) { case "jar": this.classes = JarIO.loadClasses(inputFile); - // Transform each class before execution - for (int i = 0; i < classes.size(); i++) { - try { - LogWrapper.logger.info("CAFEDOOD stripping class: {}", classes.get(i).node.name); - classes.set(i, transformClazz(classes.get(i))); - } catch (IOException | software.coley.cafedude.InvalidClassException e) { - LogWrapper.logger.error("Failed to transform class: {}", classes.get(i).node.name, e); - } - } if (classes.stream().anyMatch(c -> c.oldEntry.getCertificates() != null)) { JOptionPane.showMessageDialog(this, "Warning: File is signed and may not load correctly if already " + @@ -307,23 +298,7 @@ public void addToTree(ClassTreeNode current, Clazz c, String[] packages, int pck addToTree(newChild, c, packages, ++pckg); } - public static Clazz transformClazz(Clazz originalClazz) throws IOException, InvalidClassException, software.coley.cafedude.InvalidClassException { - // Read the original bytecode - byte[] originalBytecode = originalClazz.streamOriginal().readAllBytes(); - - // Use Cafedude to strip attributes - ClassFileReader reader = new ClassFileReader(); - ClassFile classFile = reader.read(originalBytecode); - byte[] strippedBytecode = new ClassFileWriter().write(classFile); - // Create a new ClassNode from the stripped bytecode - ClassReader classReader = new ClassReader(strippedBytecode); - ClassNode newNode = new ClassNode(); - classReader.accept(newNode, 0); - - // Return a new Clazz object - return new Clazz(newNode, originalClazz.oldEntry, originalClazz.inputFile); - } } From 5f57c8ecf0289168f5c2379101158ef94ab0ad26 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Mon, 9 Dec 2024 19:37:36 +0800 Subject: [PATCH 12/13] Modified InlineUnchangedFields - Cancel operation if it fails to allow the user to execute other executions - Use reflection to evaluate values at runtime with clinit (DANGEROUS AND NEEDS SECURITY MANAGER) - Merge multiple clinit methods if present and handle them together --- .../cleanup/InlineUnchangedFields.java | 309 +++++++++++++++--- .../nov/threadtear/util/asm/Instructions.java | 82 +++++ .../swing/dialog/ExecutionSelection.java | 1 - 3 files changed, 349 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java index 46db592..353f930 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java @@ -1,69 +1,294 @@ package me.nov.threadtear.execution.cleanup; +import me.nov.threadtear.execution.*; +import me.nov.threadtear.util.asm.*; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; + +import java.lang.reflect.Field; +import java.security.SecureClassLoader; import java.util.*; -import java.util.stream.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; +import static org.objectweb.asm.Opcodes.*; -import me.nov.threadtear.execution.*; -import me.nov.threadtear.util.asm.*; public class InlineUnchangedFields extends Execution { + private Map classes; + private List fieldAssignments; // All field assignment instructions + private int inlinedCount; + + // Map: className -> (fieldName+desc -> runtime value) + private Map> constantFieldValues = new HashMap<>(); + public InlineUnchangedFields() { - super(ExecutionCategory.CLEANING, "Inline unchanged fields", - "Inline fields that are not set anywhere in the code.
Can be useful for " + - "ZKM deobfuscation.", ExecutionTag.RUNNABLE, ExecutionTag.BETTER_DECOMPILE, - ExecutionTag.BETTER_DEOBFUSCATE); + super( + ExecutionCategory.CLEANING, + "Inline unchanged fields (Reflection Runtime) with merged ", + "Loads classes, merges multiple , runs , and uses reflection to determine static field values.
" + + "Then inlines these values in all references.", + ExecutionTag.RUNNABLE, + ExecutionTag.BETTER_DECOMPILE, + ExecutionTag.BETTER_DEOBFUSCATE + ); } - public int inlines; - private Map classes; - private List fieldPuts; - @Override public boolean execute(Map classes, boolean verbose) { this.classes = classes; - this.inlines = 0; - // TODO static initializer should be excluded, we can - // still calculate the field - // value - this.fieldPuts = classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .map(m -> m.instructions.spliterator()).flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == PUTFIELD || ain.getOpcode() == PUTSTATIC).map(ain -> (FieldInsnNode) ain) - .collect(Collectors.toList()); - - classes.values().stream().map(c -> c.node).filter(c -> !Access.isEnum(c.access)) - .forEach(c -> c.fields.stream().filter(f -> isNotReferenced(c, f)).forEach(f -> inline(c, f))); - logger.info("Inlined {} method references!", inlines); - return inlines > 0; + this.inlinedCount = 0; + + // Collect all field assignment instructions + this.fieldAssignments = classes.values().stream() + .map(c -> c.node.methods) + .flatMap(List::stream) + .map(m -> m.instructions.spliterator()) + .flatMap(insns -> StreamSupport.stream(insns, false)) + .filter(ain -> ain.getOpcode() == PUTFIELD || ain.getOpcode() == PUTSTATIC) + .map(ain -> (FieldInsnNode) ain) + .collect(Collectors.toList()); + + // Merge multiple methods if present + for (Clazz clazz : this.classes.values()) { + mergeClinitMethods(clazz.node); + } + + // Load all classes using a custom ClassLoader + ReflectiveClassLoader loader = new ReflectiveClassLoader(getClass().getClassLoader()); + Map> loadedClasses = defineAllClasses(loader, this.classes); + + // Determine which fields can be considered constant + for (Clazz clazz : this.classes.values()) { + ClassNode cn = clazz.node; + if (!Access.isEnum(cn.access)) { + analyzeClinitForConstants(cn, loadedClasses); + } + } + + // Inline all references to these constant fields + try{ + inlineAllConstantFields(); + } catch (Exception e) { + logger.error("Error inlining constant fields: {}", e.getMessage()); + return false; + } + + + logger.info("Inlined {} field references!", inlinedCount); + return inlinedCount > 0; + } + + /** + * Merge multiple methods into a single one if found. + * Java normally allows only one , but in manipulated bytecode, + * there might be multiple. We combine them for easier analysis. + */ + private void mergeClinitMethods(ClassNode cn) { + List clinitMethods = new ArrayList<>(); + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + clinitMethods.add(m); + } + } + + // If there's only one or none, nothing to do + if (clinitMethods.size() <= 1) { + return; + } + + // We have multiple methods. Let's merge them into the first one. + MethodNode primary = clinitMethods.get(0); + + for (int i = 1; i < clinitMethods.size(); i++) { + MethodNode extra = clinitMethods.get(i); + + // We'll merge instructions from 'extra' into 'primary' + // Just before primary's RETURN instruction (or at the end if no explicit return) + AbstractInsnNode insertionPoint = findMethodReturnOrEnd(primary); + + // Clone labels + Map labelMap = Instructions.cloneLabels(extra.instructions); + + // Copy instructions + InsnList extraCopy = new InsnList(); + for (AbstractInsnNode ain : extra.instructions) { + if (ain.getType() == AbstractInsnNode.LABEL || + ain.getType() == AbstractInsnNode.FRAME || + ain.getType() == AbstractInsnNode.LINE || + ain.getOpcode() != RETURN) { + extraCopy.add(ain.clone(labelMap)); + } + } + + // Insert the copied instructions before the return + primary.instructions.insertBefore(insertionPoint, extraCopy); + + // Merge try-catch blocks + if (extra.tryCatchBlocks != null) { + for (TryCatchBlockNode tcb : extra.tryCatchBlocks) { + TryCatchBlockNode copyTcb = new TryCatchBlockNode( + labelMap.get(tcb.start), + labelMap.get(tcb.end), + labelMap.get(tcb.handler), + tcb.type + ); + primary.tryCatchBlocks.add(copyTcb); + } + } + + // Merge local variables + if (extra.localVariables != null) { + if (primary.localVariables == null) { + primary.localVariables = new ArrayList<>(); + } + for (LocalVariableNode lv : extra.localVariables) { + LocalVariableNode copyLv = new LocalVariableNode( + lv.name, + lv.desc, + lv.signature, + labelMap.get(lv.start), + labelMap.get(lv.end), + lv.index + ); + primary.localVariables.add(copyLv); + } + } + } + + // Remove all extra methods + cn.methods.removeIf(m -> m.name.equals("") && m != primary); } - private boolean isNotReferenced(ClassNode cn, FieldNode f) { - return fieldPuts.stream().noneMatch(fin -> isReferenceTo(cn, fin, f)); + /** + * Find a suitable point in the primary method to insert extra instructions. + * We prefer to insert before the RETURN instruction if found, else insert at the end. + */ + private AbstractInsnNode findMethodReturnOrEnd(MethodNode m) { + for (AbstractInsnNode ain = m.instructions.getLast(); ain != null; ain = ain.getPrevious()) { + int op = ain.getOpcode(); + if (op >= IRETURN && op <= RETURN) { + return ain; // found a return instruction + } + } + // No return found, insert at the very end + return m.instructions.getLast(); } - public void inline(ClassNode cn, FieldNode fn) { - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.forEach(m -> { - for (AbstractInsnNode ain : m.instructions) { - if (ain.getType() == AbstractInsnNode.FIELD_INSN) { - FieldInsnNode fin = (FieldInsnNode) ain; - if (isGetReferenceTo(cn, fin, fn)) { - m.instructions.set(ain, Instructions.makeNullPush(Type.getType(fn.desc))); - inlines++; - logger.debug("Inlined field {} in method {} in class {}", fn.name, m.name, c.name); - } + private Map> defineAllClasses(ReflectiveClassLoader loader, Map classes) { + Map> result = new HashMap<>(); + for (Map.Entry e : classes.entrySet()) { + String className = e.getKey(); + ClassNode cn = e.getValue().node; + byte[] classBytes = Instructions.toByteArray(cn); + Class definedClass = loader.define(className.replace('/', '.'), classBytes); + result.put(className, definedClass); + } + return result; + } + + private void analyzeClinitForConstants(ClassNode cn, Map> loadedClasses) { + MethodNode clinit = null; + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + clinit = m; + break; + } + } + + if (clinit == null) return; + + Class runtimeClass = loadedClasses.get(cn.name); + if (runtimeClass == null) { + return; // Could not load class, skip + } + + // Check each static field to see if it's never assigned outside + for (FieldNode fn : cn.fields) { + if ((fn.access & ACC_STATIC) == 0) { + continue; // only handle static fields + } + + boolean assignedOutsideClinit = fieldAssignments.stream() + .anyMatch(fin -> isFieldReferenceTo(cn, fn, fin) && !isInClinit(cn, fin)); + if (!assignedOutsideClinit) { + try { + Field f = runtimeClass.getDeclaredField(fn.name); + f.setAccessible(true); + Object value = f.get(null); + + // Store discovered value + constantFieldValues + .computeIfAbsent(cn.name, k -> new HashMap<>()) + .put(fn.name + fn.desc, value); + } catch (NoSuchFieldException | IllegalAccessException ex) { + // Can't access field, skip } } - })); + } } - private boolean isGetReferenceTo(ClassNode cn, FieldInsnNode fin, FieldNode fn) { - return (fin.getOpcode() == GETSTATIC || fin.getOpcode() == GETFIELD) && isReferenceTo(cn, fin, fn); + private void inlineAllConstantFields() { + for (Clazz c : classes.values()) { + ClassNode cn = c.node; + Map fieldMap = constantFieldValues.getOrDefault(cn.name, Collections.emptyMap()); + + if (!fieldMap.isEmpty()) { + for (MethodNode m : cn.methods) { + List toReplace = new ArrayList<>(); + for (AbstractInsnNode ain : m.instructions) { + if (ain.getType() == AbstractInsnNode.FIELD_INSN) { + FieldInsnNode fin = (FieldInsnNode) ain; + String key = fin.name + fin.desc; + if (fieldMap.containsKey(key) && (fin.getOpcode() == GETSTATIC)) { + toReplace.add(ain); + } + } + } + + for (AbstractInsnNode insn : toReplace) { + FieldInsnNode fin = (FieldInsnNode) insn; + Object constantValue = fieldMap.get(fin.name + fin.desc); + Type fieldType = Type.getType(fin.desc); + + AbstractInsnNode replacement = Instructions.makeConstantPush(constantValue, fieldType); + m.instructions.set(insn, replacement); + inlinedCount++; + logger.debug("Inlined field {}.{} in method {} of class {} with value {}", + fin.owner, fin.name, m.name, cn.name, constantValue); + } + } + } + } } - private boolean isReferenceTo(ClassNode cn, FieldInsnNode fin, FieldNode fn) { + private boolean isFieldReferenceTo(ClassNode cn, FieldNode fn, FieldInsnNode fin) { return fin.owner.equals(cn.name) && fin.name.equals(fn.name) && fin.desc.equals(fn.desc); } + + private boolean isInClinit(ClassNode cn, FieldInsnNode fin) { + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + for (AbstractInsnNode ain : m.instructions) { + if (ain == fin) { + return true; + } + } + } + } + return false; + } + + public static class ReflectiveClassLoader extends SecureClassLoader { + public ReflectiveClassLoader(ClassLoader parent) { + super(parent); + } + + public Class define(String name, byte[] b) { + Class c = defineClass(name, b, 0, b.length); + resolveClass(c); + return c; + } + } } diff --git a/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java b/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java index 9028c34..cea1275 100644 --- a/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java +++ b/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java @@ -333,4 +333,86 @@ public static InsnList singleton(AbstractInsnNode ain) { list.add(ain); return list; } + + /** + * Converts a ClassNode back into a byte array. + * This uses ASM's ClassWriter and the accept() method on ClassNode. + */ + public static byte[] toByteArray(ClassNode cn) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cn.accept(cw); + return cw.toByteArray(); + } + + /** + * Creates instructions to push a given constant value onto the stack, based on its type. + * If value is null, or not compatible, it falls back to makeNullPush(). + */ + public static AbstractInsnNode makeConstantPush(Object value, Type type) { + if (value == null) { + return makeNullPush(type); + } + + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + // Expect value to be a number + int intVal = ((Number) value).intValue(); + return pushInt(intVal); + + case Type.LONG: + long longVal = ((Number) value).longValue(); + if (longVal == 0L) return new InsnNode(LCONST_0); + if (longVal == 1L) return new InsnNode(LCONST_1); + return new LdcInsnNode(longVal); + + case Type.FLOAT: + float floatVal = ((Number) value).floatValue(); + if (floatVal == 0.0f) return new InsnNode(FCONST_0); + if (floatVal == 1.0f) return new InsnNode(FCONST_1); + if (floatVal == 2.0f) return new InsnNode(FCONST_2); + return new LdcInsnNode(floatVal); + + case Type.DOUBLE: + double doubleVal = ((Number) value).doubleValue(); + if (doubleVal == 0.0d) return new InsnNode(DCONST_0); + if (doubleVal == 1.0d) return new InsnNode(DCONST_1); + return new LdcInsnNode(doubleVal); + + case Type.OBJECT: + case Type.ARRAY: + // For objects (including String) just LDC them + return new LdcInsnNode(value); + + default: + // If we can't handle this type, fallback to null/zero + return makeNullPush(type); + } + } + + /** + * Helper method to push an int value efficiently. + */ + private static AbstractInsnNode pushInt(int val) { + switch (val) { + case -1: return new InsnNode(ICONST_M1); + case 0: return new InsnNode(ICONST_0); + case 1: return new InsnNode(ICONST_1); + case 2: return new InsnNode(ICONST_2); + case 3: return new InsnNode(ICONST_3); + case 4: return new InsnNode(ICONST_4); + case 5: return new InsnNode(ICONST_5); + default: + // For larger integers, use BIPUSH/SIPUSH or LDC + if (val >= Byte.MIN_VALUE && val <= Byte.MAX_VALUE) { + return new IntInsnNode(BIPUSH, val); + } else if (val >= Short.MIN_VALUE && val <= Short.MAX_VALUE) { + return new IntInsnNode(SIPUSH, val); + } else { + return new LdcInsnNode(val); + } + } + } } diff --git a/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java b/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java index 528141c..13d5990 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java +++ b/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java @@ -13,7 +13,6 @@ import com.github.weisj.darklaf.components.border.DarkBorders; import me.nov.threadtear.execution.Execution; import me.nov.threadtear.execution.ExecutionLink; -import me.nov.threadtear.execution.allatori.*; import me.nov.threadtear.execution.analysis.*; import me.nov.threadtear.execution.cleanup.*; import me.nov.threadtear.execution.cleanup.remove.RemoveAttributes; From 0fecbb4716c16419daf491d24cd0672f8c7e02e3 Mon Sep 17 00:00:00 2001 From: neilhuang007 Date: Sat, 21 Dec 2024 06:03:04 +0800 Subject: [PATCH 13/13] Added funtionalities to serveral inline functions --- .../execution/cleanup/InlineArithmetics.java | 2 +- .../execution/cleanup/InlineMethods.java | 451 +++++++++++++----- .../cleanup/InlineUnchangedFields.java | 100 +++- 3 files changed, 409 insertions(+), 144 deletions(-) diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java index 9d7430d..f58ad3e 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java @@ -11,7 +11,7 @@ public class InlineArithmetics extends Execution { public InlineArithmetics() { super(ExecutionCategory.CLEANING, "Inline arithmetics", - "Inline simple arithmetics that only return or throw.
Can be useful for deobfuscating arithmetic obfuscation.", + "Inline arithmetic calculations.
Can be useful for deobfuscating arithmetic obfuscation used in flow.", ExecutionTag.SHRINK, ExecutionTag.RUNNABLE); } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java index eb0dd90..6f01bbe 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java @@ -3,170 +3,380 @@ import java.util.*; import java.util.stream.StreamSupport; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; import me.nov.threadtear.execution.*; import me.nov.threadtear.util.asm.*; +import static org.objectweb.asm.Opcodes.*; + +/** + * This execution attempts to inline trivial methods (e.g., methods that only return a constant or throw an exception) into their call sites. + * It identifies such methods, then replaces their invocation instructions with the method's instructions. + * + * Key Improvements: + * - Properly handles 'this' and arguments for both static and non-static methods. + * - Remaps local variables correctly. + * - Handles return instructions so that return values remain on stack (if needed). + * - Skips constructors and class initializers (, ). + * - Skips methods that reference fields or contain complex instructions. + * - Enforces a maximum method size to ensure only trivial methods are inlined. + */ public class InlineMethods extends Execution { public InlineMethods() { - super(ExecutionCategory.CLEANING, "Inline static methods without invocation", - "Inline static methods that only return or throw.
Can be" + - " useful for deobfuscating try catch block obfuscation.", ExecutionTag.SHRINK, - ExecutionTag.RUNNABLE); + super( + ExecutionCategory.CLEANING, + "Inline trivial methods without invocation", + "Inline trivial methods (only return or throw) to simplify code.
" + + "Skips /, large methods, or those referencing fields.", + ExecutionTag.SHRINK, + ExecutionTag.RUNNABLE + ); } - public int inlines; + private int inlines; + + // Maximum allowed instructions for a method to be considered trivial enough to inline + private static final int MAX_METHOD_SIZE = 32; @Override public boolean execute(Map classes, boolean verbose) { - HashMap map = new HashMap<>(); - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.stream().filter(this::isUnnecessary) - .forEach(m -> map.put(c.name + "." + m.name + m.desc, m))); - logger.info("{} unnecessary methods found that could be inlined", map.size()); + // Map to hold methods eligible for inlining: key = "owner.name.desc", value = MethodNode + HashMap eligibleMethods = new HashMap<>(); + + // Identify candidate methods for inlining + classes.values().stream() + .map(c -> c.node) + .forEach(classNode -> classNode.methods.stream() + .filter(this::isEligibleForInlining) + .forEach(method -> eligibleMethods.put(classNode.name + "." + method.name + method.desc, method)) + ); + + logger.info("{} trivial methods found that could be inlined", eligibleMethods.size()); inlines = 0; - classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .forEach(m -> m.instructions.forEach(ain -> { - if (ain.getOpcode() == INVOKESTATIC || ain.getOpcode() == INVOKEVIRTUAL || ain.getOpcode() == INVOKESPECIAL) { - MethodInsnNode min = (MethodInsnNode) ain; - String key = min.owner + "." + min.name + min.desc; - if (map.containsKey(key)) { - inlineMethod(m, min, map.get(key)); - m.maxStack = Math.max(map.get(key).maxStack, m.maxStack); - m.maxLocals = Math.max(map.get(key).maxLocals, m.maxLocals); + + // Inline all calls to these eligible methods + classes.values().stream() + .map(c -> c.node.methods) + .flatMap(List::stream) + .forEach(callerMethod -> { + List invokeInstructions = new ArrayList<>(); + + // Collect invoke instructions that target eligible methods + for (AbstractInsnNode ain : callerMethod.instructions.toArray()) { + int opcode = ain.getOpcode(); + if (opcode == INVOKESTATIC || opcode == INVOKEVIRTUAL || opcode == INVOKESPECIAL) { + // Ensure the instruction is a MethodInsnNode + if (!(ain instanceof MethodInsnNode)) { + continue; // Skip if not a MethodInsnNode + } + MethodInsnNode min = (MethodInsnNode) ain; + String key = min.owner + "." + min.name + min.desc; + if (eligibleMethods.containsKey(key)) { + invokeInstructions.add(ain); + } + } + } + + // Inline each collected invoke instruction + for (AbstractInsnNode invokeInsn : invokeInstructions) { + MethodInsnNode min = (MethodInsnNode) invokeInsn; + MethodNode calleeMethod = eligibleMethods.get(min.owner + "." + min.name + min.desc); + if (calleeMethod != null) { + inlineMethod(callerMethod, min, calleeMethod); inlines++; } } - })); + }); + + // Remove inlined methods from their respective classes + for (Map.Entry entry : eligibleMethods.entrySet()) { + String fullMethodName = entry.getKey(); // "owner.name.desc" + String className = fullMethodName.substring(0, fullMethodName.lastIndexOf('.')); + Clazz clazz = classes.get(className); + if (clazz != null) { + clazz.node.methods.remove(entry.getValue()); + } + } - map.forEach((key, method) -> classes.get(key.substring(0, key.lastIndexOf('.'))).node.methods.remove(method)); logger.info("Inlined {} method references!", inlines); - return true; + return inlines > 0; } - private void inlineMethod(MethodNode m, MethodInsnNode min, MethodNode method) { - InsnList copy = Instructions.copy(method.instructions); - StreamSupport.stream(copy.spliterator(), false) - .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) - .forEach(copy::remove); - removeReturn(copy, method); + /** + * Determines if a method is eligible for inlining based on several criteria: + * - Not a constructor or class initializer (, ). + * - Does not contain complex instructions like method calls, field accesses, jumps, or type instructions. + * - Has a number of instructions below the specified maximum threshold. + * - Ends with a return or throw instruction. + * + * @param method The method node to evaluate. + * @return True if the method is eligible for inlining; otherwise, false. + */ + private boolean isEligibleForInlining(MethodNode method) { + // Skip constructors and class initializers + if (method.name.equals("") || method.name.equals("")) { + return false; + } - InsnList fakeVarList = createFakeVarList(method); - copy.insert(fakeVarList); + // Check method size + if (method.instructions.size() > MAX_METHOD_SIZE) { + return false; + } - if (min.getOpcode() != INVOKESTATIC) { - // Handle 'this' reference for non-static methods - copy.insert(new VarInsnNode(ALOAD, 0)); // Load 'this' + // Ensure no complex instructions (method calls, field accesses, type instructions, jumps) + if (containsComplexInstructions(method)) { + return false; } - StreamSupport.stream(copy.spliterator(), false).filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) - .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); + // Ensure method ends with a return or throw + if (!endsWithReturnOrThrow(method)) { + return false; + } - m.instructions.insert(min, copy); - m.instructions.remove(min); + return true; } - private InsnList createFakeVarList(MethodNode m) { - InsnList fakeVarList = new InsnList(); + /** + * Checks if a method contains complex instructions that would make inlining unsafe or non-trivial. + * Complex instructions include method calls, field accesses, dynamic invokes, type instructions, and jumps. + * + * @param method The method node to inspect. + * @return True if the method contains complex instructions; otherwise, false. + */ + private boolean containsComplexInstructions(MethodNode method) { + for (AbstractInsnNode ain : method.instructions.toArray()) { + switch (ain.getType()) { + case AbstractInsnNode.METHOD_INSN: + case AbstractInsnNode.FIELD_INSN: + case AbstractInsnNode.INVOKE_DYNAMIC_INSN: + case AbstractInsnNode.TYPE_INSN: + case AbstractInsnNode.JUMP_INSN: + return true; + default: + break; + } + } + return false; + } - LinkedHashMap varTypes = getVarsAndTypesForDesc(m.desc.substring(1, m.desc.indexOf(')'))); - for (int var : varTypes.keySet()) { - fakeVarList.insert(new VarInsnNode(varTypes.get(var), var)); // make sure its reversed + /** + * Checks if a method ends with a return or throw instruction. + * Skips any trailing line or frame nodes. + * + * @param method The method node to inspect. + * @return True if the method ends with a return or throw; otherwise, false. + */ + private boolean endsWithReturnOrThrow(MethodNode method) { + AbstractInsnNode last = method.instructions.getLast(); + while (last != null && + (last.getType() == AbstractInsnNode.LINE || last.getType() == AbstractInsnNode.FRAME)) { + last = last.getPrevious(); } - // pop object here for non static invoke: fakeVarList - // .add(new InsnNode(POP)); - return fakeVarList; + if (last == null) return false; + int opcode = last.getOpcode(); + return opcode == RETURN || opcode == IRETURN || opcode == LRETURN || + opcode == FRETURN || opcode == DRETURN || opcode == ARETURN || opcode == ATHROW; } - public static LinkedHashMap getVarsAndTypesForDesc(String rawType) { - LinkedHashMap map = new LinkedHashMap<>(); - int var = 0; // would be 1 on non-static methods - - boolean object = false; - boolean array = false; - for (char c : rawType.toCharArray()) { - if (!object) { - if (array && c != 'L') { - map.put(var, ASTORE); // array type is astore - var++; - array = false; - continue; - } - switch (c) { - case 'L': - array = false; - map.put(var, ASTORE); - object = true; - var++; - break; - case 'I': - map.put(var, ISTORE); - var++; - break; - case 'D': - map.put(var, DSTORE); - var += 2; - break; - case 'F': - map.put(var, FSTORE); - var++; - break; - case 'J': - map.put(var, LSTORE); - var += 2; - break; - case '[': - array = true; - break; - } - } else if (c == ';') { - object = false; - } + /** + * Inlines a callee method into the caller method at the location of the invoke instruction. + * Handles argument popping, variable remapping, and return instruction removal. + * + * @param caller The caller method where inlining occurs. + * @param invoke The invoke instruction to replace with inlined code. + * @param callee The callee method being inlined. + */ + private void inlineMethod(MethodNode caller, MethodInsnNode invoke, MethodNode callee) { + // Create a copy of the callee's instructions + InsnList calleeInstructions = Instructions.copy(callee.instructions); + + // Remove line and frame nodes for simplicity + StreamSupport.stream(calleeInstructions.spliterator(), false) + .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) + .forEach(calleeInstructions::remove); + + // Remove or adjust return instructions in the copied code + removeAndHandleReturns(calleeInstructions, callee); + + // Determine method signature details + Type methodType = Type.getMethodType(callee.desc); + Type[] argTypes = methodType.getArgumentTypes(); + boolean isStatic = (callee.access & ACC_STATIC) != 0; + + // Calculate the starting index for new local variables in the caller + int newLocalBase = caller.maxLocals; + + // Calculate total size needed for parameters + int paramSize = 0; + if (!isStatic) { + paramSize += 1; // 'this' reference + } + for (Type argType : argTypes) { + paramSize += (argType.getSize() == 2) ? 2 : 1; + } + + // Update caller's maxLocals and maxStack + caller.maxLocals += paramSize + 4; // Additional buffer for safety + caller.maxStack = Math.max(callee.maxStack, caller.maxStack); + + // Generate instructions to pop arguments from the stack into new local variables + InsnList argumentPoppers = new InsnList(); + + // Pop arguments in reverse order and store them into new locals + for (int i = argTypes.length - 1; i >= 0; i--) { + Type argType = argTypes[i]; + newLocalBase = storeArgument(argumentPoppers, newLocalBase, argType); + } + + // If the method is not static, pop the 'this' reference + if (!isStatic) { + Type thisType = Type.getObjectType(invoke.owner); + newLocalBase = storeArgument(argumentPoppers, newLocalBase, thisType); + } + + // Insert argument pop instructions at the beginning of the callee instructions + calleeInstructions.insert(argumentPoppers); + + // Remap local variable indices in the callee's instructions + remapLocalVariables(calleeInstructions, callee, newLocalBase - paramSize, isStatic); + + // Insert the inlined instructions into the caller method + caller.instructions.insert(invoke, calleeInstructions); + // Remove the original invoke instruction + caller.instructions.remove(invoke); + + // Optional Sanity Check: Ensure no return instructions remain + if (!postInlineSanityCheck(calleeInstructions)) { + logger.warning("Post-inline sanity check failed for inlined method {}.{}", callee.name, callee.desc); } - return map; } - private void removeReturn(InsnList copy, MethodNode m) { - int i = copy.size() - 1; - while (i >= 0) { - AbstractInsnNode ain = copy.get(i); + /** + * Pops an argument from the stack and stores it into a new local variable. + * + * @param instructions The instruction list to append store instructions. + * @param localIndex The current local variable index. + * @param type The type of the argument to store. + * @return The next available local variable index. + */ + private int storeArgument(InsnList instructions, int localIndex, Type type) { + int storeOpcode; + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.CHAR: + case Type.SHORT: + case Type.INT: + storeOpcode = ISTORE; + break; + case Type.LONG: + storeOpcode = LSTORE; + break; + case Type.FLOAT: + storeOpcode = FSTORE; + break; + case Type.DOUBLE: + storeOpcode = DSTORE; + break; + case Type.ARRAY: + case Type.OBJECT: + default: + storeOpcode = ASTORE; + break; + } - if (ain.getOpcode() == ATHROW) { - // keep athrow, as it would still be in code - return; + instructions.add(new VarInsnNode(storeOpcode, localIndex)); + return localIndex + ((storeOpcode == LSTORE || storeOpcode == DSTORE) ? 2 : 1); + } + + /** + * Remaps local variable indices in the inlined callee instructions to avoid conflicts with caller's locals. + * + * @param instructions The instruction list containing the inlined code. + * @param callee The callee method being inlined. + * @param base The base index to offset local variables. + * @param isStatic Whether the callee method is static. + */ + private void remapLocalVariables(InsnList instructions, MethodNode callee, int base, boolean isStatic) { + for (AbstractInsnNode ain : instructions.toArray()) { + if (ain instanceof VarInsnNode) { + VarInsnNode vin = (VarInsnNode) ain; + vin.var += base; } - copy.remove(ain); - switch (ain.getOpcode()) { - case RETURN: - case ARETURN: - case DRETURN: - case FRETURN: - case IRETURN: - case LRETURN: - return; - default: - // Continue removing until a proper return statement is found - break; + } + } + + /** + * Removes return instructions from the inlined method code or adjusts them so that return values remain on stack. + * Strategy: + * - If IRETURN, LRETURN, etc.: remove the return instruction, leaving the value on the stack. + * - If RETURN (void): remove it, leaving nothing on the stack. + * - If ATHROW: leave it intact because the method is supposed to throw. + * + * @param instructions The instruction list to modify. + * @param callee The callee method being inlined. + */ + private void removeAndHandleReturns(InsnList instructions, MethodNode callee) { + ListIterator iterator = instructions.iterator(); + while (iterator.hasNext()) { + AbstractInsnNode ain = iterator.next(); + int opcode = ain.getOpcode(); + if (opcode == IRETURN || opcode == FRETURN || opcode == ARETURN || + opcode == DRETURN || opcode == LRETURN) { + // Remove the return instruction, leaving the return value on the stack + iterator.remove(); + } else if (opcode == RETURN) { + // Remove the void return instruction + iterator.remove(); + } else if (opcode == ATHROW) { + // Leave ATHROW instructions intact } - i--; } - // then skip this method and log the exception instead of throwing it - logger.error("No return found to remove, invalid method?, method name: {}", m); + } + /** + * Performs a sanity check after inlining to ensure no invalid instructions remain. + * Specifically, it checks for leftover return instructions which should have been removed. + * + * @param instructions The instruction list to check. + * @return True if the sanity check passes; otherwise, false. + */ + private boolean postInlineSanityCheck(InsnList instructions) { + for (AbstractInsnNode ain : instructions.toArray()) { + int opcode = ain.getOpcode(); + // Check for any leftover return instructions + if (opcode == RETURN || opcode == IRETURN || opcode == LRETURN || + opcode == FRETURN || opcode == DRETURN || opcode == ARETURN) { + return false; // Sanity check failed + } + } + return true; // Sanity check passed } + /** + * Determines if a method is unnecessary (i.e., can be inlined). + * Currently checks if it doesn't contain complex instructions like method calls, field accesses, etc., + * and ensures it ends with a return or throw. + * + * @param m The method node to evaluate. + * @return True if the method is unnecessary and eligible for inlining; otherwise, false. + */ public boolean isUnnecessary(MethodNode m) { -// if (m.instructions.size() > 32) { -// // do not inline huge methods -// return false; -// } else if (m.instructions.size() < 2) { -// // abstract methods or similar -// return false; -// } - return StreamSupport.stream(m.instructions.spliterator(), false).noneMatch(this::isInvocationOrJump); + // This method is deprecated in favor of isEligibleForInlining + // Keeping it for backward compatibility; it simply delegates to isEligibleForInlining + return isEligibleForInlining(m); } + /** + * Determines if an instruction is an invocation or a jump. + * Used to identify methods that cannot be inlined. + * + * @param ain The instruction node to check. + * @return True if the instruction is an invocation or a jump; otherwise, false. + */ public boolean isInvocationOrJump(AbstractInsnNode ain) { switch (ain.getType()) { case AbstractInsnNode.METHOD_INSN: @@ -175,7 +385,8 @@ public boolean isInvocationOrJump(AbstractInsnNode ain) { case AbstractInsnNode.TYPE_INSN: case AbstractInsnNode.JUMP_INSN: return true; + default: + return false; } - return false; } } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java index 353f930..7c6a9e6 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java @@ -13,7 +13,18 @@ import static org.objectweb.asm.Opcodes.*; - +/** + * This execution inlines static fields that: + * 1. Are assigned only once in . + * 2. Are never reassigned outside . + * 3. Have a known value after finishes running. + * + * Approach: + * - Load all classes into a custom ClassLoader. + * - This triggers for each class. + * - After that, reflect on the class to get the runtime values of the static fields. + * - If a field is never reassigned after , we treat its runtime value as a constant. + */ public class InlineUnchangedFields extends Execution { private Map classes; @@ -55,8 +66,13 @@ public boolean execute(Map classes, boolean verbose) { mergeClinitMethods(clazz.node); } - // Load all classes using a custom ClassLoader - ReflectiveClassLoader loader = new ReflectiveClassLoader(getClass().getClassLoader()); + // Build the classes map + Map classesMap = buildClassesMap(this.classes); + + // Instantiate the custom class loader with the classes map + ReflectiveClassLoader loader = new ReflectiveClassLoader(getClass().getClassLoader(), classesMap); + + // Define all classes using the custom class loader Map> loadedClasses = defineAllClasses(loader, this.classes); // Determine which fields can be considered constant @@ -68,20 +84,14 @@ public boolean execute(Map classes, boolean verbose) { } // Inline all references to these constant fields - try{ - inlineAllConstantFields(); - } catch (Exception e) { - logger.error("Error inlining constant fields: {}", e.getMessage()); - return false; - } - + inlineAllConstantFields(); logger.info("Inlined {} field references!", inlinedCount); return inlinedCount > 0; } /** - * Merge multiple methods into a single one if found. + * Merges multiple methods into a single one if found. * Java normally allows only one , but in manipulated bytecode, * there might be multiple. We combine them for easier analysis. */ @@ -162,7 +172,7 @@ private void mergeClinitMethods(ClassNode cn) { } /** - * Find a suitable point in the primary method to insert extra instructions. + * Finds a suitable point in the primary method to insert extra instructions. * We prefer to insert before the RETURN instruction if found, else insert at the end. */ private AbstractInsnNode findMethodReturnOrEnd(MethodNode m) { @@ -176,18 +186,46 @@ private AbstractInsnNode findMethodReturnOrEnd(MethodNode m) { return m.instructions.getLast(); } - private Map> defineAllClasses(ReflectiveClassLoader loader, Map classes) { - Map> result = new HashMap<>(); + /** + * Builds a map of internal class names to their byte arrays. + * + * @param classes The map of class names to Clazz instances. + * @return A map of internal class names to byte arrays. + */ + private Map buildClassesMap(Map classes) { + Map classBytesMap = new HashMap<>(); for (Map.Entry e : classes.entrySet()) { - String className = e.getKey(); + String internalName = e.getKey(); // e.g., "com/example/MyClass" ClassNode cn = e.getValue().node; byte[] classBytes = Instructions.toByteArray(cn); - Class definedClass = loader.define(className.replace('/', '.'), classBytes); - result.put(className, definedClass); + classBytesMap.put(internalName, classBytes); + } + return classBytesMap; + } + + /** + * Defines all classes into the provided ClassLoader. + * Once defined, will have run, so we can reflect on their static fields. + */ + private Map> defineAllClasses(ReflectiveClassLoader loader, Map classes) { + Map> result = new HashMap<>(); + for (Map.Entry e : classes.entrySet()) { + String internalName = e.getKey(); // e.g., "com/example/MyClass" + String className = internalName.replace('/', '.'); // e.g., "com.example.MyClass" + try { + Class definedClass = loader.loadClass(className); + result.put(internalName, definedClass); + } catch (ClassNotFoundException ex) { + logger.error("Failed to load class: {}", className, ex); + } } return result; } + /** + * Analyze a single class to determine which static fields can be considered constant. + * If a static field is never reassigned outside , we read its value via reflection. + */ private void analyzeClinitForConstants(ClassNode cn, Map> loadedClasses) { MethodNode clinit = null; for (MethodNode m : cn.methods) { @@ -229,6 +267,9 @@ private void analyzeClinitForConstants(ClassNode cn, Map> loade } } + /** + * Inline references to all fields determined to be constant. + */ private void inlineAllConstantFields() { for (Clazz c : classes.values()) { ClassNode cn = c.node; @@ -241,7 +282,7 @@ private void inlineAllConstantFields() { if (ain.getType() == AbstractInsnNode.FIELD_INSN) { FieldInsnNode fin = (FieldInsnNode) ain; String key = fin.name + fin.desc; - if (fieldMap.containsKey(key) && (fin.getOpcode() == GETSTATIC)) { + if (fieldMap.containsKey(key) && fin.getOpcode() == GETSTATIC) { toReplace.add(ain); } } @@ -280,15 +321,28 @@ private boolean isInClinit(ClassNode cn, FieldInsnNode fin) { return false; } + /** + * A custom ClassLoader that allows us to define classes from byte arrays. + * It holds a map of class names to byte arrays to resolve dependencies. + * + * Note: This class trusts the input. Real usage should consider security. + */ public static class ReflectiveClassLoader extends SecureClassLoader { - public ReflectiveClassLoader(ClassLoader parent) { + private final Map classesMap; + + public ReflectiveClassLoader(ClassLoader parent, Map classesMap) { super(parent); + this.classesMap = classesMap; } - public Class define(String name, byte[] b) { - Class c = defineClass(name, b, 0, b.length); - resolveClass(c); - return c; + @Override + protected Class findClass(String name) throws ClassNotFoundException { + String internalName = name.replace('.', '/'); + byte[] classBytes = classesMap.get(internalName); + if (classBytes != null) { + return defineClass(name, classBytes, 0, classBytes.length); + } + return super.findClass(name); // Delegate to parent if not found in map } } }