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):
We can immediately see 3 distinct button handlers for each stage in Form1
class, so let's analyze them one by one.
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:
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
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:
At the very start it zeroes out a stack buffer of size 0x4e4b2
and then passes it FUN_004011d0
:
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
:
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:
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:
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))
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:
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:
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:
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:
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 and in disassembly, you're most likely dealing with
std::string
s.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 inptr
field. Andcapacity
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:
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
:
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.
Now, FUN_00405210
creates a named pipe with given name, accepts connection to it and starts another thread with FUN_004032c0
as entry point.
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.
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.
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.
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))
$ file level3_payload.dll
level3_payload.dll: PE32 executable (DLL) (console) Intel 80386, for MS Windows
So we have another native dll to analyze.
The entry point looks pretty common, the only custom thing here is FUN_10003270
.
It basically does hooking and unhooking functions. Let's follow references and take a look at where those values are initialized:
So they're CryptStringToBinaryA
, GetCursorPos
and Sleep
. Let's rename them accordingly:
Now, myCryptStringToBinaryA
basically just calls the original CryptStringToBinaryA
and initializes variable g_counter
at 0x1002ae6c
to 4.
Next, myGetCursorPos
is interesing:
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.
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!
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
.
Note that it replaces the value of each byte only when its value shifted by
y
bits matches thex
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