Overwrite GOT Entry from Buffer Overlapping

Problem description

First of all, the CTF is from pwnable.kr (problem name: passcode). The problem description is as follows:
“Mommy told me to make a passcode based login system. My initial C code was compiled without any error! Well, there was some compiler warning, but who cares about that?”
There is ssh to the problem server where you can find:

We have 3 files including the flag file which can only be read by the user or group root. The other text file is a source code “passcode.c” which we can view. There is one executable that is compiled from the source code provided. This executable has read accessibility to the flag. However, the source code looks following:

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

void login() {
  int passcode1;
  int passcode2;

  printf("enter passcode1 : ");
  scanf("%d", passcode1);
  fflush(stdin);

  // ha! mommy told me that 32bit is vulnerable to bruteforcing 🙂
  printf("enter passcode2 : ");
  scanf("%d", passcode2);

  printf("checking...\n");
  if (passcode1 == 338150 && passcode2 == 13371337) {
    printf("Login OK!\n");
    system("/bin/cat flag");
  } else {
    printf("Login Failed!\n");
    exit(0);
  }
}

void welcome() {
  char name[100];
  printf("enter you name : ");
  scanf("%100s", name);
  printf("Welcome %s!\n", name);
}

int main() {
  printf("Toddler's Secure Login System 1.0 beta.\n");

  welcome();
  login();

  // something after login...
  printf("Now I can safely trust you that you have credential :)\n");
  return 0;
}

If we take a quick look, the first unusual fact we may see is how it use scanf to user input for passcode1 and passcode2 in login(). We usually send an address to libc to write down the user input, instead, here we send an arbitrary value to let the user write down. This indicates if an attacker can overwrite either passcode1 or passcode2 with an address, they can be able to overwrite any address to that address. This sounds like we have an opportunity to jump from one address to another address and then another to reach an attacker targeted region. The jump has to just after the scanf is in use. Our final target will be: execute the system call inside the executable.

Details about the executable

There are multiple reason why we should first look into the executable details. First, we have to know that what security measurement is available in the binary. Then, as we have limited access to remote server, we would like to regenerate our executable from the source code with exact architecture they have used. So, let’s do readelf in remote server.

readelf -h passcode in remote machine

It is ELF32, indicates builds with -m32. There is also Type Exec, indicates builds with -no-pie. So, I build the passcode.c in my local machine with

gcc -g -m32 -no-pie passcode.c -o passcode
readelf -h passcode in local machine

It is almost looks like same. Okay, now we have exact same executable. We have a buffer in welcome(), so there could be canary. Let’s make sure disassembling welcome() in remote server.

gdb ./passcode => disassemble welcome

Look into instruction address (0x08048612). This executable has an active canary. So, buffer overflow is not possible. However, from source code, we can also observe that number of acceptable bytes for user input is also verified in scanf(“%100s”, name), i.e. only 100 characters are acceptable.

So, where to focus?

We should focus on login() local variables passcode1 and passcode2. As we have mentioned before they can be overwritten with attacker provide address to jump into arbitrary location. But, before that, we have to overflow either of them with a legitimate address. The interesting part is that although buffer overflow is not possible for welcome local variable name[100], there could be overlap memory between welcome() and login() local variables.

welcome() local buffer name[100] starts at [ebp – 0x70]
login() local integer variable passcode1 is at [ebp – 0x10]
welcome() and login() is called consecutively from main()

The above three snippet indicates that there should be an overlap between welcome.name[100] and login.passcode1. Why? We know when a function is called, the system will allocate memory for its local variables (like for welcome() and for its only local variable name[100]). When the function call will return, this allocated memory will be rolled-back, but the data will be persistent. In next call (call to login()) will allocate memory similarly, which will be overlapped with some memory with welcome.name[100]. Question is how much? Theoretically, it will be after 0x70 – 0x10 bytes, which is 0x60 and in decimal 96. So, the data written to 97th character to 100th character in welcome.name[100] will be available in login.passcode1. Want to prove it? Let’s do it following way.

1. ragg2 generate a 100 character input string
2. strace shows the segmentation fault address (the blocked address here)
optional: echo 0x68414167 | xxd -r -p will show in ascii
3. wopO shows the offset of the invalid address from the generated input string

Okay, but now what?

The next to scanf(“%d”, passcode1) is fflush(stdin). This is definitely a jump we can use, what we need is to change one of the target from this jump. This particular jump has a direct address. What about where it is targeted? This will jump into .plt table for fflush().

the direct jump from login() to fflush()
an indirect jump from .got for fflush() at arbitrary target will be decided by memory address 0x804a010.

So, this is (0x804a010) is the memory where an attacker can write down his desired target. To do that, let’s give scanf(“%d”, passcode1) the expected memory address for passcode1 by writing welcome.name[97-100] with the indirect memory address. We can do by:

python -c "print 'A'*96+'\x10\xa0\x04\x08'" | ./passcode

Now, when the fflush() will call, it will look into 0x804a010 for next jump location. So, we should provide that at the same time appending to the input string. It is now time to decide where we finally like to jump.

choose any instruction address inside the successful conditional check and before system() is called

I choose 0x08048651 instruction address. So, my attack looks like:

python -c "print 'A'*96+'\x10\xa0\x04\x08'+str(0x08048651)" | ./passcode

if we do debug, we will see, we first call fflush() that jump into .got for fflush, then directly jump back into 0x08048651 because of the indirect jump from 0x804a010 memory access.

So, what we basically achieve?

Because the .got table target is decided by loader, the compiler keep a placeholder for it to decide the original target later at runtime. We simply write our intended target address on that placeholder that simulate the attacker by fetching the control flow for call fflush(). Once more, we always do not need buffer overflow to overwrite memory from one variable to another variable, it could be overlap naturally.

Happy hacking

CREDIT