Pwnzer0tt1

zer0pts 2023: Aush (pwn, easy)

· Domingo Dirutigliano

zer0pts 2023: Aush (pwn, easy) Domingo Dirutigliano

Aush (pwn, easy)

I will give you the shell, but after you authenticated yourself.

I spent a lot of time on this challenge because I typically focus on static analysis and don’t execute the program until I understand how it works. Initially, I solved it without fully understanding the exploit, but after further analysis, I finally grasped its mechanism and wrote this write-up.

Overview

The challenge provides us with the binary (also running on a remote server) and its C source code.

The program initially reads some bytes from /dev/urandom, which serve as the username and password. We need to input the correct values to obtain a shell.

One noteworthy aspect is that the binary uses cowsay to print messages (through execve).

#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LEN_USER 0x10
#define LEN_PASS 0x20

int setup(char *passbuf, size_t passlen, char *userbuf, size_t userlen) {
  int ret, fd;

  // TODO: change it to password/username file
  if ((fd = open("/dev/urandom", O_RDONLY)) == -1)
    return 1;
  ret  = read(fd, passbuf, passlen) != passlen;
  ret |= read(fd, userbuf, userlen) != userlen;
  close(fd);
  return ret;
}

int main(int argc, char **argv, char **envp) {
  char *args[3];
  char inpuser[LEN_USER+1] = { 0 };
  char inppass[LEN_PASS+1] = { 0 };
  char username[LEN_USER] = { 0 };
  char password[LEN_PASS] = { 0 };

  if (system("/usr/games/cowsay Welcome to AUSH: AUthenticated SHell!") != 0) {
    write(STDOUT_FILENO, "cowsay not found\n", 17);
    return 1;
  }

  /* Load password and username file */
  if (setup(password, LEN_PASS, username, LEN_USER))
    return 1;

  /* Check username */
  write(STDOUT_FILENO, "Username: ", 10);
  if (read(STDIN_FILENO, inpuser, 0x200) <= 0)
    return 1;

  if (memcmp(username, inpuser, LEN_USER) != 0) {
    args[0] = "/usr/games/cowsay";
    args[1] = "Invalid username";
    args[2] = NULL;
    execve(args[0], args, envp);
  }

  /* Check password */
  write(STDOUT_FILENO, "Password: ", 10);
  if (read(STDIN_FILENO, inppass, 0x200) <= 0)
    return 1;

  if (memcmp(password, inppass, LEN_PASS) != 0) {
    args[0] = "/usr/games/cowsay";
    args[1] = "Invalid password";
    args[2] = NULL;
    execve(args[0], args, envp);
  }

  /* Grant access */
  args[0] = "/bin/sh";
  args[1] = NULL;
  execve(args[0], args, envp);
  return 0;
}

My first doubts

There is a buffer overflow on the user input, but:

  • The binary has PIE enabled, and there’s no way to leak addresses.
  • The binary has NX enabled, and there’s no way to execute shellcode.
  • The variable holding the random username is located below the user input buffer, which means we can’t overwrite it using the buffer overflow.

So, The challenge is impossible?

However, it’s labeled as an easy challenge. So, after overthinking it, I tried executing it by inputting a different size of “A” characters.

With 411 “A” characters as the username, it resulted in the correct username… What? How is it possible?

However, when I entered the overwritten password containing “A” characters, it seemed to pass the equality check, but no shell was spawned, and a stack smash was triggered.

The brutal solution

After some debugging, I noticed that the execve for the shell was executed, but the pointer to the environment variables (envp) was partially overwritten, making it an invalid pointer. I tried overwriting it with zeros in the first input, but it didn’t work. However, when I did the same using the second input, it worked! I got a shell!

from pwn import *
p = process("./aush")
p.sendline(b"A"*411)
p.sendline(b"A"0x20+b"\x00"*400)
p.iteractive()

Why it works?

The fact that it worked triggered my obsessive side for static analysis, so I tried to understand why it worked.

Using gdb and inserting the input "A"*411, I observed that the memcmp function was executed and returned 0xffffffff, which is different from 0. Thus, the execve was executed… or was it?

And that’s the point! The execve was executed, but it failed.

Thanks to our overflow, we reached the envp pointer on the stack and overwrote it, making it an invalid address.

While execve was indeed executed, the envp pointer was invalid, causing execve to fail. However, the program continued its execution without being interrupted by the "error" because the cowsay binary was used to close the main program. This is because it calls exit_group at the end, which kills the main program as well.

The first step of the exploit is to overwrite the envp pointer with an invalid value, so execve will be skipped. Additionally, we overwrite the generated password with a password of our choice.

Now we can pass the next check by inserting the chosen password. But there’s a problem: at this point, execve is used to spawn our shell, but the envp pointer is still invalid, causing execve to fail, and no shell is spawned. D:

Now we need to enable again the execution of execve, and one way to do that is by using a null pointer instead of the envp pointer (execve accepts a null pointer as valid for environment).

So, we can insert the correct password chosen by us, but also overwrite the envp pointer with a null pointer. This way, cowsay will be skipped because the password is deemed correct, and execve will be able to spawn the shell normally.


TL; DR

The challenge was easy to solve but tricky to understand.

I understood also that: dynamic analysis >>> static analysis :D