TISC 2022 Challenge 6 Walkthrough — Pwnlindrome

Lim YS
CSIT tech blog
Published in
11 min readOct 26, 2022

--

A big congratulations to those who solved challenge 6 and moved on to attempting/solving challenge 7! I hope this challenge has been fun and fulfilling to the challengers.

TISC 2022’s Challenge 6 focused on 2 domains, namely Reverse Engineering (RE) and Pwn. This challenge was created with the idea of focusing on traditional overflow vulnerabilities spanned across 3 different levels. But … it seems like one of the challengers managed to solve it without going through level 1 and 3 :(.

A quick shoutout to Gerrard who managed to solve this challenge creatively! Check out his impressive writeup on how he completed the challenge using only level 2!

Since the way he solved the challenge deviated from how I had intended it to be solved, I shall share the intended solution in this writeup.

Upon executing the binary, you will be presented with a welcome message that has a subtle hint “FLOW THROUGH THE LEVELS!” which hints to the challenger that overFLOW vulnerabilities are coming. Alongside the welcome message, a menu is also provided as shown in Figure 1.

Figure 1: Pwnlindrome’s Welcome Message

This walkthrough will be split into 3 sections with each section covering 1 level of the binary. The following is the objective of each level before we delve into the details:

  1. Level 1 → To find a suitable pseudorandom seed that will cause an overflow in the heap, writing to a flag that will unlock level 3. This flag has some interesting requirements to be fulfilled!
  2. Level 2 → To defeat ASLR by leaking a global variable’s memory address after understanding the custom heap allocation. The leaked address is then used to compute the address of the secret function.
  3. Level 3 → To bypass an improper length check on the message string that will lead to a stack buffer overflow. This overflow will cause a function pointer to be overwritten with the challenger’s input. The function pointer overwritten will then be executed as part of the code flow and the challenge ends there!

Level 1

If you attempted to access level 3, you will be presented a message “Seems like you haven’t cleared level 1 …” as shown in Figure 2. Therefore, we will talk about the intended solution of level 1 in this section.

Figure 2: First Attempt at Level 3

At the start of level 1, you will be asked for a seed as seen in Figure 2. After some reversing (with the help of IDA Pro’s decompiler as seen in Figure 3), you will find that the seed will be used by the function, srand, to generate pseudorandom numbers.

Note: Pseudorandom numbers are not truly random numbers as they are deterministic if you know the seed value.

Figure 3: Decompiler Output of Level 1 (sub_2FC6)

The pseudorandom numbers generated are then used in some arithmetic operations. After which, the output produced by the arithmetic operations is used to determine where to allocate the challenger’s input. These arithmetic operations are actually not crucial and I will share the reason later in this section.

The next step is to find the buffer that the challenger’s input is allocated into and how it allows the challenger to access level 3. After some reversing, the challenger should find the memory allocation function as shown in Figure 4 below.

Figure 4: Decompiler Output of Memory Initilization (sub_2ED9)

It appears that there is another buffer allocated directly after the buffer used in level 1 and by cross referencing this other buffer, we can see that it is a “flag” that grants the challenger access to level 3 as seen in Figure 5.

Figure 5: Semi Decompiler Output of Level 3 (sub_3BB2)

With the current findings, we know that level 1 requires the following two things:

  1. An overflow from the user’s input to the flag that unlocks level 3
  2. The reversing of sub_2F4C to understand the checks on the flag that unlocks level 3

For the overflow, I suspect most challengers would attempt to reverse the arithmetic operations in Figure 3 and write a script to bruteforce various seeds to see which seed(s) can overflow till it overwrites the flag. However, one working seed is actually hidden in the strings of the binary as a SHA1 hash.

Figure 6: Strings output from IDA Pro
Figure 7: SHA1 Decrypt of the last hash in Figure 6. Source: https://md5decrypt.net/en/Sha1/#answer

With this seed, the challenger will be able to overwrite the flag that unlocks level 3 but there are some checks in place and the challenger will have to reverse sub_2F4C to understand these checks.

Figure 8: Decompiler Output of Checks on Flag (sub_2F4C)

The first check on the flag is that the value has to be greater than 10 as shown in Figure 8. There are 2 other checks performed on the flag and these checks are carried out by sub_2429 (Figure 9) and sub_2466 (Figure 10).

Figure 9: Decompiler Output of sub_2429
Figure 10: Decompiler Output of sub_2466

The first check in sub_2429 checks in a creative manner whether an even value has been provided. And, the second check in sub_2466 return numbers within the fibonacci series to sub_2F4C for it to check whether the flag is part of the series.

Therefore, the conclusion for level 1 will be that an overflow will occur at the 16th allocation given the seed 1338. And the value to be allocated in the 16th allocation has to fulfil all the following conditions:

  1. Greater than 10
  2. Even number
  3. Part of the fibonacci series

Level 2

After fulfilling all the conditions in level 1, the challenger will then be able to access level 3. As seen in Figure 11 below, level 3 is asking for a length to a message and subsequently, it will ask for a string to be stored as the message.

Figure 11: Gaining Level 3 Access

Challengers may reverse engineer level 3 at this point but they will realise they need a memory leak which will only be available through level 2. Therefore, let’s bring our focus to level 2 in this section. Level 2 has a menu of its own and it is as shown in Figure 12 below. Additionally, Figure 13 below shows the decompiled output of this menu.

Figure 12: Level 2’s Menu
Figure 13: Decompiler Output of Level 2 (sub_39EF)

In the intended solution, the useful options to achieve a memory leak to defeat ASLR is option 1 (sub_34C4), 2 (sub_36C0) and 4 (sub_390F). Let’s start with reversing option 1, sub_34C4. The decompiled code of sub_34C4 is as shown in Figure 14 below.

Figure 14: Decompiler Output of Add Node function (sub_34C4)

The first input requested from the user after choosing option 1 will be the length of the node’s buffer. This length is first checked to be within the range (0, 4095] and if the length is appropriate, it will be passed to the function, sub_312B. Instead of using the decompiled or assembly code of this function to aid the explanation, I will use diagram(s) as it will be easier to understand.

The binary reserved a contiguous chunk of memory for level 2’s usage and in sub_312B, the reserved chunk of memory is broken into 5 different buckets for allocation of the node’s buffer. Based on the length of the node’s buffer, this function allocates the buffer into a bucket. If the bucket is full, the function will reject the subsequent allocations to the bucket until deletion occurs.

The function initializes the metadata of each bucket during the first allocation to the bucket. After the initialization and computation of where to allocate the node’s buffer, sub_312B will return the offset to be used in the reserved memory to sub_34C4. sub_34C4 will then initialize the node’s buffer alongside initializing the other properties of the node. These nodes are then chained in a circular linked list but this property is not crucial in the memory leak. Instead, it is placed here to throw challengers off track and leading them to think that they can leak something out from the root node.

With the knowledge of what sub_312B and sub_34C4 do, the next step is to understand the layout of the reserved memory. Figure 15 below explains the various components of the reserved memory.

Figure 15: Level 2’s Heap Memory Layout

As mentioned, the bucket where the node’s buffer ends up in depends on the length of its buffer. The following are the buckets available alongside the length range for the node’s buffer to land in that bucket:

  1. Smallest Node Buffer → 0 < Length ≤ 15
  2. Small Node Buffer → 15 < Length ≤ 63
  3. Medium Node Buffer → 63 < Length ≤ 255
  4. Large Node Buffer → 255 < Length ≤ 1023
  5. Largest Node Buffer → 1023 < Length ≤ 4095

The count variable that comes before each bucket is a global variable.

There are several global variables within the reserved memory. Leaking any one of these variables will provide the challenger with a memory address that can be used to compute the binary’s base address, defeating ASLR in the process. The idea is to overflow the buffer from a smaller allocation to reach the metadata of a larger allocation. Following the overflow, call option 4 (read buffer) to obtain the memory leak. However, the add node option does not provide the ability to do so as the checks in place are comprehensive enough to prevent this from happening. Therefore, this is where option 2 (modify node) comes into the picture.

Figure 16: Decompiler Output of Modify Node function (sub_36C0)

As you can see in Figure 16, the check in Modify Node function only checks whether the length of the node’s buffer is within the range of (0, 4095]. Therefore, it is trivial to modify a node’s buffer such that it will overflow into the next bucket’s metadata (but not overwriting it). From there, all the challenger has to do is call option 4 (read buffer) and the global variable’s address will be leaked.

There are several global variables’ addresses that you can leak here and each global variables have several ways of leaking it. Therefore, I will leave it up to the readers to find their own way of leaking the global variable’s address!

Upon achieving a memory leak, the next step will be to compute the base address of the binary and the offset to the secret function that will print out the flag. The next section will focus on level 3 which provides a way to execute the secret function given the address to it.

Level 3

We have seen the binary output of level 3 in Figure 11 earlier. As seen in Figure 11, the binary first requests for a message length from the user and subsequently requests for the buffer to be stored in a variable. Figure 17 below shows the decompiled output of level 3.

Figure 17: Full Decompiler Output of Level 3 (sub_3BB2)

As seen in Figure 17, there is a function pointer call at line 59 of the decompiled code. Therefore, the goal is to overwrite this function pointer to point to the secret function (address computed based on the memory leak in level 2). As seen across line 34–37 of the decompiled code, this function pointer is allocated on the stack along with a string. This string is used to store the message, an input from the user, in line 48–49. Therefore, if we are able to overflow this message string, we will be able to overwrite the function pointer to point to the secret function and get the flag.

Before crafting the string, the binary requests for the length of the string in the function, sub_3B4C, and Figure 18 below shows the decompiled code of this function.

Figure 18: Decompiler Output of Message Length Check (sub_3B4C)

Based on the decompiled code of sub_3B4C and sub_3BB2, there are two errors here that make up a bad check for the length of the message. The first issue is that sub_3B4C only ensures that the length provided by the user is ≤ 40. Additionally, the variable type used for the length here is integer which is 4 bytes long. The second issue is that in sub_3BB2, the variable used to store the return results from sub_3B4C is of variable type short which is 2 bytes long.

With that knowledge, the challenger should realise that they are to provide a negative value to the check bypassing the ≤ 40 check by sub_3B4C. This negative value will subsequently be treated as a short variable in sub_3BB2. Due to the variable type mismatch, some negative values will become huge positive values that allow the challenger to overflow the message string and overwrite the function pointer.

At this point, the challenger should have already successfully overwrote the function pointer and led to the binary crashing. The reason I said “binary crashing” instead of “obtaining the flag” is that there is one last function (sub_3ACD) that the challenger has to reverse. This function is called with the message string provided as an input in line 51 of Figure 17. Figure 19 below shows the decompiled code of sub_3ACD.

Figure 19: Decompiler Output of sub_3ACD

This function, sub_3ACD, performs a simple encoding on the message string with the flag from level 1 being the key. Once the challenger understands this encoding, he/she should be able to write a script to bypass it and successfully obtain the flag from the server!

Summary

I have covered the intended solution in the above 3 sections, delving into the details of each level of the binary. The following is the summary of the various sections.

Level 1 → Use value 1338 as the seed to generate the pseudorandom numbers and at the 16th allocation (the overflow to the flag), provide a number that fulfils the following conditions:

  1. Greater than 10
  2. Even number
  3. Part of the fibonacci series

Level 2 → Overcome ASLR by causing an overflow after understanding the custom heap allocation to read out-of-bounds. The way to do it is as follow

  1. Allocate some smaller node
  2. Allocate some larger node (to initialize the global variables into the heap memory)
  3. Modify the smaller node’s buffer such that it overflows into the larger node’s metadata, count, which is a global variable
  4. Compute the address of the secret function within the binary using the address of the leaked global variable

Level 3 → Provide a negative value that will be treated as a positive value due to the variable type mismatch. This will overflow the string into overwriting a function pointer that will be executed with the secret function’s address and the challenge is complete!

I hope everyone had a fulfilling time attempting or solving TISC 2022 Challenge 6! Once again, congratulations to all the winners.

--

--

Recommended from Medium

Lists

See more recommendations