├── settings.gradle ├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .github └── workflows │ ├── gradle.yml │ └── release.yml ├── README.md ├── LICENSE ├── src └── main │ └── java │ └── me │ └── cortex │ └── jarscanner │ ├── Constants.java │ ├── Results.java │ ├── Gui.java │ ├── Main.java │ └── Detector.java ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jarscanner' 2 | 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCRcortex/nekodetector/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /build/ 4 | /build/classes/java/main/ 5 | /bin 6 | /.idea/ 7 | 8 | # macOS junk files 9 | .DS_Store -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Build on push or pull request 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v3 13 | - name: Set up JDK 8 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: 'zulu' 17 | java-version: 8 18 | - name: Build with Gradle 19 | uses: gradle/gradle-build-action@v2 20 | with: 21 | gradle-version: wrapper 22 | cache-read-only: ${{ github.ref != 'refs/heads/master' }} 23 | arguments: build 24 | - name: Upload artifacts 25 | uses: actions/upload-artifact@v3 26 | with: 27 | name: Package 28 | path: build/libs 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neko Detector 2 | 3 | > A tool to help detect if you are infected by the fractureiser malware. 4 | 5 | The fractureiser malware once you run it, infects any jar it is able to find. This tool will help you detect if you are infected by the malware by scanning every jar file in your computer and checking if it shows sign of infection. *For more information about the malware, please refer to the [information document](https://github.com/fractureiser-investigation/fractureiser/blob/main/README.md).* 6 | 7 | ## Usage 8 | 9 | ``` 10 | java -jar scanner.jar <# of threads> 11 | ``` 12 | 13 | ## Example 14 | 15 | ```bash 16 | # Scan your entire Windows system with 4 threads 17 | java -jar scanner.jar 4 C:\ 18 | 19 | # Scan your entire Linux system with 4 threads 20 | java -jar scanner.jar 4 / 21 | ``` 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MCRcortex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/me/cortex/jarscanner/Constants.java: -------------------------------------------------------------------------------- 1 | package me.cortex.jarscanner; 2 | 3 | /** 4 | * Constants class for Nekodetector. 5 | *

ORIGINAL SOURCE: https://github.com/MCRcortex/nekodetector

6 | * 7 | * @author mica-alex (https://github.com/mica-alex) 8 | * @author Huskydog9988 (https://github.com/Huskydog9988) 9 | */ 10 | public class Constants { 11 | 12 | /** 13 | * ANSI code for color red. 14 | */ 15 | public static final String ANSI_RED = "\u001B[31m"; 16 | 17 | 18 | /** 19 | * ANSI code for color white. 20 | */ 21 | public static final String ANSI_WHITE = "\u001B[37m"; 22 | 23 | /** 24 | * ANSI code for color green. 25 | */ 26 | public static final String ANSI_GREEN = "\u001B[32m"; 27 | 28 | /** 29 | * ANSI code for reset. 30 | */ 31 | public static final String ANSI_RESET = "\u001B[0m"; 32 | 33 | /** 34 | * The Java class file extension that is used to scan for malicious code signatures. 35 | */ 36 | public static final String CLASS_FILE_EXTENSION = ".class"; 37 | 38 | /** 39 | * The Java Jar file extension that is used to scan for malicious code signatures. 40 | */ 41 | public static final String JAR_FILE_EXTENSION = ".jar"; 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | create_release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | outputs: 14 | upload_url: ${{ steps.create_release.outputs.upload_url }} 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up JDK 8 20 | uses: actions/setup-java@v3 21 | with: 22 | distribution: 'zulu' 23 | java-version: 8 24 | 25 | - name: Grab and store version 26 | run: | 27 | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") 28 | echo "VERSION=$tag_name" >> $GITHUB_ENV 29 | 30 | - name: Build with Gradle 31 | uses: gradle/gradle-build-action@v2 32 | with: 33 | gradle-version: wrapper 34 | cache-read-only: ${{ github.ref != 'refs/heads/master' }} 35 | # specify the build version, overrides whatever is in the build.gradle file 36 | arguments: build -PoverrideVersion=${{ env.VERSION }} 37 | 38 | - name: Create release 39 | id: create_release 40 | uses: softprops/action-gh-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ github.ref }} 45 | name: Neko Detector ${{ env.VERSION }} 46 | draft: true 47 | prerelease: false 48 | files: | 49 | jarscanner-${{ env.VERSION }}.jar 50 | -------------------------------------------------------------------------------- /src/main/java/me/cortex/jarscanner/Results.java: -------------------------------------------------------------------------------- 1 | package me.cortex.jarscanner; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Class for storing Nekodetector scan results. 7 | *

ORIGINAL SOURCE: https://github.com/MCRcortex/nekodetector

8 | * 9 | * @author mica-alex (https://github.com/mica-alex) 10 | */ 11 | public class Results { 12 | 13 | /** 14 | * List of detections for stage 1. 15 | */ 16 | private List stage1Detections; 17 | 18 | /** 19 | * List of detections for stage 2. 20 | */ 21 | private List stage2Detections; 22 | 23 | /** 24 | * Creates a new instance of Results with the given stage 1 and stage 2 detections. 25 | * 26 | * @param stage1Detections List of detections for stage 1. 27 | * @param stage2Detections List of detections for stage 2. 28 | */ 29 | public Results(List stage1Detections, List stage2Detections) { 30 | this.stage1Detections = stage1Detections; 31 | this.stage2Detections = stage2Detections; 32 | } 33 | 34 | /** 35 | * Returns the list of detections for stage 1. 36 | * 37 | * @return List of detections for stage 1. 38 | */ 39 | public List getStage1Detections() { 40 | return stage1Detections; 41 | } 42 | 43 | /** 44 | * Returns the list of detections for stage 2. 45 | * 46 | * @return List of detections for stage 2. 47 | */ 48 | public List getStage2Detections() { 49 | return stage2Detections; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/me/cortex/jarscanner/Gui.java: -------------------------------------------------------------------------------- 1 | package me.cortex.jarscanner; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.file.Path; 8 | import java.util.concurrent.RejectedExecutionException; 9 | import java.util.function.Function; 10 | 11 | public class Gui { 12 | public static boolean USING_GUI; 13 | private static JTextArea textArea; 14 | private static JButton searchDirPicker; 15 | private static Path searchDir = new File(System.getProperty("user.home")).toPath(); 16 | 17 | private static Thread scanThread; 18 | 19 | public static void main(String[] args) { 20 | createAndDisplayGui(); 21 | } 22 | 23 | private static void createAndDisplayGui() { 24 | USING_GUI = true; 25 | textArea = new JTextArea(20, 40); 26 | JFrame frame = new JFrame(); 27 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 28 | JLabel searchDirPickerLabel = new JLabel("Select Search Directory:"); 29 | searchDirPickerLabel.setAlignmentX(Component.CENTER_ALIGNMENT); 30 | 31 | searchDirPicker = new JButton(new File(System.getProperty("user.home")).getName()); 32 | searchDirPicker.addActionListener(e -> { 33 | JFileChooser fileChooser = new JFileChooser(); 34 | fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 35 | fileChooser.setFileHidingEnabled(false); 36 | int option = fileChooser.showOpenDialog(frame); 37 | if (option == JFileChooser.APPROVE_OPTION) { 38 | File file = fileChooser.getSelectedFile(); 39 | searchDir = file.toPath(); 40 | searchDirPicker.setText(file.getName()); 41 | } 42 | }); 43 | 44 | JButton credsButton = new JButton("Credits"); 45 | credsButton.addActionListener(e -> { 46 | showCredits(); 47 | }); 48 | 49 | // Auto scroll checkbox 50 | JCheckBox autoScrollCheckBox = new JCheckBox("Auto-scroll"); 51 | autoScrollCheckBox.setSelected(true); 52 | 53 | // Cancel button 54 | JButton cancelButton = new JButton("Cancel!"); 55 | cancelButton.setEnabled(false); 56 | cancelButton.addActionListener(e -> { 57 | if (scanThread != null) { 58 | Main.cancelScanIfRunning(); 59 | scanThread.interrupt(); 60 | } 61 | }); 62 | 63 | // Run button 64 | JButton runButton = new JButton("Run!"); 65 | runButton.addActionListener(e -> { 66 | scanThread = new Thread(() -> { 67 | // Disable buttons (enable cancel) 68 | searchDirPicker.setEnabled(false); 69 | runButton.setEnabled(false); 70 | cancelButton.setEnabled(true); 71 | 72 | // Run scan 73 | try { 74 | // Create log output function 75 | Function logOutput = out -> { 76 | String processedOut = out.replace(Constants.ANSI_RED, "").replace(Constants.ANSI_GREEN, "").replace(Constants.ANSI_WHITE, "").replace(Constants.ANSI_RESET, ""); 77 | textArea.append(processedOut + "\n"); 78 | // Scroll to bottom of text area if auto-scroll is enabled 79 | if (autoScrollCheckBox.isSelected()) { 80 | textArea.setCaretPosition(textArea.getDocument().getLength()); 81 | } 82 | return out; 83 | }; 84 | 85 | Results run = Main.run(4, searchDir, true, logOutput); 86 | Main.outputRunResults(run, logOutput); 87 | textArea.append("Done scanning!"); 88 | } catch (Exception ex) { 89 | if (ex instanceof InterruptedException || ex instanceof RejectedExecutionException) { 90 | textArea.append("Scan cancelled!" + "\n"); 91 | } else { 92 | textArea.append("Error while running scan!" + "\n"); 93 | } 94 | } 95 | 96 | // Re-enable buttons (disable cancel) 97 | searchDirPicker.setEnabled(true); 98 | runButton.setEnabled(true); 99 | cancelButton.setEnabled(false); 100 | }); 101 | scanThread.start(); 102 | }); 103 | 104 | // Create grid bag layout 105 | frame.getContentPane().setLayout(new GridBagLayout()); 106 | GridBagConstraints gridBagConstraints = new GridBagConstraints(); 107 | 108 | // Create button panel 109 | JPanel buttonPanel = new JPanel(); 110 | buttonPanel.setLayout(new GridBagLayout()); 111 | gridBagConstraints.gridx = 0; 112 | gridBagConstraints.gridy = 0; 113 | buttonPanel.add(runButton, gridBagConstraints); 114 | gridBagConstraints.gridx = 1; 115 | gridBagConstraints.gridy = 0; 116 | buttonPanel.add(cancelButton, gridBagConstraints); 117 | gridBagConstraints.gridx = 2; 118 | gridBagConstraints.gridy = 0; 119 | buttonPanel.add(credsButton, gridBagConstraints); 120 | gridBagConstraints.gridx = 1; 121 | gridBagConstraints.gridy = 1; 122 | buttonPanel.add(autoScrollCheckBox, gridBagConstraints); 123 | 124 | // Add button panel to top right of frame 125 | gridBagConstraints.gridx = 1; 126 | gridBagConstraints.gridy = 0; 127 | gridBagConstraints.insets = new Insets(10, 10, 10, 10); 128 | gridBagConstraints.anchor = GridBagConstraints.NORTHEAST; 129 | frame.getContentPane().add(buttonPanel, gridBagConstraints); 130 | 131 | // Create panel for search dir picker 132 | JPanel searchDirPickerPanel = new JPanel(); 133 | searchDirPickerPanel.add(searchDirPickerLabel); 134 | searchDirPickerPanel.add(searchDirPicker); 135 | 136 | // Add search dir picker panel to top left of frame 137 | gridBagConstraints.gridx = 0; 138 | gridBagConstraints.gridy = 0; 139 | gridBagConstraints.insets = new Insets(10, 10, 10, 10); 140 | gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; 141 | frame.getContentPane().add(searchDirPickerPanel, gridBagConstraints); 142 | 143 | // Create panel for log area 144 | JScrollPane logAreaPanel = createTextArea(); 145 | 146 | // Add log area panel to bottom of frame 147 | gridBagConstraints.gridx = 0; 148 | gridBagConstraints.gridy = 1; 149 | gridBagConstraints.gridwidth = 2; 150 | gridBagConstraints.weightx = 1; 151 | gridBagConstraints.weighty = 1; 152 | gridBagConstraints.insets = new Insets(10, 10, 10, 10); 153 | gridBagConstraints.fill = GridBagConstraints.BOTH; 154 | frame.getContentPane().add(logAreaPanel, gridBagConstraints); 155 | 156 | // Pack and display frame 157 | frame.pack(); 158 | frame.setTitle("Neko Detector"); 159 | frame.setLocationByPlatform(true); 160 | frame.setVisible(true); 161 | frame.setMinimumSize(new Dimension(600, 300)); 162 | frame.setMaximumSize(new Dimension(600, 300)); 163 | frame.setPreferredSize(new Dimension(600, 300)); 164 | 165 | } 166 | 167 | private static String[] credits = new String[]{ 168 | "Credits to:", 169 | "Cortex, for decompiling and deobfuscating the malware, and making the initial detector.", 170 | "D3SL: Extensive reverse engineering, early discovery learned of later", 171 | "Nia: Extensive Stage 3 reverse engineering", 172 | "Jasmine: Coordination, writing the decompiler we've been using (Quiltflower)", 173 | "Emi: Coordination, initial discovery (for this team), and early research", 174 | "williewillus: Coordination, journalist", 175 | "quat: Documentation, initial infected sample research", 176 | "xylemlandmark: Coordination of documentation, crowd control", 177 | "Vazkii: they're pretty neat", 178 | "Elocin: Originally finding the malware itself" 179 | }; 180 | 181 | private static void showCredits() { 182 | JFrame frame = new JFrame("Credits"); 183 | JTextArea credits = new JTextArea(); 184 | credits.setEditable(false); 185 | for (String string : Gui.credits) { 186 | credits.append(string + "\n"); 187 | } 188 | frame.add(credits); 189 | frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 190 | frame.pack(); 191 | frame.setLocationByPlatform(true); 192 | frame.setVisible(true); 193 | } 194 | 195 | private static JScrollPane createTextArea() { 196 | JScrollPane scrollPane = new JScrollPane(textArea); 197 | textArea.setEditable(false); 198 | 199 | return scrollPane; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /src/main/java/me/cortex/jarscanner/Main.java: -------------------------------------------------------------------------------- 1 | package me.cortex.jarscanner; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.*; 6 | import java.nio.file.attribute.BasicFileAttributes; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | import java.util.function.Function; 15 | import java.util.jar.JarFile; 16 | 17 | /** 18 | * Main class for Nekodetector, which scans for malicious code signatures from the Nekoclient malware. 19 | *

ORIGINAL SOURCE: https://github.com/MCRcortex/nekodetector

20 | * 21 | * @author MCRcortex (https://github.com/MCRcortex) 22 | * @author Huskydog9988 (https://github.com/Huskydog9988) 23 | * @author mica-alex (https://github.com/mica-alex) 24 | */ 25 | public class Main { 26 | 27 | /** 28 | * The executor service used to scan jars in parallel. 29 | */ 30 | private static ExecutorService executorService; 31 | 32 | /** 33 | * Main method. Checks arguments and scans the specified directory for malicious code signatures. Outputs the 34 | * results of the scan to the console. 35 | * 36 | * @param args the command line arguments (number of threads, directory to scan, and whether to emit walk errors) 37 | */ 38 | public static void main(String[] args) { 39 | // Check arguments 40 | if (!checkArgs(args)) { 41 | return; 42 | } 43 | 44 | // Parse arguments 45 | int nThreads = Integer.parseInt(args[0]); 46 | Path dirToCheck = new File(args[1]).toPath(); 47 | boolean emitWalkErrors = false; 48 | if (args.length > 2) { 49 | emitWalkErrors = Boolean.parseBoolean(args[2]); 50 | } 51 | 52 | // Create log output function 53 | Function logOutput = outputString -> { 54 | System.out.println(outputString); 55 | return outputString; 56 | }; 57 | 58 | // Output scan start 59 | logOutput.apply("Starting scan..."); 60 | 61 | // Run scan 62 | Results results = null; 63 | try { 64 | results = run(nThreads, dirToCheck, emitWalkErrors, logOutput); 65 | } catch (IOException e) { 66 | logOutput.apply("An error occurred while scanning the directory: " + dirToCheck); 67 | e.printStackTrace(); 68 | } catch (InterruptedException e) { 69 | logOutput.apply("An error occurred while waiting for the scan to complete."); 70 | e.printStackTrace(); 71 | } 72 | 73 | // Output scan completion and results 74 | outputRunResults(results, logOutput); 75 | } 76 | 77 | /** 78 | * Output the results of a scan to the specified log output function. 79 | * 80 | * @param results the resulting from {@link #run(int, Path, boolean, Function)}. 81 | * @param logOutput the function to use for logging output 82 | */ 83 | public static void outputRunResults(Results results, Function logOutput) { 84 | if (results == null) { 85 | logOutput.apply("Scan failed. Unable to display results."); 86 | } else { 87 | List stage1Detections = results.getStage1Detections(); 88 | List stage2Detections = results.getStage2Detections(); 89 | if (stage1Detections.isEmpty() && stage2Detections.isEmpty()) { 90 | logOutput.apply(Constants.ANSI_GREEN + "Scan complete. No infected jars found." + Constants.ANSI_RESET); 91 | } else { 92 | logOutput.apply(Constants.ANSI_RED + "Scan complete. Infections found!" + Constants.ANSI_RESET); 93 | if (!stage1Detections.isEmpty()) { 94 | logOutput.apply(Constants.ANSI_RED + "Stage 1 Infections (" + stage1Detections.size() + "):" + Constants.ANSI_RESET); 95 | for (int i = 0; i < stage1Detections.size(); i++) { 96 | String stage1Infection = stage1Detections.get(i); 97 | int stage1InfectionNumber = i + 1; 98 | logOutput.apply(Constants.ANSI_RED + "[" + stage1InfectionNumber + "] " + Constants.ANSI_WHITE + stage1Infection + Constants.ANSI_RESET); 99 | } 100 | } 101 | if (!stage2Detections.isEmpty()) { 102 | logOutput.apply(Constants.ANSI_RED + "Stage 2 Infections (" + stage2Detections.size() + "):" + Constants.ANSI_RESET); 103 | for (int i = 0; i < stage2Detections.size(); i++) { 104 | String stage2Infection = stage2Detections.get(i); 105 | int stage2InfectionNumber = i + 1; 106 | logOutput.apply(Constants.ANSI_RED + "[" + stage2InfectionNumber + "] " + Constants.ANSI_WHITE + stage2Infection + Constants.ANSI_RESET); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Runs a check and scans a folder for jars with malicious code signatures. 115 | * An object containing lists of infected files found during scan stages is returned. 116 | * 117 | * @param nThreads the number of threads to use for scanning 118 | * @param dirToCheck the directory to scan 119 | * @param emitWalkErrors whether to emit errors when walking the directory tree 120 | * @param logOutput the function to use for logging output 121 | * @return a scan results object 122 | * @throws IllegalArgumentException if the specified directory does not exist or is not a directory, or if the 123 | * number of threads is less than 1. 124 | * @throws IOException if an I/O error occurs while walking the directory tree 125 | */ 126 | public static Results run(int nThreads, Path dirToCheck, boolean emitWalkErrors, Function logOutput) throws IOException, InterruptedException { 127 | // Output scan start 128 | long startTime = System.currentTimeMillis(); 129 | logOutput.apply(Constants.ANSI_GREEN + "Starting All Scans - " + Constants.ANSI_RESET 130 | + "This may take a while depending on the size of the directories and JAR files."); 131 | 132 | // Check that specified directory is valid, exists, and is a directory 133 | File dirToCheckFile = dirToCheck.toFile(); 134 | if (!dirToCheckFile.exists()) { 135 | throw new IllegalArgumentException("Specified directory does not exist: " + dirToCheck); 136 | } 137 | if (!dirToCheckFile.isDirectory()) { 138 | throw new IllegalArgumentException("Specified directory is not a directory: " + dirToCheck); 139 | } 140 | 141 | // Check number of threads is valid 142 | if (nThreads < 1) { 143 | throw new IllegalArgumentException("Number of threads must be at least 1"); 144 | } 145 | 146 | // Create executor service with number of threads 147 | executorService = Executors.newFixedThreadPool(nThreads); 148 | 149 | // Scan all jars in path 150 | long stage1StartTime = System.currentTimeMillis(); 151 | logOutput.apply(Constants.ANSI_GREEN + "Running Stage 1 Scan..." + Constants.ANSI_RESET); 152 | final List stage1InfectionsList = new ArrayList<>(); 153 | Files.walkFileTree(dirToCheck, new FileVisitor() { 154 | /** 155 | * Invoked for a directory before entries in the directory are visited. 156 | * @param dir a reference to the directory 157 | * @param attrs the directory's basic attributes 158 | * 159 | * @return {@link FileVisitResult#CONTINUE}. 160 | */ 161 | @Override 162 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 163 | return FileVisitResult.CONTINUE; 164 | } 165 | 166 | /** 167 | * Invoked for a file in a directory. 168 | * @param file a reference to the file 169 | * @param attrs the file's basic attributes 170 | * 171 | * @return {@link FileVisitResult#CONTINUE} 172 | */ 173 | @Override 174 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 175 | // Check if file is a scannable Jar file 176 | boolean isScannable = file.toString().toLowerCase().endsWith(Constants.JAR_FILE_EXTENSION); 177 | 178 | // If file is scannable, submit it to the executor service for scanning 179 | if (isScannable) { 180 | executorService.submit(() -> { 181 | try (JarFile scannableJarFile = new JarFile(file.toFile())) { 182 | boolean infectionDetected = Detector.scan(scannableJarFile, file, logOutput); 183 | if (infectionDetected) { 184 | synchronized (stage1InfectionsList) { 185 | stage1InfectionsList.add(file.toString()); 186 | } 187 | } 188 | } catch (Exception e) { 189 | if (emitWalkErrors) { 190 | logOutput.apply("Failed to scan Jar file: " + file); 191 | e.printStackTrace(); 192 | } 193 | } 194 | }); 195 | } 196 | return FileVisitResult.CONTINUE; 197 | } 198 | 199 | /** 200 | * Invoked for a file that could not be visited. 201 | * @param file a reference to the file 202 | * @param exc the I/O exception that prevented the file from being visited 203 | * 204 | * @return {@link FileVisitResult#CONTINUE} 205 | */ 206 | @Override 207 | public FileVisitResult visitFileFailed(Path file, IOException exc) { 208 | if (emitWalkErrors) { 209 | logOutput.apply("Failed to access file: " + file); 210 | } 211 | return FileVisitResult.CONTINUE; 212 | } 213 | 214 | /** 215 | * Invoked for a directory after entries in the directory, and all of their 216 | * descendants, have been visited. This method is also invoked when iteration 217 | * of the directory completes prematurely (by a {@link #visitFile visitFile} 218 | * failure, or by throwing an exception). 219 | * 220 | * @param dir a reference to the directory 221 | * @param exc {@code null} if the iteration of the directory completes without 222 | * an error; otherwise the I/O exception that caused the iteration 223 | * of the directory to complete prematurely 224 | * 225 | * @return {@link FileVisitResult#CONTINUE} 226 | */ 227 | @Override 228 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) { 229 | if (exc != null && emitWalkErrors) { 230 | logOutput.apply("Failed to access directory: " + dir); 231 | } 232 | return FileVisitResult.CONTINUE; 233 | } 234 | }); 235 | 236 | // Shutdown executor service and wait for all tasks to complete 237 | executorService.shutdown(); 238 | boolean timedOut = !executorService.awaitTermination(100000, TimeUnit.DAYS); 239 | if (timedOut) { 240 | logOutput.apply("Timed out while waiting for Jar scanning to complete."); 241 | } 242 | long stage1EndTime = System.currentTimeMillis(); 243 | long stage1Time = stage1EndTime - stage1StartTime; 244 | logOutput.apply(Constants.ANSI_GREEN + "Stage 1 Scan Complete - " + Constants.ANSI_RESET + "Took " + stage1Time + "ms."); 245 | 246 | // Run stage 2 scan 247 | long stage2StartTime = System.currentTimeMillis(); 248 | logOutput.apply(Constants.ANSI_GREEN + "Running Stage 2 Scan..." + Constants.ANSI_RESET); 249 | List stage2InfectionsList = Detector.checkForStage2(); 250 | long stage2EndTime = System.currentTimeMillis(); 251 | long stage2Time = stage2EndTime - stage2StartTime; 252 | logOutput.apply(Constants.ANSI_GREEN + "Stage 2 Scan Complete - " + Constants.ANSI_RESET + "Took " + stage2Time + "ms."); 253 | 254 | // Output scan end 255 | long endTime = System.currentTimeMillis(); 256 | long totalTime = endTime - startTime; 257 | logOutput.apply( 258 | Constants.ANSI_GREEN + "All Scans Complete - " + Constants.ANSI_RESET + "Total " + totalTime + "ms."); 259 | 260 | 261 | // Build results and return 262 | return new Results(stage1InfectionsList, stage2InfectionsList); 263 | } 264 | 265 | /** 266 | * Cancels the current scan, if one is running, by shutting down the executor service. 267 | */ 268 | public static void cancelScanIfRunning() { 269 | if (executorService != null) { 270 | executorService.shutdownNow(); 271 | } 272 | } 273 | 274 | /** 275 | * Checks the arguments passed to the program for validity. 276 | * 277 | * @param args the arguments passed to the program main method 278 | * @return {@code true} if the arguments are valid, {@code false} otherwise 279 | */ 280 | private static boolean checkArgs(String[] args) { 281 | // Output usage information if no arguments are passed 282 | if (args.length == 0) { 283 | Gui.main(args); 284 | return false; 285 | } 286 | 287 | // Check if the number of threads is an integer 288 | int nThreads; 289 | try { 290 | nThreads = Integer.parseInt(args[0]); 291 | } catch (Exception e) { 292 | System.err.println("Invalid thread count, please use an integer."); 293 | return false; 294 | } 295 | 296 | // Check if the number of threads is positive 297 | if (nThreads <= 0) { 298 | System.err.println("Invalid thread count, must be greater than 0."); 299 | return false; 300 | } 301 | 302 | // Check if the directory to scan is valid 303 | File dirToCheck = null; 304 | try { 305 | dirToCheck = new File(args[1]); 306 | } catch (Exception e) { 307 | System.err.println("Invalid path, unable to load."); 308 | return false; 309 | } 310 | 311 | // Check if the directory to scan exists 312 | if (!dirToCheck.exists()) { 313 | System.err.println("Invalid path, directory does not exist."); 314 | return false; 315 | } 316 | 317 | // Check if the directory to scan is a directory 318 | if (!dirToCheck.isDirectory()) { 319 | System.err.println("Invalid path, not a directory."); 320 | return false; 321 | } 322 | 323 | // Return true if all checks pass 324 | return true; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/main/java/me/cortex/jarscanner/Detector.java: -------------------------------------------------------------------------------- 1 | package me.cortex.jarscanner; 2 | 3 | import org.objectweb.asm.ClassReader; 4 | import org.objectweb.asm.tree.*; 5 | 6 | import java.io.*; 7 | import java.nio.file.Files; 8 | import java.nio.file.LinkOption; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.function.Function; 15 | import java.util.jar.JarFile; 16 | 17 | import static org.objectweb.asm.Opcodes.*; 18 | 19 | /** 20 | * Detector class for Nekodetector, which scans for malicious code signatures from the Nekoclient malware. 21 | *

ORIGINAL SOURCE: https://github.com/MCRcortex/nekodetector

22 | * 23 | * @author MCRcortex (https://github.com/MCRcortex) 24 | * @author Huskydog9988 (https://github.com/Huskydog9988) 25 | */ 26 | public class Detector { 27 | 28 | /** 29 | * Scans for malicious code signatures in the specified {@link JarFile} located at the specified {@link Path}. 30 | * 31 | * @param file The {@link JarFile} to scan. 32 | * @param path The {@link Path} of the {@link JarFile} to scan. 33 | * @return {@code true} if a signature match was found, otherwise {@code false}. 34 | */ 35 | public static boolean scan(JarFile file, Path path, Function output) { 36 | // Create boolean to store whether a signature match was found 37 | boolean signatureMatchFound = false; 38 | 39 | // Check Jar file for infection signatures 40 | try { 41 | // Scan .class files for signatures 42 | signatureMatchFound = file.stream() 43 | .filter(entry -> entry.getName().endsWith(Constants.CLASS_FILE_EXTENSION)) 44 | .anyMatch(entry -> { 45 | try { 46 | return scanClass(getByteArray(file.getInputStream(entry))); 47 | } catch (IOException e) { 48 | output.apply("Failed to scan class in Jar file [" + path + "] due to an IO error: " + entry.getName()); 49 | output.apply("Error:" + e.getMessage()); 50 | return false; 51 | } catch (IllegalArgumentException e) { 52 | output.apply("Failed to scan class in Jar file [" + path + "] due to a parsing error: " + entry.getName()); 53 | output.apply( 54 | "This is likely due to a malformed class file or an issue with the JAR file itself."); 55 | output.apply("Error:" + e.getMessage()); 56 | 57 | return false; 58 | } 59 | }); 60 | } catch (Exception e) { 61 | output.apply("Failed to scan Jar file: " + path); 62 | output.apply("Error:" + e.getMessage()); 63 | } finally { 64 | // Close Jar file 65 | try { 66 | file.close(); 67 | } catch (IOException e) { 68 | output.apply("Failed to close Jar file after scan: " + path); 69 | output.apply("Error:" + e.getMessage()); 70 | } 71 | } 72 | 73 | // Return whether a signature match was found 74 | return signatureMatchFound; 75 | } 76 | 77 | private static byte[] getByteArray(InputStream inputStream) throws IOException { 78 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 79 | 80 | int nRead; 81 | byte[] data = new byte[16384]; 82 | 83 | while ((nRead = inputStream.read(data, 0, data.length)) != -1) { 84 | buffer.write(data, 0, nRead); 85 | } 86 | 87 | return buffer.toByteArray(); 88 | } 89 | 90 | private static final AbstractInsnNode[] SIG1 = new AbstractInsnNode[] { 91 | new TypeInsnNode(NEW, "java/lang/String"), 92 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 93 | new TypeInsnNode(NEW, "java/lang/String"), 94 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 95 | new MethodInsnNode(INVOKESTATIC, "java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;"), 96 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Class", "getConstructor", 97 | "([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;"), 98 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 99 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 100 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 101 | new MethodInsnNode(INVOKESPECIAL, "java/net/URL", "", 102 | "(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V"), 103 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/reflect/Constructor", "newInstance", 104 | "([Ljava/lang/Object;)Ljava/lang/Object;"), 105 | new MethodInsnNode(INVOKESTATIC, "java/lang/Class", "forName", 106 | "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"), 107 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 108 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Class", "getMethod", 109 | "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"), 110 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/reflect/Method", "invoke", 111 | "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), 112 | }; 113 | 114 | private static final AbstractInsnNode[] SIG2 = new AbstractInsnNode[] { 115 | new MethodInsnNode(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;"), 116 | new MethodInsnNode(INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"), 117 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "concat", 118 | "(Ljava/lang/String;)Ljava/lang/String;"), // TODO:FIXME: this might not be in all of them 119 | new MethodInsnNode(INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"), 120 | new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), 121 | new MethodInsnNode(INVOKEVIRTUAL, "java/io/File", "getPath", "()Ljava/lang/String;"), 122 | new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;"), 123 | }; 124 | 125 | // The IP 126 | private static final AbstractInsnNode[] SIG3 = new AbstractInsnNode[] { 127 | new IntInsnNode(BIPUSH, 56), 128 | new InsnNode(BASTORE), 129 | new InsnNode(DUP), 130 | new InsnNode(ICONST_1), 131 | new IntInsnNode(BIPUSH, 53), 132 | new InsnNode(BASTORE), 133 | new InsnNode(DUP), 134 | new InsnNode(ICONST_2), 135 | new IntInsnNode(BIPUSH, 46), 136 | new InsnNode(BASTORE), 137 | new InsnNode(DUP), 138 | new InsnNode(ICONST_3), 139 | new IntInsnNode(BIPUSH, 50), 140 | new InsnNode(BASTORE), 141 | new InsnNode(DUP), 142 | new InsnNode(ICONST_4), 143 | new IntInsnNode(BIPUSH, 49), 144 | new InsnNode(BASTORE), 145 | new InsnNode(DUP), 146 | new InsnNode(ICONST_5), 147 | new IntInsnNode(BIPUSH, 55), 148 | new InsnNode(BASTORE), 149 | new InsnNode(DUP), 150 | new IntInsnNode(BIPUSH, 6), 151 | new IntInsnNode(BIPUSH, 46), 152 | new InsnNode(BASTORE), 153 | new InsnNode(DUP), 154 | new IntInsnNode(BIPUSH, 7), 155 | new IntInsnNode(BIPUSH, 49), 156 | new InsnNode(BASTORE), 157 | new InsnNode(DUP), 158 | new IntInsnNode(BIPUSH, 8), 159 | new IntInsnNode(BIPUSH, 52), 160 | new InsnNode(BASTORE), 161 | new InsnNode(DUP), 162 | new IntInsnNode(BIPUSH, 9), 163 | new IntInsnNode(BIPUSH, 52), 164 | new InsnNode(BASTORE), 165 | new InsnNode(DUP), 166 | new IntInsnNode(BIPUSH, 10), 167 | new IntInsnNode(BIPUSH, 46), 168 | new InsnNode(BASTORE), 169 | new InsnNode(DUP), 170 | new IntInsnNode(BIPUSH, 11), 171 | new IntInsnNode(BIPUSH, 49), 172 | new InsnNode(BASTORE), 173 | new InsnNode(DUP), 174 | new IntInsnNode(BIPUSH, 12), 175 | new IntInsnNode(BIPUSH, 51), 176 | new InsnNode(BASTORE), 177 | new InsnNode(DUP), 178 | new IntInsnNode(BIPUSH, 13), 179 | new IntInsnNode(BIPUSH, 48) 180 | }; 181 | 182 | private static boolean same(AbstractInsnNode a, AbstractInsnNode b) { 183 | if (a instanceof TypeInsnNode) { 184 | TypeInsnNode aa = (TypeInsnNode) a; 185 | return aa.desc.equals(((TypeInsnNode) b).desc); 186 | } 187 | if (a instanceof MethodInsnNode) { 188 | MethodInsnNode aa = (MethodInsnNode) a; 189 | return aa.owner.equals(((MethodInsnNode) b).owner) 190 | && aa.name.equals(((MethodInsnNode) b).name) 191 | && aa.desc.equals(((MethodInsnNode) b).desc); 192 | } 193 | if (a instanceof InsnNode) { 194 | return true; 195 | } 196 | throw new IllegalArgumentException("TYPE NOT ADDED"); 197 | } 198 | 199 | public static boolean scanClass(byte[] clazz) { 200 | ClassReader reader = new ClassReader(clazz); 201 | ClassNode node = new ClassNode(); 202 | try { 203 | reader.accept(node, 0); 204 | } catch (Exception e) { 205 | return false;// Yes this is very hacky but should never happen with valid clasees 206 | } 207 | for (MethodNode method : node.methods) { 208 | { 209 | // Method 1, this is a hard detect, if it matches this it is 100% chance 210 | // infected 211 | boolean match = true; 212 | int j = 0; 213 | for (int i = 0; i < method.instructions.size() && j < SIG1.length; i++) { 214 | AbstractInsnNode insn = method.instructions.get(i); 215 | if (insn.getOpcode() == -1) { 216 | continue; 217 | } 218 | if (insn.getOpcode() == SIG1[j].getOpcode()) { 219 | if (!same(insn, SIG1[j++])) { 220 | match = false; 221 | break; 222 | } 223 | } 224 | } 225 | if (j != SIG1.length) { 226 | match = false; 227 | } 228 | if (match) { 229 | return true; 230 | } 231 | } 232 | 233 | { 234 | // Method 2, this is a near hard detect, if it matches this it is 95% chance 235 | // infected 236 | boolean match = false; 237 | outer: for (int q = 0; q < method.instructions.size(); q++) { 238 | int j = 0; 239 | for (int i = q; i < method.instructions.size() && j < SIG2.length; i++) { 240 | AbstractInsnNode insn = method.instructions.get(i); 241 | if (insn.getOpcode() != SIG2[j].getOpcode()) { 242 | continue; 243 | } 244 | 245 | if (insn.getOpcode() == SIG2[j].getOpcode()) { 246 | if (!same(insn, SIG2[j++])) { 247 | continue outer; 248 | } 249 | } 250 | } 251 | if (j == SIG2.length) { 252 | match = true; 253 | break; 254 | } 255 | } 256 | if (match) { 257 | return true; 258 | } 259 | } 260 | 261 | // Method 3, this looks for a byte array with the IP. This is a likely match. 262 | { 263 | boolean match = false; 264 | // where we're looking in the SIG3 array 265 | int pos = 0; 266 | for (int i = 0; i < method.instructions.size(); i++) { 267 | if (pos == SIG3.length) { 268 | break; 269 | } 270 | AbstractInsnNode insn = method.instructions.get(i); 271 | if (insn.getOpcode() == -1) { 272 | continue; 273 | } 274 | if (insn.getOpcode() == SIG3[pos].getOpcode()) { 275 | // the opcode matches 276 | 277 | if (SIG3[pos].getType() == AbstractInsnNode.INT_INSN) { 278 | // check if operand matches 279 | IntInsnNode iInsn = (IntInsnNode) insn; 280 | IntInsnNode sigInsn = (IntInsnNode) SIG3[pos]; 281 | if (iInsn.operand == sigInsn.operand) { 282 | // operands match 283 | match = true; 284 | pos++; 285 | } 286 | } else { 287 | // this is a regular InsnNode; just match 288 | match = true; 289 | pos++; 290 | } 291 | } else { 292 | match = false; 293 | pos = 0; 294 | } 295 | } 296 | 297 | if (match) { 298 | return true; 299 | } 300 | } 301 | } 302 | return false; 303 | } 304 | 305 | /** 306 | * Checks for signs of stage 2 infection and returns a list of files that are flagged as suspicious. 307 | * Based on: 308 | * https://github.com/fractureiser-investigation/fractureiser#am-i-infected 309 | */ 310 | public static List checkForStage2() { 311 | // Create list to store suspicious files found 312 | List suspiciousFilesFound = new ArrayList<>(); 313 | 314 | // windows checks 315 | Path windowsStartupDirectory = (Objects.isNull(System.getenv("APPDATA")) 316 | ? Paths.get(System.getProperty("user.home"), "AppData", "Roaming") 317 | : Paths.get(System.getenv("APPDATA"), new String[0])) 318 | .resolve(Paths.get("Microsoft", "Windows", "Start Menu", "Programs", "Startup")); 319 | boolean windows = Files.isDirectory(windowsStartupDirectory, new LinkOption[0]) 320 | && Files.isWritable(windowsStartupDirectory); 321 | 322 | String[] maliciousFiles = { 323 | ".ref", 324 | "client.jar", 325 | "lib.dll", 326 | "libWebGL64.jar", 327 | "run.bat" 328 | }; 329 | 330 | if (windows) { 331 | // only checking for the folder because the file can be renamed 332 | File edgeFolder = new File(System.getenv("APPDATA") + "\\Microsoft Edge"); 333 | if (edgeFolder.exists()) { 334 | suspiciousFilesFound.add(edgeFolder.getAbsolutePath()); 335 | } 336 | 337 | File startFolder = new File("Microsoft\\Windows\\Start Menu\\Programs\\Startup"); 338 | // get all files in the startup folder, and check if they match the malicious 339 | if (startFolder.exists() && startFolder.isDirectory()) { 340 | File[] startFiles = startFolder.listFiles(); 341 | 342 | for (int i = 0; i < startFiles.length; i++) { 343 | 344 | for (int j = 0; j < maliciousFiles.length; j++) { 345 | if (startFiles[i].getName().equals(maliciousFiles[j])) { 346 | suspiciousFilesFound.add(startFiles[i].getAbsolutePath()); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | 353 | // linux checks 354 | if (System.getProperty("os.name").toLowerCase().contains("linux")) { 355 | File file = new File("~/.config/.data/lib.jar"); 356 | if (file.exists()) { 357 | suspiciousFilesFound.add(file.getAbsolutePath()); 358 | } 359 | } 360 | 361 | // Return list of suspicious files found 362 | return suspiciousFilesFound; 363 | } 364 | } 365 | --------------------------------------------------------------------------------