Skip to content

Instantly share code, notes, and snippets.

@pieceofsummer
Last active November 12, 2021 12:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pieceofsummer/044ef87de1e86f770987ea73efa9b726 to your computer and use it in GitHub Desktop.
Save pieceofsummer/044ef87de1e86f770987ea73efa9b726 to your computer and use it in GitHub Desktop.
Solving the Malwarebytes CrackMe #3

Solving the Malwarebytes CrackMe #3

img1.png

Preliminiary analysis

First of all, let's identify what we're dealing with:

$ file MBCrackme.exe 
MBCrackme.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

So the binary is a .NET executable, and we can load it into dnSpy (could be also ILSpy or other .NET decomplier, but your mileage may vary):

img2.png

We can immediately see 3 distinct button handlers for each stage in Form1 class, so let's analyze them one by one.

Stage 1

First handler looks like this:

private void button1_Click(object sender, EventArgs e)
{
	if (this.textBox1.Text.Length == 0)
	{
		MessageBox.Show("Enter the password!");
		return;
	}
	bool flag = false;
	string text = this.textBox1.Text;
	byte[] array = Form1.decode(Resources.mb_logo_star, text);
	if (array.Length > Form1.validSize_1)
	{
		Array.Resize<byte>(ref array, Form1.validSize_1);
	}
	if (Crc32Algorithm.Compute(array) == Form1.validCrc32_1)
	{
		flag = true;
		try
		{
			if (Form1.g_serverProcess == null || Form1.g_serverProcess.HasExited)
			{
				File.WriteAllBytes(this.g_serverPath, array);
				flag = this.runProcess(this.g_serverPath);
			}
		}
		catch (Exception ex)
		{
			MessageBox.Show(ex.Message);
			return;
		}
	}
	if (flag)
	{
		this.button1.Enabled = false;
		this.textBox1.Enabled = false;
		this.button1.BackColor = Color.OldLace;
		this.button2.Enabled = true;
		this.textBox2.Enabled = true;
		this.button2.BackColor = SystemColors.ActiveCaption;
		MessageBox.Show("Password correct!");
		return;
	}
	MessageBox.Show("Nope!");
}

It passes resource image mb_logo_star and password we enter to the decode function, then validates result size and CRC32, and then saves and executes it.

public static byte[] decode(Bitmap bm, string password_str)
{
	byte[] bytes = Encoding.ASCII.GetBytes(password_str);
	byte[] array = new byte[bm.Width * bm.Height];
	int num = 0;
	for (int i = 0; i < bm.Width; i++)
	{
		for (int j = 0; j < bm.Height; j++)
		{
			Color pixel = bm.GetPixel(i, j);
			int num2 = Form1.keep_bits((int)pixel.R, 3);
			int num3 = Form1.keep_bits((int)pixel.G, 3) << 3;
			int num4 = Form1.keep_bits((int)pixel.B, 2) << 6;
			byte b = (byte)(num2 | num3 | num4);
			if (bytes.Length != 0)
			{
				b ^= bytes[num % bytes.Length];
			}
			array[num] = b;
			num++;
		}
	}
	return array;
}

As one can see, decode function extracts 3:3:2 least significant bits for each color channel and then XORs it with next character of the password.

So let's extract the image and write a small Python script to play with it:

img3.png

from PIL import Image

img = Image.open('mb_logo_star', 'r')

width, height = img.size
result = bytearray()

for i in range(width):
	for j in range(height):
		r, g, b = img.getpixel((i, j))
		result.append((r & 7) | ((g & 7) << 3) | ((b & 3) << 6))

print(bytes(result[:100]))

The output will be:

b'(;\xe3y\\leval_o\x91\x9a_a\xd4most_do.e_xor_pe_and_keep_going!easy_level_\x97ne_os\xd7as\xc0V\xa9N\xd6d\x13\xb5N&7\x19\x16\x7f\x11\x1c\x0b8\x19\x04\x08P<\x06\x01\x07\x01\x13\x01\x07\x04'

Since it should be a PE file, there's plenty of zero bytes in the beginning and we can clearly see parts of the XOR key.

A typical PE executable starts with predictable header bytes, so we can XOR them with the result to get password:

known_header = bytes.fromhex('4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00')

tmp = bytearray(result[:len(known_header)])
for i in range(len(tmp)):
	tmp[i] ^= known_header[i]

print(tmp.decode('latin1'))
easy_level_one_almost_done_xor_pe_and_keep_going!easy_level_

We can see the output starts repeating after exclamation mark, so the password ends there. And the final password is easy_level_one_almost_done_xor_pe_and_keep_going!.

Now that we know the password, we can dump the binary:

from zlib import crc32

password = b'easy_level_one_almost_done_xor_pe_and_keep_going!'

tmp = bytearray(result[:241152])
for i in range(len(tmp)):
	tmp[i] ^= password[i % len(password)]

assert crc32(tmp) == 2741486452

with open('level2.exe', 'wb') as f:
	f.write(tmp)
$ file level2.exe
level2.exe: PE32 executable (GUI) Intel 80386, for MS Windows

Stage 2

Now let's take a look at the second button handler:

private void button2_Click(object sender, EventArgs e)
{
	if (this.textBox2.Text.Length == 0)
	{
		MessageBox.Show("Enter the password!");
		return;
	}
	bool flag = false;
	string pipeName = "crackme_pipe";
	string text = this.textBox2.Text;
	byte[] array = null;
	try
	{
		NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", pipeName);
		namedPipeClientStream.Connect(1000);
		StreamWriter streamWriter = new StreamWriter(namedPipeClientStream);
		TextReader textReader = new StreamReader(namedPipeClientStream);
		streamWriter.WriteLine(text);
		streamWriter.Flush();
		string s = textReader.ReadLine();
		array = Encoding.ASCII.GetBytes(s);
		if (Crc32Algorithm.Compute(array) == Form1.validCrc32_2)
		{
			flag = true;
		}
	}
	catch (Exception ex)
	{
		if (this.showExitReasons())
		{
			Application.Exit();
			return;
		}
		MessageBox.Show(ex.Message);
		return;
	}
	if (flag)
	{
		this.button2.Enabled = false;
		this.textBox2.Enabled = false;
		this.button2.BackColor = Color.OldLace;
		this.button3.Enabled = true;
		this.textBox3.Enabled = true;
		this.button3.BackColor = SystemColors.ActiveCaption;
		MessageBox.Show("Level up!");
		LoadNext.Load(Form1.g_serverProcess, array);
		return;
	}
	MessageBox.Show("Nope!");
}

It connects to the named pipe crackme_pipe, writes the password there, reads the output and checks if its CRC32 matches with the predefined value. If CRC32 matches, a next stage is loaded (but we'll get to that later).

So it is quite clear that all password processing is done in stage2.exe. Let's load it in Ghidra and see what's going on there.

It has a pretty standard CRT startup code at the entry, and the main function looks like this:

img4.png

At the very start it zeroes out a stack buffer of size 0x4e4b2 and then passes it FUN_004011d0:

img5.png

It resolves a function pointer by hash (a typical malware trick) and passes all its parameters to it.

Let's see how hashes are computed inside FUN_00401250:

img6.png

Other than some PE header crawling, the actual hash calculation is the highlighted part.

Let's recreate it in Python and make it print hashes for all functions in ntdll.dll and kernel32.dll:

import pefile, glob

def rol(x, n):
	return (x >> (32 - n)) | (x << n) & 0xffffffff

def name_hash(name):
	if isinstance(name, str):
		name = name.encode()
		
	hash = 0xf00df00d
	for c in name:
		hash = rol(hash, 5) ^ c
	return hash

for lib in glob.glob('libs/*.dll'):
	print(lib)
	try:
		pe = pefile.PE(lib)
		
		for fn in pe.DIRECTORY_ENTRY_EXPORT.symbols:
			if not fn.name:
				continue
			print(hex(name_hash(fn.name)), fn.name)
	except:
		continue

If we search the output we can learn that 0x3ac473d1 corresponds to RtlDecompressBuffer. So the function unpacks some data to the given buffer.

Let's take a look at the prototype of RtlDecompressBuffer from MSDN:

NT_RTL_COMPRESS_API NTSTATUS RtlDecompressBuffer(
  [in]  USHORT CompressionFormat,
  [out] PUCHAR UncompressedBuffer,
  [in]  ULONG  UncompressedBufferSize,
  [in]  PUCHAR CompressedBuffer,
  [in]  ULONG  CompressedBufferSize,
  [out] PULONG FinalUncompressedSize
);

After assiging correct types and names to arguments, the function looks like this:

img7.png

Returning back to main, there's another function hash 0xf4dd3dad resolved there. With the same script from before, we can now learn it corresponds to NtAllocateVirtualMemory:

NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
  [in]      HANDLE    ProcessHandle,
  [in, out] PVOID     *BaseAddress,
  [in]      ULONG_PTR ZeroBits,
  [in, out] PSIZE_T   RegionSize,
  [in]      ULONG     AllocationType,
  [in]      ULONG     Protect
);

So now main can be cleaned up to something like this:

img8.png

We can see it decompresses static data of size 0x27259 bytes, then allocates RWX memory for it, copies uncompressed data from stack buffer to that memory and jumps to its start.

We can extract that data with the following script (file offset can be determined from .data section source in Memory Map window, or just by searching first few bytes in hex editor):

import lznt1

with open('level2.exe', 'rb') as f:
	f.seek(0x12000)
	data = f.read(0x27259)
	
data = lznt1.decompress(data)

with open('level2_payload.bin', 'wb') as f:
	f.write(data)

print(len(data))

Analyzing the payload

After running file command on the payload we can tell it is also a PE executable (and then rename it accordingly):

$ file level2_payload.bin 
level2_payload.bin: PE32 executable (GUI) Intel 80386, for MS Windows

But normal executable is not supposed to be run by jumping to its beginning, so some loader trickery must be involved there. It can also be told by looking at file start in hex editor:

img9.png

For now, let's not deep dive into the custom loader shenanigans (we can go back to that later if needed) and just open it as a normal executable. Once again, it has a typical CRT startup code and the main function looks like this:

img10.png

Function FUN_00409276 looks like a C++ operator new (basically malloc) implementation, so it can be renamed accordingly. Similarly, FUN_004092a6 corresponds to operator delete (free).

Proceeding to FUN_00405e10, it looks like this:

img11.png

First it does something with an array of 34 numbers, one item at a time. Let's put it aside for now and continue to simpler functions.

Function FUN_00402520 calls IsDebuggerPresent and CheckRemoteDebuggerPresent and checks if the process is being debugged. Nothing particularly interesting there.

Function FUN_00402560 checks if some predefined memory page is mapped and tries to read value from there:

img12.png

From the referenced string one can deduce it checks for kernel-mode debugger (KDB) present. So it is another anti-debug check.

Now, FUN_00402320 is the most interesting one here. It uses Process32First/Process32Next to enumerate processes in a snapshot.

Unfortunately, correct prototypes for these functions are not present in Ghidra's library, so them (and PROCESSENTRY32 structure they use) should be defined manually according to MSDN reference. IDA Pro users may be a bit more lucky here.

Note on strings: once you see constructs like img13.png and img14.png in disassembly, you're most likely dealing with std::strings.

A typical layout of std::string in executable is:

struct std_string {
   union {
      char fixed[16];
      char *ptr;
   } data;
   size_t size;
   size_t capacity;
}

Small strings are stored in fixed buffer while longer ones are allocated on heap and stored in ptr field. And capacity is used to determine which one should be used and whether it should be deallocated afterwards.

No surprise compilers generate a lot of junk code when dealing with those. Swiftly recognizing those patterns and typical STL std::string-related functions (constructors, assigning etc.) simplifies analysis significantly.

With this knowledge, we can define std_string structure and retype some variables (but note that because of stack variable overlapping it is not always possible). Also, FUN_004013e0 is clearly an std::string constructor so it can be renamed accordingly.

After some analysis, FUN_00402320 fragment looks like this:

img15.png

So it enumerates processes, copies szExeName field to the stack-based std::string, strips extension (FUN_004014d0 is looking for a . backwards and copies everything before that to a new std::string) and again calculates its hash. Then hash value is somehow looked up in the structure filled before from hash array. One could guess the goal is to exit if some hacker tools are running.

Let's write a small script to bruteforce those hashes:

def rol(x, n):
	return (x >> (32 - n)) | (x << n) & 0xffffffff

def name_hash(name, lowercase=True):
	if isinstance(name, str):
		name = name.encode()
		
	hash = 0xbadc0ffe
	for c in name:
		hash = rol(hash, 5) ^ (c | 0x20 if lowercase and 0x41 <= c <= 0x5A else c)
	return hash

hashes = [
    0xc81d63c9, 0x5b2839ac, 0x17dad73f, 0x72c7241c,
    0x58e483ed, 0x82134662, 0x34204667, 0x4cd53a71,
    0x34206499, 0xffdeb191, 0x7ac6410b, 0xea3503aa,
    0xccfa2924, 0x3a09ffbc, 0x38ea0c1b, 0x58e479ec,
    0x1b964e1a, 0x707f9d9a, 0xf5a79701, 0x09f5473b,
    0xba635ac6, 0x0bb18a65, 0x46119fd8, 0xfb7bf6af,
    0x3f75d54b, 0x49110e9f, 0x5d9f9fd8, 0x5dcc9fd8,
    0x8293c33e, 0x5d112314, 0x9d9f8189, 0xc10ae786,
    0x67d8b725, 0x07fe9020
]

with open('/usr/share/wordlists/crackstation.txt', 'rb') as f:
	while line := f.readline():
		line = line.strip()
		if name_hash(line) in hashes:
			print(line.decode('latin1'), hex(name_hash(line)))

The (incomplete) list of hashes (with some false positives) looks like this:

AUTORUNS 0x72c7241c
autoruns 0x72c7241c
BIDDLES 0x8293c33e
Biddles 0x8293c33e
biddles 0x8293c33e
DUMPCAP 0x3a09ffbc
dumpcap 0x3a09ffbc
FIDDLER 0x8293c33e
FILEMON 0x82134662
Fiddler 0x8293c33e
Filemon 0x82134662
fiddler 0x8293c33e
filemon 0x82134662
IDAQ 0xffdeb191
idaq 0xffdeb191
LORDPE 0x707f9d9a
lordpe 0x707f9d9a
OLLYDBG 0xc81d63c9
ollydbg 0xc81d63c9
PIN 0x7fe9020
Pin 0x7fe9020
PROCESSHACKER 0x5b2839ac
PROCMON 0x34204667
pIN 0x7fe9020
pin 0x7fe9020
processhacker 0x5b2839ac
procmon 0x34204667
REGMON 0x4cd53a71
RESOURCEHACKER 0x49110e9f
RIDDLEW 0x8293c33e
regmon 0x4cd53a71
resourcehacker 0x49110e9f
riddlew 0x8293c33e
SYSINSPECTOR 0xf5a79701
sysinspector 0xf5a79701
TCPVIEW 0x17dad73f
tcpview 0x17dad73f
WINDBG 0x46119fd8
WIRESHARK 0xccfa2924
windbg 0x46119fd8
wireshark 0xccfa2924

So it is definitely a list of hacker tools. A bit of googling gets us to this code. From there, we can figure an almost complete list of names. A couple of hashes are still unresolved, but (hopefully) it wouldn't affect the result.

Now to the FUN_004061e0:

img16.png

It first creates a mutex, then calls a couple of functions and waits for completion.

Function FUN_004051e0 is pretty simple. It just starts a thread with FUN_00405210 as entry point and passes it the pointer it was given as argument.

img17.png

Now, FUN_00405210 creates a named pipe with given name, accepts connection to it and starts another thread with FUN_004032c0 as entry point.

img18.png

This one would read data from the pipe, call function from the structure passed as argument and write the output. So going back to FUN_004061e0 as all the magic is happening in FUN_00406290 passed from there.

img19.png

It is rather big, but it basically strips spaces from the beginning and the end of the argument string, checks if the hash of it is in the lookup table from before, and then passes it along with some static ciphertext buffer to FUN_00401200. Then it checks if result is a printable ASCII string.

img20.png

Experienced reverser could immediately tell this is RC4 crypto function.

So let's write another script:

from array import array
from zlib import crc32

def rol(x, n):
	return (x >> (32 - n)) | (x << n) & 0xffffffff

def name_hash(name, lowercase=True):
	if isinstance(name, str):
		name = name.encode()
		
	hash = 0xbadc0ffe
	for c in name:
		hash = rol(hash, 5) ^ (c | 0x20 if lowercase and 0x41 <= c <= 0x5A else c)
	return hash

def rc4(data, key):
	S = list(range(256))
	j = 0
	for i in range(256):
		j = (j + S[i] + key[i % len(key)]) % 256
		S[i], S[j] = S[j], S[i]
		
	out = []
	i = j = 0
	for b in data:
		i = (i + 1) % 256
		j = (j + S[i]) % 256
		S[i], S[j] = S[j], S[i]
		out.append(b ^ S[(S[i] + S[j]) % 256])
		
	return bytes(out)

ciphertext = array('I', [
    0xf158955a, 0xb5626d7c, 0xd68ac6c2, 0x10f6f220,
    0x4cef8fd8, 0x8b4663d6, 0xa2be0d1a, 0x51
]).tobytes().rstrip(b'\0')

hashes = [
    0xc81d63c9, 0x5b2839ac, 0x17dad73f, 0x72c7241c,
    0x58e483ed, 0x82134662, 0x34204667, 0x4cd53a71,
    0x34206499, 0xffdeb191, 0x7ac6410b, 0xea3503aa,
    0xccfa2924, 0x3a09ffbc, 0x38ea0c1b, 0x58e479ec,
    0x1b964e1a, 0x707f9d9a, 0xf5a79701, 0x09f5473b,
    0xba635ac6, 0x0bb18a65, 0x46119fd8, 0xfb7bf6af,
    0x3f75d54b, 0x49110e9f, 0x5d9f9fd8, 0x5dcc9fd8,
    0x8293c33e, 0x5d112314, 0x9d9f8189, 0xc10ae786,
    0x67d8b725, 0x07fe9020
]

names = [
    'ollydbg', 'ProcessHacker', 'tcpview', 'autoruns',
    'autorunsc', 'filemon', 'procmon', 'regmon',
    'procexp', 'idaq', 'idaq64', 'ImmunityDebugger',
    'Wireshark', 'dumpcap', 'HookExplorer', 'ImportREC',
    'PETools', 'LordPE', 'SysInspector', 'proc_analyzer',
    'sysAnalyzer', 'sniff_hit', 'windbg', 'joeboxcontrol',
    'joeboxserver', 'joeboxserver', 'ResourceHacker',
    'x32dbg', 'x64dbg', 'Fiddler', 'httpdebugger', 'pin'
]

for name in names:
	hash = name_hash(name)
	assert hash in hashes, name
	
	key = rc4(ciphertext, name.encode()).rstrip(b'\0')
	print(hex(hash), name, key)
	
	if key.isascii() and crc32(key) == 499670621:
		break

With this we know the password was ProcessHacker and the result is we_are_good_to_go_to_level3!

Note that while it uses case-insensitive compare to check password hash, the key for RC4 is actually case-sensitive!

Without finding the correct repo it may have been tricky to guess the correct one.

Stage 3

Now, back to LoadNext.Load method in .NET disassembly:

public static int Load(Process process1, byte[] password)
{
	try
	{
		Type type = Assembly.Load(LoadNext.DecompressBytes(AES.decryptContent(Convert.FromBase64String(LoadNext.EncArr), password))).GetType("Level3Bin.Class1");
		object obj = Activator.CreateInstance(type);
		Type[] types = new Type[]
		{
			typeof(Process)
		};
		MethodInfo method = type.GetMethod("RunMe", types);
		object[] parameters = new object[]
		{
			process1
		};
		method.Invoke(obj, parameters);
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message);
		return -1;
	}
	return 0;
}
public static byte[] decryptContent(byte[] fileContent, byte[] password)
{
	MemoryStream memoryStream = new MemoryStream();
	byte[] password2 = SHA256.Create().ComputeHash(password);
	byte[] salt = new byte[] { 5, 3, 3, 7, 8, 0, 0, 8 };
	RijndaelManaged rijndaelManaged = new RijndaelManaged();
	rijndaelManaged.KeySize = 256;
	rijndaelManaged.BlockSize = 128;
	Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password2, salt, 1000);
	rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
	rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
	rijndaelManaged.Mode = CipherMode.CBC;
	try
	{
		CryptoStream cryptoStream = new CryptoStream(memoryStream, rijndaelManaged.CreateDecryptor(), CryptoStreamMode.Write);
		cryptoStream.Write(fileContent, 0, fileContent.Length);
		cryptoStream.FlushFinalBlock();
		cryptoStream.Close();
	}
	catch (Exception ex)
	{
		throw ex;
	}
	finally
	{
		((IDisposable)rijndaelManaged).Dispose();
		((IDisposable)memoryStream).Dispose();
	}
	return memoryStream.ToArray();
}
public static byte[] DecompressBytes(byte[] bytes)
{
	MemoryStream memoryStream = new MemoryStream();
	memoryStream.Write(bytes, 0, bytes.Length);
	memoryStream.Position = 0L;
	GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress, true);
	MemoryStream memoryStream2 = new MemoryStream();
	byte[] array = new byte[64];
	for (int i = gzipStream.Read(array, 0, array.Length); i > 0; i = gzipStream.Read(array, 0, array.Length))
	{
		memoryStream2.Write(array, 0, i);
	}
	gzipStream.Close();
	return memoryStream2.ToArray();
}

It uses password to derive AES key and IV, decrypt some fixed base64-encoded string and then decompress GZip-compressed data.

Let's do a script for that:

#!/usr/bin/env python3

import base64, gzip
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Util.Padding import unpad

enc_arr = "6ZRrk8qJ <snip> 4kiM=";

password = b'we_are_good_to_go_to_level3!'
salt = bytes([5, 3, 3, 7, 8, 0, 0, 8])

password = SHA256.new(password).digest()

material = PBKDF2(password, salt, dkLen=48)
key, iv = material[:32], material[32:]

aes = AES.new(key, AES.MODE_CBC, iv)

data = gzip.decompress(unpad(aes.decrypt(base64.b64decode(enc_arr)), 16))

with open('level3.dll', 'wb') as f:
	f.write(data)

After that we have another managed dll to analyze:

public static int RunMe(Process process1)
{
	try
	{
		string tempFileName = Class1.GetTempFileName("dat");
		if (Class1.DropTheDll(tempFileName))
		{
			DllInj.InjectToProcess(process1, tempFileName);
		}
	}
	catch (IOException ex)
	{
		MessageBox.Show(ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Hand);
	}
	return 0;
}

Class1.DropTheDll basically just writes decoded fixed base64 string into a file and injects it into running process of stage2.exe. No need to further analyze the Dllinj class as it just does dll injection.

Again, we can recreate it in Python:

import base64

enc_dll = "TVqQAAMAA <snip> AAAAAA="

with open('level3_payload.dll', 'wb') as f:
	f.write(base64.b64decode(enc_dll))

Injected dll analysis

$ file level3_payload.dll
level3_payload.dll: PE32 executable (DLL) (console) Intel 80386, for MS Windows

So we have another native dll to analyze.

img21.png

The entry point looks pretty common, the only custom thing here is FUN_10003270.

img22.png

It basically does hooking and unhooking functions. Let's follow references and take a look at where those values are initialized:

img23.png

img24.png

So they're CryptStringToBinaryA, GetCursorPos and Sleep. Let's rename them accordingly:

img25.png

Now, myCryptStringToBinaryA basically just calls the original CryptStringToBinaryA and initializes variable g_counter at 0x1002ae6c to 4.

Next, myGetCursorPos is interesing:

img26.png

It fills point's x and y fields with content of static byte arrays at g_counter mod length of the corresponding array.

Then, mySleep just increments the g_counter value.

Now let's get back to the level2_payload.exe as we have FUN_00405a80 yet to analyze.

With some more digging one can figure out it actually uses Microsoft Detours for hooking API functions.

But it is not too important for solving the challenge so was left out of scope of this writeup.

Continue analysis

The handler for the last button looks like this:

private void button3_Click(object sender, EventArgs e)
{
	if (this.textBox3.Text.Length == 0)
	{
		MessageBox.Show("Enter the password!");
		return;
	}
	try
	{
		TcpClient tcpClient = new TcpClient("127.0.0.1", 1337);
		byte[] array = Encoding.ASCII.GetBytes(this.textBox3.Text);
		NetworkStream stream = tcpClient.GetStream();
		stream.Write(array, 0, array.Length);
		array = new byte[256];
		string text = string.Empty;
		int count = stream.Read(array, 0, array.Length);
		text = Encoding.ASCII.GetString(array, 0, count);
		if (text.Length > 10)
		{
			this.label4.Text = text;
			this.button3.BackColor = Color.OldLace;
			this.textBox3.Enabled = false;
			this.button3.Enabled = false;
		}
		MessageBox.Show(text);
	}
	catch (Exception ex)
	{
		if (this.showExitReasons())
		{
			Application.Exit();
		}
		else
		{
			MessageBox.Show(ex.Message);
		}
	}
}

It just connects to localhost on port 1337, writes the password to the socket, reads the output and displays it. So we can assume the output is the flag.

Back to native code then!

img27.png

Aside from writing some debug ouput, it just starts a new thread with FUN_004056f0 as entry point and passes it the argument.

All the ws2_32 functions used in FUN_004056f0 are imported by ordinals, so it requres some work to resolve them. But it basically creates a socket, binds it to the given port and listens for incoming connections. Once the connection is accepted, it reads up to 0x200 bytes from it, passes them as std::string to the provided function and sends the output back. All the magic, again, happens in FUN_00406530.

img28.png

Note that it replaces the value of each byte only when its value shifted by y bits matches the x value.

To produce a determinate result we must assume it should be true for every byte, otherwise the input could be just any.

Now let's write a script to solve it:

from array import array
import base64

ciphertext = array('I', [
    0xa39bb17f, 0x987ab8db, 0x2f6be93e, 0x5a40c4ac,
    0x5f900f42, 0xab9cf15c, 0xf51b7932, 0x06a3ca0c,
    0x4a4a45c4, 0x21591df6, 0xc7f3da41, 0xa3eeefba,
    0x45820d2d, 0x34d33517, 0xd7c3dccb, 0xfa5e5bb3,
    0x69e23f67, 0x5a4102ef
]).tobytes()

def rc4(data, key):
	S = list(range(256))
	j = 0
	for i in range(256):
		j = (j + S[i] + key[i % len(key)]) % 256
		S[i], S[j] = S[j], S[i]
		
	out = []
	i = j = 0
	for b in data:
		i = (i + 1) % 256
		j = (j + S[i]) % 256
		S[i], S[j] = S[j], S[i]
		out.append(b ^ S[(S[i] + S[j]) % 256])
		
	return bytes(out)

Y_LOOKUP = bytes.fromhex('831b8920378b57c6787400c44883db7c48498b48f849ff24749353034a03c048')
X_LOOKUP = bytes.fromhex('95b96359dcb558c66c5f686f6faddc5f6d58da655f58d762699dd791969966659c')

def rol(x, n):
	n &= 7
	return (x >> (8 - n)) | (x << n) & 0xff

def ror(x, n):
	n &= 7
	return (x >> n) | (x << (8 - n)) & 0xff

password = bytearray()

offset = 4
while True:
	y = Y_LOOKUP[offset % len(Y_LOOKUP)] & 7
	x = X_LOOKUP[offset % len(X_LOOKUP)]
	if offset & 1:
		password.append(rol(x, 2 * y))
	else:
		password.append(ror(x, 2 * y))
	offset += 1
	
	flag = rc4(ciphertext, password).rstrip(b'\0')
	if flag.isascii():
		print(base64.b64encode(password).decode())
		print(flag.decode('latin1'))
		break

So the password is c21hbGxfaG9va3NfbWFrZV9hX2JpZ19kaWZmZXJlbmNl (base64-encoded small_hooks_make_a_big_difference).

And the final flag is flag{you_got_this_best_of_luck_in_reversing_and_beware_of_red_herrings}.

FIN

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment