Skip to content

Session 03: Software Security / Memory Safety

Assignment(s)

Install QEMU and access the Protostar exercises

Perform this exercise:

  • https://exploit.education/protostar/stack-zero/
  • Try until you get this message: “you have changed the ‘modified’ variable”
  • Explain what happened.
  • How is it that providing an input in the line that reads “gets(buffer)” can change the value of the variable “modified”?
  • What does “Segmentation fault” mean?

Perform this exercise:

  • https://exploit.education/protostar/stack-one/
  • Try until you get this message: “you have correctly got the variable to the right value”
  • Explain what happened.
  • What does “little endian” mean?
  • Why didn’t you get “Segmentation fault”?

Perform this exercise:

  • https://exploit.education/protostar/stack-three/
  • How can you input hex values that don’t correspond to keyboard letters?
  • Try until you get this message “code flow successfully changed”
  • Explain what happened.

Setting up The Environment

QEMU: Short for “quick emulator,” QEMU is an open-source software that acts as a machine emulator and visualizer, or described in the lecture as a “machine inside a machine.” It allows users to run a complete operating system designed for a specific computer architecture on a different host system.

I began by installing QEMU on my CLI:

brew install qemu

Then, accessing this download page, I downloaded Protostar (exploit-exercises-protostar-2.iso).

From my mac terminal, I ran the virtual machine with QEMU:

qemu-system-x86_64 -boot d -cdrom ~/Downloads/exploit-exercises-protostar-2.iso -m 512

When prompted, I logged into the system with the provided credentials on the Protostar guide (username = user, password = user). I then opened the /opt/protostar/bin directory to view all of the challenges:

ls /opt/protostar/bin

Stack 0

From the stack 0 instruction page: This level introduces the concept that memory can be accessed outside of its allocated region, how the stack variables are laid out, and that modifying outside of the allocated memory can modify program execution.

The source code provided:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];  // allocated buffer size

  modified = 0;
  gets(buffer);  // unsafe input function

  if(modified != 0) {
      printf("you have changed the 'modified' variable\n");
  } else {
      printf("Try again?\n");
  }
}

Based on the context given for this exercise, it seems that my objective is to overflow the “buffer” variable by introducing a value that exceeds the 64-byte storage. To do that, I ran a simple one-line python code, python -c "print('A'*65)" | /opt/protostar/bin/stack0 that inserted a 65-character arbitrary string into buffer.

Stack 0 Analysis / Defining Segfault

What happened? The first two lines of this code – volatile int modified and char buffer[64] – establish two variables, modified and buffer. Modified is a variable with type qualifier volatile that communicates to the compiler that the variable’s value can change at any point without any action from the surrounding code; it also forces the compiler to always read and write the variable directly to physical memory, intead of keeping it in a CPU register. Meanwhile, buffer is another variable that allocates 64 bytes of storage and serves as a temporary storage space. These two variables occupy adjacent positions in the “stack,” meaning that any extra characters that buffer does not have the necessary space for will spill over, or “overflow,” into modified, changing it from its 0 value to a non-zero value.

How is it that providing an input in the line that reads “gets(buffer)” can change the value of the variable “modified”? The gets() function is the problematic vulnerability in this code: as explained by Dr. Okhravi in the lecture, gets() will read a full line of input text but does not know where to stop; its lack of bounds-checking results in buffer overflow, in which the function stores whatever is input in some available memory slot, which so happens to disrupt the initially defined value of modified. For this reason, gets() was officially removed from the language in C11.

What does “Segmentation fault” mean? Segmentation fault, otherwise referred to as segfault, is a runtime error in which a specific program attempts to access a memory location it isn’t supposed to access, serving as a protective mechanism against overflow attempts, for instance. Segmentation faults differentiate writable regions of memory from executable regions. Interestingly, when I tried to run a slightly modified python line that extended the character string from 65 A’s (one byte above buffer’s dedicated storage) to 128 A’s, I got this message:

When the main function is finished, the CPU naturally does its ret instruction, attempting to load the saved return address into the instruction pointer; however, in this case, the 128 A string has spilled over into the saved return address, effectively corrupting it. Segfault occurs because this change forces the CPU to jump to an invalid, protected, or non-executable memory segment.

low addresses  (writing starts here)
  ┌──────────────────────────────┐
  │ buffer[64]                    │ <-- 64 A's
  ├──────────────────────────────┤
  │ modified        (4 bytes)     │  
  ├──────────────────────────────┤
  │ saved EBP       (4 bytes)     │
  ├──────────────────────────────┤
  │ saved return address (4 bytes)│  <-- bytes spill all the way over here, causing segfault
  └──────────────────────────────┘
high addresses

Stack 1

From the stack 1 instruction page: This level looks at the concept of modifying variables to specific values in the program, and how the variables are laid out in memory.

The source code provided:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  if(argc == 1) {
      errx(1, "please specify an argument\n");
  }

  modified = 0;
  strcpy(buffer, argv[1]);

  if(modified == 0x61626364) {
      printf("you have correctly got the variable to the right value\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }
}

With this exercise, I wasn’t sure where to start, so I began by inputting this source code into Claude and asking it to debunk each line. Here’s what I learned: looking at int main(), the only major difference is that the program will return an error if no actual arguments are passed (note that it specifies argc == 1 instead of argc == 0 because the program itself is an argument too!). Secondly, in the line strcpy(buffer, argv[1]), the strcpy() function copies the string from argv[1] into buffer until it hits a null terminator – it is kind of similar to gets() in the sense that it does not care for the length. In order to succeed, I must fulfill the condition modified == 0x61626364; any 8-digit hexadecimal will not suffice. After searching up a hexadecimal to ASCII translation chart, I learned that 0x61626364 can be converted to the string abcd (in which 0x61 = ‘a’, 0x62 = ‘b’, 0x63 = ‘c’, and 0x64 = ‘d’).

I think I have a better perspective now about what this assignment is asking me to do. ( •̀ - • ) Following my first instinct, I ran

python -c "print('A'*64 + 'abcd')" | /opt/protostar/bin/stack1
which would theoretically insert a 64-character-long string into buffer while changing modified’s value to the expected value. However, when I ran this, I got the error:

I wasn’t sure why my string was not being registered, so I entered the command in a different form:

/opt/protostar/bin/stack1 $(python -c "print('A'*64 + 'abcd')")

and then got this message:

Shucks..but this is actually a lot closer to the desired result! The byte-order I ended up getting was 0x64636261, which appears to be the reverse of 0x61626364–what I need. Naturally, I tried rearranging the ASCII:

/opt/protostar/bin/stack1 $(python -c "print('A'*64 + 'dcba')")

and got the correct output

Stack 1 Analysis / Defining Little Endian

Explain what happened. Refer to the code analysis above. Using the same logic as the stack 0 exercise, I initiated a buffer overflow–only this time, I ensured that the part of the string that would spill over into modified matched the condition of the source code. I converted 0x61626364 into ASCII through this site and attached it to the end of an arbitrary 64-byte string that would fill buffer.

What does “little endian” mean? The instruction site specifies that Protostar is “little endian,” but what exactly does that mean? Dr. Okhravi’s lecture notes gave me some semblance of a definition by explaining how “x86 (the foundational instruction set architecture that dictates how software communicates with computer processors), is little endian.” He gives the example of writing the address 0xFFBF1234 into memory, emphasizing that you supply the bytes in a counterintuitively reversed order: 0x34 0x12 0xBF 0xFF. Searching up a formal definition for endianess clarified this case. In little endian, the most significant bytes are stored last, while the least significant bytes are stored first.

Why didn’t you get “Segmentation fault”? The null terminator at the end of the ASCII string prevents segfault. By stopping at modified, I do not remotely touch the saved return address. Had I similarly extended my string, I would’ve encountered the error. To visualize:

low addresses  (writing starts here)
  ┌──────────────────────────────┐
  │ buffer[64]                    │
  ├──────────────────────────────┤
  │ modified        (4 bytes)     │  <-- your bytes stop here
  ├──────────────────────────────┤
  │ saved EBP       (4 bytes)     │
  ├──────────────────────────────┤
  │ saved return address (4 bytes)│  <-- big no-no
  └──────────────────────────────┘
high addresses

Stack 3

From the stack 3 instruction page: Stack3 looks at environment variables, and how they can be set, and overwriting function pointers stored on the stack (as a prelude to overwriting the saved EIP)

The source code provided:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  volatile int (*fp)();
  char buffer[64];

  fp = 0;

  gets(buffer);

  if(fp) {
      printf("calling function pointer, jumping to 0x%08x\n", fp);
      fp();
  }
}
Putting this code in Claude again, I learned that fp is a new kind of variable–specifically, the notation (*fp)() designates it as a function pointer, a variable that holds the address of a function that returns an int. The later lines of the code, which contain the condition if(fp) and the function call fp(), communicate that, if the value of fp is non-zero, the code will jump to whatever address fp holds and start executing there. This gave me significantly more clarify, as I now knew that I needed to instigate an overflow that changes the value of fp to the function address of win().

The hint this time suggested using gdb and objdump to figure out where in the memory the win() function is located–the first crucial step to essentially “re-routing” the function pointer. Unfortunately, I didn’t really know what either of those options meant, so I did some quick research:

GDB, short for GNU Debugger is a CLI debugging tool for C / C++ / etc. that allows users to trace through code line-by-line and determine where crashes occur. For the purpose of this exercise, I will be using GDB as a mere analysis tool to pinpoint the memory location of a function in the source code.

To launch gdb, I passed the compiled executable to it and entered in the terminal

gdb /opt/protostar/bin/stack3

Then, to figure out where the win() function is stored, I used the print function

print win
which output

Great! Now I know that win() is located: 0x8048424.

Applying my knowledge from the stack 0 exercise, I knew that I needed to first input some arbitrary 64-character string to fill in the buffer variable’s 64-byte storage. Then, echoing the stack 1 approach, I would have to add an extension to the string that matched this address while accounting for its little endian property. The last thing I needed to figure out was how to represent 0x8048424 in the string. After some brief Googling and lecture-notes revisiting, it seems that I can make use of the \x prefix notation, which tells python not to literally print the following digits–but rather to treat it as a hexadecimal escape sequence.

In my terminal, I ran

/opt/protostar/bin/stack3 $(python -c "print('A'*64 + '\x24\x84\x04\x08')")
For some reason, QEMU was acting up again and wasn’t responding to this command…I tried multiple times and got no response. Out of curiosity, I went back to the layout I had used for the stack 0 exercise:

python -c "print('A'*64 + '\x24\x84\x04\x08')" | /opt/protostar/bin/stack3

And it worked! ദ്ദി(˵ •̀ ᴗ - ˵ ) ✧

Stack 3 Analysis / Representing Hex Values

How can you input hex values that don’t correspond to keyboard letters? Unlike the previous exercise, the hex values here do not correspond to keyboard letters. To work around this predicament and find a way to contain them in the input string, I used the \x notation. Super handy!

Explain what happened. Refer to the code analysis above. I first used gbd to determine the address of the win() function. I overflowed into the function pointer variable fp, changing its stored value from 0 to the address of win(), specially notated. Because of fp’s now non-zero value, the code will run fp() and thereby redirect the code to execute win(). To visualize:

low addresses  (writing starts here)
  ┌──────────────────────────────┐
  │ buffer[64]                    │
  ├──────────────────────────────┤
  │ fp              (4 bytes)     │  <-- win() address stored here --> points to win()
  ├──────────────────────────────┤
  │ saved EBP       (4 bytes)     │
  ├──────────────────────────────┤
  │ saved return address (4 bytes)│ 
  └──────────────────────────────┘
high addresses