LabyREnth Capture the Flag (CTF): Windows Track 1-6 Solutions

Richard Wartell


Category: Events

Welcome back to our blog series where we reveal the solutions to LabyREnth, the Unit 42 Capture the Flag (CTF) challenge. We’ll be revealing the solutions to one challenge track per week. Next up, the Windows track challenges 1 through 6, followed by 7 through 9 next week. 

Windows 1 Challenge: Deez bugs are bad bugs.

Challenge Created By: Richard Wartell @wartortell

We are given a PE file that we open in DetectItEasy and see that it is packed with UPX.

ctf_windows_1

We can try and unpack it with upx –d, but it couldn’t be that easy.

It looks like we will have to unpack this ourselves before we can solve it.  UPX uses the pushad instruction at the beginning to push the registers on to the stack so that it can retrieve them after unpacking and jumping to the original entry point.  We can script IDA’s debugger to set a hardware read breakpoint at the location of the pushed registers on the stack to get us close to the OEP.

After we hit our breakpoint, we can remove the breakpoint and run until the tail jump that gets us to the original entry point.

ctf_windows_2

popa Instruction Followed by the Tail Jump

We can take the jump to the unpacked code and then use Scylla with our new found OEP to dump the process.

ctf_windows_3

We can open our unpacked executable in Binary Ninja and can see there is a path that prints the good boy message and one that prints the bad boy message.  There is a function called right before the branch that checks the key and determines what path we will take.

ctf_windows_4

Main Showing the Good Boy and Bad Boy paths

If we look at the function we renamed to check_key, we can see that it moves bytes on to the stack and then checks to see if the input is 16 bytes long.

ctf_windows_5

The program then enters a series of anti-debugging checks that will cause the function to return 0 (FALSE) if they are triggered.  Before each check, there is also a string encoding operation performed against our string.

ctf_windows_6

The first anti-debugging check is a call to CheckRemoteDebuggerPresent, which checks to see if the process is being debugged.

ctf_windows_7

The second anti-debugging check is a call to FindWindowW checking for a Window named OLLYDBG, which is a popular debugger used by analysts.

ctf_windows_8

The third anti-debugging check is a call to IsDebuggerPresent, which checks to see if the process is being debugged.

ctf_windows_9

The fourth and final anti-debugging check uses the assembly instruction rdtsc twice as a timing check to see if the process is executing slowly and probably being debugged.

ctf_windows_10

If we pass all the anti-debugging checks, we end up getting the final string operation, which checks the result of all the operations against an offset in the initial buffer of bytes. If they are not equal, the function returns 0 (FALSE). But if they are equal, the result is added, which is used as the XOR key in the final operation.

ctf_windows_11

We can copy off the initial buffer and rewrite the operations in python, so that we can obtain the key.

When we run the script we get the key.

python solve.py

PAN{C0nf1agul4ti0ns_0n_4_J08_W3LL_D0N3!}

Windows 2 Challenge: What does the goat say?

Challenge Created By: Richard Wartell @wartortell

ctf_windows_12

We can open it in dnSpy to decompile and debug.  We can see the key_click function looks interesting because it is tracking a state if keys are pressed in a certain order.

ctf_windows_13

The keys are numbered from left to right starting at 0 for the white keys and the same for the black keys.  If we press the keys in the correct order do_a_thing() is called.

ctf_windows_14

This function plays a funny David Bowie video while the key scrolls in ascii art behind.

ctf_windows_15

ctf_windows_16

PAN{B4BY_Y3LL5_5O_LOUD!}

Windows 3 Challenge: Gotta keep your Squirtle happy

Challenge Created By: Tyler Halfpop @0xtyh

We are given an executable for the Squirtle challenge.  When we run the binary we see some ascii art of Squirtle and a check for a password.  If we get the password wrong, we sadly find out that we just killed a Squirtle and the program exits.

ctf_windows_17

Dead Squirtle from an Incorrect Password

We can open the binary in Binary Ninja and take a look at the main function. If we look at the first branch instruction, there is a function call right before at 401070 that checks the password. We can see that it is just a string compare with the string “incorrect”.

ctf_windows_18

Password Check Function

If we type the password correctly we happily find out that we didn’t kill a Squirtle and we get some more output. We have to pass some anti-debugging and anti-vm checks and then we are told that the answer is written in an answer.jpg file.

ctf_windows_19

Correct Password Output

There is an answer.jpg file written after we ran the program, but it is corrupted so we need to figure out how to make the program write it correctly to disk. We can see at the end of the main function there is a loop with a multi-byte XOR key.

ctf_windows_20

XOR Loop Writing the answer.jpg file

We can assume that if we pass each step we will get the correct key that will output the correct image. At each stage there is a check and then some fake rand() == rand() checks with some funny messages to obfuscate the code.  Thankfully there are also helpful hints at each stage if we get stuck or are unsure of the correct path.

ctf_windows_21

Sleep/GetTickCount Check along with fake rand checks

The first check is to see if there is a common debugger window class found.

The second check is to look at the Process Environment Block at offset fs: [30h+2] to see if the process is being debugged.

The third check uses the Windows API GetTickCount() to make sure the system hasn’t been freshly booted.

The fourth check used Sleep along with GetTickCount() and wanted you to bypass the sleep call.

The fifth check just used the Windows API IsDebuggerPresent to find out if the process is being debugged. The sixth check similarly called the Windows API CheckRemoteDebuggerPresent to find out if the process was being debugged.

The seventh stage checked to see if there are greater than 2 CPUs.

The eighth stage checked to see if there were more than 1024 GB of RAM.

The final check looked to see if the CPU hypervisor bit was set.

We can step through the program in a binary and make sure the correct path is taken. Then we will get the correct image.

ctf_windows_22

Graph Trace of the Correct Path

We could also get the correct key at each stage and grab the buffer of the XOR’d image and decrypt it with python.

ctf_windows_23

Correct Image

We can finally decode the binary and obtain the key (Sorry :)).

PAN{Th3_$quirtL3_$qu@d_w@z_bLuffiNg}

Windows 4 Challenge: 99 bottles of beer on the wall, 99 bottles of beer. Take one down and pass it around, 98 bottles of beer on the wall.  Nah, you need to just pass the jugs of beer around.

Challenge Created By: Jacob Soo @_jsoo_

For this particular challenge, participants were given an x64 binary asking for a valid serial number.

If the serial number is wrong, they should see the image below.

ctf_windows_24This seems like those traditional crackmes. Let’s try to find the function that is checking users’ input. In order to find it, we should always look for suspicious strings if applicable.

Using Hopper, we found the following strings as indicated in the image below.

ctf_windows_25

So let’s pick one of the “strings” and see where it was referenced.

ctf_windows_26

Ok, let’s try and see whether we can find any GetDlgItemText/GetDlgItemTextW API calls. Ok, it seems like we got one at 140001464.

ctf_windows_27

If we step debug it and follow the flow, we will come across a string length check at 1400014b3. Now we know that the input string must be 32 characters in length.

Stepping through again, at 140001500 we will encounter a check to ensure that the input characters must be 1, 2 or 3.  This makes sure that the serial for this challenge is only comprised of 1, 2 and 3.

If we were to analyze the application at 140001750, we can see that there is an array with an initial capacity of [0, 13, 7].  However the maximum capacity of the “jugs” is [19, 13, 7] and the expected end state is having the array be [10, 10, 0].  You basically have three “jugs” with a size of 7, 13 and 19.  The 7 and 13 size “jugs” are filled and the container with size 19 is empty. What you need is 10 in two of the containers (13 and 19).

Let’s re-write it in pseudo codes.  Let’s assume that 3 “jugs” are under the array, M and the 3 jugs are a,b and c.

At first it may seem to be very confusing what is actually going on here. But let’s take a closer look.

This is the limit of the jugs.

Now in the serial, if we were to start with 31, it simply means to fill up jug C to jug A.
So the aim of this is to move around the “beer” so jug A == 10 and jug B == 10.

We would have realized that this is the classic “Liquid Pouring Puzzle” that some, if not most of us, have seen while we were in school.

You can write your tool based on the findings. But if we were to do it with pen and paper. We should get something like the one below.

0-13-7
7-13-0
19-1-0
12-1-7
12-8-0
5-8-7
5-13-2
18-0-2
18-2-0
11-2-7
11-9-0
4-9-7
4-13-3
17-0-3
17-3-0
10-3-7
10-10-0

Rewriting this to serial code: 31211332133221321332133221321332

Using that as the serial, we will solve it and get this message back.

ctf_windows_28

PAN{C0ngr47ulaT1onsbuddyy0Uv3solvedthere4l_prObL3M}

Windows 5 Challenge: Pick your favorite decimal code.

Challenge Created By: Jacob Soo @_jsoo_

Upon running RGB.exe, we’re presented with three sliders, presumably corresponding to the RBG colors, and once you’ve set their values you can check them. This indicates that we’ll need to figure out the correct three values to access the key.

ctf_windows_29

Wrong values

A quick look at the PE file with Exeinfo shows that it’s a .NET program, which can be unpacked with de4dot.

ctf_windows_30

Checking binary type

Running de4dot against the executable creates a new file, “RGB-cleaned.exe” that we can then decompile with dnSpy to look at the underlying source code.

ctf_windows_31

Deobfuscating binary

When looking at the source code, we come to the challenge that we’ll need to solve.

ctf_windows_32

Algorithm

Simply put, three conditions need to be met to get the MessageBox we want to display. At this point, I started poking around to see if I can just modify the code so it always prints the answer, but when you start diving into the functions being called, you can see there is a bit more going on and requires the actual numbers.

ctf_windows_33

XORing numbers against array of numbers

So I decided to tackle the math aspect instead.

The three conditions that need to be met are that one equation result must equal another equation result and one of the specific values needs to be over 60. I opt to brute force it by iterating through every possible combination of numbers, knowing that each slider will be in a range of 1-255, with one being in the range of 60-255. This gives us roughly 12.5 million possibilities, 255*255*(255-60), which shouldn’t take long at all.

After a few minutes of thinking through the logic, I use the below script to find the value.

Within a few seconds, we have the answer and can validate it within RGB.exe to get the key.

ctf_windows_34

83 168 203

PAN{l4byr1n7h_s4yz_x0r1s_4m4z1ng}

Windows 6 Challenge: Discover the key in the shellC0DE to rescue the Princess!

Created By: Josh Grunzweig @jgrunzweig

Opening up this Windows executable quickly reveals that we’re working with some shellcode. If this wasn’t apparent, the clue provided gave some hints as to what you’d be dealing with.

Discover the key in the sh>E11C0DE to rescue the Princess!
@jgrunzweig

To make things easier for challengers, I went ahead and compiled the shellcode into a working executable, versus simply giving you the raw shellcode bytes. Once opened we see that there are no imports and only seven functions.

ctf_windows_35

Figure 1 Functions in shellcode and import table

Without even debugging this shellcode we can quickly scan the seven functions provided to see if anything jumps out. Sure enough, we quickly identify a function that is almost certainly RC4 at 0x40106C. The two loops iterating 256 times gives us a big hint. Working through this function, we can confirm it is in fact RC4.

ctf_windows_36

Figure 2 RC4 function

We also identify a very small function which starts by loading fs:0x30, which should get a reverser’s attention fairly quickly. For those unaware, fs:0x30 points to the Process Environment Block (PEB), which holds a wealth of information. This function in question is specifically looking at the PEB’s LoaderData offset, which holds information about the loaded modules in the process. We then get the third loaded module, which is kernel32.dll, and grab this DLL’s base address (offset 0x10). This function is essentially grabbing the base address of kernel32.dll, which is most likely going to be used to load further functions.

ctf_windows_37

Figure 3 Function getting kernel32 base address

We continue to identify yet another function that appears to be hashing data, as evident by the ROR13 call.

ctf_windows_38

Figure 4 Possibly hashing function

At this point, let’s start stepping through our shellcode in a debugger. We quickly see multiple calls to our function that got kernel32’s base address, followed by another function that takes this base address and a DWORD as arguments. Looking through this function we see it walking through all of kernel32’s exported functions, hashing the name, and comparing it against the provided DWORD. This is a simple shellcode trick that will allow attackers to obfuscate what functions are being loaded by the malware when viewed statically. There are a few ways we can approach this. We can debug the code and rename as we encounter them. Alternatively, we can simply search for the hashes on Google. Since the ROR13 technique is so common, there are many places online that have documented these hashes, like this one.

After getting over this minor hurdle we can start to see what the code is doing to understand what it’s looking for. Looking at the code in detail, we can see that it’s building a buffer of 54 bytes and attempting to decode it against a key that is generated using RC4. In the event the key starts with ‘PAN{‘, it will display it in a messagebox dialog window.

The key is generated using a number of variables that are pulled from the machine it is running on. The first four bytes of the key are a static value of ‘b00!’. Following this, the code looks for the following data:

  • Current month plus 0x2D
  • Current day plus 0x5E
  • Current hour plus 0x42
  • The operating system major version plus 0x3C
  • The operating system minor version plus 0x3F
  • The isDebugged flag, which is pulled from the PEB, plus 0x69
  • The language version plus 0x5E

These values together give us a key that is eleven bytes long. With only that information, it would be very difficult to brute force. However, since we know how each byte in the key is generated, we can limit our key space for the brute force and hopefully determine what the malware is looking for.

Knowing that there are only 12 months in a year, we can assume the first generated byte is in the range between 1 and 12. Similarly, there are a maximum of 31 days in a month, giving our second byte a range of 1 to 31. We continue this pattern on the rest of the bytes in the RC4 key. Most people looked to have the most trouble limiting the key space on the operating system versions, and the language version. Fortunately, there are very few legimate operating system (OS) versions overall. The major OS version will have a value of either 5, 6, or 10. The minor OS version will have a value of 0, 1, 2, or 3.

For the language version, there is a check early on in the execution flow where the result of GetUserDefaultUILanguage has its primary language identifier verified to be 0x0, or LANG_NEUTRAL. Knowing this, we can limit the possibilities of to values of 0x0, 0x04, 0x08, 0x0c, 0x10, or 0x14.

Using all of this information, we can generate a brute-forcing script such as the following.

After running the script for roughly 3 minutes, we are presented with the following:

PAN{th0se_puPP3ts_creeped_m3_out_and_I_h4d_NIGHTMARES}

Leave a comment below to share your thoughts about these challenges. Be sure to also check out how other threat researchers solved these challenges:

Windows 1:

Windows 2:

Windows 3:

Windows 4:

Windows 5:

Windows 6:

4 Reader Comments

  1. so Scylla needs OEP to parse IAT or how does that work? can you explain in a bit more details that part please? Thanks!

  2. can you also comment the idc script please? how you found OEP……thanks!

  3. Tyler Halfpop

    Hi A,

    Scylla is dumping the process and repairing it’s IAT. You want your dumped processes entrypoint to be at the start of the unpacked code and to have any of the additional imports resolved. Another popular tool that works on WinXP for dumping processes and repairing the IAT is ImpRec. Here is another example dumping UPX with ImpRec. https://practicalmalwareanalysis.com/2012/02/27/unpacking-with-windbg/

    OllyDbg has some other methods as well for finding the tail jump or you could find with UPX by just scrolling down and looking for the jmp. Check out chapter 18 of Practical Malware Analysis for a more detailed description of unpacking. I commented the IDA script as requested below to hopefully describe the process I was using better. The unpacker needs to restore the original registers. It stores them on the stack using the pushad instruction, so that it can grab them later with a popad instruction. We can take advantage of this generic behavior by setting a hardware read breakpoint on the stack memory where they are stored to catch the unpacker near the original entry point when it retrieves them.

    # Import IDA libraries
    import idc
    import idaapi
    import idautils

    # Add a Breakpoint at the current location of the cursor when you put the mouse on the pushad instruction
    idc.AddBpt(ScreenEA())

    # Start the process in the win32 debugger
    idc.LoadDebugger(“win32”, 0)
    idc.StartDebugger(“”, “”, “”)
    idc.ResumeProcess()
    # Catch the debugger event when the breakpoint is hit
    idc.GetDebuggerEvent(WFNE_SUSP, -1);

    # Get the current value of the top of the stack and minus 1
    address = idc.GetRegValue(‘ESP’) – 1

    # Add a hardware read write breakpoint on the stack where the registers are pushed
    idc.AddBptEx(address, 1, BPT_RDWR)

    # Resume the process and wait for the hardware breakpoint to hit near the OEP
    idc.ResumeProcess()

  4. awesome, thanks!

Got something to say?

Get updates: Unit 42

Sign up to receive the latest news, cyber threat intelligence and research from Unit42


SUBSCRIBE TO RSS